Files
ECC/tests/pi/agent-convert.test.js
T

188 lines
6.9 KiB
JavaScript

const AGENT_COUNT = require('fs').readdirSync(require('path').join(__dirname,'../../agents')).filter(f => f.endsWith('.md')).length;
/**
* Tests for the ECC Agent IR converter CLI (scripts/agent-convert.js).
*
* Verifies the CLI contract: `--check`, `--json`, `--out` (write and
* side-effect-free `--dry-run`), and argument/exit-code behavior.
*/
const assert = require("assert")
const fs = require("fs")
const os = require("os")
const path = require("path")
const { execFileSync } = require("child_process")
const CLI = path.join(__dirname, "..", "..", "scripts", "agent-convert.js")
const NODE = process.execPath
function runTest(name, fn) {
try {
fn()
console.log(` ✓ ${name}`)
return true
} catch (error) {
console.log(` ✗ ${name}`)
console.error(` ${error.message}`)
return false
}
}
function runCli(args) {
return execFileSync(NODE, [CLI, ...args], { encoding: "utf8" })
}
function tmpdir() {
return fs.mkdtempSync(path.join(os.tmpdir(), "ecc-agent-convert-"))
}
function main() {
let passed = 0
let failed = 0
const tests = [
["--check exits 0 and reports AGENT_COUNT agents", () => {
const out = runCli(["--check"])
assert.match(out, new RegExp(`OK: ${AGENT_COUNT} agents parsed and validated`))
}],
["--json emits a machine-readable summary", () => {
const out = runCli(["--from", "claude", "--to", "pi", "--json"])
const summary = JSON.parse(out)
assert.strictEqual(summary.schema, "ecc.agent-ir.v1")
assert.strictEqual(summary.from, "claude")
assert.strictEqual(summary.to, "pi")
assert.strictEqual(summary.converted, AGENT_COUNT)
assert.ok(Array.isArray(summary.warningsDetail))
}],
["--out writes AGENT_COUNT files", () => {
const dir = tmpdir()
runCli(["--from", "claude", "--to", "pi", "--out", dir])
const files = fs.readdirSync(dir).filter(f => f.endsWith(".md"))
assert.strictEqual(files.length, AGENT_COUNT, "expected AGENT_COUNT emitted files")
const planner = fs.readFileSync(path.join(dir, "planner.md"), "utf8")
assert.match(planner, /name: planner\n/);
assert.match(planner, /prompt_mode: replace/);
}],
["--out preserves user-owned agents before writing any output", () => {
const dir = tmpdir();
const target = path.join(dir, "planner.md");
fs.writeFileSync(target, "USER_OWNED_AGENT\n");
assert.throws(() => runCli(["--out", dir]), /refusing to overwrite existing agent/);
assert.strictEqual(fs.readFileSync(target, "utf8"), "USER_OWNED_AGENT\n");
assert.deepStrictEqual(fs.readdirSync(dir), ["planner.md"]);
}],
["--out rejects a file created at exclusive-open time without overwriting bytes", () => {
const root = tmpdir();
const dir = path.join(root, "out");
const preload = path.join(root, "race.js");
fs.mkdirSync(dir);
fs.writeFileSync(preload, `
const fs = require('fs');
const open = fs.openSync;
let injected = false;
fs.openSync = function(file, flags, mode) {
if (!injected && flags === 'wx' && String(file).endsWith('.md')) {
injected = true;
const fd = open(file, 'wx', 0o600);
try { fs.writeFileSync(fd, 'RACED_OPERATOR_BYTES'); }
finally { fs.closeSync(fd); }
}
return open(file, flags, mode);
};
`);
try {
assert.throws(() => execFileSync(NODE, ["--require", preload, CLI, "--out", dir], { encoding: "utf8", stdio: "pipe" }), /EEXIST/);
const files = fs.readdirSync(dir);
assert.strictEqual(files.length, 1);
assert.strictEqual(fs.readFileSync(path.join(dir, files[0]), "utf8"), "RACED_OPERATOR_BYTES");
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}],
["--out fails closed when existing output changes after batch preflight", () => {
const root = tmpdir();
const dir = path.join(root, "out");
const preload = path.join(root, "change.js");
runCli(["--out", dir]);
const target = path.join(dir, fs.readdirSync(dir).sort()[0]);
fs.writeFileSync(preload, `
const fs = require('fs');
const mkdir = fs.mkdirSync;
fs.mkdirSync = function(file, options) {
if (String(file) === ${JSON.stringify(dir)}) fs.writeFileSync(${JSON.stringify(target)}, 'CHANGED_AFTER_PREFLIGHT');
return mkdir(file, options);
};
`);
try {
assert.throws(() => execFileSync(NODE, ["--require", preload, CLI, "--out", dir], { encoding: "utf8", stdio: "pipe" }), /refusing to overwrite existing agent/);
assert.strictEqual(fs.readFileSync(target, "utf8"), "CHANGED_AFTER_PREFLIGHT");
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}],
["--out repeats identical output without changing bytes", () => {
const dir = tmpdir();
runCli(["--out", dir]);
const target = path.join(dir, "planner.md");
const before = fs.readFileSync(target, "utf8");
runCli(["--out", dir]);
assert.strictEqual(fs.readFileSync(target, "utf8"), before);
}],
["--out --dry-run writes nothing", () => {
const dir = tmpdir()
const out = runCli(["--from", "claude", "--to", "pi", "--out", dir, "--dry-run"])
assert.match(out, /\[dry-run\] would write/)
assert.strictEqual(fs.readdirSync(dir).length, 0, "dry-run must not create files")
}],
["unsupported conversion exits non-zero", () => {
assert.throws(() => runCli(["--from", "claude", "--to", "zed"]), /unsupported conversion/);
}],
["unknown flag exits non-zero", () => {
assert.throws(() => runCli(["--bogus"]), /unknown argument/)
}],
["--json --out keeps stdout parseable JSON", () => {
const dir = tmpdir()
const out = runCli(["--from", "claude", "--to", "pi", "--out", dir, "--json"])
const summary = JSON.parse(out)
assert.strictEqual(summary.converted, AGENT_COUNT)
assert.strictEqual(summary.schema, "ecc.agent-ir.v1")
}],
["missing value for --out exits non-zero", () => {
assert.throws(() => runCli(["--from", "claude", "--to", "pi", "--out"]), /missing value for --out/)
}],
["--check --json emits JSON", () => {
const out = runCli(["--check", "--json"])
const summary = JSON.parse(out)
assert.strictEqual(summary.ok, true)
assert.strictEqual(summary.checked, AGENT_COUNT)
}],
["--out followed by a flag is rejected (no unintended write)", () => {
const dir = tmpdir()
assert.throws(() => runCli(["--from", "claude", "--to", "pi", "--out", "--dry-run"]), /missing value for --out/)
assert.strictEqual(fs.readdirSync(dir).length, 0, "must not write anything")
}],
]
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()