chore: import upstream snapshot with attribution
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 11:55:55 +08:00
commit d48cda4081
3322 changed files with 668744 additions and 0 deletions
+395
View File
@@ -0,0 +1,395 @@
/**
* Tests for scripts/auto-update.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
parseArgs,
deriveRepoRootFromState,
buildInstallApplyArgs,
determineInstallCwd,
runAutoUpdate,
} = require('../../scripts/auto-update');
const {
createInstallState,
} = require('../../scripts/lib/install-state');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function makeRecord({ repoRoot, homeDir, projectRoot, adapter, request, resolution, operations }) {
const targetRoot = adapter.kind === 'project'
? path.join(projectRoot, `.${adapter.target}`)
: path.join(homeDir, '.claude');
const installStatePath = adapter.kind === 'project'
? path.join(targetRoot, 'ecc-install-state.json')
: path.join(targetRoot, 'ecc', 'install-state.json');
const state = createInstallState({
adapter,
targetRoot,
installStatePath,
request,
resolution,
operations,
source: {
repoVersion: '1.10.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
});
return {
adapter,
targetRoot,
installStatePath,
exists: true,
state,
error: null,
repoRoot,
};
}
function ensureFakeRepo(repoRoot) {
fs.mkdirSync(path.join(repoRoot, 'scripts'), { recursive: true });
fs.writeFileSync(
path.join(repoRoot, 'package.json'),
JSON.stringify({ name: 'everything-claude-code', version: '1.10.0' }, null, 2)
);
fs.writeFileSync(path.join(repoRoot, 'scripts', 'install-apply.js'), '#!/usr/bin/env node\n');
}
function runTests() {
console.log('\n=== Testing auto-update.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs reads repo-root, target, dry-run, and json flags', () => {
const parsed = parseArgs([
'node',
'scripts/auto-update.js',
'--target',
'cursor',
'--repo-root',
'/tmp/ecc',
'--dry-run',
'--json',
]);
assert.deepStrictEqual(parsed.targets, ['cursor']);
assert.strictEqual(parsed.repoRoot, '/tmp/ecc');
assert.strictEqual(parsed.dryRun, true);
assert.strictEqual(parsed.json, true);
})) passed += 1; else failed += 1;
if (test('parseArgs rejects unknown arguments', () => {
assert.throws(
() => parseArgs(['node', 'scripts/auto-update.js', '--bogus']),
/Unknown argument: --bogus/
);
})) passed += 1; else failed += 1;
if (test('deriveRepoRootFromState uses sourcePath and sourceRelativePath', () => {
const state = {
operations: [
{
sourcePath: path.join('/tmp', 'ecc', 'scripts', 'setup-package-manager.js'),
sourceRelativePath: path.join('scripts', 'setup-package-manager.js'),
},
],
};
assert.strictEqual(
deriveRepoRootFromState(state),
path.resolve(path.join('/tmp', 'ecc'))
);
})) passed += 1; else failed += 1;
if (test('deriveRepoRootFromState fails when source metadata is unavailable', () => {
assert.throws(
() => deriveRepoRootFromState({ operations: [{ destinationPath: '/tmp/file' }] }),
/Unable to infer ECC repo root/
);
})) passed += 1; else failed += 1;
if (test('buildInstallApplyArgs reconstructs legacy installs', () => {
const record = {
adapter: { target: 'claude', kind: 'home' },
state: {
target: { target: 'claude' },
request: {
profile: null,
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: ['typescript', 'python'],
legacyMode: true,
},
},
};
assert.deepStrictEqual(buildInstallApplyArgs(record), [
'--target', 'claude',
'typescript',
'python',
]);
})) passed += 1; else failed += 1;
if (test('buildInstallApplyArgs reconstructs manifest installs', () => {
const record = {
adapter: { target: 'cursor', kind: 'project' },
state: {
target: { target: 'cursor' },
request: {
profile: 'developer',
modules: ['platform-configs'],
includeComponents: ['component:alpha'],
excludeComponents: ['component:beta'],
legacyLanguages: [],
legacyMode: false,
},
},
};
assert.deepStrictEqual(buildInstallApplyArgs(record), [
'--target', 'cursor',
'--profile', 'developer',
'--modules', 'platform-configs',
'--with', 'component:alpha',
'--without', 'component:beta',
]);
})) passed += 1; else failed += 1;
if (test('determineInstallCwd uses the project root for project installs', () => {
const record = {
adapter: { kind: 'project' },
state: {
target: {
root: path.join('/tmp', 'project', '.cursor'),
},
},
};
assert.strictEqual(determineInstallCwd(record, '/tmp/ecc'), path.join('/tmp', 'project'));
})) passed += 1; else failed += 1;
if (test('runAutoUpdate reports when no install-state files are present', () => {
const result = runAutoUpdate(
{
homeDir: '/tmp/home',
projectRoot: '/tmp/project',
dryRun: true,
},
{
discoverInstalledStates: () => [],
}
);
assert.strictEqual(result.results.length, 0);
assert.strictEqual(result.summary.checkedCount, 0);
assert.strictEqual(result.summary.errorCount, 0);
})) passed += 1; else failed += 1;
if (test('runAutoUpdate rejects mixed inferred repo roots', () => {
const homeDir = createTempDir('auto-update-home-');
const projectRoot = createTempDir('auto-update-project-');
const repoOne = createTempDir('auto-update-repo-');
const repoTwo = createTempDir('auto-update-repo-');
try {
ensureFakeRepo(repoOne);
ensureFakeRepo(repoTwo);
const records = [
makeRecord({
repoRoot: repoOne,
homeDir,
projectRoot,
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
request: {
profile: null,
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: { selectedModules: ['legacy-claude-rules'], skippedModules: [] },
operations: [
{
kind: 'copy-file',
moduleId: 'legacy-claude-rules',
sourcePath: path.join(repoOne, 'rules', 'common', 'coding-style.md'),
sourceRelativePath: path.join('rules', 'common', 'coding-style.md'),
destinationPath: path.join(homeDir, '.claude', 'rules', 'common', 'coding-style.md'),
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
},
],
}),
makeRecord({
repoRoot: repoTwo,
homeDir,
projectRoot,
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
request: {
profile: 'core',
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: { selectedModules: ['rules-core'], skippedModules: [] },
operations: [
{
kind: 'copy-file',
moduleId: 'rules-core',
sourcePath: path.join(repoTwo, '.cursor', 'mcp.json'),
sourceRelativePath: path.join('.cursor', 'mcp.json'),
destinationPath: path.join(projectRoot, '.cursor', 'mcp.json'),
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
}),
];
assert.throws(
() => runAutoUpdate(
{
homeDir,
projectRoot,
dryRun: true,
},
{
discoverInstalledStates: () => records,
}
),
/Multiple ECC repo roots detected/
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(repoOne);
cleanup(repoTwo);
}
})) passed += 1; else failed += 1;
if (test('runAutoUpdate fetches, pulls, and reinstalls using reconstructed args', () => {
const homeDir = createTempDir('auto-update-home-');
const projectRoot = createTempDir('auto-update-project-');
const repoRoot = createTempDir('auto-update-repo-');
try {
ensureFakeRepo(repoRoot);
const records = [
makeRecord({
repoRoot,
homeDir,
projectRoot,
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
request: {
profile: 'developer',
modules: [],
includeComponents: ['component:alpha'],
excludeComponents: ['component:beta'],
legacyLanguages: [],
legacyMode: false,
},
resolution: { selectedModules: ['rules-core'], skippedModules: [] },
operations: [
{
kind: 'copy-file',
moduleId: 'platform-configs',
sourcePath: path.join(repoRoot, '.cursor', 'mcp.json'),
sourceRelativePath: path.join('.cursor', 'mcp.json'),
destinationPath: path.join(projectRoot, '.cursor', 'mcp.json'),
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
}),
];
const commands = [];
const result = runAutoUpdate(
{
homeDir,
projectRoot,
dryRun: false,
},
{
discoverInstalledStates: () => records,
runExternalCommand: (command, args, options) => {
commands.push({ command, args, options });
if (command === process.execPath) {
return {
stdout: JSON.stringify({
dryRun: false,
result: {
installStatePath: path.join(projectRoot, '.cursor', 'ecc-install-state.json'),
},
}),
stderr: '',
};
}
return { stdout: '', stderr: '' };
},
}
);
assert.strictEqual(result.summary.checkedCount, 1);
assert.strictEqual(result.summary.updatedCount, 1);
assert.deepStrictEqual(commands.map(entry => [entry.command, entry.args[0]]), [
['git', 'fetch'],
['git', 'pull'],
[process.execPath, path.join(repoRoot, 'scripts', 'install-apply.js')],
]);
assert.deepStrictEqual(commands[2].args.slice(1), [
'--target', 'cursor',
'--profile', 'developer',
'--with', 'component:alpha',
'--without', 'component:beta',
'--json',
]);
assert.strictEqual(commands[2].options.cwd, projectRoot);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(repoRoot);
}
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+123
View File
@@ -0,0 +1,123 @@
/**
* Tests for scripts/build-opencode.js
*/
const assert = require("assert")
const fs = require("fs")
const path = require("path")
const { spawnSync } = require("child_process")
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")
}],
["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 packagedPaths = new Set(packOutput[0]?.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()
+104
View File
@@ -0,0 +1,104 @@
/**
* Tests for scripts/catalog.js
*/
const assert = require('assert');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'catalog.js');
function run(args = []) {
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing catalog.js ===\n');
let passed = 0;
let failed = 0;
if (test('shows help with no arguments', () => {
const result = run();
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Discover ECC install components and profiles'));
})) passed++; else failed++;
if (test('shows help with an explicit help flag', () => {
const result = run(['--help']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Usage:'));
assert.ok(result.stdout.includes('node scripts/catalog.js show <component-id>'));
})) passed++; else failed++;
if (test('lists install profiles', () => {
const result = run(['profiles']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Install profiles'));
assert.ok(result.stdout.includes('core'));
})) passed++; else failed++;
if (test('filters components by family and emits JSON', () => {
const result = run(['components', '--family', 'language', '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.ok(Array.isArray(parsed.components));
assert.ok(parsed.components.length > 0);
assert.ok(parsed.components.every(component => component.family === 'language'));
assert.ok(parsed.components.some(component => component.id === 'lang:typescript'));
assert.ok(parsed.components.every(component => component.id !== 'framework:nextjs'));
})) passed++; else failed++;
if (test('shows a resolved component payload', () => {
const result = run(['show', 'framework:nextjs', '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.id, 'framework:nextjs');
assert.strictEqual(parsed.family, 'framework');
assert.deepStrictEqual(parsed.moduleIds, ['framework-language']);
assert.ok(Array.isArray(parsed.modules));
assert.strictEqual(parsed.modules[0].id, 'framework-language');
})) passed++; else failed++;
if (test('fails on unknown subcommands', () => {
const result = run(['bogus']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown catalog command'));
})) passed++; else failed++;
if (test('fails on unknown component ids', () => {
const result = run(['show', 'framework:not-real']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown install component'));
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+203
View File
@@ -0,0 +1,203 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'ci', 'check-unicode-safety.js');
function test(name, fn) {
try {
fn();
console.log(`PASS: ${name}`);
return true;
} catch (error) {
console.log(`FAIL: ${name}`);
console.log(` ${error.message}`);
return false;
}
}
function runCheck(root, args = []) {
return spawnSync('node', [scriptPath, ...args], {
env: {
...process.env,
ECC_UNICODE_SCAN_ROOT: root,
},
encoding: 'utf8',
});
}
function makeTempRoot(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
const warningEmoji = String.fromCodePoint(0x26A0, 0xFE0F);
const toolsEmoji = String.fromCodePoint(0x1F6E0, 0xFE0F);
const zeroWidthSpace = String.fromCodePoint(0x200B);
const rocketEmoji = String.fromCodePoint(0x1F680);
let passed = 0;
let failed = 0;
if (
test('fails on invisible unicode and emoji before cleanup', () => {
const root = makeTempRoot('ecc-unicode-check-');
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
fs.mkdirSync(path.join(root, 'scripts'), { recursive: true });
fs.writeFileSync(path.join(root, 'docs', 'guide.md'), `> ${warningEmoji} Important launch note\n`);
fs.writeFileSync(path.join(root, 'scripts', 'sample.js'), `const x = "a${zeroWidthSpace}";\n`);
const result = runCheck(root);
assert.notStrictEqual(result.status, 0, result.stdout + result.stderr);
assert.match(result.stderr, /dangerous-invisible U\+200B/);
assert.match(result.stderr, /emoji U\+26A0/);
})
)
passed++;
else failed++;
if (
test('write mode removes emoji and invisible unicode', () => {
const root = makeTempRoot('ecc-unicode-fix-');
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
fs.writeFileSync(path.join(root, 'docs', 'guide.md'), `> ${warningEmoji} Important launch note\n`);
fs.writeFileSync(path.join(root, 'README.md'), `## ${toolsEmoji} Tools\n`);
fs.writeFileSync(path.join(root, 'note.txt'), `one${zeroWidthSpace}two\n`);
const writeResult = runCheck(root, ['--write']);
assert.strictEqual(writeResult.status, 0, writeResult.stdout + writeResult.stderr);
assert.strictEqual(fs.readFileSync(path.join(root, 'docs', 'guide.md'), 'utf8'), '> WARNING: Important launch note\n');
assert.strictEqual(fs.readFileSync(path.join(root, 'README.md'), 'utf8'), '## Tools\n');
assert.strictEqual(fs.readFileSync(path.join(root, 'note.txt'), 'utf8'), 'onetwo\n');
const cleanResult = runCheck(root);
assert.strictEqual(cleanResult.status, 0, cleanResult.stdout + cleanResult.stderr);
})
)
passed++;
else failed++;
if (
test('write mode does not rewrite executable files', () => {
const root = makeTempRoot('ecc-unicode-code-');
fs.mkdirSync(path.join(root, 'scripts'), { recursive: true });
const scriptFile = path.join(root, 'scripts', 'sample.js');
const original = `const label = "Launch ${rocketEmoji}";\n`;
fs.writeFileSync(scriptFile, original);
const result = runCheck(root, ['--write']);
assert.notStrictEqual(result.status, 0, result.stdout + result.stderr);
assert.match(result.stderr, /scripts[/\\]sample\.js:1:23 emoji U\+1F680/);
assert.strictEqual(fs.readFileSync(scriptFile, 'utf8'), original);
})
)
passed++;
else failed++;
if (
test('plain symbols like copyright remain allowed', () => {
const root = makeTempRoot('ecc-unicode-symbols-');
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
fs.writeFileSync(path.join(root, 'docs', 'legal.md'), 'Copyright © ECC\nTrademark ® ECC\n');
const result = runCheck(root);
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
})
)
passed++;
else failed++;
// Invisible code points newly covered by the denylist. These were missing
// from the previous denylist and silently passed through both detection and
// `--write` mode. Each is a documented LLM-prompt-injection vector
// (Tag block "ASCII smuggling"; the other invisibles are widely cited in
// homograph / Discord / Twitter smuggling references).
const NEWLY_COVERED_RANGES = [
{ codePoint: 0xE0041, label: 'Tag block U+E0041 (TAG LATIN CAPITAL LETTER A)' },
{ codePoint: 0xE007F, label: 'Tag block U+E007F (CANCEL TAG, range end)' },
{ codePoint: 0x180E, label: 'U+180E MONGOLIAN VOWEL SEPARATOR' },
{ codePoint: 0x115F, label: 'U+115F HANGUL CHOSEONG FILLER' },
{ codePoint: 0x1160, label: 'U+1160 HANGUL JUNGSEONG FILLER' },
{ codePoint: 0x2061, label: 'U+2061 FUNCTION APPLICATION' },
{ codePoint: 0x2064, label: 'U+2064 INVISIBLE PLUS (range end)' },
{ codePoint: 0x3164, label: 'U+3164 HANGUL FILLER' },
];
for (const { codePoint, label } of NEWLY_COVERED_RANGES) {
if (
test(`detects ${label}`, () => {
const root = makeTempRoot('ecc-unicode-newly-covered-');
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
const hex = codePoint.toString(16).toUpperCase().padStart(4, '0');
fs.writeFileSync(
path.join(root, 'docs', `probe-${hex}.md`),
`# Probe\n\nBenign${String.fromCodePoint(codePoint)}text\n`
);
const result = runCheck(root);
assert.notStrictEqual(result.status, 0,
`expected exit non-zero on U+${hex}, got ${result.status}: ${result.stderr}`);
assert.match(result.stderr, new RegExp(`dangerous-invisible U\\+${hex}`),
`expected violation message for U+${hex}, got: ${result.stderr}`);
})
)
passed++;
else failed++;
}
if (
test('write mode strips newly-covered invisibles from markdown', () => {
const root = makeTempRoot('ecc-unicode-newly-covered-write-');
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
const tagHidden = [...Array(5)].map((_, i) => String.fromCodePoint(0xE0041 + i)).join('');
const mongolianHidden = String.fromCodePoint(0x180E);
const filePath = path.join(root, 'docs', 'mixed.md');
fs.writeFileSync(filePath, `# Title\n\nBenign${tagHidden}${mongolianHidden}text.\n`);
const writeResult = runCheck(root, ['--write']);
assert.strictEqual(writeResult.status, 0,
`expected --write to succeed, got ${writeResult.status}: ${writeResult.stderr}`);
const sanitized = fs.readFileSync(filePath, 'utf8');
assert.doesNotMatch(sanitized, /[\u{E0000}-\u{E007F}]/u,
'expected tag block characters stripped');
assert.doesNotMatch(sanitized, /\u{180E}/u,
'expected U+180E stripped');
assert.strictEqual(sanitized, '# Title\n\nBenigntext.\n',
'expected only the invisible characters removed, surrounding text preserved');
// Re-run without --write; should now pass cleanly.
const clean = runCheck(root);
assert.strictEqual(clean.status, 0,
`expected post-sanitize re-run to pass, got: ${clean.stderr}`);
})
)
passed++;
else failed++;
if (
test('skips Python virtual environments', () => {
const root = makeTempRoot('ecc-unicode-venv-');
fs.mkdirSync(path.join(root, '.venv', 'lib', 'python3.12', 'site-packages'), { recursive: true });
fs.mkdirSync(path.join(root, 'venv', 'lib', 'python3.12', 'site-packages'), { recursive: true });
fs.writeFileSync(
path.join(root, '.venv', 'lib', 'python3.12', 'site-packages', 'package.py'),
`message = "hello ${rocketEmoji}"\n`
);
fs.writeFileSync(
path.join(root, 'venv', 'lib', 'python3.12', 'site-packages', 'package.py'),
`message = "hello ${rocketEmoji}"\n`
);
const result = runCheck(root);
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
+325
View File
@@ -0,0 +1,325 @@
/**
* Tests for scripts/claw.js
*
* Tests the NanoClaw agent REPL module — storage, context, delegation, meta.
*
* Run with: node tests/scripts/claw.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
getClawDir,
getSessionPath,
listSessions,
loadHistory,
appendTurn,
loadECCContext,
buildPrompt,
askClaude,
isValidSessionName,
handleClear,
getSessionMetrics,
searchSessions,
branchSession,
exportSession,
compactSession
} = require(path.join(__dirname, '..', '..', 'scripts', 'claw.js'));
// Test helper — matches ECC's custom test pattern
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
if (err.stack) { console.log(` Stack: ${err.stack}`); }
return false;
}
}
function makeTmpDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'claw-test-'));
}
function runTests() {
console.log('\n=== Testing claw.js ===\n');
let passed = 0;
let failed = 0;
// ── Storage tests (6) ──────────────────────────────────────────────────
console.log('Storage:');
if (test('getClawDir() returns path ending in .claude/claw', () => {
const dir = getClawDir();
assert.ok(dir.endsWith(path.join('.claude', 'claw')),
`Expected path ending in .claude/claw, got: ${dir}`);
})) passed++; else failed++;
if (test('getSessionPath("foo") returns correct .md path', () => {
const p = getSessionPath('foo');
assert.ok(p.endsWith(path.join('.claude', 'claw', 'foo.md')),
`Expected path ending in .claude/claw/foo.md, got: ${p}`);
})) passed++; else failed++;
if (test('listSessions() returns empty array for empty dir', () => {
const tmpDir = makeTmpDir();
try {
const sessions = listSessions(tmpDir);
assert.deepStrictEqual(sessions, []);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('listSessions() finds .md files and strips extension', () => {
const tmpDir = makeTmpDir();
try {
fs.writeFileSync(path.join(tmpDir, 'alpha.md'), 'test');
fs.writeFileSync(path.join(tmpDir, 'beta.md'), 'test');
fs.writeFileSync(path.join(tmpDir, 'not-a-session.txt'), 'test');
const sessions = listSessions(tmpDir);
assert.ok(sessions.includes('alpha'), 'Should find alpha');
assert.ok(sessions.includes('beta'), 'Should find beta');
assert.strictEqual(sessions.length, 2, 'Should only find .md files');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('loadHistory() returns "" for non-existent file', () => {
const result = loadHistory('/tmp/claw-test-nonexistent-' + Date.now() + '.md');
assert.strictEqual(result, '');
})) passed++; else failed++;
if (test('appendTurn() writes correct markdown format', () => {
const tmpDir = makeTmpDir();
const filePath = path.join(tmpDir, 'test.md');
try {
appendTurn(filePath, 'User', 'Hello world', '2025-01-15T10:00:00.000Z');
const content = fs.readFileSync(filePath, 'utf8');
assert.ok(content.includes('### [2025-01-15T10:00:00.000Z] User'),
'Should include timestamp and role header');
assert.ok(content.includes('Hello world'), 'Should include content');
assert.ok(content.includes('---'), 'Should include separator');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Context tests (3) ─────────────────────────────────────────────────
console.log('\nContext:');
if (test('loadECCContext() returns "" when no skills specified', () => {
const result = loadECCContext('');
assert.strictEqual(result, '');
})) passed++; else failed++;
if (test('loadECCContext() skips missing skill directories gracefully', () => {
const result = loadECCContext('nonexistent-skill-xyz');
assert.strictEqual(result, '');
})) passed++; else failed++;
if (test('loadECCContext() concatenates multiple skill files', () => {
// Use real skills from the ECC repo if they exist
const skillsDir = path.join(process.cwd(), 'skills');
if (!fs.existsSync(skillsDir)) {
console.log(' (skipped — no skills/ directory in CWD)');
return;
}
const available = fs.readdirSync(skillsDir).filter(d => {
const skillFile = path.join(skillsDir, d, 'SKILL.md');
return fs.existsSync(skillFile);
});
if (available.length < 2) {
console.log(' (skipped — need 2+ skills with SKILL.md)');
return;
}
const twoSkills = available.slice(0, 2).join(',');
const result = loadECCContext(twoSkills);
assert.ok(result.length > 0, 'Should return non-empty context');
// Should contain content from both skills
for (const name of available.slice(0, 2)) {
const skillContent = fs.readFileSync(
path.join(skillsDir, name, 'SKILL.md'), 'utf8'
);
// Check that at least part of each skill is present
const firstLine = skillContent.split('\n').find(l => l.trim().length > 10);
if (firstLine) {
assert.ok(result.includes(firstLine.trim()),
`Should include content from skill ${name}`);
}
}
})) passed++; else failed++;
// ── Delegation tests (2) ──────────────────────────────────────────────
console.log('\nDelegation:');
if (test('buildPrompt() constructs correct prompt structure', () => {
const prompt = buildPrompt('system info', 'chat history', 'user question');
assert.ok(prompt.includes('=== SYSTEM CONTEXT ==='), 'Should have system section');
assert.ok(prompt.includes('system info'), 'Should include system prompt');
assert.ok(prompt.includes('=== CONVERSATION HISTORY ==='), 'Should have history section');
assert.ok(prompt.includes('chat history'), 'Should include history');
assert.ok(prompt.includes('=== USER MESSAGE ==='), 'Should have user section');
assert.ok(prompt.includes('user question'), 'Should include user message');
// Sections should be in order
const sysIdx = prompt.indexOf('SYSTEM CONTEXT');
const histIdx = prompt.indexOf('CONVERSATION HISTORY');
const userIdx = prompt.indexOf('USER MESSAGE');
assert.ok(sysIdx < histIdx, 'System should come before history');
assert.ok(histIdx < userIdx, 'History should come before user message');
})) passed++; else failed++;
if (test('askClaude() handles subprocess error gracefully', () => {
// Use a non-existent command to trigger an error
const result = askClaude('sys', 'hist', 'msg');
// Should return an error string, not throw
assert.strictEqual(typeof result, 'string', 'Should return a string');
// If claude is not installed, we get an error message
// If claude IS installed, we get an actual response — both are valid
assert.ok(result.length > 0, 'Should return non-empty result');
})) passed++; else failed++;
// ── REPL/Meta tests (3) ───────────────────────────────────────────────
console.log('\nREPL/Meta:');
if (test('module exports all required functions', () => {
const claw = require(path.join(__dirname, '..', '..', 'scripts', 'claw.js'));
const required = [
'getClawDir', 'getSessionPath', 'listSessions', 'loadHistory',
'appendTurn', 'loadECCContext', 'askClaude', 'main'
];
for (const fn of required) {
assert.strictEqual(typeof claw[fn], 'function',
`Should export function ${fn}`);
}
})) passed++; else failed++;
if (test('/clear truncates session file', () => {
const tmpDir = makeTmpDir();
const filePath = path.join(tmpDir, 'session.md');
try {
fs.writeFileSync(filePath, 'some existing history content');
assert.ok(fs.readFileSync(filePath, 'utf8').length > 0, 'File should have content before clear');
handleClear(filePath);
const after = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(after, '', 'File should be empty after clear');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('isValidSessionName rejects invalid characters', () => {
assert.strictEqual(isValidSessionName('my-project'), true);
assert.strictEqual(isValidSessionName('default'), true);
assert.strictEqual(isValidSessionName('test123'), true);
assert.strictEqual(isValidSessionName('a'), true);
assert.strictEqual(isValidSessionName(''), false);
assert.strictEqual(isValidSessionName('has spaces'), false);
assert.strictEqual(isValidSessionName('has/slash'), false);
assert.strictEqual(isValidSessionName('../traversal'), false);
assert.strictEqual(isValidSessionName('-starts-dash'), false);
assert.strictEqual(isValidSessionName(null), false);
assert.strictEqual(isValidSessionName(undefined), false);
})) passed++; else failed++;
console.log('\nNanoClaw v2:');
if (test('getSessionMetrics returns non-zero token estimate for populated history', () => {
const tmpDir = makeTmpDir();
const filePath = path.join(tmpDir, 'metrics.md');
try {
appendTurn(filePath, 'User', 'Implement auth');
appendTurn(filePath, 'Assistant', 'Working on it');
const metrics = getSessionMetrics(filePath);
assert.strictEqual(metrics.turns, 2);
assert.ok(metrics.tokenEstimate > 0);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('searchSessions finds query in saved session', () => {
const tmpDir = makeTmpDir();
try {
const clawDir = path.join(tmpDir, '.claude', 'claw');
const sessionPath = path.join(clawDir, 'alpha.md');
fs.mkdirSync(clawDir, { recursive: true });
appendTurn(sessionPath, 'User', 'Need oauth migration');
const results = searchSessions('oauth', clawDir);
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].session, 'alpha');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('branchSession copies history into new branch session', () => {
const tmpDir = makeTmpDir();
try {
const clawDir = path.join(tmpDir, '.claude', 'claw');
const source = path.join(clawDir, 'base.md');
fs.mkdirSync(clawDir, { recursive: true });
appendTurn(source, 'User', 'base content');
const result = branchSession(source, 'feature-branch', clawDir);
assert.strictEqual(result.ok, true);
const branched = fs.readFileSync(result.path, 'utf8');
assert.ok(branched.includes('base content'));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('exportSession writes JSON export', () => {
const tmpDir = makeTmpDir();
const filePath = path.join(tmpDir, 'export.md');
const outPath = path.join(tmpDir, 'export.json');
try {
appendTurn(filePath, 'User', 'hello');
appendTurn(filePath, 'Assistant', 'world');
const result = exportSession(filePath, 'json', outPath);
assert.strictEqual(result.ok, true);
const exported = JSON.parse(fs.readFileSync(outPath, 'utf8'));
assert.strictEqual(Array.isArray(exported.turns), true);
assert.strictEqual(exported.turns.length, 2);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('compactSession reduces long histories', () => {
const tmpDir = makeTmpDir();
const filePath = path.join(tmpDir, 'compact.md');
try {
for (let i = 0; i < 30; i++) {
appendTurn(filePath, i % 2 ? 'Assistant' : 'User', `turn-${i}`);
}
const changed = compactSession(filePath, 10);
assert.strictEqual(changed, true);
const content = fs.readFileSync(filePath, 'utf8');
assert.ok(content.includes('NanoClaw Compaction'));
assert.ok(!content.includes('turn-0'));
assert.ok(content.includes('turn-29'));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Summary ───────────────────────────────────────────────────────────
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+744
View File
@@ -0,0 +1,744 @@
/**
* Tests for Codex shell helpers.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const TOML = require('@iarna/toml');
const repoRoot = path.join(__dirname, '..', '..');
const installScript = path.join(repoRoot, 'scripts', 'codex', 'install-global-git-hooks.sh');
const pluginCacheCheckScript = path.join(repoRoot, 'scripts', 'codex', 'check-plugin-cache.js');
const mergeCodexConfigScript = path.join(repoRoot, 'scripts', 'codex', 'merge-codex-config.js');
const mergeMcpConfigScript = path.join(repoRoot, 'scripts', 'codex', 'merge-mcp-config.js');
const syncScript = path.join(repoRoot, 'scripts', 'sync-ecc-to-codex.sh');
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
const packageVersion = packageJson.version;
const deterministicPackageEnv = {
CLAUDE_PACKAGE_MANAGER: 'npm',
CLAUDE_CODE_PACKAGE_MANAGER: 'npm',
};
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function runBash(scriptPath, args = [], env = {}, cwd = repoRoot) {
return spawnSync('bash', [scriptPath, ...args], {
cwd,
env: {
...process.env,
...env,
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
}
function runNode(scriptPath, args = [], env = {}, cwd = repoRoot) {
return spawnSync('node', [scriptPath, ...args], {
cwd,
env: {
...process.env,
...env,
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
}
function makeHermeticCodexEnv(homeDir, codexDir, extraEnv = {}) {
const agentsHome = path.join(homeDir, '.agents');
const hooksDir = path.join(codexDir, 'git-hooks');
return {
HOME: homeDir,
USERPROFILE: homeDir,
XDG_CONFIG_HOME: path.join(homeDir, '.config'),
GIT_CONFIG_GLOBAL: path.join(homeDir, '.gitconfig'),
CODEX_HOME: codexDir,
AGENTS_HOME: agentsHome,
ECC_GLOBAL_HOOKS_DIR: hooksDir,
CLAUDE_PACKAGE_MANAGER: 'npm',
CLAUDE_CODE_PACKAGE_MANAGER: 'npm',
LANG: 'C.UTF-8',
LC_ALL: 'C.UTF-8',
...extraEnv,
};
}
function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
function seedPluginCache(codexDir, manifest, files = []) {
const cacheDir = path.join(codexDir, 'plugins', 'cache', 'ecc', 'ecc', packageVersion);
writeJson(path.join(cacheDir, '.codex-plugin', 'plugin.json'), manifest);
fs.writeFileSync(path.join(cacheDir, 'README.md'), '# cached plugin\n');
for (const [relativePath, content] of files) {
const target = path.join(cacheDir, relativePath);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, content);
}
return cacheDir;
}
const cacheManifestWithLocalRefs = {
name: 'ecc',
version: packageVersion,
skills: './skills/',
mcpServers: './.mcp.json',
interface: {
composerIcon: './assets/ecc-icon.svg',
logo: './assets/hero.png',
},
};
let passed = 0;
let failed = 0;
if (
test('check-plugin-cache fails when the installed cache is missing manifest-referenced files', () => {
const homeDir = createTempDir('codex-plugin-cache-home-');
const codexDir = path.join(homeDir, '.codex');
try {
seedPluginCache(codexDir, cacheManifestWithLocalRefs);
const result = runNode(pluginCacheCheckScript, [], makeHermeticCodexEnv(homeDir, codexDir));
assert.strictEqual(result.status, 1, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Plugin cache:/);
assert.match(result.stdout, /\[FAIL\] skills missing/);
assert.match(result.stdout, /\[FAIL\] mcpServers missing/);
assert.match(result.stdout, /codex plugin list only confirms marketplace registration/);
assert.match(result.stdout, /sync-ecc-to-codex\.sh/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('check-plugin-cache rejects manifest references that escape the cache boundary', () => {
const homeDir = createTempDir('codex-plugin-cache-manifest-traversal-home-');
const codexDir = path.join(homeDir, '.codex');
try {
seedPluginCache(codexDir, {
name: 'ecc',
version: packageVersion,
skills: '../../../../../etc/passwd',
mcpServers: '../../.mcp.json',
});
const result = runNode(pluginCacheCheckScript, [], makeHermeticCodexEnv(homeDir, codexDir));
assert.strictEqual(result.status, 1, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /\[FAIL\] skills escapes cache boundary/);
assert.match(result.stdout, /\[FAIL\] mcpServers escapes cache boundary/);
assert.doesNotMatch(result.stdout, /etc\/passwd/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('check-plugin-cache passes when cached manifest references resolve inside the cache', () => {
const homeDir = createTempDir('codex-plugin-cache-ok-home-');
const codexDir = path.join(homeDir, '.codex');
try {
const cacheDir = seedPluginCache(codexDir, cacheManifestWithLocalRefs, [
['.mcp.json', '{"mcpServers":{}}\n'],
['assets/ecc-icon.svg', '<svg />\n'],
['assets/hero.png', 'png\n'],
]);
fs.mkdirSync(path.join(cacheDir, 'skills'), { recursive: true });
const result = runNode(pluginCacheCheckScript, [], makeHermeticCodexEnv(homeDir, codexDir));
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /\[OK\] skills/);
assert.match(result.stdout, /\[OK\] mcpServers/);
assert.match(result.stdout, /All cached manifest references resolve/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('check-plugin-cache reports a missing installed cache clearly', () => {
const homeDir = createTempDir('codex-plugin-cache-missing-home-');
const codexDir = path.join(homeDir, '.codex');
try {
const result = runNode(pluginCacheCheckScript, [], makeHermeticCodexEnv(homeDir, codexDir));
assert.strictEqual(result.status, 1, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Cached plugin manifest missing/);
assert.match(result.stdout, /codex plugin marketplace add affaan-m\/ECC/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('check-plugin-cache rejects traversal in cache path segments', () => {
const homeDir = createTempDir('codex-plugin-cache-traversal-home-');
const codexDir = path.join(homeDir, '.codex');
try {
const result = runNode(
pluginCacheCheckScript,
['--marketplace', '../outside'],
makeHermeticCodexEnv(homeDir, codexDir)
);
assert.strictEqual(result.status, 1, `${result.stdout}\n${result.stderr}`);
assert.match(result.stderr, /Invalid --marketplace/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('check-plugin-cache names custom missing cache entries in diagnostics', () => {
const homeDir = createTempDir('codex-plugin-cache-custom-home-');
const codexDir = path.join(homeDir, '.codex');
try {
const result = runNode(
pluginCacheCheckScript,
['--marketplace', 'custom-market', '--plugin', 'custom-plugin', '--version', '1.2.3'],
makeHermeticCodexEnv(homeDir, codexDir)
);
assert.strictEqual(result.status, 1, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /No installed cache entries found for custom-market\/custom-plugin/);
assert.match(result.stdout, /Install the requested plugin into the Codex plugin cache/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
// Windows NTFS does not allow double-quote characters in file paths,
// so the quoted-path shell-injection test is only meaningful on Unix.
if (os.platform() === 'win32') {
console.log(' - install-global-git-hooks.sh quoted paths (skipped on Windows)');
} else if (
test('install-global-git-hooks.sh handles quoted hook paths without shell injection', () => {
const homeDir = createTempDir('codex-hooks-home-');
const weirdHooksDir = path.join(homeDir, 'git-hooks "quoted"');
try {
const result = runBash(installScript, [], {
HOME: homeDir,
ECC_GLOBAL_HOOKS_DIR: weirdHooksDir,
});
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
assert.ok(fs.existsSync(path.join(weirdHooksDir, 'pre-commit')));
assert.ok(fs.existsSync(path.join(weirdHooksDir, 'pre-push')));
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('merge-codex-config reports usage, missing files, and TOML parse failures', () => {
const tempDir = createTempDir('codex-merge-errors-');
try {
const noArgs = runNode(mergeCodexConfigScript);
assert.strictEqual(noArgs.status, 1);
assert.match(noArgs.stderr, /Usage: merge-codex-config\.js/);
const missingPath = path.join(tempDir, 'missing-config.toml');
const missing = runNode(mergeCodexConfigScript, [missingPath]);
assert.strictEqual(missing.status, 1);
assert.match(missing.stderr, /Config file not found/);
const invalidPath = path.join(tempDir, 'invalid-config.toml');
fs.writeFileSync(invalidPath, 'approval_policy = [\n');
const invalid = runNode(mergeCodexConfigScript, [invalidPath]);
assert.strictEqual(invalid.status, 1);
assert.match(invalid.stderr, /Failed to parse TOML/);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-codex-config dry-run reports additions without mutating the target', () => {
const tempDir = createTempDir('codex-merge-dry-run-');
const configPath = path.join(tempDir, 'config.toml');
const original = '';
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeCodexConfigScript, [configPath, '--dry-run']);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /\[add-root\]/);
assert.match(result.stdout, /\[add-table\] \[features\]/);
assert.match(result.stdout, /Dry run/);
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-codex-config preserves user root choices while adding missing baseline tables', () => {
const tempDir = createTempDir('codex-merge-add-only-');
const configPath = path.join(tempDir, 'config.toml');
try {
fs.writeFileSync(configPath, 'approval_policy = "never"\n');
const result = runNode(mergeCodexConfigScript, [configPath]);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Done\. Baseline Codex settings merged\./);
const merged = fs.readFileSync(configPath, 'utf8');
const parsed = TOML.parse(merged);
assert.strictEqual(parsed.approval_policy, 'never');
assert.strictEqual(parsed.sandbox_mode, 'workspace-write');
assert.strictEqual(parsed.web_search, 'live');
assert.strictEqual(parsed.features.multi_agent, true);
assert.strictEqual(parsed.profiles.strict.approval_policy, 'on-request');
assert.strictEqual(parsed.profiles.yolo.approval_policy, 'never');
assert.strictEqual(parsed.agents.max_threads, 6);
assert.strictEqual(parsed.agents.explorer.config_file, 'agents/explorer.toml');
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-codex-config no-ops when the Codex baseline is already present', () => {
const tempDir = createTempDir('codex-merge-noop-');
const configPath = path.join(tempDir, 'config.toml');
const original = fs.readFileSync(path.join(repoRoot, '.codex', 'config.toml'), 'utf8');
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeCodexConfigScript, [configPath]);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /All baseline Codex settings already present/);
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-codex-config warns when inline tables cannot be safely extended', () => {
const tempDir = createTempDir('codex-merge-inline-warn-');
const configPath = path.join(tempDir, 'config.toml');
const original = 'agents = { explorer = { description = "custom explorer" } }\n';
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeCodexConfigScript, [configPath, '--dry-run']);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stderr, /WARNING: Skipping missing keys/);
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config reports usage, missing files, and TOML parse failures', () => {
const tempDir = createTempDir('mcp-merge-errors-');
try {
const noArgs = runNode(mergeMcpConfigScript, [], deterministicPackageEnv);
assert.strictEqual(noArgs.status, 1);
assert.match(noArgs.stderr, /Usage: merge-mcp-config\.js/);
const missingPath = path.join(tempDir, 'missing-config.toml');
const missing = runNode(mergeMcpConfigScript, [missingPath], deterministicPackageEnv);
assert.strictEqual(missing.status, 1);
assert.match(missing.stderr, /Config file not found/);
const invalidPath = path.join(tempDir, 'invalid-config.toml');
fs.writeFileSync(invalidPath, '[mcp_servers.github\n');
const invalid = runNode(mergeMcpConfigScript, [invalidPath], deterministicPackageEnv);
assert.strictEqual(invalid.status, 1);
assert.match(invalid.stderr, /Failed to parse/);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config dry-run appends the current default set without mutating target', () => {
const tempDir = createTempDir('mcp-merge-dry-run-');
const configPath = path.join(tempDir, 'config.toml');
const original = '';
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeMcpConfigScript, [configPath, '--dry-run'], deterministicPackageEnv);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Package manager: npm \(exec: npx\)/);
assert.match(result.stdout, /\[add\] mcp_servers\.chrome-devtools/);
assert.match(result.stdout, /\[mcp_servers\.chrome-devtools\]/);
assert.match(result.stdout, /Dry run/);
// Retired defaults (June 2026 connector policy) must not be emitted.
assert.doesNotMatch(result.stdout, /mcp_servers\.(supabase|playwright|context7|exa|github|memory|sequential-thinking)\b/);
assert.doesNotMatch(result.stdout, /url = /);
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config no-ops after all recommended servers are present', () => {
const tempDir = createTempDir('mcp-merge-noop-');
const configPath = path.join(tempDir, 'config.toml');
try {
fs.writeFileSync(configPath, '');
const first = runNode(mergeMcpConfigScript, [configPath], deterministicPackageEnv);
assert.strictEqual(first.status, 0, `${first.stdout}\n${first.stderr}`);
const merged = fs.readFileSync(configPath, 'utf8');
const parsed = TOML.parse(merged);
assert.strictEqual(parsed.mcp_servers['chrome-devtools'].command, 'npx');
assert.deepStrictEqual(parsed.mcp_servers['chrome-devtools'].args, ['chrome-devtools-mcp@latest']);
assert.strictEqual(parsed.mcp_servers['chrome-devtools'].startup_timeout_sec, 30);
// No retired server may be (re-)emitted — exa's url form broke Codex (#2224).
assert.strictEqual(parsed.mcp_servers.exa, undefined);
assert.strictEqual(parsed.mcp_servers.github, undefined);
assert.strictEqual(parsed.mcp_servers.supabase, undefined);
const second = runNode(mergeMcpConfigScript, [configPath], deterministicPackageEnv);
assert.strictEqual(second.status, 0, `${second.stdout}\n${second.stderr}`);
assert.match(second.stdout, /\[ok\] mcp_servers\.chrome-devtools/);
assert.match(second.stdout, /All ECC MCP servers already present/);
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), merged);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config repairs the invalid exa url entry from earlier ECC versions (#2224)', () => {
const tempDir = createTempDir('mcp-merge-exa-repair-');
const configPath = path.join(tempDir, 'config.toml');
const original = [
'[mcp_servers.github]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-github"]',
'',
'[mcp_servers.exa]',
'url = "https://mcp.exa.ai/mcp"',
'',
].join('\n');
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeMcpConfigScript, [configPath], deterministicPackageEnv);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /\[repair\] mcp_servers\.exa/);
const updated = fs.readFileSync(configPath, 'utf8');
const parsed = TOML.parse(updated);
assert.strictEqual(parsed.mcp_servers.exa, undefined, 'invalid exa url entry must be removed');
assert.doesNotMatch(updated, /url = "https:\/\/mcp\.exa\.ai\/mcp"/);
// User-managed servers are untouched; current default is added.
assert.strictEqual(parsed.mcp_servers.github.command, 'npx');
assert.strictEqual(parsed.mcp_servers['chrome-devtools'].command, 'npx');
// Re-running must not re-introduce the invalid entry.
const second = runNode(mergeMcpConfigScript, [configPath], deterministicPackageEnv);
assert.strictEqual(second.status, 0, `${second.stdout}\n${second.stderr}`);
assert.doesNotMatch(fs.readFileSync(configPath, 'utf8'), /mcp_servers\.exa/);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config leaves a user-managed stdio exa entry untouched', () => {
const tempDir = createTempDir('mcp-merge-exa-stdio-');
const configPath = path.join(tempDir, 'config.toml');
const original = [
'[mcp_servers.exa]',
'command = "npx"',
'args = ["-y", "mcp-remote", "https://mcp.exa.ai/mcp"]',
'startup_timeout_sec = 30',
'',
].join('\n');
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeMcpConfigScript, [configPath], deterministicPackageEnv);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.doesNotMatch(result.stdout, /\[repair\]/);
const parsed = TOML.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(parsed.mcp_servers.exa.command, 'npx');
assert.deepStrictEqual(parsed.mcp_servers.exa.args, ['-y', 'mcp-remote', 'https://mcp.exa.ai/mcp']);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config update dry-run refreshes managed sections and leaves user servers alone', () => {
const tempDir = createTempDir('mcp-merge-update-dry-run-');
const configPath = path.join(tempDir, 'config.toml');
const original = [
'[mcp_servers.chrome-devtools]',
'command = "custom"',
'args = ["old"]',
'',
'[mcp_servers.context7]',
'command = "npx"',
'args = ["-y", "@upstash/context7-mcp@latest"]',
'',
].join('\n');
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeMcpConfigScript, [configPath, '--update-mcp', '--dry-run'], deterministicPackageEnv);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /\[remove\] mcp_servers\.chrome-devtools/);
assert.match(result.stdout, /\[mcp_servers\.chrome-devtools\]/);
// Retired servers are no longer ECC-managed: never removed or re-added.
assert.doesNotMatch(result.stdout, /\[remove\] mcp_servers\.context7/);
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('merge-mcp-config removes disabled servers without appending replacements', () => {
const tempDir = createTempDir('mcp-merge-disabled-');
const configPath = path.join(tempDir, 'config.toml');
const original = [
'[mcp_servers.chrome-devtools]',
'command = "npx"',
'args = ["chrome-devtools-mcp@latest"]',
'',
].join('\n');
try {
fs.writeFileSync(configPath, original);
const result = runNode(mergeMcpConfigScript, [configPath], {
...deterministicPackageEnv,
ECC_DISABLED_MCPS: 'chrome-devtools',
});
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Disabled via ECC_DISABLED_MCPS/);
assert.match(result.stdout, /\[skip\] mcp_servers\.chrome-devtools \(disabled\)/);
assert.match(result.stdout, /\[update\] mcp_servers\.chrome-devtools \(disabled\)/);
assert.match(result.stdout, /Done\. Removed 1 server section\(s\)\./);
const updated = fs.readFileSync(configPath, 'utf8');
assert.doesNotMatch(updated, /chrome-devtools/);
} finally {
cleanup(tempDir);
}
})
)
passed++;
else failed++;
if (
test('sync installs the missing Codex baseline and accepts the legacy context7 MCP section', () => {
const homeDir = createTempDir('codex-sync-home-');
const codexDir = path.join(homeDir, '.codex');
const configPath = path.join(codexDir, 'config.toml');
const agentsPath = path.join(codexDir, 'AGENTS.md');
const config = [
'persistent_instructions = ""',
'',
'[agents]',
'explorer = { description = "Read-only codebase explorer for gathering evidence before changes are proposed." }',
'',
'[mcp_servers.context7]',
'command = "npx"',
'args = ["-y", "@upstash/context7-mcp"]',
'',
'[mcp_servers.github]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-github"]',
'',
'[mcp_servers.memory]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-memory"]',
'',
'[mcp_servers.sequential-thinking]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
'',
].join('\n');
try {
fs.mkdirSync(codexDir, { recursive: true });
fs.writeFileSync(configPath, config);
const syncResult = runBash(syncScript, ['--update-mcp'], makeHermeticCodexEnv(homeDir, codexDir));
assert.strictEqual(syncResult.status, 0, `${syncResult.stdout}\n${syncResult.stderr}`);
const syncedAgents = fs.readFileSync(agentsPath, 'utf8');
assert.match(syncedAgents, /^# Everything Claude Code \(ECC\) — Agent Instructions/m);
assert.match(syncedAgents, /^# Codex Supplement \(From ECC \.codex\/AGENTS\.md\)/m);
const syncedConfig = fs.readFileSync(configPath, 'utf8');
const parsedConfig = TOML.parse(syncedConfig);
assert.strictEqual(parsedConfig.approval_policy, 'on-request');
assert.strictEqual(parsedConfig.sandbox_mode, 'workspace-write');
assert.strictEqual(parsedConfig.web_search, 'live');
assert.ok(!Object.prototype.hasOwnProperty.call(parsedConfig, 'multi_agent'));
assert.ok(parsedConfig.features);
assert.strictEqual(parsedConfig.features.multi_agent, true);
assert.ok(parsedConfig.profiles);
assert.strictEqual(parsedConfig.profiles.strict.approval_policy, 'on-request');
assert.strictEqual(parsedConfig.profiles.yolo.approval_policy, 'never');
assert.ok(parsedConfig.agents);
assert.strictEqual(parsedConfig.agents.max_threads, 6);
assert.strictEqual(parsedConfig.agents.max_depth, 1);
assert.strictEqual(parsedConfig.agents.explorer.config_file, 'agents/explorer.toml');
assert.strictEqual(parsedConfig.agents.reviewer.config_file, 'agents/reviewer.toml');
assert.strictEqual(parsedConfig.agents.docs_researcher.config_file, 'agents/docs-researcher.toml');
// Current default connector is added; retired servers are not emitted,
// and pre-existing user-managed entries are preserved untouched.
assert.ok(parsedConfig.mcp_servers['chrome-devtools']);
assert.strictEqual(parsedConfig.mcp_servers.exa, undefined);
assert.ok(parsedConfig.mcp_servers.github);
assert.ok(parsedConfig.mcp_servers.memory);
assert.ok(parsedConfig.mcp_servers['sequential-thinking']);
assert.ok(parsedConfig.mcp_servers.context7);
for (const roleFile of ['explorer.toml', 'reviewer.toml', 'docs-researcher.toml']) {
assert.ok(fs.existsSync(path.join(codexDir, 'agents', roleFile)));
}
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
if (
test('sync adds parent-table keys when the target only declares an implicit parent table', () => {
const homeDir = createTempDir('codex-sync-implicit-parent-home-');
const codexDir = path.join(homeDir, '.codex');
const configPath = path.join(codexDir, 'config.toml');
const config = [
'persistent_instructions = ""',
'',
'[agents.explorer]',
'description = "Read-only codebase explorer for gathering evidence before changes are proposed."',
'',
].join('\n');
try {
fs.mkdirSync(codexDir, { recursive: true });
fs.writeFileSync(configPath, config);
const syncResult = runBash(syncScript, [], makeHermeticCodexEnv(homeDir, codexDir));
assert.strictEqual(syncResult.status, 0, `${syncResult.stdout}\n${syncResult.stderr}`);
const parsedConfig = TOML.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(parsedConfig.agents.max_threads, 6);
assert.strictEqual(parsedConfig.agents.max_depth, 1);
assert.strictEqual(parsedConfig.agents.explorer.config_file, 'agents/explorer.toml');
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
+194
View File
@@ -0,0 +1,194 @@
/**
* Tests for scripts/consult.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'consult.js');
function run(args = [], options = {}) {
return spawnSync(process.execPath, [SCRIPT, ...args], {
cwd: options.cwd || process.cwd(),
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
}
function parseJson(stdout) {
return JSON.parse(stdout.trim());
}
function findMatch(payload, componentId) {
return payload.matches.find(match => match.componentId === componentId);
}
function findMatchIndex(payload, componentId) {
return payload.matches.findIndex(match => match.componentId === componentId);
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing consult.js ===\n');
let passed = 0;
let failed = 0;
if (test('shows help with an explicit help flag', () => {
const result = run(['--help']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Consult ECC install components/);
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
})) passed++; else failed++;
if (test('shows help even when other flags would be invalid', () => {
const result = run(['--help', '--target', 'not-a-target']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Consult ECC install components/);
})) passed++; else failed++;
if (test('recommends security components and profile for a natural language query', () => {
const result = run(['security', 'reviews', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.consult.v1');
assert.strictEqual(payload.query, 'security reviews');
assert.strictEqual(payload.target, 'claude');
assert.strictEqual(payload.matches[0].componentId, 'capability:security');
assert.ok(payload.matches[0].reasons.some(reason => reason.includes('security')));
assert.strictEqual(
payload.matches[0].installCommand,
'npx ecc install --profile minimal --target claude --with capability:security'
);
assert.ok(payload.profiles.some(profile => profile.id === 'security'));
assert.ok(payload.profiles.find(profile => profile.id === 'security').installCommand.includes('--profile security'));
})) passed++; else failed++;
if (test('prints text recommendations with install and plan commands', () => {
const result = run(['I', 'want', 'a', 'skill', 'for', 'security', 'reviews']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /ECC consult/);
assert.match(result.stdout, /capability:security/);
assert.match(result.stdout, /npx ecc install --profile minimal --target claude --with capability:security/);
assert.match(result.stdout, /npx ecc plan --profile minimal --target claude --with capability:security/);
})) passed++; else failed++;
if (test('recommends machine-learning component and reviewer agent', () => {
const result = run(['mlops', 'training', 'model', 'deployment', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
const capabilityIndex = findMatchIndex(payload, 'capability:machine-learning');
const reviewerIndex = findMatchIndex(payload, 'agent:mle-reviewer');
assert.ok(capabilityIndex >= 0, 'Should include capability:machine-learning');
assert.ok(reviewerIndex >= 0, 'Should include agent:mle-reviewer');
assert.ok(capabilityIndex < reviewerIndex,
'The workflow capability should rank ahead of the reviewer agent for broad MLE setup queries');
assert.ok(findMatch(payload, 'capability:machine-learning').installCommand.includes('--with capability:machine-learning'));
assert.ok(!payload.profiles.some(profile => profile.id === 'mle'));
})) passed++; else failed++;
if (test('matches tokenized model review queries without making review a generic alias', () => {
const result = run(['model', 'review', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
const capabilityIndex = findMatchIndex(payload, 'capability:machine-learning');
const securityIndex = findMatchIndex(payload, 'capability:security');
const reviewerIndex = findMatchIndex(payload, 'agent:mle-reviewer');
const codeReviewerIndex = findMatchIndex(payload, 'agent:code-reviewer');
const reviewer = findMatch(payload, 'agent:mle-reviewer');
assert.ok(reviewer, 'Should include agent:mle-reviewer');
assert.ok(reviewer.reasons.includes('matched "model"'));
assert.ok(!reviewer.reasons.includes('matched "review"'));
assert.ok(!reviewer.reasons.includes('fuzzy matched "review"'));
assert.ok(capabilityIndex >= 0, 'Should include capability:machine-learning');
assert.ok(securityIndex < 0 || capabilityIndex < securityIndex,
'Model review queries should prefer the MLE capability over generic security review');
assert.ok(codeReviewerIndex < 0 || reviewerIndex < codeReviewerIndex,
'Model review queries should prefer the MLE reviewer over generic code review');
})) passed++; else failed++;
if (test('surfaces MLE reviewer for PyTorch model review queries', () => {
const result = run(['pytorch', 'model', 'review', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
const reviewer = findMatch(payload, 'agent:mle-reviewer');
assert.ok(findMatch(payload, 'capability:machine-learning'), 'Should include capability:machine-learning');
assert.ok(reviewer, 'Should include agent:mle-reviewer');
assert.ok(reviewer.reasons.includes('matched "pytorch"'));
})) passed++; else failed++;
if (test('does not route generic review queries to MLE components', () => {
const result = run(['review', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.ok(!findMatch(payload, 'capability:machine-learning'));
assert.ok(!findMatch(payload, 'agent:mle-reviewer'));
assert.ok(!payload.profiles.some(profile => profile.id === 'mle'));
})) passed++; else failed++;
if (test('works from outside the ECC repository', () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-consult-project-'));
try {
const result = run(['nextjs', 'react', '--json'], { cwd: projectDir });
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.matches[0].componentId, 'framework:nextjs');
assert.ok(payload.matches.some(match => match.componentId === 'framework:react'));
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('filters recommendations by target and limit', () => {
const result = run(['operator', 'workflows', '--target', 'codex', '--limit', '1', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.target, 'codex');
assert.strictEqual(payload.matches.length, 1);
assert.ok(payload.matches[0].targets.includes('codex'));
assert.ok(payload.matches[0].installCommand.includes('--target codex'));
})) passed++; else failed++;
if (test('rejects unknown targets', () => {
const result = run(['security', '--target', 'not-a-target']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /Unknown install target/);
})) passed++; else failed++;
if (test('rejects flag-like target values as missing target names', () => {
const result = run(['security', '--target', '--json']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /Missing value for --target/);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+669
View File
@@ -0,0 +1,669 @@
/**
* Tests for scripts/control-pane.js and its local HTTP API.
*/
const assert = require('assert');
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
const initSqlJs = require('sql.js');
const { createControlPaneServer, parseArgs, runAction, isAllowedHostHeader, isAllowedOrigin, buildAllowedHostnames } = require('../../scripts/lib/control-pane/server');
const { main: runControlPaneCli } = require('../../scripts/control-pane');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'control-pane.js');
const REPO_ROOT = path.join(__dirname, '..', '..');
async function test(name, fn) {
try {
await fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function writeMinimalDatabase(dbPath) {
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run(`
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
task TEXT NOT NULL,
project TEXT NOT NULL DEFAULT '',
task_group TEXT NOT NULL DEFAULT '',
agent_type TEXT NOT NULL,
harness TEXT NOT NULL DEFAULT 'unknown',
detected_harnesses_json TEXT NOT NULL DEFAULT '[]',
working_dir TEXT NOT NULL DEFAULT '.',
state TEXT NOT NULL DEFAULT 'pending',
pid INTEGER,
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
files_changed INTEGER DEFAULT 0,
duration_secs INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_heartbeat_at TEXT NOT NULL
);
INSERT INTO sessions (
id, task, agent_type, harness, detected_harnesses_json, working_dir, state,
created_at, updated_at, last_heartbeat_at
) VALUES (
'session-a', 'Build the control pane', 'codex', 'codex', '["codex"]', '/repo/ecc',
'running', '2026-06-03T10:00:00Z', '2026-06-03T10:05:00Z', '2026-06-03T10:05:00Z'
);
`);
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
}
function waitForCliReady(child) {
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill('SIGTERM');
reject(new Error(`Timed out waiting for control pane CLI.\nstdout:\n${stdout}\nstderr:\n${stderr}`));
}, 5000);
child.stdout.on('data', chunk => {
stdout += chunk.toString('utf8');
if (!settled && stdout.includes('ECC Control Pane:') && stdout.includes('Actions:')) {
settled = true;
clearTimeout(timer);
resolve({ stdout, stderr });
}
});
child.stderr.on('data', chunk => {
stderr += chunk.toString('utf8');
});
child.on('error', error => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(error);
});
child.on('exit', code => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(new Error(`control pane CLI exited early with ${code}.\nstdout:\n${stdout}\nstderr:\n${stderr}`));
});
});
}
function waitForExit(child) {
return new Promise(resolve => {
child.once('exit', (code, signal) => resolve({ code, signal }));
});
}
async function fetchLocal(url, options) {
let lastError;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
await new Promise(resolve => setTimeout(resolve, 25 * (attempt + 1)));
}
}
throw lastError;
}
async function runTests() {
console.log('\n=== Testing control-pane server ===\n');
let passed = 0;
let failed = 0;
if (
await test('parses CLI arguments for local-only serving', async () => {
const parsed = parseArgs([
'node',
'scripts/control-pane.js',
'--host',
'127.0.0.1',
'--port',
'8788',
'--db',
'/tmp/ecc2.db',
'--state-db',
'/tmp/ecc-state.db',
'--query',
'Hermes memory',
'--no-open'
]);
assert.strictEqual(parsed.host, '127.0.0.1');
assert.strictEqual(parsed.port, 8788);
assert.strictEqual(parsed.dbPath, '/tmp/ecc2.db');
assert.strictEqual(parsed.stateDbPath, '/tmp/ecc-state.db');
assert.strictEqual(parsed.query, 'Hermes memory');
assert.strictEqual(parsed.openBrowser, false);
})
)
passed++;
else failed++;
if (
await test('rejects invalid CLI port values', async () => {
assert.throws(() => parseArgs(['node', 'scripts/control-pane.js', '--port', '70000']), /Invalid --port value/);
assert.throws(() => parseArgs(['node', 'scripts/control-pane.js', '--port', 'wat']), /Invalid --port value/);
})
)
passed++;
else failed++;
if (
await test('rejects missing state database path values', async () => {
assert.throws(() => parseArgs(['node', 'scripts/control-pane.js', '--state-db']), /Invalid --state-db value/);
assert.throws(() => parseArgs(['node', 'scripts/control-pane.js', '--state-db', '--query', 'Hermes']), /Invalid --state-db value/);
})
)
passed++;
else failed++;
if (
await test('serves HTML and snapshot JSON from a temp ECC2 database', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-server-'));
const dbPath = path.join(tempDir, 'ecc2.db');
try {
await writeMinimalDatabase(dbPath);
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath,
repoRoot: REPO_ROOT,
query: 'control pane',
allowActions: false
});
await app.listen();
try {
const html = await fetchLocal(`${app.url}/`).then(response => response.text());
assert.ok(html.includes('ECC Control Pane'));
assert.ok(html.includes('id="app"'));
assert.ok(html.includes('id="work-items"'));
assert.ok(html.includes('function renderWorkItems'));
assert.ok(html.includes('function showError'));
assert.ok(html.includes('response.ok'));
// Board controls must use escaped data-* attributes + delegated
// listeners, never ids concatenated into inline onclick JS (XSS).
assert.ok(html.includes('data-wi-action'));
assert.ok(!/onclick="ecc(Claim|Move)Item\(/.test(html), 'no inline onclick handlers with interpolated ids');
const snapshot = await fetchLocal(`${app.url}/api/snapshot?query=control`).then(response => response.json());
assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1');
assert.strictEqual(snapshot.summary.totalSessions, 1);
assert.strictEqual(snapshot.workItems.totalCount, 0);
assert.strictEqual(snapshot.sessions[0].id, 'session-a');
} finally {
await app.close();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if (
await test('serves the 3D agent-airspace page and the proximity JSON feed', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-proximity-'));
const dbPath = path.join(tempDir, 'ecc2.db');
try {
await writeMinimalDatabase(dbPath);
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath,
repoRoot: REPO_ROOT,
allowActions: false
});
await app.listen();
try {
// The Enterprise/Pro 3D observability view: a self-contained HTML page.
const page = await fetchLocal(`${app.url}/proximity`);
assert.strictEqual(page.status, 200);
assert.ok((page.headers.get('content-type') || '').includes('text/html'));
const html = await page.text();
assert.ok(html.includes('Agent Airspace'), 'page is titled Agent Airspace');
assert.ok(html.includes('<canvas'), 'page renders a canvas');
assert.ok(html.includes('/api/proximity'), 'page polls the proximity feed');
// The feed the page polls: shape must carry the airspace arrays.
const prox = await fetchLocal(`${app.url}/api/proximity`).then(r => r.json());
assert.ok(Array.isArray(prox.positions), 'positions array present');
assert.ok(Array.isArray(prox.links), 'links array present');
assert.ok(Array.isArray(prox.advisories), 'advisories array present');
assert.ok(prox.counts && typeof prox.counts === 'object', 'counts present');
} finally {
await app.close();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if (
await test('serves health, asset, not-found, invalid body, and read-only action responses', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-routes-'));
try {
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath: path.join(tempDir, 'missing.db'),
repoRoot: tempDir,
allowActions: false
});
await app.listen();
try {
const health = await fetchLocal(`${app.url}/api/health`).then(response => response.json());
assert.strictEqual(health.ok, true);
assert.strictEqual(health.allowActions, false);
const realAssetApp = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath: path.join(tempDir, 'missing.db'),
repoRoot: REPO_ROOT,
allowActions: false
});
await realAssetApp.listen();
try {
const realAsset = await fetchLocal(`${realAssetApp.url}/assets/ecc-icon.svg`);
assert.strictEqual(realAsset.status, 200);
assert.match(await realAsset.text(), /<svg/);
} finally {
await realAssetApp.close();
}
const missingAsset = await fetchLocal(`${app.url}/assets/ecc-icon.svg`);
assert.strictEqual(missingAsset.status, 404);
assert.strictEqual(await missingAsset.text(), 'not found');
const missing = await fetchLocal(`${app.url}/not-here`).then(response => response.json());
assert.strictEqual(missing.ok, false);
assert.strictEqual(missing.error, 'not found');
const blocked = await fetchLocal(`${app.url}/api/actions/sync-knowledge`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query: 'memory' })
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(blocked.status, 403);
assert.match(blocked.body.error, /disabled/);
const invalidBody = await fetchLocal(`${app.url}/api/actions/sync-knowledge`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{bad json'
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(invalidBody.status, 403);
} finally {
await app.close();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if (
await test('guards copy-only and unknown action requests', async () => {
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
repoRoot: REPO_ROOT,
allowActions: true
});
await app.listen();
try {
const copyOnly = await fetchLocal(`${app.url}/api/actions/open-dashboard`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}'
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(copyOnly.status, 400);
assert.strictEqual(copyOnly.body.action, 'open-dashboard');
assert.match(copyOnly.body.error, /copy-only/);
const unknown = await fetchLocal(`${app.url}/api/actions/nope`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}'
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(unknown.status, 500);
assert.match(unknown.body.error, /Unknown control-pane action/);
const invalidBody = await fetchLocal(`${app.url}/api/actions/sync-knowledge`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{bad json'
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(invalidBody.status, 500);
assert.match(invalidBody.body.error, /JSON/);
} finally {
await app.close();
}
})
)
passed++;
else failed++;
if (
await test('classifies Host and Origin headers against the loopback allowlist', async () => {
const allowed = buildAllowedHostnames('127.0.0.1');
assert.strictEqual(isAllowedHostHeader('127.0.0.1:8765', allowed), true);
assert.strictEqual(isAllowedHostHeader('localhost:8765', allowed), true);
assert.strictEqual(isAllowedHostHeader('LOCALHOST:8765', allowed), true);
assert.strictEqual(isAllowedHostHeader('[::1]:8765', allowed), true);
assert.strictEqual(isAllowedHostHeader('attacker.example.com:8765', allowed), false);
assert.strictEqual(isAllowedHostHeader('rebind.dnsbin.io', allowed), false);
assert.strictEqual(isAllowedHostHeader('', allowed), false);
assert.strictEqual(isAllowedHostHeader(undefined, allowed), false);
// Origin is optional; absence is allowed for non-browser clients.
assert.strictEqual(isAllowedOrigin(undefined, allowed), true);
assert.strictEqual(isAllowedOrigin('', allowed), true);
assert.strictEqual(isAllowedOrigin('http://127.0.0.1:8765', allowed), true);
assert.strictEqual(isAllowedOrigin('http://localhost', allowed), true);
assert.strictEqual(isAllowedOrigin('http://attacker.example.com', allowed), false);
assert.strictEqual(isAllowedOrigin('not-a-url', allowed), false);
// A non-default configured host should still admit loopback variants.
const lan = buildAllowedHostnames('192.168.1.10');
assert.strictEqual(isAllowedHostHeader('192.168.1.10:8765', lan), true);
assert.strictEqual(isAllowedHostHeader('127.0.0.1:8765', lan), true);
assert.strictEqual(isAllowedHostHeader('attacker.example.com:8765', lan), false);
})
)
passed++;
else failed++;
if (
await test('rejects requests forged with a non-loopback Host header (DNS rebinding gate)', async () => {
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
repoRoot: REPO_ROOT,
allowActions: true
});
await app.listen();
try {
const address = app.server.address();
const actualPort = address && typeof address === 'object' ? address.port : 0;
const sendWithHeaders = (method, pathname, headers, body) =>
new Promise((resolve, reject) => {
const req = http.request({ host: '127.0.0.1', port: actualPort, method, path: pathname, headers }, response => {
let chunks = '';
response.on('data', chunk => {
chunks += chunk.toString('utf8');
});
response.on('end', () => {
resolve({ status: response.statusCode, body: chunks });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
const forgedHost = await sendWithHeaders('GET', '/api/health', { Host: 'attacker.example.com:1234' });
assert.strictEqual(forgedHost.status, 421);
assert.match(forgedHost.body, /Misdirected request/);
const forgedActionHost = await sendWithHeaders(
'POST',
'/api/actions/sync-knowledge',
{ Host: 'attacker.example.com:1234', 'content-type': 'application/json' },
JSON.stringify({ query: 'rebound' })
);
assert.strictEqual(forgedActionHost.status, 421);
const forgedOrigin = await sendWithHeaders('GET', '/api/health', {
Host: '127.0.0.1:' + actualPort,
Origin: 'http://attacker.example.com'
});
assert.strictEqual(forgedOrigin.status, 403);
assert.match(forgedOrigin.body, /Forbidden origin/);
const okHost = await sendWithHeaders('GET', '/api/health', { Host: '127.0.0.1:' + actualPort });
assert.strictEqual(okHost.status, 200);
const okBody = JSON.parse(okHost.body);
assert.strictEqual(okBody.ok, true);
} finally {
await app.close();
}
})
)
passed++;
else failed++;
if (
await test('runAction captures success, failure, and bounded output', async () => {
const repoRoot = REPO_ROOT;
const success = await runAction({
id: 'node-success',
command: process.execPath,
args: ['-e', 'process.stdout.write("x".repeat(21010))'],
cwd: repoRoot
});
assert.strictEqual(success.ok, true);
assert.strictEqual(success.code, 0);
assert.ok(success.stdout.includes('[truncated '));
const failure = await runAction({
id: 'node-failure',
command: process.execPath,
args: ['-e', 'process.stderr.write("bad"); process.exit(7)'],
cwd: repoRoot
});
assert.strictEqual(failure.ok, false);
assert.strictEqual(failure.code, 7);
assert.strictEqual(failure.stderr, 'bad');
const spawnError = await runAction({
id: 'spawn-error',
command: 'definitely-not-ecc-control-pane-command',
args: [],
cwd: repoRoot
});
assert.strictEqual(spawnError.ok, false);
assert.strictEqual(spawnError.code, null);
assert.match(spawnError.error, /ENOENT/);
})
)
passed++;
else failed++;
if (
await test('runAction terminates commands that exceed the local timeout', async () => {
const timedOut = await runAction(
{
id: 'node-timeout',
command: process.execPath,
args: ['-e', 'setTimeout(() => {}, 5000)'],
cwd: REPO_ROOT
},
{ timeoutMs: 25 }
);
assert.strictEqual(timedOut.ok, false);
assert.strictEqual(timedOut.signal, 'SIGTERM');
})
)
passed++;
else failed++;
if (
await test('CLI prints help', async () => {
const result = spawnSync('node', [SCRIPT, '--help'], {
encoding: 'utf8',
cwd: REPO_ROOT
});
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(result.stdout.includes('Usage:'));
assert.ok(result.stdout.includes('control-pane'));
})
)
passed++;
else failed++;
if (
await test('CLI browser opener handles spawn errors', async () => {
const source = fs.readFileSync(SCRIPT, 'utf8');
assert.match(source, /child\.on\('error'/);
assert.match(source, /child\.unref\(\)/);
})
)
passed++;
else failed++;
if (
await test('CLI main handles help without starting a server', async () => {
const originalLog = console.log;
const lines = [];
console.log = line => {
lines.push(String(line));
};
try {
await runControlPaneCli(['node', 'scripts/control-pane.js', '--help']);
} finally {
console.log = originalLog;
}
assert.match(lines.join('\n'), /Usage:/);
assert.match(lines.join('\n'), /--read-only/);
})
)
passed++;
else failed++;
if (
await test('CLI starts a read-only local server and shuts down on SIGTERM', async () => {
const child = spawn(process.execPath, [SCRIPT, '--host', '127.0.0.1', '--port', '0', '--read-only', '--no-open'], {
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
ECC2_DB_PATH: path.join(os.tmpdir(), 'missing-ecc2-cli.db')
}
});
const exitPromise = waitForExit(child);
try {
const ready = await waitForCliReady(child);
assert.match(ready.stdout, /ECC Control Pane: http:\/\/127\.0\.0\.1:\d+/);
assert.match(ready.stdout, /Actions: read-only/);
} finally {
if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
const result = await exitPromise;
assert.ok(result.code === 0 || result.signal === 'SIGTERM', `expected graceful shutdown or SIGTERM, got code=${result.code} signal=${result.signal}`);
}
})
)
passed++;
else failed++;
if (
await test('interactive board: claim and move work items via POST endpoints', async () => {
const { createStateStore } = require('../../scripts/lib/state-store');
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-board-'));
const stateDbPath = path.join(tempDir, 'state.db');
try {
const store = await createStateStore({ dbPath: stateDbPath });
store.upsertWorkItem({ id: 'wi-1', source: 'github-issue', title: 'Unassigned card', status: 'open', priority: 'high', owner: null, metadata: {} });
store.upsertWorkItem({ id: 'wi-2', source: 'manual', title: 'Movable card', status: 'open', owner: 'codex', metadata: {} });
store.close();
const post = (app, suffix, body) =>
fetchLocal(`${app.url}/api/work-items/${suffix}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
// Read-only server rejects board edits.
const ro = await createControlPaneServer({ host: '127.0.0.1', port: 0, stateDbPath, repoRoot: REPO_ROOT, allowActions: false });
await ro.listen();
try {
const denied = await post(ro, 'wi-1/claim', { owner: 'alice' });
assert.strictEqual(denied.status, 403);
} finally {
await ro.close();
}
// Interactive server claims and moves.
const app = await createControlPaneServer({ host: '127.0.0.1', port: 0, stateDbPath, repoRoot: REPO_ROOT, allowActions: true });
await app.listen();
try {
const claim = await post(app, 'wi-1/claim', { owner: 'alice', as: 'human' }).then(r => r.json());
assert.strictEqual(claim.ok, true);
assert.strictEqual(claim.item.owner, 'alice');
assert.strictEqual(claim.item.status, 'running');
assert.strictEqual(claim.item.metadata.assigneeKind, 'human');
const move = await post(app, 'wi-2/move', { lane: 'blocked' }).then(r => r.json());
assert.strictEqual(move.ok, true);
assert.strictEqual(move.item.status, 'blocked');
// Snapshot reflects the mutations.
const snapshot = await fetchLocal(`${app.url}/api/snapshot`).then(r => r.json());
const byId = id => snapshot.workItems.items.find(i => i.id === id);
assert.strictEqual(byId('wi-1').assigneeKind, 'human');
assert.strictEqual(byId('wi-2').kanbanState, 'blocked');
// Invalid lane is a 400.
const bad = await post(app, 'wi-2/move', { lane: 'nope' });
assert.strictEqual(bad.status, 400);
} finally {
await app.close();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+785
View File
@@ -0,0 +1,785 @@
/**
* Tests for scripts/dashboard-web.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'dashboard-web.js');
let testRoot;
let testPassed = 0;
let testFailed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
testPassed++;
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
testFailed++;
return false;
}
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
// ===================== parsePort =====================
test('parsePort returns 3456 for undefined', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort(undefined), 3456);
});
test('parsePort returns numeric port for valid string', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort('8080'), 8080);
assert.strictEqual(parsePort('3456'), 3456);
});
test('parsePort returns numeric port for numeric input', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort(8080), 8080);
});
test('parsePort returns 3456 for port below 1', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort('-1'), 3456);
assert.strictEqual(parsePort('0'), 3456);
});
test('parsePort returns 3456 for port above 65535', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort('70000'), 3456);
assert.strictEqual(parsePort('65536'), 3456);
});
test('parsePort accepts boundary ports 1 and 65535', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort('1'), 1);
assert.strictEqual(parsePort('65535'), 65535);
});
test('parsePort returns 3456 for non-numeric string', () => {
const { parsePort } = require(SCRIPT);
assert.strictEqual(parsePort('abc'), 3456);
assert.strictEqual(parsePort(''), 3456);
});
// ===================== readFrontmatter =====================
test('readFrontmatter parses simple frontmatter', () => {
const { readFrontmatter } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'test.md', [
'---',
'name: test-agent',
'description: A test agent description',
'model: claude-sonnet-4-6',
'---',
'# Body content',
'This is the body.',
].join('\n'));
const fm = readFrontmatter(path.join(testRoot, 'test.md'));
assert.strictEqual(fm.name, 'test-agent');
assert.strictEqual(fm.description, 'A test agent description');
assert.strictEqual(fm.model, 'claude-sonnet-4-6');
assert.ok(fm._body.includes('# Body content'));
cleanup(testRoot);
});
test('readFrontmatter parses array tools field', () => {
const { readFrontmatter } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'agent.md', [
'---',
'name: array-agent',
'tools: [Bash, Read, Write]',
'---',
'body',
].join('\n'));
const fm = readFrontmatter(path.join(testRoot, 'agent.md'));
assert.strictEqual(fm.name, 'array-agent');
assert.ok(Array.isArray(fm.tools));
assert.deepStrictEqual(fm.tools, ['Bash', 'Read', 'Write']);
cleanup(testRoot);
});
test('readFrontmatter handles quoted values', () => {
const { readFrontmatter } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'test.md', [
'---',
'name: "quoted-name"',
"description: 'single-quoted-desc'",
'---',
'body',
].join('\n'));
const fm = readFrontmatter(path.join(testRoot, 'test.md'));
assert.strictEqual(fm.name, 'quoted-name');
assert.strictEqual(fm.description, 'single-quoted-desc');
cleanup(testRoot);
});
test('readFrontmatter returns empty object for file without frontmatter', () => {
const { readFrontmatter } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'no-fm.md', '# Just a heading\nNo frontmatter here.');
const fm = readFrontmatter(path.join(testRoot, 'no-fm.md'));
assert.deepStrictEqual(fm, {});
cleanup(testRoot);
});
test('readFrontmatter returns empty object for missing file', () => {
const { readFrontmatter } = require(SCRIPT);
const fm = readFrontmatter('/nonexistent/path/test.md');
assert.deepStrictEqual(fm, {});
});
// ===================== readSkill =====================
test('readSkill parses skill frontmatter and body', () => {
const { readSkill } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'SKILL.md', [
'---',
'name: test-skill',
'description: A test skill',
'---',
'# Skill Workflow',
'Step 1: Do this.',
].join('\n'));
const skill = readSkill(path.join(testRoot, 'SKILL.md'));
assert.strictEqual(skill.d, 'A test skill');
assert.ok(skill.b.includes('# Skill Workflow'));
assert.ok(!skill.b.includes('---')); // frontmatter stripped from body
cleanup(testRoot);
});
test('readSkill returns empty defaults for missing file', () => {
const { readSkill } = require(SCRIPT);
const skill = readSkill('/nonexistent/skill/SKILL.md');
assert.strictEqual(skill.d, '');
assert.strictEqual(skill.b, '');
});
// ===================== loadAgents =====================
test('loadAgents returns empty array for missing directory', () => {
const { loadAgents } = require(SCRIPT);
const agents = loadAgents('/nonexistent/path');
assert.deepStrictEqual(agents, []);
});
test('loadAgents loads agent markdown files', () => {
const { loadAgents } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'agents/typescript-reviewer.md', [
'---',
'name: typescript-reviewer',
'description: Reviews TypeScript code',
'model: claude-sonnet-4-6',
'tools: [Bash, Read, Write, Grep]',
'---',
'# TypeScript Reviewer',
'You are a TypeScript code reviewer.',
].join('\n'));
writeFile(testRoot, 'agents/python-reviewer.md', [
'---',
'name: python-reviewer',
'description: Reviews Python code',
'model: claude-opus-4-8',
'tools: [Bash, Read]',
'---',
'# Python Reviewer',
].join('\n'));
const agents = loadAgents(testRoot);
assert.strictEqual(agents.length, 2);
assert.strictEqual(agents[0].n, 'python-reviewer'); // alphabetical sort
assert.strictEqual(agents[1].n, 'typescript-reviewer');
assert.strictEqual(agents[1].m, 'claude-sonnet-4-6');
assert.strictEqual(agents[1].d, 'Reviews TypeScript code');
assert.deepStrictEqual(agents[1].t, ['Bash', 'Read', 'Write', 'Grep']);
assert.ok(agents[1].b.length > 0);
cleanup(testRoot);
});
test('loadAgents defaults missing fields', () => {
const { loadAgents } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'agents/minimal.md', [
'# Minimal Agent',
'No frontmatter at all.',
].join('\n'));
const agents = loadAgents(testRoot);
assert.strictEqual(agents.length, 1);
assert.strictEqual(agents[0].n, 'minimal');
assert.strictEqual(agents[0].m, 'default');
assert.strictEqual(agents[0].d, '');
assert.deepStrictEqual(agents[0].t, []);
cleanup(testRoot);
});
test('loadAgents ignores non-markdown files', () => {
const { loadAgents } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'agents/agent.md', '---\nname: real-agent\n---\nbody');
writeFile(testRoot, 'agents/README.txt', 'not an agent');
const agents = loadAgents(testRoot);
assert.strictEqual(agents.length, 1);
assert.strictEqual(agents[0].n, 'real-agent');
cleanup(testRoot);
});
// ===================== loadSkills =====================
test('loadSkills returns empty array for missing directory', () => {
const { loadSkills } = require(SCRIPT);
const skills = loadSkills('/nonexistent/path');
assert.deepStrictEqual(skills, []);
});
test('loadSkills loads skill directories with SKILL.md', () => {
const { loadSkills } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'skills/seo-audit/SKILL.md', [
'---',
'name: seo-audit',
'description: Full website SEO audit',
'---',
'# SEO Audit Workflow',
].join('\n'));
writeFile(testRoot, 'skills/code-review/SKILL.md', [
'---',
'name: code-review',
'description: Review code changes',
'---',
'# Code Review Workflow',
].join('\n'));
const skills = loadSkills(testRoot);
assert.strictEqual(skills.length, 2);
assert.strictEqual(skills[0].n, 'code-review'); // alphabetical sort
assert.strictEqual(skills[1].n, 'seo-audit');
assert.strictEqual(skills[1].d, 'Full website SEO audit');
assert.ok(skills[1].b.length > 0);
cleanup(testRoot);
});
test('loadSkills ignores non-directories', () => {
const { loadSkills } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'skills/README.md', 'no skill here');
writeFile(testRoot, 'skills/real-skill/SKILL.md', '---\ndescription: A real skill\n---\nbody');
const skills = loadSkills(testRoot);
assert.strictEqual(skills.length, 1);
assert.strictEqual(skills[0].n, 'real-skill');
cleanup(testRoot);
});
// ===================== loadCommands =====================
test('loadCommands returns empty array for missing directory', () => {
const { loadCommands } = require(SCRIPT);
const commands = loadCommands('/nonexistent/path');
assert.deepStrictEqual(commands, []);
});
test('loadCommands loads command markdown files with category detection', () => {
const { loadCommands } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'commands/pr.md', [
'---',
'description: Create a pull request',
'---',
'body',
].join('\n'));
writeFile(testRoot, 'commands/go-test.md', [
'---',
'description: Run Go tests',
'---',
'body',
].join('\n'));
writeFile(testRoot, 'commands/unknown-cmd.md', [
'---',
'description: Some unknown command',
'---',
'body',
].join('\n'));
const commands = loadCommands(testRoot);
assert.strictEqual(commands.length, 3);
const prCmd = commands.find(c => c.n === '/pr');
assert.ok(prCmd);
assert.strictEqual(prCmd.c, 'Git & PR');
assert.strictEqual(prCmd.d, 'Create a pull request');
const goCmd = commands.find(c => c.n === '/go-test');
assert.ok(goCmd);
assert.strictEqual(goCmd.c, 'Languages');
const unknownCmd = commands.find(c => c.n === '/unknown-cmd');
assert.ok(unknownCmd);
assert.strictEqual(unknownCmd.c, 'Other');
cleanup(testRoot);
});
// ===================== loadRules =====================
test('loadRules returns empty array for missing directory', () => {
const { loadRules } = require(SCRIPT);
const rules = loadRules('/nonexistent/path');
assert.deepStrictEqual(rules, []);
});
test('loadRules loads language directories with rule files', () => {
const { loadRules } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'rules/python/coding-style.md', '');
writeFile(testRoot, 'rules/python/testing.md', '');
writeFile(testRoot, 'rules/python/patterns.md', '');
writeFile(testRoot, 'rules/typescript/coding-style.md', '');
writeFile(testRoot, 'rules/typescript/testing.md', '');
const rules = loadRules(testRoot);
assert.strictEqual(rules.length, 2);
const pyRules = rules.find(r => r.l === 'python');
assert.ok(pyRules);
assert.strictEqual(pyRules.f.length, 3);
assert.ok(pyRules.f.includes('coding-style'));
assert.ok(pyRules.f.includes('testing'));
assert.ok(pyRules.f.includes('patterns'));
const tsRules = rules.find(r => r.l === 'typescript');
assert.ok(tsRules);
assert.strictEqual(tsRules.f.length, 2);
cleanup(testRoot);
});
test('loadRules ignores non-directories in rules folder', () => {
const { loadRules } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'rules/README.md', 'no rules here');
writeFile(testRoot, 'rules/go/testing.md', '');
const rules = loadRules(testRoot);
assert.strictEqual(rules.length, 1);
assert.strictEqual(rules[0].l, 'go');
assert.strictEqual(rules[0].f.length, 1);
assert.strictEqual(rules[0].f[0], 'testing');
cleanup(testRoot);
});
// ===================== loadMcps =====================
test('loadMcps returns empty array when no configs exist', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
const mcps = loadMcps(testRoot);
assert.deepStrictEqual(mcps, []);
cleanup(testRoot);
});
test('loadMcps loads .mcp.json config', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, '.mcp.json', JSON.stringify({
mcpServers: {
'test-server': {
command: 'node',
args: ['server.js'],
env: { SECRET: 'real-secret' },
type: 'stdio',
},
},
}));
const mcps = loadMcps(testRoot);
assert.strictEqual(mcps.length, 1);
assert.strictEqual(mcps[0].f, '.mcp.json');
assert.strictEqual(mcps[0].s.length, 1);
assert.strictEqual(mcps[0].s[0].n, 'test-server');
assert.strictEqual(mcps[0].s[0].cmd, 'node');
assert.deepStrictEqual(mcps[0].s[0].args, ['server.js']);
assert.strictEqual(mcps[0].s[0].type, 'stdio');
// Env vars should be masked
assert.strictEqual(mcps[0].s[0].env.SECRET, '••••••');
cleanup(testRoot);
});
test('loadMcps loads mcp-configs/ directory files', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'mcp-configs/brave.json', JSON.stringify({
mcpServers: {
'brave-search': {
command: 'npx',
args: ['@anthropic/mcp-brave'],
type: 'stdio',
},
},
}));
const mcps = loadMcps(testRoot);
assert.strictEqual(mcps.length, 1);
assert.strictEqual(mcps[0].f, 'brave.json');
assert.strictEqual(mcps[0].s.length, 1);
assert.strictEqual(mcps[0].s[0].n, 'brave-search');
cleanup(testRoot);
});
test('loadMcps masks environment variables', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'mcp-configs/with-env.json', JSON.stringify({
mcpServers: {
server: {
command: 'python',
env: { API_KEY: 'super-secret-key', DEBUG: 'true' },
},
},
}));
const mcps = loadMcps(testRoot);
assert.strictEqual(mcps[0].s[0].env.API_KEY, '••••••');
assert.strictEqual(mcps[0].s[0].env.DEBUG, '••••••');
cleanup(testRoot);
});
test('loadMcps handles url-based MCP servers', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, '.mcp.json', JSON.stringify({
mcpServers: {
'remote-server': {
url: 'https://example.com/mcp',
type: 'sse',
},
},
}));
const mcps = loadMcps(testRoot);
assert.strictEqual(mcps[0].s[0].cmd, 'https://example.com/mcp');
assert.strictEqual(mcps[0].s[0].type, 'sse');
cleanup(testRoot);
});
test('loadMcps handles malformed JSON gracefully', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, '.mcp.json', '{not valid json}');
const mcps = loadMcps(testRoot);
assert.deepStrictEqual(mcps, []); // returns empty array on parse error
cleanup(testRoot);
});
// ===================== loadHooks =====================
test('loadHooks returns empty array when hooks.json missing', () => {
const { loadHooks } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
const hooks = loadHooks(testRoot);
assert.deepStrictEqual(hooks, []);
cleanup(testRoot);
});
test('loadHooks loads hook definitions', () => {
const { loadHooks } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'hooks/hooks.json', JSON.stringify({
hooks: {
'post-commit': [
{ matcher: '*.js', id: 'lint-js', description: 'Lint JS files after commit' },
{ matcher: '*.py', id: 'lint-py', description: 'Lint Python files' },
],
'pre-push': [
{ matcher: '*', id: 'run-tests', description: 'Run all tests before push' },
],
},
}));
const hooks = loadHooks(testRoot);
assert.strictEqual(hooks.length, 3);
const lintJs = hooks.find(h => h.id === 'lint-js');
assert.ok(lintJs);
assert.strictEqual(lintJs.ev, 'post-commit');
assert.strictEqual(lintJs.m, '*.js');
assert.strictEqual(lintJs.d, 'Lint JS files after commit');
cleanup(testRoot);
});
test('loadHooks handles malformed JSON gracefully', () => {
const { loadHooks } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'hooks/hooks.json', '{invalid}');
const hooks = loadHooks(testRoot);
assert.deepStrictEqual(hooks, []);
cleanup(testRoot);
});
// ===================== LANG =====================
test('LANG object has all expected language keys', () => {
const { LANG, LANG_KEYS } = require(SCRIPT);
assert.ok(LANG_KEYS.length >= 10); // at least 10 languages
// Verify key languages are present
assert.ok(LANG.en);
assert.ok(LANG.pt);
assert.ok(LANG.zh);
assert.ok(LANG.de);
assert.strictEqual(LANG_KEYS.length, Object.keys(LANG).length);
});
test('LANG English has all required keys', () => {
const { LANG } = require(SCRIPT);
const en = LANG.en;
assert.ok(en.title);
assert.ok(en.search);
assert.ok(en.agents);
assert.ok(en.skills);
assert.ok(en.commands);
assert.ok(en.rules);
assert.ok(en.mcps);
assert.ok(en.hooks);
assert.ok(en.all);
assert.ok(en.description);
assert.ok(en.tools);
assert.ok(en.copied);
});
// ===================== renderHTML =====================
test('renderHTML returns valid HTML string', () => {
const { renderHTML } = require(SCRIPT);
const data = {
agents: [],
skills: [],
commands: [],
rules: [],
mcps: [],
hooks: [],
};
const html = renderHTML(data);
assert.ok(typeof html === 'string');
assert.ok(html.startsWith('<!DOCTYPE html>'));
assert.ok(html.includes('</html>'));
});
test('renderHTML includes agent data as JSON', () => {
const { renderHTML } = require(SCRIPT);
const data = {
agents: [{ n: 'test-agent', d: 'Test desc', m: 'claude-sonnet-4-6', t: ['Bash'], b: 'body', f: 'test.md' }],
skills: [],
commands: [],
rules: [],
mcps: [],
hooks: [],
};
const html = renderHTML(data);
assert.ok(html.includes('test-agent'));
assert.ok(html.includes('claude-sonnet-4-6'));
});
test('renderHTML escapes HTML in data values', () => {
const { renderHTML } = require(SCRIPT);
const data = {
agents: [],
skills: [{ n: '<script>alert("xss")</script>', d: 'Skill & description', b: '' }],
commands: [],
rules: [],
mcps: [],
hooks: [],
};
const html = renderHTML(data);
// The JSON serialization with .replace(/</g, '\\u003c') converts < to < in JS strings
// So the rendered HTML contains <script> not <script> for data values
assert.ok(html.includes('\\u003cscript'));
assert.ok(html.includes('\\u003c/script'));
});
test('renderHTML includes LANG and LANG_KEYS in the output', () => {
const { renderHTML } = require(SCRIPT);
const data = { agents: [], skills: [], commands: [], rules: [], mcps: [], hooks: [] };
const html = renderHTML(data);
assert.ok(html.includes('ECC Capabilities'));
assert.ok(html.includes('const L ='));
assert.ok(html.includes('const LANG_KEYS'));
});
test('renderHTML includes the dashboard title and footer', () => {
const { renderHTML } = require(SCRIPT);
const data = { agents: [], skills: [], commands: [], rules: [], mcps: [], hooks: [] };
const html = renderHTML(data);
assert.ok(html.includes('ECC Capabilities'));
assert.ok(html.includes('github.com/affaan-m/ECC'));
});
// ===================== Server / HTTP =====================
test('server returns HTML on GET /', (done) => {
const { server } = require(SCRIPT);
// Server may or may not be listening — we start it on a random port
const testServer = http.createServer(server._events.request);
testServer.listen(0, () => {
const port = testServer.address().port;
http.get(`http://localhost:${port}/`, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8');
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
assert.ok(body.includes('<!DOCTYPE html>'));
assert.ok(body.includes('ECC Capabilities'));
testServer.close();
done();
});
});
});
});
test('server returns JSON on GET /api/data', (done) => {
const { server } = require(SCRIPT);
const testServer = http.createServer(server._events.request);
testServer.listen(0, () => {
const port = testServer.address().port;
http.get(`http://localhost:${port}/api/data`, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers['content-type'], 'application/json');
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
const parsed = JSON.parse(body);
assert.ok(Array.isArray(parsed.agents));
assert.ok(Array.isArray(parsed.skills));
assert.ok(Array.isArray(parsed.commands));
assert.ok(Array.isArray(parsed.rules));
assert.ok(Array.isArray(parsed.mcps));
assert.ok(Array.isArray(parsed.hooks));
testServer.close();
done();
});
});
});
});
// ===================== esc function (via HTML output) =====================
test('HTML output escapes angle brackets in renderHTML', () => {
const { renderHTML } = require(SCRIPT);
const data = {
agents: [],
skills: [{ n: 'bad<script>', d: '<img onerror=alert(1)>', b: '' }],
commands: [],
rules: [],
mcps: [],
hooks: [],
};
const html = renderHTML(data);
// The < in data values are escaped to < in JS string literals
// So we should find the escaped form in the output
assert.ok(html.includes('bad\\u003cscript>'));
assert.ok(html.includes('\\u003cimg onerror'));
});
// ===================== Edge Cases =====================
test('parsePort handles whitespace', () => {
const { parsePort } = require(SCRIPT);
// parseInt handles whitespace naturally
assert.strictEqual(parsePort(' 8080 '), 8080);
});
test('readFrontmatter handles empty file', () => {
const { readFrontmatter } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'empty.md', '');
const fm = readFrontmatter(path.join(testRoot, 'empty.md'));
assert.deepStrictEqual(fm, {});
cleanup(testRoot);
});
test('readFrontmatter handles malformed frontmatter', () => {
const { readFrontmatter } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
writeFile(testRoot, 'malformed.md', [
'---',
'name: test',
'this is not a key value',
'---',
'body',
].join('\n'));
const fm = readFrontmatter(path.join(testRoot, 'malformed.md'));
assert.strictEqual(fm.name, 'test');
assert.ok(fm._body.includes('body'));
cleanup(testRoot);
});
test('loadAgents handles empty agents directory', () => {
const { loadAgents } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
fs.mkdirSync(path.join(testRoot, 'agents'));
const agents = loadAgents(testRoot);
assert.deepStrictEqual(agents, []);
cleanup(testRoot);
});
test('loadSkills handles empty skills directory', () => {
const { loadSkills } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
fs.mkdirSync(path.join(testRoot, 'skills'));
const skills = loadSkills(testRoot);
assert.deepStrictEqual(skills, []);
cleanup(testRoot);
});
test('loadMcps handles empty mcp-configs directory', () => {
const { loadMcps } = require(SCRIPT);
testRoot = createTempDir('ecc-test-');
fs.mkdirSync(path.join(testRoot, 'mcp-configs'));
const mcps = loadMcps(testRoot);
assert.deepStrictEqual(mcps, []);
cleanup(testRoot);
});
// ===================== Results =====================
console.log(`\nResults: Passed: ${testPassed}, Failed: ${testFailed}`);
process.exit(testFailed > 0 ? 1 : 0);
+302
View File
@@ -0,0 +1,302 @@
/**
* Tests for scripts/discussion-audit.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'discussion-audit.js');
const {
DISCUSSION_ENABLED_QUERY,
DISCUSSION_QUERY
} = require(path.join(__dirname, '..', '..', 'scripts', 'lib', 'github-discussions'));
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function discussionGhKey(owner, name, first = 100) {
return `api graphql -f owner=${owner} -f name=${name} -F first=${first} -f query=${DISCUSSION_QUERY}`;
}
function discussionEnabledGhKey(owner, name) {
return `api graphql -f owner=${owner} -f name=${name} -f query=${DISCUSSION_ENABLED_QUERY}`;
}
function writeGhShim(rootDir, responses) {
const shimPath = path.join(rootDir, 'gh-shim.js');
fs.writeFileSync(shimPath, `
const responses = ${JSON.stringify(responses)};
const args = process.argv.slice(2);
const key = args.join(' ');
if (process.env.GITHUB_TOKEN) {
console.error('GITHUB_TOKEN should be unset by default');
process.exit(42);
}
if (!Object.prototype.hasOwnProperty.call(responses, key)) {
console.error('Unexpected gh args: ' + key);
process.exit(3);
}
process.stdout.write(JSON.stringify(responses[key]));
`);
return shimPath;
}
function run(args = [], options = {}) {
const env = {
...process.env,
...(options.env || {})
};
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function runProcess(args = [], options = {}) {
const env = {
...process.env,
...(options.env || {})
};
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing discussion-audit.js ===\n');
let passed = 0;
let failed = 0;
if (test('passes when discussions have maintainer touch and accepted answers', () => {
const rootDir = createTempDir('discussion-audit-pass-');
try {
const shimPath = writeGhShim(rootDir, {
[discussionEnabledGhKey('affaan-m', 'ECC')]: {
data: { repository: { hasDiscussionsEnabled: true } }
},
[discussionGhKey('affaan-m', 'ECC')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: {
totalCount: 2,
nodes: [
{
number: 1923,
title: 'Does Continuous Learning v2 work with VS Code Claude Code?',
url: 'https://github.com/example/discussions/1923',
updatedAt: '2026-05-15T19:08:52Z',
authorAssociation: 'NONE',
category: { name: 'Q&A', isAnswerable: true },
answer: { url: 'https://github.com/example/discussions/1923#discussioncomment-1', authorAssociation: 'OWNER' },
comments: { nodes: [] }
},
{
number: 73,
title: 'Compacting during workflow',
url: 'https://github.com/example/discussions/73',
updatedAt: '2026-05-15T00:00:00Z',
authorAssociation: 'NONE',
category: { name: 'General', isAnswerable: false },
answer: null,
comments: { nodes: [{ authorAssociation: 'MEMBER' }] }
}
]
}
}
}
}
});
const parsed = JSON.parse(run([
'--json',
'--repo',
'affaan-m/ECC'
], {
cwd: rootDir,
env: {
ECC_GH_SHIM: shimPath,
GITHUB_TOKEN: 'must-be-removed'
}
}));
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.totals.needingMaintainerTouch, 0);
assert.strictEqual(parsed.totals.missingAcceptedAnswer, 0);
assert.ok(parsed.checks.some(check => check.id === 'discussion-accepted-answers' && check.status === 'pass'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('fails when Q&A lacks accepted answer and maintainer touch', () => {
const rootDir = createTempDir('discussion-audit-fail-');
try {
const shimPath = writeGhShim(rootDir, {
[discussionEnabledGhKey('affaan-m', 'ECC')]: {
data: { repository: { hasDiscussionsEnabled: true } }
},
[discussionGhKey('affaan-m', 'ECC')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: {
totalCount: 1,
nodes: [
{
number: 1239,
title: 'Losing context',
url: 'https://github.com/example/discussions/1239',
updatedAt: '2026-05-15T00:00:00Z',
authorAssociation: 'NONE',
category: { name: 'Q&A', isAnswerable: true },
answer: null,
comments: { nodes: [] }
}
]
}
}
}
}
});
const result = runProcess([
'--json',
'--repo',
'affaan-m/ECC',
'--exit-code'
], {
cwd: rootDir,
env: { ECC_GH_SHIM: shimPath }
});
const parsed = JSON.parse(result.stdout);
assert.strictEqual(result.status, 2);
assert.strictEqual(parsed.ready, false);
assert.strictEqual(parsed.totals.needingMaintainerTouch, 1);
assert.strictEqual(parsed.totals.missingAcceptedAnswer, 1);
assert.ok(parsed.top_actions.some(action => action.id === 'discussion-maintainer-touch'));
assert.ok(parsed.top_actions.some(action => action.id === 'discussion-accepted-answers'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('writes markdown output as a durable operator artifact', () => {
const rootDir = createTempDir('discussion-audit-markdown-');
const outputPath = path.join(rootDir, 'artifacts', 'discussion-audit.md');
try {
const shimPath = writeGhShim(rootDir, {
[discussionEnabledGhKey('affaan-m', 'ECC')]: {
data: { repository: { hasDiscussionsEnabled: true } }
},
[discussionGhKey('affaan-m', 'ECC')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: { totalCount: 0, nodes: [] }
}
}
}
});
const stdout = run([
'--markdown',
'--write',
outputPath,
'--repo',
'affaan-m/ECC'
], {
cwd: rootDir,
env: { ECC_GH_SHIM: shimPath }
});
const written = fs.readFileSync(outputPath, 'utf8');
assert.strictEqual(stdout, written);
assert.ok(written.includes('# ECC Discussion Audit'));
assert.ok(written.includes('Answerable discussions missing accepted answer'));
assert.ok(written.includes('- none'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('passes without heavy query when discussions are disabled', () => {
const rootDir = createTempDir('discussion-audit-disabled-');
try {
const shimPath = writeGhShim(rootDir, {
[discussionEnabledGhKey('ECC-Tools', 'ECC-website')]: {
data: { repository: { hasDiscussionsEnabled: false } }
}
});
const parsed = JSON.parse(run([
'--json',
'--repo',
'ECC-Tools/ECC-website'
], {
cwd: rootDir,
env: { ECC_GH_SHIM: shimPath }
}));
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.repos[0].discussions.enabled, false);
assert.strictEqual(parsed.totals.totalDiscussions, 0);
assert.strictEqual(parsed.totals.errors, 0);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('cli help and invalid args exit cleanly', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.ok(help.stdout.includes('Usage: node scripts/discussion-audit.js'));
const invalid = runProcess(['--format', 'xml']);
assert.strictEqual(invalid.status, 1);
assert.ok(invalid.stderr.includes('Invalid format'));
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
runTests();
+190
View File
@@ -0,0 +1,190 @@
/**
* Tests for scripts/doctor.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'doctor.js');
const REPO_ROOT = path.join(__dirname, '..', '..');
const CURRENT_PACKAGE_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')
).version;
const CURRENT_MANIFEST_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'manifests', 'install-modules.json'), 'utf8')
).version;
const {
createInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeState(filePath, options) {
const state = createInstallState(options);
writeInstallState(filePath, state);
}
function run(args = [], options = {}) {
const env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
};
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing doctor.js ===\n');
let passed = 0;
let failed = 0;
if (test('reports a healthy install with exit code 0', () => {
const homeDir = createTempDir('doctor-home-');
const projectRoot = createTempDir('doctor-project-');
try {
const targetRoot = path.join(homeDir, '.claude');
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
const managedFile = path.join(targetRoot, 'rules', 'common', 'coding-style.md');
const sourceContent = fs.readFileSync(path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md'), 'utf8');
fs.mkdirSync(path.dirname(managedFile), { recursive: true });
fs.writeFileSync(managedFile, sourceContent);
writeState(statePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-claude-rules'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'legacy-claude-rules',
sourceRelativePath: 'rules/common/coding-style.md',
destinationPath: managedFile,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = run(['--target', 'claude'], { cwd: projectRoot, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Doctor report'));
assert.ok(result.stdout.includes('Status: OK'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('reports issues and exits 1 for unhealthy installs', () => {
const homeDir = createTempDir('doctor-home-');
const projectRoot = createTempDir('doctor-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
fs.mkdirSync(targetRoot, { recursive: true });
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath: path.join(targetRoot, 'hooks.json'),
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = run(['--target', 'cursor', '--json'], { cwd: projectRoot, homeDir });
assert.strictEqual(result.code, 1);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.summary.errorCount, 1);
assert.ok(parsed.results[0].issues.some(issue => issue.code === 'missing-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+142
View File
@@ -0,0 +1,142 @@
/**
* Behavioral tests for ecc_dashboard.py helper functions.
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const repoRoot = path.join(__dirname, '..', '..');
const runtimeHelpersPath = path.join(repoRoot, 'scripts', 'lib', 'ecc_dashboard_runtime.py');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runPython(source) {
const candidates = process.platform === 'win32' ? ['python', 'python3'] : ['python3', 'python'];
let lastError = null;
for (const command of candidates) {
const result = spawnSync(command, ['-c', source], {
cwd: repoRoot,
encoding: 'utf8',
});
if (result.error && result.error.code === 'ENOENT') {
lastError = result.error;
continue;
}
if (result.status !== 0) {
throw new Error((result.stderr || result.stdout || '').trim() || `${command} exited ${result.status}`);
}
return result.stdout.trim();
}
throw lastError || new Error('No Python interpreter available');
}
function runTests() {
console.log('\n=== Testing ecc_dashboard.py ===\n');
let passed = 0;
let failed = 0;
if (test('build_terminal_launch keeps Linux path separate from shell command text', () => {
const output = runPython(`
import importlib.util, json
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
argv, kwargs = module.build_terminal_launch('/tmp/proj; rm -rf ~', os_name='posix', system_name='Linux')
print(json.dumps({'argv': argv, 'kwargs': kwargs}))
`);
const parsed = JSON.parse(output);
assert.deepStrictEqual(
parsed.argv,
['x-terminal-emulator', '-e', 'bash', '-lc', 'cd -- "$1"; exec bash', 'bash', '/tmp/proj; rm -rf ~']
);
assert.deepStrictEqual(parsed.kwargs, {});
})) passed++; else failed++;
if (test('build_terminal_launch uses cwd + CREATE_NEW_CONSOLE style launch on Windows', () => {
const output = runPython(`
import importlib.util, json
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
argv, kwargs = module.build_terminal_launch(r'C:\\\\Users\\\\user\\\\proj & del C:\\\\*', os_name='nt', system_name='Windows')
print(json.dumps({'argv': argv, 'kwargs': kwargs}))
`);
const parsed = JSON.parse(output);
assert.deepStrictEqual(parsed.argv, ['cmd.exe']);
assert.ok(parsed.kwargs.cwd.includes('proj & del'), 'path should remain a literal cwd value');
assert.ok(parsed.kwargs.cwd.includes('C:'), 'windows drive prefix should be preserved');
assert.ok(Object.prototype.hasOwnProperty.call(parsed.kwargs, 'creationflags'));
})) passed++; else failed++;
if (test('launch_terminal rejects missing or non-directory paths', () => {
const output = runPython(`
import importlib.util, json
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
try:
module.launch_terminal('/definitely/not/a/real/ecc/path')
except ValueError as exc:
print(json.dumps({'error': str(exc)}))
`);
const parsed = JSON.parse(output);
assert.ok(parsed.error.includes('Path is not a valid directory'));
})) passed++; else failed++;
if (test('maximize_window falls back to Linux zoom attribute when zoomed state is unsupported', () => {
const output = runPython(`
import importlib.util, json
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class FakeWindow:
def __init__(self):
self.calls = []
def state(self, value):
self.calls.append(['state', value])
raise RuntimeError('bad argument "zoomed"')
def attributes(self, name, value):
self.calls.append(['attributes', name, value])
original = module.platform.system
module.platform.system = lambda: 'Linux'
try:
window = FakeWindow()
module.maximize_window(window)
finally:
module.platform.system = original
print(json.dumps(window.calls))
`);
const parsed = JSON.parse(output);
assert.deepStrictEqual(parsed, [
['state', 'zoomed'],
['attributes', '-zoomed', true],
]);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+265
View File
@@ -0,0 +1,265 @@
/**
* Tests for scripts/ecc.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
function runCli(args, options = {}) {
const envOverrides = {
...(options.env || {}),
};
if (typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)) {
envOverrides.USERPROFILE = envOverrides.HOME;
}
if (typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)) {
envOverrides.HOME = envOverrides.USERPROFILE;
}
return spawnSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
cwd: options.cwd || process.cwd(),
maxBuffer: 10 * 1024 * 1024,
env: {
...process.env,
...envOverrides,
},
});
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function parseJson(stdout) {
return JSON.parse(stdout.trim());
}
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 ecc.js ===\n');
let passed = 0;
let failed = 0;
const tests = [
['shows top-level help', () => {
const result = runCli(['--help']);
assert.strictEqual(result.status, 0);
assert.match(result.stdout, /ECC selective-install CLI/);
assert.match(result.stdout, /catalog/);
assert.match(result.stdout, /list-installed/);
assert.match(result.stdout, /doctor/);
assert.match(result.stdout, /auto-update/);
assert.match(result.stdout, /consult/);
assert.match(result.stdout, /control-pane/);
assert.match(result.stdout, /loop-status/);
assert.match(result.stdout, /work-items/);
assert.match(result.stdout, /platform-audit/);
assert.match(result.stdout, /security-ioc-scan/);
}],
['delegates explicit install command', () => {
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.dryRun, true);
assert.strictEqual(payload.plan.mode, 'legacy-compat');
assert.deepStrictEqual(payload.plan.legacyLanguages, ['typescript']);
assert.ok(payload.plan.selectedModuleIds.includes('framework-language'));
}],
['routes implicit top-level args to install', () => {
const result = runCli(['--dry-run', '--json', 'typescript']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.dryRun, true);
assert.strictEqual(payload.plan.mode, 'legacy-compat');
assert.deepStrictEqual(payload.plan.legacyLanguages, ['typescript']);
assert.ok(payload.plan.selectedModuleIds.includes('framework-language'));
}],
['delegates plan command', () => {
const result = runCli(['plan', '--list-profiles', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.ok(Array.isArray(payload.profiles));
assert.ok(payload.profiles.length > 0);
}],
['delegates catalog command', () => {
const result = runCli(['catalog', 'show', 'framework:nextjs', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.id, 'framework:nextjs');
assert.deepStrictEqual(payload.moduleIds, ['framework-language']);
}],
['delegates consult command', () => {
const result = runCli(['consult', 'security', 'reviews', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.consult.v1');
assert.strictEqual(payload.matches[0].componentId, 'capability:security');
}],
['supports help for the control-pane subcommand', () => {
const result = runCli(['help', 'control-pane']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage:/);
assert.match(result.stdout, /control-pane/);
}],
['delegates lifecycle commands', () => {
const homeDir = createTempDir('ecc-cli-home-');
const projectRoot = createTempDir('ecc-cli-project-');
const result = runCli(['list-installed', '--json'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.records, []);
}],
['delegates auto-update command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const projectRoot = createTempDir('ecc-cli-project-');
const result = runCli(['auto-update', '--dry-run', '--json'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.results, []);
}],
['delegates session-inspect command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
'# ECC Session\n\n**Branch:** feat/ecc-cli\n'
);
const result = runCli(['session-inspect', 'claude:latest'], {
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.adapterId, 'claude-history');
assert.strictEqual(payload.workers[0].branch, 'feat/ecc-cli');
}],
['delegates loop-status command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const transcriptDir = path.join(homeDir, '.claude', 'projects', '-tmp-ecc');
fs.mkdirSync(transcriptDir, { recursive: true });
fs.writeFileSync(
path.join(transcriptDir, 'session-loop.jsonl'),
JSON.stringify({
timestamp: '2026-04-30T09:00:00.000Z',
sessionId: 'session-loop',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id: 'toolu_loop',
name: 'ScheduleWakeup',
input: { delaySeconds: 300 },
},
],
},
}) + '\n'
);
const result = runCli(['loop-status', '--home', homeDir, '--now', '2026-04-30T10:00:00.000Z', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.loop-status.v1');
assert.strictEqual(payload.sessions[0].sessionId, 'session-loop');
}],
['supports help for a subcommand', () => {
const result = runCli(['help', 'repair']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/repair\.js/);
}],
['supports help for the auto-update subcommand', () => {
const result = runCli(['help', 'auto-update']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/auto-update\.js/);
}],
['supports help for the catalog subcommand', () => {
const result = runCli(['help', 'catalog']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /node scripts\/catalog\.js show <component-id>/);
}],
['supports help for the consult subcommand', () => {
const result = runCli(['help', 'consult']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
}],
['supports help for the work-items subcommand', () => {
const result = runCli(['help', 'work-items']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /node scripts\/work-items\.js upsert/);
}],
['supports help for the platform-audit subcommand', () => {
const result = runCli(['help', 'platform-audit']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/platform-audit\.js/);
}],
['supports help for the security-ioc-scan subcommand', () => {
const result = runCli(['help', 'security-ioc-scan']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/ci\/scan-supply-chain-iocs\.js/);
}],
['delegates security-ioc-scan command', () => {
const projectRoot = createTempDir('ecc-cli-ioc-scan-');
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ dependencies: { leftpad: '1.0.0' } }, null, 2)
);
const result = runCli(['security-ioc-scan', '--root', projectRoot, '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.findings, []);
}],
['fails on unknown commands instead of treating them as installs', () => {
const result = runCli(['bogus']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /Unknown command: bogus/);
}],
['fails on unknown help subcommands', () => {
const result = runCli(['help', 'bogus']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /Unknown command: bogus/);
}],
];
for (const [name, fn] of tests) {
if (runTest(name, fn)) {
passed += 1;
} else {
failed += 1;
}
}
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
main();
+136
View File
@@ -0,0 +1,136 @@
/**
* Tests for scripts/gemini-adapt-agents.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'gemini-adapt-agents.js');
function run(args = [], options = {}) {
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
cwd: options.cwd,
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-gemini-adapt-'));
}
function cleanupTempDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeAgent(dirPath, name, body) {
fs.mkdirSync(dirPath, { recursive: true });
fs.writeFileSync(path.join(dirPath, name), body);
}
function runTests() {
console.log('\n=== Testing gemini-adapt-agents.js ===\n');
let passed = 0;
let failed = 0;
if (test('shows help with an explicit help flag', () => {
const result = run(['--help']);
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Adapt ECC agent frontmatter for Gemini CLI'));
assert.ok(result.stdout.includes('Usage:'));
})) passed++; else failed++;
if (test('adapts Claude Code tool names and strips unsupported color metadata', () => {
const tempDir = createTempDir();
const agentsDir = path.join(tempDir, '.gemini', 'agents');
try {
writeAgent(
agentsDir,
'gan-planner.md',
[
'---',
'name: gan-planner',
'description: Planner agent',
'tools: [Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__resolve-library-id]',
'model: opus',
'color: purple',
'---',
'',
'Body'
].join('\n')
);
const result = run([agentsDir]);
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Updated 1 agent file(s)'));
const updated = fs.readFileSync(path.join(agentsDir, 'gan-planner.md'), 'utf8');
assert.ok(updated.includes('tools: ["read_file", "write_file", "replace", "run_shell_command", "grep_search", "glob", "google_web_search", "web_fetch", "mcp_context7_resolve_library_id"]'));
assert.ok(!updated.includes('color: purple'));
} finally {
cleanupTempDir(tempDir);
}
})) passed++; else failed++;
if (test('defaults to the cwd .gemini/agents directory', () => {
const tempDir = createTempDir();
const agentsDir = path.join(tempDir, '.gemini', 'agents');
try {
writeAgent(
agentsDir,
'architect.md',
[
'---',
'name: architect',
'description: Architect agent',
'tools: ["Read", "Grep", "Glob"]',
'model: opus',
'---',
'',
'Body'
].join('\n')
);
const result = run([], { cwd: tempDir });
assert.strictEqual(result.code, 0, result.stderr);
const updated = fs.readFileSync(path.join(agentsDir, 'architect.md'), 'utf8');
assert.ok(updated.includes('tools: ["read_file", "grep_search", "glob"]'));
} finally {
cleanupTempDir(tempDir);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+253
View File
@@ -0,0 +1,253 @@
/**
* Tests for scripts/github-coordination.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const { createStateStore } = require('../../scripts/lib/state-store');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'github-coordination.js');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeGhShim(rootDir, responses) {
const shimPath = path.join(rootDir, 'gh-shim.js');
const logPath = path.join(rootDir, 'gh-calls.jsonl');
fs.writeFileSync(
shimPath,
`
const fs = require('fs');
const responses = ${JSON.stringify(responses)};
const args = process.argv.slice(2);
const key = args.join(' ');
const logPath = process.env.ECC_GH_SHIM_LOG;
if (logPath) {
fs.appendFileSync(logPath, JSON.stringify({ args }, null, 0) + '\\n');
}
if (Object.prototype.hasOwnProperty.call(responses, key)) {
process.stdout.write(JSON.stringify(responses[key]));
process.exit(0);
}
if (args[0] === 'issue' && (args[1] === 'edit' || args[1] === 'comment')) {
process.stdout.write('{}');
process.exit(0);
}
console.error('Unexpected gh args: ' + key);
process.exit(3);
`
);
return { shimPath, logPath };
}
function run(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env: {
...process.env,
...(options.env || {})
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function parseJson(stdout) {
return JSON.parse(stdout.trim());
}
async function test(name, fn) {
try {
await fn();
process.stdout.write(` PASS ${name}\n`);
return true;
} catch (error) {
process.stdout.write(` FAIL ${name}\n`);
process.stdout.write(` Error: ${error.message}\n`);
return false;
}
}
async function readStore(dbPath) {
const store = await createStateStore({ dbPath });
try {
return store.listWorkItems({ limit: 20 });
} finally {
store.close();
}
}
async function runTests() {
process.stdout.write('\n=== Testing github-coordination.js ===\n\n');
let passed = 0;
let failed = 0;
if (
await test('claims an epic issue, updates GitHub state, and caches a work item', async () => {
const rootDir = createTempDir('github-coordination-claim-');
const dbPath = path.join(rootDir, 'state.db');
try {
const epicBody = ['# Ship GitHub-native coordination', '', 'We want deterministic epic state.', '', '## Tasks', '- [ ] Claim the epic', '- [ ] Validate the epic'].join('\n');
const issueView = {
number: 12,
title: 'Ship GitHub-native coordination',
body: epicBody,
url: 'https://github.com/affaan-m/ECC/issues/12',
state: 'OPEN',
labels: [{ name: 'epic' }],
author: { login: 'maintainer' },
updatedAt: '2026-06-01T12:00:00Z'
};
const shim = writeGhShim(rootDir, {
'issue view 12 --repo affaan-m/ECC --json number,title,body,url,state,labels,author,updatedAt,assignees': issueView
});
const result = run(['claim', '12', '--repo', 'affaan-m/ECC', '--actor', 'codex', '--db', dbPath, '--json'], {
cwd: rootDir,
env: {
ECC_GH_SHIM: shim.shimPath,
ECC_GH_SHIM_LOG: shim.logPath
}
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.status, 'claimed');
assert.strictEqual(payload.owner, 'codex');
assert.strictEqual(payload.project.state, 'in-progress');
const logEntries = fs
.readFileSync(shim.logPath, 'utf8')
.trim()
.split(/\r?\n/)
.map(line => JSON.parse(line));
assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'edit'));
assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'comment'));
const stored = await readStore(dbPath);
const epicItem = stored.items.find(item => item.source === 'github-epic');
assert.ok(epicItem, 'expected github epic work item');
assert.strictEqual(epicItem.status, 'in-progress');
assert.strictEqual(epicItem.metadata.coordination.status, 'claimed');
assert.strictEqual(epicItem.metadata.coordination.owner, 'codex');
} finally {
cleanup(rootDir);
}
})
)
passed++;
else failed++;
if (
await test('unblocks an epic when dependencies are closed', async () => {
const rootDir = createTempDir('github-coordination-unblock-');
const dbPath = path.join(rootDir, 'state.db');
try {
const blockedBody = [
'# Release readiness',
'',
'Dependencies: #2',
'',
'<!-- ecc-coordination:start -->',
'```json',
JSON.stringify(
{
schemaVersion: 'ecc.github.coordination.v1',
kind: 'epic',
status: 'blocked',
owner: 'codex',
branch: 'feat/release-readiness',
validation: 'pending',
review: 'requested',
project: { state: 'blocked', fields: {} },
dependencies: [2],
tasks: [{ title: 'Check release checklist', done: false }],
labels: ['epic', 'coordination:blocked'],
lastAction: 'claim',
lastActionAt: '2026-06-01T13:00:00Z',
lastSyncAt: '2026-06-01T13:00:00Z',
notes: null
},
null,
2
),
'```',
'<!-- ecc-coordination:end -->'
].join('\n');
const openIssue = {
number: 1,
title: 'Release readiness',
body: blockedBody,
url: 'https://github.com/affaan-m/ECC/issues/1',
state: 'OPEN',
labels: [{ name: 'epic' }, { name: 'coordination:blocked' }],
author: { login: 'codex' },
updatedAt: '2026-06-01T13:00:00Z'
};
const closedDependency = {
number: 2,
title: 'Release prerequisite',
body: '# Release prerequisite',
url: 'https://github.com/affaan-m/ECC/issues/2',
state: 'CLOSED',
labels: [{ name: 'blocked-by-release' }],
author: { login: 'maintainer' },
updatedAt: '2026-06-01T10:00:00Z'
};
const shim = writeGhShim(rootDir, {
'issue list --repo affaan-m/ECC --state all --limit 100 --json number,title,body,url,state,labels,author,updatedAt,assignees': [openIssue, closedDependency],
'issue view 1 --repo affaan-m/ECC --json number,title,body,url,state,labels,author,updatedAt,assignees': openIssue
});
const result = run(['unblock', '--repo', 'affaan-m/ECC', '--db', dbPath, '--json'], {
cwd: rootDir,
env: {
ECC_GH_SHIM: shim.shimPath,
ECC_GH_SHIM_LOG: shim.logPath
}
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.count, 1);
assert.strictEqual(payload.items[0].status, 'ready');
const logEntries = fs
.readFileSync(shim.logPath, 'utf8')
.trim()
.split(/\r?\n/)
.map(line => JSON.parse(line));
assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'edit'));
assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'comment'));
const stored = await readStore(dbPath);
const epicItem = stored.items.find(item => item.source === 'github-epic');
assert.ok(epicItem, 'expected github epic work item');
assert.strictEqual(epicItem.metadata.coordination.status, 'ready');
} finally {
cleanup(rootDir);
}
})
)
passed++;
else failed++;
process.stdout.write(`\nResults: Passed: ${passed}, Failed: ${failed}\n`);
process.exit(failed > 0 ? 1 : 0);
}
runTests().catch(error => {
console.error(error);
process.exit(1);
});
+667
View File
@@ -0,0 +1,667 @@
/**
* Tests for scripts/harness-audit.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'harness-audit.js');
const { parseArgs, findPluginInstall, compareVersionDesc } = require(SCRIPT);
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function buildEnv(options = {}) {
const userProfile = options.userProfile || options.homeDir || process.env.USERPROFILE;
const env = {
...process.env,
USERPROFILE: userProfile,
};
if (Object.prototype.hasOwnProperty.call(options, 'homeDir')) {
env.HOME = options.homeDir;
} else {
env.HOME = process.env.HOME;
}
return env;
}
function run(args = [], options = {}) {
const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env: buildEnv(options),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return stdout;
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env: buildEnv(options),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing harness-audit.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts supported forms and rejects invalid arguments', () => {
const rootDir = createTempDir('harness-audit-args-root-');
try {
assert.strictEqual(parseArgs(['node', 'script', '--help']).help, true);
assert.strictEqual(parseArgs(['node', 'script', '-h']).help, true);
const spaced = parseArgs(['node', 'script', '--format', 'json', '--scope', 'skills', '--root', rootDir]);
assert.strictEqual(spaced.format, 'json');
assert.strictEqual(spaced.scope, 'skills');
assert.strictEqual(spaced.root, path.resolve(rootDir));
const equals = parseArgs(['node', 'script', '--format=json', '--scope=hooks', `--root=${rootDir}`]);
assert.strictEqual(equals.format, 'json');
assert.strictEqual(equals.scope, 'hooks');
assert.strictEqual(equals.root, path.resolve(rootDir));
assert.strictEqual(parseArgs(['node', 'script', 'commands']).scope, 'commands');
assert.strictEqual(parseArgs(['node', 'script', '--scope']).scope, 'repo');
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format: xml/);
assert.throws(() => parseArgs(['node', 'script', '--scope', 'bad-scope']), /Invalid scope: bad-scope/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument: --unknown/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('cli help exits cleanly and invalid cli args exit with stderr', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/harness-audit.js'));
assert.ok(help.stdout.includes('Deterministic harness audit'));
const invalid = runProcess(['--format', 'xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.ok(invalid.stderr.includes('Error: Invalid format: xml. Use text or json.'));
})) passed++; else failed++;
if (test('json output is deterministic between runs', () => {
const first = run(['repo', '--format', 'json']);
const second = run(['repo', '--format', 'json']);
assert.strictEqual(first, second);
})) passed++; else failed++;
if (test('report includes bounded scores and fixed categories', () => {
const parsed = JSON.parse(run(['repo', '--format', 'json']));
assert.strictEqual(parsed.deterministic, true);
assert.strictEqual(parsed.rubric_version, '2026-05-19');
assert.strictEqual(parsed.target_mode, 'repo');
assert.ok(parsed.overall_score >= 0);
assert.ok(parsed.max_score > 0);
assert.ok(parsed.overall_score <= parsed.max_score);
const categoryNames = Object.keys(parsed.categories);
assert.ok(categoryNames.includes('Tool Coverage'));
assert.ok(categoryNames.includes('Context Efficiency'));
assert.ok(categoryNames.includes('Quality Gates'));
assert.ok(categoryNames.includes('Memory Persistence'));
assert.ok(categoryNames.includes('Eval Coverage'));
assert.ok(categoryNames.includes('Security Guardrails'));
assert.ok(categoryNames.includes('Cost Efficiency'));
assert.ok(categoryNames.includes('GitHub Integration'));
})) passed++; else failed++;
if (test('report exposes applicable_categories and category_count', () => {
const parsed = JSON.parse(run(['repo', '--format', 'json']));
assert.ok(Array.isArray(parsed.applicable_categories), 'applicable_categories must be an array');
assert.ok(parsed.applicable_categories.length > 0);
assert.strictEqual(parsed.category_count, parsed.applicable_categories.length);
for (const name of parsed.applicable_categories) {
assert.ok(parsed.categories[name].max > 0, `${name} must have max > 0 to be applicable`);
}
})) passed++; else failed++;
if (test('GitHub Integration category scores against a fully-wired consumer fixture', () => {
const homeDir = createTempDir('harness-audit-home-gh-');
const projectRoot = createTempDir('harness-audit-project-gh-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'PULL_REQUEST_TEMPLATE.md'), '# PR\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'ISSUE_TEMPLATE', 'bug.md'), '# Bug\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'CODEOWNERS'), '* @owner\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'dependabot.yml'), 'version: 2\n');
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({ name: 'gh-test' }));
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
const github = parsed.categories['GitHub Integration'];
assert.ok(github, 'GitHub Integration category must exist');
assert.strictEqual(github.score, 10, `GitHub Integration should score 10/10, got ${github.score}`);
assert.strictEqual(github.earned, github.max);
assert.ok(parsed.applicable_categories.includes('GitHub Integration'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('provider categories are omitted unless a marker is present', () => {
const homeDir = createTempDir('harness-audit-home-no-provider-');
const projectRoot = createTempDir('harness-audit-project-no-provider-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({ name: 'p' }));
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
assert.ok(!parsed.applicable_categories.includes('Vercel Integration'));
const vercel = parsed.categories['Vercel Integration'];
assert.ok(!vercel || vercel.max === 0, 'Vercel Integration should not contribute when no marker');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('Vercel Integration category scores when vercel.json present', () => {
const homeDir = createTempDir('harness-audit-home-vercel-');
const projectRoot = createTempDir('harness-audit-project-vercel-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'vercel.json'), '{}\n');
fs.writeFileSync(path.join(projectRoot, '.env.example'), 'VERCEL_TOKEN=\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'deploy.yml'), 'uses: amondnet/vercel-action@v25\n');
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'p', scripts: { build: 'next build', deploy: 'vercel deploy' } })
);
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
const vercel = parsed.categories['Vercel Integration'];
assert.ok(vercel, 'Vercel Integration category must exist when vercel.json present');
assert.ok(vercel.max > 0);
assert.ok(parsed.applicable_categories.includes('Vercel Integration'));
assert.strictEqual(vercel.score, 10, `Vercel should score 10/10 with full wiring, got ${vercel.score}`);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('detector map: Netlify, Cloudflare, Fly each trigger their category', () => {
const homeDir = createTempDir('harness-audit-home-multi-');
function probe(markerFile, markerContents, expectedCategory) {
const root = createTempDir('harness-audit-project-multi-');
try {
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'p' }));
fs.writeFileSync(path.join(root, markerFile), markerContents);
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: root, homeDir }));
assert.ok(
parsed.applicable_categories.includes(expectedCategory),
`${markerFile} should activate ${expectedCategory}`
);
} finally {
cleanup(root);
}
}
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
probe('netlify.toml', '[build]\n', 'Netlify Integration');
probe('wrangler.toml', 'name = "p"\n', 'Cloudflare Integration');
probe('fly.toml', 'app = "p"\n', 'Fly Integration');
} finally {
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('max_score reflects only applicable categories', () => {
const homeDir = createTempDir('harness-audit-home-max-');
const noVercel = createTempDir('harness-audit-project-max-novercel-');
const withVercel = createTempDir('harness-audit-project-max-vercel-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.writeFileSync(path.join(noVercel, 'package.json'), JSON.stringify({ name: 'p' }));
fs.writeFileSync(path.join(withVercel, 'package.json'), JSON.stringify({ name: 'p' }));
fs.writeFileSync(path.join(withVercel, 'vercel.json'), '{}\n');
const noVercelParsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: noVercel, homeDir }));
const withVercelParsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: withVercel, homeDir }));
assert.ok(
withVercelParsed.max_score > noVercelParsed.max_score,
`with-vercel max_score (${withVercelParsed.max_score}) should exceed no-vercel (${noVercelParsed.max_score})`
);
} finally {
cleanup(homeDir);
cleanup(noVercel);
cleanup(withVercel);
}
})) passed++; else failed++;
if (test('non-git directory does not crash the script', () => {
const homeDir = createTempDir('harness-audit-home-bare-');
const bare = createTempDir('harness-audit-project-bare-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.writeFileSync(path.join(bare, 'package.json'), JSON.stringify({ name: 'p' }));
const output = run(['repo', '--format', 'json'], { cwd: bare, homeDir });
const parsed = JSON.parse(output);
assert.ok(parsed.overall_score >= 0);
assert.ok(parsed.max_score > 0);
} finally {
cleanup(homeDir);
cleanup(bare);
}
})) passed++; else failed++;
if (test('scope filtering changes max score and check list', () => {
const full = JSON.parse(run(['repo', '--format', 'json']));
const scoped = JSON.parse(run(['hooks', '--format', 'json']));
assert.strictEqual(scoped.scope, 'hooks');
assert.ok(scoped.max_score < full.max_score);
assert.ok(scoped.checks.length < full.checks.length);
assert.ok(scoped.checks.every(check => check.path.includes('hooks') || check.path.includes('scripts/hooks')));
})) passed++; else failed++;
if (test('text format includes summary header', () => {
const output = run(['repo']);
assert.ok(output.includes('Harness Audit (repo, repo):'));
assert.ok(output.includes('Top 3 Actions:') || output.includes('Checks:'));
})) passed++; else failed++;
if (test('detects repo mode from structural markers when package name differs', () => {
const projectRoot = createTempDir('harness-audit-structural-repo-');
try {
fs.mkdirSync(path.join(projectRoot, 'scripts'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.claude-plugin'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'agents'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'skills'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'scripts', 'harness-audit.js'), '#!/usr/bin/env node\n');
fs.writeFileSync(path.join(projectRoot, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'ecc' }, null, 2));
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'forked-harness', scripts: { test: 'node scripts/validate-commands.js && node tests/run-all.js' } }, null, 2)
);
const parsed = JSON.parse(run(['--format=json', `--root=${projectRoot}`]));
assert.strictEqual(parsed.target_mode, 'repo');
assert.strictEqual(parsed.root_dir, path.resolve(projectRoot));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('audits consumer projects from cwd instead of the ECC repo root', () => {
const homeDir = createTempDir('harness-audit-home-');
const projectRoot = createTempDir('harness-audit-project-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'ecc', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n');
fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2));
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n');
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2));
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2)
);
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
assert.strictEqual(parsed.target_mode, 'consumer');
assert.strictEqual(parsed.root_dir, fs.realpathSync(projectRoot));
assert.ok(parsed.overall_score > 0, 'Consumer project should receive non-zero score when harness signals exist');
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass));
assert.ok(parsed.checks.every(check => !check.path.startsWith('agents/') && !check.path.startsWith('skills/')));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('scores empty consumer projects without plugin or harness signals as failing checks', () => {
const homeDir = createTempDir('harness-audit-empty-home-');
const projectRoot = createTempDir('harness-audit-empty-project-');
try {
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
assert.strictEqual(parsed.target_mode, 'consumer');
assert.strictEqual(parsed.overall_score, 0);
assert.ok(parsed.max_score > 0);
assert.strictEqual(parsed.top_actions.length, 3);
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && !check.pass));
assert.ok(parsed.checks.some(check => check.id === 'consumer-project-overrides' && !check.pass));
assert.ok(parsed.checks.some(check => check.id === 'consumer-secret-hygiene' && !check.pass));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('prints no top actions when consumer checks all pass', () => {
const homeDir = createTempDir('harness-audit-passing-home-');
const projectRoot = createTempDir('harness-audit-passing-project-');
try {
fs.mkdirSync(path.join(projectRoot, '.claude', 'plugins', 'ecc@ecc'), { recursive: true });
fs.writeFileSync(
path.join(projectRoot, '.claude', 'plugins', 'ecc@ecc', 'plugin.json'),
JSON.stringify({ name: 'ecc' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows', 'nested'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'docs', 'adr'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'evals'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'src'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, '.claude', 'hooks.json'), JSON.stringify({ hooks: [] }, null, 2));
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.local.json'), JSON.stringify({ local: true }, null, 2));
fs.writeFileSync(path.join(projectRoot, 'CLAUDE.md'), '# Consumer instructions\n');
fs.writeFileSync(path.join(projectRoot, 'src', 'app.spec.ts'), 'test placeholder\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'nested', 'ci.yaml'), 'name: ci\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'PULL_REQUEST_TEMPLATE.md'), '# PR\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'ISSUE_TEMPLATE', 'bug.md'), '# Bug\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'CODEOWNERS'), '* @owner\n');
fs.writeFileSync(path.join(projectRoot, 'docs', 'adr', '001.md'), '# Record\n');
fs.writeFileSync(path.join(projectRoot, 'evals', 'smoke.json'), '{}\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'dependabot.yml'), 'version: 2\n');
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env.local\n');
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'passing-consumer', scripts: {} }, null, 2)
);
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
assert.strictEqual(parsed.target_mode, 'consumer');
assert.strictEqual(parsed.overall_score, parsed.max_score);
const text = run(['repo'], { cwd: projectRoot, homeDir });
assert.ok(text.includes(`Harness Audit (repo, consumer): ${parsed.max_score}/${parsed.max_score}`));
assert.ok(text.includes('Checks: 16 total, 0 failing'));
assert.ok(!text.includes('Top 3 Actions:'));
const scopedText = run(['agents'], { cwd: projectRoot, homeDir });
assert.ok(scopedText.includes('Harness Audit (agents, consumer):'));
assert.ok(scopedText.includes('Checks: 1 total, 0 failing'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('detects marketplace-installed Claude plugins under home marketplaces/', () => {
const homeDir = createTempDir('harness-audit-marketplace-home-');
const projectRoot = createTempDir('harness-audit-marketplace-project-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'everything-claude-code' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n');
fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2));
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n');
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2));
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2)
);
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('detects marketplace-installed Claude plugins under project marketplaces/', () => {
const homeDir = createTempDir('harness-audit-marketplace-home-');
const projectRoot = createTempDir('harness-audit-marketplace-project-');
try {
fs.mkdirSync(path.join(projectRoot, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(projectRoot, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'everything-claude-code' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n');
fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2));
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n');
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2));
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2)
);
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('detects marketplace-installed Claude plugins from USERPROFILE fallback on Windows-style setups', () => {
const homeDir = createTempDir('harness-audit-marketplace-home-');
const projectRoot = createTempDir('harness-audit-marketplace-project-');
try {
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'everything-claude-code' }, null, 2)
);
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n');
fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2));
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n');
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n');
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2));
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2)
);
const parsed = JSON.parse(run(['repo', '--format', 'json'], {
cwd: projectRoot,
homeDir: '',
userProfile: homeDir,
}));
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('detects Claude plugin installs from installed_plugins.json', () => {
const homeDir = createTempDir('harness-audit-manifest-home-');
const projectRoot = createTempDir('harness-audit-manifest-project-');
const pluginsDir = path.join(homeDir, '.claude', 'plugins');
const installRoot = path.join(pluginsDir, 'cache', 'everything-claude-code', 'ecc', '2.0.0');
try {
fs.mkdirSync(path.join(installRoot, '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(installRoot, '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc', version: '2.0.0' }, null, 2)
);
fs.writeFileSync(
path.join(pluginsDir, 'installed_plugins.json'),
JSON.stringify({
plugins: {
'ecc@everything-claude-code': [
{ installPath: path.join('cache', 'everything-claude-code', 'ecc', '2.0.0') },
],
},
}, null, 2)
);
const originalHome = process.env.HOME;
process.env.HOME = homeDir;
try {
const found = findPluginInstall(projectRoot);
assert.ok(found);
assert.ok(found.includes(`${path.sep}cache${path.sep}everything-claude-code${path.sep}ecc${path.sep}2.0.0${path.sep}`));
} finally {
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
}
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('detects newest Claude plugin install from cache marketplace layout', () => {
const homeDir = createTempDir('harness-audit-cache-home-');
const projectRoot = createTempDir('harness-audit-cache-project-');
const pluginRoot = path.join(homeDir, '.claude', 'plugins', 'cache', 'everything-claude-code', 'ecc');
try {
for (const version of ['1.8.0', '1.10.0']) {
fs.mkdirSync(path.join(pluginRoot, version, '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(pluginRoot, version, '.claude-plugin', 'plugin.json'),
JSON.stringify({ name: 'ecc', version }, null, 2)
);
}
const originalHome = process.env.HOME;
process.env.HOME = homeDir;
try {
const found = findPluginInstall(projectRoot);
assert.ok(found);
assert.ok(found.includes(`${path.sep}1.10.0${path.sep}`), `expected newest version, got ${found}`);
} finally {
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
}
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('compareVersionDesc orders numeric version components', () => {
const versions = ['1.8.0', '1.10.0', '1.9.0', '2.0.0'].sort(compareVersionDesc);
assert.deepStrictEqual(versions, ['2.0.0', '1.10.0', '1.9.0', '1.8.0']);
assert.doesNotThrow(() => compareVersionDesc('1.0.0-rc.1', '1.0.0'));
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+962
View File
@@ -0,0 +1,962 @@
/**
* Tests for scripts/install-apply.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const DEFAULT_INSTALL_APPLY_TIMEOUT_MS = process.platform === 'win32' ? 30000 : 10000;
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function run(args = [], options = {}) {
const homeDir = options.homeDir || process.env.HOME;
const env = {
...process.env,
HOME: homeDir,
USERPROFILE: homeDir,
...(options.env || {}),
};
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: options.timeout || DEFAULT_INSTALL_APPLY_TIMEOUT_MS,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || error.message || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing install-apply.js ===\n');
let passed = 0;
let failed = 0;
if (test('shows help with --help', () => {
const result = run(['--help']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Usage:'));
assert.ok(result.stdout.includes('--dry-run'));
assert.ok(result.stdout.includes('--profile <name>'));
assert.ok(result.stdout.includes('--modules <id,id,...>'));
})) passed++; else failed++;
if (test('rejects mixing legacy languages with manifest profile flags', () => {
const result = run(['--profile', 'core', 'typescript']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('cannot be combined'));
})) passed++; else failed++;
if (test('installs Claude rules and writes install-state', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'typescript', 'testing.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'lib', 'utils.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'coding-standards', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json')));
const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json');
const state = readJson(statePath);
assert.strictEqual(state.target.id, 'claude-home');
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.legacyMode, true);
assert.deepStrictEqual(state.request.modules, []);
assert.ok(state.resolution.selectedModules.includes('rules-core'));
assert.ok(state.resolution.selectedModules.includes('framework-language'));
assert.ok(
state.operations.some(operation => (
operation.destinationPath === path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')
)),
'Should record common rule file operation'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('rewrites namespaced skill links to the ecc/ rules path (#2340)', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
const skillPath = path.join(claudeRoot, 'skills', 'ecc', 'react-patterns', 'SKILL.md');
assert.ok(fs.existsSync(skillPath), 'react-patterns SKILL.md should be installed');
const content = fs.readFileSync(skillPath, 'utf8');
assert.ok(
content.includes('../../../rules/ecc/react/'),
'source-relative rules link should be rewritten for the ecc/ namespace'
);
assert.ok(
!content.includes('](../../rules/'),
'no un-namespaced ](../../rules/ links should remain'
);
// The rewritten link must resolve to a file that actually exists on disk.
const linkTarget = path.join(
path.dirname(skillPath),
'../../../rules/ecc/react/hooks.md'
);
assert.ok(fs.existsSync(linkTarget), 'rewritten link target should exist');
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs Cursor configs and writes install-state', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-coding-style.mdc')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript-testing.mdc')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'README.mdc')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'ecc-architect.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'mcp.json')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'scripts', 'lib', 'utils.js')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'coding-standards', 'SKILL.md')));
const hooksConfig = readJson(path.join(projectDir, '.cursor', 'hooks.json'));
const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json'));
assert.strictEqual(hooksConfig.version, 1);
assert.ok(hooksConfig.hooks.sessionStart, 'Should keep Cursor sessionStart hooks');
assert.ok(mcpConfig.mcpServers['chrome-devtools'], 'Should install shared MCP servers into Cursor');
const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json');
const state = readJson(statePath);
const normalizedProjectDir = fs.realpathSync(projectDir);
assert.strictEqual(state.target.id, 'cursor-project');
assert.strictEqual(state.target.root, path.join(normalizedProjectDir, '.cursor'));
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.legacyMode, true);
assert.ok(state.resolution.selectedModules.includes('framework-language'));
assert.ok(
state.operations.some(operation => (
operation.destinationPath === path.join(normalizedProjectDir, '.cursor', 'commands', 'plan.md')
)),
'Should record manifest command file copy operation'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs Cursor MCP config by merging bundled servers into an existing mcp.json', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const cursorRoot = path.join(projectDir, '.cursor');
fs.mkdirSync(cursorRoot, { recursive: true });
fs.writeFileSync(path.join(cursorRoot, 'mcp.json'), JSON.stringify({
mcpServers: {
custom: {
command: 'node',
args: ['custom-mcp.js'],
},
},
}, null, 2));
const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json'));
assert.ok(mcpConfig.mcpServers.custom, 'Should preserve existing custom Cursor MCP servers');
assert.ok(mcpConfig.mcpServers['chrome-devtools'], 'Should merge the bundled chrome-devtools MCP server');
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs Antigravity configs and writes install-state', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'antigravity', 'typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'typescript-testing.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
const statePath = path.join(projectDir, '.agent', 'ecc-install-state.json');
const state = readJson(statePath);
assert.strictEqual(state.target.id, 'antigravity-project');
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.legacyMode, true);
assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']);
assert.ok(
state.operations.some(operation => (
operation.destinationPath.endsWith(path.join('.agent', 'workflows', 'plan.md'))
)),
'Should record manifest command file copy operation'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs JoyCode profile through managed install-state', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'joycode', '--profile', 'minimal'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'rules', 'common-coding-style.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.joycode', 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'mcp-configs', 'mcp-servers.json')));
assert.ok(!fs.existsSync(path.join(projectDir, '.joycode', 'hooks')));
const statePath = path.join(projectDir, '.joycode', 'ecc-install-state.json');
const state = readJson(statePath);
assert.strictEqual(state.target.id, 'joycode-project');
assert.deepStrictEqual(state.request.modules, []);
assert.strictEqual(state.request.profile, 'minimal');
assert.ok(state.resolution.selectedModules.includes('workflow-quality'));
assert.ok(
state.operations.some(operation => (
operation.destinationPath.endsWith(path.join('.joycode', 'skills', 'tdd-workflow', 'SKILL.md'))
)),
'Should record JoyCode skill file operation'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs Qwen profile through managed home install-state', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'qwen', '--profile', 'minimal'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'QWEN.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'mcp-configs', 'mcp-servers.json')));
assert.ok(!fs.existsSync(path.join(homeDir, '.qwen', 'hooks')));
const statePath = path.join(homeDir, '.qwen', 'ecc-install-state.json');
const state = readJson(statePath);
assert.strictEqual(state.target.id, 'qwen-home');
assert.deepStrictEqual(state.request.modules, []);
assert.strictEqual(state.request.profile, 'minimal');
assert.ok(state.resolution.selectedModules.includes('workflow-quality'));
assert.ok(
state.operations.some(operation => (
operation.destinationPath.endsWith(path.join('.qwen', 'skills', 'tdd-workflow', 'SKILL.md'))
)),
'Should record Qwen skill file operation'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('supports dry-run without mutating the target project', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'cursor', '--dry-run', 'typescript'], {
cwd: projectDir,
homeDir,
});
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Dry-run install plan'));
assert.ok(result.stdout.includes('Mode: legacy-compat'));
assert.ok(result.stdout.includes('Legacy languages: typescript'));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'ecc-install-state.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('supports manifest profile dry-runs through the installer', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--profile', 'core', '--dry-run'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Mode: manifest'));
assert.ok(result.stdout.includes('Profile: core'));
assert.ok(result.stdout.includes('Included components: (none)'));
assert.ok(result.stdout.includes('Selected modules: rules-core, agents-core, commands-core, hooks-runtime, platform-configs, workflow-quality'));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('full profile dry-runs include delivery-gate in the install plan', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--profile', 'full', '--dry-run', '--json'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.dryRun, true);
assert.ok(parsed.plan.selectedModuleIds.includes('workflow-quality'));
assert.ok(
parsed.plan.operations.some(operation => (
String(operation.sourceRelativePath || '').replace(/\\/g, '/').startsWith('skills/delivery-gate/')
)),
'Full profile dry-run should include the delivery-gate skill'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('supports minimal profile dry-runs without hooks through the installer', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--profile', 'minimal', '--dry-run'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Mode: manifest'));
assert.ok(result.stdout.includes('Profile: minimal'));
assert.ok(result.stdout.includes('Selected modules: rules-core, agents-core, commands-core, platform-configs, workflow-quality'));
assert.ok(!result.stdout.includes('hooks-runtime'));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs manifest profiles and writes non-legacy install-state', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'lib', 'session-manager.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json')));
const state = readJson(path.join(claudeRoot, 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'core');
assert.strictEqual(state.request.legacyMode, false);
assert.deepStrictEqual(state.request.legacyLanguages, []);
assert.ok(state.resolution.selectedModules.includes('platform-configs'));
assert.ok(
state.operations.some(operation => (
operation.destinationPath === path.join(claudeRoot, 'commands', 'plan.md')
)),
'Should record manifest-driven command file copy'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('preserves existing top-level Claude rules and skills during managed install', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeRoot = path.join(homeDir, '.claude');
const userRulePath = path.join(claudeRoot, 'rules', 'common', 'coding-style.md');
const userSkillPath = path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md');
fs.mkdirSync(path.dirname(userRulePath), { recursive: true });
fs.mkdirSync(path.dirname(userSkillPath), { recursive: true });
fs.writeFileSync(userRulePath, '# User custom rule\n');
fs.writeFileSync(userSkillPath, '# User custom skill\n');
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.strictEqual(fs.readFileSync(userRulePath, 'utf8'), '# User custom rule\n');
assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User custom skill\n');
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'tdd-workflow', 'SKILL.md')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs antigravity manifest profiles while skipping only unsupported modules', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'antigravity', '--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json'));
assert.strictEqual(state.request.profile, 'core');
assert.strictEqual(state.request.legacyMode, false);
assert.deepStrictEqual(
state.resolution.selectedModules,
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
);
assert.ok(state.resolution.skippedModules.includes('hooks-runtime'));
assert.ok(!state.resolution.skippedModules.includes('workflow-quality'));
assert.ok(!state.resolution.skippedModules.includes('platform-configs'));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs explicit modules for cursor using manifest operations', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'cursor', '--modules', 'platform-configs'], {
cwd: projectDir,
homeDir,
});
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md')));
const state = readJson(path.join(projectDir, '.cursor', 'ecc-install-state.json'));
assert.strictEqual(state.request.profile, null);
assert.deepStrictEqual(state.request.modules, ['platform-configs']);
assert.deepStrictEqual(state.request.includeComponents, []);
assert.deepStrictEqual(state.request.excludeComponents, []);
assert.strictEqual(state.request.legacyMode, false);
assert.ok(state.resolution.selectedModules.includes('platform-configs'));
assert.ok(
!state.operations.some(operation => operation.destinationPath.endsWith('ecc-install-state.json')),
'Manifest copy operations should not include generated install-state files'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('rejects unknown explicit manifest modules before resolution', () => {
const result = run(['--modules', 'ghost-module']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown install module: ghost-module'));
})) passed++; else failed++;
if (test('installs claude hooks without generating settings.json', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')), 'hooks.json should be copied');
assert.ok(!fs.existsSync(path.join(claudeRoot, 'settings.json')), 'settings.json should not be created just to install managed hooks');
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs claude hooks with the safe plugin bootstrap contract', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
const installedHooks = readJson(path.join(claudeRoot, 'hooks', 'hooks.json'));
const installedBashDispatcherEntry = installedHooks.hooks.PreToolUse.find(entry => entry.id === 'pre:bash:dispatcher');
assert.ok(installedBashDispatcherEntry, 'hooks/hooks.json should include the consolidated Bash dispatcher hook');
assert.strictEqual(typeof installedBashDispatcherEntry.hooks[0].command, 'string', 'hooks/hooks.json should install string-form commands for Claude Code schema compatibility');
assert.ok(
installedBashDispatcherEntry.hooks[0].command.startsWith('node -e '),
'hooks/hooks.json should use the inline node bootstrap contract'
);
assert.ok(
installedBashDispatcherEntry.hooks[0].command.includes('plugin-hook-bootstrap.js'),
'hooks/hooks.json should route plugin-managed hooks through the shared bootstrap'
);
assert.ok(
installedBashDispatcherEntry.hooks[0].command.includes('CLAUDE_PLUGIN_ROOT'),
'hooks/hooks.json should still consult CLAUDE_PLUGIN_ROOT for runtime resolution'
);
assert.ok(
installedBashDispatcherEntry.hooks[0].command.includes('pre-bash-dispatcher.js'),
'hooks/hooks.json should point the Bash preflight contract at the consolidated dispatcher'
);
assert.ok(
!installedBashDispatcherEntry.hooks[0].command.includes('\\"'),
'hooks/hooks.json should avoid escaped double quotes that break Windows Git Bash parsing'
);
assert.ok(
!installedBashDispatcherEntry.hooks[0].command.includes('${CLAUDE_PLUGIN_ROOT}'),
'hooks/hooks.json should not retain raw CLAUDE_PLUGIN_ROOT shell placeholders after install'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('preserves existing settings.json without mutating it during claude install', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeRoot = path.join(homeDir, '.claude');
fs.mkdirSync(claudeRoot, { recursive: true });
fs.writeFileSync(
path.join(claudeRoot, 'settings.json'),
JSON.stringify({
effortLevel: 'high',
env: { MY_VAR: '1' },
hooks: {
PreToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'echo custom-pretool' }] }],
UserPromptSubmit: [{ matcher: '*', hooks: [{ type: 'command', command: 'echo custom-submit' }] }],
},
}, null, 2)
);
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const settings = readJson(path.join(claudeRoot, 'settings.json'));
assert.strictEqual(settings.effortLevel, 'high', 'existing effortLevel should be preserved');
assert.deepStrictEqual(settings.env, { MY_VAR: '1' }, 'existing env should be preserved');
assert.deepStrictEqual(
settings.hooks.UserPromptSubmit,
[{ matcher: '*', hooks: [{ type: 'command', command: 'echo custom-submit' }] }],
'existing hooks should be left untouched'
);
assert.deepStrictEqual(
settings.hooks.PreToolUse,
[{ matcher: 'Write', hooks: [{ type: 'command', command: 'echo custom-pretool' }] }],
'managed Claude hooks should not be injected into settings.json'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('filters copied mcp config files when ECC_DISABLED_MCPS is set', () => {
const tempDir = createTempDir('install-apply-mcp-');
const sourcePath = path.join(tempDir, '.mcp.json');
const destinationPath = path.join(tempDir, 'installed', '.mcp.json');
const installStatePath = path.join(tempDir, 'installed', 'ecc-install-state.json');
const previousValue = process.env.ECC_DISABLED_MCPS;
try {
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
fs.writeFileSync(sourcePath, JSON.stringify({
mcpServers: {
github: { command: 'npx' },
exa: { url: 'https://mcp.exa.ai/mcp' },
memory: { command: 'npx' },
},
}, null, 2));
process.env.ECC_DISABLED_MCPS = 'github,memory';
applyInstallPlan({
targetRoot: path.join(tempDir, 'installed'),
installStatePath,
statePreview: {
schemaVersion: 'ecc.install.v1',
installedAt: new Date().toISOString(),
target: {
id: 'test-install',
kind: 'project',
root: path.join(tempDir, 'installed'),
installStatePath,
},
request: {
profile: null,
modules: ['test-mcp'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['test-mcp'],
skippedModules: [],
},
source: {
repoVersion: null,
repoCommit: null,
manifestVersion: 1,
},
operations: [],
},
operations: [{
kind: 'copy-file',
moduleId: 'test-mcp',
sourcePath,
sourceRelativePath: '.mcp.json',
destinationPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
}],
});
const installed = readJson(destinationPath);
assert.deepStrictEqual(Object.keys(installed.mcpServers), ['exa']);
} finally {
if (previousValue === undefined) {
delete process.env.ECC_DISABLED_MCPS;
} else {
process.env.ECC_DISABLED_MCPS = previousValue;
}
cleanup(tempDir);
}
})) passed++; else failed++;
if (test('reinstall does not create settings.json when only managed hooks are installed', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const firstInstall = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(firstInstall.code, 0, firstInstall.stderr);
const secondInstall = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(secondInstall.code, 0, secondInstall.stderr);
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'settings.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('reinstall leaves pre-existing hook-based settings.json untouched', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeRoot = path.join(homeDir, '.claude');
fs.mkdirSync(claudeRoot, { recursive: true });
const settingsPath = path.join(claudeRoot, 'settings.json');
const legacySettings = {
hooks: {
PreToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'echo legacy-pretool' }] }],
},
};
fs.writeFileSync(settingsPath, JSON.stringify(legacySettings, null, 2));
const secondInstall = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(secondInstall.code, 0, secondInstall.stderr);
const afterSecondInstall = readJson(settingsPath);
assert.deepStrictEqual(afterSecondInstall, legacySettings);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('ignores malformed existing settings.json during claude install', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeRoot = path.join(homeDir, '.claude');
fs.mkdirSync(claudeRoot, { recursive: true });
const settingsPath = path.join(claudeRoot, 'settings.json');
fs.writeFileSync(settingsPath, '{ invalid json\n');
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.strictEqual(fs.readFileSync(settingsPath, 'utf8'), '{ invalid json\n');
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')), 'hooks.json should still be copied');
assert.ok(fs.existsSync(path.join(claudeRoot, 'ecc', 'install-state.json')), 'install state should still be written');
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('ignores non-object existing settings.json during claude install', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeRoot = path.join(homeDir, '.claude');
fs.mkdirSync(claudeRoot, { recursive: true });
const settingsPath = path.join(claudeRoot, 'settings.json');
fs.writeFileSync(settingsPath, '[]\n');
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.strictEqual(fs.readFileSync(settingsPath, 'utf8'), '[]\n');
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')), 'hooks.json should still be copied');
assert.ok(fs.existsSync(path.join(claudeRoot, 'ecc', 'install-state.json')), 'install state should still be written');
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('fails when source hooks.json root is not an object before copying files', () => {
const tempDir = createTempDir('install-apply-invalid-hooks-');
const targetRoot = path.join(tempDir, '.claude');
const installStatePath = path.join(targetRoot, 'ecc', 'install-state.json');
const sourceHooksPath = path.join(tempDir, 'hooks.json');
try {
fs.writeFileSync(sourceHooksPath, '[]\n');
assert.throws(() => {
applyInstallPlan({
targetRoot,
installStatePath,
statePreview: {
schemaVersion: 'ecc.install.v1',
installedAt: new Date().toISOString(),
target: {
id: 'claude-home',
kind: 'home',
root: targetRoot,
installStatePath,
},
request: {
profile: 'core',
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['hooks-runtime'],
skippedModules: [],
},
source: {
repoVersion: null,
repoCommit: null,
manifestVersion: 1,
},
operations: [],
},
adapter: { target: 'claude' },
operations: [{
kind: 'copy-file',
moduleId: 'hooks-runtime',
sourcePath: sourceHooksPath,
sourceRelativePath: 'hooks/hooks.json',
destinationPath: path.join(targetRoot, 'hooks', 'hooks.json'),
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
}],
});
}, /Invalid hooks config at .*expected a JSON object/);
assert.ok(!fs.existsSync(path.join(targetRoot, 'hooks', 'hooks.json')), 'hooks.json should not be copied when source hooks are invalid');
assert.ok(!fs.existsSync(installStatePath), 'install state should not be written when source hooks are invalid');
} finally {
cleanup(tempDir);
}
})) passed++; else failed++;
if (test('installs from ecc-install.json and persists component selections', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
const configPath = path.join(projectDir, 'ecc-install.json');
try {
fs.writeFileSync(configPath, JSON.stringify({
version: 1,
target: 'claude',
profile: 'developer',
include: ['capability:security'],
exclude: ['capability:orchestration'],
}, null, 2));
const result = run(['--config', configPath], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'dmux-workflows', 'SKILL.md')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer');
assert.deepStrictEqual(state.request.includeComponents, ['capability:security']);
assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']);
assert.ok(state.resolution.selectedModules.includes('security'));
assert.ok(!state.resolution.selectedModules.includes('orchestration'));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('auto-detects ecc-install.json from the project root', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
const configPath = path.join(projectDir, 'ecc-install.json');
try {
fs.writeFileSync(configPath, JSON.stringify({
version: 1,
target: 'claude',
profile: 'developer',
include: ['capability:security'],
exclude: ['capability:orchestration'],
}, null, 2));
const result = run([], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'dmux-workflows', 'SKILL.md')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer');
assert.deepStrictEqual(state.request.includeComponents, ['capability:security']);
assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']);
assert.ok(state.resolution.selectedModules.includes('security'));
assert.ok(!state.resolution.selectedModules.includes('orchestration'));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('preserves legacy language installs when a project config is present', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
const configPath = path.join(projectDir, 'ecc-install.json');
try {
fs.writeFileSync(configPath, JSON.stringify({
version: 1,
target: 'claude',
profile: 'developer',
include: ['capability:security'],
}, null, 2));
const result = run(['typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.legacyMode, true);
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.profile, null);
assert.deepStrictEqual(state.request.includeComponents, []);
assert.ok(state.resolution.selectedModules.includes('framework-language'));
assert.ok(!state.resolution.selectedModules.includes('security'));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+194
View File
@@ -0,0 +1,194 @@
/**
* Tests for scripts/install-plan.js
*/
const assert = require('assert');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-plan.js');
function run(args = [], options = {}) {
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
cwd: options.cwd,
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing install-plan.js ===\n');
let passed = 0;
let failed = 0;
if (test('shows help with no arguments', () => {
const result = run();
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Inspect ECC selective-install manifests'));
})) passed++; else failed++;
if (test('lists install profiles', () => {
const result = run(['--list-profiles']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Install profiles'));
assert.ok(result.stdout.includes('core'));
})) passed++; else failed++;
if (test('lists install modules', () => {
const result = run(['--list-modules']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Install modules'));
assert.ok(result.stdout.includes('rules-core'));
})) passed++; else failed++;
if (test('lists install components', () => {
const result = run(['--list-components', '--family', 'language']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Install components'));
assert.ok(result.stdout.includes('lang:typescript'));
assert.ok(!result.stdout.includes('capability:security'));
})) passed++; else failed++;
if (test('prints a filtered install plan for a profile and target', () => {
const result = run([
'--profile', 'developer',
'--with', 'capability:security',
'--without', 'capability:orchestration',
'--target', 'cursor'
]);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Install plan'));
assert.ok(result.stdout.includes('Included components: capability:security'));
assert.ok(result.stdout.includes('Excluded components: capability:orchestration'));
assert.ok(result.stdout.includes('Adapter: cursor-project'));
assert.ok(result.stdout.includes('Target root:'));
assert.ok(result.stdout.includes('Install-state:'));
assert.ok(result.stdout.includes('Operation plan'));
assert.ok(result.stdout.includes('Excluded by selection'));
assert.ok(result.stdout.includes('security'));
})) passed++; else failed++;
if (test('emits JSON for explicit module resolution', () => {
const result = run([
'--modules', 'security',
'--with', 'capability:research',
'--target', 'cursor',
'--json'
]);
assert.strictEqual(result.code, 0);
const parsed = JSON.parse(result.stdout);
assert.ok(parsed.selectedModuleIds.includes('security'));
assert.ok(parsed.selectedModuleIds.includes('research-apis'));
assert.ok(parsed.selectedModuleIds.includes('workflow-quality'));
assert.deepStrictEqual(parsed.includedComponentIds, ['capability:research']);
assert.strictEqual(parsed.targetAdapterId, 'cursor-project');
assert.ok(Array.isArray(parsed.operations));
assert.ok(parsed.operations.length > 0);
})) passed++; else failed++;
if (test('emits JSON for --skills without pulling parent module', () => {
const result = run([
'--skills', 'continuous-learning-v2',
'--target', 'claude',
'--json',
]);
assert.strictEqual(result.code, 0);
const parsed = JSON.parse(result.stdout);
assert.deepStrictEqual(parsed.includedComponentIds, ['skill:continuous-learning-v2']);
assert.deepStrictEqual(parsed.selectedModuleIds, ['skill-continuous-learning-v2']);
assert.ok(parsed.operations.some(operation => operation.sourceRelativePath === 'skills/continuous-learning-v2'));
assert.ok(!parsed.operations.some(operation => operation.sourceRelativePath === 'skills/tdd-workflow'));
})) passed++; else failed++;
if (test('loads planning intent from ecc-install.json', () => {
const configDir = path.join(__dirname, '..', 'fixtures', 'tmp-install-plan-config');
const configPath = path.join(configDir, 'ecc-install.json');
try {
require('fs').mkdirSync(configDir, { recursive: true });
require('fs').writeFileSync(configPath, JSON.stringify({
version: 1,
target: 'cursor',
profile: 'core',
include: ['capability:security'],
exclude: ['capability:orchestration'],
}, null, 2));
const result = run(['--config', configPath, '--json']);
assert.strictEqual(result.code, 0);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.target, 'cursor');
assert.deepStrictEqual(parsed.includedComponentIds, ['capability:security']);
assert.deepStrictEqual(parsed.excludedComponentIds, ['capability:orchestration']);
assert.ok(parsed.selectedModuleIds.includes('security'));
assert.ok(!parsed.selectedModuleIds.includes('orchestration'));
} finally {
require('fs').rmSync(configDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('auto-detects planning intent from project ecc-install.json', () => {
const configDir = path.join(__dirname, '..', 'fixtures', 'tmp-install-plan-autodetect');
const configPath = path.join(configDir, 'ecc-install.json');
try {
require('fs').mkdirSync(configDir, { recursive: true });
require('fs').writeFileSync(configPath, JSON.stringify({
version: 1,
target: 'cursor',
profile: 'core',
include: ['capability:security'],
}, null, 2));
const result = run(['--json'], { cwd: configDir });
assert.strictEqual(result.code, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.target, 'cursor');
assert.strictEqual(parsed.profileId, 'core');
assert.deepStrictEqual(parsed.includedComponentIds, ['capability:security']);
assert.ok(parsed.selectedModuleIds.includes('security'));
} finally {
require('fs').rmSync(configDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('fails on unknown arguments', () => {
const result = run(['--unknown-flag']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown argument'));
})) passed++; else failed++;
if (test('fails on invalid install target', () => {
const result = run(['--profile', 'core', '--target', 'not-a-target']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown install target'));
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+128
View File
@@ -0,0 +1,128 @@
/**
* Tests for install.ps1 wrapper delegation
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'install.ps1');
const PACKAGE_JSON = path.join(__dirname, '..', '..', 'package.json');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function resolvePowerShellCommand() {
const candidates = process.platform === 'win32'
? ['powershell.exe', 'pwsh.exe', 'pwsh']
: ['pwsh'];
for (const candidate of candidates) {
const result = spawnSync(candidate, ['-NoLogo', '-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 5000,
});
if (!result.error && result.status === 0) {
return candidate;
}
}
return null;
}
function run(powerShellCommand, args = [], options = {}) {
const env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
USERPROFILE: options.homeDir || process.env.USERPROFILE,
};
try {
const stdout = execFileSync(powerShellCommand, ['-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', SCRIPT, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing install.ps1 ===\n');
let passed = 0;
let failed = 0;
const powerShellCommand = resolvePowerShellCommand();
if (test('publishes ecc-install through the Node installer runtime for cross-platform npm usage', () => {
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8'));
assert.strictEqual(packageJson.bin['ecc-install'], 'scripts/install-apply.js');
})) passed++; else failed++;
if (!powerShellCommand) {
console.log(' - skipped delegation test; PowerShell is not available in PATH');
} else if (test('delegates to the Node installer and preserves dry-run output', () => {
const homeDir = createTempDir('install-ps1-home-');
const projectDir = createTempDir('install-ps1-project-');
try {
const result = run(powerShellCommand, ['--target', 'cursor', '--dry-run', 'typescript'], {
cwd: projectDir,
homeDir,
});
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Dry-run install plan'));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (!powerShellCommand) {
console.log(' - skipped help text test; PowerShell is not available in PATH');
} else if (test('exposes the corrected Claude target help text', () => {
const result = run(powerShellCommand, ['--help']);
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(
result.stdout.includes('claude (default) - Install ECC into ~/.claude/'),
'help text should describe the Claude target as a full ~/.claude install surface'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
@@ -0,0 +1,170 @@
/**
* Regression coverage for install/uninstall clarity in README.md.
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const README = path.join(__dirname, '..', '..', 'README.md');
const RULES_README = path.join(__dirname, '..', '..', 'rules', 'README.md');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing install README clarity ===\n');
let passed = 0;
let failed = 0;
const readme = fs.readFileSync(README, 'utf8');
const rulesReadme = fs.readFileSync(RULES_README, 'utf8');
if (test('README marks one default path and warns against stacked installs', () => {
assert.ok(
readme.includes('### Pick one path only'),
'README should surface a top-level install decision section'
);
assert.ok(
readme.includes('**Recommended default:** install the Claude Code plugin'),
'README should name the recommended default install path'
);
assert.ok(
readme.includes('**Do not stack install methods.**'),
'README should explicitly warn against stacking install methods'
);
assert.ok(
readme.includes('If you choose this path, stop there. Do not also run `/plugin install`.'),
'README should tell manual-install users not to continue layering installs'
);
})) passed++; else failed++;
if (test('README documents reset and uninstall flow', () => {
assert.ok(
readme.includes('### Reset / Uninstall ECC'),
'README should have a visible reset/uninstall section'
);
assert.ok(
readme.includes('node scripts/uninstall.js --dry-run'),
'README should document dry-run uninstall'
);
assert.ok(
readme.includes('node scripts/ecc.js list-installed'),
'README should document install-state inspection before reinstalling'
);
assert.ok(
readme.includes('node scripts/ecc.js doctor'),
'README should document doctor before reinstalling'
);
assert.ok(
readme.includes('ECC only removes files recorded in its install-state.'),
'README should explain uninstall safety boundaries'
);
})) passed++; else failed++;
if (test('README documents low-context no-hooks install path', () => {
assert.ok(
readme.includes('### Low-context / no-hooks path'),
'README should surface a low-context no-hooks install option near Quick Start'
);
assert.ok(
readme.includes('./install.sh --profile minimal --target claude'),
'README should document the shell minimal profile command'
);
assert.ok(
readme.includes('npx ecc-install --profile minimal --target claude'),
'README should document the npx minimal profile command'
);
assert.ok(
readme.includes('--profile core --without baseline:hooks --target claude'),
'README should document the hook opt-out path for the core profile'
);
assert.ok(
readme.includes('This profile intentionally excludes `hooks-runtime`.'),
'README should state that the minimal profile excludes hooks'
);
})) passed++; else failed++;
if (test('README documents consult-based component discovery', () => {
assert.ok(
readme.includes('### Find the right components first'),
'README should surface component discovery before install steps'
);
assert.ok(
readme.includes('npx ecc consult "security reviews" --target claude'),
'README should document the packaged consult command'
);
assert.ok(
readme.includes('It returns matching components, related profiles, and preview/install commands.'),
'README should explain what consult returns'
);
})) passed++; else failed++;
if (test('README documents Cursor agent namespace and loading caveat', () => {
assert.ok(
readme.includes('`.cursor/agents/ecc-*.md`'),
'README should document the Cursor agent namespace'
);
assert.ok(
readme.includes('Cursor-native loading behavior can vary by Cursor build.'),
'README should avoid overclaiming Cursor agent loading semantics'
);
assert.ok(
readme.includes('ECC does not install root `AGENTS.md` into `.cursor/`.'),
'README should explain why root AGENTS.md is not copied into Cursor context'
);
})) passed++; else failed++;
if (test('README explains plugin-path cleanup and rules scoping', () => {
assert.ok(
readme.includes('remove the plugin from Claude Code'),
'README should tell plugin users how to start cleanup'
);
assert.ok(
readme.includes('Start with `rules/common` plus one language or framework pack you actually use.'),
'README should steer users away from copying every rules directory'
);
assert.ok(
readme.includes('~/.claude/rules/ecc/'),
'README should steer plugin-path rules into an ECC-owned namespace'
);
})) passed++; else failed++;
if (test('rules README mirrors ECC namespaced install path', () => {
assert.ok(
rulesReadme.includes('mkdir -p ~/.claude/rules/ecc'),
'rules README should create the ECC-owned user-level rules namespace'
);
assert.ok(
rulesReadme.includes('cp -r rules/common ~/.claude/rules/ecc/'),
'rules README should copy common rules under ~/.claude/rules/ecc/'
);
assert.ok(
rulesReadme.includes('cp -r rules/typescript ~/.claude/rules/ecc/'),
'rules README should copy language rules under ~/.claude/rules/ecc/'
);
assert.ok(
rulesReadme.includes('mkdir -p .claude/rules/ecc'),
'rules README should document the project-local ECC namespace'
);
assert.ok(
!rulesReadme.includes('~/.claude/rules/typescript'),
'rules README should not recommend flat user-level rule destinations'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+102
View File
@@ -0,0 +1,102 @@
/**
* Tests for install.sh wrapper delegation
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'install.sh');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function run(args = [], options = {}) {
const env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
};
try {
const stdout = execFileSync('bash', [SCRIPT, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing install.sh ===\n');
let passed = 0;
let failed = 0;
if (process.platform === 'win32') {
console.log(' - skipped on Windows; install.ps1 covers the native wrapper path');
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(0);
}
if (test('delegates to the Node installer and preserves dry-run output', () => {
const homeDir = createTempDir('install-sh-home-');
const projectDir = createTempDir('install-sh-project-');
try {
const result = run(['--target', 'cursor', '--dry-run', 'typescript'], {
cwd: projectDir,
homeDir,
});
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Dry-run install plan'));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('exposes the corrected Claude target help text', () => {
const result = run(['--help']);
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(
result.stdout.includes('claude (default) - Install ECC into ~/.claude/'),
'help text should describe the Claude target as a full ~/.claude install surface'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+336
View File
@@ -0,0 +1,336 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const crypto = require('crypto');
const { spawnSync } = require('child_process');
let passed = 0;
let failed = 0;
const repoRoot = path.resolve(__dirname, '..', '..');
const cliPath = path.join(
repoRoot,
'skills',
'continuous-learning-v2',
'scripts',
'instinct-cli.py'
);
function detectPython3() {
for (const bin of ['python3', 'python']) {
const r = spawnSync(bin, ['--version'], { encoding: 'utf8' });
if (r.status === 0 && /Python 3/.test(r.stdout + r.stderr)) return bin;
}
return null;
}
const PYTHON3 = detectPython3();
if (!PYTHON3) {
console.log('\n=== Testing instinct-cli.py projects maintenance ===\n');
console.log(' - skipped: Python 3 not found in PATH');
console.log('\nPassed: 0');
console.log('Failed: 0');
process.exit(0);
}
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed += 1;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-instinct-cli-projects-'));
}
function cleanupDir(dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
function writeJson(filePath, payload) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function writeInstinct(filePath, id, confidence = 0.9) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(
filePath,
[
'---',
`id: ${id}`,
'trigger: "when repeated"',
`confidence: ${confidence}`,
'domain: workflow',
'---',
'',
`Action for ${id}.`,
'',
].join('\n')
);
}
function seedProject(root, id, options = {}) {
const projectDir = path.join(root, 'projects', id);
const personalDir = path.join(projectDir, 'instincts', 'personal');
const inheritedDir = path.join(projectDir, 'instincts', 'inherited');
fs.mkdirSync(personalDir, { recursive: true });
fs.mkdirSync(inheritedDir, { recursive: true });
for (const instinct of options.personal || []) {
writeInstinct(path.join(personalDir, `${instinct}.yaml`), instinct);
}
for (const instinct of options.inherited || []) {
writeInstinct(path.join(inheritedDir, `${instinct}.yaml`), instinct);
}
if (options.observations) {
fs.writeFileSync(
path.join(projectDir, 'observations.jsonl'),
options.observations.map(row => JSON.stringify(row)).join('\n') + '\n'
);
}
return projectDir;
}
function projectHash(value) {
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
}
function runGit(cwd, args) {
const result = spawnSync('git', args, {
cwd,
encoding: 'utf8',
});
assert.strictEqual(result.status, 0, result.stderr);
return result.stdout.trim();
}
function runCli(root, args, options = {}) {
return spawnSync(PYTHON3, [cliPath, ...args], {
cwd: options.cwd || repoRoot,
encoding: 'utf8',
env: {
...process.env,
CLV2_HOMUNCULUS_DIR: root,
HOME: path.join(root, 'home'),
USERPROFILE: path.join(root, 'home'),
CLAUDE_PROJECT_DIR: '',
...(options.env || {}),
},
});
}
console.log('\n=== Testing instinct-cli.py projects maintenance ===\n');
test('projects delete --dry-run preserves registry and project files', () => {
const root = createTempDir();
try {
const registryPath = path.join(root, 'projects.json');
seedProject(root, 'alpha123', {
personal: ['keep-me'],
observations: [{ event: 'tool_complete' }],
});
writeJson(registryPath, {
alpha123: { name: 'alpha', root: '/repo/alpha', remote: '', last_seen: '2026-01-01T00:00:00Z' },
});
const result = runCli(root, ['projects', 'delete', 'alpha123', '--dry-run']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /would delete/i);
assert.ok(fs.existsSync(path.join(root, 'projects', 'alpha123')));
assert.ok(readJson(registryPath).alpha123);
} finally {
cleanupDir(root);
}
});
test('projects delete --force removes registry entry and project directory', () => {
const root = createTempDir();
try {
const registryPath = path.join(root, 'projects.json');
seedProject(root, 'alpha123', { personal: ['delete-me'] });
writeJson(registryPath, {
alpha123: { name: 'alpha', root: '/repo/alpha', remote: '', last_seen: '2026-01-01T00:00:00Z' },
});
const result = runCli(root, ['projects', 'delete', 'alpha123', '--force']);
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(!fs.existsSync(path.join(root, 'projects', 'alpha123')));
assert.ok(!readJson(registryPath).alpha123);
} finally {
cleanupDir(root);
}
});
test('projects gc --force removes only zero-value project entries', () => {
const root = createTempDir();
try {
const registryPath = path.join(root, 'projects.json');
seedProject(root, 'empty000');
seedProject(root, 'active999', { personal: ['active'] });
writeJson(registryPath, {
empty000: { name: 'empty', root: '/tmp/empty', remote: '', last_seen: '2026-01-01T00:00:00Z' },
active999: { name: 'active', root: '/repo/active', remote: '', last_seen: '2026-01-02T00:00:00Z' },
});
const result = runCli(root, ['projects', 'gc', '--force']);
assert.strictEqual(result.status, 0, result.stderr);
const registry = readJson(registryPath);
assert.ok(!registry.empty000);
assert.ok(registry.active999);
assert.ok(!fs.existsSync(path.join(root, 'projects', 'empty000')));
assert.ok(fs.existsSync(path.join(root, 'projects', 'active999')));
} finally {
cleanupDir(root);
}
});
test('projects merge deduplicates instincts, appends observations, and removes source', () => {
const root = createTempDir();
try {
const registryPath = path.join(root, 'projects.json');
seedProject(root, 'from111', {
personal: ['shared', 'from-only'],
observations: [{ event: 'from-event' }],
});
seedProject(root, 'into222', {
personal: ['shared', 'into-only'],
observations: [{ event: 'into-event' }],
});
writeJson(registryPath, {
from111: { name: 'from', root: '/repo/from', remote: '', last_seen: '2026-01-01T00:00:00Z' },
into222: { name: 'into', root: '/repo/into', remote: '', last_seen: '2026-01-02T00:00:00Z' },
});
const result = runCli(root, ['projects', 'merge', 'from111', 'into222', '--force']);
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(!fs.existsSync(path.join(root, 'projects', 'from111')));
assert.ok(!readJson(registryPath).from111);
assert.ok(readJson(registryPath).into222);
const intoPersonal = path.join(root, 'projects', 'into222', 'instincts', 'personal');
assert.ok(fs.existsSync(path.join(intoPersonal, 'shared.yaml')));
assert.ok(fs.existsSync(path.join(intoPersonal, 'from-only.yaml')));
assert.ok(fs.existsSync(path.join(intoPersonal, 'into-only.yaml')));
const observations = fs.readFileSync(
path.join(root, 'projects', 'into222', 'observations.jsonl'),
'utf8'
);
assert.match(observations, /from-event/);
assert.match(observations, /into-event/);
} finally {
cleanupDir(root);
}
});
test('status warns when legacy ~/.claude/homunculus contains files', () => {
const root = createTempDir();
try {
const legacyDir = path.join(root, 'home', '.claude', 'homunculus', 'instincts', 'personal');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'old-instinct.yaml'), '---\nid: old\n---\nOld instinct.\n');
const result = runCli(root, ['status']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /LEGACY DATA DETECTED/);
assert.match(result.stdout, /legacy path/i);
assert.match(result.stdout, /migration script/i);
} finally {
cleanupDir(root);
}
});
test('status does not warn when legacy dir is empty', () => {
const root = createTempDir();
try {
const legacyDir = path.join(root, 'home', '.claude', 'homunculus');
fs.mkdirSync(legacyDir, { recursive: true });
const result = runCli(root, ['status']);
assert.strictEqual(result.status, 0, result.stderr);
assert.doesNotMatch(result.stdout, /LEGACY DATA DETECTED/);
} finally {
cleanupDir(root);
}
});
test('status does not warn when no legacy dir exists', () => {
const root = createTempDir();
try {
const result = runCli(root, ['status']);
assert.strictEqual(result.status, 0, result.stderr);
assert.doesNotMatch(result.stdout, /LEGACY DATA DETECTED/);
} finally {
cleanupDir(root);
}
});
test('status does not warn when CLV2_HOMUNCULUS_DIR points at legacy path', () => {
const root = createTempDir();
try {
const legacyDir = path.join(root, 'home', '.claude', 'homunculus', 'instincts', 'personal');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'active.yaml'), '---\nid: active\n---\nActive.\n');
const result = runCli(root, ['status'], {
env: { CLV2_HOMUNCULUS_DIR: path.join(root, 'home', '.claude', 'homunculus') },
});
assert.strictEqual(result.status, 0, result.stderr);
assert.doesNotMatch(result.stdout, /LEGACY DATA DETECTED/);
} finally {
cleanupDir(root);
}
});
test('status migrates legacy no-remote linked worktree project dirs to main worktree id', () => {
const root = createTempDir();
const repoParent = createTempDir();
try {
const mainWorktree = path.join(repoParent, 'main');
const linkedWorktree = path.join(repoParent, 'linked');
fs.mkdirSync(mainWorktree, { recursive: true });
runGit(mainWorktree, ['init']);
runGit(mainWorktree, ['config', 'user.email', 'ecc@example.test']);
runGit(mainWorktree, ['config', 'user.name', 'ECC Test']);
fs.writeFileSync(path.join(mainWorktree, 'README.md'), 'test\n');
runGit(mainWorktree, ['add', 'README.md']);
runGit(mainWorktree, ['commit', '-m', 'init']);
runGit(mainWorktree, ['worktree', 'add', linkedWorktree]);
const mainRoot = runGit(mainWorktree, ['rev-parse', '--show-toplevel']);
const linkedRoot = runGit(linkedWorktree, ['rev-parse', '--show-toplevel']);
const oldLinkedId = projectHash(linkedRoot);
const mainId = projectHash(mainRoot);
seedProject(root, oldLinkedId, { personal: ['legacy-worktree'] });
const result = runCli(root, ['status'], { cwd: linkedRoot });
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(!fs.existsSync(path.join(root, 'projects', oldLinkedId)));
assert.ok(fs.existsSync(path.join(root, 'projects', mainId)));
assert.ok(
fs.existsSync(path.join(root, 'projects', mainId, 'instincts', 'personal', 'legacy-worktree.yaml'))
);
assert.match(result.stdout, new RegExp(`\\(${mainId}\\)`));
} finally {
cleanupDir(root);
cleanupDir(repoParent);
}
});
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
+139
View File
@@ -0,0 +1,139 @@
/**
* Tests for scripts/list-installed.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'list-installed.js');
const REPO_ROOT = path.join(__dirname, '..', '..');
const CURRENT_PACKAGE_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')
).version;
const CURRENT_MANIFEST_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'manifests', 'install-modules.json'), 'utf8')
).version;
const {
createInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeState(filePath, options) {
const state = createInstallState(options);
writeInstallState(filePath, state);
}
function run(args = [], options = {}) {
const env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
};
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing list-installed.js ===\n');
let passed = 0;
let failed = 0;
if (test('reports when no install-state files are present', () => {
const homeDir = createTempDir('list-installed-home-');
const projectRoot = createTempDir('list-installed-project-');
try {
const result = run([], { cwd: projectRoot, homeDir });
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('No ECC install-state files found'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('emits JSON for discovered install-state records', () => {
const homeDir = createTempDir('list-installed-home-');
const projectRoot = createTempDir('list-installed-project-');
try {
const statePath = path.join(projectRoot, '.cursor', 'ecc-install-state.json');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: path.join(projectRoot, '.cursor'),
installStatePath: statePath,
request: {
profile: 'core',
modules: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core', 'platform-configs'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = run(['--json'], { cwd: projectRoot, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.records.length, 1);
assert.strictEqual(parsed.records[0].state.target.id, 'cursor-project');
assert.strictEqual(parsed.records[0].state.request.profile, 'core');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+749
View File
@@ -0,0 +1,749 @@
/**
* Tests for scripts/loop-status.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
const {
analyzeTranscript,
buildStatus,
getStatusExitCode,
parseArgs,
writeStatusSnapshots,
} = require('../../scripts/loop-status');
const NOW = '2026-04-30T10:00:00.000Z';
function run(args = [], options = {}) {
const envOverrides = {
...(options.env || {}),
};
if (typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)) {
envOverrides.USERPROFILE = envOverrides.HOME;
}
if (typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)) {
envOverrides.HOME = envOverrides.USERPROFILE;
}
const result = spawnSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
cwd: options.cwd || process.cwd(),
env: {
...process.env,
...envOverrides,
},
});
return {
code: result.status || (result.signal ? 1 : 0),
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function createTempHome() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-home-'));
}
function writeTranscript(homeDir, projectSlug, fileName, entries) {
const transcriptDir = path.join(homeDir, '.claude', 'projects', projectSlug);
fs.mkdirSync(transcriptDir, { recursive: true });
const transcriptPath = path.join(transcriptDir, fileName);
fs.writeFileSync(
transcriptPath,
entries.map(entry => JSON.stringify(entry)).join('\n') + '\n',
'utf8'
);
return transcriptPath;
}
function toolUse(timestamp, sessionId, id, name, input = {}) {
return {
timestamp,
sessionId,
type: 'assistant',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id,
name,
input,
},
],
},
};
}
function toolResult(timestamp, sessionId, toolUseId, content = 'ok') {
return {
timestamp,
sessionId,
type: 'user',
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: toolUseId,
content,
},
],
},
};
}
function assistantMessage(timestamp, sessionId, text) {
return {
timestamp,
sessionId,
type: 'assistant',
message: {
role: 'assistant',
content: [
{
type: 'text',
text,
},
],
},
};
}
function parsePayload(stdout) {
return JSON.parse(stdout.trim());
}
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.error(` ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing loop-status.js ===\n');
let passed = 0;
let failed = 0;
if (test('reports overdue ScheduleWakeup calls from Claude transcripts', () => {
const homeDir = createTempHome();
try {
const transcriptPath = writeTranscript(homeDir, '-Users-affoon-project-a', 'session-a.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-a', 'toolu_wake', 'ScheduleWakeup', {
delaySeconds: 300,
reason: 'Iter 15: continue autonomous loop',
}),
]);
const result = run(['--home', homeDir, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.loop-status.v1');
assert.strictEqual(payload.sessions.length, 1);
assert.strictEqual(payload.sessions[0].sessionId, 'session-a');
assert.strictEqual(payload.sessions[0].transcriptPath, transcriptPath);
assert.strictEqual(payload.sessions[0].state, 'attention');
assert.ok(payload.sessions[0].signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
assert.strictEqual(payload.sessions[0].latestWake.dueAt, '2026-04-30T09:05:00.000Z');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('analyzeTranscript applies default thresholds when called directly', () => {
const homeDir = createTempHome();
try {
const transcriptPath = writeTranscript(homeDir, '-Users-affoon-project-direct', 'session-direct.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-direct', 'toolu_direct_wake', 'ScheduleWakeup', {
delaySeconds: 300,
reason: 'Direct API default threshold check',
}),
]);
const session = analyzeTranscript(transcriptPath, { now: NOW });
assert.strictEqual(session.state, 'attention');
assert.ok(session.signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('reports stale Bash tool_use entries without matching tool_result', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-b', 'session-b.jsonl', [
toolUse('2026-04-30T09:10:00.000Z', 'session-b', 'toolu_bash', 'Bash', {
command: 'pytest tests/integration/test_pipeline.py',
}),
]);
const result = run(['--home', homeDir, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions[0].state, 'attention');
assert.ok(payload.sessions[0].signals.some(signal => (
signal.type === 'pending_bash_tool_result'
&& signal.toolUseId === 'toolu_bash'
&& signal.ageSeconds === 3000
)));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('does not flag Bash tool_use entries that have a matching tool_result', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-c', 'session-c.jsonl', [
toolUse('2026-04-30T09:40:00.000Z', 'session-c', 'toolu_bash_ok', 'Bash', {
command: 'npm test',
}),
toolResult('2026-04-30T09:41:00.000Z', 'session-c', 'toolu_bash_ok', 'passed'),
]);
const result = run(['--home', homeDir, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions[0].state, 'ok');
assert.deepStrictEqual(payload.sessions[0].signals, []);
assert.deepStrictEqual(payload.sessions[0].pendingTools, []);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('does not flag ScheduleWakeup when later assistant progress exists', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-d', 'session-d.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-d', 'toolu_wake_ok', 'ScheduleWakeup', {
delaySeconds: 300,
reason: 'Loop checkpoint',
}),
assistantMessage('2026-04-30T09:06:00.000Z', 'session-d', 'Wake fired; continuing.'),
]);
const result = run(['--home', homeDir, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions[0].state, 'ok');
assert.ok(!payload.sessions[0].signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('supports inspecting one transcript path directly', () => {
const homeDir = createTempHome();
try {
const transcriptPath = writeTranscript(homeDir, '-Users-affoon-project-e', 'session-e.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-e', 'toolu_direct', 'Bash', {
command: 'sleep 999',
}),
]);
const result = run(['--transcript', transcriptPath, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions.length, 1);
assert.strictEqual(payload.sessions[0].transcriptPath, transcriptPath);
assert.ok(payload.sessions[0].signals.some(signal => signal.type === 'pending_bash_tool_result'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('prints text output with state and recommended action', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-f', 'session-f.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-f', 'toolu_text', 'ScheduleWakeup', {
delaySeconds: 600,
reason: 'Loop checkpoint',
}),
]);
const result = run(['--home', homeDir, '--now', NOW]);
assert.strictEqual(result.code, 0, result.stderr);
assert.match(result.stdout, /session-f/);
assert.match(result.stdout, /attention/);
assert.match(result.stdout, /schedule_wakeup_overdue/);
assert.match(result.stdout, /Open the transcript or interrupt the parked session/);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('continues when an explicit transcript path cannot be read', () => {
const missingTranscript = path.join(os.tmpdir(), `missing-loop-status-${Date.now()}.jsonl`);
const result = run(['--transcript', missingTranscript, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.deepStrictEqual(payload.sessions, []);
assert.strictEqual(payload.errors.length, 1);
assert.strictEqual(payload.errors[0].transcriptPath, missingTranscript);
})) passed++; else failed++;
if (test('text output distinguishes explicit transcript read failures from empty discovery', () => {
const missingTranscript = path.join(os.tmpdir(), `missing-loop-status-text-${Date.now()}.jsonl`);
const result = run(['--transcript', missingTranscript, '--now', NOW]);
assert.strictEqual(result.code, 0, result.stderr);
assert.match(result.stdout, /No readable Claude transcript JSONL files were found/);
assert.match(result.stdout, /Skipped transcript errors/);
assert.ok(!result.stdout.includes('No Claude transcript JSONL files found under'));
})) passed++; else failed++;
if (test('continues when one transcript directory cannot be read', () => {
const homeDir = createTempHome();
const blockedDir = path.join(homeDir, '.claude', 'projects', '-blocked-project');
const originalReaddirSync = fs.readdirSync;
try {
writeTranscript(homeDir, '-Users-affoon-project-readable', 'session-readable.jsonl', [
toolResult('2026-04-30T09:41:00.000Z', 'session-readable', 'toolu_done', 'done'),
]);
fs.mkdirSync(blockedDir, { recursive: true });
fs.readdirSync = (dir, options) => {
if (path.resolve(dir) === path.resolve(blockedDir)) {
const error = new Error('permission denied');
error.code = 'EACCES';
throw error;
}
return originalReaddirSync(dir, options);
};
const payload = buildStatus({ home: homeDir, now: NOW });
assert.strictEqual(payload.sessions.length, 1);
assert.strictEqual(payload.sessions[0].sessionId, 'session-readable');
assert.strictEqual(payload.errors.length, 1);
assert.strictEqual(payload.errors[0].code, 'EACCES');
assert.strictEqual(payload.errors[0].transcriptPath, blockedDir);
} finally {
fs.readdirSync = originalReaddirSync;
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('reports malformed JSONL lines as an attention signal', () => {
const homeDir = createTempHome();
try {
const transcriptDir = path.join(homeDir, '.claude', 'projects', '-Users-affoon-project-malformed');
fs.mkdirSync(transcriptDir, { recursive: true });
fs.writeFileSync(
path.join(transcriptDir, 'session-malformed.jsonl'),
[
JSON.stringify({
timestamp: '2026-04-30T09:55:00.000Z',
sessionId: 'session-malformed',
message: { role: 'assistant', content: [{ type: 'text', text: 'partial log' }] },
}),
'{"timestamp":',
].join('\n') + '\n',
'utf8'
);
const result = run(['--home', homeDir, '--now', NOW, '--json']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions[0].state, 'attention');
assert.ok(payload.sessions[0].signals.some(signal => (
signal.type === 'transcript_parse_errors'
&& signal.count === 1
)));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('rejects non-integer limit values', () => {
const result = run(['--limit', '1.5']);
assert.strictEqual(result.code, 1);
assert.match(result.stderr, /--limit must be a positive integer/);
})) passed++; else failed++;
if (test('parses watch mode controls', () => {
const options = parseArgs([
'node',
'scripts/loop-status.js',
'--exit-code',
'--watch',
'--watch-count',
'2',
'--watch-interval-seconds',
'0.01',
]);
assert.strictEqual(options.exitCode, true);
assert.strictEqual(options.watch, true);
assert.strictEqual(options.watchCount, 2);
assert.strictEqual(options.watchIntervalSeconds, 0.01);
})) passed++; else failed++;
if (test('parses write-dir snapshot option', () => {
const options = parseArgs([
'node',
'scripts/loop-status.js',
'--write-dir',
'/tmp/ecc-loop-snapshots',
]);
assert.strictEqual(options.writeDir, '/tmp/ecc-loop-snapshots');
})) passed++; else failed++;
if (test('exit-code mode returns 2 when attention signals are present', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-exit-code', 'session-exit-code.jsonl', [
toolUse('2026-04-30T09:10:00.000Z', 'session-exit-code', 'toolu_exit_bash', 'Bash', {
command: 'pytest tests/integration/test_pipeline.py',
}),
]);
const result = run(['--home', homeDir, '--now', NOW, '--json', '--exit-code']);
assert.strictEqual(result.code, 2, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions[0].state, 'attention');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('exit-code mode returns 1 for scan errors without attention signals', () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-missing-'));
const missingTranscript = path.join(tempDir, 'missing.jsonl');
const result = run(['--transcript', missingTranscript, '--now', NOW, '--json', '--exit-code']);
try {
assert.strictEqual(result.code, 1, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.sessions.length, 0);
assert.strictEqual(payload.errors.length, 1);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('exit-code mode rejects unbounded watch mode', () => {
const result = run(['--watch', '--exit-code']);
assert.strictEqual(result.code, 1);
assert.match(result.stderr, /--exit-code with --watch requires --watch-count/);
})) passed++; else failed++;
if (test('getStatusExitCode prioritizes attention signals over scan errors', () => {
const payload = {
errors: [{ message: 'unreadable' }],
sessions: [{ state: 'attention' }],
};
assert.strictEqual(getStatusExitCode(payload), 2);
})) passed++; else failed++;
if (test('watch mode emits repeated JSON status frames', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-watch', 'session-watch.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-watch', 'toolu_watch', 'ScheduleWakeup', {
delaySeconds: 300,
reason: 'Loop checkpoint',
}),
]);
const result = run([
'--home',
homeDir,
'--now',
NOW,
'--json',
'--watch',
'--watch-count',
'2',
'--watch-interval-seconds',
'0.01',
]);
assert.strictEqual(result.code, 0, result.stderr);
const frames = result.stdout.trim().split(/\r?\n/).map(line => JSON.parse(line));
assert.strictEqual(frames.length, 2);
assert.strictEqual(frames[0].schemaVersion, 'ecc.loop-status.v1');
assert.strictEqual(frames[1].schemaVersion, 'ecc.loop-status.v1');
assert.strictEqual(frames[0].sessions[0].sessionId, 'session-watch');
assert.strictEqual(frames[1].sessions[0].sessionId, 'session-watch');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('watch mode honors exit-code after bounded refreshes', () => {
const homeDir = createTempHome();
try {
writeTranscript(homeDir, '-Users-affoon-project-watch-exit', 'session-watch-exit.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-watch-exit', 'toolu_watch_exit', 'ScheduleWakeup', {
delaySeconds: 300,
reason: 'Loop checkpoint',
}),
]);
const result = run([
'--home',
homeDir,
'--now',
NOW,
'--json',
'--watch',
'--watch-count',
'1',
'--watch-interval-seconds',
'0.01',
'--exit-code',
]);
assert.strictEqual(result.code, 2, result.stderr);
const frame = JSON.parse(result.stdout.trim());
assert.strictEqual(frame.sessions[0].state, 'attention');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('writes per-session status snapshots and index when write-dir is set', () => {
const homeDir = createTempHome();
const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-snapshots-'));
try {
writeTranscript(homeDir, '-Users-affoon-project-snapshot', 'session-snapshot.jsonl', [
toolUse('2026-04-30T09:00:00.000Z', 'session-snapshot', 'toolu_snapshot', 'ScheduleWakeup', {
delaySeconds: 300,
reason: 'Loop checkpoint',
}),
]);
const result = run([
'--home',
homeDir,
'--now',
NOW,
'--json',
'--write-dir',
snapshotDir,
]);
assert.strictEqual(result.code, 0, result.stderr);
const stdoutPayload = parsePayload(result.stdout);
assert.strictEqual(stdoutPayload.schemaVersion, 'ecc.loop-status.v1');
const indexPath = path.join(snapshotDir, 'index.json');
const snapshotPath = path.join(snapshotDir, 'session-snapshot.json');
assert.ok(fs.existsSync(indexPath), 'write-dir should include an index.json file');
assert.ok(fs.existsSync(snapshotPath), 'write-dir should include a per-session snapshot');
const indexPayload = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
assert.strictEqual(indexPayload.schemaVersion, 'ecc.loop-status.index.v1');
assert.strictEqual(indexPayload.sessions.length, 1);
assert.strictEqual(indexPayload.sessions[0].sessionId, 'session-snapshot');
assert.strictEqual(indexPayload.sessions[0].state, 'attention');
assert.strictEqual(indexPayload.sessions[0].snapshotPath, snapshotPath);
const snapshotPayload = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
assert.strictEqual(snapshotPayload.schemaVersion, 'ecc.loop-status.session.v1');
assert.strictEqual(snapshotPayload.generatedAt, NOW);
assert.strictEqual(snapshotPayload.session.sessionId, 'session-snapshot');
assert.ok(snapshotPayload.session.signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(snapshotDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('keeps index.json reserved when session id sanitizes to index', () => {
const homeDir = createTempHome();
const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-index-collision-'));
try {
writeTranscript(homeDir, '-Users-affoon-project-index-collision', 'index.jsonl', [
assistantMessage('2026-04-30T09:55:00.000Z', 'index', 'Loop checkpoint.'),
]);
const result = run([
'--home',
homeDir,
'--now',
NOW,
'--json',
'--write-dir',
snapshotDir,
]);
assert.strictEqual(result.code, 0, result.stderr);
const indexPath = path.join(snapshotDir, 'index.json');
const indexPayload = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
assert.strictEqual(indexPayload.schemaVersion, 'ecc.loop-status.index.v1');
assert.strictEqual(indexPayload.sessions.length, 1);
assert.strictEqual(indexPayload.sessions[0].sessionId, 'index');
assert.notStrictEqual(indexPayload.sessions[0].snapshotPath, indexPath);
const snapshotPayload = JSON.parse(fs.readFileSync(indexPayload.sessions[0].snapshotPath, 'utf8'));
assert.strictEqual(snapshotPayload.schemaVersion, 'ecc.loop-status.session.v1');
assert.strictEqual(snapshotPayload.session.sessionId, 'index');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(snapshotDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('avoids Windows reserved basenames for session snapshots', () => {
const homeDir = createTempHome();
const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-windows-name-'));
try {
writeTranscript(homeDir, '-Users-affoon-project-windows-name', 'con.jsonl', [
assistantMessage('2026-04-30T09:55:00.000Z', 'con', 'Loop checkpoint.'),
]);
writeTranscript(homeDir, '-Users-affoon-project-windows-name', 'con-txt.jsonl', [
assistantMessage('2026-04-30T09:56:00.000Z', 'con.txt', 'Loop checkpoint.'),
]);
const result = run([
'--home',
homeDir,
'--now',
NOW,
'--json',
'--write-dir',
snapshotDir,
]);
assert.strictEqual(result.code, 0, result.stderr);
const indexPath = path.join(snapshotDir, 'index.json');
const indexPayload = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
assert.strictEqual(indexPayload.sessions.length, 2);
for (const sessionIndex of indexPayload.sessions) {
const snapshotName = path.basename(sessionIndex.snapshotPath);
assert.notStrictEqual(snapshotName.toLowerCase(), `${sessionIndex.sessionId}.json`);
assert.ok(!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(snapshotName.split('.')[0]));
const snapshotPayload = JSON.parse(fs.readFileSync(sessionIndex.snapshotPath, 'utf8'));
assert.strictEqual(snapshotPayload.schemaVersion, 'ecc.loop-status.session.v1');
assert.strictEqual(snapshotPayload.session.sessionId, sessionIndex.sessionId);
}
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(snapshotDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('cleans temporary snapshot files when atomic rename fails', () => {
const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-rename-failure-'));
const originalRenameSync = fs.renameSync;
try {
fs.renameSync = () => {
throw new Error('simulated rename failure');
};
assert.throws(() => writeStatusSnapshots({
errors: [],
generatedAt: NOW,
sessions: [
{
eventCount: 1,
lastEventAt: NOW,
pendingTools: [],
recommendedAction: 'No action needed.',
sessionId: 'rename-failure',
signals: [],
state: 'ok',
transcriptPath: path.join(snapshotDir, 'rename-failure.jsonl'),
},
],
source: {},
}, snapshotDir), /simulated rename failure/);
const tempFiles = fs.readdirSync(snapshotDir).filter(fileName => fileName.endsWith('.tmp'));
assert.deepStrictEqual(tempFiles, []);
} finally {
fs.renameSync = originalRenameSync;
fs.rmSync(snapshotDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('write-dir failures do not suppress normal stdout', () => {
const homeDir = createTempHome();
try {
const blockedPath = path.join(homeDir, 'snapshot-target-is-a-file');
fs.writeFileSync(blockedPath, 'not a directory\n', 'utf8');
writeTranscript(homeDir, '-Users-affoon-project-write-error', 'session-write-error.jsonl', [
assistantMessage('2026-04-30T09:55:00.000Z', 'session-write-error', 'Loop checkpoint.'),
]);
const result = run([
'--home',
homeDir,
'--now',
NOW,
'--json',
'--write-dir',
blockedPath,
]);
assert.strictEqual(result.code, 0, result.stderr);
const payload = parsePayload(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.loop-status.v1');
assert.strictEqual(payload.sessions[0].sessionId, 'session-write-error');
assert.match(result.stderr, /\[loop-status\] WARNING: could not write status snapshots:/);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
@@ -0,0 +1,71 @@
/**
* Regression coverage for supported manual Claude hook installation guidance.
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const README = path.join(__dirname, '..', '..', 'README.md');
const HOOKS_README = path.join(__dirname, '..', '..', 'hooks', 'README.md');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing manual hook install docs ===\n');
let passed = 0;
let failed = 0;
const readme = fs.readFileSync(README, 'utf8');
const hooksReadme = fs.readFileSync(HOOKS_README, 'utf8');
if (test('README warns against raw hook file copying', () => {
assert.ok(
readme.includes('Do not copy the raw repo `hooks/hooks.json` into `~/.claude/settings.json` or `~/.claude/hooks/hooks.json`'),
'README should warn against unsupported raw hook copying'
);
assert.ok(
readme.includes('bash ./install.sh --target claude --modules hooks-runtime'),
'README should document the supported Bash hook install path'
);
assert.ok(
readme.includes('pwsh -File .\\install.ps1 --target claude --modules hooks-runtime'),
'README should document the supported PowerShell hook install path'
);
assert.ok(
readme.includes('%USERPROFILE%\\\\.claude'),
'README should call out the correct Windows Claude config root'
);
})) passed++; else failed++;
if (test('hooks/README mirrors supported manual install guidance', () => {
assert.ok(
hooksReadme.includes('do not paste the raw repo `hooks.json` into `~/.claude/settings.json` or copy it directly into `~/.claude/hooks/hooks.json`'),
'hooks/README should warn against unsupported raw hook copying'
);
assert.ok(
hooksReadme.includes('bash ./install.sh --target claude --modules hooks-runtime'),
'hooks/README should document the supported Bash hook install path'
);
assert.ok(
hooksReadme.includes('pwsh -File .\\install.ps1 --target claude --modules hooks-runtime'),
'hooks/README should document the supported PowerShell hook install path'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+202
View File
@@ -0,0 +1,202 @@
/**
* Tests for the npm publish surface contract.
*/
const assert = require("assert")
const fs = require("fs")
const path = require("path")
const { spawnSync } = require("child_process")
function runTest(name, fn) {
try {
fn()
console.log(`${name}`)
return true
} catch (error) {
console.log(`${name}`)
console.error(` ${error.message}`)
return false
}
}
function normalizePublishPath(value) {
return String(value).replace(/\\/g, "/").replace(/\/$/, "")
}
function isCoveredByAncestor(target, roots) {
const parts = target.split("/")
for (let index = 1; index < parts.length; index += 1) {
const ancestor = parts.slice(0, index).join("/")
if (roots.has(ancestor)) {
return true
}
}
return false
}
function buildExpectedPublishPaths(repoRoot) {
const modules = JSON.parse(
fs.readFileSync(path.join(repoRoot, "manifests", "install-modules.json"), "utf8")
).modules
const extraPaths = [
"manifests",
"scripts/ecc.js",
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/control-pane.js",
"scripts/dashboard-web.js",
"scripts/discussion-audit.js",
"scripts/doctor.js",
"scripts/status.js",
"scripts/sessions-cli.js",
"scripts/work-items.js",
"scripts/install-apply.js",
"scripts/install-plan.js",
"scripts/list-installed.js",
"scripts/loop-status.js",
"scripts/observability-readiness.js",
"scripts/plan-canvas.js",
"scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js",
"scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js",
"scripts/skill-create-output.js",
"scripts/repair.js",
"scripts/harness-adapter-compliance.js",
"scripts/session-inspect.js",
"scripts/uninstall.js",
"scripts/gemini-adapt-agents.js",
"scripts/codex/check-plugin-cache.js",
"scripts/codex/merge-codex-config.js",
"scripts/codex/merge-mcp-config.js",
".codex-plugin",
"plugins/ecc",
".mcp.json",
"install.sh",
"install.ps1",
"schemas",
"agent.yaml",
"VERSION",
"assets/ecc-icon.svg",
"assets/hero.png",
]
const exclusionPaths = [
"!**/__pycache__/**",
"!**/*.pyc",
"!**/*.pyo",
"!**/*.pyd",
"!**/.pytest_cache/**",
]
const combined = new Set(
[...modules.flatMap((module) => module.paths || []), ...extraPaths, ...exclusionPaths].map(normalizePublishPath)
)
return [...combined]
.filter((publishPath) => !isCoveredByAncestor(publishPath, combined))
.sort()
}
function main() {
console.log("\n=== Testing npm publish surface ===\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 expectedPublishPaths = buildExpectedPublishPaths(repoRoot)
const actualPublishPaths = packageJson.files.map(normalizePublishPath).sort()
const tests = [
["package.json files align to the module graph and explicit runtime allowlist", () => {
assert.deepStrictEqual(actualPublishPaths, expectedPublishPaths)
}],
["npm pack publishes the reduced runtime surface", () => {
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 packagedPaths = new Set(packOutput[0]?.files?.map((file) => file.path) ?? [])
for (const requiredPath of [
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/control-pane.js",
"scripts/discussion-audit.js",
"scripts/operator-readiness-dashboard.js",
"scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js",
"scripts/work-items.js",
"scripts/platform-audit.js",
"scripts/codex/check-plugin-cache.js",
".gemini/GEMINI.md",
".qwen/QWEN.md",
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
"plugins/ecc/.codex-plugin/plugin.json",
"assets/ecc-icon.svg",
"assets/hero.png",
"schemas/install-state.schema.json",
"skills/backend-patterns/SKILL.md",
]) {
assert.ok(
packagedPaths.has(requiredPath),
`npm pack should include ${requiredPath}`
)
}
for (const excludedPath of [
"contexts/dev.md",
"examples/CLAUDE.md",
"plugins/README.md",
"scripts/ci/catalog.js",
"skills/skill-comply/SKILL.md",
]) {
assert.ok(
!packagedPaths.has(excludedPath),
`npm pack should not include ${excludedPath}`
)
}
for (const packagedPath of packagedPaths) {
assert.ok(
!packagedPath.includes("__pycache__/"),
`npm pack should not include Python bytecode cache path ${packagedPath}`
)
assert.ok(
!/\.py[cod]$/.test(packagedPath),
`npm pack should not include Python bytecode file ${packagedPath}`
)
}
}],
]
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()
@@ -0,0 +1,331 @@
/**
* Tests for scripts/observability-readiness.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'observability-readiness.js');
const { buildReport, parseArgs } = require(SCRIPT);
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function seedMinimalRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
name: 'everything-claude-code',
files: ['scripts/observability-readiness.js'],
scripts: {
'harness:audit': 'node scripts/harness-audit.js',
'observability:ready': 'node scripts/observability-readiness.js'
}
}, null, 2),
'scripts/loop-status.js': '--json --watch --write-dir',
'scripts/session-inspect.js': '--list-adapters --write inspectSessionTarget',
'scripts/lib/session-adapters/registry.js': 'module.exports = {};',
'scripts/harness-audit.js': 'Deterministic harness audit --format overall_score',
'scripts/work-items.js': 'sync-github github-pr github-issue sourceClosedAt ecc-work-items-sync-github',
'scripts/hooks/session-activity-tracker.js': 'tool-usage.jsonl session_id tool_name',
'ecc2/src/observability/mod.rs': 'ToolCallEvent RiskAssessment ToolLogger',
'ecc2/src/session/store.rs': 'insert_tool_log query_tool_logs',
'ecc2/src/session/manager.rs': 'sync_tool_activity_metrics tool-usage.jsonl',
'docs/architecture/observability-readiness.md': 'node scripts/observability-readiness.js --format json',
'docs/architecture/progress-sync-contract.md': [
'Linear GitHub handoff work-items issue capacity status update',
'queue counts release gate flow lanes evidence'
].join('\n'),
'docs/ECC-2.0-GA-ROADMAP.md': [
'Execution Lanes And Tracking Contract',
'docs/architecture/progress-sync-contract.md',
'Linear progress',
'Every significant merge batch'
].join('\n'),
'docs/architecture/hud-status-session-control.md': [
'context toolCalls activeAgents todos checks cost risk queueState',
'create resume status stop diff pr mergeQueue conflictQueue',
'Linear GitHub handoff'
].join('\n'),
'examples/hud-status-contract.json': JSON.stringify({
schema_version: 'ecc.hud-status.v1',
context: {},
toolCalls: {},
activeAgents: [],
todos: {},
checks: {},
cost: {},
risk: {},
queueState: {},
sessionControls: {},
sync: {}
}, null, 2),
'docs/releases/2.0.0-rc.1/quickstart.md': 'observability-readiness.md',
'docs/releases/2.0.0-rc.1/release-notes.md': 'observability-readiness.md',
'docs/releases/2.0.0-rc.1/publication-readiness.md': [
'Publication Gates',
'Required Command Evidence',
'Do Not Publish If',
'npm dist-tag',
'GitGuardian',
'Dependabot alerts',
'npm audit signatures'
].join('\n'),
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md': [
'npm audit --json',
'npm audit signatures',
'cargo audit',
'Dependabot alert API',
'TanStack',
'Mini Shai-Hulud',
'GitGuardian Security Checks'
].join('\n'),
'docs/security/supply-chain-incident-response.md': [
'TanStack',
'Mini Shai-Hulud',
'scan-supply-chain-iocs.js',
'gh-token-monitor',
'.claude/settings.json',
'.vscode/tasks.json',
'npm audit signatures',
'trusted publishing',
'pull_request_target',
'id-token: write'
].join('\n'),
'scripts/ci/validate-workflow-security.js': [
'persist-credentials: false',
'npm audit signatures',
'pull_request_target',
'id-token: write',
'shared cache'
].join('\n'),
'scripts/ci/scan-supply-chain-iocs.js': 'TanStack Mini Shai-Hulud gh-token-monitor',
'tests/ci/scan-supply-chain-iocs.test.js': 'scan-supply-chain-iocs',
'tests/ci/validate-workflow-security.test.js': 'npm audit signatures persist-credentials: false',
'tests/scripts/npm-publish-surface.test.js': 'npm pack --dry-run Python bytecode',
'tests/docs/ecc2-release-surface.test.js': 'publication-readiness.md',
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing observability-readiness.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts supported forms and rejects invalid input', () => {
const rootDir = createTempDir('observability-readiness-args-');
try {
assert.strictEqual(parseArgs(['node', 'script', '--help']).help, true);
assert.strictEqual(parseArgs(['node', 'script', '-h']).help, true);
const spaced = parseArgs(['node', 'script', '--format', 'json', '--root', rootDir]);
assert.strictEqual(spaced.format, 'json');
assert.strictEqual(spaced.root, path.resolve(rootDir));
const equals = parseArgs(['node', 'script', '--format=json', `--root=${rootDir}`]);
assert.strictEqual(equals.format, 'json');
assert.strictEqual(equals.root, path.resolve(rootDir));
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format: xml/);
assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument: --unknown/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('cli help exits cleanly and invalid cli args exit with stderr', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/observability-readiness.js'));
const invalid = runProcess(['--format', 'xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.ok(invalid.stderr.includes('Error: Invalid format: xml. Use text or json.'));
})) passed++; else failed++;
if (test('current repo reports a complete readiness score', () => {
const parsed = JSON.parse(run(['--format=json']));
assert.strictEqual(parsed.schema_version, 'ecc.observability-readiness.v1');
assert.strictEqual(parsed.deterministic, true);
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.overall_score, parsed.max_score);
assert.strictEqual(parsed.top_actions.length, 0);
})) passed++; else failed++;
if (test('text output includes summary, categories, and checks', () => {
const output = run();
assert.ok(output.includes('Observability Readiness:'));
assert.ok(output.includes('Categories:'));
assert.ok(output.includes('Checks:'));
assert.ok(output.includes('PASS loop-status-live-signal'));
})) passed++; else failed++;
if (test('minimal seeded repo passes all checks', () => {
const projectRoot = createTempDir('observability-readiness-pass-');
try {
seedMinimalRepo(projectRoot);
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, true);
assert.strictEqual(report.overall_score, report.max_score);
assert.deepStrictEqual(report.top_actions, []);
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('missing tool logger surfaces become prioritized top actions', () => {
const projectRoot = createTempDir('observability-readiness-fail-');
try {
seedMinimalRepo(projectRoot, {
'ecc2/src/observability/mod.rs': 'ToolCallEvent only'
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.top_actions.some(action => action.id === 'ecc2-tool-risk-ledger'));
assert.ok(report.checks.some(check => check.id === 'ecc2-tool-risk-ledger' && !check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('missing release onramp fails without disturbing core tool checks', () => {
const projectRoot = createTempDir('observability-readiness-doc-fail-');
try {
seedMinimalRepo(projectRoot, {
'docs/releases/2.0.0-rc.1/quickstart.md': 'quickstart without link'
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'release-observability-onramp' && !check.pass));
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('missing HUD status contract fails without disturbing core tool checks', () => {
const projectRoot = createTempDir('observability-readiness-hud-fail-');
try {
seedMinimalRepo(projectRoot, {
'examples/hud-status-contract.json': null
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'hud-status-control-contract' && !check.pass));
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('missing progress sync contract fails without disturbing core tool checks', () => {
const projectRoot = createTempDir('observability-readiness-sync-fail-');
try {
seedMinimalRepo(projectRoot, {
'docs/architecture/progress-sync-contract.md': null
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'progress-sync-contract' && !check.pass));
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('missing release safety evidence fails without disturbing live status checks', () => {
const projectRoot = createTempDir('observability-readiness-release-safety-fail-');
try {
seedMinimalRepo(projectRoot, {
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md': 'npm audit --json only'
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'release-safety-evidence' && !check.pass));
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
console.log('\nResults:');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}
@@ -0,0 +1,92 @@
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT = path.join(
__dirname,
'..',
'..',
'skills',
'openclaw-persona-forge',
'gacha.py'
);
function findPython() {
const candidates = process.platform === 'win32'
? ['python', 'python3']
: ['python3', 'python'];
for (const candidate of candidates) {
const result = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
if (result.status === 0) {
return candidate;
}
}
return null;
}
function runGacha(pythonBin, arg) {
return spawnSync(pythonBin, [SCRIPT, arg], {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
env: { ...process.env, PYTHONUTF8: '1' },
});
}
function runTest(name, fn) {
try {
fn();
console.log(` PASS: ${name}`);
return true;
} catch (error) {
console.log(` FAIL: ${name}`);
console.error(` ${error.message}`);
return false;
}
}
function assertSingleDrawOutput(result) {
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /\[身份\] 前世身份:/);
assert.match(result.stdout, /\[概括\] 一句话概括:/);
}
function main() {
console.log('\n=== Testing openclaw-persona-forge/gacha.py ===\n');
const pythonBin = findPython();
if (!pythonBin) {
console.log(' PASS: skipped (python runtime unavailable)');
return;
}
let passed = 0;
let failed = 0;
const tests = [
['clamps zero draws to one', () => {
assertSingleDrawOutput(runGacha(pythonBin, '0'));
}],
['clamps negative draws to one', () => {
assertSingleDrawOutput(runGacha(pythonBin, '-3'));
}],
];
for (const [name, fn] of tests) {
if (runTest(name, fn)) {
passed += 1;
} else {
failed += 1;
}
}
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
main();
@@ -0,0 +1,840 @@
/**
* Tests for scripts/operator-readiness-dashboard.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'operator-readiness-dashboard.js');
const { buildReport, parseArgs, renderMarkdown, renderText } = require(SCRIPT);
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function seedRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
name: 'everything-claude-code',
files: [
'scripts/observability-readiness.js',
'scripts/operator-readiness-dashboard.js',
'scripts/platform-audit.js',
'scripts/preview-pack-smoke.js',
'scripts/release-video-suite.js'
],
scripts: {
'discussion:audit': 'node scripts/discussion-audit.js',
'observability:ready': 'node scripts/observability-readiness.js',
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'platform:audit': 'node scripts/platform-audit.js',
'preview-pack:smoke': 'node scripts/preview-pack-smoke.js',
'release:video-suite': 'node scripts/release-video-suite.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js'
}
}, null, 2),
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator',
'scripts/preview-pack-smoke.js': [
'ecc.preview-pack-smoke.v1',
'preview-pack-artifacts-present',
'hermes-boundary-sanitized',
'publication-blockers-preserved'
].join('\n'),
'scripts/release-video-suite.js': [
'ecc.release-video-suite.v1',
'video-source-assets-present',
'video-release-artifacts-present'
].join('\n'),
'docs/ECC-2.0-GA-ROADMAP.md': [
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
'Linear ITO-44 ITO-59',
'AgentShield PR #92 #78-#92 checksum-backed policy export policy promote checksum-verified policy promotion',
'AgentShield Enterprise Iteration',
'ECC-Tools PR #78',
'hosted promotion',
'operator-visible promotion output values',
'hosted promotion judge audit traces',
'package-manager hardening Action outputs',
'production Marketplace readback state',
'eb69412',
'Marketplace webhook provenance',
'2859678',
'Wrangler OAuth readback',
'42653f9',
'target account billing readback',
'632e059',
'select-ready-target',
'selected-target official announcement gate',
'billing gate env-file operator path',
'non-breaking operator bearer path',
'announcementGateReady` is `true',
'd3d62df83fa075660fa4530c3e0edc311a4355fe',
'72119a1',
'16a5bb3',
'f14ed2fe-a219-470c-8119-63429e197027',
'old "no Marketplace-managed Pro target billing-state" blocker is cleared',
'30f60710',
'26135974576',
'467d148a-712a-4777-aad9-95593e9f1739',
'7642ee9c-3107-400c-a229-53e2895a8914',
'69ca535',
'team feedback controls',
'e56fc1a',
'1Password CLI authorization timed out',
'Cloudflare API auth returned `Authentication error [code: 10000]`',
'announcementGate',
'ITO-55',
'Linear live sync is current for the May 17 merge batch',
'operator progress snapshot'
].join('\n'),
'docs/releases/2.0.0-rc.1/publication-readiness.md': 'Claude plugin Codex plugin release-name-plugin-publication-checklist-2026-05-18.md',
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md': 'Claude plugin Codex plugin npm package Publication Paths',
'docs/releases/2.0.0-rc.1/release-name-plugin-publication-checklist-2026-05-18.md': [
'Ship `v2.0.0-rc.1` as **ECC**',
'affaan-m/ECC',
'ecc-universal',
'claude plugin tag .claude-plugin --dry-run',
'codex plugin marketplace add',
'Do not rename the npm package until rc.1 is published'
].join('\n'),
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': [
'publication-readiness.md release-notes.md quickstart.md',
'release-name-plugin-publication-checklist-2026-05-18.md',
'owner-approval-packet-2026-05-19.md',
'`scripts/preview-pack-smoke.js`',
'npm run preview-pack:smoke'
].join('\n'),
'docs/releases/2.0.0-rc.1/owner-approval-packet-2026-05-19.md': [
'Owner Approval Packet',
'Decision Register',
'GitHub prerelease',
'npm `next` publish',
'Claude plugin tag',
'Video upload',
'Final URL Fill-In',
'Do Not Approve If',
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.'
].join('\n'),
'docs/releases/2.0.0-rc.1/release-notes.md': 'release notes',
'docs/releases/2.0.0-rc.1/x-thread.md': 'x thread',
'docs/releases/2.0.0-rc.1/linkedin-post.md': 'linkedin post',
'docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md': [
'This dashboard is generated by `npm run operator:dashboard`',
'operator:dashboard',
'Prompt-To-Artifact Checklist',
'Next Work Order',
'ITO-44',
'ITO-59',
'PR queue',
'Not complete'
].join('\n'),
'docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md': [
'This dashboard is generated by `npm run operator:dashboard`',
'operator:dashboard',
'Growth Baseline',
'hypergrowth release command center',
'Prompt-To-Artifact Checklist',
'Next Work Order',
'ITO-44',
'ITO-59',
'PR queue',
'Not complete'
].join('\n'),
'docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-20.md': [
'This dashboard is generated by `npm run operator:dashboard`',
'operator:dashboard',
'Growth Baseline',
'hypergrowth release command center',
'Prompt-To-Artifact Checklist',
'Next Work Order',
'ITO-44',
'ITO-59',
'PR queue',
'Not complete'
].join('\n'),
'docs/releases/2.0.0-rc.1/owner-queue-cleanup-2026-05-18.md': [
'Owner-wide open PRs after cleanup: 0.',
'Owner-wide open issues after cleanup: 0.',
'Stale dependency-bot PRs closed: 24.',
'Stale legacy payments/0EM roadmap issues closed: 72.'
].join('\n'),
'docs/HERMES-SETUP.md': 'Hermes setup Public Release Candidate Scope',
'skills/hermes-imports/SKILL.md': 'Hermes imports Sanitization Checklist Do not ship raw workspace exports Output Contract',
'docs/stale-pr-salvage-ledger.md': [
'Remaining Manual-Review Backlog',
'Linear ITO-55',
'#1687 zh-CN localization tail',
'#1609 Persian README translation',
'#1563 zh-TW README sync',
'#1564 Turkish README sync',
'#1565 pt-BR README sync',
'not a release-blocking salvage task'
].join('\n'),
'docs/legacy-artifact-inventory.md': [
'Translator/manual review',
'ITO-55',
'#1687 zh-CN localization tail',
'#1609 Persian README translation',
'#1563 zh-TW README sync',
'#1564 Turkish README sync',
'#1565 pt-BR README sync',
'no automatic import remains release-blocking'
].join('\n'),
'docs/architecture/progress-sync-contract.md': [
'GitHub PRs/issues/discussions Linear project local handoff repo roadmap scripts/work-items.js',
'node scripts/work-items.js sync-github --repo <owner/repo>',
'node scripts/status.js --json',
'Linear remains the external status surface'
].join('\n'),
'docs/architecture/observability-readiness.md': 'observability-readiness.js',
'docs/security/supply-chain-incident-response.md': 'TanStack Mini Shai-Hulud node-ipc scan-supply-chain-iocs.js supply-chain-advisory-sources.js',
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-18.md': [
'TanStack',
'Mini Shai-Hulud',
'Home persistence IOC scan',
'Supply-Chain Watch',
'npm signatures',
'Node IPC follow-up node-ipc IOC scan'
].join('\n'),
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md': [
'Release video suite',
'growth outreach',
'Operator dashboard',
'GitGuardian',
'macOS/Ubuntu/Windows test matrix',
'2568 passed',
'Business baseline',
'$1,728/mo',
'$8,272/mo'
].join('\n'),
'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md': [
'harness-native operator system',
'| MRR | `$1,728/mo` | `$10,000/mo` | `$8,272/mo` |',
'Video Suite',
'Distribution Plan',
'Owner Approvals'
].join('\n'),
'docs/releases/2.0.0-rc.1/video-suite-production.md': [
'ECC 2.0 Video Suite Production Manifest',
'Primary launch video',
'Self-Eval Gate',
'timeline'
].join('\n'),
'docs/releases/2.0.0-rc.1/partner-sponsor-talks-pack.md': [
'Sponsor Outbound',
'Platform Partner DM',
'Consulting Intro',
'Talk And Podcast Pitch',
'GitHub Discussion Announcement',
'Do Not Send Or Publish If'
].join('\n'),
'.github/workflows/supply-chain-watch.yml': 'name: Supply-Chain Watch supply-chain-advisory-sources.js supply-chain-advisory-sources.json'
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function buildSeededReport(rootDir) {
return buildReport({
allowUntracked: [],
exitCode: false,
format: 'json',
generatedAt: '2026-05-15T00:00:00.000Z',
help: false,
repos: [],
root: rootDir,
skipGithub: true,
thresholds: { maxOpenPrs: 20, maxOpenIssues: 20, maxDirtyFiles: 0 },
useEnvGithubToken: false,
writePath: null
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing operator-readiness-dashboard.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts dashboard flags and rejects invalid values', () => {
const rootDir = createTempDir('operator-dashboard-args-');
try {
const parsed = parseArgs([
'node',
'script',
'--format=json',
`--root=${rootDir}`,
'--skip-github',
'--allow-untracked',
'docs/drafts/',
'--repo',
'affaan-m/ECC',
'--generated-at',
'2026-05-15T00:00:00.000Z'
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.strictEqual(parsed.skipGithub, true);
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
assert.deepStrictEqual(parsed.repos, ['affaan-m/ECC']);
assert.strictEqual(parsed.generatedAt, '2026-05-15T00:00:00.000Z');
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--write', 'dashboard.md', '--format', 'text']), /--write requires/);
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('seeded repo emits an objective audit with remaining work', () => {
const rootDir = createTempDir('operator-dashboard-report-');
try {
seedRepo(rootDir);
const report = buildSeededReport(rootDir);
assert.strictEqual(report.schema_version, 'ecc.operator-readiness-dashboard.v1');
assert.strictEqual(report.generatedAt, '2026-05-15T00:00:00.000Z');
assert.strictEqual(report.dashboardReady, true);
assert.strictEqual(report.ready, false);
assert.strictEqual(report.publicationReady, false);
assert.ok(report.requirements.some(item => item.id === 'completion-dashboard' && item.status === 'complete'));
assert.ok(report.requirements.some(item => (
item.id === 'ecc-preview-pack'
&& item.status === 'current'
&& item.evidence.includes('deterministic smoke gate')
&& item.gap === 'repeat clean-checkout preview-pack smoke before publication'
)));
assert.ok(report.requirements.some(item => (
item.id === 'hermes-specialized-skills'
&& item.status === 'current'
&& item.evidence.includes('covered by preview-pack smoke')
&& item.gap === 'repeat preview-pack smoke before release review'
)));
assert.ok(report.requirements.some(item => item.id === 'ecc-tools-next-level' && item.status === 'in_progress'));
assert.ok(report.requirements.some(item => (
item.id === 'agentshield-enterprise-iteration'
&& item.gap === 'deepen live operator approval/readback after Marketplace/payment gates'
&& item.evidence.includes('policy-promotion Action outputs')
&& item.evidence.includes('hosted promotion judge audit traces')
)));
assert.ok(report.requirements.some(item => (
item.id === 'ecc-tools-next-level'
&& item.gap === 'repeat KV readback and selected-target announcement gate immediately before launch; keep native-payments copy behind the final release, plugin, URL, and owner-approval gates'
&& item.evidence.includes('operator-visible promotion output details')
&& item.evidence.includes('hosted promotion judge audit traces')
&& item.evidence.includes('selected-target announcement gate')
&& item.evidence.includes('billing gate env-file operator path')
&& item.evidence.includes('non-breaking operator bearer path')
&& item.evidence.includes('billing announcement preflight')
&& item.evidence.includes('aggregate production billing KV readback')
&& item.evidence.includes('Wrangler selected-target readback')
&& item.evidence.includes('target-account billing readback')
&& item.evidence.includes('provenance-aware Marketplace billing-state gates')
&& item.evidence.includes('ready Marketplace Pro target selection')
&& item.evidence.includes('hosted team-learning feedback controls')
&& item.evidence.includes('ECC-Tools Dependabot alert remediation')
)));
assert.ok(report.requirements.some(item => (
item.id === 'naming-and-plugin-publication'
&& item.artifact.includes('release-name-plugin-publication checklist')
&& item.evidence.includes('release publication checklist')
&& item.gap === 'real tag/push, marketplace submission, and final channel choice remain approval-gated'
)));
assert.deepStrictEqual(report.growth, {
currentMrr: '$1,728/mo',
targetMrr: '$10,000/mo',
gapMrr: '$8,272/mo',
lanes: [
'GitHub Sponsors and OSS partner sponsors',
'ECC Tools Pro subscriptions',
'consulting and implementation contracts',
'talks, podcasts, conference demos, and partner webinars',
],
});
assert.ok(report.requirements.some(item => (
item.id === 'hypergrowth-command-center'
&& item.status === 'current'
&& item.evidence.includes('current MRR')
&& item.gap === 'refresh after every MRR, channel, or approval-state change before public launch'
)));
assert.ok(report.requirements.some(item => (
item.id === 'release-video-suite'
&& item.status === 'in_progress'
&& item.evidence.includes('deterministic video-suite gate')
&& item.gap === 'render final owner-approved MP4s, captions, platform reframes, and editable timeline before posting'
)));
assert.ok(report.requirements.some(item => (
item.id === 'partner-sponsor-talks-pack'
&& item.status === 'in_progress'
&& item.evidence.includes('sponsor outbound')
&& item.gap === 'replace final URLs after publication gates, then get explicit approval before outbound or personal-account posts'
)));
assert.ok(report.requirements.some(item => (
item.id === 'owner-approval-packet'
&& item.status === 'current'
&& item.evidence.includes('release, package, plugin, video, billing, social, and outbound decisions')
&& item.gap === 'review owner approvals from the final release commit before any publication or outbound action'
)));
assert.ok(report.requirements.some(item => (
item.id === 'supply-chain-local-protection'
&& item.artifact.includes('AgentShield package-manager hardening')
&& item.evidence.includes('known AI-tool persistence IOCs')
&& item.evidence.includes('unsupported npm age-key drift')
&& item.gap === 'repeat advisory/source refresh and Linear sync after each significant supply-chain batch'
)));
assert.ok(report.requirements.some(item => (
item.id === 'legacy-salvage'
&& item.status === 'current'
&& item.evidence.includes('all localization tails are attached to Linear ITO-55')
&& item.gap === 'repeat legacy scan before release'
)));
assert.ok(report.requirements.some(item => (
item.id === 'linear-roadmap-and-progress'
&& item.status === 'current'
&& item.evidence.includes('May 20 Marketplace Pro release-gate comments')
&& item.gap === 'repeat Linear/project status update and local work-items sync after each significant merge batch'
)));
assert.ok(report.top_actions.some(item => item.id === 'naming-and-plugin-publication'));
assert.ok(report.top_actions.some(item => item.id === 'release-video-suite'));
assert.ok(report.top_actions.some(item => item.id === 'partner-sponsor-talks-pack'));
assert.ok(!report.top_actions.some(item => item.id === 'owner-approval-packet'));
assert.ok(!report.top_actions.some(item => item.id === 'ecc-preview-pack'));
assert.ok(!report.top_actions.some(item => item.id === 'hermes-specialized-skills'));
assert.ok(!report.top_actions.some(item => item.id === 'hypergrowth-command-center'));
assert.ok(!report.top_actions.some(item => item.id === 'legacy-salvage'));
assert.ok(!report.top_actions.some(item => item.id === 'linear-roadmap-and-progress'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('release video suite moves current when publish-candidate evidence is recorded', () => {
const rootDir = createTempDir('operator-dashboard-video-current-');
try {
seedRepo(rootDir, {
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md': [
'Release video suite',
'growth outreach',
'Operator dashboard',
'GitGuardian',
'macOS/Ubuntu/Windows test matrix',
'2568 passed',
'Business baseline',
'$1,728/mo',
'$8,272/mo',
'Ready true',
'15/15 source assets present',
'13/13 render, timeline, caption, EDL, and segment artifacts present',
'12/12 publish-candidate outputs present with zero detected black-frame segments',
'primary rough render self-eval passed'
].join('\n')
});
const report = buildSeededReport(rootDir);
const releaseVideo = report.requirements.find(item => item.id === 'release-video-suite');
assert.strictEqual(releaseVideo.status, 'current');
assert.ok(releaseVideo.evidence.includes('15/15 source assets'));
assert.ok(releaseVideo.evidence.includes('12/12 publish candidates'));
assert.ok(releaseVideo.evidence.includes('zero detected black-frame segments'));
assert.strictEqual(releaseVideo.gap, 'final owner approval, upload, and public video URLs remain approval-gated');
assert.ok(!report.top_actions.some(item => item.id === 'release-video-suite'));
assert.ok(report.next_work_order.some(item => item.includes('Review the owner-approved primary launch video candidates')));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('Linear progress stays in progress until live sync evidence is mirrored', () => {
const rootDir = createTempDir('operator-dashboard-linear-progress-');
try {
seedRepo(rootDir, {
'docs/ECC-2.0-GA-ROADMAP.md': [
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
'Linear ITO-44 ITO-59',
'AgentShield Enterprise Iteration',
'ECC-Tools PR #78',
'hosted promotion',
'announcementGate',
'ITO-55'
].join('\n')
});
const report = buildSeededReport(rootDir);
const linearProgress = report.requirements.find(item => item.id === 'linear-roadmap-and-progress');
assert.strictEqual(linearProgress.status, 'in_progress');
assert.strictEqual(linearProgress.evidence, 'repo mirror and progress-sync contract are present');
assert.strictEqual(linearProgress.gap, 'recurring Linear status sync and productized realtime sync remain pending');
assert.ok(report.top_actions.some(item => item.id === 'linear-roadmap-and-progress'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('preview pack and Hermes gates stay in progress until smoke gate is wired', () => {
const rootDir = createTempDir('operator-dashboard-preview-smoke-');
try {
seedRepo(rootDir, {
'package.json': JSON.stringify({
files: [
'scripts/observability-readiness.js',
'scripts/operator-readiness-dashboard.js',
'scripts/platform-audit.js'
],
scripts: {
'discussion:audit': 'node scripts/discussion-audit.js',
'observability:ready': 'node scripts/observability-readiness.js',
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'platform:audit': 'node scripts/platform-audit.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js'
}
}, null, 2),
'scripts/preview-pack-smoke.js': null,
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md'
});
const report = buildSeededReport(rootDir);
const previewPack = report.requirements.find(item => item.id === 'ecc-preview-pack');
const hermes = report.requirements.find(item => item.id === 'hermes-specialized-skills');
assert.strictEqual(previewPack.status, 'in_progress');
assert.strictEqual(previewPack.gap, 'final clean-checkout release approval and publish evidence still pending');
assert.strictEqual(hermes.status, 'in_progress');
assert.strictEqual(hermes.gap, 'final preview-pack smoke and release review pending');
assert.ok(report.top_actions.some(item => item.id === 'ecc-preview-pack'));
assert.ok(report.top_actions.some(item => item.id === 'hermes-specialized-skills'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('owner approval packet fails closed when it is missing from the release pack', () => {
const rootDir = createTempDir('operator-dashboard-owner-packet-');
try {
seedRepo(rootDir, {
'docs/releases/2.0.0-rc.1/owner-approval-packet-2026-05-19.md': null,
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': [
'publication-readiness.md release-notes.md quickstart.md',
'release-name-plugin-publication-checklist-2026-05-18.md',
'`scripts/preview-pack-smoke.js`',
'npm run preview-pack:smoke'
].join('\n')
});
const report = buildSeededReport(rootDir);
const ownerPacket = report.requirements.find(item => item.id === 'owner-approval-packet');
assert.strictEqual(ownerPacket.status, 'not_complete');
assert.strictEqual(ownerPacket.evidence, 'owner approval packet is missing or incomplete');
assert.strictEqual(ownerPacket.gap, 'add the owner decision sheet before publication review');
assert.ok(report.top_actions.some(item => item.id === 'owner-approval-packet'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('AgentShield enterprise evidence covers export and policy promotion markers', () => {
const cases = [
{
marker: 'AgentShield PR #92',
gap: 'workflow automation around protected rollout and richer runtime review UX pending after policy promotion shipped'
},
{
marker: 'AgentShield #92',
gap: 'workflow automation around protected rollout and richer runtime review UX pending after policy promotion shipped'
},
{
marker: 'policy promote',
gap: 'workflow automation around protected rollout and richer runtime review UX pending after policy promotion shipped'
},
{
marker: 'checksum-verified policy promotion',
gap: 'workflow automation around protected rollout and richer runtime review UX pending after policy promotion shipped'
},
{
marker: 'hosted promotion judge audit traces',
gap: 'deepen live operator approval/readback after Marketplace/payment gates'
},
{
marker: '#78-#91',
gap: 'workflow automation plus policy promotion/review UX pending after policy export shipped'
},
{
marker: 'AgentShield PR #91',
gap: 'workflow automation plus policy promotion/review UX pending after policy export shipped'
},
{
marker: 'AgentShield #91',
gap: 'workflow automation plus policy promotion/review UX pending after policy export shipped'
},
{
marker: 'checksum-backed policy export',
gap: 'workflow automation plus policy promotion/review UX pending after policy export shipped'
},
{
marker: '#78-#90',
gap: 'durable policy export and fleet-review workflow automation remain pending after reviewItems shipped'
}
];
for (const { marker, gap } of cases) {
const rootDir = createTempDir('operator-dashboard-agentshield-');
try {
seedRepo(rootDir, {
'docs/ECC-2.0-GA-ROADMAP.md': [
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
'Linear ITO-44 ITO-59',
'AgentShield Enterprise Iteration',
marker,
'ECC-Tools PR #78',
'hosted promotion',
'announcementGate',
'ITO-55'
].join('\n')
});
const report = buildSeededReport(rootDir);
const item = report.requirements.find(requirement => requirement.id === 'agentshield-enterprise-iteration');
assert.strictEqual(item.status, 'in_progress', marker);
assert.strictEqual(item.gap, gap, marker);
} finally {
cleanup(rootDir);
}
}
})) passed++; else failed++;
if (test('legacy salvage recognizes the real manual-review backlog heading', () => {
const rootDir = createTempDir('operator-dashboard-legacy-salvage-');
try {
seedRepo(rootDir, {
'docs/ECC-2.0-GA-ROADMAP.md': [
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
'Linear ITO-44 ITO-59',
'AgentShield PR #92 #78-#92 checksum-backed policy export policy promote checksum-verified policy promotion',
'AgentShield Enterprise Iteration',
'ECC-Tools PR #78',
'hosted promotion',
'announcementGate'
].join('\n'),
'docs/stale-pr-salvage-ledger.md': [
'# Stale PR Salvage Ledger',
'',
'## Remaining Manual-Review Backlog',
'',
'- #1609 Persian README translation',
'- #1563 zh-TW README sync'
].join('\n')
});
const report = buildSeededReport(rootDir);
const legacySalvage = report.requirements.find(item => item.id === 'legacy-salvage');
assert.strictEqual(legacySalvage.status, 'in_progress');
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('markdown output can be written as the dashboard artifact', () => {
const rootDir = createTempDir('operator-dashboard-markdown-');
const outputPath = path.join(rootDir, 'artifacts', 'dashboard.md');
try {
seedRepo(rootDir);
const stdout = run([
'--markdown',
'--skip-github',
`--root=${rootDir}`,
'--generated-at=2026-05-15T00:00:00.000Z',
'--write',
outputPath
], { cwd: rootDir });
const written = fs.readFileSync(outputPath, 'utf8');
assert.strictEqual(stdout, written);
assert.ok(written.includes('# ECC Operator Readiness Dashboard'));
assert.ok(written.includes('Generated: 2026-05-15T00:00:00.000Z'));
assert.ok(written.includes('## Growth Baseline'));
assert.ok(written.includes('| MRR | $1,728/mo | $10,000/mo | $8,272/mo |'));
assert.ok(written.includes('## Prompt-To-Artifact Checklist'));
assert.ok(written.includes('Build ITO-44 completion dashboard into a repeatable command'));
assert.ok(written.includes('## Next Work Order'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('text output renders compact status and top actions', () => {
const rootDir = createTempDir('operator-dashboard-text-');
try {
seedRepo(rootDir);
const stdout = run([
'--format=text',
'--skip-github',
`--root=${rootDir}`,
'--generated-at=2026-05-15T00:00:00.000Z'
], { cwd: rootDir });
assert.ok(stdout.includes('ECC Operator Readiness Dashboard'));
assert.ok(stdout.includes('work remaining'));
assert.ok(stdout.includes('Dashboard ready: true'));
assert.ok(stdout.includes('Publication ready: false'));
assert.ok(stdout.includes('MRR: $1,728/mo -> $10,000/mo (gap $8,272/mo)'));
assert.ok(stdout.includes('Top actions:'));
assert.ok(stdout.includes('naming-and-plugin-publication'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('renderers handle a ready report with no top actions', () => {
const report = {
dashboardReady: true,
generatedAt: '2026-05-15T00:00:00.000Z',
head: 'abc123',
next_work_order: ['Ship release evidence'],
platform: {
blockingDirtyCount: 0,
discussionsMissingAcceptedAnswer: 0,
discussionsNeedingMaintainerTouch: 0,
githubSkipped: false,
ignoredDirtyCount: 0,
openIssues: 1,
openPrs: 1,
ready: true
},
publicationReady: true,
ready: true,
requirements: [
{
artifact: 'artifact.md',
evidence: 'verified',
gap: '',
id: 'release',
requirement: 'Release is approved',
status: 'complete'
}
],
top_actions: []
};
const text = renderText(report);
assert.ok(text.includes('objective ready'));
assert.ok(text.includes('Commit: abc123'));
assert.ok(text.includes(' none'));
const markdown = renderMarkdown(report);
assert.ok(markdown.includes('Status: objective ready'));
assert.ok(markdown.includes('| PR queue | Current | 1 open PRs across tracked repos |'));
assert.ok(markdown.includes('| Publication | Ready |'));
assert.ok(markdown.includes('- none'));
})) passed++; else failed++;
if (test('exit-code mode fails closed while macro objective has gaps', () => {
const rootDir = createTempDir('operator-dashboard-exit-');
try {
seedRepo(rootDir);
const result = runProcess([
'--json',
'--skip-github',
`--root=${rootDir}`,
'--generated-at=2026-05-15T00:00:00.000Z',
'--exit-code'
], { cwd: rootDir });
assert.strictEqual(result.status, 2);
assert.strictEqual(result.stderr, '');
assert.ok(result.stdout.includes('"ready": false'));
assert.ok(result.stdout.includes('"publicationReady": false'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('cli help exits successfully and invalid cli flags fail before reporting', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/operator-readiness-dashboard.js'));
assert.ok(help.stdout.includes('--write <path>'));
const invalid = runProcess(['--format=xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.match(invalid.stderr, /Error: Invalid format/);
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}
@@ -0,0 +1,63 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'orchestrate-codex-worker.sh');
console.log('=== Testing orchestrate-codex-worker.sh ===\n');
let passed = 0;
let failed = 0;
function test(desc, fn) {
try {
fn();
console.log(`${desc}`);
passed++;
} catch (error) {
console.log(`${desc}: ${error.message}`);
failed++;
}
}
test('fails fast for an unreadable task file and records failure artifacts', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-worker-'));
const handoffFile = path.join(tempRoot, '.orchestration', 'docs', 'handoff.md');
const statusFile = path.join(tempRoot, '.orchestration', 'docs', 'status.md');
const missingTaskFile = path.join(tempRoot, '.orchestration', 'docs', 'task.md');
try {
spawnSync('git', ['init'], { cwd: tempRoot, stdio: 'ignore' });
const result = spawnSync('bash', [SCRIPT, missingTaskFile, handoffFile, statusFile], {
cwd: tempRoot,
encoding: 'utf8'
});
assert.notStrictEqual(result.status, 0, 'Script should fail when task file is unreadable');
assert.ok(fs.existsSync(statusFile), 'Script should still write a status file');
assert.ok(fs.existsSync(handoffFile), 'Script should still write a handoff file');
const statusContent = fs.readFileSync(statusFile, 'utf8');
const handoffContent = fs.readFileSync(handoffFile, 'utf8');
assert.ok(statusContent.includes('- State: failed'), 'Status file should record the failure state');
assert.ok(
statusContent.includes('task file is missing or unreadable'),
'Status file should explain the task-file failure'
);
assert.ok(
handoffContent.includes('Task file is missing or unreadable'),
'Handoff file should explain the task-file failure'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);
@@ -0,0 +1,76 @@
/**
* Tests for scripts/orchestration-status.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'orchestration-status.js');
function run(args = [], options = {}) {
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
cwd: options.cwd || process.cwd(),
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing orchestration-status.js ===\n');
let passed = 0;
let failed = 0;
if (test('emits canonical dmux snapshots for plan files', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-status-repo-'));
try {
const planPath = path.join(repoRoot, 'workflow.json');
fs.writeFileSync(planPath, JSON.stringify({
sessionName: 'workflow-visual-proof',
repoRoot,
coordinationRoot: path.join(repoRoot, '.claude', 'orchestration')
}));
const result = run([planPath], { cwd: repoRoot });
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.adapterId, 'dmux-tmux');
assert.strictEqual(payload.session.id, 'workflow-visual-proof');
assert.strictEqual(payload.session.sourceTarget.type, 'plan');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+377
View File
@@ -0,0 +1,377 @@
/**
* Integration tests for the Plan Canvas server (scripts/lib/plan-canvas/).
*
* Spins up the real HTTP server in-process and drives it exactly like the
* browser chrome (fetch + SSE) and the agent CLI (long-poll) do.
*
* Run with: node tests/scripts/plan-canvas.test.js
*/
const assert = require('assert');
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const { createSessionStore } = require('../../scripts/lib/plan-canvas/sessions');
const { createPlanCanvasServer } = require('../../scripts/lib/plan-canvas/server');
async function test(name, fn) {
try {
await fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.stack || err.message}`);
return false;
}
}
function request(port, method, requestPath, { body = null, headers = {} } = {}) {
return new Promise((resolve, reject) => {
const payload = body === null ? null : JSON.stringify(body);
const req = http.request(
{
host: '127.0.0.1',
port,
method,
path: requestPath,
agent: false,
headers: payload
? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload), ...headers }
: headers
},
res => {
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, body: data }));
}
);
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
function jsonBody(res) {
return JSON.parse(res.body.trim());
}
// Open an SSE stream and collect parsed events into `received`.
function openSse(port, key) {
const received = [];
let close = () => {};
const ready = new Promise((resolve, reject) => {
const req = http.get(
{ host: '127.0.0.1', port, path: `/events/${key}`, agent: false },
res => {
let buffer = '';
res.on('data', chunk => {
buffer += chunk;
let idx;
while ((idx = buffer.indexOf('\n\n')) >= 0) {
const frame = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
const eventMatch = frame.match(/^event: (.+)$/m);
const dataMatch = frame.match(/^data: (.+)$/m);
if (eventMatch && dataMatch) {
received.push({ event: eventMatch[1], data: JSON.parse(dataMatch[1]) });
}
}
});
resolve();
}
);
req.on('error', reject);
close = () => req.destroy();
});
return { received, ready, close: () => close() };
}
function waitFor(predicate, { timeoutMs = 3000, intervalMs = 20 } = {}) {
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const timer = setInterval(() => {
if (predicate()) {
clearInterval(timer);
resolve();
} else if (Date.now() - startedAt > timeoutMs) {
clearInterval(timer);
reject(new Error('waitFor timed out'));
}
}, intervalMs);
});
}
async function main() {
console.log('\n=== Testing plan-canvas server ===\n');
let passed = 0;
let failed = 0;
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-canvas-server-'));
const artifact = path.join(tmp, 'demo.plan.md');
fs.writeFileSync(artifact, '# Plan: Demo\n\n## Files to Change\n\n| File | Action |\n|---|---|\n| `a.js` | UPDATE |\n');
const htmlArtifact = path.join(tmp, 'report.html');
fs.writeFileSync(htmlArtifact, '<!DOCTYPE html><html><body><h1>Report</h1></body></html>');
fs.writeFileSync(path.join(tmp, 'style.css'), 'body { color: red }');
fs.writeFileSync(path.join(os.tmpdir(), 'plan-canvas-outside.txt'), 'secret');
const store = createSessionStore({ stateDir: path.join(tmp, 'state') });
let idleFired = false;
const canvas = createPlanCanvasServer({
store,
version: '9.9.9-test',
heartbeatMs: 25,
idleTimeoutMs: 0,
onIdleShutdown: () => {
idleFired = true;
}
});
const { port } = await canvas.listen(0);
let key = null;
let htmlKey = null;
if (await test('GET /health identifies the app and version', async () => {
const res = await request(port, 'GET', '/health');
assert.deepStrictEqual(jsonBody(res), { ok: true, app: 'ecc-plan-canvas', version: '9.9.9-test' });
})) passed++; else failed++;
if (await test('requests with a non-loopback Host header are rejected', async () => {
const res = await request(port, 'GET', '/health', { headers: { host: 'evil.example.com' } });
assert.strictEqual(res.statusCode, 403);
})) passed++; else failed++;
if (await test('requests with a cross-site Origin are rejected', async () => {
const res = await request(port, 'POST', '/shutdown', { headers: { origin: 'https://evil.example.com' } });
assert.strictEqual(res.statusCode, 403);
})) passed++; else failed++;
if (await test('POST /api/sessions opens a session for an existing artifact', async () => {
const res = await request(port, 'POST', '/api/sessions', { body: { file: artifact } });
assert.strictEqual(res.statusCode, 200);
const body = jsonBody(res);
assert.strictEqual(body.status, 'open');
assert.match(body.key, /^[a-f0-9]{12}$/);
key = body.key;
})) passed++; else failed++;
if (await test('POST /api/sessions 404s for a missing artifact', async () => {
const res = await request(port, 'POST', '/api/sessions', { body: { file: path.join(tmp, 'nope.md') } });
assert.strictEqual(res.statusCode, 404);
})) passed++; else failed++;
if (await test('GET /canvas/:key serves the ECC chrome with CSP', async () => {
const res = await request(port, 'GET', `/canvas/${key}`);
assert.strictEqual(res.statusCode, 200);
assert.ok(res.headers['content-security-policy'].includes("default-src 'self'"));
assert.ok(res.body.includes('Plan Canvas'));
assert.ok(res.body.includes('pc-session'));
assert.ok(res.body.includes('Approve plan'));
assert.ok(res.body.includes('sandbox="allow-scripts allow-forms allow-popups"'));
})) passed++; else failed++;
if (await test('markdown artifacts render in the ECC plan template with the SDK', async () => {
const res = await request(port, 'GET', `/artifact/${key}/`);
assert.strictEqual(res.statusCode, 200);
assert.ok(res.body.includes('<h1 id="plan-demo">'));
assert.ok(res.body.includes('<table>'));
assert.ok(res.body.includes('<script src="/sdk.js">'));
assert.strictEqual(res.headers['content-security-policy'], undefined);
// No diagram in this plan → no Mermaid loader shipped.
assert.ok(!res.body.includes('mermaid.run'));
})) passed++; else failed++;
if (await test('a plan containing ```mermaid serves the themed Mermaid loader', async () => {
const diagram = path.join(tmp, 'flow.plan.md');
fs.writeFileSync(diagram, '# Flow\n\n```mermaid\nflowchart LR\n A --> B\n```\n');
const opened = jsonBody(await request(port, 'POST', '/api/sessions', { body: { file: diagram } }));
const res = await request(port, 'GET', `/artifact/${opened.key}/`);
assert.ok(res.body.includes('<pre class="mermaid">'), 'diagram container present');
assert.ok(res.body.includes('mermaid.run'), 'loader injected');
assert.ok(res.body.includes("securityLevel: 'strict'"), 'sanitizing config present');
await request(port, 'POST', '/api/end', { body: { file: diagram } });
})) passed++; else failed++;
if (await test('HTML artifacts pass through with the SDK injected before </body>', async () => {
const open = await request(port, 'POST', '/api/sessions', { body: { file: htmlArtifact } });
htmlKey = jsonBody(open).key;
const res = await request(port, 'GET', `/artifact/${htmlKey}/`);
assert.ok(res.body.includes('<h1>Report</h1>'));
assert.ok(res.body.includes('<script src="/sdk.js"></script>\n</body>'));
})) passed++; else failed++;
if (await test('sibling assets are served, traversal is blocked', async () => {
const ok = await request(port, 'GET', `/artifact/${key}/style.css`);
assert.strictEqual(ok.statusCode, 200);
assert.ok(ok.body.includes('color: red'));
const escape = await request(port, 'GET', `/artifact/${key}/..%2Fplan-canvas-outside.txt`);
assert.strictEqual(escape.statusCode, 403);
})) passed++; else failed++;
if (await test('static chrome assets are served', async () => {
for (const asset of ['/canvas.css', '/client.js', '/sdk.js']) {
const res = await request(port, 'GET', asset);
assert.strictEqual(res.statusCode, 200, `${asset} should be 200`);
}
})) passed++; else failed++;
if (await test('await with timeoutMs returns waiting when idle', async () => {
const res = await request(port, 'GET', `/api/await?file=${encodeURIComponent(artifact)}&timeoutMs=50`);
assert.strictEqual(jsonBody(res).status, 'waiting');
})) passed++; else failed++;
if (await test('await returns missing for files without a session', async () => {
const res = await request(port, 'GET', `/api/await?file=${encodeURIComponent(path.join(tmp, 'other.md'))}`);
assert.strictEqual(jsonBody(res).status, 'missing');
})) passed++; else failed++;
if (await test('browser feedback wakes a blocking await; presence transitions', async () => {
const sse = openSse(port, key);
await sse.ready;
const awaitPromise = request(port, 'GET', `/api/await?file=${encodeURIComponent(artifact)}`);
await waitFor(() => sse.received.some(e => e.event === 'presence' && e.data.state === 'listening'));
const post = await request(port, 'POST', `/api/session/${key}/feedback`, {
body: {
items: [
{ kind: 'annotation', text: 'tighten this', anchor: { selector: 'h2:nth-of-type(1)', tag: 'h2', snippet: 'Files to Change' } },
{ kind: 'verdict', verdict: 'request-changes' }
]
}
});
assert.strictEqual(jsonBody(post).accepted, 2);
const result = jsonBody(await awaitPromise);
assert.strictEqual(result.status, 'feedback');
assert.strictEqual(result.items.length, 2);
assert.strictEqual(result.items[0].anchor.selector, 'h2:nth-of-type(1)');
assert.strictEqual(result.items[1].verdict, 'request-changes');
await waitFor(() => sse.received.some(e => e.event === 'presence' && e.data.state === 'working'));
await waitFor(() => sse.received.some(e => e.event === 'chat-sync' && e.data.chat.length === 2));
sse.close();
})) passed++; else failed++;
if (await test('long-poll heartbeat whitespace arrives before the payload', async () => {
const chunks = [];
const done = new Promise((resolve, reject) => {
const req = http.get(
{ host: '127.0.0.1', port, path: `/api/await?file=${encodeURIComponent(artifact)}`, agent: false },
res => {
res.on('data', chunk => chunks.push(chunk.toString()));
res.on('end', resolve);
}
);
req.on('error', reject);
});
// Heartbeats tick every 25ms in this test server; wait for a few first.
await waitFor(() => chunks.join('').length >= 3);
assert.ok(/^\s+$/.test(chunks.join('')), 'expected only whitespace before payload');
await request(port, 'POST', `/api/session/${key}/feedback`, { body: { items: [{ kind: 'chat', text: 'wake up' }] } });
await done;
const full = chunks.join('');
assert.strictEqual(JSON.parse(full.trim()).status, 'feedback');
})) passed++; else failed++;
if (await test('agent reply lands in the chat via SSE chat-sync', async () => {
const sse = openSse(port, key);
await sse.ready;
const res = await request(port, 'POST', `/api/session/${key}/reply`, { body: { text: 'reworked, please re-check' } });
assert.strictEqual(jsonBody(res).status, 'sent');
await waitFor(() =>
sse.received.some(
e => e.event === 'chat-sync' && e.data.chat.some(m => m.role === 'agent' && m.text.includes('reworked'))
)
);
sse.close();
})) passed++; else failed++;
if (await test('live reload: editing the artifact emits an SSE reload event', async () => {
const sse = openSse(port, key);
await sse.ready;
fs.appendFileSync(artifact, '\n## Addendum\n');
await waitFor(() => sse.received.some(e => e.event === 'reload'), { timeoutMs: 4000 });
sse.close();
})) passed++; else failed++;
if (await test('send-and-end delivers the final batch and ends the session', async () => {
const awaitPromise = request(port, 'GET', `/api/await?file=${encodeURIComponent(artifact)}`);
await waitFor(() => canvas.presenceFor(key) === 'listening');
await request(port, 'POST', `/api/session/${key}/feedback`, {
body: { items: [{ kind: 'chat', text: 'looks good, wrapping up' }], endSession: true }
});
const result = jsonBody(await awaitPromise);
assert.strictEqual(result.status, 'feedback');
assert.strictEqual(result.sessionEnded, true);
assert.strictEqual(result.endedBy, 'user');
const after = await request(port, 'GET', `/api/await?file=${encodeURIComponent(artifact)}&timeoutMs=0`);
assert.strictEqual(jsonBody(after).status, 'ended');
})) passed++; else failed++;
if (await test('user-ended sessions return 409 on plain reopen, open with reopen:true', async () => {
const refused = await request(port, 'POST', '/api/sessions', { body: { file: artifact } });
assert.strictEqual(refused.statusCode, 409);
assert.strictEqual(jsonBody(refused).status, 'user-ended');
const forced = await request(port, 'POST', '/api/sessions', { body: { file: artifact, reopen: true } });
assert.strictEqual(forced.statusCode, 200);
})) passed++; else failed++;
if (await test('agent end via POST /api/end allows plain reopen', async () => {
const res = await request(port, 'POST', '/api/end', { body: { file: artifact } });
assert.strictEqual(jsonBody(res).endedBy, 'agent');
const reopened = await request(port, 'POST', '/api/sessions', { body: { file: artifact } });
assert.strictEqual(reopened.statusCode, 200);
})) passed++; else failed++;
if (await test('feedback on an ended session is refused with 409', async () => {
await request(port, 'POST', `/api/end`, { body: { file: htmlArtifact } });
const res = await request(port, 'POST', `/api/session/${htmlKey}/feedback`, {
body: { items: [{ kind: 'chat', text: 'too late' }] }
});
assert.strictEqual(res.statusCode, 409);
})) passed++; else failed++;
if (await test('GET / lists sessions in the ECC shell', async () => {
const res = await request(port, 'GET', '/');
assert.ok(res.body.includes('Plan Canvas sessions'));
assert.ok(res.body.includes('demo.plan.md'));
})) passed++; else failed++;
if (await test('POST /shutdown triggers the shutdown callback', async () => {
const res = await request(port, 'POST', '/shutdown');
assert.strictEqual(jsonBody(res).status, 'stopping');
await waitFor(() => idleFired);
})) passed++; else failed++;
if (await test('close() settles a held long-poll instead of hanging', async () => {
await request(port, 'POST', '/api/sessions', { body: { file: artifact, reopen: true } });
const held = request(port, 'GET', `/api/await?file=${encodeURIComponent(artifact)}`);
await waitFor(() => canvas.presenceFor(store.findByFile(artifact).key) === 'listening');
await canvas.close();
const result = jsonBody(await held);
assert.strictEqual(result.status, 'waiting');
assert.ok(result.note.includes('shutting down'));
})) passed++; else failed++;
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(path.join(os.tmpdir(), 'plan-canvas-outside.txt'), { force: true });
console.log('\n' + '='.repeat(40));
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log('='.repeat(40));
process.exit(failed > 0 ? 1 : 0);
}
main().catch(err => {
console.error(err);
console.log('Passed: 0');
console.log('Failed: 1');
process.exit(1);
});
+461
View File
@@ -0,0 +1,461 @@
/**
* Tests for scripts/platform-audit.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'platform-audit.js');
const {
DISCUSSION_ENABLED_QUERY,
DISCUSSION_QUERY
} = require(path.join(__dirname, '..', '..', 'scripts', 'lib', 'github-discussions'));
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function seedRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
name: 'everything-claude-code',
scripts: {
'platform:audit': 'node scripts/platform-audit.js',
'discussion:audit': 'node scripts/discussion-audit.js',
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'observability:ready': 'node scripts/observability-readiness.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js',
'harness:audit': 'node scripts/harness-audit.js'
}
}, null, 2),
'docs/ECC-2.0-GA-ROADMAP.md': [
'ECC Platform Roadmap',
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
'ITO-44',
'ITO-59'
].join('\n'),
'docs/architecture/progress-sync-contract.md': [
'GitHub PRs/issues/discussions',
'Linear project',
'local handoff',
'repo roadmap',
'scripts/work-items.js'
].join('\n'),
'docs/security/supply-chain-incident-response.md': [
'TanStack',
'Mini Shai-Hulud',
'node-ipc',
'scan-supply-chain-iocs.js',
'supply-chain-advisory-sources.js'
].join('\n'),
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md': [
'Release video suite',
'growth outreach',
'Operator dashboard',
'GitGuardian',
'macOS/Ubuntu/Windows test matrix',
'2568 passed'
].join('\n'),
'docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-20.md': [
'This dashboard is generated by `npm run operator:dashboard`',
'Growth Baseline',
'hypergrowth release command center',
'Prompt-To-Artifact Checklist',
'ITO-44',
'ITO-59',
'PR queue',
'Not complete',
'operator:dashboard',
'Next Work Order'
].join('\n'),
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator'
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function discussionGhKey(owner, name, first = 100) {
return `api graphql -f owner=${owner} -f name=${name} -F first=${first} -f query=${DISCUSSION_QUERY}`;
}
function discussionEnabledGhKey(owner, name) {
return `api graphql -f owner=${owner} -f name=${name} -f query=${DISCUSSION_ENABLED_QUERY}`;
}
function writeGhShim(rootDir, responses) {
const shimPath = path.join(rootDir, 'gh-shim.js');
fs.writeFileSync(shimPath, `
const responses = ${JSON.stringify(responses)};
const args = process.argv.slice(2);
const key = args.join(' ');
if (process.env.GITHUB_TOKEN) {
console.error('GITHUB_TOKEN should be unset by default');
process.exit(42);
}
if (!Object.prototype.hasOwnProperty.call(responses, key)) {
console.error('Unexpected gh args: ' + key);
process.exit(3);
}
process.stdout.write(JSON.stringify(responses[key]));
`);
return shimPath;
}
function run(args = [], options = {}) {
const env = {
...process.env,
...(options.env || {})
};
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function runProcess(args = [], options = {}) {
const env = {
...process.env,
...(options.env || {})
};
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing platform-audit.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts supported flags and rejects invalid values', () => {
const { parseArgs } = require(SCRIPT);
const rootDir = createTempDir('platform-audit-args-');
try {
const parsed = parseArgs([
'node',
'script',
'--format=json',
`--root=${rootDir}`,
'--json',
'--repo',
'affaan-m/ECC',
'--max-open-prs',
'5',
'--max-open-issues',
'6',
'--allow-untracked',
'docs/drafts/'
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.deepStrictEqual(parsed.repos, ['affaan-m/ECC']);
assert.strictEqual(parsed.thresholds.maxOpenPrs, 5);
assert.strictEqual(parsed.thresholds.maxOpenIssues, 6);
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--write', 'audit.md']), /--write requires/);
assert.throws(() => parseArgs(['node', 'script', '--repo']), /--repo requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('skip-github report checks local release and security evidence', () => {
const projectRoot = createTempDir('platform-audit-local-');
try {
seedRepo(projectRoot);
const parsed = JSON.parse(run(['--format=json', `--root=${projectRoot}`, '--skip-github'], { cwd: projectRoot }));
assert.strictEqual(parsed.schema_version, 'ecc.platform-audit.v1');
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.github.skipped, true);
assert.ok(parsed.checks.some(check => check.id === 'roadmap-linear-mirror' && check.status === 'pass'));
assert.ok(parsed.checks.some(check => check.id === 'supply-chain-runbook' && check.status === 'pass'));
assert.ok(parsed.checks.some(check => check.id === 'operator-dashboard-command' && check.status === 'pass'));
assert.ok(parsed.checks.some(check => check.id === 'operator-readiness-dashboard' && check.status === 'pass'));
assert.ok(parsed.checks.some(check => check.id === 'release-evidence-current' && check.status === 'pass'));
assert.deepStrictEqual(parsed.top_actions, []);
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('release evidence gate rejects stale root suite counts', () => {
const projectRoot = createTempDir('platform-audit-stale-release-evidence-');
try {
seedRepo(projectRoot, {
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md': [
'Release video suite',
'growth outreach',
'Operator dashboard',
'GitGuardian',
'macOS/Ubuntu/Windows test matrix',
'2560 passed'
].join('\n')
});
const parsed = JSON.parse(run(['--format=json', `--root=${projectRoot}`, '--skip-github'], { cwd: projectRoot }));
const releaseEvidence = parsed.checks.find(check => check.id === 'release-evidence-current');
assert.strictEqual(releaseEvidence.status, 'fail');
assert.ok(parsed.top_actions.some(action => action.id === 'release-evidence-current'));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('markdown output can be written as an operator artifact', () => {
const projectRoot = createTempDir('platform-audit-markdown-');
const outputPath = path.join(projectRoot, 'artifacts', 'platform-audit.md');
try {
seedRepo(projectRoot);
const stdout = run([
'--markdown',
'--write',
outputPath,
`--root=${projectRoot}`,
'--skip-github'
], { cwd: projectRoot });
const written = fs.readFileSync(outputPath, 'utf8');
assert.strictEqual(stdout, written);
assert.ok(written.includes('# ECC Platform Audit'));
assert.ok(written.includes('## Queue Summary'));
assert.ok(written.includes('| Open PRs | 0 | 20 | PASS |'));
assert.ok(written.includes('`roadmap-linear-mirror`'));
assert.ok(written.includes('## Top Actions'));
assert.ok(written.includes('- none'));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('github queue and discussion budgets pass with maintainer touch', () => {
const projectRoot = createTempDir('platform-audit-github-pass-');
try {
seedRepo(projectRoot);
const shimPath = writeGhShim(projectRoot, {
'pr list --repo affaan-m/ECC --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': [],
'issue list --repo affaan-m/ECC --state open --json number,title,updatedAt,url,author,labels': [],
[discussionEnabledGhKey('affaan-m', 'ECC')]: {
data: { repository: { hasDiscussionsEnabled: true } }
},
[discussionGhKey('affaan-m', 'ECC')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: {
totalCount: 1,
nodes: [
{
number: 73,
title: 'Compacting during workflow',
url: 'https://github.com/example/discussions/73',
updatedAt: '2026-05-15T00:00:00Z',
authorAssociation: 'NONE',
category: { name: 'General', isAnswerable: false },
answer: null,
comments: { nodes: [{ authorAssociation: 'OWNER' }] }
}
]
}
}
}
}
});
const parsed = JSON.parse(run([
'--format=json',
`--root=${projectRoot}`,
'--repo',
'affaan-m/ECC'
], {
cwd: projectRoot,
env: {
ECC_GH_SHIM: shimPath,
GITHUB_TOKEN: 'must-be-removed'
}
}));
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.github.totals.openPrs, 0);
assert.strictEqual(parsed.github.totals.openIssues, 0);
assert.strictEqual(parsed.github.totals.discussionsNeedingMaintainerTouch, 0);
assert.strictEqual(parsed.github.totals.discussionsMissingAcceptedAnswer, 0);
assert.ok(parsed.checks.some(check => check.id === 'github-discussion-touch' && check.status === 'pass'));
assert.ok(parsed.checks.some(check => check.id === 'github-discussion-answers' && check.status === 'pass'));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('threshold failures and untouched discussions become top actions', () => {
const projectRoot = createTempDir('platform-audit-github-fail-');
try {
seedRepo(projectRoot);
const prs = Array.from({ length: 3 }, (_, index) => ({
number: index + 1,
title: `PR ${index + 1}`,
isDraft: false,
mergeStateStatus: 'CLEAN',
updatedAt: '2026-05-15T00:00:00Z',
url: `https://github.com/example/pull/${index + 1}`,
author: { login: 'contributor' }
}));
const shimPath = writeGhShim(projectRoot, {
'pr list --repo affaan-m/ECC --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': prs,
'issue list --repo affaan-m/ECC --state open --json number,title,updatedAt,url,author,labels': [],
[discussionEnabledGhKey('affaan-m', 'ECC')]: {
data: { repository: { hasDiscussionsEnabled: true } }
},
[discussionGhKey('affaan-m', 'ECC')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: {
totalCount: 1,
nodes: [
{
number: 1239,
title: 'Losing context',
url: 'https://github.com/example/discussions/1239',
updatedAt: '2026-05-15T00:00:00Z',
authorAssociation: 'NONE',
category: { name: 'Q&A', isAnswerable: true },
answer: null,
comments: { nodes: [] }
}
]
}
}
}
}
});
const parsed = JSON.parse(run([
'--format=json',
`--root=${projectRoot}`,
'--repo',
'affaan-m/ECC',
'--max-open-prs',
'2'
], {
cwd: projectRoot,
env: { ECC_GH_SHIM: shimPath }
}));
assert.strictEqual(parsed.ready, false);
assert.ok(parsed.top_actions.some(action => action.id === 'github-open-pr-budget'));
assert.ok(parsed.top_actions.some(action => action.id === 'github-discussion-touch'));
assert.ok(parsed.top_actions.some(action => action.id === 'github-discussion-answers'));
assert.strictEqual(parsed.github.totals.discussionsNeedingMaintainerTouch, 1);
assert.strictEqual(parsed.github.totals.discussionsMissingAcceptedAnswer, 1);
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('discussion-disabled repos skip the heavy discussion query', () => {
const projectRoot = createTempDir('platform-audit-discussions-disabled-');
try {
seedRepo(projectRoot);
const shimPath = writeGhShim(projectRoot, {
'pr list --repo ECC-Tools/ECC-website --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': [],
'issue list --repo ECC-Tools/ECC-website --state open --json number,title,updatedAt,url,author,labels': [],
[discussionEnabledGhKey('ECC-Tools', 'ECC-website')]: {
data: { repository: { hasDiscussionsEnabled: false } }
}
});
const parsed = JSON.parse(run([
'--format=json',
`--root=${projectRoot}`,
'--repo',
'ECC-Tools/ECC-website'
], {
cwd: projectRoot,
env: { ECC_GH_SHIM: shimPath }
}));
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.github.repos[0].discussions.enabled, false);
assert.strictEqual(parsed.github.repos[0].discussions.totalCount, 0);
assert.strictEqual(parsed.github.totals.errors, 0);
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('cli help and invalid args exit cleanly', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.ok(help.stdout.includes('Usage: node scripts/platform-audit.js'));
const invalid = runProcess(['--format', 'xml']);
assert.strictEqual(invalid.status, 1);
assert.ok(invalid.stderr.includes('Invalid format'));
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}
+105
View File
@@ -0,0 +1,105 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-command-log.js');
const { sanitizeCommand } = require(scriptPath);
function test(name, fn) {
try {
fn();
console.log(`PASS: ${name}`);
return true;
} catch (error) {
console.log(`FAIL: ${name}`);
console.log(` ${error.message}`);
return false;
}
}
function runHook(mode, payload, homeDir) {
return spawnSync('node', [scriptPath, mode], {
input: JSON.stringify(payload),
encoding: 'utf8',
env: {
...process.env,
HOME: homeDir,
USERPROFILE: homeDir,
},
});
}
let passed = 0;
let failed = 0;
if (
test('sanitizeCommand redacts common secret formats', () => {
const input = 'gh pr create --token abc123 Authorization: Bearer hello password=swordfish ghp_abc github_pat_xyz';
const sanitized = sanitizeCommand(input);
assert.ok(!sanitized.includes('abc123'));
assert.ok(!sanitized.includes('swordfish'));
assert.ok(!sanitized.includes('ghp_abc'));
assert.ok(!sanitized.includes('github_pat_xyz'));
assert.ok(sanitized.includes('--token=<REDACTED>'));
assert.ok(sanitized.includes('Authorization:<REDACTED>'));
assert.ok(sanitized.includes('password=<REDACTED>'));
})
)
passed++;
else failed++;
if (
test('audit mode logs sanitized bash commands and preserves stdout', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-bash-log-'));
const payload = {
tool_input: {
command: 'git push --token abc123',
},
};
try {
const result = runHook('audit', payload, homeDir);
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
assert.strictEqual(result.stdout, JSON.stringify(payload));
const logFile = path.join(homeDir, '.claude', 'bash-commands.log');
const logContent = fs.readFileSync(logFile, 'utf8');
assert.ok(logContent.includes('--token=<REDACTED>'));
assert.ok(!logContent.includes('abc123'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if (
test('cost mode writes command metrics log', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-cost-log-'));
const payload = {
tool_input: {
command: 'npm publish',
},
};
try {
const result = runHook('cost', payload, homeDir);
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
const logFile = path.join(homeDir, '.claude', 'cost-tracker.log');
const logContent = fs.readFileSync(logFile, 'utf8');
assert.match(logContent, /tool=Bash command=npm publish/);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
+260
View File
@@ -0,0 +1,260 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'preview-pack-smoke.js');
const {
REQUIRED_ARTIFACTS,
REQUIRED_PUBLICATION_BLOCKERS,
REQUIRED_VERIFICATION_COMMANDS,
buildReport,
parseArgs,
renderText,
} = require(SCRIPT);
const RELEASE_DIR = 'docs/releases/2.0.0-rc.1';
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function manifestContent() {
return [
'# ECC v2.0.0-rc.1 Preview Pack Manifest',
'',
'## Pack Contents',
'',
'| Artifact | Role | Gate |',
'| --- | --- | --- |',
...REQUIRED_ARTIFACTS.map(artifact => `| \`${artifact}\` | release artifact | checked |`),
'',
'## Hermes Skill Boundary',
'',
'- no raw workspace exports;',
'',
'## Final Verification Commands',
'',
'```bash',
...REQUIRED_VERIFICATION_COMMANDS,
'```',
'',
'## Publication Blockers',
'',
...REQUIRED_PUBLICATION_BLOCKERS.map(blocker => `- ${blocker}`),
'',
'The preview pack is not public without approval-gated release, package, plugin, and announcement steps.',
].join('\n');
}
function seedRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
files: ['scripts/preview-pack-smoke.js'],
scripts: {
'preview-pack:smoke': 'node scripts/preview-pack-smoke.js',
},
}, null, 2),
'scripts/preview-pack-smoke.js': 'preview pack smoke script',
[`${RELEASE_DIR}/preview-pack-manifest.md`]: manifestContent(),
'docs/HERMES-SETUP.md': [
'# Hermes Setup',
'Public Release Candidate Scope',
'ECC v2.0.0-rc.1 documents the Hermes surface',
'No raw workspace export is included.',
].join('\n'),
'skills/hermes-imports/SKILL.md': [
'---',
'name: hermes-imports',
'---',
'Sanitization Checklist',
'Do not ship raw workspace exports',
'Output Contract',
].join('\n'),
};
for (const artifact of REQUIRED_ARTIFACTS) {
if (!Object.prototype.hasOwnProperty.call(files, artifact)) {
files[artifact] = `${artifact} public preview-pack content`;
}
}
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing preview-pack-smoke.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts smoke flags and rejects invalid values', () => {
const rootDir = createTempDir('preview-pack-smoke-args-');
try {
const parsed = parseArgs([
'node',
'script',
'--format=json',
`--root=${rootDir}`,
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('seeded release pack passes every smoke check', () => {
const rootDir = createTempDir('preview-pack-smoke-pass-');
try {
seedRepo(rootDir);
const report = buildReport({ root: rootDir });
assert.strictEqual(report.schema_version, 'ecc.preview-pack-smoke.v1');
assert.strictEqual(report.ready, true);
assert.strictEqual(report.summary.failed, 0);
assert.ok(report.checks.every(check => check.status === 'pass'));
const text = renderText(report);
assert.ok(text.includes('Ready: yes'));
assert.ok(text.includes('Passed: 5'));
assert.ok(text.includes('Failed: 0'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('script registration fails closed without package wiring', () => {
const rootDir = createTempDir('preview-pack-smoke-package-');
try {
seedRepo(rootDir, {
'package.json': JSON.stringify({ files: [], scripts: {} }, null, 2),
});
const report = buildReport({ root: rootDir });
const registration = report.checks.find(check => check.id === 'preview-pack-script-registered');
assert.strictEqual(report.ready, false);
assert.strictEqual(registration.status, 'fail');
assert.ok(registration.fix.includes('preview-pack:smoke'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('Hermes boundary fails closed on private local paths', () => {
const rootDir = createTempDir('preview-pack-smoke-private-path-');
try {
seedRepo(rootDir, {
[`${RELEASE_DIR}/quickstart.md`]: 'Do not ship /Users/affoon/private-state in public docs.',
});
const report = buildReport({ root: rootDir });
const boundary = report.checks.find(check => check.id === 'hermes-boundary-sanitized');
assert.strictEqual(report.ready, false);
assert.strictEqual(boundary.status, 'fail');
assert.ok(boundary.evidence.includes(`${RELEASE_DIR}/quickstart.md:1`));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI emits json and uses status 2 for failed smoke reports', () => {
const rootDir = createTempDir('preview-pack-smoke-cli-');
try {
seedRepo(rootDir);
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
const parsed = JSON.parse(stdout);
assert.strictEqual(parsed.ready, true);
writeFile(rootDir, 'package.json', JSON.stringify({ files: [], scripts: {} }, null, 2));
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
assert.strictEqual(failedRun.status, 2);
assert.strictEqual(failedRun.stderr, '');
assert.ok(failedRun.stdout.includes('"ready": false'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI help exits successfully and invalid flags fail before reporting', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/preview-pack-smoke.js'));
const invalid = runProcess(['--format=xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.match(invalid.stderr, /Error: Invalid format/);
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}
+348
View File
@@ -0,0 +1,348 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'release-approval-gate.js');
const {
REQUIRED_DECISIONS,
REQUIRED_URL_SURFACES,
buildReport,
parseArgs,
renderText,
} = require(SCRIPT);
const CURRENT_RELEASE = require(path.join(__dirname, '..', '..', 'package.json')).version;
const RC_RELEASE = '2.0.0-rc.1';
function releaseDirFor(release) {
return `docs/releases/${release}`;
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function approvedPacketContent(overrides = {}, release = CURRENT_RELEASE) {
const decisions = new Map(REQUIRED_DECISIONS.map(decision => [decision.label, 'approve']));
for (const [label, value] of Object.entries(overrides)) {
decisions.set(label, value);
}
return [
`# ECC v${release} Owner Approval Packet`,
'',
'## Decision Register',
'',
'| Decision | Approve / defer / block | Evidence required first | Notes |',
'| --- | --- | --- | --- |',
...REQUIRED_DECISIONS.map(decision => (
`| ${decision.label} | ${decisions.get(decision.label)} | final evidence | approved fixture |`
)),
'',
'## Final Evidence Commands',
'',
'```bash',
'npm run release:approval-gate -- --format json',
'```',
'',
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.',
].join('\n');
}
function finalLedgerContent(extra = '', release = CURRENT_RELEASE) {
return [
`# ECC v${release} Release URL Ledger`,
'',
'## Final Published URLs',
'',
'| Surface | URL | Verification |',
'| --- | --- | --- |',
...REQUIRED_URL_SURFACES.map(surface => (
`| ${surface.label} | ${surface.exampleUrl.split(RC_RELEASE).join(release)} | readback from final release commit |`
)),
'',
'## Final Verification Commands',
'',
'```bash',
'npm run release:approval-gate -- --format json',
'```',
'',
extra,
].join('\n');
}
function manifestContent(release = CURRENT_RELEASE) {
return [
`# ECC v${release} Preview Pack Manifest`,
'',
'| Artifact | Role | Gate |',
'| --- | --- | --- |',
'| `scripts/release-approval-gate.js` | Final owner approval and live URL gate | Verified by `npm run release:approval-gate -- --format json` |',
'',
'## Final Verification Commands',
'',
'```bash',
'npm run release:approval-gate -- --format json',
'```',
].join('\n');
}
function seedRepo(rootDir, overrides = {}, options = {}) {
const release = options.release || CURRENT_RELEASE;
const releaseDir = releaseDirFor(release);
const files = {
'package.json': JSON.stringify({
version: release,
files: ['scripts/release-approval-gate.js'],
scripts: {
'release:approval-gate': 'node scripts/release-approval-gate.js',
},
}, null, 2),
'scripts/release-approval-gate.js': 'release approval gate script',
[`${releaseDir}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({}, release),
[`${releaseDir}/release-url-ledger-2026-05-19.md`]: finalLedgerContent('', release),
[`${releaseDir}/preview-pack-manifest.md`]: manifestContent(release),
[`${releaseDir}/release-notes.md`]: 'Release notes with final URLs.',
[`${releaseDir}/x-thread.md`]: 'X post with final URLs.',
[`${releaseDir}/linkedin-post.md`]: 'LinkedIn post with final URLs.',
[`${releaseDir}/article-outline.md`]: 'Article outline with final URLs.',
[`${releaseDir}/partner-sponsor-talks-pack.md`]: 'Outbound copy with final URLs.',
'docs/business/social-launch-copy.md': 'Business launch copy with final URLs.',
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing release-approval-gate.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts approval gate flags and rejects invalid values', () => {
const rootDir = createTempDir('release-approval-args-');
try {
const parsed = parseArgs([
'node',
'script',
'--format=json',
`--root=${rootDir}`,
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('seeded approved release passes every publication approval check', () => {
const rootDir = createTempDir('release-approval-pass-');
try {
seedRepo(rootDir);
const report = buildReport({ root: rootDir });
assert.strictEqual(report.schema_version, 'ecc.release-approval-gate.v1');
assert.strictEqual(report.release, CURRENT_RELEASE);
assert.strictEqual(report.ready, true);
assert.strictEqual(report.summary.failed, 0);
assert.deepStrictEqual(report.top_actions, []);
assert.ok(report.checks.every(check => check.status === 'pass'));
const text = renderText(report);
assert.ok(text.includes('Ready: yes'));
assert.ok(text.includes('Failed: 0'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('release override keeps rc.1 approval fixtures testable', () => {
const rootDir = createTempDir('release-approval-rc-');
try {
seedRepo(rootDir, {}, { release: RC_RELEASE });
const report = buildReport({ root: rootDir, release: RC_RELEASE });
assert.strictEqual(report.release, RC_RELEASE);
assert.strictEqual(report.ready, true);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('deferred owner decisions keep the publication gate blocked', () => {
const rootDir = createTempDir('release-approval-deferred-');
try {
const releaseDir = releaseDirFor(CURRENT_RELEASE);
seedRepo(rootDir, {
[`${releaseDir}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({
'GitHub prerelease': 'defer',
'Sponsor, partner, consulting, conference, podcast outreach': 'block',
}),
});
const report = buildReport({ root: rootDir });
const decisions = report.checks.find(check => check.id === 'owner-decisions-approved');
assert.strictEqual(report.ready, false);
assert.strictEqual(decisions.status, 'fail');
assert.ok(decisions.evidence.includes('GitHub prerelease=defer'));
assert.ok(decisions.evidence.includes('Sponsor, partner, consulting, conference, podcast outreach=block'));
assert.ok(report.top_actions.some(action => action.includes('Approve, defer, or block')));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('approval-gated URL ledger rows keep the publication gate blocked', () => {
const rootDir = createTempDir('release-approval-ledger-');
try {
const releaseDir = releaseDirFor(CURRENT_RELEASE);
seedRepo(rootDir, {
[`${releaseDir}/release-url-ledger-2026-05-19.md`]: [
`# ECC v${CURRENT_RELEASE} Release URL Ledger`,
'',
'## Approval-Gated URLs',
'',
'| Surface | Intended URL or command | Gate before use |',
'| --- | --- | --- |',
`| GitHub prerelease | https://github.com/affaan-m/ECC/releases/tag/v${CURRENT_RELEASE} | must return the prerelease |`,
].join('\n'),
});
const report = buildReport({ root: rootDir });
const ledger = report.checks.find(check => check.id === 'release-url-ledger-finalized');
assert.strictEqual(report.ready, false);
assert.strictEqual(ledger.status, 'fail');
assert.ok(ledger.evidence.includes('approval-gated URL section still present'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('announcement drafts fail on unresolved placeholders and private paths', () => {
const rootDir = createTempDir('release-approval-copy-');
try {
const releaseDir = releaseDirFor(CURRENT_RELEASE);
seedRepo(rootDir, {
[`${releaseDir}/x-thread.md`]: 'Ship copy with <video-url> and /Users/affaan/raw-footage.',
});
const report = buildReport({ root: rootDir });
const copy = report.checks.find(check => check.id === 'announcement-copy-finalized');
assert.strictEqual(report.ready, false);
assert.strictEqual(copy.status, 'fail');
assert.ok(copy.evidence.includes(`${releaseDir}/x-thread.md:1`));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI emits json and uses status 2 for blocked approval reports', () => {
const rootDir = createTempDir('release-approval-cli-');
try {
seedRepo(rootDir);
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
const parsed = JSON.parse(stdout);
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.release, CURRENT_RELEASE);
const releaseDir = releaseDirFor(CURRENT_RELEASE);
writeFile(
rootDir,
`${releaseDir}/owner-approval-packet-2026-05-19.md`,
approvedPacketContent({ 'Video upload': 'defer' })
);
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
assert.strictEqual(failedRun.status, 2);
assert.strictEqual(failedRun.stderr, '');
assert.ok(failedRun.stdout.includes('"ready": false'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI help exits successfully and invalid flags fail before reporting', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/release-approval-gate.js'));
const invalid = runProcess(['--format=xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.match(invalid.stderr, /Error: Invalid format/);
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}
+79
View File
@@ -0,0 +1,79 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const repoRoot = path.resolve(__dirname, '..', '..');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed++;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
failed++;
}
}
function load(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8').replace(/\r\n/g, '\n');
}
console.log('\n=== Testing release publish workflow ===\n');
for (const workflow of [
'.github/workflows/release.yml',
'.github/workflows/reusable-release.yml',
]) {
const content = load(workflow);
const jobsIndex = content.search(/^jobs:\s*$/m);
const workflowHeader = jobsIndex >= 0 ? content.slice(0, jobsIndex) : content;
test(`${workflow} scopes id-token to the publish job for npm provenance`, () => {
assert.doesNotMatch(workflowHeader, /id-token:\s*write/);
assert.match(content, /\n\s+permissions:\n\s+contents:\s*write\n\s+id-token:\s*write/m);
});
test(`${workflow} configures the npm registry`, () => {
assert.match(content, /registry-url:\s*['"]https:\/\/registry\.npmjs\.org['"]/);
});
test(`${workflow} ignores dependency lifecycle scripts before privileged publish`, () => {
assert.match(content, /npm ci --ignore-scripts/);
});
test(`${workflow} checks whether the tagged npm version already exists`, () => {
assert.match(content, /Check npm publish state/);
assert.match(content, /npm view "\$\{PACKAGE_NAME\}@\$\{PACKAGE_VERSION\}" version/);
});
test(`${workflow} publishes new tag versions to npm`, () => {
assert.match(content, /npm publish "\$\{\{ needs\.verify\.outputs\.package_file \}\}" --access public --provenance/);
assert.match(content, /NODE_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NPM_TOKEN\s*\}\}/);
});
test(`${workflow} creates the GitHub Release before publishing to npm`, () => {
const releaseIndex = content.indexOf('name: Create GitHub Release');
const publishIndex = content.indexOf('name: Publish npm package');
assert.ok(releaseIndex >= 0, `${workflow} should create a GitHub Release`);
assert.ok(publishIndex >= 0, `${workflow} should publish the npm package`);
assert.ok(
releaseIndex < publishIndex,
`${workflow} should not publish to npm until GitHub Release creation has succeeded`
);
});
}
if (failed > 0) {
console.log(`\nFailed: ${failed}`);
process.exit(1);
}
console.log(`\nPassed: ${passed}`);
+362
View File
@@ -0,0 +1,362 @@
/**
* Tests for scripts/release-video-suite.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'release-video-suite.js');
const {
REQUIRED_PUBLISH_CANDIDATES,
REQUIRED_SOURCE_ASSETS,
REQUIRED_SUITE_ARTIFACTS,
buildReport,
parseArgs,
renderText,
summarizeReport,
} = require(SCRIPT);
const CURRENT_RELEASE = require(path.join(__dirname, '..', '..', 'package.json')).version;
const RC_RELEASE = '2.0.0-rc.1';
function releaseDirFor(release) {
return `docs/releases/${release}`;
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content = 'fixture') {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function videoManifestContent(extra = '') {
return [
'# ECC 2.0 Video Suite Production Manifest',
'ECC_VIDEO_SOURCE_ROOT',
'ECC_VIDEO_RELEASE_SUITE_ROOT',
'Primary launch video',
'video-use compatible workflow',
'Self-Eval Gate',
'Do Not Publish If',
'Do not commit raw footage, transcript JSON, or timeline exports',
extra,
].join('\n');
}
function seedRepo(rootDir, overrides = {}, options = {}) {
const release = options.release || CURRENT_RELEASE;
const releaseDir = releaseDirFor(release);
const files = {
'package.json': JSON.stringify({
name: 'ecc-universal',
version: release,
files: ['scripts/release-video-suite.js'],
scripts: {
'release:video-suite': 'node scripts/release-video-suite.js',
},
}, null, 2),
[`${releaseDir}/video-suite-production.md`]: videoManifestContent(),
'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md': [
'Keep raw absolute paths out of public docs',
'Pick final video cuts, upload after approval, and attach public URLs',
].join('\n'),
[`${releaseDir}/preview-pack-manifest.md`]: 'video-suite-production.md',
[`${releaseDir}/launch-checklist.md`]: 'release video suite',
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function seedMedia(sourceRoot, suiteRoot) {
for (const asset of REQUIRED_SOURCE_ASSETS) {
writeFile(sourceRoot, asset.file, `source ${asset.id}`);
}
for (const artifact of REQUIRED_SUITE_ARTIFACTS) {
writeFile(suiteRoot, artifact.relativePath, `artifact ${artifact.id}`);
}
for (const candidate of REQUIRED_PUBLISH_CANDIDATES) {
writeFile(suiteRoot, candidate.relativePath, `candidate ${candidate.id}`);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing release-video-suite.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts release video flags and rejects invalid values', () => {
const rootDir = createTempDir('release-video-args-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
const parsed = parseArgs([
'node',
'script',
'--json',
`--root=${rootDir}`,
'--source-root',
sourceRoot,
`--suite-root=${suiteRoot}`,
'--skip-probe',
'--summary',
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.strictEqual(parsed.sourceRoot, path.resolve(sourceRoot));
assert.strictEqual(parsed.suiteRoot, path.resolve(suiteRoot));
assert.strictEqual(parsed.skipProbe, true);
assert.strictEqual(parsed.summary, true);
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--source-root']), /--source-root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('buildReport passes with a sanitized manifest and complete local media fixture', () => {
const rootDir = createTempDir('release-video-report-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
seedRepo(rootDir);
seedMedia(sourceRoot, suiteRoot);
const report = buildReport({
root: rootDir,
sourceRoot,
suiteRoot,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.schema_version, 'ecc.release-video-suite.v1');
assert.strictEqual(report.release, CURRENT_RELEASE);
assert.strictEqual(report.ready, true);
assert.strictEqual(report.mediaPathsRedacted, true);
assert.ok(report.checks.every(check => check.status === 'pass'));
assert.ok(report.checks.some(check => (
check.id === 'video-primary-render-self-eval'
&& check.summary.includes('skipped by --skip-probe')
)));
assert.strictEqual(report.sourceAssets.length, REQUIRED_SOURCE_ASSETS.length);
assert.strictEqual(report.suiteArtifacts.length, REQUIRED_SUITE_ARTIFACTS.length);
assert.strictEqual(report.publishCandidates.length, REQUIRED_PUBLISH_CANDIDATES.length);
assert.ok(renderText(report).includes('Ready: yes'));
assert.strictEqual(summarizeReport(report).sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length);
assert.strictEqual(
summarizeReport(report).publishCandidateSummary.present,
REQUIRED_PUBLISH_CANDIDATES.length
);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('release override keeps rc.1 video fixtures testable', () => {
const rootDir = createTempDir('release-video-rc-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
seedRepo(rootDir, {}, { release: RC_RELEASE });
seedMedia(sourceRoot, suiteRoot);
const report = buildReport({
root: rootDir,
release: RC_RELEASE,
sourceRoot,
suiteRoot,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.release, RC_RELEASE);
assert.strictEqual(report.ready, true);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('publish candidate videos require visual blank-frame QA', () => {
const publishVideos = REQUIRED_PUBLISH_CANDIDATES.filter(candidate => candidate.kind === 'video');
assert.ok(publishVideos.length > 0);
assert.ok(publishVideos.every(candidate => candidate.noBlackFrames === true));
})) passed++; else failed++;
if (test('missing local roots keep the release video gate blocked', () => {
const rootDir = createTempDir('release-video-missing-roots-');
try {
seedRepo(rootDir);
const report = buildReport({
root: rootDir,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.ready, false);
assert.ok(report.top_actions.some(action => action.includes('ECC_VIDEO_SOURCE_ROOT')));
assert.ok(report.top_actions.some(action => action.includes('ECC_VIDEO_RELEASE_SUITE_ROOT')));
assert.ok(report.checks.some(check => check.id === 'video-source-assets-present' && check.status === 'fail'));
assert.ok(report.checks.some(check => check.id === 'video-release-artifacts-present' && check.status === 'fail'));
assert.ok(report.checks.some(check => check.id === 'video-primary-render-self-eval' && check.status === 'fail'));
assert.ok(report.checks.some(check => check.id === 'video-publish-candidates-present' && check.status === 'fail'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('private media paths in public docs fail sanitization', () => {
const rootDir = createTempDir('release-video-private-path-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
const releaseDir = releaseDirFor(CURRENT_RELEASE);
seedRepo(rootDir, {
[`${releaseDir}/video-suite-production.md`]: videoManifestContent('/Users/affoon/private-media'),
});
seedMedia(sourceRoot, suiteRoot);
const report = buildReport({
root: rootDir,
sourceRoot,
suiteRoot,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'video-suite-public-sanitization' && check.status === 'fail'));
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('CLI emits JSON and exits successfully for complete fixture', () => {
const rootDir = createTempDir('release-video-cli-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
seedRepo(rootDir);
seedMedia(sourceRoot, suiteRoot);
const output = run([
'--format=json',
`--root=${rootDir}`,
`--source-root=${sourceRoot}`,
`--suite-root=${suiteRoot}`,
'--skip-probe',
'--summary',
], { cwd: rootDir });
const parsed = JSON.parse(output);
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.release, CURRENT_RELEASE);
assert.strictEqual(parsed.sourceRootConfigured, true);
assert.strictEqual(parsed.suiteRootConfigured, true);
assert.strictEqual(parsed.sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length);
assert.strictEqual(parsed.suiteArtifactSummary.present, REQUIRED_SUITE_ARTIFACTS.length);
assert.strictEqual(parsed.publishCandidateSummary.present, REQUIRED_PUBLISH_CANDIDATES.length);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('CLI exits nonzero when media roots are missing', () => {
const rootDir = createTempDir('release-video-cli-blocked-');
try {
seedRepo(rootDir);
const result = runProcess([
'--format=json',
`--root=${rootDir}`,
'--skip-probe',
'--summary',
], { cwd: rootDir });
assert.strictEqual(result.status, 1);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.ready, false);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
if (require.main === module) {
runTests();
}
+150
View File
@@ -0,0 +1,150 @@
/**
* Source-level tests for scripts/release.sh
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'release.sh');
const source = fs.readFileSync(scriptPath, 'utf8');
const releaseWorkflowPath = path.join(__dirname, '..', '..', '.github', 'workflows', 'release.yml');
const reusableReleaseWorkflowPath = path.join(
__dirname,
'..',
'..',
'.github',
'workflows',
'reusable-release.yml'
);
const ciWorkflowPath = path.join(__dirname, '..', '..', '.github', 'workflows', 'ci.yml');
const releaseWorkflowSource = fs.readFileSync(releaseWorkflowPath, 'utf8');
const reusableReleaseWorkflowSource = fs.readFileSync(reusableReleaseWorkflowPath, 'utf8');
const ciWorkflowSource = fs.readFileSync(ciWorkflowPath, 'utf8');
const normalizedCiWorkflowSource = ciWorkflowSource.replace(/\r\n/g, '\n');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing release.sh ===\n');
let passed = 0;
let failed = 0;
if (test('release script rejects untracked files when checking cleanliness', () => {
assert.ok(
source.includes('git status --porcelain --untracked-files=all'),
'release.sh should use git status --porcelain --untracked-files=all for cleanliness checks'
);
})) passed++; else failed++;
if (test('release script reruns release metadata sync validation before commit/tag', () => {
const syncCheckIndex = source.lastIndexOf('node tests/plugin-manifest.test.js');
const commitIndex = source.indexOf('git commit -m "chore: bump plugin version to $VERSION"');
assert.ok(syncCheckIndex >= 0, 'release.sh should run plugin-manifest.test.js');
assert.ok(commitIndex >= 0, 'release.sh should create the release commit');
assert.ok(
syncCheckIndex < commitIndex,
'plugin-manifest.test.js should run before the release commit is created'
);
})) passed++; else failed++;
if (test('release script verifies npm pack payload after version updates and before commit/tag', () => {
const updateIndex = source.indexOf('update_version "$ROOT_PACKAGE_JSON"');
const packCheckIndex = source.indexOf('node tests/scripts/build-opencode.test.js');
const commitIndex = source.indexOf('git commit -m "chore: bump plugin version to $VERSION"');
assert.ok(updateIndex >= 0, 'release.sh should update package version fields');
assert.ok(packCheckIndex >= 0, 'release.sh should run build-opencode.test.js');
assert.ok(commitIndex >= 0, 'release.sh should create the release commit');
assert.ok(
updateIndex < packCheckIndex,
'build-opencode.test.js should run after versioned files are updated'
);
assert.ok(
packCheckIndex < commitIndex,
'build-opencode.test.js should run before the release commit is created'
);
})) passed++; else failed++;
if (test('release script supports prerelease semver and release heading sync', () => {
assert.ok(
source.includes('2.0.0-rc.1'),
'release.sh should document an accepted prerelease semver example'
);
assert.ok(
source.includes('(-[0-9A-Za-z.-]+)?'),
'release.sh should allow prerelease semver suffixes'
);
assert.ok(
source.includes('update_latest_release_heading "$ROOT_ZH_CN_README_FILE"'),
'release.sh should update localized latest-release headings that plugin-manifest.test.js verifies'
);
})) passed++; else failed++;
if (test('release workflows mark prerelease tags as GitHub prereleases', () => {
assert.ok(
releaseWorkflowSource.includes('prerelease: ${{ contains(github.ref_name, \'-\') }}'),
'release.yml should mark hyphenated tag pushes as GitHub prereleases'
);
assert.ok(
releaseWorkflowSource.includes('make_latest: ${{ contains(github.ref_name, \'-\') && \'false\' || \'true\' }}'),
'release.yml should avoid making hyphenated prereleases the latest GitHub release'
);
assert.ok(
reusableReleaseWorkflowSource.includes('prerelease: ${{ contains(inputs.tag, \'-\') }}'),
'reusable-release.yml should mark hyphenated manual tags as GitHub prereleases'
);
assert.ok(
reusableReleaseWorkflowSource.includes('make_latest: ${{ contains(inputs.tag, \'-\') && \'false\' || \'true\' }}'),
'reusable-release.yml should avoid making hyphenated prereleases the latest GitHub release'
);
})) passed++; else failed++;
if (test('reusable release checks out the requested tag before validating and publishing', () => {
const checkoutIndex = reusableReleaseWorkflowSource.indexOf('uses: actions/checkout@');
const refIndex = reusableReleaseWorkflowSource.indexOf('ref: ${{ inputs.tag }}');
const validateIndex = reusableReleaseWorkflowSource.indexOf('name: Validate version tag');
assert.ok(checkoutIndex >= 0, 'reusable-release.yml should check out repository content');
assert.ok(refIndex >= 0, 'reusable-release.yml checkout should use inputs.tag as ref');
assert.ok(validateIndex >= 0, 'reusable-release.yml should validate requested tag');
assert.ok(
checkoutIndex < refIndex && refIndex < validateIndex,
'reusable release should check out inputs.tag before tag validation and publish steps'
);
})) passed++; else failed++;
if (test('CI runs for release branches and version tags before release workflows execute', () => {
const pushBlockMatch = normalizedCiWorkflowSource.match(/on:\n\s+push:\n([\s\S]*?)\n\s+pull_request:/);
const pushBlock = pushBlockMatch ? pushBlockMatch[1] : '';
assert.ok(pushBlock, 'ci.yml should define a push trigger block');
assert.match(
pushBlock,
/branches:\s*\[[^\]]*main[^\]]*['"]release\/\*\*['"][^\]]*\]/,
'ci.yml push branches should include release/**'
);
assert.match(
pushBlock,
/tags:\s*\[[^\]]*['"]v\*['"][^\]]*\]/,
'ci.yml push tags should include v*'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+326
View File
@@ -0,0 +1,326 @@
/**
* Tests for scripts/repair.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const INSTALL_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const DOCTOR_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'doctor.js');
const REPAIR_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'repair.js');
const REPO_ROOT = path.join(__dirname, '..', '..');
const CLI_TIMEOUT_MS = 30000;
const CURRENT_PACKAGE_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')
).version;
const CURRENT_MANIFEST_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'manifests', 'install-modules.json'), 'utf8')
).version;
const {
createInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeState(filePath, options) {
const state = createInstallState(options);
writeInstallState(filePath, state);
return state;
}
function runNode(scriptPath, args = [], options = {}) {
const homeDir = options.homeDir || process.env.HOME;
const env = {
...process.env,
HOME: homeDir,
USERPROFILE: homeDir,
};
try {
const stdout = execFileSync('node', [scriptPath, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: options.timeout || CLI_TIMEOUT_MS,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || error.message || '',
};
}
}
function normalizeComparablePath(filePath) {
const normalized = path.normalize(filePath);
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
}
function pathListIncludes(paths, expectedPath) {
const normalizedExpected = normalizeComparablePath(expectedPath);
return paths.some(filePath => normalizeComparablePath(filePath) === normalizedExpected);
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing repair.js ===\n');
let passed = 0;
let failed = 0;
if (test('repairs drifted files from a real install-apply state', () => {
const homeDir = createTempDir('repair-home-');
const projectRoot = createTempDir('repair-project-');
try {
const installResult = runNode(INSTALL_SCRIPT, ['--target', 'cursor', 'typescript'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(installResult.code, 0, installResult.stderr);
const normalizedProjectRoot = fs.realpathSync(projectRoot);
const managedPath = path.join(normalizedProjectRoot, '.cursor', 'hooks', 'session-start.js');
const statePath = path.join(normalizedProjectRoot, '.cursor', 'ecc-install-state.json');
const expectedContent = fs.readFileSync(
path.join(REPO_ROOT, '.cursor', 'hooks', 'session-start.js'),
'utf8'
);
fs.writeFileSync(managedPath, '// drifted\n');
const doctorBefore = runNode(DOCTOR_SCRIPT, ['--target', 'cursor', '--json'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(doctorBefore.code, 1);
assert.ok(JSON.parse(doctorBefore.stdout).results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
const repairResult = runNode(REPAIR_SCRIPT, ['--target', 'cursor', '--json'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(repairResult.code, 0, repairResult.stderr);
const parsed = JSON.parse(repairResult.stdout);
assert.strictEqual(parsed.results[0].status, 'repaired');
assert.ok(pathListIncludes(parsed.results[0].repairedPaths, managedPath));
assert.strictEqual(fs.readFileSync(managedPath, 'utf8'), expectedContent);
assert.ok(fs.existsSync(statePath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repairs drifted non-copy managed operations and refreshes install-state', () => {
const homeDir = createTempDir('repair-home-');
const projectRoot = createTempDir('repair-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
fs.mkdirSync(targetRoot, { recursive: true });
const normalizedTargetRoot = fs.realpathSync(targetRoot);
const statePath = path.join(normalizedTargetRoot, 'ecc-install-state.json');
const jsonPath = path.join(normalizedTargetRoot, 'hooks.json');
const renderedPath = path.join(normalizedTargetRoot, 'generated.md');
const removedPath = path.join(normalizedTargetRoot, 'legacy-note.txt');
fs.writeFileSync(jsonPath, JSON.stringify({ existing: true, managed: false }, null, 2));
fs.writeFileSync(renderedPath, '# drifted\n');
fs.writeFileSync(removedPath, 'stale\n');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: normalizedTargetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath: jsonPath,
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
managed: true,
nested: {
enabled: true,
},
},
},
{
kind: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/generated.md.template',
destinationPath: renderedPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '# generated\n',
},
{
kind: 'remove',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/legacy-note.txt',
destinationPath: removedPath,
strategy: 'remove',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const doctorBefore = runNode(DOCTOR_SCRIPT, ['--target', 'cursor', '--json'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(doctorBefore.code, 1);
assert.ok(JSON.parse(doctorBefore.stdout).results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
const installedAtBefore = JSON.parse(fs.readFileSync(statePath, 'utf8')).installedAt;
const repairResult = runNode(REPAIR_SCRIPT, ['--target', 'cursor', '--json'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(repairResult.code, 0, repairResult.stderr);
const parsed = JSON.parse(repairResult.stdout);
assert.strictEqual(parsed.results[0].status, 'repaired');
assert.ok(parsed.results[0].repairedPaths.includes(jsonPath));
assert.ok(parsed.results[0].repairedPaths.includes(renderedPath));
assert.ok(parsed.results[0].repairedPaths.includes(removedPath));
assert.deepStrictEqual(JSON.parse(fs.readFileSync(jsonPath, 'utf8')), {
existing: true,
managed: true,
nested: {
enabled: true,
},
});
assert.strictEqual(fs.readFileSync(renderedPath, 'utf8'), '# generated\n');
assert.ok(!fs.existsSync(removedPath));
const repairedState = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(repairedState.installedAt, installedAtBefore);
assert.ok(repairedState.lastValidatedAt);
const doctorAfter = runNode(DOCTOR_SCRIPT, ['--target', 'cursor'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(doctorAfter.code, 0, doctorAfter.stderr);
assert.ok(doctorAfter.stdout.includes('Status: OK'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('supports dry-run without mutating drifted non-copy operations', () => {
const homeDir = createTempDir('repair-home-');
const projectRoot = createTempDir('repair-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
fs.mkdirSync(targetRoot, { recursive: true });
const normalizedTargetRoot = fs.realpathSync(targetRoot);
const statePath = path.join(normalizedTargetRoot, 'ecc-install-state.json');
const renderedPath = path.join(normalizedTargetRoot, 'generated.md');
fs.writeFileSync(renderedPath, '# drifted\n');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: normalizedTargetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/generated.md.template',
destinationPath: renderedPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '# generated\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const repairResult = runNode(REPAIR_SCRIPT, ['--target', 'cursor', '--dry-run', '--json'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(repairResult.code, 0, repairResult.stderr);
const parsed = JSON.parse(repairResult.stdout);
assert.strictEqual(parsed.dryRun, true);
assert.ok(parsed.results[0].plannedRepairs.includes(renderedPath));
assert.strictEqual(fs.readFileSync(renderedPath, 'utf8'), '# drifted\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+300
View File
@@ -0,0 +1,300 @@
/**
* Tests for scripts/session-inspect.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const { getFallbackSessionRecordingPath } = require('../../scripts/lib/session-adapters/canonical-session');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'session-inspect.js');
function run(args = [], options = {}) {
const envOverrides = {
...(options.env || {})
};
if (typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)) {
envOverrides.USERPROFILE = envOverrides.HOME;
}
if (typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)) {
envOverrides.HOME = envOverrides.USERPROFILE;
}
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
cwd: options.cwd || process.cwd(),
env: {
...process.env,
...envOverrides
}
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing session-inspect.js ===\n');
let passed = 0;
let failed = 0;
if (test('shows usage when no target is provided', () => {
const result = run();
assert.strictEqual(result.code, 1);
assert.ok(result.stdout.includes('Usage:'));
})) passed++; else failed++;
if (test('lists registered adapters', () => {
const result = run(['--list-adapters']);
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.ok(Array.isArray(payload.adapters));
assert.ok(payload.adapters.some(adapter => adapter.id === 'claude-history'));
assert.ok(payload.adapters.some(adapter => adapter.id === 'dmux-tmux'));
})) passed++; else failed++;
if (test('prints canonical JSON for claude history targets', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-recordings-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
try {
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
'# Inspect Session\n\n**Branch:** feat/session-inspect\n'
);
const result = run(['claude:latest'], {
env: {
HOME: homeDir,
ECC_SESSION_RECORDING_DIR: recordingDir
}
});
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
const recordingPath = getFallbackSessionRecordingPath(payload, { recordingDir });
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
assert.strictEqual(payload.adapterId, 'claude-history');
assert.strictEqual(payload.session.kind, 'history');
assert.strictEqual(payload.workers[0].branch, 'feat/session-inspect');
assert.strictEqual(persisted.latest.adapterId, 'claude-history');
assert.strictEqual(persisted.history.length, 1);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(recordingDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('supports explicit target types for structured registry routing', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
try {
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
'# Inspect Session\n\n**Branch:** feat/typed-inspect\n'
);
const result = run(['latest', '--target-type', 'claude-history'], {
env: { HOME: homeDir }
});
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.adapterId, 'claude-history');
assert.strictEqual(payload.session.sourceTarget.type, 'claude-history');
assert.strictEqual(payload.workers[0].branch, 'feat/typed-inspect');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('writes snapshot JSON to disk when --write is provided', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-out-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
const outputPath = path.join(outputDir, 'snapshot.json');
try {
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
'# Inspect Session\n\n**Branch:** feat/session-inspect\n'
);
const result = run(['claude:latest', '--write', outputPath], {
env: { HOME: homeDir }
});
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(outputPath));
const written = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
assert.strictEqual(written.adapterId, 'claude-history');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(outputDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('inspects skill health from recorded observations', () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-skills-'));
const observationsDir = path.join(projectRoot, '.claude', 'ecc', 'skills');
fs.mkdirSync(observationsDir, { recursive: true });
fs.writeFileSync(
path.join(observationsDir, 'observations.jsonl'),
[
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-1',
timestamp: '2026-03-14T12:00:00.000Z',
task: 'Review auth middleware',
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
outcome: { success: false, status: 'failure', error: 'missing csrf guidance', feedback: 'Need CSRF coverage' },
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-1' }
}),
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-2',
timestamp: '2026-03-14T12:05:00.000Z',
task: 'Review auth middleware',
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
outcome: { success: false, status: 'failure', error: 'missing csrf guidance', feedback: null },
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-2' }
})
].join('\n') + '\n'
);
try {
const result = run(['skills:health'], { cwd: projectRoot });
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.skill-health.v1');
assert.ok(payload.skills.some(skill => skill.skill.id === 'security-review'));
} finally {
fs.rmSync(projectRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('proposes skill amendments through session-inspect', () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-amend-'));
const observationsDir = path.join(projectRoot, '.claude', 'ecc', 'skills');
fs.mkdirSync(observationsDir, { recursive: true });
fs.writeFileSync(
path.join(observationsDir, 'observations.jsonl'),
[
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-1',
timestamp: '2026-03-14T12:00:00.000Z',
task: 'Add rate limiting',
skill: { id: 'api-design', path: 'skills/api-design/SKILL.md' },
outcome: { success: false, status: 'failure', error: 'missing rate limiting guidance', feedback: 'Need rate limiting examples' },
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-1' }
})
].join('\n') + '\n'
);
try {
const result = run(['skills:amendify', '--skill', 'api-design'], { cwd: projectRoot });
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.skill-amendment-proposal.v1');
assert.strictEqual(payload.skill.id, 'api-design');
assert.ok(payload.patch.preview.includes('Failure-Driven Amendments'));
} finally {
fs.rmSync(projectRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('builds skill evaluation scaffolding through session-inspect', () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-eval-'));
const observationsDir = path.join(projectRoot, '.claude', 'ecc', 'skills');
fs.mkdirSync(observationsDir, { recursive: true });
fs.writeFileSync(
path.join(observationsDir, 'observations.jsonl'),
[
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-1',
timestamp: '2026-03-14T12:00:00.000Z',
task: 'Fix flaky login test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
outcome: { success: false, status: 'failure', error: null, feedback: null },
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-1' }
}),
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-2',
timestamp: '2026-03-14T12:10:00.000Z',
task: 'Fix flaky checkout test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
outcome: { success: true, status: 'success', error: null, feedback: null },
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-2' }
}),
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-3',
timestamp: '2026-03-14T12:20:00.000Z',
task: 'Fix flaky login test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
outcome: { success: true, status: 'success', error: null, feedback: null },
run: { variant: 'amended', amendmentId: 'amend-1', sessionId: 'sess-3' }
}),
JSON.stringify({
schemaVersion: 'ecc.skill-observation.v1',
observationId: 'obs-4',
timestamp: '2026-03-14T12:30:00.000Z',
task: 'Fix flaky checkout test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
outcome: { success: true, status: 'success', error: null, feedback: null },
run: { variant: 'amended', amendmentId: 'amend-1', sessionId: 'sess-4' }
})
].join('\n') + '\n'
);
try {
const result = run(['skills:evaluate', '--skill', 'e2e-testing', '--amendment-id', 'amend-1'], { cwd: projectRoot });
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.skill-evaluation.v1');
assert.strictEqual(payload.recommendation, 'promote-amendment');
} finally {
fs.rmSync(projectRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+397
View File
@@ -0,0 +1,397 @@
/**
* Tests for scripts/setup-package-manager.js
*
* Tests CLI argument parsing and output via subprocess invocation.
*
* Run with: node tests/scripts/setup-package-manager.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'setup-package-manager.js');
// Run the script with given args, return { stdout, stderr, code }
function run(args = [], env = {}) {
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ...env },
timeout: 10000
});
return { stdout, stderr: '', code: 0 };
} catch (err) {
return {
stdout: err.stdout || '',
stderr: err.stderr || '',
code: err.status || 1
};
}
}
// Test helper
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing setup-package-manager.js ===\n');
let passed = 0;
let failed = 0;
// --help flag
console.log('--help:');
if (test('shows help with --help flag', () => {
const result = run(['--help']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Package Manager Setup'));
assert.ok(result.stdout.includes('--detect'));
assert.ok(result.stdout.includes('--global'));
assert.ok(result.stdout.includes('--project'));
})) passed++; else failed++;
if (test('shows help with -h flag', () => {
const result = run(['-h']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Package Manager Setup'));
})) passed++; else failed++;
if (test('shows help with no arguments', () => {
const result = run([]);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Package Manager Setup'));
})) passed++; else failed++;
// --detect flag
console.log('\n--detect:');
if (test('detects current package manager', () => {
const result = run(['--detect']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Package Manager Detection'));
assert.ok(result.stdout.includes('Current selection'));
})) passed++; else failed++;
if (test('shows detection sources', () => {
const result = run(['--detect']);
assert.ok(result.stdout.includes('From package.json'));
assert.ok(result.stdout.includes('From lock file'));
assert.ok(result.stdout.includes('Environment var'));
})) passed++; else failed++;
if (test('shows available managers in detection output', () => {
const result = run(['--detect']);
assert.ok(result.stdout.includes('npm'));
assert.ok(result.stdout.includes('pnpm'));
assert.ok(result.stdout.includes('yarn'));
assert.ok(result.stdout.includes('bun'));
})) passed++; else failed++;
// --list flag
console.log('\n--list:');
if (test('lists available package managers', () => {
const result = run(['--list']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Available Package Managers'));
assert.ok(result.stdout.includes('npm'));
assert.ok(result.stdout.includes('Lock file'));
assert.ok(result.stdout.includes('Install'));
})) passed++; else failed++;
// --global flag
console.log('\n--global:');
if (test('rejects --global without package manager name', () => {
const result = run(['--global']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('rejects --global with unknown package manager', () => {
const result = run(['--global', 'unknown-pm']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown package manager'));
})) passed++; else failed++;
// --project flag
console.log('\n--project:');
if (test('rejects --project without package manager name', () => {
const result = run(['--project']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('rejects --project with unknown package manager', () => {
const result = run(['--project', 'unknown-pm']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown package manager'));
})) passed++; else failed++;
// Positional argument
console.log('\npositional argument:');
if (test('rejects unknown positional argument', () => {
const result = run(['not-a-pm']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown option or package manager'));
})) passed++; else failed++;
// Environment variable
console.log('\nenvironment variable:');
if (test('detects env var override', () => {
const result = run(['--detect'], { CLAUDE_PACKAGE_MANAGER: 'pnpm' });
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('pnpm'));
})) passed++; else failed++;
// --detect output completeness
console.log('\n--detect output completeness:');
if (test('shows all three command types in detection output', () => {
const result = run(['--detect']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Install:'), 'Should show Install command');
assert.ok(result.stdout.includes('Run script:'), 'Should show Run script command');
assert.ok(result.stdout.includes('Execute binary:'), 'Should show Execute binary command');
})) passed++; else failed++;
if (test('shows current marker for active package manager', () => {
const result = run(['--detect']);
assert.ok(result.stdout.includes('(current)'), 'Should mark current PM');
})) passed++; else failed++;
// ── Round 31: flag-as-PM-name rejection ──
// Note: --help, --detect, --list are checked BEFORE --global/--project in argv
// parsing, so passing e.g. --global --list triggers the --list handler first.
// The startsWith('-') fix protects against flags that AREN'T caught earlier,
// like --global --project or --project --unknown-flag.
console.log('\n--global flag validation (Round 31):');
if (test('rejects --global --project (flag not caught by earlier checks)', () => {
const result = run(['--global', '--project']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('rejects --global --unknown-flag (arbitrary flag as PM name)', () => {
const result = run(['--global', '--foo-bar']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('rejects --global -x (single-dash flag as PM name)', () => {
const result = run(['--global', '-x']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('--global --list is handled by --list check first (exit 0)', () => {
// --list is checked before --global in the parsing order
const result = run(['--global', '--list']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Available Package Managers'));
})) passed++; else failed++;
console.log('\n--project flag validation (Round 31):');
if (test('rejects --project --global (cross-flag confusion)', () => {
// --global handler runs before --project, catches it first
const result = run(['--project', '--global']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('rejects --project --unknown-flag', () => {
const result = run(['--project', '--bar']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
if (test('rejects --project -z (single-dash flag)', () => {
const result = run(['--project', '-z']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('requires a package manager name'));
})) passed++; else failed++;
// ── Round 45: output completeness and marker uniqueness ──
console.log('\n--detect marker uniqueness (Round 45):');
if (test('--detect output shows exactly one (current) marker', () => {
const result = run(['--detect']);
assert.strictEqual(result.code, 0);
const lines = result.stdout.split('\n');
const currentLines = lines.filter(l => l.includes('(current)'));
assert.strictEqual(currentLines.length, 1, `Expected exactly 1 "(current)" marker, found ${currentLines.length}`);
// The (current) marker should be on a line with a PM name
assert.ok(/\b(npm|pnpm|yarn|bun)\b/.test(currentLines[0]), 'Current marker should be on a PM line');
})) passed++; else failed++;
console.log('\n--list output completeness (Round 45):');
if (test('--list shows all four supported package managers', () => {
const result = run(['--list']);
assert.strictEqual(result.code, 0);
for (const pm of ['npm', 'pnpm', 'yarn', 'bun']) {
assert.ok(result.stdout.includes(pm), `Should list ${pm}`);
}
// Each PM should show Lock file and Install info
const lockFileCount = (result.stdout.match(/Lock file:/g) || []).length;
assert.strictEqual(lockFileCount, 4, `Expected 4 "Lock file:" entries, found ${lockFileCount}`);
const installCount = (result.stdout.match(/Install:/g) || []).length;
assert.strictEqual(installCount, 4, `Expected 4 "Install:" entries, found ${installCount}`);
})) passed++; else failed++;
// ── Round 62: --global success path and bare PM name ──
console.log('\n--global success path (Round 62):');
if (test('--global npm writes config and succeeds', () => {
const tmpDir = path.join(os.tmpdir(), `spm-test-global-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
try {
const result = run(['--global', 'npm'], { HOME: tmpDir, USERPROFILE: tmpDir });
assert.strictEqual(result.code, 0, `Expected exit 0, got ${result.code}. stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Global preference set to'), 'Should show success message');
assert.ok(result.stdout.includes('npm'), 'Should mention npm');
// Verify config file was created
const configPath = path.join(tmpDir, '.claude', 'package-manager.json');
assert.ok(fs.existsSync(configPath), 'Config file should be created');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(config.packageManager, 'npm', 'Config should contain npm');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log('\nbare PM name success (Round 62):');
if (test('bare npm sets global preference and succeeds', () => {
const tmpDir = path.join(os.tmpdir(), `spm-test-bare-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
try {
const result = run(['npm'], { HOME: tmpDir, USERPROFILE: tmpDir });
assert.strictEqual(result.code, 0, `Expected exit 0, got ${result.code}. stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Global preference set to'), 'Should show success message');
// Verify config file was created
const configPath = path.join(tmpDir, '.claude', 'package-manager.json');
assert.ok(fs.existsSync(configPath), 'Config file should be created');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(config.packageManager, 'npm', 'Config should contain npm');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log('\n--detect source label (Round 62):');
if (test('--detect with env var shows source as environment', () => {
const result = run(['--detect'], { CLAUDE_PACKAGE_MANAGER: 'pnpm' });
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('Source: environment'), 'Should show environment as source');
})) passed++; else failed++;
// ── Round 68: --project success path and --list (current) marker ──
console.log('\n--project success path (Round 68):');
if (test('--project npm writes project config and succeeds', () => {
const tmpDir = path.join(os.tmpdir(), `spm-test-project-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
try {
const result = require('child_process').spawnSync('node', [SCRIPT, '--project', 'npm'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
timeout: 10000,
cwd: tmpDir
});
assert.strictEqual(result.status, 0, `Expected exit 0, got ${result.status}. stderr: ${result.stderr}`);
assert.ok(result.stdout.includes('Project preference set to'), 'Should show project success message');
assert.ok(result.stdout.includes('npm'), 'Should mention npm');
// Verify config file was created in the project CWD
const configPath = path.join(tmpDir, '.claude', 'package-manager.json');
assert.ok(fs.existsSync(configPath), 'Project config file should be created in CWD');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(config.packageManager, 'npm', 'Config should contain npm');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log('\n--list (current) marker (Round 68):');
if (test('--list output includes (current) marker for active PM', () => {
const result = run(['--list']);
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('(current)'), '--list should mark the active PM with (current)');
// The (current) marker should appear exactly once
const currentCount = (result.stdout.match(/\(current\)/g) || []).length;
assert.strictEqual(currentCount, 1, `Expected exactly 1 "(current)" in --list, found ${currentCount}`);
})) passed++; else failed++;
// ── Round 74: setGlobal catch — setPreferredPackageManager throws ──
console.log('\nRound 74: setGlobal catch (save failure):');
if (test('--global npm fails when HOME is not a directory', () => {
if (process.platform === 'win32') {
console.log(' (skipped — /dev/null not available on Windows)');
return;
}
// HOME=/dev/null causes ensureDir to throw ENOTDIR when creating ~/.claude/
const result = run(['--global', 'npm'], { HOME: '/dev/null', USERPROFILE: '/dev/null' });
assert.strictEqual(result.code, 1, `Expected exit 1, got ${result.code}`);
assert.ok(result.stderr.includes('Error:'),
`stderr should contain Error:, got: ${result.stderr}`);
})) passed++; else failed++;
// ── Round 74: setProject catch — setProjectPackageManager throws ──
console.log('\nRound 74: setProject catch (save failure):');
if (test('--project npm fails when CWD is read-only', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return;
}
const tmpDir = path.join(os.tmpdir(), `spm-test-ro-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
try {
// Make CWD read-only so .claude/ dir creation fails with EACCES
fs.chmodSync(tmpDir, 0o555);
const result = require('child_process').spawnSync('node', [SCRIPT, '--project', 'npm'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
timeout: 10000,
cwd: tmpDir
});
assert.strictEqual(result.status, 1,
`Expected exit 1, got ${result.status}. stderr: ${result.stderr}`);
assert.ok(result.stderr.includes('Error:'),
`stderr should contain Error:, got: ${result.stderr}`);
} finally {
try { fs.chmodSync(tmpDir, 0o755); } catch { /* best-effort */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+533
View File
@@ -0,0 +1,533 @@
/**
* Tests for scripts/skill-create-output.js
*
* Tests the SkillCreateOutput class and helper functions.
*
* Run with: node tests/scripts/skill-create-output.test.js
*/
const assert = require('assert');
// Import the module
const { SkillCreateOutput } = require('../../scripts/skill-create-output');
// We also need to test the un-exported helpers by requiring the source
// and extracting them from the module scope. Since they're not exported,
// we test them indirectly through the class methods, plus test the
// exported class directly.
// Test helper
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
// Strip ANSI escape sequences for assertions
function stripAnsi(str) {
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b\[[0-9;]*m/g, '');
}
// Capture console.log output
function captureLog(fn) {
const logs = [];
const origLog = console.log;
console.log = (...args) => logs.push(args.join(' '));
try {
fn();
return logs;
} finally {
console.log = origLog;
}
}
function runTests() {
console.log('\n=== Testing skill-create-output.js ===\n');
let passed = 0;
let failed = 0;
// Constructor tests
console.log('SkillCreateOutput constructor:');
if (test('creates instance with repo name', () => {
const output = new SkillCreateOutput('test-repo');
assert.strictEqual(output.repoName, 'test-repo');
assert.strictEqual(output.width, 70); // default width
})) passed++; else failed++;
if (test('accepts custom width option', () => {
const output = new SkillCreateOutput('repo', { width: 100 });
assert.strictEqual(output.width, 100);
})) passed++; else failed++;
// header() tests
console.log('\nheader():');
if (test('outputs header with repo name', () => {
const output = new SkillCreateOutput('my-project');
const logs = captureLog(() => output.header());
const combined = logs.join('\n');
assert.ok(combined.includes('Skill Creator'), 'Should include Skill Creator');
assert.ok(combined.includes('my-project'), 'Should include repo name');
})) passed++; else failed++;
if (test('header handles long repo names without crash', () => {
const output = new SkillCreateOutput('a-very-long-repository-name-that-exceeds-normal-width-limits');
// Should not throw RangeError
const logs = captureLog(() => output.header());
assert.ok(logs.length > 0, 'Should produce output');
})) passed++; else failed++;
// analysisResults() tests
console.log('\nanalysisResults():');
if (test('displays analysis data', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.analysisResults({
commits: 150,
timeRange: 'Jan 2026 - Feb 2026',
contributors: 3,
files: 200,
}));
const combined = logs.join('\n');
assert.ok(combined.includes('150'), 'Should show commit count');
assert.ok(combined.includes('Jan 2026'), 'Should show time range');
assert.ok(combined.includes('200'), 'Should show file count');
})) passed++; else failed++;
// patterns() tests
console.log('\npatterns():');
if (test('displays patterns with confidence bars', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.patterns([
{ name: 'Test Pattern', trigger: 'when testing', confidence: 0.9, evidence: 'Tests exist' },
{ name: 'Another Pattern', trigger: 'when building', confidence: 0.5, evidence: 'Build exists' },
]));
const combined = logs.join('\n');
assert.ok(combined.includes('Test Pattern'), 'Should show pattern name');
assert.ok(combined.includes('when testing'), 'Should show trigger');
assert.ok(stripAnsi(combined).includes('90%'), 'Should show confidence as percentage');
})) passed++; else failed++;
if (test('handles patterns with missing confidence', () => {
const output = new SkillCreateOutput('repo');
// Should default to 0.8 confidence
const logs = captureLog(() => output.patterns([
{ name: 'No Confidence', trigger: 'always', evidence: 'evidence' },
]));
const combined = logs.join('\n');
assert.ok(stripAnsi(combined).includes('80%'), 'Should default to 80% confidence');
})) passed++; else failed++;
// instincts() tests
console.log('\ninstincts():');
if (test('displays instincts in a box', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.instincts([
{ name: 'instinct-1', confidence: 0.95 },
{ name: 'instinct-2', confidence: 0.7 },
]));
const combined = logs.join('\n');
assert.ok(combined.includes('instinct-1'), 'Should show instinct name');
assert.ok(combined.includes('95%'), 'Should show confidence percentage');
assert.ok(combined.includes('70%'), 'Should show second confidence');
})) passed++; else failed++;
// output() tests
console.log('\noutput():');
if (test('displays file paths', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.output(
'/path/to/SKILL.md',
'/path/to/instincts.yaml'
));
const combined = logs.join('\n');
assert.ok(combined.includes('SKILL.md'), 'Should show skill path');
assert.ok(combined.includes('instincts.yaml'), 'Should show instincts path');
assert.ok(combined.includes('Complete'), 'Should show completion message');
})) passed++; else failed++;
// nextSteps() tests
console.log('\nnextSteps():');
if (test('displays next steps with commands', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.nextSteps());
const combined = logs.join('\n');
assert.ok(combined.includes('Next Steps'), 'Should show Next Steps title');
assert.ok(combined.includes('/instinct-import'), 'Should show import command');
assert.ok(combined.includes('/evolve'), 'Should show evolve command');
})) passed++; else failed++;
// footer() tests
console.log('\nfooter():');
if (test('displays footer with attribution', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.footer());
const combined = logs.join('\n');
assert.ok(combined.includes('Everything Claude Code'), 'Should include project name');
})) passed++; else failed++;
// progressBar edge cases (tests the clamp fix)
console.log('\nprogressBar edge cases:');
if (test('does not crash with confidence > 1.0 (percent > 100)', () => {
const output = new SkillCreateOutput('repo');
// confidence 1.5 => percent 150 — previously crashed with RangeError
const logs = captureLog(() => output.patterns([
{ name: 'Overconfident', trigger: 'always', confidence: 1.5, evidence: 'too much' },
]));
const combined = stripAnsi(logs.join('\n'));
assert.ok(combined.includes('150%'), 'Should show 150%');
})) passed++; else failed++;
if (test('renders 0% confidence bar without crash', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.patterns([
{ name: 'Zero Confidence', trigger: 'never', confidence: 0.0, evidence: 'none' },
]));
const combined = stripAnsi(logs.join('\n'));
assert.ok(combined.includes('0%'), 'Should show 0%');
})) passed++; else failed++;
if (test('renders 100% confidence bar without crash', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.patterns([
{ name: 'Perfect', trigger: 'always', confidence: 1.0, evidence: 'certain' },
]));
const combined = stripAnsi(logs.join('\n'));
assert.ok(combined.includes('100%'), 'Should show 100%');
})) passed++; else failed++;
// Empty array edge cases
console.log('\nempty array edge cases:');
if (test('patterns() with empty array produces header but no entries', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.patterns([]));
const combined = logs.join('\n');
assert.ok(combined.includes('Patterns'), 'Should show header');
})) passed++; else failed++;
if (test('instincts() with empty array produces box but no entries', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.instincts([]));
const combined = logs.join('\n');
assert.ok(combined.includes('Instincts'), 'Should show box title');
})) passed++; else failed++;
// Box drawing crash fix (regression test)
console.log('\nbox() crash prevention:');
if (test('box does not crash on title longer than width', () => {
const output = new SkillCreateOutput('repo', { width: 20 });
// The instincts() method calls box() internally with a title
// that could exceed the narrow width
const logs = captureLog(() => output.instincts([
{ name: 'a-very-long-instinct-name', confidence: 0.9 },
]));
assert.ok(logs.length > 0, 'Should produce output without crash');
})) passed++; else failed++;
if (test('analysisResults does not crash with very narrow width', () => {
const output = new SkillCreateOutput('repo', { width: 10 });
// box() is called with a title that exceeds width=10
const logs = captureLog(() => output.analysisResults({
commits: 1, timeRange: 'today', contributors: 1, files: 1,
}));
assert.ok(logs.length > 0, 'Should produce output without crash');
})) passed++; else failed++;
// box() alignment regression test
console.log('\nbox() alignment:');
if (test('top, middle, and bottom lines have equal visual width', () => {
const output = new SkillCreateOutput('repo', { width: 40 });
const logs = captureLog(() => output.instincts([
{ name: 'test', confidence: 0.9 },
]));
const combined = logs.join('\n');
const boxLines = combined.split('\n').filter(l => stripAnsi(l).trim().length > 0);
// Find lines that start with box-drawing characters
const boxDrawn = boxLines.filter(l => {
const s = stripAnsi(l).trim();
return s.startsWith('\u256D') || s.startsWith('\u2502') || s.startsWith('\u2570');
});
if (boxDrawn.length >= 3) {
const widths = boxDrawn.map(l => stripAnsi(l).length);
const firstWidth = widths[0];
widths.forEach((w, i) => {
assert.strictEqual(w, firstWidth,
`Line ${i} width ${w} should match first line width ${firstWidth}`);
});
}
})) passed++; else failed++;
// ── Round 27: box and progressBar edge cases ──
console.log('\nbox() content overflow:');
if (test('box does not crash when content line exceeds width', () => {
const output = new SkillCreateOutput('repo', { width: 30 });
// Force a very long instinct name that exceeds width
const logs = captureLog(() => output.instincts([
{ name: 'this-is-an-extremely-long-instinct-name-that-clearly-exceeds-width', confidence: 0.9 },
]));
// Math.max(0, padding) should prevent RangeError
assert.ok(logs.length > 0, 'Should produce output without RangeError');
})) passed++; else failed++;
if (test('patterns renders negative confidence without crash', () => {
const output = new SkillCreateOutput('repo');
// confidence -0.1 => percent -10 — Math.max(0, ...) should clamp filled to 0
const logs = captureLog(() => output.patterns([
{ name: 'Negative', trigger: 'never', confidence: -0.1, evidence: 'impossible' },
]));
const combined = stripAnsi(logs.join('\n'));
assert.ok(combined.includes('-10%'), 'Should show -10%');
})) passed++; else failed++;
if (test('header does not crash with very long repo name', () => {
const longRepo = 'A'.repeat(100);
const output = new SkillCreateOutput(longRepo);
// Math.max(0, 55 - stripAnsi(subtitle).length) protects against negative repeat
const logs = captureLog(() => output.header());
assert.ok(logs.length > 0, 'Should produce output without crash');
})) passed++; else failed++;
if (test('stripAnsi handles nested ANSI codes with multi-digit params', () => {
// Simulate bold + color + reset
const ansiStr = '\x1b[1m\x1b[36mBold Cyan\x1b[0m\x1b[0m';
const stripped = stripAnsi(ansiStr);
assert.strictEqual(stripped, 'Bold Cyan', 'Should strip all nested ANSI sequences');
})) passed++; else failed++;
if (test('footer produces output', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.footer());
const combined = stripAnsi(logs.join('\n'));
assert.ok(combined.includes('Powered by'), 'Should include attribution text');
})) passed++; else failed++;
// ── Round 34: header width alignment ──
console.log('\nheader() width alignment (Round 34):');
if (test('header subtitle line matches border width', () => {
const output = new SkillCreateOutput('test-repo');
const logs = captureLog(() => output.header());
// Find the border and subtitle lines
const lines = logs.map(l => stripAnsi(l));
const borderLine = lines.find(l => l.includes('═══'));
const subtitleLine = lines.find(l => l.includes('Extracting patterns'));
assert.ok(borderLine, 'Should find border line');
assert.ok(subtitleLine, 'Should find subtitle line');
// Both lines should have the same visible width
assert.strictEqual(subtitleLine.length, borderLine.length,
`Subtitle width (${subtitleLine.length}) should match border width (${borderLine.length})`);
})) passed++; else failed++;
if (test('header all lines have consistent width for short repo name', () => {
const output = new SkillCreateOutput('abc');
const logs = captureLog(() => output.header());
const lines = logs.map(l => stripAnsi(l)).filter(l => l.includes('║') || l.includes('╔') || l.includes('╚'));
assert.ok(lines.length >= 4, 'Should have at least 4 box lines');
const widths = lines.map(l => l.length);
const first = widths[0];
widths.forEach((w, i) => {
assert.strictEqual(w, first,
`Line ${i} width (${w}) should match first line (${first})`);
});
})) passed++; else failed++;
if (test('header subtitle has correct content area width of 64 chars', () => {
const output = new SkillCreateOutput('myrepo');
const logs = captureLog(() => output.header());
const lines = logs.map(l => stripAnsi(l));
const subtitleLine = lines.find(l => l.includes('Extracting patterns'));
assert.ok(subtitleLine, 'Should find subtitle line');
// Content between ║ and ║ should be 64 chars (border is 66 total)
// Format: ║ + content(64) + ║ = 66
assert.strictEqual(subtitleLine.length, 66,
`Total subtitle line width should be 66, got ${subtitleLine.length}`);
})) passed++; else failed++;
if (test('header subtitle line does not truncate with medium-length repo name', () => {
const output = new SkillCreateOutput('my-medium-repo-name');
const logs = captureLog(() => output.header());
const combined = logs.join('\n');
assert.ok(combined.includes('my-medium-repo-name'), 'Should include full repo name');
const lines = logs.map(l => stripAnsi(l));
const subtitleLine = lines.find(l => l.includes('Extracting patterns'));
assert.ok(subtitleLine, 'Should have subtitle line');
// Should still be 66 chars even with a longer name
assert.strictEqual(subtitleLine.length, 66,
`Subtitle line should be 66 chars, got ${subtitleLine.length}`);
})) passed++; else failed++;
// ── Round 35: box() width accuracy ──
console.log('\nbox() width accuracy (Round 35):');
if (test('box lines in instincts() match the default box width of 60', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.instincts([
{ name: 'test-instinct', confidence: 0.85 },
]));
const combined = logs.join('\n');
const boxLines = combined.split('\n').filter(l => {
const s = stripAnsi(l).trim();
return s.startsWith('\u256D') || s.startsWith('\u2502') || s.startsWith('\u2570');
});
assert.ok(boxLines.length >= 3, 'Should have at least 3 box lines');
// The box() default width is 60 — each line should be exactly 60 chars
boxLines.forEach((l, i) => {
const w = stripAnsi(l).length;
assert.strictEqual(w, 60,
`Box line ${i} should be 60 chars wide, got ${w}`);
});
})) passed++; else failed++;
if (test('box lines with custom width match the requested width', () => {
const output = new SkillCreateOutput('repo', { width: 40 });
const logs = captureLog(() => output.instincts([
{ name: 'short', confidence: 0.9 },
]));
const combined = logs.join('\n');
const boxLines = combined.split('\n').filter(l => {
const s = stripAnsi(l).trim();
return s.startsWith('\u256D') || s.startsWith('\u2502') || s.startsWith('\u2570');
});
assert.ok(boxLines.length >= 3, 'Should have at least 3 box lines');
// instincts() calls box() with no explicit width, so it uses the default 60
// regardless of this.width — verify self-consistency at least
const firstWidth = stripAnsi(boxLines[0]).length;
boxLines.forEach((l, i) => {
const w = stripAnsi(l).length;
assert.strictEqual(w, firstWidth,
`Box line ${i} width ${w} should match first line ${firstWidth}`);
});
})) passed++; else failed++;
if (test('analysisResults box lines are all 60 chars wide', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.analysisResults({
commits: 50, timeRange: 'Jan 2026', contributors: 2, files: 100,
}));
const combined = logs.join('\n');
const boxLines = combined.split('\n').filter(l => {
const s = stripAnsi(l).trim();
return s.startsWith('\u256D') || s.startsWith('\u2502') || s.startsWith('\u2570');
});
assert.ok(boxLines.length >= 3, 'Should have at least 3 box lines');
boxLines.forEach((l, i) => {
const w = stripAnsi(l).length;
assert.strictEqual(w, 60,
`Analysis box line ${i} should be 60 chars, got ${w}`);
});
})) passed++; else failed++;
if (test('nextSteps box lines are all 60 chars wide', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.nextSteps());
const combined = logs.join('\n');
const boxLines = combined.split('\n').filter(l => {
const s = stripAnsi(l).trim();
return s.startsWith('\u256D') || s.startsWith('\u2502') || s.startsWith('\u2570');
});
assert.ok(boxLines.length >= 3, 'Should have at least 3 box lines');
boxLines.forEach((l, i) => {
const w = stripAnsi(l).length;
assert.strictEqual(w, 60,
`NextSteps box line ${i} should be 60 chars, got ${w}`);
});
})) passed++; else failed++;
// ── Round 54: analysisResults with zero values ──
console.log('\nanalysisResults zero values (Round 54):');
if (test('analysisResults handles zero values for all data fields', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.analysisResults({
commits: 0, timeRange: '', contributors: 0, files: 0,
}));
const combined = logs.join('\n');
assert.ok(combined.includes('0'), 'Should display zero values');
assert.ok(logs.length > 0, 'Should produce output without crash');
// Box lines should still be 60 chars wide
const boxLines = combined.split('\n').filter(l => {
const s = stripAnsi(l).trim();
return s.startsWith('\u256D') || s.startsWith('\u2502') || s.startsWith('\u2570');
});
assert.ok(boxLines.length >= 3, 'Should render a complete box');
})) passed++; else failed++;
// ── Round 68: demo function export ──
console.log('\ndemo export (Round 68):');
if (test('module exports demo function alongside SkillCreateOutput', () => {
const mod = require('../../scripts/skill-create-output');
assert.ok(mod.demo, 'Should export demo function');
assert.strictEqual(typeof mod.demo, 'function', 'demo should be a function');
assert.ok(mod.SkillCreateOutput, 'Should also export SkillCreateOutput');
assert.strictEqual(typeof mod.SkillCreateOutput, 'function', 'SkillCreateOutput should be a constructor');
})) passed++; else failed++;
// ── Round 85: patterns() confidence=0 uses ?? (not ||) ──
console.log('\nRound 85: patterns() confidence=0 nullish coalescing:');
if (test('patterns() with confidence=0 shows 0%, not 80% (nullish coalescing fix)', () => {
const output = new SkillCreateOutput('repo');
const logs = captureLog(() => output.patterns([
{ name: 'Zero Confidence', trigger: 'never', confidence: 0, evidence: 'none' },
]));
const combined = stripAnsi(logs.join('\n'));
// With ?? operator: 0 ?? 0.8 = 0 → Math.round(0 * 100) = 0 → shows "0%"
// With || operator (bug): 0 || 0.8 = 0.8 → shows "80%"
assert.ok(combined.includes('0%'), 'Should show 0% for zero confidence');
assert.ok(!combined.includes('80%'),
'Should NOT show 80% — confidence=0 is explicitly provided, not missing');
})) passed++; else failed++;
// ── Round 87: analyzePhase() async method (untested) ──
console.log('\nRound 87: analyzePhase() async method:');
if (test('analyzePhase completes without error and writes to stdout', () => {
const output = new SkillCreateOutput('test-repo');
// analyzePhase is async and calls animateProgress which uses sleep() and
// process.stdout.write/clearLine/cursorTo. In non-TTY environments clearLine
// and cursorTo are undefined, but the code uses optional chaining (?.) to
// handle this safely. We verify it resolves without throwing.
// Capture stdout.write to verify output was produced.
const writes = [];
const origWrite = process.stdout.write;
process.stdout.write = function(str) { writes.push(String(str)); return true; };
try {
// Call synchronously by accessing the returned promise — we just need to
// verify it doesn't throw during setup. The sleeps total 1.9s so we
// verify the promise is a thenable (async function returns Promise).
const promise = output.analyzePhase({ commits: 42 });
assert.ok(promise && typeof promise.then === 'function',
'analyzePhase should return a Promise');
} finally {
process.stdout.write = origWrite;
}
// Verify that process.stdout.write was called (the header line is written synchronously)
assert.ok(writes.length > 0, 'Should have written output via process.stdout.write');
assert.ok(writes.some(w => w.includes('Analyzing')), 'Should include "Analyzing" label');
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+90
View File
@@ -0,0 +1,90 @@
/**
* Source-level tests for scripts/sync-ecc-to-codex.sh
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'sync-ecc-to-codex.sh');
const source = fs.readFileSync(scriptPath, 'utf8');
const normalizedSource = source.replace(/\r\n/g, '\n');
const runOrEchoSource = (() => {
const start = normalizedSource.indexOf('run_or_echo() {');
if (start < 0) {
return '';
}
let depth = 0;
let bodyStart = normalizedSource.indexOf('{', start);
if (bodyStart < 0) {
return '';
}
for (let i = bodyStart; i < normalizedSource.length; i++) {
const char = normalizedSource[i];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return normalizedSource.slice(start, i + 1);
}
}
}
return '';
})();
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing sync-ecc-to-codex.sh ===\n');
let passed = 0;
let failed = 0;
if (test('run_or_echo does not use eval', () => {
assert.ok(runOrEchoSource, 'Expected to locate run_or_echo function body');
assert.ok(!runOrEchoSource.includes('eval "$@"'), 'run_or_echo should not execute through eval');
})) passed++; else failed++;
if (test('run_or_echo executes argv directly', () => {
assert.ok(runOrEchoSource.includes(' "$@"'), 'run_or_echo should execute the argv vector directly');
})) passed++; else failed++;
if (test('dry-run output shell-escapes argv', () => {
assert.ok(runOrEchoSource.includes(`printf ' %q' "$@"`), 'Dry-run mode should print shell-escaped argv');
})) passed++; else failed++;
if (test('filesystem-changing calls use argv-form run_or_echo invocations', () => {
assert.ok(source.includes('run_or_echo mkdir -p "$BACKUP_DIR"'), 'mkdir should use argv form');
// Skills sync rm/cp calls were removed — Codex reads from ~/.agents/skills/ natively
assert.ok(!source.includes('run_or_echo rm -rf "$dest"'), 'skill sync rm should be removed');
assert.ok(!source.includes('run_or_echo cp -R "$skill_dir" "$dest"'), 'skill sync cp should be removed');
})) passed++; else failed++;
if (test('sync script avoids GNU-only grep -P parsing', () => {
assert.ok(!source.includes('grep -oP'), 'sync-ecc-to-codex.sh should remain portable across BSD and GNU environments');
})) passed++; else failed++;
if (test('extract_context7_key uses a portable parser', () => {
assert.ok(source.includes('extract_context7_key() {'), 'Expected extract_context7_key helper');
assert.ok(source.includes('node - "$file"'), 'extract_context7_key should use Node-based parsing');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+179
View File
@@ -0,0 +1,179 @@
/**
* Tests for .trae/install.sh and .trae/uninstall.sh
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const REPO_ROOT = path.join(__dirname, '..', '..');
const INSTALL_SCRIPT = path.join(REPO_ROOT, '.trae', 'install.sh');
const UNINSTALL_SCRIPT = path.join(REPO_ROOT, '.trae', 'uninstall.sh');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function runInstall(options = {}) {
return execFileSync('bash', [INSTALL_SCRIPT, ...(options.args || [])], {
cwd: options.cwd,
env: {
...process.env,
HOME: options.homeDir || process.env.HOME,
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 300000,
});
}
function runUninstall(options = {}) {
return execFileSync('bash', [UNINSTALL_SCRIPT, ...(options.args || [])], {
cwd: options.cwd,
env: {
...process.env,
HOME: options.homeDir || process.env.HOME,
},
encoding: 'utf8',
input: options.input || 'y\n',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 300000,
});
}
function readManifestLines(projectRoot) {
const manifestPath = path.join(projectRoot, '.trae', '.ecc-manifest');
return fs.readFileSync(manifestPath, 'utf8')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing Trae install/uninstall scripts ===\n');
let passed = 0;
let failed = 0;
if (process.platform === 'win32') {
console.log(' - skipped on Windows; Trae shell scripts are Unix-only');
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(0);
}
if (test('does not claim ownership of preexisting target files', () => {
const homeDir = createTempDir('trae-home-');
const projectRoot = createTempDir('trae-project-');
try {
const preexistingCommandPath = path.join(projectRoot, '.trae', 'commands', 'quality-gate.md');
fs.mkdirSync(path.dirname(preexistingCommandPath), { recursive: true });
fs.writeFileSync(preexistingCommandPath, 'user owned command\n');
runInstall({ cwd: projectRoot, homeDir });
const manifestLines = readManifestLines(projectRoot);
assert.ok(!manifestLines.includes('commands/quality-gate.md'), 'Preexisting file should not be recorded in manifest');
runUninstall({ cwd: projectRoot, homeDir });
assert.strictEqual(fs.readFileSync(preexistingCommandPath, 'utf8'), 'user owned command\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('records nested skill files and the full rules tree in the manifest', () => {
const homeDir = createTempDir('trae-home-');
const projectRoot = createTempDir('trae-project-');
try {
runInstall({ cwd: projectRoot, homeDir });
const manifestLines = readManifestLines(projectRoot);
assert.ok(manifestLines.includes('skills/skill-comply/pyproject.toml'));
assert.ok(manifestLines.includes('rules/common/code-review.md'));
assert.ok(manifestLines.includes('rules/python/coding-style.md'));
assert.ok(manifestLines.includes('rules/web/performance.md'));
assert.ok(fs.existsSync(path.join(projectRoot, '.trae', 'skills', 'skill-comply', 'pyproject.toml')));
assert.ok(fs.existsSync(path.join(projectRoot, '.trae', 'rules', 'python', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(projectRoot, '.trae', 'rules', 'web', 'performance.md')));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('reinstall preserves managed manifest coverage without duplicate entries', () => {
const homeDir = createTempDir('trae-home-');
const projectRoot = createTempDir('trae-project-');
try {
runInstall({ cwd: projectRoot, homeDir });
const managedCommandPath = path.join(projectRoot, '.trae', 'commands', 'quality-gate.md');
fs.rmSync(managedCommandPath);
runInstall({ cwd: projectRoot, homeDir });
const manifestLines = readManifestLines(projectRoot);
const entryCount = manifestLines.filter((line) => line === 'commands/quality-gate.md').length;
assert.strictEqual(entryCount, 1, 'Managed file should appear once in manifest after reinstall');
assert.ok(fs.existsSync(managedCommandPath), 'Managed file should be recreated on reinstall');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall rejects manifest entries that escape the Trae root via symlink traversal', () => {
const homeDir = createTempDir('trae-home-');
const projectRoot = createTempDir('trae-project-');
const externalRoot = createTempDir('trae-outside-');
try {
const traeRoot = path.join(projectRoot, '.trae');
fs.mkdirSync(traeRoot, { recursive: true });
const outsideSecretPath = path.join(externalRoot, 'secret.txt');
fs.writeFileSync(outsideSecretPath, 'do not remove\n');
fs.symlinkSync(externalRoot, path.join(traeRoot, 'escape-link'));
fs.writeFileSync(path.join(traeRoot, '.ecc-manifest'), 'escape-link/secret.txt\n.ecc-manifest\n');
const stdout = runUninstall({ cwd: projectRoot, homeDir });
assert.ok(stdout.includes('Skipped: escape-link/secret.txt (invalid manifest entry)'));
assert.strictEqual(fs.readFileSync(outsideSecretPath, 'utf8'), 'do not remove\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(externalRoot);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+287
View File
@@ -0,0 +1,287 @@
/**
* Tests for scripts/uninstall.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const INSTALL_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'uninstall.js');
const REPO_ROOT = path.join(__dirname, '..', '..');
const CURRENT_PACKAGE_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')
).version;
const CURRENT_MANIFEST_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'manifests', 'install-modules.json'), 'utf8')
).version;
const CLI_TIMEOUT_MS = 30000;
const {
createInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeState(filePath, options) {
const state = createInstallState(options);
writeInstallState(filePath, state);
return state;
}
function run(args = [], options = {}) {
const env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
};
try {
const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd,
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: CLI_TIMEOUT_MS,
});
return { code: 0, stdout, stderr: '' };
} catch (error) {
return {
code: error.status || 1,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing uninstall.js ===\n');
let passed = 0;
let failed = 0;
if (test('uninstalls files from a real install-apply state and preserves unrelated files', () => {
const homeDir = createTempDir('uninstall-home-');
const projectRoot = createTempDir('uninstall-project-');
try {
const installStdout = execFileSync('node', [INSTALL_SCRIPT, '--target', 'cursor', 'typescript'], {
cwd: projectRoot,
env: {
...process.env,
HOME: homeDir,
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: CLI_TIMEOUT_MS,
});
assert.ok(installStdout.includes('Done. Install-state written'));
const normalizedProjectRoot = fs.realpathSync(projectRoot);
const managedPath = path.join(normalizedProjectRoot, '.cursor', 'hooks.json');
const statePath = path.join(normalizedProjectRoot, '.cursor', 'ecc-install-state.json');
const unrelatedPath = path.join(normalizedProjectRoot, '.cursor', 'custom-user-note.txt');
fs.writeFileSync(unrelatedPath, 'leave me alone');
const uninstallResult = run(['--target', 'cursor'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr);
assert.ok(uninstallResult.stdout.includes('Uninstall summary'));
assert.ok(!fs.existsSync(managedPath));
assert.ok(!fs.existsSync(statePath));
assert.ok(fs.existsSync(unrelatedPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('reverses non-copy operations and keeps unrelated files', () => {
const homeDir = createTempDir('uninstall-home-');
const projectRoot = createTempDir('uninstall-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
fs.mkdirSync(targetRoot, { recursive: true });
const normalizedTargetRoot = fs.realpathSync(targetRoot);
const statePath = path.join(normalizedTargetRoot, 'ecc-install-state.json');
const copiedPath = path.join(normalizedTargetRoot, 'managed-rule.md');
const mergedPath = path.join(normalizedTargetRoot, 'hooks.json');
const removedPath = path.join(normalizedTargetRoot, 'legacy-note.txt');
const unrelatedPath = path.join(normalizedTargetRoot, 'custom-user-note.txt');
fs.writeFileSync(copiedPath, 'managed\n');
fs.writeFileSync(mergedPath, JSON.stringify({
existing: true,
managed: true,
}, null, 2));
fs.writeFileSync(unrelatedPath, 'leave me alone');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: normalizedTargetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'platform-configs',
sourceRelativePath: 'rules/common/coding-style.md',
destinationPath: copiedPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
},
{
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath: mergedPath,
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
managed: true,
},
previousContent: JSON.stringify({
existing: true,
}, null, 2),
},
{
kind: 'remove',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/legacy-note.txt',
destinationPath: removedPath,
strategy: 'remove',
ownership: 'managed',
scaffoldOnly: false,
previousContent: 'restore me\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const uninstallResult = run(['--target', 'cursor'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr);
assert.ok(uninstallResult.stdout.includes('Uninstall summary'));
assert.ok(!fs.existsSync(copiedPath));
assert.deepStrictEqual(JSON.parse(fs.readFileSync(mergedPath, 'utf8')), {
existing: true,
});
assert.strictEqual(fs.readFileSync(removedPath, 'utf8'), 'restore me\n');
assert.ok(!fs.existsSync(statePath));
assert.ok(fs.existsSync(unrelatedPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('supports dry-run without mutating managed files', () => {
const homeDir = createTempDir('uninstall-home-');
const projectRoot = createTempDir('uninstall-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
fs.mkdirSync(targetRoot, { recursive: true });
const normalizedTargetRoot = fs.realpathSync(targetRoot);
const statePath = path.join(normalizedTargetRoot, 'ecc-install-state.json');
const renderedPath = path.join(normalizedTargetRoot, 'generated.md');
fs.writeFileSync(renderedPath, '# generated\n');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: normalizedTargetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/generated.md.template',
destinationPath: renderedPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '# generated\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const uninstallResult = run(['--target', 'cursor', '--dry-run', '--json'], {
cwd: projectRoot,
homeDir,
});
assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr);
const parsed = JSON.parse(uninstallResult.stdout);
assert.strictEqual(parsed.dryRun, true);
assert.ok(parsed.results[0].plannedRemovals.includes(renderedPath));
assert.ok(fs.existsSync(renderedPath));
assert.ok(fs.existsSync(statePath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();
+152
View File
@@ -0,0 +1,152 @@
'use strict';
/**
* Tests for scripts/work-items.js — focused on the `claim` JIT pickup command.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const { createStateStore } = require('../../scripts/lib/state-store');
const CLI = path.join(__dirname, '..', '..', 'scripts', 'work-items.js');
let passed = 0;
let failed = 0;
async function test(name, fn) {
try {
await fn();
console.log(` PASS ${name}`);
passed += 1;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
function runClaim(dbPath, args) {
const result = spawnSync('node', [CLI, 'claim', '--db', dbPath, '--json', ...args], {
encoding: 'utf8'
});
return result;
}
async function seed(dbPath) {
const store = await createStateStore({ dbPath });
try {
// High-priority, unassigned, open — the JIT pickup target.
store.upsertWorkItem({
id: 'wi-unassigned-high',
source: 'github-issue',
title: 'Fix the gate bypass',
status: 'open',
priority: 'high',
owner: null,
metadata: {}
});
// Low-priority, unassigned, open — should be picked only after the high one.
store.upsertWorkItem({
id: 'wi-unassigned-low',
source: 'manual',
title: 'Tidy docs',
status: 'open',
priority: 'low',
owner: null,
metadata: {}
});
// Already owned — must never be auto-claimed.
store.upsertWorkItem({
id: 'wi-owned',
source: 'manual',
title: 'In progress',
status: 'running',
priority: 'high',
owner: 'codex',
metadata: {}
});
// Done — must never be claimed.
store.upsertWorkItem({
id: 'wi-done',
source: 'manual',
title: 'Shipped',
status: 'done',
priority: 'high',
owner: null,
metadata: {}
});
} finally {
store.close();
}
}
async function run() {
console.log('\n=== Testing work-items.js claim ===\n');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'work-items-claim-'));
const dbPath = path.join(dir, 'state.db');
try {
await seed(dbPath);
await test('claim picks the highest-priority unassigned open item and sets owner + kind', async () => {
const result = runClaim(dbPath, ['--owner', 'alice', '--as', 'human']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.claimed, true);
assert.strictEqual(payload.item.id, 'wi-unassigned-high', 'high-priority item claimed first');
assert.strictEqual(payload.item.owner, 'alice');
assert.strictEqual(payload.item.status, 'running', 'claim moves the card to running');
assert.strictEqual(payload.item.metadata.assigneeKind, 'human');
});
await test('a second claim takes the next unassigned item, not an owned or done one', async () => {
const result = runClaim(dbPath, ['--owner', 'bot-7', '--as', 'agent']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.claimed, true);
assert.strictEqual(payload.item.id, 'wi-unassigned-low');
assert.strictEqual(payload.item.metadata.assigneeKind, 'agent');
});
await test('claim reports nothing to do once the queue is drained', async () => {
const result = runClaim(dbPath, ['--owner', 'alice']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.claimed, false);
assert.strictEqual(payload.reason, 'no-unassigned-open-items');
});
await test('claim of a specific id works and rejects a missing id', async () => {
const owned = runClaim(dbPath, ['wi-owned', '--owner', 'carol']);
assert.strictEqual(owned.status, 0, owned.stderr);
assert.strictEqual(JSON.parse(owned.stdout).item.owner, 'carol', 'explicit id can be re-claimed');
const missing = runClaim(dbPath, ['nope-404', '--owner', 'carol']);
assert.notStrictEqual(missing.status, 0, 'missing id should fail');
assert.ok(/not found/i.test(missing.stderr), 'reports not found');
});
await test('claim requires --owner and validates --as', async () => {
const noOwner = runClaim(dbPath, ['wi-done']);
assert.notStrictEqual(noOwner.status, 0);
assert.ok(/requires --owner/i.test(noOwner.stderr));
const badKind = runClaim(dbPath, ['wi-owned', '--owner', 'x', '--as', 'robot']);
assert.notStrictEqual(badKind.status, 0);
assert.ok(/agent.*human/i.test(badKind.stderr));
});
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
run();