/** * Tests for scripts/build-opencode.js */ const assert = require("assert") const fs = require("fs") const path = require("path") const { spawnSync } = require("child_process") const { getNpmPackEntry } = require("../lib/npm-pack-output") function runTest(name, fn) { try { fn() console.log(` ✓ ${name}`) return true } catch (error) { console.log(` ✗ ${name}`) console.error(` ${error.message}`) return false } } function main() { console.log("\n=== Testing build-opencode.js ===\n") let passed = 0 let failed = 0 const repoRoot = path.join(__dirname, "..", "..") const packageJson = JSON.parse( fs.readFileSync(path.join(repoRoot, "package.json"), "utf8") ) const buildScript = path.join(repoRoot, "scripts", "build-opencode.js") const distEntry = path.join(repoRoot, ".opencode", "dist", "index.js") const tests = [ ["package.json exposes the OpenCode build and prepack hooks", () => { assert.strictEqual(packageJson.scripts["build:opencode"], "node scripts/build-opencode.js") assert.strictEqual(packageJson.scripts.prepack, "npm run build:opencode") assert.ok(packageJson.files.includes(".opencode/")) }], ["build script generates .opencode/dist", () => { const result = spawnSync("node", [buildScript], { cwd: repoRoot, encoding: "utf8", }) assert.strictEqual(result.status, 0, result.stderr) assert.ok(fs.existsSync(distEntry), ".opencode/dist/index.js should exist after build") }], ["built OpenCode entry exports only the plugin function", () => { const check = ` const assert = require("assert") const { pathToFileURL } = require("url") async function main() { let mod try { mod = await import(pathToFileURL(process.argv[1]).href) } catch (error) { console.error(error) process.exit(1) } assert.deepStrictEqual(Object.keys(mod).sort(), ["default"]) assert.strictEqual(typeof mod.default, "function") let shellCalls = 0 const plugin = await mod.default({ client: { app: { log: () => {} } }, $: async () => { shellCalls += 1 throw new Error("$ must not be called during plugin init") }, directory: process.cwd(), worktree: process.cwd(), }) assert.strictEqual(shellCalls, 0, "$ must not be called during plugin init") assert.ok(plugin && typeof plugin === "object", "default export must return a plugin record") const expectedHooks = [ "file.edited", "tool.execute.after", "tool.execute.before", "session.created", "session.idle", "session.deleted", "file.watcher.updated", "todo.updated", "shell.env", "experimental.session.compacting", "permission.ask", ] for (const hook of expectedHooks) { assert.strictEqual(typeof plugin[hook], "function", "missing hook: " + hook) } assert.ok(plugin.tool && typeof plugin.tool === "object", "plugin record must expose a tool object") assert.deepStrictEqual( Object.keys(plugin.tool).sort(), ["changed-files", "dependency-analyzer"], "plugin.tool must expose exactly the custom tools" ) for (const toolName of ["changed-files", "dependency-analyzer"]) { const toolDefinition = plugin.tool[toolName] assert.ok(toolDefinition && typeof toolDefinition === "object", "missing tool: " + toolName) assert.strictEqual(typeof toolDefinition.description, "string", toolName + " must declare a description") assert.ok(toolDefinition.args && typeof toolDefinition.args === "object", toolName + " must declare args") assert.strictEqual(typeof toolDefinition.execute, "function", toolName + " must declare an execute function") } } main().catch((error) => { console.error(error) process.exit(1) }) ` const result = spawnSync(process.execPath, ["-e", check, distEntry], { cwd: repoRoot, encoding: "utf8", }) assert.strictEqual(result.status, 0, result.stderr) }], ["npm pack includes the compiled OpenCode dist payload", () => { const result = spawnSync("npm", ["pack", "--dry-run", "--json"], { cwd: repoRoot, encoding: "utf8", shell: process.platform === "win32", }) assert.strictEqual(result.status, 0, result.error?.message || result.stderr) const packOutput = JSON.parse(result.stdout) const packEntry = getNpmPackEntry(packOutput, packageJson.name) const packagedPaths = new Set(packEntry?.files?.map((file) => file.path) ?? []) assert.ok( packagedPaths.has(".opencode/dist/index.js"), "npm pack should include .opencode/dist/index.js" ) assert.ok( packagedPaths.has(".opencode/dist/plugins/index.js"), "npm pack should include compiled OpenCode plugin output" ) assert.ok( packagedPaths.has(".opencode/dist/tools/index.js"), "npm pack should include compiled OpenCode tool output" ) assert.ok( packagedPaths.has(".claude-plugin/marketplace.json"), "npm pack should include .claude-plugin/marketplace.json" ) assert.ok( packagedPaths.has(".claude-plugin/plugin.json"), "npm pack should include .claude-plugin/plugin.json" ) assert.ok( packagedPaths.has(".codex-plugin/plugin.json"), "npm pack should include .codex-plugin/plugin.json" ) assert.ok( packagedPaths.has(".agents/plugins/marketplace.json"), "npm pack should include .agents/plugins/marketplace.json" ) assert.ok( packagedPaths.has(".opencode/package.json"), "npm pack should include .opencode/package.json" ) assert.ok( packagedPaths.has(".opencode/package-lock.json"), "npm pack should include .opencode/package-lock.json" ) assert.ok( packagedPaths.has("agent.yaml"), "npm pack should include agent.yaml" ) assert.ok( packagedPaths.has("AGENTS.md"), "npm pack should include AGENTS.md" ) assert.ok( packagedPaths.has("VERSION"), "npm pack should include VERSION" ) }], ] for (const [name, fn] of tests) { if (runTest(name, fn)) { passed += 1 } else { failed += 1 } } console.log(`\nPassed: ${passed}`) console.log(`Failed: ${failed}`) process.exit(failed > 0 ? 1 : 0) } main()