mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-20 16:47:59 +02:00
264 lines
9.7 KiB
JavaScript
264 lines
9.7 KiB
JavaScript
/**
|
|
* Tests for scripts/build-opencode.js
|
|
*/
|
|
|
|
const assert = require("assert")
|
|
const fs = require("fs")
|
|
const os = require("os")
|
|
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")
|
|
}],
|
|
["package.json declares a resolvable OpenCode plugin entry", () => {
|
|
assert.strictEqual(packageJson.main, ".opencode/dist/index.js")
|
|
assert.ok(packageJson.exports, "package.json must declare an exports map")
|
|
assert.deepStrictEqual(packageJson.exports["."], {
|
|
types: "./.opencode/dist/index.d.ts",
|
|
import: "./.opencode/dist/index.js",
|
|
default: "./.opencode/dist/index.js",
|
|
})
|
|
}],
|
|
["installed package resolves and imports its root module by name", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-entry-"))
|
|
try {
|
|
fs.mkdirSync(path.join(tempDir, "node_modules"), { recursive: true })
|
|
fs.symlinkSync(
|
|
repoRoot,
|
|
path.join(tempDir, "node_modules", "ecc-universal"),
|
|
process.platform === "win32" ? "junction" : "dir"
|
|
)
|
|
const probe = `
|
|
const resolved = import.meta.resolve("ecc-universal")
|
|
if (!resolved.endsWith("/.opencode/dist/index.js")) {
|
|
throw new Error("unexpected entry resolution: " + resolved)
|
|
}
|
|
const mod = await import("ecc-universal")
|
|
if (Object.keys(mod).join(",") !== "default" || typeof mod.default !== "function") {
|
|
throw new Error("root module must export exactly the plugin function")
|
|
}
|
|
`
|
|
const probePath = path.join(tempDir, "probe.mjs")
|
|
fs.writeFileSync(probePath, probe)
|
|
const result = spawnSync(process.execPath, [probePath], {
|
|
cwd: tempDir,
|
|
encoding: "utf8",
|
|
})
|
|
assert.strictEqual(result.status, 0, result.stderr)
|
|
} finally {
|
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
}
|
|
}],
|
|
["OpenCode TypeScript sources resolve their relative imports in place", () => {
|
|
const opencodeDir = path.join(repoRoot, ".opencode")
|
|
const sourceFiles = []
|
|
const walk = (dir) => {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const entryPath = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (entry.name !== "node_modules" && entry.name !== "dist") walk(entryPath)
|
|
} else if (entry.name.endsWith(".ts")) {
|
|
sourceFiles.push(entryPath)
|
|
}
|
|
}
|
|
}
|
|
walk(opencodeDir)
|
|
assert.ok(sourceFiles.length > 0, "expected OpenCode TypeScript sources")
|
|
const unresolved = []
|
|
for (const sourceFile of sourceFiles) {
|
|
const source = fs.readFileSync(sourceFile, "utf8")
|
|
for (const match of source.matchAll(/(?:from|import)\s*\(?\s*"(\.[^"]+)"/g)) {
|
|
const target = path.resolve(path.dirname(sourceFile), match[1])
|
|
if (!fs.existsSync(target)) {
|
|
unresolved.push(`${path.relative(repoRoot, sourceFile)} -> ${match[1]}`)
|
|
}
|
|
}
|
|
}
|
|
assert.deepStrictEqual(unresolved, [])
|
|
}],
|
|
["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", () => {
|
|
fs.rmSync(path.dirname(distEntry), { recursive: true, force: true })
|
|
const result = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts=false"], {
|
|
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()
|