wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation

This commit is contained in:
2026-03-29 22:08:40 +09:00
parent aca7bf592a
commit 2507de45d3
4289 changed files with 732689 additions and 28672 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
/**
* Tests for `.codex/config.toml` reference defaults.
*
* Run with: node tests/codex-config.test.js
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const repoRoot = path.join(__dirname, '..');
const configPath = path.join(repoRoot, '.codex', 'config.toml');
const config = fs.readFileSync(configPath, 'utf8');
const codexAgentsDir = path.join(repoRoot, '.codex', 'agents');
let passed = 0;
let failed = 0;
if (
test('reference config does not pin a top-level model', () => {
assert.ok(!/^model\s*=/m.test(config), 'Expected `.codex/config.toml` to inherit the CLI default model');
})
)
passed++;
else failed++;
if (
test('reference config does not pin a top-level model provider', () => {
assert.ok(
!/^model_provider\s*=/m.test(config),
'Expected `.codex/config.toml` to inherit the CLI default provider',
);
})
)
passed++;
else failed++;
if (
test('sample Codex role configs do not use o4-mini', () => {
const roleFiles = fs.readdirSync(codexAgentsDir).filter(file => file.endsWith('.toml'));
assert.ok(roleFiles.length > 0, 'Expected sample role config files under `.codex/agents`');
for (const roleFile of roleFiles) {
const rolePath = path.join(codexAgentsDir, roleFile);
const roleConfig = fs.readFileSync(rolePath, 'utf8');
assert.ok(
!/^model\s*=\s*"o4-mini"$/m.test(roleConfig),
`Expected sample role config to avoid o4-mini: ${roleFile}`,
);
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,145 @@
/**
* Tests for scripts/hooks/auto-tmux-dev.js
*
* Tests dev server command transformation for tmux wrapping.
*
* Run with: node tests/hooks/auto-tmux-dev.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'auto-tmux-dev.js');
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 runScript(input) {
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: typeof input === 'string' ? input : JSON.stringify(input),
timeout: 10000,
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function runTests() {
console.log('\n=== Testing auto-tmux-dev.js ===\n');
let passed = 0;
let failed = 0;
// Check if tmux is available for conditional tests
const tmuxAvailable = spawnSync('which', ['tmux'], { encoding: 'utf8' }).status === 0;
console.log('Dev server detection:');
if (test('transforms npm run dev command', () => {
const result = runScript({ tool_input: { command: 'npm run dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'), 'Should contain tmux');
assert.ok(output.tool_input.command.includes('npm run dev'), 'Should contain original command');
}
})) passed++; else failed++;
if (test('transforms pnpm dev command', () => {
const result = runScript({ tool_input: { command: 'pnpm dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'));
}
})) passed++; else failed++;
if (test('transforms yarn dev command', () => {
const result = runScript({ tool_input: { command: 'yarn dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'));
}
})) passed++; else failed++;
if (test('transforms bun run dev command', () => {
const result = runScript({ tool_input: { command: 'bun run dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'));
}
})) passed++; else failed++;
console.log('\nNon-dev commands (pass-through):');
if (test('does not transform npm install', () => {
const input = { tool_input: { command: 'npm install' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm install');
})) passed++; else failed++;
if (test('does not transform npm test', () => {
const input = { tool_input: { command: 'npm test' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm test');
})) passed++; else failed++;
if (test('does not transform npm run build', () => {
const input = { tool_input: { command: 'npm run build' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm run build');
})) passed++; else failed++;
if (test('does not transform npm run develop (partial match)', () => {
const input = { tool_input: { command: 'npm run develop' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm run develop');
})) passed++; else failed++;
console.log('\nEdge cases:');
if (test('handles empty input gracefully', () => {
const result = runScript('{}');
assert.strictEqual(result.code, 0);
})) passed++; else failed++;
if (test('handles invalid JSON gracefully', () => {
const result = runScript('not json');
assert.strictEqual(result.code, 0);
assert.strictEqual(result.stdout, 'not json');
})) passed++; else failed++;
if (test('passes through missing command field', () => {
const input = { tool_input: {} };
const result = runScript(input);
assert.strictEqual(result.code, 0);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,108 @@
/**
* Tests for scripts/hooks/check-hook-enabled.js
*
* Tests the CLI wrapper around isHookEnabled.
*
* Run with: node tests/hooks/check-hook-enabled.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'check-hook-enabled.js');
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 runScript(args = [], envOverrides = {}) {
const env = { ...process.env, ...envOverrides };
// Remove potentially interfering env vars unless explicitly set
if (!envOverrides.ECC_HOOK_PROFILE) delete env.ECC_HOOK_PROFILE;
if (!envOverrides.ECC_DISABLED_HOOKS) delete env.ECC_DISABLED_HOOKS;
const result = spawnSync('node', [script, ...args], {
encoding: 'utf8',
timeout: 10000,
env,
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function runTests() {
console.log('\n=== Testing check-hook-enabled.js ===\n');
let passed = 0;
let failed = 0;
console.log('No arguments:');
if (test('returns yes when no hookId provided', () => {
const result = runScript([]);
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
console.log('\nDefault profile (standard):');
if (test('returns yes for hook with default profiles', () => {
const result = runScript(['my-hook']);
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
if (test('returns yes for hook with standard,strict profiles', () => {
const result = runScript(['my-hook', 'standard,strict']);
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
if (test('returns no for hook with only strict profile', () => {
const result = runScript(['my-hook', 'strict']);
assert.strictEqual(result.stdout, 'no');
})) passed++; else failed++;
if (test('returns no for hook with only minimal profile', () => {
const result = runScript(['my-hook', 'minimal']);
assert.strictEqual(result.stdout, 'no');
})) passed++; else failed++;
console.log('\nDisabled hooks:');
if (test('returns no when hook is disabled via env', () => {
const result = runScript(['my-hook'], { ECC_DISABLED_HOOKS: 'my-hook' });
assert.strictEqual(result.stdout, 'no');
})) passed++; else failed++;
if (test('returns yes when different hook is disabled', () => {
const result = runScript(['my-hook'], { ECC_DISABLED_HOOKS: 'other-hook' });
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
console.log('\nProfile overrides:');
if (test('returns yes for strict profile with strict-only hook', () => {
const result = runScript(['my-hook', 'strict'], { ECC_HOOK_PROFILE: 'strict' });
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
if (test('returns yes for minimal profile with minimal-only hook', () => {
const result = runScript(['my-hook', 'minimal'], { ECC_HOOK_PROFILE: 'minimal' });
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,157 @@
/**
* Tests for scripts/hooks/config-protection.js via run-with-flags.js
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runHook(input, env = {}) {
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [runner, 'pre:config-protection', 'scripts/hooks/config-protection.js', 'standard,strict'], {
input: rawInput,
encoding: 'utf8',
env: {
...process.env,
ECC_HOOK_PROFILE: 'standard',
...env
},
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe']
});
return {
code: Number.isInteger(result.status) ? result.status : 1,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
function runCustomHook(pluginRoot, hookId, relScriptPath, input, env = {}) {
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [runner, hookId, relScriptPath, 'standard,strict'], {
input: rawInput,
encoding: 'utf8',
env: {
...process.env,
CLAUDE_PLUGIN_ROOT: pluginRoot,
ECC_HOOK_PROFILE: 'standard',
...env
},
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe']
});
return {
code: Number.isInteger(result.status) ? result.status : 1,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
function runTests() {
console.log('\n=== Testing config-protection ===\n');
let passed = 0;
let failed = 0;
if (test('blocks protected config file edits through run-with-flags', () => {
const input = {
tool_name: 'Write',
tool_input: {
file_path: '.eslintrc.js',
content: 'module.exports = {};'
}
};
const result = runHook(input);
assert.strictEqual(result.code, 2, 'Expected protected config edit to be blocked');
assert.strictEqual(result.stdout, '', 'Blocked hook should not echo raw input');
assert.ok(result.stderr.includes('BLOCKED: Modifying .eslintrc.js is not allowed.'), `Expected block message, got: ${result.stderr}`);
})) passed++; else failed++;
if (test('passes through safe file edits unchanged', () => {
const input = {
tool_name: 'Write',
tool_input: {
file_path: 'src/index.js',
content: 'console.log("ok");'
}
};
const rawInput = JSON.stringify(input);
const result = runHook(input);
assert.strictEqual(result.code, 0, 'Expected safe file edit to pass');
assert.strictEqual(result.stdout, rawInput, 'Expected exact raw JSON passthrough');
assert.strictEqual(result.stderr, '', 'Expected no stderr for safe edits');
})) passed++; else failed++;
if (test('blocks truncated protected config payloads instead of failing open', () => {
const rawInput = JSON.stringify({
tool_name: 'Write',
tool_input: {
file_path: '.eslintrc.js',
content: 'x'.repeat(1024 * 1024 + 2048)
}
});
const result = runHook(rawInput);
assert.strictEqual(result.code, 2, 'Expected truncated protected payload to be blocked');
assert.strictEqual(result.stdout, '', 'Blocked truncated payload should not echo raw input');
assert.ok(result.stderr.includes('Hook input exceeded 1048576 bytes'), `Expected size warning, got: ${result.stderr}`);
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
})) passed++; else failed++;
if (test('legacy hooks do not echo raw input when they fail without stdout', () => {
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
const scriptDir = path.join(pluginRoot, 'scripts', 'hooks');
const scriptPath = path.join(scriptDir, 'legacy-block.js');
try {
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(
scriptPath,
'#!/usr/bin/env node\nprocess.stderr.write("blocked by legacy hook\\n");\nprocess.exit(2);\n'
);
const rawInput = JSON.stringify({
tool_name: 'Write',
tool_input: {
file_path: '.eslintrc.js',
content: 'module.exports = {};'
}
});
const result = runCustomHook(pluginRoot, 'pre:legacy-block', 'scripts/hooks/legacy-block.js', rawInput);
assert.strictEqual(result.code, 2, 'Expected failing legacy hook exit code to propagate');
assert.strictEqual(result.stdout, '', 'Expected failing legacy hook to avoid raw passthrough');
assert.ok(result.stderr.includes('blocked by legacy hook'), `Expected legacy hook stderr, got: ${result.stderr}`);
} finally {
try {
fs.rmSync(pluginRoot, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,138 @@
/**
* Tests for cost-tracker.js hook
*
* Run with: node tests/hooks/cost-tracker.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'cost-tracker.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function makeTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'cost-tracker-test-'));
}
function withTempHome(homeDir) {
return {
HOME: homeDir,
USERPROFILE: homeDir,
};
}
function runScript(input, envOverrides = {}) {
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
env: { ...process.env, ...envOverrides },
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function runTests() {
console.log('\n=== Testing cost-tracker.js ===\n');
let passed = 0;
let failed = 0;
// 1. Passes through input on stdout
(test('passes through input on stdout', () => {
const input = {
model: 'claude-sonnet-4-20250514',
usage: { input_tokens: 100, output_tokens: 50 },
};
const inputStr = JSON.stringify(input);
const result = runScript(input);
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stdout, inputStr, 'Expected stdout to match original input');
}) ? passed++ : failed++);
// 2. Creates metrics file when given valid usage data
(test('creates metrics file when given valid usage data', () => {
const tmpHome = makeTempDir();
const input = {
model: 'claude-sonnet-4-20250514',
usage: { input_tokens: 1000, output_tokens: 500 },
};
const result = runScript(input, withTempHome(tmpHome));
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
assert.ok(fs.existsSync(metricsFile), `Expected metrics file to exist at ${metricsFile}`);
const content = fs.readFileSync(metricsFile, 'utf8').trim();
const row = JSON.parse(content);
assert.strictEqual(row.input_tokens, 1000, 'Expected input_tokens to be 1000');
assert.strictEqual(row.output_tokens, 500, 'Expected output_tokens to be 500');
assert.ok(row.timestamp, 'Expected timestamp to be present');
assert.ok(typeof row.estimated_cost_usd === 'number', 'Expected estimated_cost_usd to be a number');
assert.ok(row.estimated_cost_usd > 0, 'Expected estimated_cost_usd to be positive');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
// 3. Handles empty input gracefully
(test('handles empty input gracefully', () => {
const tmpHome = makeTempDir();
const result = runScript('', withTempHome(tmpHome));
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
// stdout should be empty since input was empty
assert.strictEqual(result.stdout, '', 'Expected empty stdout for empty input');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
// 4. Handles invalid JSON gracefully
(test('handles invalid JSON gracefully', () => {
const tmpHome = makeTempDir();
const invalidInput = 'not valid json {{{';
const result = runScript(invalidInput, withTempHome(tmpHome));
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
// Should still pass through the raw input on stdout
assert.strictEqual(result.stdout, invalidInput, 'Expected stdout to contain original invalid input');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
// 5. Handles missing usage fields gracefully
(test('handles missing usage fields gracefully', () => {
const tmpHome = makeTempDir();
const input = { model: 'claude-sonnet-4-20250514' };
const inputStr = JSON.stringify(input);
const result = runScript(input, withTempHome(tmpHome));
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stdout, inputStr, 'Expected stdout to match original input');
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
assert.ok(fs.existsSync(metricsFile), 'Expected metrics file to exist even with missing usage');
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
assert.strictEqual(row.input_tokens, 0, 'Expected input_tokens to be 0 when missing');
assert.strictEqual(row.output_tokens, 0, 'Expected output_tokens to be 0 when missing');
assert.strictEqual(row.estimated_cost_usd, 0, 'Expected estimated_cost_usd to be 0 when no tokens');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,260 @@
/**
* Tests for worktree project-ID mismatch fix
*
* Validates that detect-project.sh uses -e (not -d) for .git existence
* checks, so that git worktrees (where .git is a file) are detected
* correctly.
*
* Run with: node tests/hooks/detect-project-worktree.test.js
*/
// Skip on Windows — these tests invoke bash scripts directly
if (process.platform === 'win32') {
console.log('Skipping bash-dependent worktree tests on Windows\n');
process.exit(0);
}
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execFileSync, execSync } = require('child_process');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
passed++;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
failed++;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-worktree-test-'));
}
function cleanupDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup errors
}
}
function toBashPath(filePath) {
if (process.platform !== 'win32') {
return filePath;
}
return String(filePath)
.replace(/^([A-Za-z]):/, (_, driveLetter) => `/${driveLetter.toLowerCase()}`)
.replace(/\\/g, '/');
}
function runBash(command, options = {}) {
return execFileSync('bash', ['-lc', command], options).toString().trim();
}
const repoRoot = path.resolve(__dirname, '..', '..');
const detectProjectPath = path.join(
repoRoot,
'skills',
'continuous-learning-v2',
'scripts',
'detect-project.sh'
);
console.log('\n=== Worktree Project-ID Mismatch Tests ===\n');
// ──────────────────────────────────────────────────────
// Group 1: Content checks on detect-project.sh
// ──────────────────────────────────────────────────────
console.log('--- Content checks on detect-project.sh ---');
test('uses -e (not -d) for .git existence check', () => {
const content = fs.readFileSync(detectProjectPath, 'utf8');
assert.ok(
content.includes('[ -e "${project_root}/.git" ]'),
'detect-project.sh should use -e for .git check'
);
assert.ok(
!content.includes('[ -d "${project_root}/.git" ]'),
'detect-project.sh should NOT use -d for .git check'
);
});
test('has command -v git fallback check', () => {
const content = fs.readFileSync(detectProjectPath, 'utf8');
assert.ok(
content.includes('command -v git'),
'detect-project.sh should check for git availability with command -v'
);
});
test('uses git -C for safe directory operations', () => {
const content = fs.readFileSync(detectProjectPath, 'utf8');
assert.ok(
content.includes('git -C'),
'detect-project.sh should use git -C for directory-scoped operations'
);
});
// ──────────────────────────────────────────────────────
// Group 2: Behavior test — -e vs -d
// ──────────────────────────────────────────────────────
console.log('\n--- Behavior test: -e vs -d ---');
const behaviorDir = createTempDir();
test('[ -d ] returns true for .git directory', () => {
const dir = path.join(behaviorDir, 'test-d-dir');
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(path.join(dir, '.git'));
const result = runBash(`[ -d "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
assert.strictEqual(result, 'yes');
});
test('[ -d ] returns false for .git file', () => {
const dir = path.join(behaviorDir, 'test-d-file');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: /some/path\n');
const result = runBash(`[ -d "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
assert.strictEqual(result, 'no');
});
test('[ -e ] returns true for .git directory', () => {
const dir = path.join(behaviorDir, 'test-e-dir');
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(path.join(dir, '.git'));
const result = runBash(`[ -e "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
assert.strictEqual(result, 'yes');
});
test('[ -e ] returns true for .git file', () => {
const dir = path.join(behaviorDir, 'test-e-file');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: /some/path\n');
const result = runBash(`[ -e "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
assert.strictEqual(result, 'yes');
});
test('[ -e ] returns false when .git does not exist', () => {
const dir = path.join(behaviorDir, 'test-e-none');
fs.mkdirSync(dir, { recursive: true });
const result = runBash(`[ -e "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
assert.strictEqual(result, 'no');
});
cleanupDir(behaviorDir);
// ──────────────────────────────────────────────────────
// Group 3: E2E test — detect-project.sh with worktree .git file
// ──────────────────────────────────────────────────────
console.log('\n--- E2E: detect-project.sh with worktree .git file ---');
test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree', () => {
const testDir = createTempDir();
try {
// Create a "main" repo with git init so we have real git structures
const mainRepo = path.join(testDir, 'main-repo');
fs.mkdirSync(mainRepo, { recursive: true });
execSync('git init', { cwd: mainRepo, stdio: 'pipe' });
execSync('git commit --allow-empty -m "init"', {
cwd: mainRepo,
stdio: 'pipe',
env: {
...process.env,
GIT_AUTHOR_NAME: 'Test',
GIT_AUTHOR_EMAIL: 'test@test.com',
GIT_COMMITTER_NAME: 'Test',
GIT_COMMITTER_EMAIL: 'test@test.com'
}
});
// Create a worktree-like directory with .git as a file
const worktreeDir = path.join(testDir, 'my-worktree');
fs.mkdirSync(worktreeDir, { recursive: true });
// Set up the worktree directory structure in the main repo
const worktreesDir = path.join(mainRepo, '.git', 'worktrees', 'my-worktree');
fs.mkdirSync(worktreesDir, { recursive: true });
// Create the gitdir file and commondir in the worktree metadata
const mainGitDir = path.join(mainRepo, '.git');
fs.writeFileSync(
path.join(worktreesDir, 'commondir'),
'../..\n'
);
fs.writeFileSync(
path.join(worktreesDir, 'HEAD'),
fs.readFileSync(path.join(mainGitDir, 'HEAD'), 'utf8')
);
// Write .git file in the worktree directory (this is what git worktree creates)
fs.writeFileSync(
path.join(worktreeDir, '.git'),
`gitdir: ${worktreesDir}\n`
);
// Source detect-project.sh from the worktree directory and capture results
const script = `
export CLAUDE_PROJECT_DIR="${toBashPath(worktreeDir)}"
export HOME="${toBashPath(testDir)}"
source "${toBashPath(detectProjectPath)}"
echo "PROJECT_NAME=\${PROJECT_NAME}"
echo "PROJECT_ID=\${PROJECT_ID}"
`;
const result = execFileSync('bash', ['-lc', script], {
cwd: worktreeDir,
timeout: 10000,
env: {
...process.env,
HOME: toBashPath(testDir),
USERPROFILE: testDir,
CLAUDE_PROJECT_DIR: toBashPath(worktreeDir)
}
}).toString();
const lines = result.trim().split('\n');
const vars = {};
for (const line of lines) {
const match = line.match(/^(PROJECT_NAME|PROJECT_ID)=(.*)$/);
if (match) {
vars[match[1]] = match[2];
}
}
assert.ok(
vars.PROJECT_NAME && vars.PROJECT_NAME.length > 0,
`PROJECT_NAME should be set, got: "${vars.PROJECT_NAME || ''}"`
);
assert.ok(
vars.PROJECT_ID && vars.PROJECT_ID !== 'global',
`PROJECT_ID should not be "global", got: "${vars.PROJECT_ID || ''}"`
);
} finally {
cleanupDir(testDir);
}
});
// ──────────────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────────────
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}\n`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env node
'use strict';
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'doc-file-warning.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(input) {
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: JSON.stringify(input),
timeout: 10000,
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function runTests() {
console.log('\n=== Testing doc-file-warning.js ===\n');
let passed = 0;
let failed = 0;
// 1. Allowed standard doc files - no warning in stderr
const standardFiles = [
'README.md',
'CLAUDE.md',
'AGENTS.md',
'CONTRIBUTING.md',
'CHANGELOG.md',
'LICENSE.md',
'SKILL.md',
'MEMORY.md',
'WORKLOG.md',
];
for (const file of standardFiles) {
(test(`allows standard doc file: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0, `expected exit code 0, got ${code}`);
assert.strictEqual(stderr, '', `expected no warning for ${file}, got: ${stderr}`);
}) ? passed++ : failed++);
}
// 2. Allowed directory paths - no warning
const allowedDirPaths = [
'docs/foo.md',
'docs/guide/setup.md',
'skills/bar.md',
'skills/testing/tdd.md',
'.history/session.md',
'memory/patterns.md',
'.claude/commands/deploy.md',
'.claude/plans/roadmap.md',
'.claude/projects/myproject.md',
];
for (const file of allowedDirPaths) {
(test(`allows directory path: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0, `expected exit code 0, got ${code}`);
assert.strictEqual(stderr, '', `expected no warning for ${file}, got: ${stderr}`);
}) ? passed++ : failed++);
}
// 3. Allowed .plan.md files - no warning
(test('allows .plan.md files', () => {
const { code, stderr } = runScript({ tool_input: { file_path: 'feature.plan.md' } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for .plan.md, got: ${stderr}`);
}) ? passed++ : failed++);
(test('allows nested .plan.md files', () => {
const { code, stderr } = runScript({ tool_input: { file_path: 'src/refactor.plan.md' } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for nested .plan.md, got: ${stderr}`);
}) ? passed++ : failed++);
// 4. Non-md/txt files always pass - no warning
const nonDocFiles = ['foo.js', 'app.py', 'styles.css', 'data.json', 'image.png'];
for (const file of nonDocFiles) {
(test(`allows non-doc file: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for ${file}, got: ${stderr}`);
}) ? passed++ : failed++);
}
// 5. Non-standard doc files - warning in stderr
const nonStandardFiles = ['random-notes.md', 'TODO.md', 'notes.txt', 'scratch.md', 'ideas.txt'];
for (const file of nonStandardFiles) {
(test(`warns on non-standard doc file: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0, 'should still exit 0 (warn only)');
assert.ok(stderr.includes('WARNING'), `expected warning in stderr for ${file}, got: ${stderr}`);
assert.ok(stderr.includes(file), `expected file path in stderr for ${file}`);
}) ? passed++ : failed++);
}
// 6. Invalid/empty input - passes through without error
(test('handles empty object input without error', () => {
const { code, stderr } = runScript({});
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for empty input, got: ${stderr}`);
}) ? passed++ : failed++);
(test('handles missing file_path without error', () => {
const { code, stderr } = runScript({ tool_input: {} });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for missing file_path, got: ${stderr}`);
}) ? passed++ : failed++);
(test('handles empty file_path without error', () => {
const { code, stderr } = runScript({ tool_input: { file_path: '' } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for empty file_path, got: ${stderr}`);
}) ? passed++ : failed++);
// 7. Stdout always contains the original input (pass-through)
(test('passes through input to stdout for allowed file', () => {
const input = { tool_input: { file_path: 'README.md' } };
const { stdout } = runScript(input);
assert.strictEqual(stdout, JSON.stringify(input));
}) ? passed++ : failed++);
(test('passes through input to stdout for warned file', () => {
const input = { tool_input: { file_path: 'random-notes.md' } };
const { stdout } = runScript(input);
assert.strictEqual(stdout, JSON.stringify(input));
}) ? passed++ : failed++);
(test('passes through input to stdout for empty input', () => {
const input = {};
const { stdout } = runScript(input);
assert.strictEqual(stdout, JSON.stringify(input));
}) ? passed++ : failed++);
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,419 @@
/**
* Tests for scripts/hooks/evaluate-session.js
*
* Tests the session evaluation threshold logic, config loading,
* and stdin parsing. Uses temporary JSONL transcript files.
*
* Run with: node tests/hooks/evaluate-session.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js');
// Test helpers
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 createTestDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'eval-session-test-'));
}
function cleanupTestDir(testDir) {
fs.rmSync(testDir, { recursive: true, force: true });
}
/**
* Create a JSONL transcript file with N user messages.
* Each line is a JSON object with `"type":"user"`.
*/
function createTranscript(dir, messageCount) {
const filePath = path.join(dir, 'transcript.jsonl');
const lines = [];
for (let i = 0; i < messageCount; i++) {
lines.push(JSON.stringify({ type: 'user', content: `Message ${i + 1}` }));
// Intersperse assistant messages to be realistic
lines.push(JSON.stringify({ type: 'assistant', content: `Response ${i + 1}` }));
}
fs.writeFileSync(filePath, lines.join('\n') + '\n');
return filePath;
}
/**
* Run evaluate-session.js with stdin providing the transcript_path.
* Uses spawnSync to capture both stdout and stderr regardless of exit code.
* Returns { code, stdout, stderr }.
*/
function runEvaluate(stdinJson) {
const result = spawnSync('node', [evaluateScript], {
encoding: 'utf8',
input: JSON.stringify(stdinJson),
timeout: 10000,
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function runTests() {
console.log('\n=== Testing evaluate-session.js ===\n');
let passed = 0;
let failed = 0;
// Threshold boundary tests (default minSessionLength = 10)
console.log('Threshold boundary (default min=10):');
if (test('skips session with 9 user messages (below threshold)', () => {
const testDir = createTestDir();
const transcript = createTranscript(testDir, 9);
const result = runEvaluate({ transcript_path: transcript });
assert.strictEqual(result.code, 0, 'Should exit 0');
// "too short" message should appear in stderr (log goes to stderr)
assert.ok(
result.stderr.includes('too short') || result.stderr.includes('9 messages'),
'Should indicate session too short'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('evaluates session with exactly 10 user messages (at threshold)', () => {
const testDir = createTestDir();
const transcript = createTranscript(testDir, 10);
const result = runEvaluate({ transcript_path: transcript });
assert.strictEqual(result.code, 0, 'Should exit 0');
// Should NOT say "too short" — should say "evaluate for extractable patterns"
assert.ok(!result.stderr.includes('too short'), 'Should NOT say too short at threshold');
assert.ok(
result.stderr.includes('10 messages') || result.stderr.includes('evaluate'),
'Should indicate evaluation'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('evaluates session with 11 user messages (above threshold)', () => {
const testDir = createTestDir();
const transcript = createTranscript(testDir, 11);
const result = runEvaluate({ transcript_path: transcript });
assert.strictEqual(result.code, 0);
assert.ok(!result.stderr.includes('too short'), 'Should NOT say too short');
assert.ok(result.stderr.includes('evaluate'), 'Should trigger evaluation');
cleanupTestDir(testDir);
})) passed++; else failed++;
// Edge cases
console.log('\nEdge cases:');
if (test('exits 0 with missing transcript_path', () => {
const result = runEvaluate({});
assert.strictEqual(result.code, 0, 'Should exit 0 gracefully');
})) passed++; else failed++;
if (test('exits 0 with non-existent transcript file', () => {
const result = runEvaluate({ transcript_path: '/nonexistent/path/transcript.jsonl' });
assert.strictEqual(result.code, 0, 'Should exit 0 gracefully');
})) passed++; else failed++;
if (test('exits 0 with invalid stdin JSON', () => {
// Pass raw string instead of JSON
const result = spawnSync('node', [evaluateScript], {
encoding: 'utf8',
input: 'not valid json at all',
timeout: 10000,
});
assert.strictEqual(result.status, 0, 'Should exit 0 even on bad stdin');
})) passed++; else failed++;
if (test('skips empty transcript file (0 user messages)', () => {
const testDir = createTestDir();
const filePath = path.join(testDir, 'empty.jsonl');
fs.writeFileSync(filePath, '');
const result = runEvaluate({ transcript_path: filePath });
assert.strictEqual(result.code, 0);
// 0 < 10, so should be "too short"
assert.ok(
result.stderr.includes('too short') || result.stderr.includes('0 messages'),
'Empty transcript should be too short'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('counts only user messages (ignores assistant messages)', () => {
const testDir = createTestDir();
const filePath = path.join(testDir, 'mixed.jsonl');
// 5 user messages + 50 assistant messages — should still be "too short"
const lines = [];
for (let i = 0; i < 5; i++) {
lines.push(JSON.stringify({ type: 'user', content: `msg ${i}` }));
}
for (let i = 0; i < 50; i++) {
lines.push(JSON.stringify({ type: 'assistant', content: `resp ${i}` }));
}
fs.writeFileSync(filePath, lines.join('\n') + '\n');
const result = runEvaluate({ transcript_path: filePath });
assert.strictEqual(result.code, 0);
assert.ok(
result.stderr.includes('too short') || result.stderr.includes('5 messages'),
'Should count only user messages'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
// ── Round 28: config file parsing ──
console.log('\nConfig file parsing:');
if (test('uses custom min_session_length from config file', () => {
const testDir = createTestDir();
// Create a config that sets min_session_length to 3
const configDir = path.join(testDir, 'skills', 'continuous-learning');
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify({
min_session_length: 3
}));
// Create 4 user messages (above threshold of 3, but below default of 10)
const transcript = createTranscript(testDir, 4);
// Run the script from the testDir so it finds config relative to script location
// The config path is: path.join(__dirname, '..', '..', 'skills', 'continuous-learning', 'config.json')
// __dirname = scripts/hooks, so config = repo_root/skills/continuous-learning/config.json
// We can't easily change __dirname, so we test that the REAL config path doesn't interfere
// Instead, test that 4 messages with default threshold (10) is indeed too short
const result = runEvaluate({ transcript_path: transcript });
assert.strictEqual(result.code, 0);
// With default min=10, 4 messages should be too short
assert.ok(
result.stderr.includes('too short') || result.stderr.includes('4 messages'),
'With default config, 4 messages should be too short'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('handles transcript with only assistant messages (0 user match)', () => {
const testDir = createTestDir();
const filePath = path.join(testDir, 'assistant-only.jsonl');
const lines = [];
for (let i = 0; i < 20; i++) {
lines.push(JSON.stringify({ type: 'assistant', content: `response ${i}` }));
}
fs.writeFileSync(filePath, lines.join('\n') + '\n');
const result = runEvaluate({ transcript_path: filePath });
assert.strictEqual(result.code, 0);
// countInFile looks for /"type"\s*:\s*"user"/ — no matches
assert.ok(
result.stderr.includes('too short') || result.stderr.includes('0 messages'),
'Should report too short with 0 user messages'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('handles transcript with malformed JSON lines (still counts valid ones)', () => {
const testDir = createTestDir();
const filePath = path.join(testDir, 'mixed.jsonl');
// 12 valid user lines + 5 invalid lines
const lines = [];
for (let i = 0; i < 12; i++) {
lines.push(JSON.stringify({ type: 'user', content: `msg ${i}` }));
}
for (let i = 0; i < 5; i++) {
lines.push('not valid json {{{');
}
fs.writeFileSync(filePath, lines.join('\n') + '\n');
const result = runEvaluate({ transcript_path: filePath });
assert.strictEqual(result.code, 0);
// countInFile uses regex matching, not JSON parsing — counts all lines matching /"type"\s*:\s*"user"/
// 12 user messages >= 10 threshold → should evaluate
assert.ok(
result.stderr.includes('evaluate') && result.stderr.includes('12 messages'),
'Should evaluate session with 12 valid user messages'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('handles empty stdin (no input) gracefully', () => {
const result = spawnSync('node', [evaluateScript], {
encoding: 'utf8',
input: '',
timeout: 10000,
});
// Empty stdin → JSON.parse('') throws → fallback to env var (unset) → null → exit 0
assert.strictEqual(result.status, 0, 'Should exit 0 on empty stdin');
})) passed++; else failed++;
// ── Round 53: env var fallback path ──
console.log('\nRound 53: CLAUDE_TRANSCRIPT_PATH fallback:');
if (test('falls back to CLAUDE_TRANSCRIPT_PATH env var when stdin is invalid JSON', () => {
const testDir = createTestDir();
const transcript = createTranscript(testDir, 15);
const result = spawnSync('node', [evaluateScript], {
encoding: 'utf8',
input: 'invalid json {{{',
timeout: 10000,
env: { ...process.env, CLAUDE_TRANSCRIPT_PATH: transcript }
});
assert.strictEqual(result.status, 0, 'Should exit 0');
assert.ok(
result.stderr.includes('15 messages'),
'Should evaluate using env var fallback path'
);
assert.ok(
result.stderr.includes('evaluate'),
'Should indicate session evaluation'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
// ── Round 65: regex whitespace tolerance in countInFile ──
console.log('\nRound 65: regex whitespace tolerance around colon:');
if (test('counts user messages when JSON has spaces around colon ("type" : "user")', () => {
const testDir = createTestDir();
const filePath = path.join(testDir, 'spaced.jsonl');
// Manually write JSON with spaces around the colon — NOT JSON.stringify
// The regex /"type"\s*:\s*"user"/g should match these
const lines = [];
for (let i = 0; i < 12; i++) {
lines.push(`{"type" : "user", "content": "msg ${i}"}`);
lines.push(`{"type" : "assistant", "content": "resp ${i}"}`);
}
fs.writeFileSync(filePath, lines.join('\n') + '\n');
const result = runEvaluate({ transcript_path: filePath });
assert.strictEqual(result.code, 0);
// 12 user messages >= 10 threshold → should evaluate (not "too short")
assert.ok(!result.stderr.includes('too short'),
'Should NOT say too short for 12 spaced-colon user messages');
assert.ok(
result.stderr.includes('12 messages') || result.stderr.includes('evaluate'),
`Should evaluate session with spaced-colon JSON. Got stderr: ${result.stderr}`
);
cleanupTestDir(testDir);
})) passed++; else failed++;
// ── Round 85: config file parse error (corrupt JSON) ──
console.log('\nRound 85: config parse error catch block:');
if (test('falls back to defaults when config file contains invalid JSON', () => {
// The evaluate-session.js script reads config from:
// path.join(__dirname, '..', '..', 'skills', 'continuous-learning', 'config.json')
// where __dirname = scripts/hooks/ → config = repo_root/skills/continuous-learning/config.json
const configPath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning', 'config.json');
let originalContent = null;
try {
originalContent = fs.readFileSync(configPath, 'utf8');
} catch {
// Config file may not exist — that's fine
}
try {
// Write corrupt JSON to the config file
fs.writeFileSync(configPath, 'NOT VALID JSON {{{ corrupt data !!!', 'utf8');
// Create a transcript with 12 user messages (above default threshold of 10)
const testDir = createTestDir();
const transcript = createTranscript(testDir, 12);
const result = runEvaluate({ transcript_path: transcript });
assert.strictEqual(result.code, 0, 'Should exit 0 despite corrupt config');
// With corrupt config, defaults apply: min_session_length = 10
// 12 >= 10 → should evaluate (not "too short")
assert.ok(!result.stderr.includes('too short'),
`Should NOT say too short — corrupt config falls back to default min=10. Got: ${result.stderr}`);
assert.ok(
result.stderr.includes('12 messages') || result.stderr.includes('evaluate'),
`Should evaluate with 12 messages using default threshold. Got: ${result.stderr}`
);
// The catch block logs "Failed to parse config" — verify that log message
assert.ok(result.stderr.includes('Failed to parse config'),
`Should log config parse error. Got: ${result.stderr}`);
cleanupTestDir(testDir);
} finally {
// Restore original config file
if (originalContent !== null) {
fs.writeFileSync(configPath, originalContent, 'utf8');
} else {
// Config didn't exist before — remove the corrupt one we created
try { fs.unlinkSync(configPath); } catch { /* best-effort */ }
}
}
})) passed++; else failed++;
// ── Round 86: config learned_skills_path override with ~ expansion ──
console.log('\nRound 86: config learned_skills_path override:');
if (test('uses learned_skills_path from config with ~ expansion', () => {
// evaluate-session.js lines 69-72:
// if (config.learned_skills_path) {
// learnedSkillsPath = config.learned_skills_path.replace(/^~/, require('os').homedir());
// }
// This branch was never tested — only the parse error (Round 85) and default path.
const configPath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning', 'config.json');
let originalContent = null;
try {
originalContent = fs.readFileSync(configPath, 'utf8');
} catch {
// Config file may not exist
}
try {
// Write config with a custom learned_skills_path using ~ prefix
fs.writeFileSync(configPath, JSON.stringify({
min_session_length: 10,
learned_skills_path: '~/custom-learned-skills-dir'
}));
// Create a transcript with 12 user messages (above threshold)
const testDir = createTestDir();
const transcript = createTranscript(testDir, 12);
const result = runEvaluate({ transcript_path: transcript });
assert.strictEqual(result.code, 0, 'Should exit 0');
// The script logs "Save learned skills to: <path>" where <path> should
// be the expanded home directory, NOT the literal "~"
assert.ok(!result.stderr.includes('~/custom-learned-skills-dir'),
'Should NOT contain literal ~ in output (should be expanded)');
assert.ok(result.stderr.includes('custom-learned-skills-dir'),
`Should reference the custom learned skills dir. Got: ${result.stderr}`);
// The ~ should have been replaced with os.homedir()
assert.ok(result.stderr.includes(os.homedir()),
`Should contain expanded home directory. Got: ${result.stderr}`);
cleanupTestDir(testDir);
} finally {
// Restore original config file
if (originalContent !== null) {
fs.writeFileSync(configPath, originalContent, 'utf8');
} else {
try { fs.unlinkSync(configPath); } catch { /* best-effort */ }
}
}
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,360 @@
/**
* Tests for governance event capture hook.
*/
const assert = require('assert');
const {
detectSecrets,
detectApprovalRequired,
detectSensitivePath,
analyzeForGovernanceEvents,
run,
} = require('../../scripts/hooks/governance-capture');
async function test(name, fn) {
try {
await fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function runTests() {
console.log('\n=== Testing governance-capture ===\n');
let passed = 0;
let failed = 0;
// ── detectSecrets ──────────────────────────────────────────
if (await test('detectSecrets finds AWS access keys', async () => {
const findings = detectSecrets('my key is AKIAIOSFODNN7EXAMPLE');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'aws_key'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets finds generic secrets', async () => {
const findings = detectSecrets('api_key = "sk-proj-abcdefghij1234567890"');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'generic_secret'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets finds private keys', async () => {
const findings = detectSecrets('-----BEGIN RSA PRIVATE KEY-----\nMIIE...');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'private_key'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets finds GitHub tokens', async () => {
const findings = detectSecrets('token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'github_token'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets returns empty array for clean text', async () => {
const findings = detectSecrets('This is a normal log message with no secrets.');
assert.strictEqual(findings.length, 0);
})) passed += 1; else failed += 1;
if (await test('detectSecrets handles null and undefined', async () => {
assert.deepStrictEqual(detectSecrets(null), []);
assert.deepStrictEqual(detectSecrets(undefined), []);
assert.deepStrictEqual(detectSecrets(''), []);
})) passed += 1; else failed += 1;
// ── detectApprovalRequired ─────────────────────────────────
if (await test('detectApprovalRequired flags force push', async () => {
const findings = detectApprovalRequired('git push origin main --force');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired flags hard reset', async () => {
const findings = detectApprovalRequired('git reset --hard HEAD~3');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired flags rm -rf', async () => {
const findings = detectApprovalRequired('rm -rf /tmp/important');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired flags DROP TABLE', async () => {
const findings = detectApprovalRequired('DROP TABLE users');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired allows safe commands', async () => {
const findings = detectApprovalRequired('git status');
assert.strictEqual(findings.length, 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired handles null', async () => {
assert.deepStrictEqual(detectApprovalRequired(null), []);
assert.deepStrictEqual(detectApprovalRequired(''), []);
})) passed += 1; else failed += 1;
// ── detectSensitivePath ────────────────────────────────────
if (await test('detectSensitivePath identifies .env files', async () => {
assert.ok(detectSensitivePath('.env'));
assert.ok(detectSensitivePath('.env.local'));
assert.ok(detectSensitivePath('/project/.env.production'));
})) passed += 1; else failed += 1;
if (await test('detectSensitivePath identifies credential files', async () => {
assert.ok(detectSensitivePath('credentials.json'));
assert.ok(detectSensitivePath('/home/user/.ssh/id_rsa'));
assert.ok(detectSensitivePath('server.key'));
assert.ok(detectSensitivePath('cert.pem'));
})) passed += 1; else failed += 1;
if (await test('detectSensitivePath returns false for normal files', async () => {
assert.ok(!detectSensitivePath('index.js'));
assert.ok(!detectSensitivePath('README.md'));
assert.ok(!detectSensitivePath('package.json'));
})) passed += 1; else failed += 1;
if (await test('detectSensitivePath handles null', async () => {
assert.ok(!detectSensitivePath(null));
assert.ok(!detectSensitivePath(''));
})) passed += 1; else failed += 1;
// ── analyzeForGovernanceEvents ─────────────────────────────
if (await test('analyzeForGovernanceEvents detects secrets in tool input', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: {
file_path: '/tmp/config.js',
content: 'const key = "AKIAIOSFODNN7EXAMPLE";',
},
});
assert.ok(events.length > 0);
const secretEvent = events.find(e => e.eventType === 'secret_detected');
assert.ok(secretEvent);
assert.strictEqual(secretEvent.payload.severity, 'critical');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents detects approval-required commands', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: {
command: 'git push origin main --force',
},
});
assert.ok(events.length > 0);
const approvalEvent = events.find(e => e.eventType === 'approval_requested');
assert.ok(approvalEvent);
assert.strictEqual(approvalEvent.payload.severity, 'high');
})) passed += 1; else failed += 1;
if (await test('approval events fingerprint commands instead of storing raw command text', async () => {
const command = 'git push origin main --force';
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: { command },
});
const approvalEvent = events.find(e => e.eventType === 'approval_requested');
assert.ok(approvalEvent);
assert.strictEqual(approvalEvent.payload.commandName, 'git');
assert.ok(/^[a-f0-9]{12}$/.test(approvalEvent.payload.commandFingerprint), 'Expected short command fingerprint');
assert.ok(!Object.prototype.hasOwnProperty.call(approvalEvent.payload, 'command'), 'Should not store raw command text');
})) passed += 1; else failed += 1;
if (await test('security findings fingerprint elevated commands instead of storing raw command text', async () => {
const command = 'sudo chmod 600 ~/.ssh/id_rsa';
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: { command },
}, {
hookPhase: 'post',
});
const securityEvent = events.find(e => e.eventType === 'security_finding');
assert.ok(securityEvent);
assert.strictEqual(securityEvent.payload.commandName, 'sudo');
assert.ok(/^[a-f0-9]{12}$/.test(securityEvent.payload.commandFingerprint), 'Expected short command fingerprint');
assert.ok(!Object.prototype.hasOwnProperty.call(securityEvent.payload, 'command'), 'Should not store raw command text');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents detects sensitive file access', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Edit',
tool_input: {
file_path: '/project/.env.production',
old_string: 'DB_URL=old',
new_string: 'DB_URL=new',
},
});
assert.ok(events.length > 0);
const policyEvent = events.find(e => e.eventType === 'policy_violation');
assert.ok(policyEvent);
assert.strictEqual(policyEvent.payload.reason, 'sensitive_file_access');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents detects elevated privilege commands', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: { command: 'sudo rm -rf /etc/something' },
}, {
hookPhase: 'post',
});
const securityEvent = events.find(e => e.eventType === 'security_finding');
assert.ok(securityEvent);
assert.strictEqual(securityEvent.payload.reason, 'elevated_privilege_command');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents returns empty for clean inputs', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Read',
tool_input: { file_path: '/project/src/index.js' },
});
assert.strictEqual(events.length, 0);
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents populates session ID from context', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: {
file_path: '/project/.env',
content: 'DB_URL=test',
},
}, {
sessionId: 'test-session-123',
});
assert.ok(events.length > 0);
assert.strictEqual(events[0].sessionId, 'test-session-123');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents generates unique event IDs', async () => {
const events1 = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: { file_path: '.env', content: '' },
});
const events2 = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: { file_path: '.env.local', content: '' },
});
if (events1.length > 0 && events2.length > 0) {
assert.notStrictEqual(events1[0].id, events2[0].id);
}
})) passed += 1; else failed += 1;
// ── run() function ─────────────────────────────────────────
if (await test('run() passes through input when feature flag is off', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
delete process.env.ECC_GOVERNANCE_CAPTURE;
try {
const input = JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'git push --force' } });
const result = run(input);
assert.strictEqual(result, input);
} finally {
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
}
}
})) passed += 1; else failed += 1;
if (await test('run() passes through input when feature flag is on', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
process.env.ECC_GOVERNANCE_CAPTURE = '1';
try {
const input = JSON.stringify({ tool_name: 'Read', tool_input: { file_path: 'index.js' } });
const result = run(input);
assert.strictEqual(result, input);
} finally {
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
} else {
delete process.env.ECC_GOVERNANCE_CAPTURE;
}
}
})) passed += 1; else failed += 1;
if (await test('run() handles invalid JSON gracefully', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
process.env.ECC_GOVERNANCE_CAPTURE = '1';
try {
const result = run('not valid json');
assert.strictEqual(result, 'not valid json');
} finally {
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
} else {
delete process.env.ECC_GOVERNANCE_CAPTURE;
}
}
})) passed += 1; else failed += 1;
if (await test('run() emits hook_input_truncated event without logging raw command text', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
const originalHookEvent = process.env.CLAUDE_HOOK_EVENT_NAME;
const originalWrite = process.stderr.write;
const stderr = [];
process.env.ECC_GOVERNANCE_CAPTURE = '1';
process.env.CLAUDE_HOOK_EVENT_NAME = 'PreToolUse';
process.stderr.write = (chunk, encoding, callback) => {
stderr.push(String(chunk));
if (typeof encoding === 'function') encoding();
if (typeof callback === 'function') callback();
return true;
};
try {
const input = JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'rm -rf /tmp/important' } });
const result = run(input, { truncated: true, maxStdin: 1024 });
assert.strictEqual(result, input);
} finally {
process.stderr.write = originalWrite;
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
} else {
delete process.env.ECC_GOVERNANCE_CAPTURE;
}
if (originalHookEvent !== undefined) {
process.env.CLAUDE_HOOK_EVENT_NAME = originalHookEvent;
} else {
delete process.env.CLAUDE_HOOK_EVENT_NAME;
}
}
const combined = stderr.join('');
assert.ok(combined.includes('"eventType":"hook_input_truncated"'), 'Should emit truncation event');
assert.ok(combined.includes('"sizeLimitBytes":1024'), 'Should record the truncation limit');
assert.ok(!combined.includes('rm -rf /tmp/important'), 'Should not leak raw command text to governance logs');
})) passed += 1; else failed += 1;
if (await test('run() can detect multiple event types in one input', async () => {
// Bash command with force push AND secret in command
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: {
command: 'API_KEY="AKIAIOSFODNN7EXAMPLE" git push --force',
},
});
const eventTypes = events.map(e => e.eventType);
assert.ok(eventTypes.includes('secret_detected'));
assert.ok(eventTypes.includes('approval_requested'));
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,397 @@
/**
* Tests for scripts/lib/hook-flags.js
*
* Run with: node tests/hooks/hook-flags.test.js
*/
const assert = require('assert');
// Import the module
const {
VALID_PROFILES,
normalizeId,
getHookProfile,
getDisabledHookIds,
parseProfiles,
isHookEnabled,
} = require('../../scripts/lib/hook-flags');
// Test helper
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
// Helper to save and restore env vars
function withEnv(vars, fn) {
const saved = {};
for (const key of Object.keys(vars)) {
saved[key] = process.env[key];
if (vars[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = vars[key];
}
}
try {
fn();
} finally {
for (const key of Object.keys(saved)) {
if (saved[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = saved[key];
}
}
}
}
// Test suite
function runTests() {
console.log('\n=== Testing hook-flags.js ===\n');
let passed = 0;
let failed = 0;
// VALID_PROFILES tests
console.log('VALID_PROFILES:');
if (test('is a Set', () => {
assert.ok(VALID_PROFILES instanceof Set);
})) passed++; else failed++;
if (test('contains minimal, standard, strict', () => {
assert.ok(VALID_PROFILES.has('minimal'));
assert.ok(VALID_PROFILES.has('standard'));
assert.ok(VALID_PROFILES.has('strict'));
})) passed++; else failed++;
if (test('contains exactly 3 profiles', () => {
assert.strictEqual(VALID_PROFILES.size, 3);
})) passed++; else failed++;
// normalizeId tests
console.log('\nnormalizeId:');
if (test('returns empty string for null', () => {
assert.strictEqual(normalizeId(null), '');
})) passed++; else failed++;
if (test('returns empty string for undefined', () => {
assert.strictEqual(normalizeId(undefined), '');
})) passed++; else failed++;
if (test('returns empty string for empty string', () => {
assert.strictEqual(normalizeId(''), '');
})) passed++; else failed++;
if (test('trims whitespace', () => {
assert.strictEqual(normalizeId(' hello '), 'hello');
})) passed++; else failed++;
if (test('converts to lowercase', () => {
assert.strictEqual(normalizeId('MyHook'), 'myhook');
})) passed++; else failed++;
if (test('handles mixed case with whitespace', () => {
assert.strictEqual(normalizeId(' My-Hook-ID '), 'my-hook-id');
})) passed++; else failed++;
if (test('converts numbers to string', () => {
assert.strictEqual(normalizeId(123), '123');
})) passed++; else failed++;
if (test('returns empty string for whitespace-only input', () => {
assert.strictEqual(normalizeId(' '), '');
})) passed++; else failed++;
// getHookProfile tests
console.log('\ngetHookProfile:');
if (test('defaults to standard when env var not set', () => {
withEnv({ ECC_HOOK_PROFILE: undefined }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
if (test('returns minimal when set to minimal', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal' }, () => {
assert.strictEqual(getHookProfile(), 'minimal');
});
})) passed++; else failed++;
if (test('returns standard when set to standard', () => {
withEnv({ ECC_HOOK_PROFILE: 'standard' }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
if (test('returns strict when set to strict', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict' }, () => {
assert.strictEqual(getHookProfile(), 'strict');
});
})) passed++; else failed++;
if (test('is case-insensitive', () => {
withEnv({ ECC_HOOK_PROFILE: 'STRICT' }, () => {
assert.strictEqual(getHookProfile(), 'strict');
});
})) passed++; else failed++;
if (test('trims whitespace from env var', () => {
withEnv({ ECC_HOOK_PROFILE: ' minimal ' }, () => {
assert.strictEqual(getHookProfile(), 'minimal');
});
})) passed++; else failed++;
if (test('defaults to standard for invalid value', () => {
withEnv({ ECC_HOOK_PROFILE: 'invalid' }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
if (test('defaults to standard for empty string', () => {
withEnv({ ECC_HOOK_PROFILE: '' }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
// getDisabledHookIds tests
console.log('\ngetDisabledHookIds:');
if (test('returns empty Set when env var not set', () => {
withEnv({ ECC_DISABLED_HOOKS: undefined }, () => {
const result = getDisabledHookIds();
assert.ok(result instanceof Set);
assert.strictEqual(result.size, 0);
});
})) passed++; else failed++;
if (test('returns empty Set for empty string', () => {
withEnv({ ECC_DISABLED_HOOKS: '' }, () => {
assert.strictEqual(getDisabledHookIds().size, 0);
});
})) passed++; else failed++;
if (test('returns empty Set for whitespace-only string', () => {
withEnv({ ECC_DISABLED_HOOKS: ' ' }, () => {
assert.strictEqual(getDisabledHookIds().size, 0);
});
})) passed++; else failed++;
if (test('parses single hook id', () => {
withEnv({ ECC_DISABLED_HOOKS: 'my-hook' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 1);
assert.ok(result.has('my-hook'));
});
})) passed++; else failed++;
if (test('parses multiple comma-separated hook ids', () => {
withEnv({ ECC_DISABLED_HOOKS: 'hook-a,hook-b,hook-c' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 3);
assert.ok(result.has('hook-a'));
assert.ok(result.has('hook-b'));
assert.ok(result.has('hook-c'));
});
})) passed++; else failed++;
if (test('trims whitespace around hook ids', () => {
withEnv({ ECC_DISABLED_HOOKS: ' hook-a , hook-b ' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 2);
assert.ok(result.has('hook-a'));
assert.ok(result.has('hook-b'));
});
})) passed++; else failed++;
if (test('normalizes hook ids to lowercase', () => {
withEnv({ ECC_DISABLED_HOOKS: 'MyHook,ANOTHER' }, () => {
const result = getDisabledHookIds();
assert.ok(result.has('myhook'));
assert.ok(result.has('another'));
});
})) passed++; else failed++;
if (test('filters out empty entries from trailing commas', () => {
withEnv({ ECC_DISABLED_HOOKS: 'hook-a,,hook-b,' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 2);
assert.ok(result.has('hook-a'));
assert.ok(result.has('hook-b'));
});
})) passed++; else failed++;
// parseProfiles tests
console.log('\nparseProfiles:');
if (test('returns fallback for null input', () => {
const result = parseProfiles(null);
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('returns fallback for undefined input', () => {
const result = parseProfiles(undefined);
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('uses custom fallback when provided', () => {
const result = parseProfiles(null, ['minimal']);
assert.deepStrictEqual(result, ['minimal']);
})) passed++; else failed++;
if (test('parses comma-separated string', () => {
const result = parseProfiles('minimal,strict');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('parses single string value', () => {
const result = parseProfiles('strict');
assert.deepStrictEqual(result, ['strict']);
})) passed++; else failed++;
if (test('parses array of profiles', () => {
const result = parseProfiles(['minimal', 'standard']);
assert.deepStrictEqual(result, ['minimal', 'standard']);
})) passed++; else failed++;
if (test('filters invalid profiles from string', () => {
const result = parseProfiles('minimal,invalid,strict');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('filters invalid profiles from array', () => {
const result = parseProfiles(['minimal', 'bogus', 'strict']);
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('returns fallback when all string values are invalid', () => {
const result = parseProfiles('invalid,bogus');
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('returns fallback when all array values are invalid', () => {
const result = parseProfiles(['invalid', 'bogus']);
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('is case-insensitive for string input', () => {
const result = parseProfiles('MINIMAL,STRICT');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('is case-insensitive for array input', () => {
const result = parseProfiles(['MINIMAL', 'STRICT']);
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('trims whitespace in string input', () => {
const result = parseProfiles(' minimal , strict ');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('handles null values in array', () => {
const result = parseProfiles([null, 'strict']);
assert.deepStrictEqual(result, ['strict']);
})) passed++; else failed++;
// isHookEnabled tests
console.log('\nisHookEnabled:');
if (test('returns true by default for a hook (standard profile)', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('returns true for empty hookId', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled(''), true);
});
})) passed++; else failed++;
if (test('returns true for null hookId', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled(null), true);
});
})) passed++; else failed++;
if (test('returns false when hook is in disabled list', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: 'my-hook' }, () => {
assert.strictEqual(isHookEnabled('my-hook'), false);
});
})) passed++; else failed++;
if (test('disabled check is case-insensitive', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: 'MY-HOOK' }, () => {
assert.strictEqual(isHookEnabled('my-hook'), false);
});
})) passed++; else failed++;
if (test('returns true when hook is not in disabled list', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: 'other-hook' }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('returns false when current profile is not in allowed profiles', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'strict' }), false);
});
})) passed++; else failed++;
if (test('returns true when current profile is in allowed profiles', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'standard,strict' }), true);
});
})) passed++; else failed++;
if (test('returns true when current profile matches single allowed profile', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'minimal' }), true);
});
})) passed++; else failed++;
if (test('disabled hooks take precedence over profile match', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict', ECC_DISABLED_HOOKS: 'my-hook' }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'strict' }), false);
});
})) passed++; else failed++;
if (test('uses default profiles (standard, strict) when none specified', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), false);
});
})) passed++; else failed++;
if (test('allows standard profile by default', () => {
withEnv({ ECC_HOOK_PROFILE: 'standard', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('allows strict profile by default', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('accepts array profiles option', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: ['minimal', 'standard'] }), true);
});
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,298 @@
/**
* Tests for scripts/hooks/mcp-health-check.js
*
* Run with: node tests/hooks/mcp-health-check.test.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', 'hooks', 'mcp-health-check.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
async function asyncTest(name, fn) {
try {
await fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-mcp-health-'));
}
function cleanupTempDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeConfig(configPath, body) {
fs.writeFileSync(configPath, JSON.stringify(body, null, 2));
}
function readState(statePath) {
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
}
function createCommandConfig(scriptPath) {
return {
command: process.execPath,
args: [scriptPath]
};
}
function runHook(input, env = {}) {
const result = spawnSync('node', [script], {
input: JSON.stringify(input),
encoding: 'utf8',
env: {
...process.env,
ECC_HOOK_PROFILE: 'standard',
...env
},
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe']
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
function runRawHook(rawInput, env = {}) {
const result = spawnSync('node', [script], {
input: rawInput,
encoding: 'utf8',
env: {
...process.env,
ECC_HOOK_PROFILE: 'standard',
...env
},
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe']
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
async function runTests() {
console.log('\n=== Testing mcp-health-check.js ===\n');
let passed = 0;
let failed = 0;
if (test('passes through non-MCP tools untouched', () => {
const result = runHook(
{ tool_name: 'Read', tool_input: { file_path: 'README.md' } },
{ CLAUDE_HOOK_EVENT_NAME: 'PreToolUse' }
);
assert.strictEqual(result.code, 0, 'Expected non-MCP tool to pass through');
assert.strictEqual(result.stderr, '', 'Expected no stderr for non-MCP tool');
})) passed++; else failed++;
if (test('blocks truncated MCP hook input by default', () => {
const rawInput = JSON.stringify({ tool_name: 'mcp__flaky__search', tool_input: {} });
const result = runRawHook(rawInput, {
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
ECC_HOOK_INPUT_TRUNCATED: '1',
ECC_HOOK_INPUT_MAX_BYTES: '512'
});
assert.strictEqual(result.code, 2, 'Expected truncated MCP input to block by default');
assert.strictEqual(result.stdout, rawInput, 'Expected raw input passthrough on stdout');
assert.ok(result.stderr.includes('Hook input exceeded 512 bytes'), `Expected size warning, got: ${result.stderr}`);
assert.ok(/blocking search/i.test(result.stderr), `Expected blocking message, got: ${result.stderr}`);
})) passed++; else failed++;
if (await asyncTest('marks healthy command MCP servers and allows the tool call', async () => {
const tempDir = createTempDir();
const configPath = path.join(tempDir, 'claude.json');
const statePath = path.join(tempDir, 'mcp-health.json');
const serverScript = path.join(tempDir, 'healthy-server.js');
try {
fs.writeFileSync(serverScript, "setInterval(() => {}, 1000);\n");
writeConfig(configPath, {
mcpServers: {
mock: createCommandConfig(serverScript)
}
});
const input = { tool_name: 'mcp__mock__list_items', tool_input: {} };
const result = runHook(input, {
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
ECC_MCP_CONFIG_PATH: configPath,
ECC_MCP_HEALTH_STATE_PATH: statePath,
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
});
assert.strictEqual(result.code, 0, `Expected healthy server to pass, got ${result.code}`);
assert.strictEqual(result.stdout.trim(), JSON.stringify(input), 'Expected original JSON on stdout');
const state = readState(statePath);
assert.strictEqual(state.servers.mock.status, 'healthy', 'Expected mock server to be marked healthy');
} finally {
cleanupTempDir(tempDir);
}
})) passed++; else failed++;
if (await asyncTest('blocks unhealthy command MCP servers and records backoff state', async () => {
const tempDir = createTempDir();
const configPath = path.join(tempDir, 'claude.json');
const statePath = path.join(tempDir, 'mcp-health.json');
const serverScript = path.join(tempDir, 'unhealthy-server.js');
try {
fs.writeFileSync(serverScript, "process.exit(1);\n");
writeConfig(configPath, {
mcpServers: {
flaky: createCommandConfig(serverScript)
}
});
const result = runHook(
{ tool_name: 'mcp__flaky__search', tool_input: {} },
{
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
ECC_MCP_CONFIG_PATH: configPath,
ECC_MCP_HEALTH_STATE_PATH: statePath,
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
}
);
assert.strictEqual(result.code, 2, 'Expected unhealthy server to block the MCP tool');
assert.ok(result.stderr.includes('Blocking search'), `Expected blocking message, got: ${result.stderr}`);
const state = readState(statePath);
assert.strictEqual(state.servers.flaky.status, 'unhealthy', 'Expected flaky server to be marked unhealthy');
assert.ok(state.servers.flaky.nextRetryAt > state.servers.flaky.checkedAt, 'Expected retry backoff to be recorded');
} finally {
cleanupTempDir(tempDir);
}
})) passed++; else failed++;
if (await asyncTest('fail-open mode warns but does not block unhealthy MCP servers', async () => {
const tempDir = createTempDir();
const configPath = path.join(tempDir, 'claude.json');
const statePath = path.join(tempDir, 'mcp-health.json');
const serverScript = path.join(tempDir, 'relaxed-server.js');
try {
fs.writeFileSync(serverScript, "process.exit(1);\n");
writeConfig(configPath, {
mcpServers: {
relaxed: createCommandConfig(serverScript)
}
});
const result = runHook(
{ tool_name: 'mcp__relaxed__list', tool_input: {} },
{
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
ECC_MCP_CONFIG_PATH: configPath,
ECC_MCP_HEALTH_STATE_PATH: statePath,
ECC_MCP_HEALTH_FAIL_OPEN: '1',
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
}
);
assert.strictEqual(result.code, 0, 'Expected fail-open mode to allow execution');
assert.ok(result.stderr.includes('Blocking list') || result.stderr.includes('fall back'), 'Expected warning output in fail-open mode');
} finally {
cleanupTempDir(tempDir);
}
})) passed++; else failed++;
if (await asyncTest('post-failure reconnect command restores server health when a reprobe succeeds', async () => {
const tempDir = createTempDir();
const configPath = path.join(tempDir, 'claude.json');
const statePath = path.join(tempDir, 'mcp-health.json');
const switchFile = path.join(tempDir, 'server-mode.txt');
const reconnectFile = path.join(tempDir, 'reconnected.txt');
const probeScript = path.join(tempDir, 'probe-server.js');
fs.writeFileSync(switchFile, 'down');
fs.writeFileSync(
probeScript,
[
"const fs = require('fs');",
`const mode = fs.readFileSync(${JSON.stringify(switchFile)}, 'utf8').trim();`,
"if (mode === 'up') { setInterval(() => {}, 1000); } else { console.error('401 Unauthorized'); process.exit(1); }"
].join('\n')
);
const reconnectScript = path.join(tempDir, 'reconnect.js');
fs.writeFileSync(
reconnectScript,
[
"const fs = require('fs');",
`fs.writeFileSync(${JSON.stringify(switchFile)}, 'up');`,
`fs.writeFileSync(${JSON.stringify(reconnectFile)}, 'done');`
].join('\n')
);
try {
writeConfig(configPath, {
mcpServers: {
authy: createCommandConfig(probeScript)
}
});
const result = runHook(
{
tool_name: 'mcp__authy__messages',
tool_input: {},
error: '401 Unauthorized'
},
{
CLAUDE_HOOK_EVENT_NAME: 'PostToolUseFailure',
ECC_MCP_CONFIG_PATH: configPath,
ECC_MCP_HEALTH_STATE_PATH: statePath,
ECC_MCP_RECONNECT_COMMAND: `node ${JSON.stringify(reconnectScript)}`,
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
}
);
assert.strictEqual(result.code, 0, 'Expected failure hook to remain non-blocking');
assert.ok(result.stderr.includes('reported 401'), `Expected reconnect log, got: ${result.stderr}`);
assert.ok(result.stderr.includes('connection restored'), `Expected restored log, got: ${result.stderr}`);
assert.ok(fs.existsSync(reconnectFile), 'Expected reconnect command to run');
const state = readState(statePath);
assert.strictEqual(state.servers.authy.status, 'healthy', 'Expected authy server to be restored after reconnect');
} finally {
cleanupTempDir(tempDir);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests().catch(error => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,363 @@
/**
* Tests for observer memory explosion fix (#521)
*
* Validates three fixes:
* 1. SIGUSR1 throttling in observe.sh (signal counter)
* 2. Tail-based sampling in observer-loop.sh (not loading entire file)
* 3. Re-entrancy guard + cooldown in observer-loop.sh on_usr1()
*
* Run with: node tests/hooks/observer-memory.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
passed++;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
failed++;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observer-test-'));
}
function cleanupDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup errors
}
}
const repoRoot = path.resolve(__dirname, '..', '..');
const observeShPath = path.join(repoRoot, 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
const observerLoopPath = path.join(repoRoot, 'skills', 'continuous-learning-v2', 'agents', 'observer-loop.sh');
console.log('\n=== Observer Memory Fix Tests (#521) ===\n');
// ──────────────────────────────────────────────────────
// Test group 1: observe.sh SIGUSR1 throttling
// ──────────────────────────────────────────────────────
console.log('--- observe.sh signal throttling ---');
test('observe.sh contains SIGNAL_EVERY_N throttle variable', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(content.includes('SIGNAL_EVERY_N'), 'observe.sh should define SIGNAL_EVERY_N for throttling');
});
test('observe.sh uses a counter file instead of signaling every call', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(content.includes('.observer-signal-counter'), 'observe.sh should use a signal counter file');
});
test('observe.sh only signals when counter reaches threshold', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(content.includes('should_signal=0'), 'observe.sh should default should_signal to 0');
assert.ok(content.includes('should_signal=1'), 'observe.sh should set should_signal=1 when threshold reached');
assert.ok(content.includes('if [ "$should_signal" -eq 1 ]'), 'observe.sh should gate kill -USR1 behind should_signal check');
});
test('observe.sh default throttle is 20 observations per signal', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(content.includes('ECC_OBSERVER_SIGNAL_EVERY_N:-20'), 'Default signal frequency should be every 20 observations');
});
// ──────────────────────────────────────────────────────
// Test group 2: observer-loop.sh re-entrancy guard
// ──────────────────────────────────────────────────────
console.log('\n--- observer-loop.sh re-entrancy guard ---');
test('observer-loop.sh defines ANALYZING guard variable', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('ANALYZING=0'), 'observer-loop.sh should initialize ANALYZING=0');
});
test('on_usr1 checks ANALYZING before starting analysis', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('if [ "$ANALYZING" -eq 1 ]'), 'on_usr1 should check ANALYZING flag');
assert.ok(content.includes('Analysis already in progress, skipping signal'), 'on_usr1 should log when skipping due to re-entrancy');
});
test('on_usr1 sets ANALYZING=1 before and ANALYZING=0 after analysis', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
// Check that ANALYZING=1 is set before analyze_observations
const analyzeCall = content.indexOf('ANALYZING=1');
const analyzeObsCall = content.indexOf('analyze_observations', analyzeCall);
const analyzeReset = content.indexOf('ANALYZING=0', analyzeObsCall);
assert.ok(analyzeCall > 0, 'ANALYZING=1 should be set');
assert.ok(analyzeObsCall > analyzeCall, 'analyze_observations should be called after ANALYZING=1');
assert.ok(analyzeReset > analyzeObsCall, 'ANALYZING=0 should follow analyze_observations');
});
// ──────────────────────────────────────────────────────
// Test group 3: observer-loop.sh cooldown throttle
// ──────────────────────────────────────────────────────
console.log('\n--- observer-loop.sh cooldown throttle ---');
test('observer-loop.sh defines ANALYSIS_COOLDOWN', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('ANALYSIS_COOLDOWN'), 'observer-loop.sh should define ANALYSIS_COOLDOWN');
});
test('on_usr1 enforces cooldown between analyses', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('LAST_ANALYSIS_EPOCH'), 'Should track last analysis time');
assert.ok(content.includes('Analysis cooldown active'), 'Should log when cooldown prevents analysis');
});
test('default cooldown is 60 seconds', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('ECC_OBSERVER_ANALYSIS_COOLDOWN:-60'), 'Default cooldown should be 60 seconds');
});
// ──────────────────────────────────────────────────────
// Test group 4: Tail-based sampling (no full file load)
// ──────────────────────────────────────────────────────
console.log('\n--- observer-loop.sh tail-based sampling ---');
test('analyze_observations uses tail to sample recent observations', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('tail -n "$MAX_ANALYSIS_LINES"'), 'Should use tail to limit observations sent to LLM');
});
test('default max analysis lines is 500', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('ECC_OBSERVER_MAX_ANALYSIS_LINES:-500'), 'Default should sample last 500 lines');
});
test('analysis temp file is created and cleaned up', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('ecc-observer-analysis'), 'Should create a temp analysis file');
assert.ok(content.includes('rm -f "$prompt_file" "$analysis_file"'), 'Should clean up both prompt and analysis temp files');
});
test('observer-loop uses project-local temp directory for analysis artifacts', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('observer_tmp_dir="${PROJECT_DIR}/.observer-tmp"'), 'Should keep observer temp files inside the project');
assert.ok(content.includes('mktemp "${observer_tmp_dir}/ecc-observer-analysis.'), 'Analysis temp file should use the project temp dir');
assert.ok(content.includes('mktemp "${observer_tmp_dir}/ecc-observer-prompt.'), 'Prompt temp file should use the project temp dir');
});
test('observer-loop prompt requires direct instinct writes without asking permission', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
const heredocStart = content.indexOf('cat > "$prompt_file" <<PROMPT');
const heredocEnd = content.indexOf('\nPROMPT', heredocStart + 1);
assert.ok(heredocStart > 0, 'Should find prompt heredoc start');
assert.ok(heredocEnd > heredocStart, 'Should find prompt heredoc end');
const promptSection = content.substring(heredocStart, heredocEnd);
assert.ok(promptSection.includes('MUST write an instinct file directly'), 'Prompt should require direct file creation');
assert.ok(promptSection.includes('Do NOT ask for permission'), 'Prompt should forbid permission-seeking');
assert.ok(promptSection.includes('write or update the instinct file in this run'), 'Prompt should require same-run writes');
});
test('prompt references analysis_file not full OBSERVATIONS_FILE', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
// The prompt heredoc should reference analysis_file for the Read instruction.
// Find the section between the heredoc open and close markers.
const heredocStart = content.indexOf('cat > "$prompt_file" <<PROMPT');
const heredocEnd = content.indexOf('\nPROMPT', heredocStart + 1);
assert.ok(heredocStart > 0, 'Should find prompt heredoc start');
assert.ok(heredocEnd > heredocStart, 'Should find prompt heredoc end');
const promptSection = content.substring(heredocStart, heredocEnd);
assert.ok(promptSection.includes('${analysis_file}'), 'Prompt should point Claude at the sampled analysis file, not the full observations file');
});
// ──────────────────────────────────────────────────────
// Test group 5: Signal counter file simulation
// ──────────────────────────────────────────────────────
console.log('\n--- Signal counter file behavior ---');
test('counter file increments and resets correctly', () => {
const testDir = createTempDir();
const counterFile = path.join(testDir, '.observer-signal-counter');
// Simulate 20 calls - first 19 should not signal, 20th should
const signalEveryN = 20;
let signalCount = 0;
for (let i = 0; i < 40; i++) {
let shouldSignal = false;
if (fs.existsSync(counterFile)) {
let counter = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10) || 0;
counter++;
if (counter >= signalEveryN) {
shouldSignal = true;
counter = 0;
}
fs.writeFileSync(counterFile, String(counter));
} else {
fs.writeFileSync(counterFile, '1');
}
if (shouldSignal) signalCount++;
}
// 40 calls with threshold 20 should signal exactly 2 times
// (at call 20 and call 40)
assert.strictEqual(signalCount, 2, `Expected 2 signals over 40 calls, got ${signalCount}`);
cleanupDir(testDir);
});
test('counter file handles missing/corrupt file gracefully', () => {
const testDir = createTempDir();
const counterFile = path.join(testDir, '.observer-signal-counter');
// Write corrupt content
fs.writeFileSync(counterFile, 'not-a-number');
const counter = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10) || 0;
assert.strictEqual(counter, 0, 'Corrupt counter should default to 0');
cleanupDir(testDir);
});
// ──────────────────────────────────────────────────────
// Test group 6: End-to-end observe.sh signal throttle (shell)
// ──────────────────────────────────────────────────────
console.log('\n--- observe.sh end-to-end throttle (shell execution) ---');
test('observe.sh creates counter file and increments on each call', () => {
if (process.platform === 'win32') {
return;
}
// This test runs observe.sh with minimal input to verify counter behavior.
// We need python3, bash, and a valid project dir to test the full flow.
// We use ECC_SKIP_OBSERVE=0 and minimal JSON so observe.sh processes but
// exits before signaling (no observer PID running).
const testDir = createTempDir();
const projectDir = path.join(testDir, 'project');
fs.mkdirSync(projectDir, { recursive: true });
// Create a minimal detect-project.sh that sets required vars
const skillRoot = path.join(testDir, 'skill');
const scriptsDir = path.join(skillRoot, 'scripts');
const hooksDir = path.join(skillRoot, 'hooks');
fs.mkdirSync(scriptsDir, { recursive: true });
fs.mkdirSync(hooksDir, { recursive: true });
// Minimal detect-project.sh stub
fs.writeFileSync(
path.join(scriptsDir, 'detect-project.sh'),
[
'#!/bin/bash',
`PROJECT_ID="test-project"`,
`PROJECT_NAME="test-project"`,
`PROJECT_ROOT="${projectDir}"`,
`PROJECT_DIR="${projectDir}"`,
`CLV2_PYTHON_CMD="${process.platform === 'win32' ? 'python' : 'python3'}"`,
''
].join('\n')
);
// Copy observe.sh but patch SKILL_ROOT to our test dir
let observeContent = fs.readFileSync(observeShPath, 'utf8');
observeContent = observeContent.replace('SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"', `SKILL_ROOT="${skillRoot}"`);
const testObserve = path.join(hooksDir, 'observe.sh');
fs.writeFileSync(testObserve, observeContent, { mode: 0o755 });
const hookInput = JSON.stringify({
tool_name: 'Read',
tool_input: { file_path: '/tmp/test.txt' },
session_id: 'test-session',
cwd: projectDir
});
// Run observe.sh twice
for (let i = 0; i < 2; i++) {
spawnSync('bash', [testObserve, 'post'], {
input: hookInput,
env: {
...process.env,
HOME: testDir,
CLAUDE_CODE_ENTRYPOINT: 'cli',
ECC_HOOK_PROFILE: 'standard',
ECC_SKIP_OBSERVE: '0',
CLAUDE_PROJECT_DIR: projectDir
},
timeout: 5000
});
}
const counterFile = path.join(projectDir, '.observer-signal-counter');
if (fs.existsSync(counterFile)) {
const val = fs.readFileSync(counterFile, 'utf8').trim();
const counterVal = parseInt(val, 10);
assert.ok(counterVal >= 1 && counterVal <= 2, `Counter should be 1 or 2 after 2 calls, got ${counterVal}`);
} else {
// If python3 is not available the hook exits early - that is acceptable
const hasPython = spawnSync('python3', ['--version']).status === 0;
if (hasPython) {
assert.fail('Counter file should exist after running observe.sh');
}
}
cleanupDir(testDir);
});
// ──────────────────────────────────────────────────────
// Test group 7: Observer Haiku invocation flags
// ──────────────────────────────────────────────────────
console.log('\n--- Observer Haiku invocation flags ---');
test('claude invocation includes --allowedTools flag', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('--allowedTools'), 'observer-loop.sh should include --allowedTools flag in claude invocation');
});
test('allowedTools includes Read permission', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
const match = content.match(/--allowedTools\s+"([^"]+)"/);
assert.ok(match, 'Should find --allowedTools with quoted value');
assert.ok(match[1].includes('Read'), `allowedTools should include Read, got: ${match[1]}`);
});
test('allowedTools includes Write permission', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
const match = content.match(/--allowedTools\s+"([^"]+)"/);
assert.ok(match, 'Should find --allowedTools with quoted value');
assert.ok(match[1].includes('Write'), `allowedTools should include Write, got: ${match[1]}`);
});
test('claude invocation still includes ECC_SKIP_OBSERVE and ECC_HOOK_PROFILE guards', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
// Find the claude execution line(s)
const lines = content.split('\n');
const claudeLine = lines.find(l => l.includes('claude --model haiku'));
assert.ok(claudeLine, 'Should find claude --model haiku invocation line');
// The env vars are on the same line as the claude command
const claudeLineIndex = lines.indexOf(claudeLine);
const fullCommand = lines.slice(Math.max(0, claudeLineIndex - 1), claudeLineIndex + 3).join(' ');
assert.ok(fullCommand.includes('ECC_SKIP_OBSERVE=1'), 'claude invocation should include ECC_SKIP_OBSERVE=1 guard');
assert.ok(fullCommand.includes('ECC_HOOK_PROFILE=minimal'), 'claude invocation should include ECC_HOOK_PROFILE=minimal guard');
});
// ──────────────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────────────
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}\n`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,207 @@
/**
* Tests for post-bash-build-complete.js and post-bash-pr-created.js
*
* Run with: node tests/hooks/post-bash-hooks.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const buildCompleteScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-build-complete.js');
const prCreatedScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-pr-created.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(scriptPath, input) {
return spawnSync('node', [scriptPath], {
encoding: 'utf8',
input,
stdio: ['pipe', 'pipe', 'pipe']
});
}
let passed = 0;
let failed = 0;
// ── post-bash-build-complete.js ──────────────────────────────────
console.log('\nPost-Bash Build Complete Hook Tests');
console.log('====================================\n');
console.log('Build command detection:');
if (test('stderr contains "Build completed" for npm run build command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm run build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('Build completed'), `stderr should contain "Build completed", got: ${result.stderr}`);
})) passed++; else failed++;
if (test('stderr contains "Build completed" for pnpm build command', () => {
const input = JSON.stringify({ tool_input: { command: 'pnpm build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('Build completed'), `stderr should contain "Build completed", got: ${result.stderr}`);
})) passed++; else failed++;
if (test('stderr contains "Build completed" for yarn build command', () => {
const input = JSON.stringify({ tool_input: { command: 'yarn build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('Build completed'), `stderr should contain "Build completed", got: ${result.stderr}`);
})) passed++; else failed++;
console.log('\nNon-build command detection:');
if (test('no stderr message for npm test command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm test' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-build command');
})) passed++; else failed++;
if (test('no stderr message for ls command', () => {
const input = JSON.stringify({ tool_input: { command: 'ls -la' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-build command');
})) passed++; else failed++;
if (test('no stderr message for git status command', () => {
const input = JSON.stringify({ tool_input: { command: 'git status' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-build command');
})) passed++; else failed++;
console.log('\nStdout pass-through:');
if (test('stdout passes through input for build command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm run build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for non-build command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm test' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for invalid JSON', () => {
const input = 'not valid json';
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through empty input', () => {
const input = '';
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
// ── post-bash-pr-created.js ──────────────────────────────────────
console.log('\n\nPost-Bash PR Created Hook Tests');
console.log('================================\n');
console.log('PR creation detection:');
if (test('stderr contains PR URL when gh pr create output has PR URL', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create --title "Fix bug" --body "desc"' },
tool_output: { output: 'https://github.com/owner/repo/pull/42\n' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('https://github.com/owner/repo/pull/42'), `stderr should contain PR URL, got: ${result.stderr}`);
assert.ok(result.stderr.includes('[Hook] PR created:'), 'stderr should contain PR created message');
assert.ok(result.stderr.includes('gh pr review 42'), 'stderr should contain review command');
})) passed++; else failed++;
if (test('stderr contains correct repo in review command', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create' },
tool_output: { output: 'Created PR\nhttps://github.com/my-org/my-repo/pull/123\nDone' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('--repo my-org/my-repo'), `stderr should contain correct repo, got: ${result.stderr}`);
assert.ok(result.stderr.includes('gh pr review 123'), 'stderr should contain correct PR number');
})) passed++; else failed++;
console.log('\nNon-PR command detection:');
if (test('no stderr about PR for non-gh command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm test' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-PR command');
})) passed++; else failed++;
if (test('no stderr about PR for gh issue command', () => {
const input = JSON.stringify({ tool_input: { command: 'gh issue list' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-PR create command');
})) passed++; else failed++;
if (test('no stderr about PR for gh pr create without PR URL in output', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create' },
tool_output: { output: 'Error: could not create PR' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty when no PR URL in output');
})) passed++; else failed++;
if (test('no stderr about PR for gh pr list command', () => {
const input = JSON.stringify({ tool_input: { command: 'gh pr list' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for gh pr list');
})) passed++; else failed++;
console.log('\nStdout pass-through:');
if (test('stdout passes through input for PR create command', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create' },
tool_output: { output: 'https://github.com/owner/repo/pull/1' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for non-PR command', () => {
const input = JSON.stringify({ tool_input: { command: 'echo hello' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for invalid JSON', () => {
const input = 'not valid json';
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through empty input', () => {
const input = '';
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,121 @@
/**
* Tests for pre-bash-dev-server-block.js hook
*
* Run with: node tests/hooks/pre-bash-dev-server-block.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-dev-server-block.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(command) {
const input = { tool_input: { command } };
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: JSON.stringify(input),
timeout: 10000,
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function runTests() {
console.log('\n=== Testing pre-bash-dev-server-block.js ===\n');
let passed = 0;
let failed = 0;
const isWindows = process.platform === 'win32';
// --- Blocking tests (non-Windows only) ---
if (!isWindows) {
(test('blocks npm run dev (exit code 2, stderr contains BLOCKED)', () => {
const result = runScript('npm run dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
assert.ok(result.stderr.includes('BLOCKED'), `Expected stderr to contain BLOCKED, got: ${result.stderr}`);
}) ? passed++ : failed++);
(test('blocks pnpm dev (exit code 2)', () => {
const result = runScript('pnpm dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks yarn dev (exit code 2)', () => {
const result = runScript('yarn dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks bun run dev (exit code 2)', () => {
const result = runScript('bun run dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
} else {
console.log(' (skipping blocking tests on Windows)\n');
}
// --- Allow tests ---
(test('allows tmux-wrapped npm run dev (exit code 0)', () => {
const result = runScript('tmux new-session -d -s dev "npm run dev"');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows npm install (exit code 0)', () => {
const result = runScript('npm install');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows npm test (exit code 0)', () => {
const result = runScript('npm test');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows npm run build (exit code 0)', () => {
const result = runScript('npm run build');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
// --- Edge cases ---
(test('empty/invalid input passes through (exit code 0)', () => {
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: '',
timeout: 10000,
});
assert.strictEqual(result.status || 0, 0, `Expected exit code 0, got ${result.status}`);
}) ? passed++ : failed++);
(test('stdout contains original input on pass-through', () => {
const input = { tool_input: { command: 'npm install' } };
const inputStr = JSON.stringify(input);
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
});
assert.strictEqual(result.status || 0, 0);
assert.strictEqual(result.stdout.trim(), inputStr, `Expected stdout to contain original input`);
}) ? passed++ : failed++);
// --- Summary ---
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,104 @@
/**
* Tests for pre-bash-git-push-reminder.js and pre-bash-tmux-reminder.js hooks
*
* Run with: node tests/hooks/pre-bash-reminders.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const gitPushScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-git-push-reminder.js');
const tmuxScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-tmux-reminder.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(scriptPath, command, envOverrides = {}) {
const input = { tool_input: { command } };
const inputStr = JSON.stringify(input);
const result = spawnSync('node', [scriptPath], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
env: { ...process.env, ...envOverrides },
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '', inputStr };
}
function runTests() {
console.log('\n=== Testing pre-bash-git-push-reminder.js & pre-bash-tmux-reminder.js ===\n');
let passed = 0;
let failed = 0;
// --- git-push-reminder tests ---
console.log(' git-push-reminder:');
(test('git push triggers stderr warning', () => {
const result = runScript(gitPushScript, 'git push origin main');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.ok(result.stderr.includes('[Hook]'), `Expected stderr to contain [Hook], got: ${result.stderr}`);
assert.ok(result.stderr.includes('Review changes before push'), `Expected stderr to mention review`);
}) ? passed++ : failed++);
(test('git status has no warning', () => {
const result = runScript(gitPushScript, 'git status');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stderr, '', `Expected no stderr, got: ${result.stderr}`);
}) ? passed++ : failed++);
(test('git push always passes through input on stdout', () => {
const result = runScript(gitPushScript, 'git push');
assert.strictEqual(result.stdout, result.inputStr, 'Expected stdout to match original input');
}) ? passed++ : failed++);
// --- tmux-reminder tests (non-Windows only) ---
const isWindows = process.platform === 'win32';
if (!isWindows) {
console.log('\n tmux-reminder:');
(test('npm install triggers tmux suggestion', () => {
const result = runScript(tmuxScript, 'npm install', { TMUX: '' });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.ok(result.stderr.includes('[Hook]'), `Expected stderr to contain [Hook], got: ${result.stderr}`);
assert.ok(result.stderr.includes('tmux'), `Expected stderr to mention tmux`);
}) ? passed++ : failed++);
(test('npm test triggers tmux suggestion', () => {
const result = runScript(tmuxScript, 'npm test', { TMUX: '' });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.ok(result.stderr.includes('tmux'), `Expected stderr to mention tmux`);
}) ? passed++ : failed++);
(test('regular command like ls has no tmux suggestion', () => {
const result = runScript(tmuxScript, 'ls -la', { TMUX: '' });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stderr, '', `Expected no stderr for ls, got: ${result.stderr}`);
}) ? passed++ : failed++);
(test('tmux reminder always passes through input on stdout', () => {
const result = runScript(tmuxScript, 'npm install', { TMUX: '' });
assert.strictEqual(result.stdout, result.inputStr, 'Expected stdout to match original input');
}) ? passed++ : failed++);
} else {
console.log('\n (skipping tmux-reminder tests on Windows)\n');
}
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,159 @@
/**
* Tests for scripts/hooks/quality-gate.js
*
* Run with: node tests/hooks/quality-gate.test.js
*/
const assert = require('assert');
const path = require('path');
const os = require('os');
const fs = require('fs');
const qualityGate = require('../../scripts/hooks/quality-gate');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
let passed = 0;
let failed = 0;
console.log('\nQuality Gate Hook Tests');
console.log('========================\n');
// --- run() returns original input for valid JSON ---
console.log('run() pass-through behavior:');
if (test('returns original input for valid JSON with file_path', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/nonexistent-file.js' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for valid JSON without file_path', () => {
const input = JSON.stringify({ tool_input: { command: 'ls' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for valid JSON with nested structure', () => {
const input = JSON.stringify({ tool_input: { file_path: '/some/path.ts', content: 'hello' }, other: [1, 2, 3] });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() returns original input for invalid JSON ---
console.log('\nInvalid JSON handling:');
if (test('returns original input for invalid JSON (no crash)', () => {
const input = 'this is not json at all {{{';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for partial JSON', () => {
const input = '{"tool_input": {';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for JSON with trailing garbage', () => {
const input = '{"tool_input": {}}extra';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() returns original input when file does not exist ---
console.log('\nNon-existent file handling:');
if (test('returns original input when file_path points to non-existent file', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/does-not-exist-12345.js' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input when file_path is a non-existent .py file', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/does-not-exist-12345.py' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input when file_path is a non-existent .go file', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/does-not-exist-12345.go' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() returns original input for empty input ---
console.log('\nEmpty input handling:');
if (test('returns original input for empty string', () => {
const input = '';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return empty string unchanged');
})) passed++; else failed++;
if (test('returns original input for whitespace-only string', () => {
const input = ' ';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return whitespace string unchanged');
})) passed++; else failed++;
// --- run() handles missing tool_input gracefully ---
console.log('\nMissing tool_input handling:');
if (test('handles missing tool_input gracefully', () => {
const input = JSON.stringify({ something_else: 'value' });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('handles null tool_input gracefully', () => {
const input = JSON.stringify({ tool_input: null });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('handles tool_input with empty file_path', () => {
const input = JSON.stringify({ tool_input: { file_path: '' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('handles empty JSON object', () => {
const input = JSON.stringify({});
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() with a real file (but no formatter installed) ---
console.log('\nReal file without formatter:');
if (test('returns original input for existing file with no formatter configured', () => {
const tmpFile = path.join(os.tmpdir(), `quality-gate-test-${Date.now()}.js`);
fs.writeFileSync(tmpFile, 'const x = 1;\n');
try {
const input = JSON.stringify({ tool_input: { file_path: tmpFile } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
} finally {
fs.unlinkSync(tmpFile);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,400 @@
/**
* Tests for scripts/hooks/suggest-compact.js
*
* Tests the tool-call counter, threshold logic, interval suggestions,
* and environment variable handling.
*
* Run with: node tests/hooks/suggest-compact.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
const compactScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'suggest-compact.js');
// Test helpers
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;
}
}
/**
* Run suggest-compact.js with optional env overrides.
* Returns { code, stdout, stderr }.
*/
function runCompact(envOverrides = {}) {
const env = { ...process.env, ...envOverrides };
const result = spawnSync('node', [compactScript], {
encoding: 'utf8',
input: '{}',
timeout: 10000,
env,
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
/**
* Get the counter file path for a given session ID.
*/
function getCounterFilePath(sessionId) {
return path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
}
let counterContextSeq = 0;
function createCounterContext(prefix = 'test-compact') {
counterContextSeq += 1;
const sessionId = `${prefix}-${Date.now()}-${counterContextSeq}`;
const counterFile = getCounterFilePath(sessionId);
return {
sessionId,
counterFile,
cleanup() {
try {
fs.unlinkSync(counterFile);
} catch (_err) {
// Ignore missing temp files between runs
}
}
};
}
function runTests() {
console.log('\n=== Testing suggest-compact.js ===\n');
let passed = 0;
let failed = 0;
// Basic functionality
console.log('Basic counter functionality:');
if (test('creates counter file on first run', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0, 'Should exit 0');
assert.ok(fs.existsSync(counterFile), 'Counter file should be created');
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 after first run');
cleanup();
})) passed++;
else failed++;
if (test('increments counter on subsequent runs', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
runCompact({ CLAUDE_SESSION_ID: sessionId });
runCompact({ CLAUDE_SESSION_ID: sessionId });
runCompact({ CLAUDE_SESSION_ID: sessionId });
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 3, 'Counter should be 3 after three runs');
cleanup();
})) passed++;
else failed++;
// Threshold suggestion
console.log('\nThreshold suggestion:');
if (test('suggests compact at threshold (COMPACT_THRESHOLD=3)', () => {
const { sessionId, cleanup } = createCounterContext();
cleanup();
// Run 3 times with threshold=3
runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
assert.ok(
result.stderr.includes('3 tool calls reached') || result.stderr.includes('consider /compact'),
`Should suggest compact at threshold. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
if (test('does NOT suggest compact before threshold', () => {
const { sessionId, cleanup } = createCounterContext();
cleanup();
runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '5' });
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '5' });
assert.ok(
!result.stderr.includes('StrategicCompact'),
'Should NOT suggest compact before threshold'
);
cleanup();
})) passed++;
else failed++;
// Interval suggestion (every 25 calls after threshold)
console.log('\nInterval suggestion:');
if (test('suggests at threshold + 25 interval', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
// Set counter to threshold+24 (so next run = threshold+25)
// threshold=3, so we need count=28 → 25 calls past threshold
// Write 27 to the counter file, next run will be 28 = 3 + 25
fs.writeFileSync(counterFile, '27');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
// count=28, threshold=3, 28-3=25, 25 % 25 === 0 → should suggest
assert.ok(
result.stderr.includes('28 tool calls') || result.stderr.includes('checkpoint'),
`Should suggest at threshold+25 interval. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
// Environment variable handling
console.log('\nEnvironment variable handling:');
if (test('uses default threshold (50) when COMPACT_THRESHOLD is not set', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
// Write counter to 49, next run will be 50 = default threshold
fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
// Remove COMPACT_THRESHOLD from env
assert.ok(
result.stderr.includes('50 tool calls reached'),
`Should use default threshold of 50. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '-5' });
// Invalid threshold falls back to 50
assert.ok(
result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
if (test('ignores non-numeric COMPACT_THRESHOLD', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: 'abc' });
// NaN falls back to 50
assert.ok(
result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
// Corrupted counter file
console.log('\nCorrupted counter file:');
if (test('resets counter on corrupted file content', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, 'not-a-number');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0);
// Corrupted file → parsed is NaN → falls back to count=1
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file');
cleanup();
})) passed++;
else failed++;
if (test('resets counter on extremely large value', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
// Value > 1000000 should be clamped
fs.writeFileSync(counterFile, '9999999');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0);
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000');
cleanup();
})) passed++;
else failed++;
if (test('handles empty counter file', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0);
// Empty file → bytesRead=0 → count starts at 1
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should start at 1 for empty file');
cleanup();
})) passed++;
else failed++;
// Session isolation
console.log('\nSession isolation:');
if (test('uses separate counter files per session ID', () => {
const sessionA = `compact-a-${Date.now()}`;
const sessionB = `compact-b-${Date.now()}`;
const fileA = getCounterFilePath(sessionA);
const fileB = getCounterFilePath(sessionB);
try {
runCompact({ CLAUDE_SESSION_ID: sessionA });
runCompact({ CLAUDE_SESSION_ID: sessionA });
runCompact({ CLAUDE_SESSION_ID: sessionB });
const countA = parseInt(fs.readFileSync(fileA, 'utf8').trim(), 10);
const countB = parseInt(fs.readFileSync(fileB, 'utf8').trim(), 10);
assert.strictEqual(countA, 2, 'Session A should have count 2');
assert.strictEqual(countB, 1, 'Session B should have count 1');
} finally {
try { fs.unlinkSync(fileA); } catch (_err) { /* ignore */ }
try { fs.unlinkSync(fileB); } catch (_err) { /* ignore */ }
}
})) passed++;
else failed++;
// Always exits 0
console.log('\nExit code:');
if (test('always exits 0 (never blocks Claude)', () => {
const { sessionId, cleanup } = createCounterContext();
cleanup();
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0, 'Should always exit 0');
cleanup();
})) passed++;
else failed++;
// ── Round 29: threshold boundary values ──
console.log('\nThreshold boundary values:');
if (test('rejects COMPACT_THRESHOLD=0 (falls back to 50)', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '0' });
// 0 is invalid (must be > 0), falls back to 50, count becomes 50 → should suggest
assert.ok(
result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '9999');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '10000' });
// count becomes 10000, threshold=10000 → should suggest
assert.ok(
result.stderr.includes('10000 tool calls reached'),
`Should accept threshold=10000. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '10001' });
// 10001 > 10000, invalid, falls back to 50, count becomes 50 → should suggest
assert.ok(
result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}`
);
cleanup();
})) passed++;
else failed++;
if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3.5' });
// parseInt('3.5') = 3, which is valid (> 0 && <= 10000)
// count becomes 50, threshold=3, 50-3=47, 47%25≠0 and 50≠3 → no suggestion
assert.strictEqual(result.code, 0);
// No suggestion expected (50 !== 3, and (50-3) % 25 !== 0)
assert.ok(
!result.stderr.includes('StrategicCompact'),
'Float threshold should be parseInt-ed to 3, no suggestion at count=50'
);
cleanup();
})) passed++;
else failed++;
if (test('counter value at exact boundary 1000000 is valid', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '999999');
runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
// 999999 is valid (> 0, <= 1000000), count becomes 1000000
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid');
cleanup();
})) passed++;
else failed++;
if (test('counter value at 1000001 is clamped (reset to 1)', () => {
const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '1000001');
runCompact({ CLAUDE_SESSION_ID: sessionId });
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1');
cleanup();
})) passed++;
else failed++;
// ── Round 64: default session ID fallback ──
console.log('\nDefault session ID fallback (Round 64):');
if (test('uses "default" session ID when CLAUDE_SESSION_ID is empty', () => {
const defaultCounterFile = getCounterFilePath('default');
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
try {
// Pass empty CLAUDE_SESSION_ID — falsy, so script uses 'default'
const env = { ...process.env, CLAUDE_SESSION_ID: '' };
const result = spawnSync('node', [compactScript], {
encoding: 'utf8',
input: '{}',
timeout: 10000,
env,
});
assert.strictEqual(result.status || 0, 0, 'Should exit 0');
assert.ok(fs.existsSync(defaultCounterFile), 'Counter file should use "default" session ID');
const count = parseInt(fs.readFileSync(defaultCounterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 for first run with default session');
} finally {
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
}
})) passed++;
else failed++;
// Summary
console.log(`
Results: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,775 @@
/**
* Integration tests for hook scripts
*
* Tests hook behavior in realistic scenarios with proper input/output handling.
*
* Run with: node tests/integration/hooks.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawn } = require('child_process');
const REPO_ROOT = path.join(__dirname, '..', '..');
// Test helper
function _test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
// Async test helper
async function asyncTest(name, fn) {
try {
await fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
/**
* Run a hook script with simulated Claude Code input
* @param {string} scriptPath - Path to the hook script
* @param {object} input - Hook input object (will be JSON stringified)
* @param {object} env - Environment variables
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
*/
function runHookWithInput(scriptPath, input = {}, env = {}, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const proc = spawn('node', [scriptPath], {
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', data => stdout += data);
proc.stderr.on('data', data => stderr += data);
// Ignore EPIPE/EOF errors (process may exit before we finish writing)
// Windows uses EOF instead of EPIPE for closed pipe writes
proc.stdin.on('error', (err) => {
if (err.code !== 'EPIPE' && err.code !== 'EOF') {
reject(err);
}
});
// Send JSON input on stdin (simulating Claude Code hook invocation)
if (input && Object.keys(input).length > 0) {
proc.stdin.write(JSON.stringify(input));
}
proc.stdin.end();
const timer = setTimeout(() => {
proc.kill('SIGKILL');
reject(new Error(`Hook timed out after ${timeoutMs}ms`));
}, timeoutMs);
proc.on('close', code => {
clearTimeout(timer);
resolve({ code, stdout, stderr });
});
proc.on('error', err => {
clearTimeout(timer);
reject(err);
});
});
}
function getSessionStartPayload(stdout) {
assert.ok(stdout.trim(), 'Expected SessionStart hook to emit stdout payload');
const payload = JSON.parse(stdout);
assert.strictEqual(payload.hookSpecificOutput?.hookEventName, 'SessionStart');
assert.strictEqual(typeof payload.hookSpecificOutput?.additionalContext, 'string');
return payload;
}
/**
* Run a hook command string exactly as declared in hooks.json.
* Supports wrapped node script commands and shell wrappers.
* @param {string} command - Hook command from hooks.json
* @param {object} input - Hook input object
* @param {object} env - Environment variables
*/
function runHookCommand(command, input = {}, env = {}, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32';
const mergedEnv = { ...process.env, CLAUDE_PLUGIN_ROOT: REPO_ROOT, ...env };
const resolvedCommand = command.replace(
/\$\{([A-Z_][A-Z0-9_]*)\}/g,
(_, name) => String(mergedEnv[name] || '')
);
const nodeMatch = resolvedCommand.match(/^node\s+"([^"]+)"\s*(.*)$/);
const useDirectNodeSpawn = Boolean(nodeMatch);
const shell = isWindows ? 'cmd' : 'bash';
const shellArgs = isWindows ? ['/d', '/s', '/c', resolvedCommand] : ['-lc', resolvedCommand];
const nodeArgs = nodeMatch
? [
nodeMatch[1],
...Array.from(
nodeMatch[2].matchAll(/"([^"]*)"|(\S+)/g),
m => m[1] !== undefined ? m[1] : m[2]
)
]
: [];
const proc = useDirectNodeSpawn
? spawn('node', nodeArgs, { env: mergedEnv, stdio: ['pipe', 'pipe', 'pipe'] })
: spawn(shell, shellArgs, { env: mergedEnv, stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
let timer;
proc.stdout.on('data', data => stdout += data);
proc.stderr.on('data', data => stderr += data);
// Ignore EPIPE/EOF errors (process may exit before we finish writing)
proc.stdin.on('error', (err) => {
if (err.code !== 'EPIPE' && err.code !== 'EOF') {
if (timer) clearTimeout(timer);
reject(err);
}
});
if (input && Object.keys(input).length > 0) {
proc.stdin.write(JSON.stringify(input));
}
proc.stdin.end();
timer = setTimeout(() => {
proc.kill(isWindows ? undefined : 'SIGKILL');
reject(new Error(`Hook command timed out after ${timeoutMs}ms`));
}, timeoutMs);
proc.on('close', code => {
clearTimeout(timer);
resolve({ code, stdout, stderr });
});
proc.on('error', err => {
clearTimeout(timer);
reject(err);
});
});
}
// Create a temporary test directory
function createTestDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'hook-integration-test-'));
}
// Clean up test directory
function cleanupTestDir(testDir) {
fs.rmSync(testDir, { recursive: true, force: true });
}
function getHookCommandByDescription(hooks, lifecycle, descriptionText) {
const hookGroup = hooks.hooks[lifecycle]?.find(
entry => entry.description && entry.description.includes(descriptionText)
);
assert.ok(hookGroup, `Expected ${lifecycle} hook matching "${descriptionText}"`);
assert.ok(hookGroup.hooks?.[0]?.command, `Expected ${lifecycle} hook command for "${descriptionText}"`);
return hookGroup.hooks[0].command;
}
// Test suite
async function runTests() {
console.log('\n=== Hook Integration Tests ===\n');
let passed = 0;
let failed = 0;
const scriptsDir = path.join(__dirname, '..', '..', 'scripts', 'hooks');
const hooksJsonPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const hooks = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
// ==========================================
// Input Format Tests
// ==========================================
console.log('Hook Input Format Handling:');
if (await asyncTest('hooks handle empty stdin gracefully', async () => {
const result = await runHookWithInput(path.join(scriptsDir, 'session-start.js'), {});
assert.strictEqual(result.code, 0, `Should exit 0, got ${result.code}`);
})) passed++; else failed++;
if (await asyncTest('hooks handle malformed JSON input', async () => {
const proc = spawn('node', [path.join(scriptsDir, 'session-start.js')], {
stdio: ['pipe', 'pipe', 'pipe']
});
let code = null;
proc.stdin.write('{ invalid json }');
proc.stdin.end();
await new Promise((resolve) => {
proc.on('close', (c) => {
code = c;
resolve();
});
});
// Hook should not crash on malformed input (exit 0)
assert.strictEqual(code, 0, 'Should handle malformed JSON gracefully');
})) passed++; else failed++;
if (await asyncTest('hooks parse valid tool_input correctly', async () => {
// Test the console.log warning hook with valid input
const command = 'node -e "const fs=require(\'fs\');let d=\'\';process.stdin.on(\'data\',c=>d+=c);process.stdin.on(\'end\',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||\'\';console.log(\'Path:\',p)})"';
const match = command.match(/^node -e "(.+)"$/s);
const proc = spawn('node', ['-e', match[1]], {
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
proc.stdout.on('data', data => stdout += data);
proc.stdin.write(JSON.stringify({
tool_input: { file_path: '/test/path.js' }
}));
proc.stdin.end();
await new Promise(resolve => proc.on('close', resolve));
assert.ok(stdout.includes('/test/path.js'), 'Should extract file_path from input');
})) passed++; else failed++;
// ==========================================
// Output Format Tests
// ==========================================
console.log('\nHook Output Format:');
if (await asyncTest('session-start logs diagnostics to stderr and emits structured stdout when context exists', async () => {
const result = await runHookWithInput(path.join(scriptsDir, 'session-start.js'), {});
// Session-start should write info to stderr
assert.ok(result.stderr.length > 0, 'Should have stderr output');
assert.ok(result.stderr.includes('[SessionStart]'), 'Should have [SessionStart] prefix');
const payload = getSessionStartPayload(result.stdout);
assert.ok(payload.hookSpecificOutput, 'Should include hookSpecificOutput');
assert.strictEqual(payload.hookSpecificOutput.hookEventName, 'SessionStart');
})) passed++; else failed++;
if (await asyncTest('PreCompact hook logs to stderr', async () => {
const result = await runHookWithInput(path.join(scriptsDir, 'pre-compact.js'), {});
assert.ok(result.stderr.includes('[PreCompact]'), 'Should output to stderr with prefix');
})) passed++; else failed++;
if (await asyncTest('dev server hook transforms command to tmux session', async () => {
// Test the auto-tmux dev hook — transforms dev commands to run in tmux
const hookCommand = getHookCommandByDescription(
hooks,
'PreToolUse',
'Auto-start dev servers in tmux'
);
const result = await runHookCommand(hookCommand, {
tool_input: { command: 'npm run dev' }
});
assert.strictEqual(result.code, 0, 'Hook should exit 0 (transforms, does not block)');
// On Unix with tmux, stdout contains transformed JSON with tmux command
// On Windows or without tmux, stdout contains original JSON passthrough
const output = result.stdout.trim();
if (output) {
const parsed = JSON.parse(output);
assert.ok(parsed.tool_input, 'Should output valid JSON with tool_input');
}
})) passed++; else failed++;
// ==========================================
// Exit Code Tests
// ==========================================
console.log('\nHook Exit Codes:');
if (await asyncTest('non-blocking hooks exit with code 0', async () => {
const result = await runHookWithInput(path.join(scriptsDir, 'session-end.js'), {});
assert.strictEqual(result.code, 0, 'Non-blocking hook should exit 0');
})) passed++; else failed++;
if (await asyncTest('dev server hook transforms yarn dev to tmux session', async () => {
// The auto-tmux dev hook transforms dev commands (yarn dev, npm run dev, etc.)
const hookCommand = getHookCommandByDescription(
hooks,
'PreToolUse',
'Auto-start dev servers in tmux'
);
const result = await runHookCommand(hookCommand, {
tool_input: { command: 'yarn dev' }
});
// Hook always exits 0 — it transforms, never blocks
assert.strictEqual(result.code, 0, 'Hook should exit 0 (transforms, does not block)');
const output = result.stdout.trim();
if (output) {
const parsed = JSON.parse(output);
assert.ok(parsed.tool_input, 'Should output valid JSON with tool_input');
assert.ok(parsed.tool_input.command, 'Should have a command in output');
}
})) passed++; else failed++;
if (await asyncTest('MCP health hook blocks unhealthy MCP tool calls through hooks.json', async () => {
const hookCommand = getHookCommandByDescription(
hooks,
'PreToolUse',
'Check MCP server health before MCP tool execution'
);
const testDir = createTestDir();
const configPath = path.join(testDir, 'claude.json');
const statePath = path.join(testDir, 'mcp-health.json');
const serverScript = path.join(testDir, 'broken-mcp.js');
try {
fs.writeFileSync(serverScript, 'process.exit(1);\n');
fs.writeFileSync(
configPath,
JSON.stringify({
mcpServers: {
broken: {
command: process.execPath,
args: [serverScript]
}
}
})
);
const result = await runHookCommand(
hookCommand,
{ tool_name: 'mcp__broken__search', tool_input: {} },
{
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
ECC_MCP_CONFIG_PATH: configPath,
ECC_MCP_HEALTH_STATE_PATH: statePath,
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
}
);
assert.strictEqual(result.code, 2, 'Expected unhealthy MCP preflight to block');
assert.ok(result.stderr.includes('broken is unavailable'), `Expected health warning, got: ${result.stderr}`);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (await asyncTest('hooks handle missing files gracefully', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'nonexistent.jsonl');
try {
const result = await runHookWithInput(
path.join(scriptsDir, 'evaluate-session.js'),
{ transcript_path: transcriptPath }
);
// Should not crash, just skip processing
assert.strictEqual(result.code, 0, 'Should exit 0 for missing file');
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
// ==========================================
// Realistic Scenario Tests
// ==========================================
console.log('\nRealistic Scenarios:');
if (await asyncTest('suggest-compact increments and triggers at threshold', async () => {
const sessionId = 'integration-test-' + Date.now();
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
try {
// Set counter just below threshold
fs.writeFileSync(counterFile, '49');
const result = await runHookWithInput(
path.join(scriptsDir, 'suggest-compact.js'),
{},
{ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '50' }
);
assert.ok(
result.stderr.includes('50 tool calls'),
'Should suggest compact at threshold'
);
} finally {
if (fs.existsSync(counterFile)) fs.unlinkSync(counterFile);
}
})) passed++; else failed++;
if (await asyncTest('evaluate-session processes transcript with sufficient messages', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'transcript.jsonl');
// Create a transcript with 15 user messages
const messages = Array(15).fill(null).map((_, i) => ({
type: 'user',
content: `Test message ${i + 1}`
}));
fs.writeFileSync(
transcriptPath,
messages.map(m => JSON.stringify(m)).join('\n')
);
try {
const result = await runHookWithInput(
path.join(scriptsDir, 'evaluate-session.js'),
{ transcript_path: transcriptPath }
);
assert.ok(result.stderr.includes('15 messages'), 'Should process session');
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (await asyncTest('PostToolUse PR hook extracts PR URL', async () => {
// Find the PR logging hook
const prHook = hooks.hooks.PostToolUse.find(h =>
h.description && h.description.includes('PR URL')
);
assert.ok(prHook, 'PR hook should exist');
const result = await runHookCommand(prHook.hooks[0].command, {
tool_input: { command: 'gh pr create --title "Test"' },
tool_output: { output: 'Creating pull request...\nhttps://github.com/owner/repo/pull/123' }
});
assert.ok(
result.stderr.includes('PR created') || result.stderr.includes('github.com'),
'Should extract and log PR URL'
);
})) passed++; else failed++;
// ==========================================
// Session End Transcript Parsing Tests
// ==========================================
console.log('\nSession End Transcript Parsing:');
if (await asyncTest('session-end extracts summary from mixed JSONL formats', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'mixed-transcript.jsonl');
// Create transcript with both direct tool_use and nested assistant message formats
const lines = [
JSON.stringify({ type: 'user', content: 'Fix the login bug' }),
JSON.stringify({ type: 'tool_use', name: 'Read', input: { file_path: 'src/auth.ts' } }),
JSON.stringify({ type: 'assistant', message: { content: [
{ type: 'tool_use', name: 'Edit', input: { file_path: 'src/auth.ts' } }
]}}),
JSON.stringify({ type: 'user', content: 'Now add tests' }),
JSON.stringify({ type: 'assistant', message: { content: [
{ type: 'tool_use', name: 'Write', input: { file_path: 'tests/auth.test.ts' } },
{ type: 'text', text: 'Here are the tests' }
]}}),
JSON.stringify({ type: 'user', content: 'Looks good, commit' })
];
fs.writeFileSync(transcriptPath, lines.join('\n'));
try {
const result = await runHookWithInput(
path.join(scriptsDir, 'session-end.js'),
{ transcript_path: transcriptPath },
{ HOME: testDir, USERPROFILE: testDir }
);
assert.strictEqual(result.code, 0, 'Should exit 0');
assert.ok(result.stderr.includes('[SessionEnd]'), 'Should have SessionEnd log');
// Verify a session file was created
const sessionsDir = path.join(testDir, '.claude', 'sessions');
if (fs.existsSync(sessionsDir)) {
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.tmp'));
assert.ok(files.length > 0, 'Should create a session file');
// Verify session content includes tasks from user messages
const content = fs.readFileSync(path.join(sessionsDir, files[0]), 'utf8');
assert.ok(content.includes('Fix the login bug'), 'Should include first user message');
assert.ok(content.includes('auth.ts'), 'Should include modified files');
}
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (await asyncTest('session-end handles transcript with malformed lines gracefully', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'malformed-transcript.jsonl');
const lines = [
JSON.stringify({ type: 'user', content: 'Task 1' }),
'{broken json here',
JSON.stringify({ type: 'user', content: 'Task 2' }),
'{"truncated":',
JSON.stringify({ type: 'user', content: 'Task 3' })
];
fs.writeFileSync(transcriptPath, lines.join('\n'));
try {
const result = await runHookWithInput(
path.join(scriptsDir, 'session-end.js'),
{ transcript_path: transcriptPath },
{ HOME: testDir, USERPROFILE: testDir }
);
assert.strictEqual(result.code, 0, 'Should exit 0 despite malformed lines');
// Should still process the valid lines
assert.ok(result.stderr.includes('[SessionEnd]'), 'Should have SessionEnd log');
assert.ok(result.stderr.includes('unparseable'), 'Should warn about unparseable lines');
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (await asyncTest('session-end creates session file with nested user messages', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'nested-transcript.jsonl');
// Claude Code JSONL format uses nested message.content arrays
const lines = [
JSON.stringify({ type: 'user', message: { role: 'user', content: [
{ type: 'text', text: 'Refactor the utils module' }
]}}),
JSON.stringify({ type: 'assistant', message: { content: [
{ type: 'tool_use', name: 'Read', input: { file_path: 'lib/utils.js' } }
]}}),
JSON.stringify({ type: 'user', message: { role: 'user', content: 'Approve the changes' }})
];
fs.writeFileSync(transcriptPath, lines.join('\n'));
try {
const result = await runHookWithInput(
path.join(scriptsDir, 'session-end.js'),
{ transcript_path: transcriptPath },
{ HOME: testDir, USERPROFILE: testDir }
);
assert.strictEqual(result.code, 0, 'Should exit 0');
// Check session file was created
const sessionsDir = path.join(testDir, '.claude', 'sessions');
if (fs.existsSync(sessionsDir)) {
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.tmp'));
assert.ok(files.length > 0, 'Should create session file');
const content = fs.readFileSync(path.join(sessionsDir, files[0]), 'utf8');
assert.ok(content.includes('Refactor the utils module') || content.includes('Approve'),
'Should extract user messages from nested format');
}
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
// ==========================================
// Error Handling Tests
// ==========================================
console.log('\nError Handling:');
if (await asyncTest('hooks do not crash on unexpected input structure', async () => {
const result = await runHookWithInput(
path.join(scriptsDir, 'suggest-compact.js'),
{ unexpected: { nested: { deeply: 'value' } } }
);
assert.strictEqual(result.code, 0, 'Should handle unexpected input structure');
})) passed++; else failed++;
if (await asyncTest('hooks handle null and missing values in input', async () => {
const result = await runHookWithInput(
path.join(scriptsDir, 'session-start.js'),
{ tool_input: null }
);
assert.strictEqual(result.code, 0, 'Should handle null/missing values gracefully');
})) passed++; else failed++;
if (await asyncTest('hooks handle very large input without hanging', async () => {
const largeInput = {
tool_input: { file_path: '/test.js' },
tool_output: { output: 'x'.repeat(100000) }
};
const startTime = Date.now();
const result = await runHookWithInput(
path.join(scriptsDir, 'session-start.js'),
largeInput
);
const elapsed = Date.now() - startTime;
assert.strictEqual(result.code, 0, 'Should complete successfully');
assert.ok(elapsed < 5000, `Should complete in <5s, took ${elapsed}ms`);
})) passed++; else failed++;
if (await asyncTest('hooks survive stdin exceeding 1MB limit', async () => {
// The post-edit-console-warn hook reads stdin up to 1MB then passes through
// Send > 1MB to verify truncation doesn't crash the hook
const oversizedInput = JSON.stringify({
tool_input: { file_path: '/test.js' },
tool_output: { output: 'x'.repeat(1200000) } // ~1.2MB
});
const proc = spawn('node', [path.join(scriptsDir, 'post-edit-console-warn.js')], {
stdio: ['pipe', 'pipe', 'pipe']
});
let code = null;
// MUST drain stdout/stderr to prevent backpressure blocking the child process
proc.stdout.on('data', () => {});
proc.stderr.on('data', () => {});
proc.stdin.on('error', (err) => {
if (err.code !== 'EPIPE' && err.code !== 'EOF') throw err;
});
proc.stdin.write(oversizedInput);
proc.stdin.end();
await new Promise(resolve => {
proc.on('close', (c) => { code = c; resolve(); });
});
assert.strictEqual(code, 0, 'Should exit 0 despite oversized input');
})) passed++; else failed++;
if (await asyncTest('hooks handle truncated JSON from overflow gracefully', async () => {
// session-end parses stdin JSON. If input is > 1MB and truncated mid-JSON,
// JSON.parse should fail and fall back to env var
const proc = spawn('node', [path.join(scriptsDir, 'session-end.js')], {
stdio: ['pipe', 'pipe', 'pipe']
});
let code = null;
let stderr = '';
// MUST drain stdout to prevent backpressure blocking the child process
proc.stdout.on('data', () => {});
proc.stderr.on('data', data => stderr += data);
proc.stdin.on('error', (err) => {
if (err.code !== 'EPIPE' && err.code !== 'EOF') throw err;
});
// Build a string that will be truncated mid-JSON at 1MB
const bigValue = 'x'.repeat(1200000);
proc.stdin.write(`{"transcript_path":"/tmp/none","padding":"${bigValue}"}`);
proc.stdin.end();
await new Promise(resolve => {
proc.on('close', (c) => { code = c; resolve(); });
});
// Should exit 0 even if JSON parse fails (falls back to env var or null)
assert.strictEqual(code, 0, 'Should not crash on truncated JSON');
})) passed++; else failed++;
// ==========================================
// Round 51: Timeout Enforcement
// ==========================================
console.log('\nRound 51: Timeout Enforcement:');
if (await asyncTest('runHookWithInput kills hanging hooks after timeout', async () => {
const testDir = createTestDir();
const hangingHookPath = path.join(testDir, 'hanging-hook.js');
fs.writeFileSync(hangingHookPath, 'setInterval(() => {}, 100);');
try {
const startTime = Date.now();
let error = null;
try {
await runHookWithInput(hangingHookPath, {}, {}, 500);
} catch (err) {
error = err;
}
const elapsed = Date.now() - startTime;
assert.ok(error, 'Should throw timeout error');
assert.ok(error.message.includes('timed out'), 'Error should mention timeout');
assert.ok(elapsed >= 450, `Should wait at least ~500ms, waited ${elapsed}ms`);
assert.ok(elapsed < 2000, `Should not wait much longer than 500ms, waited ${elapsed}ms`);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
// ==========================================
// Round 51: hooks.json Schema Validation
// ==========================================
console.log('\nRound 51: hooks.json Schema Validation:');
if (await asyncTest('hooks.json async hook has valid timeout field', async () => {
const asyncHook = hooks.hooks.PostToolUse.find(h =>
h.hooks && h.hooks[0] && h.hooks[0].async === true
);
assert.ok(asyncHook, 'Should have at least one async hook defined');
assert.strictEqual(asyncHook.hooks[0].async, true, 'async field should be true');
assert.ok(asyncHook.hooks[0].timeout, 'Should have timeout field');
assert.strictEqual(typeof asyncHook.hooks[0].timeout, 'number', 'Timeout should be a number');
assert.ok(asyncHook.hooks[0].timeout > 0, 'Timeout should be positive');
const command = asyncHook.hooks[0].command;
const isNodeInline = command.startsWith('node -e');
const isNodeScript = command.startsWith('node "');
const isShellWrapper =
command.startsWith('bash "') ||
command.startsWith('sh "') ||
command.startsWith('bash -lc ') ||
command.startsWith('sh -c ');
assert.ok(
isNodeInline || isNodeScript || isShellWrapper,
`Async hook command should be runnable (node -e, node script, or shell wrapper), got: ${command.substring(0, 80)}`
);
})) passed++; else failed++;
if (await asyncTest('all hook commands in hooks.json are valid format', async () => {
for (const [hookType, hookArray] of Object.entries(hooks.hooks)) {
for (const hookDef of hookArray) {
assert.ok(hookDef.hooks, `${hookType} entry should have hooks array`);
for (const hook of hookDef.hooks) {
assert.ok(hook.command, `Hook in ${hookType} should have command field`);
const isInline = hook.command.startsWith('node -e');
const isFilePath = hook.command.startsWith('node "');
const isNpx = hook.command.startsWith('npx ');
const isShellWrapper =
hook.command.startsWith('bash "') ||
hook.command.startsWith('sh "') ||
hook.command.startsWith('bash -lc ') ||
hook.command.startsWith('sh -c ');
const isShellScriptPath = hook.command.endsWith('.sh');
assert.ok(
isInline || isFilePath || isNpx || isShellWrapper || isShellScriptPath,
`Hook command in ${hookType} should be node -e, node script, npx, or shell wrapper/script, got: ${hook.command.substring(0, 80)}`
);
}
}
}
})) passed++; else failed++;
// Summary
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}\n`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,271 @@
/**
* Tests for scripts/lib/agent-compress.js
*
* Run with: node tests/lib/agent-compress.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
parseFrontmatter,
extractSummary,
loadAgent,
loadAgents,
compressToCatalog,
compressToSummary,
buildAgentCatalog,
lazyLoadAgent,
} = require('../../scripts/lib/agent-compress');
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 agent-compress ===\n');
let passed = 0;
let failed = 0;
// --- parseFrontmatter ---
if (test('parseFrontmatter extracts YAML frontmatter and body', () => {
const content = '---\nname: test-agent\ndescription: A test\ntools: ["Read", "Grep"]\nmodel: sonnet\n---\n\nBody text here.';
const { frontmatter, body } = parseFrontmatter(content);
assert.strictEqual(frontmatter.name, 'test-agent');
assert.strictEqual(frontmatter.description, 'A test');
assert.deepStrictEqual(frontmatter.tools, ['Read', 'Grep']);
assert.strictEqual(frontmatter.model, 'sonnet');
assert.ok(body.includes('Body text here.'));
})) passed++; else failed++;
if (test('parseFrontmatter handles content without frontmatter', () => {
const content = 'Just a regular markdown file.';
const { frontmatter, body } = parseFrontmatter(content);
assert.deepStrictEqual(frontmatter, {});
assert.strictEqual(body, content);
})) passed++; else failed++;
if (test('parseFrontmatter handles colons in values', () => {
const content = '---\nname: test\ndescription: Use this: it works\n---\n\nBody.';
const { frontmatter } = parseFrontmatter(content);
assert.strictEqual(frontmatter.description, 'Use this: it works');
})) passed++; else failed++;
if (test('parseFrontmatter strips surrounding quotes', () => {
const content = '---\nname: "quoted-name"\n---\n\nBody.';
const { frontmatter } = parseFrontmatter(content);
assert.strictEqual(frontmatter.name, 'quoted-name');
})) passed++; else failed++;
if (test('parseFrontmatter handles content ending right after closing ---', () => {
const content = '---\nname: test\ndescription: No body\n---';
const { frontmatter, body } = parseFrontmatter(content);
assert.strictEqual(frontmatter.name, 'test');
assert.strictEqual(frontmatter.description, 'No body');
assert.strictEqual(body, '');
})) passed++; else failed++;
// --- extractSummary ---
if (test('extractSummary returns the first paragraph of the body', () => {
const body = '# Heading\n\nThis is the first paragraph. It has two sentences.\n\nSecond paragraph.';
const summary = extractSummary(body);
assert.strictEqual(summary, 'This is the first paragraph.');
})) passed++; else failed++;
if (test('extractSummary returns empty string for empty body', () => {
assert.strictEqual(extractSummary(''), '');
assert.strictEqual(extractSummary('# Only Headings\n\n## Another'), '');
})) passed++; else failed++;
if (test('extractSummary skips code blocks', () => {
const body = '```\ncode here\n```\n\nActual summary sentence.';
const summary = extractSummary(body);
assert.strictEqual(summary, 'Actual summary sentence.');
})) passed++; else failed++;
if (test('extractSummary respects maxSentences', () => {
const body = 'First sentence. Second sentence. Third sentence.';
const one = extractSummary(body, 1);
const two = extractSummary(body, 2);
assert.strictEqual(one, 'First sentence.');
assert.strictEqual(two, 'First sentence. Second sentence.');
})) passed++; else failed++;
if (test('extractSummary skips plain bullet items', () => {
const body = '- plain bullet\n- another bullet\n\nActual paragraph here.';
const summary = extractSummary(body);
assert.strictEqual(summary, 'Actual paragraph here.');
})) passed++; else failed++;
if (test('extractSummary skips asterisk bullets and numbered lists', () => {
const body = '* star bullet\n1. numbered item\n2. second item\n\nReal paragraph.';
const summary = extractSummary(body);
assert.strictEqual(summary, 'Real paragraph.');
})) passed++; else failed++;
// --- loadAgent / loadAgents ---
// Create a temp directory with test agent files
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-compress-test-'));
const agentContent = '---\nname: test-agent\ndescription: A test agent\ntools: ["Read"]\nmodel: haiku\n---\n\nTest agent body paragraph.\n\n## Details\nMore info.';
fs.writeFileSync(path.join(tmpDir, 'test-agent.md'), agentContent);
fs.writeFileSync(path.join(tmpDir, 'not-an-agent.txt'), 'ignored');
if (test('loadAgent reads and parses a single agent file', () => {
const agent = loadAgent(path.join(tmpDir, 'test-agent.md'));
assert.strictEqual(agent.name, 'test-agent');
assert.strictEqual(agent.description, 'A test agent');
assert.deepStrictEqual(agent.tools, ['Read']);
assert.strictEqual(agent.model, 'haiku');
assert.ok(agent.body.includes('Test agent body paragraph'));
assert.strictEqual(agent.fileName, 'test-agent');
assert.ok(agent.byteSize > 0);
})) passed++; else failed++;
if (test('loadAgents reads all .md files from a directory', () => {
const agents = loadAgents(tmpDir);
assert.strictEqual(agents.length, 1);
assert.strictEqual(agents[0].name, 'test-agent');
})) passed++; else failed++;
if (test('loadAgents returns empty array for non-existent directory', () => {
const agents = loadAgents(path.join(os.tmpdir(), 'does-not-exist-agent-compress-test'));
assert.deepStrictEqual(agents, []);
})) passed++; else failed++;
// --- compressToCatalog / compressToSummary ---
const sampleAgent = loadAgent(path.join(tmpDir, 'test-agent.md'));
if (test('compressToCatalog strips body and keeps only metadata', () => {
const catalog = compressToCatalog(sampleAgent);
assert.strictEqual(catalog.name, 'test-agent');
assert.strictEqual(catalog.description, 'A test agent');
assert.deepStrictEqual(catalog.tools, ['Read']);
assert.strictEqual(catalog.model, 'haiku');
assert.strictEqual(catalog.body, undefined);
assert.strictEqual(catalog.byteSize, undefined);
})) passed++; else failed++;
if (test('compressToSummary includes first paragraph summary', () => {
const summary = compressToSummary(sampleAgent);
assert.strictEqual(summary.name, 'test-agent');
assert.ok(summary.summary.includes('Test agent body paragraph'));
assert.strictEqual(summary.body, undefined);
})) passed++; else failed++;
// --- buildAgentCatalog ---
if (test('buildAgentCatalog in catalog mode produces minimal output with stats', () => {
const result = buildAgentCatalog(tmpDir, { mode: 'catalog' });
assert.strictEqual(result.agents.length, 1);
assert.strictEqual(result.agents[0].body, undefined);
assert.strictEqual(result.stats.totalAgents, 1);
assert.strictEqual(result.stats.mode, 'catalog');
assert.ok(result.stats.originalBytes > 0);
assert.ok(result.stats.compressedBytes < result.stats.originalBytes);
assert.ok(result.stats.compressedTokenEstimate > 0);
})) passed++; else failed++;
if (test('buildAgentCatalog in summary mode includes summaries', () => {
const result = buildAgentCatalog(tmpDir, { mode: 'summary' });
assert.ok(result.agents[0].summary);
assert.strictEqual(result.agents[0].body, undefined);
})) passed++; else failed++;
if (test('buildAgentCatalog in full mode preserves body', () => {
const result = buildAgentCatalog(tmpDir, { mode: 'full' });
assert.ok(result.agents[0].body);
})) passed++; else failed++;
if (test('buildAgentCatalog throws on invalid mode', () => {
assert.throws(
() => buildAgentCatalog(tmpDir, { mode: 'invalid' }),
/Invalid mode "invalid"/
);
})) passed++; else failed++;
if (test('buildAgentCatalog supports filter function', () => {
// Add a second agent
fs.writeFileSync(
path.join(tmpDir, 'other-agent.md'),
'---\nname: other\ndescription: Other agent\ntools: ["Bash"]\nmodel: opus\n---\n\nOther body.'
);
const result = buildAgentCatalog(tmpDir, {
filter: a => a.model === 'opus',
});
assert.strictEqual(result.agents.length, 1);
assert.strictEqual(result.agents[0].name, 'other');
// Clean up
fs.unlinkSync(path.join(tmpDir, 'other-agent.md'));
})) passed++; else failed++;
// --- lazyLoadAgent ---
if (test('lazyLoadAgent loads a single agent by name', () => {
const agent = lazyLoadAgent(tmpDir, 'test-agent');
assert.ok(agent);
assert.strictEqual(agent.name, 'test-agent');
assert.ok(agent.body.includes('Test agent body paragraph'));
})) passed++; else failed++;
if (test('lazyLoadAgent returns null for non-existent agent', () => {
const agent = lazyLoadAgent(tmpDir, 'does-not-exist');
assert.strictEqual(agent, null);
})) passed++; else failed++;
if (test('lazyLoadAgent rejects path traversal attempts', () => {
const agent = lazyLoadAgent(tmpDir, '../etc/passwd');
assert.strictEqual(agent, null);
})) passed++; else failed++;
if (test('lazyLoadAgent rejects names with invalid characters', () => {
const agent = lazyLoadAgent(tmpDir, 'foo/bar');
assert.strictEqual(agent, null);
const agent2 = lazyLoadAgent(tmpDir, 'foo bar');
assert.strictEqual(agent2, null);
})) passed++; else failed++;
// --- Real agents directory ---
const realAgentsDir = path.resolve(__dirname, '../../agents');
if (test('buildAgentCatalog works with real agents directory', () => {
if (!fs.existsSync(realAgentsDir)) return; // skip if not present
const result = buildAgentCatalog(realAgentsDir, { mode: 'catalog' });
assert.ok(result.agents.length > 0, 'Should find at least one agent');
assert.ok(result.stats.compressedBytes < result.stats.originalBytes, 'Catalog should be smaller than original');
// Verify significant compression ratio
const ratio = result.stats.compressedBytes / result.stats.originalBytes;
assert.ok(ratio < 0.5, `Compression ratio ${ratio.toFixed(2)} should be < 0.5`);
})) passed++; else failed++;
if (test('catalog mode token estimate is under 5000 for real agents', () => {
if (!fs.existsSync(realAgentsDir)) return;
const result = buildAgentCatalog(realAgentsDir, { mode: 'catalog' });
assert.ok(
result.stats.compressedTokenEstimate < 5000,
`Token estimate ${result.stats.compressedTokenEstimate} exceeds 5000`
);
})) passed++; else failed++;
// Cleanup
fs.rmSync(tmpDir, { recursive: true, force: true });
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,232 @@
/**
* Tests for inspection logic — pattern detection from failures.
*/
const assert = require('assert');
const {
normalizeFailureReason,
groupFailures,
detectPatterns,
generateReport,
suggestAction,
DEFAULT_FAILURE_THRESHOLD,
} = require('../../scripts/lib/inspection');
async function test(name, fn) {
try {
await fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function makeSkillRun(overrides = {}) {
return {
id: overrides.id || `run-${Math.random().toString(36).slice(2, 8)}`,
skillId: overrides.skillId || 'test-skill',
skillVersion: overrides.skillVersion || '1.0.0',
sessionId: overrides.sessionId || 'session-1',
taskDescription: overrides.taskDescription || 'test task',
outcome: overrides.outcome || 'failure',
failureReason: overrides.failureReason || 'generic error',
tokensUsed: overrides.tokensUsed || 500,
durationMs: overrides.durationMs || 1000,
userFeedback: overrides.userFeedback || null,
createdAt: overrides.createdAt || '2026-03-15T08:00:00.000Z',
};
}
async function runTests() {
console.log('\n=== Testing inspection ===\n');
let passed = 0;
let failed = 0;
if (await test('normalizeFailureReason strips timestamps and UUIDs', async () => {
const normalized = normalizeFailureReason(
'Error at 2026-03-15T08:00:00.000Z for id 550e8400-e29b-41d4-a716-446655440000'
);
assert.ok(!normalized.includes('2026'));
assert.ok(!normalized.includes('550e8400'));
assert.ok(normalized.includes('<timestamp>'));
assert.ok(normalized.includes('<uuid>'));
})) passed += 1; else failed += 1;
if (await test('normalizeFailureReason strips file paths', async () => {
const normalized = normalizeFailureReason('File not found: /usr/local/bin/node');
assert.ok(!normalized.includes('/usr/local'));
assert.ok(normalized.includes('<path>'));
})) passed += 1; else failed += 1;
if (await test('normalizeFailureReason handles null and empty values', async () => {
assert.strictEqual(normalizeFailureReason(null), 'unknown');
assert.strictEqual(normalizeFailureReason(''), 'unknown');
assert.strictEqual(normalizeFailureReason(undefined), 'unknown');
})) passed += 1; else failed += 1;
if (await test('groupFailures groups by skillId and normalized reason', async () => {
const runs = [
makeSkillRun({ id: 'r1', skillId: 'skill-a', failureReason: 'timeout' }),
makeSkillRun({ id: 'r2', skillId: 'skill-a', failureReason: 'timeout' }),
makeSkillRun({ id: 'r3', skillId: 'skill-b', failureReason: 'parse error' }),
makeSkillRun({ id: 'r4', skillId: 'skill-a', outcome: 'success' }), // should be excluded
];
const groups = groupFailures(runs);
assert.strictEqual(groups.size, 2);
const skillAGroup = groups.get('skill-a::timeout');
assert.ok(skillAGroup);
assert.strictEqual(skillAGroup.runs.length, 2);
const skillBGroup = groups.get('skill-b::parse error');
assert.ok(skillBGroup);
assert.strictEqual(skillBGroup.runs.length, 1);
})) passed += 1; else failed += 1;
if (await test('groupFailures handles mixed outcome casing', async () => {
const runs = [
makeSkillRun({ id: 'r1', outcome: 'FAILURE', failureReason: 'timeout' }),
makeSkillRun({ id: 'r2', outcome: 'Failed', failureReason: 'timeout' }),
makeSkillRun({ id: 'r3', outcome: 'error', failureReason: 'timeout' }),
];
const groups = groupFailures(runs);
assert.strictEqual(groups.size, 1);
const group = groups.values().next().value;
assert.strictEqual(group.runs.length, 3);
})) passed += 1; else failed += 1;
if (await test('detectPatterns returns empty array when below threshold', async () => {
const runs = [
makeSkillRun({ id: 'r1', failureReason: 'timeout' }),
makeSkillRun({ id: 'r2', failureReason: 'timeout' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 0);
})) passed += 1; else failed += 1;
if (await test('detectPatterns detects patterns at or above threshold', async () => {
const runs = [
makeSkillRun({ id: 'r1', failureReason: 'timeout', createdAt: '2026-03-15T08:00:00Z' }),
makeSkillRun({ id: 'r2', failureReason: 'timeout', createdAt: '2026-03-15T08:01:00Z' }),
makeSkillRun({ id: 'r3', failureReason: 'timeout', createdAt: '2026-03-15T08:02:00Z' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 1);
assert.strictEqual(patterns[0].count, 3);
assert.strictEqual(patterns[0].skillId, 'test-skill');
assert.strictEqual(patterns[0].normalizedReason, 'timeout');
assert.strictEqual(patterns[0].firstSeen, '2026-03-15T08:00:00Z');
assert.strictEqual(patterns[0].lastSeen, '2026-03-15T08:02:00Z');
assert.strictEqual(patterns[0].runIds.length, 3);
})) passed += 1; else failed += 1;
if (await test('detectPatterns uses default threshold', async () => {
const runs = Array.from({ length: DEFAULT_FAILURE_THRESHOLD }, (_, i) =>
makeSkillRun({ id: `r${i}`, failureReason: 'permission denied' })
);
const patterns = detectPatterns(runs);
assert.strictEqual(patterns.length, 1);
})) passed += 1; else failed += 1;
if (await test('detectPatterns sorts by count descending', async () => {
const runs = [
// 4 timeouts
...Array.from({ length: 4 }, (_, i) =>
makeSkillRun({ id: `t${i}`, skillId: 'skill-a', failureReason: 'timeout' })
),
// 3 parse errors
...Array.from({ length: 3 }, (_, i) =>
makeSkillRun({ id: `p${i}`, skillId: 'skill-b', failureReason: 'parse error' })
),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 2);
assert.strictEqual(patterns[0].count, 4);
assert.strictEqual(patterns[0].skillId, 'skill-a');
assert.strictEqual(patterns[1].count, 3);
assert.strictEqual(patterns[1].skillId, 'skill-b');
})) passed += 1; else failed += 1;
if (await test('detectPatterns groups similar failure reasons with different timestamps', async () => {
const runs = [
makeSkillRun({ id: 'r1', failureReason: 'Error at 2026-03-15T08:00:00Z in /tmp/foo' }),
makeSkillRun({ id: 'r2', failureReason: 'Error at 2026-03-15T09:00:00Z in /tmp/bar' }),
makeSkillRun({ id: 'r3', failureReason: 'Error at 2026-03-15T10:00:00Z in /tmp/baz' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 1);
assert.ok(patterns[0].normalizedReason.includes('<timestamp>'));
assert.ok(patterns[0].normalizedReason.includes('<path>'));
})) passed += 1; else failed += 1;
if (await test('detectPatterns tracks unique session IDs and versions', async () => {
const runs = [
makeSkillRun({ id: 'r1', sessionId: 'sess-1', skillVersion: '1.0.0', failureReason: 'err' }),
makeSkillRun({ id: 'r2', sessionId: 'sess-2', skillVersion: '1.0.0', failureReason: 'err' }),
makeSkillRun({ id: 'r3', sessionId: 'sess-1', skillVersion: '1.1.0', failureReason: 'err' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 1);
assert.deepStrictEqual(patterns[0].sessionIds.sort(), ['sess-1', 'sess-2']);
assert.deepStrictEqual(patterns[0].versions.sort(), ['1.0.0', '1.1.0']);
})) passed += 1; else failed += 1;
if (await test('generateReport returns clean status with no patterns', async () => {
const report = generateReport([]);
assert.strictEqual(report.status, 'clean');
assert.strictEqual(report.patternCount, 0);
assert.ok(report.summary.includes('No recurring'));
assert.ok(report.generatedAt);
})) passed += 1; else failed += 1;
if (await test('generateReport produces structured report from patterns', async () => {
const runs = [
...Array.from({ length: 3 }, (_, i) =>
makeSkillRun({ id: `r${i}`, skillId: 'my-skill', failureReason: 'timeout' })
),
];
const patterns = detectPatterns(runs, { threshold: 3 });
const report = generateReport(patterns, { generatedAt: '2026-03-15T09:00:00Z' });
assert.strictEqual(report.status, 'attention_needed');
assert.strictEqual(report.patternCount, 1);
assert.strictEqual(report.totalFailures, 3);
assert.deepStrictEqual(report.affectedSkills, ['my-skill']);
assert.strictEqual(report.patterns[0].skillId, 'my-skill');
assert.ok(report.patterns[0].suggestedAction);
assert.strictEqual(report.generatedAt, '2026-03-15T09:00:00Z');
})) passed += 1; else failed += 1;
if (await test('suggestAction returns timeout-specific advice', async () => {
const action = suggestAction({ normalizedReason: 'timeout after 30s', versions: ['1.0.0'] });
assert.ok(action.toLowerCase().includes('timeout'));
})) passed += 1; else failed += 1;
if (await test('suggestAction returns permission-specific advice', async () => {
const action = suggestAction({ normalizedReason: 'permission denied', versions: ['1.0.0'] });
assert.ok(action.toLowerCase().includes('permission'));
})) passed += 1; else failed += 1;
if (await test('suggestAction returns version-span advice when multiple versions affected', async () => {
const action = suggestAction({ normalizedReason: 'something broke', versions: ['1.0.0', '1.1.0'] });
assert.ok(action.toLowerCase().includes('version'));
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,144 @@
/**
* Tests for scripts/lib/install/config.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
findDefaultInstallConfigPath,
loadInstallConfig,
resolveInstallConfigPath,
} = require('../../scripts/lib/install/config');
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 writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
}
function runTests() {
console.log('\n=== Testing install/config.js ===\n');
let passed = 0;
let failed = 0;
if (test('resolves relative config paths from the provided cwd', () => {
const cwd = '/workspace/app';
const resolved = resolveInstallConfigPath('configs/ecc-install.json', { cwd });
assert.strictEqual(resolved, path.join(cwd, 'configs', 'ecc-install.json'));
})) passed++; else failed++;
if (test('finds the default project install config in the provided cwd', () => {
const cwd = createTempDir('install-config-');
try {
const configPath = path.join(cwd, 'ecc-install.json');
writeJson(configPath, {
version: 1,
profile: 'core',
});
assert.strictEqual(findDefaultInstallConfigPath({ cwd }), configPath);
} finally {
cleanup(cwd);
}
})) passed++; else failed++;
if (test('returns null when no default project install config exists', () => {
const cwd = createTempDir('install-config-');
try {
assert.strictEqual(findDefaultInstallConfigPath({ cwd }), null);
} finally {
cleanup(cwd);
}
})) passed++; else failed++;
if (test('loads and normalizes a valid install config', () => {
const cwd = createTempDir('install-config-');
try {
const configPath = path.join(cwd, 'ecc-install.json');
writeJson(configPath, {
version: 1,
target: 'cursor',
profile: 'developer',
modules: ['platform-configs', 'platform-configs'],
include: ['lang:typescript', 'framework:nextjs', 'lang:typescript'],
exclude: ['capability:media'],
options: {
includeExamples: false,
},
});
const config = loadInstallConfig('ecc-install.json', { cwd });
assert.strictEqual(config.path, configPath);
assert.strictEqual(config.target, 'cursor');
assert.strictEqual(config.profileId, 'developer');
assert.deepStrictEqual(config.moduleIds, ['platform-configs']);
assert.deepStrictEqual(config.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(config.excludeComponentIds, ['capability:media']);
assert.deepStrictEqual(config.options, { includeExamples: false });
} finally {
cleanup(cwd);
}
})) passed++; else failed++;
if (test('rejects invalid config schema values', () => {
const cwd = createTempDir('install-config-');
try {
writeJson(path.join(cwd, 'ecc-install.json'), {
version: 2,
target: 'ghost-target',
});
assert.throws(
() => loadInstallConfig('ecc-install.json', { cwd }),
/Invalid install config/
);
} finally {
cleanup(cwd);
}
})) passed++; else failed++;
if (test('fails when the install config does not exist', () => {
const cwd = createTempDir('install-config-');
try {
assert.throws(
() => loadInstallConfig('ecc-install.json', { cwd }),
/Install config not found/
);
} finally {
cleanup(cwd);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,738 @@
/**
* Tests for scripts/lib/install-lifecycle.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
buildDoctorReport,
discoverInstalledStates,
repairInstalledStates,
uninstallInstalledStates,
} = require('../../scripts/lib/install-lifecycle');
const {
createInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
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;
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 writeState(filePath, options) {
const state = createInstallState(options);
writeInstallState(filePath, state);
return state;
}
function runTests() {
console.log('\n=== Testing install-lifecycle.js ===\n');
let passed = 0;
let failed = 0;
if (test('discovers installed states for multiple targets in the current context', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const claudeStatePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json');
const cursorStatePath = path.join(projectRoot, '.cursor', 'ecc-install-state.json');
writeState(claudeStatePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot: path.join(homeDir, '.claude'),
installStatePath: claudeStatePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-claude-rules'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
writeState(cursorStatePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: path.join(projectRoot, '.cursor'),
installStatePath: cursorStatePath,
request: {
profile: 'core',
modules: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core', 'platform-configs'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'def456',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const records = discoverInstalledStates({
homeDir,
projectRoot,
targets: ['claude', 'cursor'],
});
assert.strictEqual(records.length, 2);
assert.strictEqual(records[0].exists, true);
assert.strictEqual(records[1].exists, true);
assert.strictEqual(records[0].state.target.id, 'claude-home');
assert.strictEqual(records[1].state.target.id, 'cursor-project');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports missing managed files as an error', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-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 report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'error');
assert.ok(report.results[0].issues.some(issue => issue.code === 'missing-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports a healthy legacy install when managed files are present', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-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 report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'ok');
assert.strictEqual(report.results[0].issues.length, 0);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports drifted managed files as a warning', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const sourcePath = path.join(REPO_ROOT, '.cursor', 'hooks.json');
const destinationPath = path.join(targetRoot, 'hooks.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, '{"drifted":true}\n');
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',
sourcePath,
sourceRelativePath: '.cursor/hooks.json',
destinationPath,
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'warning');
assert.ok(report.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports manifest resolution drift for non-legacy installs', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-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: 'core',
modules: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'warning');
assert.ok(report.results[0].issues.some(issue => issue.code === 'resolution-drift'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair restores render-template outputs from recorded rendered content', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(homeDir, '.claude');
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
const destinationPath = path.join(targetRoot, 'plugin.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, '{"drifted":true}\n');
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: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.claude-plugin/plugin.json.template',
destinationPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '{"ok":true}\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), '{"ok":true}\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair reapplies merge-json operations without clobbering unrelated keys', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'hooks.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, JSON.stringify({
existing: true,
nested: {
enabled: false,
},
}, null, 2));
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath,
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
nested: {
enabled: true,
},
managed: 'yes',
},
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
existing: true,
nested: {
enabled: true,
},
managed: 'yes',
});
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair re-applies managed remove operations when files reappear', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'legacy-note.txt');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, 'stale');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'remove',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/legacy-note.txt',
destinationPath,
strategy: 'remove',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall restores JSON merged files from recorded previous content', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'hooks.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, JSON.stringify({
existing: true,
managed: true,
}, null, 2));
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath,
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
managed: true,
},
previousContent: JSON.stringify({
existing: true,
}, null, 2),
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
existing: true,
});
assert.ok(!fs.existsSync(statePath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall restores rendered template files from recorded previous content', () => {
const tempDir = createTempDir('install-lifecycle-');
try {
const targetRoot = path.join(tempDir, '.claude');
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
const destinationPath = path.join(targetRoot, 'plugin.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, '{"generated":true}\n');
writeInstallState(statePath, createInstallState({
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot,
installStatePath: statePath,
request: {
profile: 'core',
modules: ['platform-configs'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
source: {
repoVersion: '1.8.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
operations: [
{
kind: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.claude/plugin.json.template',
destinationPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '{"generated":true}\n',
previousContent: '{"existing":true}\n',
},
],
}));
const result = uninstallInstalledStates({
homeDir: tempDir,
projectRoot: tempDir,
targets: ['claude'],
});
assert.strictEqual(result.summary.uninstalledCount, 1);
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), '{"existing":true}\n');
assert.ok(!fs.existsSync(statePath));
} finally {
cleanup(tempDir);
}
})) passed++; else failed++;
if (test('uninstall restores files removed during install when previous content is recorded', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'legacy-note.txt');
fs.mkdirSync(targetRoot, { recursive: true });
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'remove',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/legacy-note.txt',
destinationPath,
strategy: 'remove',
ownership: 'managed',
scaffoldOnly: false,
previousContent: 'restore me\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), 'restore me\n');
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();

View File

@@ -0,0 +1,302 @@
/**
* Tests for scripts/lib/install-manifests.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
loadInstallManifests,
listInstallComponents,
listLegacyCompatibilityLanguages,
listInstallModules,
listInstallProfiles,
resolveInstallPlan,
resolveLegacyCompatibilitySelection,
validateInstallModuleIds,
} = require('../../scripts/lib/install-manifests');
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 createTestRepo() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'install-manifests-'));
fs.mkdirSync(path.join(root, 'manifests'), { recursive: true });
return root;
}
function cleanupTestRepo(root) {
fs.rmSync(root, { recursive: true, force: true });
}
function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
}
function runTests() {
console.log('\n=== Testing install-manifests.js ===\n');
let passed = 0;
let failed = 0;
if (test('loads real project install manifests', () => {
const manifests = loadInstallManifests();
assert.ok(manifests.modules.length >= 1, 'Should load modules');
assert.ok(Object.keys(manifests.profiles).length >= 1, 'Should load profiles');
assert.ok(manifests.components.length >= 1, 'Should load components');
})) passed++; else failed++;
if (test('lists install profiles from the real project', () => {
const profiles = listInstallProfiles();
assert.ok(profiles.some(profile => profile.id === 'core'), 'Should include core profile');
assert.ok(profiles.some(profile => profile.id === 'full'), 'Should include full profile');
})) passed++; else failed++;
if (test('lists install modules from the real project', () => {
const modules = listInstallModules();
assert.ok(modules.some(module => module.id === 'rules-core'), 'Should include rules-core');
assert.ok(modules.some(module => module.id === 'orchestration'), 'Should include orchestration');
})) passed++; else failed++;
if (test('lists install components from the real project', () => {
const components = listInstallComponents();
assert.ok(components.some(component => component.id === 'lang:typescript'),
'Should include lang:typescript');
assert.ok(components.some(component => component.id === 'capability:security'),
'Should include capability:security');
})) passed++; else failed++;
if (test('lists supported legacy compatibility languages', () => {
const languages = listLegacyCompatibilityLanguages();
assert.ok(languages.includes('typescript'));
assert.ok(languages.includes('python'));
assert.ok(languages.includes('go'));
assert.ok(languages.includes('golang'));
assert.ok(languages.includes('kotlin'));
assert.ok(languages.includes('rust'));
assert.ok(languages.includes('cpp'));
assert.ok(languages.includes('csharp'));
})) passed++; else failed++;
if (test('resolves a real project profile with target-specific skips', () => {
const projectRoot = '/workspace/app';
const plan = resolveInstallPlan({ profileId: 'developer', target: 'cursor', projectRoot });
assert.ok(plan.selectedModuleIds.includes('rules-core'), 'Should keep rules-core');
assert.ok(plan.selectedModuleIds.includes('commands-core'), 'Should keep commands-core');
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should not select unsupported orchestration module for cursor');
assert.ok(plan.skippedModuleIds.includes('orchestration'),
'Should report unsupported orchestration module as skipped');
assert.strictEqual(plan.targetAdapterId, 'cursor-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.cursor'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
assert.ok(plan.operations.length > 0, 'Should include scaffold operations');
assert.ok(
plan.operations.some(operation => (
operation.sourceRelativePath === '.cursor'
&& operation.strategy === 'sync-root-children'
)),
'Should flatten the native cursor root'
);
})) passed++; else failed++;
if (test('resolves antigravity profiles while skipping only unsupported modules', () => {
const projectRoot = '/workspace/app';
const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot });
assert.deepStrictEqual(
plan.selectedModuleIds,
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
);
assert.ok(plan.skippedModuleIds.includes('hooks-runtime'));
assert.ok(!plan.skippedModuleIds.includes('platform-configs'));
assert.ok(!plan.skippedModuleIds.includes('workflow-quality'));
assert.strictEqual(plan.targetAdapterId, 'antigravity-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent'));
})) passed++; else failed++;
if (test('resolves explicit modules with dependency expansion', () => {
const plan = resolveInstallPlan({ moduleIds: ['security'] });
assert.ok(plan.selectedModuleIds.includes('security'), 'Should include requested module');
assert.ok(plan.selectedModuleIds.includes('workflow-quality'),
'Should include transitive dependency');
assert.ok(plan.selectedModuleIds.includes('platform-configs'),
'Should include nested dependency');
})) passed++; else failed++;
if (test('validates explicit module IDs against the real manifest catalog', () => {
const moduleIds = validateInstallModuleIds(['security', 'security', 'platform-configs']);
assert.deepStrictEqual(moduleIds, ['security', 'platform-configs']);
assert.throws(
() => validateInstallModuleIds(['ghost-module']),
/Unknown install module: ghost-module/
);
})) passed++; else failed++;
if (test('resolves legacy compatibility selections into manifest module IDs', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['typescript', 'go', 'golang'],
});
assert.deepStrictEqual(selection.legacyLanguages, ['typescript', 'go', 'golang']);
assert.ok(selection.moduleIds.includes('rules-core'));
assert.ok(selection.moduleIds.includes('agents-core'));
assert.ok(selection.moduleIds.includes('commands-core'));
assert.ok(selection.moduleIds.includes('hooks-runtime'));
assert.ok(selection.moduleIds.includes('platform-configs'));
assert.ok(selection.moduleIds.includes('workflow-quality'));
assert.ok(selection.moduleIds.includes('framework-language'));
})) passed++; else failed++;
if (test('resolves rust legacy compatibility into framework-language module', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['rust'],
});
assert.ok(selection.moduleIds.includes('rules-core'));
assert.ok(selection.moduleIds.includes('framework-language'),
'rust should resolve to framework-language module');
})) passed++; else failed++;
if (test('resolves cpp legacy compatibility into framework-language module', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['cpp'],
});
assert.ok(selection.moduleIds.includes('rules-core'));
assert.ok(selection.moduleIds.includes('framework-language'),
'cpp should resolve to framework-language module');
})) passed++; else failed++;
if (test('resolves csharp legacy compatibility into framework-language module', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['csharp'],
});
assert.ok(selection.moduleIds.includes('rules-core'));
assert.ok(selection.moduleIds.includes('framework-language'),
'csharp should resolve to framework-language module');
})) passed++; else failed++;
if (test('keeps antigravity legacy compatibility selections target-safe', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'antigravity',
legacyLanguages: ['typescript'],
});
assert.deepStrictEqual(selection.moduleIds, ['rules-core', 'agents-core', 'commands-core']);
})) passed++; else failed++;
if (test('rejects unknown legacy compatibility languages', () => {
assert.throws(
() => resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['brainfuck'],
}),
/Unknown legacy language: brainfuck/
);
})) passed++; else failed++;
if (test('resolves included and excluded user-facing components', () => {
const plan = resolveInstallPlan({
profileId: 'core',
includeComponentIds: ['capability:security'],
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.deepStrictEqual(plan.includedComponentIds, ['capability:security']);
assert.deepStrictEqual(plan.excludedComponentIds, ['capability:orchestration']);
assert.ok(plan.selectedModuleIds.includes('security'), 'Should include modules from selected components');
assert.ok(!plan.selectedModuleIds.includes('orchestration'), 'Should exclude modules from excluded components');
assert.ok(plan.excludedModuleIds.includes('orchestration'),
'Should report modules removed by excluded components');
})) passed++; else failed++;
if (test('fails when a selected component depends on an excluded component module', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['capability:social'],
excludeComponentIds: ['capability:content'],
}),
/depends on excluded module business-content/
);
})) passed++; else failed++;
if (test('throws on unknown install profile', () => {
assert.throws(
() => resolveInstallPlan({ profileId: 'ghost-profile' }),
/Unknown install profile/
);
})) passed++; else failed++;
if (test('throws on unknown install target', () => {
assert.throws(
() => resolveInstallPlan({ profileId: 'core', target: 'not-a-target' }),
/Unknown install target/
);
})) passed++; else failed++;
if (test('skips a requested module when its dependency chain does not support the target', () => {
const repoRoot = createTestRepo();
writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), {
version: 1,
modules: [
{
id: 'parent',
kind: 'skills',
description: 'Parent',
paths: ['parent'],
targets: ['claude'],
dependencies: ['child'],
defaultInstall: false,
cost: 'light',
stability: 'stable'
},
{
id: 'child',
kind: 'skills',
description: 'Child',
paths: ['child'],
targets: ['cursor'],
dependencies: [],
defaultInstall: false,
cost: 'light',
stability: 'stable'
}
]
});
writeJson(path.join(repoRoot, 'manifests', 'install-profiles.json'), {
version: 1,
profiles: {
core: { description: 'Core', modules: ['parent'] }
}
});
const plan = resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' });
assert.deepStrictEqual(plan.selectedModuleIds, []);
assert.deepStrictEqual(plan.skippedModuleIds, ['parent']);
cleanupTestRepo(repoRoot);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,163 @@
/**
* Tests for scripts/lib/install/request.js
*/
const assert = require('assert');
const {
normalizeInstallRequest,
parseInstallArgs,
} = require('../../scripts/lib/install/request');
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/request.js ===\n');
let passed = 0;
let failed = 0;
if (test('parses manifest-mode CLI arguments', () => {
const parsed = parseInstallArgs([
'node',
'scripts/install-apply.js',
'--target', 'cursor',
'--profile', 'developer',
'--modules', 'platform-configs, workflow-quality ,platform-configs',
'--with', 'lang:typescript',
'--without', 'capability:media',
'--config', 'ecc-install.json',
'--dry-run',
'--json'
]);
assert.strictEqual(parsed.target, 'cursor');
assert.strictEqual(parsed.profileId, 'developer');
assert.strictEqual(parsed.configPath, 'ecc-install.json');
assert.deepStrictEqual(parsed.moduleIds, ['platform-configs', 'workflow-quality']);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']);
assert.strictEqual(parsed.dryRun, true);
assert.strictEqual(parsed.json, true);
assert.deepStrictEqual(parsed.languages, []);
})) passed++; else failed++;
if (test('normalizes legacy language installs into a canonical request', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
languages: ['typescript', 'python']
});
assert.strictEqual(request.mode, 'legacy-compat');
assert.strictEqual(request.target, 'claude');
assert.deepStrictEqual(request.legacyLanguages, ['typescript', 'python']);
assert.deepStrictEqual(request.moduleIds, []);
assert.strictEqual(request.profileId, null);
})) passed++; else failed++;
if (test('normalizes manifest installs into a canonical request', () => {
const request = normalizeInstallRequest({
target: 'cursor',
profileId: 'developer',
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: ['capability:media'],
languages: []
});
assert.strictEqual(request.mode, 'manifest');
assert.strictEqual(request.target, 'cursor');
assert.strictEqual(request.profileId, 'developer');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(request.excludeComponentIds, ['capability:media']);
assert.deepStrictEqual(request.legacyLanguages, []);
})) passed++; else failed++;
if (test('merges config-backed component selections with CLI overrides', () => {
const request = normalizeInstallRequest({
target: 'cursor',
profileId: null,
moduleIds: ['platform-configs'],
includeComponentIds: ['framework:nextjs'],
excludeComponentIds: ['capability:media'],
languages: [],
configPath: '/workspace/app/ecc-install.json',
config: {
path: '/workspace/app/ecc-install.json',
target: 'claude',
profileId: 'developer',
moduleIds: ['workflow-quality'],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: ['capability:orchestration'],
},
});
assert.strictEqual(request.mode, 'manifest');
assert.strictEqual(request.target, 'cursor');
assert.strictEqual(request.profileId, 'developer');
assert.deepStrictEqual(request.moduleIds, ['workflow-quality', 'platform-configs']);
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(request.excludeComponentIds, ['capability:orchestration', 'capability:media']);
assert.strictEqual(request.configPath, '/workspace/app/ecc-install.json');
})) passed++; else failed++;
if (test('validates explicit module IDs against the manifest catalog', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'cursor',
profileId: null,
moduleIds: ['ghost-module'],
includeComponentIds: [],
excludeComponentIds: [],
languages: [],
}),
/Unknown install module: ghost-module/
);
})) passed++; else failed++;
if (test('rejects mixing legacy languages with manifest flags', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: 'core',
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: [],
languages: ['typescript']
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('rejects empty install requests when not asking for help', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: [],
languages: [],
help: false
}),
/No install profile, module IDs, included components, or legacy languages/
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,231 @@
/**
* Tests for scripts/lib/install-state.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
createInstallState,
readInstallState,
writeInstallState,
} = 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 createTestDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'install-state-'));
}
function cleanupTestDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function runTests() {
console.log('\n=== Testing install-state.js ===\n');
let passed = 0;
let failed = 0;
if (test('creates a valid install-state payload', () => {
const state = createInstallState({
adapter: { id: 'cursor-project' },
targetRoot: '/repo/.cursor',
installStatePath: '/repo/.cursor/ecc-install-state.json',
request: {
profile: 'developer',
modules: ['orchestration'],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['rules-core', 'orchestration'],
skippedModules: [],
},
operations: [
{
kind: 'copy-path',
moduleId: 'rules-core',
sourceRelativePath: 'rules',
destinationPath: '/repo/.cursor/rules',
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: true,
},
],
source: {
repoVersion: '1.9.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
installedAt: '2026-03-13T00:00:00Z',
});
assert.strictEqual(state.schemaVersion, 'ecc.install.v1');
assert.strictEqual(state.target.id, 'cursor-project');
assert.strictEqual(state.request.profile, 'developer');
assert.strictEqual(state.operations.length, 1);
})) passed++; else failed++;
if (test('writes and reads install-state from disk', () => {
const testDir = createTestDir();
const statePath = path.join(testDir, 'ecc-install-state.json');
try {
const state = createInstallState({
adapter: { id: 'claude-home' },
targetRoot: path.join(testDir, '.claude'),
installStatePath: statePath,
request: {
profile: 'core',
modules: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: '1.9.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
});
writeInstallState(statePath, state);
const loaded = readInstallState(statePath);
assert.strictEqual(loaded.target.id, 'claude-home');
assert.strictEqual(loaded.request.profile, 'core');
assert.deepStrictEqual(loaded.resolution.selectedModules, ['rules-core']);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (test('deep-clones nested operation metadata for lifecycle-managed operations', () => {
const operation = {
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath: '/repo/.cursor/hooks.json',
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
nested: {
enabled: true,
},
},
previousValue: {
nested: {
enabled: false,
},
},
};
const state = createInstallState({
adapter: { id: 'cursor-project' },
targetRoot: '/repo/.cursor',
installStatePath: '/repo/.cursor/ecc-install-state.json',
request: {
profile: null,
modules: ['platform-configs'],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [operation],
source: {
repoVersion: '1.9.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
});
operation.mergePayload.nested.enabled = false;
operation.previousValue.nested.enabled = true;
assert.strictEqual(state.operations[0].mergePayload.nested.enabled, true);
assert.strictEqual(state.operations[0].previousValue.nested.enabled, false);
})) passed++; else failed++;
if (test('rejects invalid install-state payloads on read', () => {
const testDir = createTestDir();
const statePath = path.join(testDir, 'ecc-install-state.json');
try {
fs.writeFileSync(statePath, JSON.stringify({ schemaVersion: 'ecc.install.v1' }, null, 2));
assert.throws(
() => readInstallState(statePath),
/Invalid install-state/
);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (test('rejects unexpected properties and missing required request fields', () => {
const testDir = createTestDir();
const statePath = path.join(testDir, 'ecc-install-state.json');
try {
fs.writeFileSync(statePath, JSON.stringify({
schemaVersion: 'ecc.install.v1',
installedAt: '2026-03-13T00:00:00Z',
unexpected: true,
target: {
id: 'cursor-project',
root: '/repo/.cursor',
installStatePath: '/repo/.cursor/ecc-install-state.json',
},
request: {
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: [],
skippedModules: [],
},
source: {
repoVersion: '1.9.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
operations: [],
}, null, 2));
assert.throws(
() => readInstallState(statePath),
/Invalid install-state/
);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,223 @@
/**
* Tests for scripts/lib/install-targets/registry.js
*/
const assert = require('assert');
const path = require('path');
const {
getInstallTargetAdapter,
listInstallTargetAdapters,
planInstallTargetScaffold,
} = require('../../scripts/lib/install-targets/registry');
function normalizedRelativePath(value) {
return String(value || '').replace(/\\/g, '/');
}
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-target adapters ===\n');
let passed = 0;
let failed = 0;
if (test('lists supported target adapters', () => {
const adapters = listInstallTargetAdapters();
const targets = adapters.map(adapter => adapter.target);
assert.ok(targets.includes('claude'), 'Should include claude target');
assert.ok(targets.includes('cursor'), 'Should include cursor target');
assert.ok(targets.includes('antigravity'), 'Should include antigravity target');
assert.ok(targets.includes('codex'), 'Should include codex target');
assert.ok(targets.includes('opencode'), 'Should include opencode target');
})) passed++; else failed++;
if (test('resolves cursor adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('cursor');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(root, path.join(projectRoot, '.cursor'));
assert.strictEqual(statePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('resolves claude adapter root and install-state path from home dir', () => {
const adapter = getInstallTargetAdapter('claude');
const homeDir = '/Users/example';
const root = adapter.resolveRoot({ homeDir, repoRoot: '/repo/ecc' });
const statePath = adapter.getInstallStatePath({ homeDir, repoRoot: '/repo/ecc' });
assert.strictEqual(root, path.join(homeDir, '.claude'));
assert.strictEqual(statePath, path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
})) passed++; else failed++;
if (test('plans scaffold operations and flattens native target roots', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const modules = [
{
id: 'platform-configs',
paths: ['.cursor', 'mcp-configs'],
},
{
id: 'rules-core',
paths: ['rules'],
},
];
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules,
});
assert.strictEqual(plan.adapter.id, 'cursor-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.cursor'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
const flattened = plan.operations.find(operation => operation.sourceRelativePath === '.cursor');
const preserved = plan.operations.find(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
));
assert.ok(flattened, 'Should include .cursor scaffold operation');
assert.strictEqual(flattened.strategy, 'sync-root-children');
assert.strictEqual(flattened.destinationPath, path.join(projectRoot, '.cursor'));
assert.ok(preserved, 'Should include flattened rules scaffold operations');
assert.strictEqual(preserved.strategy, 'flatten-copy');
assert.strictEqual(
preserved.destinationPath,
path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.md')
);
})) passed++; else failed++;
if (test('plans cursor rules with flat namespaced filenames to avoid rule collisions', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.md')
)),
'Should flatten common rules into namespaced files'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/typescript/testing.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'typescript-testing.md')
)),
'Should flatten language rules into namespaced files'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common', 'coding-style.md')
)),
'Should not preserve nested rule directories for cursor installs'
);
})) passed++; else failed++;
if (test('plans antigravity remaps for workflows, skills, and flat rules', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'antigravity',
repoRoot,
projectRoot,
modules: [
{
id: 'commands-core',
paths: ['commands'],
},
{
id: 'agents-core',
paths: ['agents'],
},
{
id: 'rules-core',
paths: ['rules'],
},
],
});
assert.ok(
plan.operations.some(operation => (
operation.sourceRelativePath === 'commands'
&& operation.destinationPath === path.join(projectRoot, '.agent', 'workflows')
)),
'Should remap commands into workflows'
);
assert.ok(
plan.operations.some(operation => (
operation.sourceRelativePath === 'agents'
&& operation.destinationPath === path.join(projectRoot, '.agent', 'skills')
)),
'Should remap agents into skills'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.agent', 'rules', 'common-coding-style.md')
)),
'Should flatten common rules for antigravity'
);
})) passed++; else failed++;
if (test('exposes validate and planOperations on adapters', () => {
const claudeAdapter = getInstallTargetAdapter('claude');
const cursorAdapter = getInstallTargetAdapter('cursor');
assert.strictEqual(typeof claudeAdapter.planOperations, 'function');
assert.strictEqual(typeof claudeAdapter.validate, 'function');
assert.deepStrictEqual(
claudeAdapter.validate({ homeDir: '/Users/example', repoRoot: '/repo/ecc' }),
[]
);
assert.strictEqual(typeof cursorAdapter.planOperations, 'function');
assert.strictEqual(typeof cursorAdapter.validate, 'function');
assert.deepStrictEqual(
cursorAdapter.validate({ projectRoot: '/workspace/app', repoRoot: '/repo/ecc' }),
[]
);
})) passed++; else failed++;
if (test('throws on unknown target adapter', () => {
assert.throws(
() => getInstallTargetAdapter('ghost-target'),
/Unknown install target adapter/
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,225 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
buildSessionSnapshot,
listTmuxPanes,
loadWorkerSnapshots,
parseWorkerHandoff,
parseWorkerStatus,
parseWorkerTask,
resolveSnapshotTarget
} = require('../../scripts/lib/orchestration-session');
console.log('=== Testing orchestration-session.js ===\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('parseWorkerStatus extracts structured status fields', () => {
const status = parseWorkerStatus([
'# Status',
'',
'- State: completed',
'- Updated: 2026-03-12T14:09:15Z',
'- Branch: feature-branch',
'- Worktree: `/tmp/worktree`',
'',
'- Handoff file: `/tmp/handoff.md`'
].join('\n'));
assert.deepStrictEqual(status, {
state: 'completed',
updated: '2026-03-12T14:09:15Z',
branch: 'feature-branch',
worktree: '/tmp/worktree',
taskFile: null,
handoffFile: '/tmp/handoff.md'
});
});
test('parseWorkerTask extracts objective and seeded overlays', () => {
const task = parseWorkerTask([
'# Worker Task',
'',
'## Seeded Local Overlays',
'- `scripts/orchestrate-worktrees.js`',
'- `commands/orchestrate.md`',
'',
'## Objective',
'Verify seeded files and summarize status.'
].join('\n'));
assert.deepStrictEqual(task.seedPaths, [
'scripts/orchestrate-worktrees.js',
'commands/orchestrate.md'
]);
assert.strictEqual(task.objective, 'Verify seeded files and summarize status.');
});
test('parseWorkerHandoff extracts summary, validation, and risks', () => {
const handoff = parseWorkerHandoff([
'# Handoff',
'',
'## Summary',
'- Worker completed successfully',
'',
'## Validation',
'- Ran tests',
'',
'## Remaining Risks',
'- No runtime screenshot'
].join('\n'));
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
assert.deepStrictEqual(handoff.remainingRisks, ['No runtime screenshot']);
});
test('parseWorkerHandoff also supports bold section headers', () => {
const handoff = parseWorkerHandoff([
'# Handoff',
'',
'**Summary**',
'- Worker completed successfully',
'',
'**Validation**',
'- Ran tests',
'',
'**Remaining Risks**',
'- No runtime screenshot'
].join('\n'));
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
assert.deepStrictEqual(handoff.remainingRisks, ['No runtime screenshot']);
});
test('loadWorkerSnapshots reads coordination worker directories', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-session-'));
const coordinationDir = path.join(tempRoot, 'coordination');
const workerDir = path.join(coordinationDir, 'seed-check');
const proofDir = path.join(coordinationDir, 'proof');
fs.mkdirSync(workerDir, { recursive: true });
fs.mkdirSync(proofDir, { recursive: true });
try {
fs.writeFileSync(path.join(workerDir, 'status.md'), [
'# Status',
'',
'- State: running',
'- Branch: seed-branch',
'- Worktree: `/tmp/seed-worktree`'
].join('\n'));
fs.writeFileSync(path.join(workerDir, 'task.md'), [
'# Worker Task',
'',
'## Objective',
'Inspect seed paths.'
].join('\n'));
fs.writeFileSync(path.join(workerDir, 'handoff.md'), [
'# Handoff',
'',
'## Summary',
'- Pending'
].join('\n'));
const workers = loadWorkerSnapshots(coordinationDir);
assert.strictEqual(workers.length, 1);
assert.strictEqual(workers[0].workerSlug, 'seed-check');
assert.strictEqual(workers[0].status.branch, 'seed-branch');
assert.strictEqual(workers[0].task.objective, 'Inspect seed paths.');
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('buildSessionSnapshot merges tmux panes with worker metadata', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-snapshot-'));
const coordinationDir = path.join(tempRoot, 'coordination');
const workerDir = path.join(coordinationDir, 'seed-check');
fs.mkdirSync(workerDir, { recursive: true });
try {
fs.writeFileSync(path.join(workerDir, 'status.md'), '- State: completed\n- Branch: seed-branch\n');
fs.writeFileSync(path.join(workerDir, 'task.md'), '## Objective\nInspect seed paths.\n');
fs.writeFileSync(path.join(workerDir, 'handoff.md'), '## Summary\n- ok\n');
const snapshot = buildSessionSnapshot({
sessionName: 'workflow-visual-proof',
coordinationDir,
panes: [
{
paneId: '%95',
windowIndex: 1,
paneIndex: 2,
title: 'seed-check',
currentCommand: 'codex',
currentPath: '/tmp/worktree',
active: false,
dead: false,
pid: 1234
}
]
});
assert.strictEqual(snapshot.sessionActive, true);
assert.strictEqual(snapshot.workerCount, 1);
assert.strictEqual(snapshot.workerStates.completed, 1);
assert.strictEqual(snapshot.workers[0].pane.paneId, '%95');
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('listTmuxPanes returns an empty array when tmux is unavailable', () => {
const panes = listTmuxPanes('workflow-visual-proof', {
spawnSyncImpl: () => ({
error: Object.assign(new Error('tmux not found'), { code: 'ENOENT' })
})
});
assert.deepStrictEqual(panes, []);
});
test('resolveSnapshotTarget handles plan files and direct session names', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
const repoRoot = path.join(tempRoot, 'repo');
fs.mkdirSync(repoRoot, { recursive: true });
const planPath = path.join(repoRoot, 'plan.json');
fs.writeFileSync(planPath, JSON.stringify({
sessionName: 'workflow-visual-proof',
repoRoot,
coordinationRoot: path.join(repoRoot, '.claude', 'orchestration')
}));
try {
const fromPlan = resolveSnapshotTarget(planPath, repoRoot);
assert.strictEqual(fromPlan.targetType, 'plan');
assert.strictEqual(fromPlan.sessionName, 'workflow-visual-proof');
const fromSession = resolveSnapshotTarget('workflow-visual-proof', repoRoot);
assert.strictEqual(fromSession.targetType, 'session');
assert.ok(fromSession.coordinationDir.endsWith(path.join('.claude', 'orchestration', 'workflow-visual-proof')));
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,418 @@
/**
* Tests for scripts/lib/project-detect.js
*
* Run with: node tests/lib/project-detect.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
detectProjectType,
LANGUAGE_RULES,
FRAMEWORK_RULES,
getPackageJsonDeps,
getPythonDeps,
getGoDeps,
getRustDeps,
getComposerDeps,
getElixirDeps
} = require('../../scripts/lib/project-detect');
// 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;
}
}
// Create a temporary directory for testing
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-test-'));
}
// Clean up temp directory
function cleanupDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch { /* ignore */ }
}
// Write a file in the temp directory
function writeTestFile(dir, filePath, content = '') {
const fullPath = path.join(dir, filePath);
const dirName = path.dirname(fullPath);
fs.mkdirSync(dirName, { recursive: true });
fs.writeFileSync(fullPath, content, 'utf8');
}
function runTests() {
console.log('\n=== Testing project-detect.js ===\n');
let passed = 0;
let failed = 0;
// Rule definitions tests
console.log('Rule Definitions:');
if (test('LANGUAGE_RULES is non-empty array', () => {
assert.ok(Array.isArray(LANGUAGE_RULES));
assert.ok(LANGUAGE_RULES.length > 0);
})) passed++; else failed++;
if (test('FRAMEWORK_RULES is non-empty array', () => {
assert.ok(Array.isArray(FRAMEWORK_RULES));
assert.ok(FRAMEWORK_RULES.length > 0);
})) passed++; else failed++;
if (test('each language rule has type, markers, and extensions', () => {
for (const rule of LANGUAGE_RULES) {
assert.ok(typeof rule.type === 'string', `Missing type`);
assert.ok(Array.isArray(rule.markers), `Missing markers for ${rule.type}`);
assert.ok(Array.isArray(rule.extensions), `Missing extensions for ${rule.type}`);
}
})) passed++; else failed++;
if (test('each framework rule has framework, language, markers, packageKeys', () => {
for (const rule of FRAMEWORK_RULES) {
assert.ok(typeof rule.framework === 'string', `Missing framework`);
assert.ok(typeof rule.language === 'string', `Missing language for ${rule.framework}`);
assert.ok(Array.isArray(rule.markers), `Missing markers for ${rule.framework}`);
assert.ok(Array.isArray(rule.packageKeys), `Missing packageKeys for ${rule.framework}`);
}
})) passed++; else failed++;
// Empty directory detection
console.log('\nEmpty Directory:');
if (test('empty directory returns unknown primary', () => {
const dir = createTempDir();
try {
const result = detectProjectType(dir);
assert.strictEqual(result.primary, 'unknown');
assert.deepStrictEqual(result.languages, []);
assert.deepStrictEqual(result.frameworks, []);
assert.strictEqual(result.projectDir, dir);
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Python detection
console.log('\nPython Detection:');
if (test('detects python from requirements.txt', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'requirements.txt', 'flask==3.0.0\nrequests>=2.31');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('python'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects python from pyproject.toml', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'pyproject.toml', '[project]\nname = "test"');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('python'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects flask framework from requirements.txt', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'requirements.txt', 'flask==3.0.0\nrequests>=2.31');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('flask'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects django framework from manage.py', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'manage.py', '#!/usr/bin/env python');
writeTestFile(dir, 'requirements.txt', 'django>=4.2');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('django'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects fastapi from pyproject.toml dependencies', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'pyproject.toml', '[project]\nname = "test"\ndependencies = [\n "fastapi>=0.100",\n "uvicorn"\n]');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('fastapi'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// TypeScript/JavaScript detection
console.log('\nTypeScript/JavaScript Detection:');
if (test('detects typescript from tsconfig.json', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'tsconfig.json', '{}');
writeTestFile(dir, 'package.json', '{"dependencies":{}}');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('typescript'));
// Should NOT also include javascript when TS is detected
assert.ok(!result.languages.includes('javascript'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects nextjs from next.config.mjs', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'tsconfig.json', '{}');
writeTestFile(dir, 'next.config.mjs', 'export default {}');
writeTestFile(dir, 'package.json', '{"dependencies":{"next":"14.0.0","react":"18.0.0"}}');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('nextjs'));
assert.ok(result.frameworks.includes('react'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects react from package.json', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'package.json', '{"dependencies":{"react":"18.0.0","react-dom":"18.0.0"}}');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('react'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('detects angular from angular.json', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'angular.json', '{}');
writeTestFile(dir, 'tsconfig.json', '{}');
writeTestFile(dir, 'package.json', '{"dependencies":{"@angular/core":"17.0.0"}}');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('angular'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Go detection
console.log('\nGo Detection:');
if (test('detects golang from go.mod', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'go.mod', 'module github.com/test/app\n\ngo 1.22\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n)');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('golang'));
assert.ok(result.frameworks.includes('gin'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Rust detection
console.log('\nRust Detection:');
if (test('detects rust from Cargo.toml', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'Cargo.toml', '[package]\nname = "test"\n\n[dependencies]\naxum = "0.7"');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('rust'));
assert.ok(result.frameworks.includes('axum'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Ruby detection
console.log('\nRuby Detection:');
if (test('detects ruby and rails', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'Gemfile', 'source "https://rubygems.org"\ngem "rails"');
writeTestFile(dir, 'config/routes.rb', 'Rails.application.routes.draw do\nend');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('ruby'));
assert.ok(result.frameworks.includes('rails'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// PHP detection
console.log('\nPHP Detection:');
if (test('detects php and laravel', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'composer.json', '{"require":{"laravel/framework":"^10.0"}}');
writeTestFile(dir, 'artisan', '#!/usr/bin/env php');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('php'));
assert.ok(result.frameworks.includes('laravel'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Fullstack detection
console.log('\nFullstack Detection:');
if (test('detects fullstack when frontend + backend frameworks present', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'package.json', '{"dependencies":{"react":"18.0.0","express":"4.18.0"}}');
const result = detectProjectType(dir);
assert.ok(result.frameworks.includes('react'));
assert.ok(result.frameworks.includes('express'));
assert.strictEqual(result.primary, 'fullstack');
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Dependency reader tests
console.log('\nDependency Readers:');
if (test('getPackageJsonDeps reads deps and devDeps', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'package.json', '{"dependencies":{"react":"18.0.0"},"devDependencies":{"typescript":"5.0.0"}}');
const deps = getPackageJsonDeps(dir);
assert.ok(deps.includes('react'));
assert.ok(deps.includes('typescript'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('getPythonDeps reads requirements.txt', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'requirements.txt', 'flask>=3.0\n# comment\nrequests==2.31\n-r other.txt');
const deps = getPythonDeps(dir);
assert.ok(deps.includes('flask'));
assert.ok(deps.includes('requests'));
assert.ok(!deps.includes('-r'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('getGoDeps reads go.mod require block', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'go.mod', 'module test\n\ngo 1.22\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/lib/pq v1.10.9\n)');
const deps = getGoDeps(dir);
assert.ok(deps.some(d => d.includes('gin-gonic/gin')));
assert.ok(deps.some(d => d.includes('lib/pq')));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('getRustDeps reads Cargo.toml', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'Cargo.toml', '[package]\nname = "test"\n\n[dependencies]\nserde = "1.0"\ntokio = { version = "1.0", features = ["full"] }');
const deps = getRustDeps(dir);
assert.ok(deps.includes('serde'));
assert.ok(deps.includes('tokio'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('returns empty arrays for missing files', () => {
const dir = createTempDir();
try {
assert.deepStrictEqual(getPackageJsonDeps(dir), []);
assert.deepStrictEqual(getPythonDeps(dir), []);
assert.deepStrictEqual(getGoDeps(dir), []);
assert.deepStrictEqual(getRustDeps(dir), []);
assert.deepStrictEqual(getComposerDeps(dir), []);
assert.deepStrictEqual(getElixirDeps(dir), []);
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Elixir detection
console.log('\nElixir Detection:');
if (test('detects elixir from mix.exs', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'mix.exs', 'defmodule Test.MixProject do\n defp deps do\n [{:phoenix, "~> 1.7"},\n {:ecto, "~> 3.0"}]\n end\nend');
const result = detectProjectType(dir);
assert.ok(result.languages.includes('elixir'));
assert.ok(result.frameworks.includes('phoenix'));
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Edge cases
console.log('\nEdge Cases:');
if (test('handles non-existent directory gracefully', () => {
const result = detectProjectType('/tmp/nonexistent-dir-' + Date.now());
assert.strictEqual(result.primary, 'unknown');
assert.deepStrictEqual(result.languages, []);
})) passed++; else failed++;
if (test('handles malformed package.json', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'package.json', 'not valid json{{{');
const deps = getPackageJsonDeps(dir);
assert.deepStrictEqual(deps, []);
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
if (test('handles malformed composer.json', () => {
const dir = createTempDir();
try {
writeTestFile(dir, 'composer.json', '{invalid');
const deps = getComposerDeps(dir);
assert.deepStrictEqual(deps, []);
} finally {
cleanupDir(dir);
}
})) passed++; else failed++;
// Summary
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,315 @@
/**
* Tests for scripts/lib/resolve-ecc-root.js
*
* Covers the ECC root resolution fallback chain:
* 1. CLAUDE_PLUGIN_ROOT env var
* 2. Standard install (~/.claude/)
* 3. Exact legacy plugin roots under ~/.claude/plugins/
* 4. Plugin cache auto-detection
* 5. Fallback to ~/.claude/
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { resolveEccRoot, INLINE_RESOLVE } = require('../../scripts/lib/resolve-ecc-root');
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() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-root-test-'));
}
function setupStandardInstall(homeDir) {
const claudeDir = path.join(homeDir, '.claude');
const scriptDir = path.join(claudeDir, 'scripts', 'lib');
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
return claudeDir;
}
function setupLegacyPluginInstall(homeDir, segments) {
const legacyDir = path.join(homeDir, '.claude', 'plugins', ...segments);
const scriptDir = path.join(legacyDir, 'scripts', 'lib');
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
return legacyDir;
}
function setupPluginCache(homeDir, orgName, version) {
const cacheDir = path.join(
homeDir, '.claude', 'plugins', 'cache',
'everything-claude-code', orgName, version
);
const scriptDir = path.join(cacheDir, 'scripts', 'lib');
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
return cacheDir;
}
function runTests() {
console.log('\n=== Testing resolve-ecc-root.js ===\n');
let passed = 0;
let failed = 0;
// ─── Env Var Priority ───
if (test('returns CLAUDE_PLUGIN_ROOT when set', () => {
const result = resolveEccRoot({ envRoot: '/custom/plugin/root' });
assert.strictEqual(result, '/custom/plugin/root');
})) passed++; else failed++;
if (test('trims whitespace from CLAUDE_PLUGIN_ROOT', () => {
const result = resolveEccRoot({ envRoot: ' /trimmed/root ' });
assert.strictEqual(result, '/trimmed/root');
})) passed++; else failed++;
if (test('skips empty CLAUDE_PLUGIN_ROOT', () => {
const homeDir = createTempDir();
try {
setupStandardInstall(homeDir);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('skips whitespace-only CLAUDE_PLUGIN_ROOT', () => {
const homeDir = createTempDir();
try {
setupStandardInstall(homeDir);
const result = resolveEccRoot({ envRoot: ' ', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Standard Install ───
if (test('finds standard install at ~/.claude/', () => {
const homeDir = createTempDir();
try {
setupStandardInstall(homeDir);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('finds exact legacy plugin install at ~/.claude/plugins/everything-claude-code', () => {
const homeDir = createTempDir();
try {
const expected = setupLegacyPluginInstall(homeDir, ['everything-claude-code']);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('finds exact legacy plugin install at ~/.claude/plugins/everything-claude-code@everything-claude-code', () => {
const homeDir = createTempDir();
try {
const expected = setupLegacyPluginInstall(homeDir, ['everything-claude-code@everything-claude-code']);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('finds marketplace legacy plugin install at ~/.claude/plugins/marketplace/everything-claude-code', () => {
const homeDir = createTempDir();
try {
const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'everything-claude-code']);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('prefers exact legacy plugin install over plugin cache', () => {
const homeDir = createTempDir();
try {
const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'everything-claude-code']);
setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Plugin Cache Auto-Detection ───
if (test('discovers plugin root from cache directory', () => {
const homeDir = createTempDir();
try {
const expected = setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('prefers standard install over plugin cache', () => {
const homeDir = createTempDir();
try {
const claudeDir = setupStandardInstall(homeDir);
setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, claudeDir,
'Standard install should take precedence over plugin cache');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('handles multiple versions in plugin cache', () => {
const homeDir = createTempDir();
try {
setupPluginCache(homeDir, 'everything-claude-code', '1.7.0');
const expected = setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
// Should find one of them (either is valid)
assert.ok(
result === expected ||
result === path.join(homeDir, '.claude', 'plugins', 'cache', 'everything-claude-code', 'everything-claude-code', '1.7.0'),
'Should resolve to a valid plugin cache directory'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Fallback ───
if (test('falls back to ~/.claude/ when nothing is found', () => {
const homeDir = createTempDir();
try {
// Create ~/.claude but don't put scripts there
fs.mkdirSync(path.join(homeDir, '.claude'), { recursive: true });
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('falls back gracefully when ~/.claude/ does not exist', () => {
const homeDir = createTempDir();
try {
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Custom Probe ───
if (test('supports custom probe path', () => {
const homeDir = createTempDir();
try {
const claudeDir = path.join(homeDir, '.claude');
fs.mkdirSync(path.join(claudeDir, 'custom'), { recursive: true });
fs.writeFileSync(path.join(claudeDir, 'custom', 'marker.js'), '// probe');
const result = resolveEccRoot({
envRoot: '',
homeDir,
probe: path.join('custom', 'marker.js'),
});
assert.strictEqual(result, claudeDir);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── INLINE_RESOLVE ───
if (test('INLINE_RESOLVE is a non-empty string', () => {
assert.ok(typeof INLINE_RESOLVE === 'string');
assert.ok(INLINE_RESOLVE.length > 50, 'Should be a substantial inline expression');
})) passed++; else failed++;
if (test('INLINE_RESOLVE returns CLAUDE_PLUGIN_ROOT when set', () => {
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { ...process.env, CLAUDE_PLUGIN_ROOT: '/inline/test/root' },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, '/inline/test/root');
})) passed++; else failed++;
if (test('INLINE_RESOLVE discovers exact legacy plugin root when env var is unset', () => {
const homeDir = createTempDir();
try {
const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'everything-claude-code']);
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { PATH: process.env.PATH, HOME: homeDir, USERPROFILE: homeDir },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('INLINE_RESOLVE discovers plugin cache when env var is unset', () => {
const homeDir = createTempDir();
try {
const expected = setupPluginCache(homeDir, 'everything-claude-code', '1.9.0');
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { PATH: process.env.PATH, HOME: homeDir, USERPROFILE: homeDir },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('INLINE_RESOLVE falls back to ~/.claude/ when nothing found', () => {
const homeDir = createTempDir();
try {
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { PATH: process.env.PATH, HOME: homeDir, USERPROFILE: homeDir },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, path.join(homeDir, '.claude'));
} 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();

View File

@@ -0,0 +1,246 @@
/**
* Tests for scripts/lib/resolve-formatter.js
*
* Run with: node tests/lib/resolve-formatter.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { findProjectRoot, detectFormatter, resolveFormatterBin, clearCaches } = require('../../scripts/lib/resolve-formatter');
/**
* Run a single test case, printing pass/fail.
*
* @param {string} name - Test description
* @param {() => void} fn - Test body (throws on failure)
* @returns {boolean} Whether the test passed
*/
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
/** Track all created tmp dirs for cleanup */
const tmpDirs = [];
/**
* Create a temporary directory and track it for cleanup.
*
* @returns {string} Absolute path to the new temp directory
*/
function makeTmpDir() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'resolve-fmt-'));
tmpDirs.push(dir);
return dir;
}
/**
* Remove all tracked temporary directories.
*/
function cleanupTmpDirs() {
for (const dir of tmpDirs) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// Best-effort cleanup
}
}
tmpDirs.length = 0;
}
function runTests() {
console.log('\n=== Testing resolve-formatter.js ===\n');
let passed = 0;
let failed = 0;
function run(name, fn) {
clearCaches();
if (test(name, fn)) passed++;
else failed++;
}
// ── findProjectRoot ───────────────────────────────────────────
run('findProjectRoot: finds package.json in parent dir', () => {
const root = makeTmpDir();
const sub = path.join(root, 'src', 'lib');
fs.mkdirSync(sub, { recursive: true });
fs.writeFileSync(path.join(root, 'package.json'), '{}');
assert.strictEqual(findProjectRoot(sub), root);
});
run('findProjectRoot: returns startDir when no package.json', () => {
const root = makeTmpDir();
const sub = path.join(root, 'deep');
fs.mkdirSync(sub, { recursive: true });
// No package.json anywhere in tmp → falls back to startDir
assert.strictEqual(findProjectRoot(sub), sub);
});
run('findProjectRoot: caches result for same startDir', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'package.json'), '{}');
const first = findProjectRoot(root);
// Remove package.json — cache should still return the old result
fs.unlinkSync(path.join(root, 'package.json'));
const second = findProjectRoot(root);
assert.strictEqual(first, second);
});
// ── detectFormatter ───────────────────────────────────────────
run('detectFormatter: detects biome.json', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'biome.json'), '{}');
assert.strictEqual(detectFormatter(root), 'biome');
});
run('detectFormatter: detects biome.jsonc', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'biome.jsonc'), '{}');
assert.strictEqual(detectFormatter(root), 'biome');
});
run('detectFormatter: detects .prettierrc', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, '.prettierrc'), '{}');
assert.strictEqual(detectFormatter(root), 'prettier');
});
run('detectFormatter: detects prettier.config.js', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'prettier.config.js'), 'module.exports = {}');
assert.strictEqual(detectFormatter(root), 'prettier');
});
run('detectFormatter: detects prettier key in package.json', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test', prettier: { singleQuote: true } }));
assert.strictEqual(detectFormatter(root), 'prettier');
});
run('detectFormatter: ignores package.json without prettier key', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test' }));
assert.strictEqual(detectFormatter(root), null);
});
run('detectFormatter: biome takes priority over prettier', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'biome.json'), '{}');
fs.writeFileSync(path.join(root, '.prettierrc'), '{}');
assert.strictEqual(detectFormatter(root), 'biome');
});
run('detectFormatter: returns null when no config found', () => {
const root = makeTmpDir();
assert.strictEqual(detectFormatter(root), null);
});
// ── resolveFormatterBin ───────────────────────────────────────
run('resolveFormatterBin: uses local biome binary when available', () => {
const root = makeTmpDir();
const binDir = path.join(root, 'node_modules', '.bin');
fs.mkdirSync(binDir, { recursive: true });
const binName = process.platform === 'win32' ? 'biome.cmd' : 'biome';
fs.writeFileSync(path.join(binDir, binName), '');
const result = resolveFormatterBin(root, 'biome');
assert.strictEqual(result.bin, path.join(binDir, binName));
assert.deepStrictEqual(result.prefix, []);
});
run('resolveFormatterBin: falls back to npx for biome', () => {
const root = makeTmpDir();
const result = resolveFormatterBin(root, 'biome');
const expectedBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
assert.strictEqual(result.bin, expectedBin);
assert.deepStrictEqual(result.prefix, ['@biomejs/biome']);
});
run('resolveFormatterBin: uses local prettier binary when available', () => {
const root = makeTmpDir();
const binDir = path.join(root, 'node_modules', '.bin');
fs.mkdirSync(binDir, { recursive: true });
const binName = process.platform === 'win32' ? 'prettier.cmd' : 'prettier';
fs.writeFileSync(path.join(binDir, binName), '');
const result = resolveFormatterBin(root, 'prettier');
assert.strictEqual(result.bin, path.join(binDir, binName));
assert.deepStrictEqual(result.prefix, []);
});
run('resolveFormatterBin: falls back to npx for prettier', () => {
const root = makeTmpDir();
const result = resolveFormatterBin(root, 'prettier');
const expectedBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
assert.strictEqual(result.bin, expectedBin);
assert.deepStrictEqual(result.prefix, ['prettier']);
});
run('resolveFormatterBin: returns null for unknown formatter', () => {
const root = makeTmpDir();
const result = resolveFormatterBin(root, 'unknown');
assert.strictEqual(result, null);
});
run('resolveFormatterBin: caches resolved binary', () => {
const root = makeTmpDir();
const binDir = path.join(root, 'node_modules', '.bin');
fs.mkdirSync(binDir, { recursive: true });
const binName = process.platform === 'win32' ? 'biome.cmd' : 'biome';
fs.writeFileSync(path.join(binDir, binName), '');
const first = resolveFormatterBin(root, 'biome');
fs.unlinkSync(path.join(binDir, binName));
const second = resolveFormatterBin(root, 'biome');
assert.strictEqual(first.bin, second.bin);
});
// ── clearCaches ───────────────────────────────────────────────
run('clearCaches: clears all cached values', () => {
const root = makeTmpDir();
fs.writeFileSync(path.join(root, 'package.json'), '{}');
fs.writeFileSync(path.join(root, 'biome.json'), '{}');
findProjectRoot(root);
detectFormatter(root);
resolveFormatterBin(root, 'biome');
clearCaches();
// After clearing, removing config should change detection
fs.unlinkSync(path.join(root, 'biome.json'));
assert.strictEqual(detectFormatter(root), null);
});
// ── Summary & Cleanup ─────────────────────────────────────────
cleanupTmpDirs();
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,717 @@
/**
* Tests for --with / --without selective install flags (issue #470)
*
* Covers:
* - CLI argument parsing for --with and --without
* - Request normalization with include/exclude component IDs
* - Component-to-module expansion via the manifest catalog
* - End-to-end install plans with --with and --without
* - Validation and error handling for unknown component IDs
* - Combined --profile + --with + --without flows
* - Standalone --with without a profile
* - agent: and skill: component families
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
parseInstallArgs,
normalizeInstallRequest,
} = require('../../scripts/lib/install/request');
const {
listInstallComponents,
resolveInstallPlan,
} = require('../../scripts/lib/install-manifests');
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 --with / --without selective install flags ===\n');
let passed = 0;
let failed = 0;
// ─── CLI Argument Parsing ───
if (test('parses single --with flag', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'core',
'--with', 'lang:typescript',
]);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(parsed.excludeComponentIds, []);
})) passed++; else failed++;
if (test('parses single --without flag', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'developer',
'--without', 'capability:orchestration',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']);
assert.deepStrictEqual(parsed.includeComponentIds, []);
})) passed++; else failed++;
if (test('parses multiple --with flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--with', 'lang:typescript',
'--with', 'framework:nextjs',
'--with', 'capability:database',
]);
assert.deepStrictEqual(parsed.includeComponentIds, [
'lang:typescript',
'framework:nextjs',
'capability:database',
]);
})) passed++; else failed++;
if (test('parses multiple --without flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'full',
'--without', 'capability:media',
'--without', 'capability:social',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, [
'capability:media',
'capability:social',
]);
})) passed++; else failed++;
if (test('parses combined --with and --without flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'developer',
'--with', 'lang:typescript',
'--with', 'framework:nextjs',
'--without', 'capability:orchestration',
]);
assert.strictEqual(parsed.profileId, 'developer');
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']);
})) passed++; else failed++;
if (test('ignores empty --with values', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--with', '',
'--with', 'lang:python',
]);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:python']);
})) passed++; else failed++;
if (test('ignores empty --without values', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'core',
'--without', '',
'--without', 'capability:media',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']);
})) passed++; else failed++;
// ─── Request Normalization ───
if (test('normalizes --with-only request as manifest mode', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: [],
languages: [],
});
assert.strictEqual(request.mode, 'manifest');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(request.excludeComponentIds, []);
})) passed++; else failed++;
if (test('normalizes --profile + --with + --without as manifest mode', () => {
const request = normalizeInstallRequest({
target: 'cursor',
profileId: 'developer',
moduleIds: [],
includeComponentIds: ['lang:typescript', 'framework:nextjs'],
excludeComponentIds: ['capability:orchestration'],
languages: [],
});
assert.strictEqual(request.mode, 'manifest');
assert.strictEqual(request.profileId, 'developer');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(request.excludeComponentIds, ['capability:orchestration']);
})) passed++; else failed++;
if (test('rejects --with combined with legacy language arguments', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: [],
languages: ['python'],
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('rejects --without combined with legacy language arguments', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: ['capability:media'],
languages: ['typescript'],
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('deduplicates repeated --with component IDs', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript', 'lang:typescript', 'lang:python'],
excludeComponentIds: [],
languages: [],
});
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'lang:python']);
})) passed++; else failed++;
if (test('deduplicates repeated --without component IDs', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: 'full',
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: ['capability:media', 'capability:media', 'capability:social'],
languages: [],
});
assert.deepStrictEqual(request.excludeComponentIds, ['capability:media', 'capability:social']);
})) passed++; else failed++;
// ─── Component Catalog Validation ───
if (test('component catalog includes lang: family entries', () => {
const components = listInstallComponents({ family: 'language' });
assert.ok(components.some(c => c.id === 'lang:typescript'), 'Should have lang:typescript');
assert.ok(components.some(c => c.id === 'lang:python'), 'Should have lang:python');
assert.ok(components.some(c => c.id === 'lang:go'), 'Should have lang:go');
assert.ok(components.some(c => c.id === 'lang:java'), 'Should have lang:java');
})) passed++; else failed++;
if (test('component catalog includes framework: family entries', () => {
const components = listInstallComponents({ family: 'framework' });
assert.ok(components.some(c => c.id === 'framework:react'), 'Should have framework:react');
assert.ok(components.some(c => c.id === 'framework:nextjs'), 'Should have framework:nextjs');
assert.ok(components.some(c => c.id === 'framework:django'), 'Should have framework:django');
assert.ok(components.some(c => c.id === 'framework:springboot'), 'Should have framework:springboot');
})) passed++; else failed++;
if (test('component catalog includes capability: family entries', () => {
const components = listInstallComponents({ family: 'capability' });
assert.ok(components.some(c => c.id === 'capability:database'), 'Should have capability:database');
assert.ok(components.some(c => c.id === 'capability:security'), 'Should have capability:security');
assert.ok(components.some(c => c.id === 'capability:orchestration'), 'Should have capability:orchestration');
})) passed++; else failed++;
if (test('component catalog includes agent: family entries', () => {
const components = listInstallComponents({ family: 'agent' });
assert.ok(components.length > 0, 'Should have at least one agent component');
assert.ok(components.some(c => c.id === 'agent:security-reviewer'), 'Should have agent:security-reviewer');
})) passed++; else failed++;
if (test('component catalog includes skill: family entries', () => {
const components = listInstallComponents({ family: 'skill' });
assert.ok(components.length > 0, 'Should have at least one skill component');
assert.ok(components.some(c => c.id === 'skill:continuous-learning'), 'Should have skill:continuous-learning');
})) passed++; else failed++;
// ─── Install Plan Resolution with --with ───
if (test('--with alone resolves component modules and their dependencies', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['lang:typescript'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('framework-language'),
'Should include the module behind lang:typescript');
assert.ok(plan.selectedModuleIds.includes('rules-core'),
'Should include framework-language dependency rules-core');
assert.ok(plan.selectedModuleIds.includes('platform-configs'),
'Should include framework-language dependency platform-configs');
})) passed++; else failed++;
if (test('--with adds modules on top of a profile', () => {
const plan = resolveInstallPlan({
profileId: 'core',
includeComponentIds: ['capability:security'],
target: 'claude',
});
// core profile modules
assert.ok(plan.selectedModuleIds.includes('rules-core'));
assert.ok(plan.selectedModuleIds.includes('workflow-quality'));
// added by --with
assert.ok(plan.selectedModuleIds.includes('security'),
'Should include security module from --with');
})) passed++; else failed++;
if (test('multiple --with flags union their modules', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['lang:typescript', 'capability:database'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('framework-language'),
'Should include framework-language from lang:typescript');
assert.ok(plan.selectedModuleIds.includes('database'),
'Should include database from capability:database');
})) passed++; else failed++;
// ─── Install Plan Resolution with --without ───
if (test('--without excludes modules from a profile', () => {
const plan = resolveInstallPlan({
profileId: 'developer',
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should exclude orchestration module');
assert.ok(plan.excludedModuleIds.includes('orchestration'),
'Should report orchestration as excluded');
// rest of developer profile should remain
assert.ok(plan.selectedModuleIds.includes('rules-core'));
assert.ok(plan.selectedModuleIds.includes('framework-language'));
assert.ok(plan.selectedModuleIds.includes('database'));
})) passed++; else failed++;
if (test('multiple --without flags exclude multiple modules', () => {
const plan = resolveInstallPlan({
profileId: 'full',
excludeComponentIds: ['capability:media', 'capability:social', 'capability:supply-chain'],
target: 'claude',
});
assert.ok(!plan.selectedModuleIds.includes('media-generation'));
assert.ok(!plan.selectedModuleIds.includes('social-distribution'));
assert.ok(!plan.selectedModuleIds.includes('supply-chain-domain'));
assert.ok(plan.excludedModuleIds.includes('media-generation'));
assert.ok(plan.excludedModuleIds.includes('social-distribution'));
assert.ok(plan.excludedModuleIds.includes('supply-chain-domain'));
})) passed++; else failed++;
// ─── Combined --with + --without ───
if (test('--with and --without work together on a profile', () => {
const plan = resolveInstallPlan({
profileId: 'developer',
includeComponentIds: ['capability:security'],
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('security'),
'Should include security from --with');
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should exclude orchestration from --without');
assert.ok(plan.selectedModuleIds.includes('rules-core'),
'Should keep profile base modules');
})) passed++; else failed++;
if (test('--without on a dependency of --with raises an error', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['capability:social'],
excludeComponentIds: ['capability:content'],
}),
/depends on excluded module/
);
})) passed++; else failed++;
// ─── Validation Errors ───
if (test('throws for unknown component ID in --with', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['lang:brainfuck-plus-plus'],
}),
/Unknown install component/
);
})) passed++; else failed++;
if (test('throws for unknown component ID in --without', () => {
assert.throws(
() => resolveInstallPlan({
profileId: 'core',
excludeComponentIds: ['capability:teleportation'],
}),
/Unknown install component/
);
})) passed++; else failed++;
if (test('throws when all modules are excluded', () => {
assert.throws(
() => resolveInstallPlan({
profileId: 'core',
excludeComponentIds: [
'baseline:rules',
'baseline:agents',
'baseline:commands',
'baseline:hooks',
'baseline:platform',
'baseline:workflow',
],
target: 'claude',
}),
/excludes every requested install module/
);
})) passed++; else failed++;
// ─── Target-Specific Behavior ───
if (test('--with respects target compatibility filtering', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['capability:orchestration'],
target: 'cursor',
});
// orchestration module only supports claude, codex, opencode
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should skip orchestration for cursor target');
assert.ok(plan.skippedModuleIds.includes('orchestration'),
'Should report orchestration as skipped for cursor');
})) passed++; else failed++;
if (test('--without with agent: component excludes the agent module', () => {
const plan = resolveInstallPlan({
profileId: 'core',
excludeComponentIds: ['agent:security-reviewer'],
target: 'claude',
});
// agent:security-reviewer maps to agents-core module
// Since core profile includes agents-core and it is excluded, it should be gone
assert.ok(!plan.selectedModuleIds.includes('agents-core'),
'Should exclude agents-core when agent:security-reviewer is excluded');
assert.ok(plan.excludedModuleIds.includes('agents-core'),
'Should report agents-core as excluded');
})) passed++; else failed++;
if (test('--with agent: component includes the agents-core module', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['agent:security-reviewer'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('agents-core'),
'Should include agents-core module from agent:security-reviewer');
})) passed++; else failed++;
if (test('--with skill: component includes the parent skill module', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['skill:continuous-learning'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('workflow-quality'),
'Should include workflow-quality module from skill:continuous-learning');
})) passed++; else failed++;
// ─── Help Text ───
if (test('help text documents --with and --without flags', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const result = execFileSync('node', [scriptPath, '--help'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('--with'), 'Help should mention --with');
assert.ok(result.includes('--without'), 'Help should mention --without');
assert.ok(result.includes('component'), 'Help should describe components');
})) passed++; else failed++;
// ─── End-to-End Dry-Run ───
if (test('end-to-end: --profile developer --with capability:security --without capability:orchestration --dry-run', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--profile', 'developer',
'--with', 'capability:security',
'--without', 'capability:orchestration',
'--dry-run',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode');
assert.ok(result.includes('Profile: developer'), 'Should show developer profile');
assert.ok(result.includes('capability:security'), 'Should show included component');
assert.ok(result.includes('capability:orchestration'), 'Should show excluded component');
assert.ok(result.includes('security'), 'Selected modules should include security');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with lang:python --with agent:security-reviewer --dry-run', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--with', 'lang:python',
'--with', 'agent:security-reviewer',
'--dry-run',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode');
assert.ok(result.includes('lang:python'), 'Should show lang:python as included');
assert.ok(result.includes('agent:security-reviewer'), 'Should show agent:security-reviewer as included');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with with unknown component fails cleanly', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
let exitCode = 0;
let stderr = '';
try {
execFileSync('node', [
scriptPath,
'--with', 'lang:nonexistent-language',
'--dry-run',
], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
exitCode = error.status || 1;
stderr = error.stderr || '';
}
assert.strictEqual(exitCode, 1, 'Should exit with error code 1');
assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component');
})) passed++; else failed++;
if (test('end-to-end: --without with unknown component fails cleanly', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
let exitCode = 0;
let stderr = '';
try {
execFileSync('node', [
scriptPath,
'--profile', 'core',
'--without', 'capability:nonexistent',
'--dry-run',
], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
exitCode = error.status || 1;
stderr = error.stderr || '';
}
assert.strictEqual(exitCode, 1, 'Should exit with error code 1');
assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component');
})) passed++; else failed++;
// ─── End-to-End Actual Install ───
if (test('end-to-end: installs --profile core --with capability:security and writes state', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
const _result = execFileSync('node', [
scriptPath,
'--profile', 'core',
'--with', 'capability:security',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// Security skill should be installed (from --with)
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'security-review', 'SKILL.md')),
'Should install security-review skill from --with');
// Core profile modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install core rules');
// Install state should record include/exclude
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, 'core');
assert.deepStrictEqual(state.request.includeComponents, ['capability:security']);
assert.deepStrictEqual(state.request.excludeComponents, []);
assert.ok(state.resolution.selectedModules.includes('security'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: installs --profile developer --without capability:orchestration and state reflects exclusion', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
execFileSync('node', [
scriptPath,
'--profile', 'developer',
'--without', 'capability:orchestration',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// Orchestration skills should NOT be installed (from --without)
assert.ok(!fs.existsSync(path.join(claudeRoot, 'skills', 'dmux-workflows', 'SKILL.md')),
'Should not install orchestration skills');
// Developer profile base modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install core rules');
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')),
'Should install workflow skills');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, 'developer');
assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']);
assert.ok(!state.resolution.selectedModules.includes('orchestration'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with alone (no profile) installs just the component modules', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
execFileSync('node', [
scriptPath,
'--with', 'lang:typescript',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// framework-language skill (from lang:typescript) should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')),
'Should install framework-language skills');
// Its dependencies should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install dependency rules-core');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, null);
assert.deepStrictEqual(state.request.includeComponents, ['lang:typescript']);
assert.ok(state.resolution.selectedModules.includes('framework-language'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── JSON output mode ───
if (test('end-to-end: --dry-run --json includes component selections in output', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const output = execFileSync('node', [
scriptPath,
'--profile', 'core',
'--with', 'capability:database',
'--without', 'baseline:hooks',
'--dry-run',
'--json',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const json = JSON.parse(output);
assert.strictEqual(json.dryRun, true);
assert.ok(json.plan, 'Should include plan object');
assert.ok(
json.plan.includedComponentIds.includes('capability:database'),
'JSON output should include capability:database in included components'
);
assert.ok(
json.plan.excludedComponentIds.includes('baseline:hooks'),
'JSON output should include baseline:hooks in excluded components'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,578 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
getFallbackSessionRecordingPath,
persistCanonicalSnapshot
} = require('../../scripts/lib/session-adapters/canonical-session');
const { createClaudeHistoryAdapter } = require('../../scripts/lib/session-adapters/claude-history');
const { createDmuxTmuxAdapter } = require('../../scripts/lib/session-adapters/dmux-tmux');
const {
createAdapterRegistry,
inspectSessionTarget
} = require('../../scripts/lib/session-adapters/registry');
console.log('=== Testing session-adapters ===\n');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed += 1;
} catch (error) {
console.log(`${name}: ${error.message}`);
failed += 1;
}
}
function withHome(homeDir, fn) {
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
process.env.HOME = homeDir;
process.env.USERPROFILE = homeDir;
try {
fn();
} finally {
if (typeof previousHome === 'string') {
process.env.HOME = previousHome;
} else {
delete process.env.HOME;
}
if (typeof previousUserProfile === 'string') {
process.env.USERPROFILE = previousUserProfile;
} else {
delete process.env.USERPROFILE;
}
}
}
test('dmux adapter normalizes orchestration snapshots into canonical form', () => {
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
try {
const recentUpdated = new Date(Date.now() - 60000).toISOString();
const adapter = createDmuxTmuxAdapter({
loadStateStoreImpl: () => null,
collectSessionSnapshotImpl: () => ({
sessionName: 'workflow-visual-proof',
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
repoRoot: '/tmp/repo',
targetType: 'plan',
sessionActive: true,
paneCount: 1,
workerCount: 1,
workerStates: { running: 1 },
panes: [{
paneId: '%95',
windowIndex: 1,
paneIndex: 0,
title: 'seed-check',
currentCommand: 'codex',
currentPath: '/tmp/worktree',
active: false,
dead: false,
pid: 1234
}],
workers: [{
workerSlug: 'seed-check',
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
status: {
state: 'running',
updated: recentUpdated,
branch: 'feature/seed-check',
worktree: '/tmp/worktree',
taskFile: '/tmp/task.md',
handoffFile: '/tmp/handoff.md'
},
task: {
objective: 'Inspect seeded files.',
seedPaths: ['scripts/orchestrate-worktrees.js']
},
handoff: {
summary: ['Pending'],
validation: [],
remainingRisks: ['No screenshot yet']
},
files: {
status: '/tmp/status.md',
task: '/tmp/task.md',
handoff: '/tmp/handoff.md'
},
pane: {
paneId: '%95',
title: 'seed-check'
}
}]
}),
recordingDir
});
const snapshot = adapter.open('workflow-visual-proof').getSnapshot();
const recordingPath = getFallbackSessionRecordingPath(snapshot, { recordingDir });
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
assert.strictEqual(snapshot.schemaVersion, 'ecc.session.v1');
assert.strictEqual(snapshot.adapterId, 'dmux-tmux');
assert.strictEqual(snapshot.session.id, 'workflow-visual-proof');
assert.strictEqual(snapshot.session.kind, 'orchestrated');
assert.strictEqual(snapshot.session.state, 'active');
assert.strictEqual(snapshot.session.sourceTarget.type, 'session');
assert.strictEqual(snapshot.aggregates.workerCount, 1);
assert.strictEqual(snapshot.workers[0].health, 'healthy');
assert.strictEqual(snapshot.workers[0].runtime.kind, 'tmux-pane');
assert.strictEqual(snapshot.workers[0].outputs.remainingRisks[0], 'No screenshot yet');
assert.strictEqual(persisted.latest.session.state, 'active');
assert.strictEqual(persisted.latest.adapterId, 'dmux-tmux');
assert.strictEqual(persisted.history.length, 1);
} finally {
fs.rmSync(recordingDir, { recursive: true, force: true });
}
});
test('dmux adapter marks finished sessions as completed and records history', () => {
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
try {
const adapter = createDmuxTmuxAdapter({
loadStateStoreImpl: () => null,
collectSessionSnapshotImpl: () => ({
sessionName: 'workflow-visual-proof',
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
repoRoot: '/tmp/repo',
targetType: 'session',
sessionActive: false,
paneCount: 0,
workerCount: 2,
workerStates: { completed: 2 },
panes: [],
workers: [{
workerSlug: 'seed-check',
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
status: {
state: 'completed',
updated: '2026-03-13T00:00:00Z',
branch: 'feature/seed-check',
worktree: '/tmp/worktree-a',
taskFile: '/tmp/task-a.md',
handoffFile: '/tmp/handoff-a.md'
},
task: {
objective: 'Inspect seeded files.',
seedPaths: ['scripts/orchestrate-worktrees.js']
},
handoff: {
summary: ['Finished'],
validation: ['Reviewed outputs'],
remainingRisks: []
},
files: {
status: '/tmp/status-a.md',
task: '/tmp/task-a.md',
handoff: '/tmp/handoff-a.md'
},
pane: null
}, {
workerSlug: 'proof',
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/proof',
status: {
state: 'completed',
updated: '2026-03-13T00:10:00Z',
branch: 'feature/proof',
worktree: '/tmp/worktree-b',
taskFile: '/tmp/task-b.md',
handoffFile: '/tmp/handoff-b.md'
},
task: {
objective: 'Capture proof.',
seedPaths: ['README.md']
},
handoff: {
summary: ['Delivered proof'],
validation: ['Checked screenshots'],
remainingRisks: []
},
files: {
status: '/tmp/status-b.md',
task: '/tmp/task-b.md',
handoff: '/tmp/handoff-b.md'
},
pane: null
}]
}),
recordingDir
});
const snapshot = adapter.open('workflow-visual-proof').getSnapshot();
const recordingPath = getFallbackSessionRecordingPath(snapshot, { recordingDir });
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
assert.strictEqual(snapshot.session.state, 'completed');
assert.strictEqual(snapshot.aggregates.states.completed, 2);
assert.strictEqual(snapshot.workers[0].health, 'healthy');
assert.strictEqual(snapshot.workers[1].health, 'healthy');
assert.strictEqual(persisted.latest.session.state, 'completed');
assert.strictEqual(persisted.history.length, 1);
} finally {
fs.rmSync(recordingDir, { recursive: true, force: true });
}
});
test('fallback recording does not append duplicate history entries for unchanged snapshots', () => {
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
try {
const adapter = createDmuxTmuxAdapter({
loadStateStoreImpl: () => null,
collectSessionSnapshotImpl: () => ({
sessionName: 'workflow-visual-proof',
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
repoRoot: '/tmp/repo',
targetType: 'session',
sessionActive: true,
paneCount: 1,
workerCount: 1,
workerStates: { running: 1 },
panes: [],
workers: [{
workerSlug: 'seed-check',
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
status: {
state: 'running',
updated: '2026-03-13T00:00:00Z',
branch: 'feature/seed-check',
worktree: '/tmp/worktree',
taskFile: '/tmp/task.md',
handoffFile: '/tmp/handoff.md'
},
task: {
objective: 'Inspect seeded files.',
seedPaths: ['scripts/orchestrate-worktrees.js']
},
handoff: {
summary: ['Pending'],
validation: [],
remainingRisks: []
},
files: {
status: '/tmp/status.md',
task: '/tmp/task.md',
handoff: '/tmp/handoff.md'
},
pane: null
}]
}),
recordingDir
});
const handle = adapter.open('workflow-visual-proof');
const firstSnapshot = handle.getSnapshot();
const secondSnapshot = handle.getSnapshot();
const recordingPath = getFallbackSessionRecordingPath(firstSnapshot, { recordingDir });
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
assert.deepStrictEqual(secondSnapshot, firstSnapshot);
assert.strictEqual(persisted.history.length, 1);
assert.deepStrictEqual(persisted.latest, secondSnapshot);
} finally {
fs.rmSync(recordingDir, { recursive: true, force: true });
}
});
test('claude-history adapter loads the latest recorded session', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-adapter-home-'));
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
const sessionPath = path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp');
fs.writeFileSync(sessionPath, [
'# Session Review',
'',
'**Date:** 2026-03-13',
'**Started:** 09:00',
'**Last Updated:** 11:30',
'**Project:** everything-claude-code',
'**Branch:** feat/session-adapter',
'**Worktree:** /tmp/ecc-worktree',
'',
'### Completed',
'- [x] Build snapshot prototype',
'',
'### In Progress',
'- [ ] Add CLI wrapper',
'',
'### Notes for Next Session',
'Need a second adapter.',
'',
'### Context to Load',
'```',
'scripts/lib/orchestration-session.js',
'```'
].join('\n'));
try {
withHome(homeDir, () => {
const adapter = createClaudeHistoryAdapter({
loadStateStoreImpl: () => null,
recordingDir
});
const snapshot = adapter.open('claude:latest').getSnapshot();
const recordingPath = getFallbackSessionRecordingPath(snapshot, { recordingDir });
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
assert.strictEqual(snapshot.schemaVersion, 'ecc.session.v1');
assert.strictEqual(snapshot.adapterId, 'claude-history');
assert.strictEqual(snapshot.session.kind, 'history');
assert.strictEqual(snapshot.session.state, 'recorded');
assert.strictEqual(snapshot.workers.length, 1);
assert.strictEqual(snapshot.workers[0].branch, 'feat/session-adapter');
assert.strictEqual(snapshot.workers[0].worktree, '/tmp/ecc-worktree');
assert.strictEqual(snapshot.workers[0].runtime.kind, 'claude-session');
assert.deepStrictEqual(snapshot.workers[0].intent.seedPaths, ['scripts/lib/orchestration-session.js']);
assert.strictEqual(snapshot.workers[0].artifacts.sessionFile, sessionPath);
assert.ok(snapshot.workers[0].outputs.summary.includes('Build snapshot prototype'));
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 });
}
});
test('adapter registry routes plan files to dmux and explicit claude targets to history', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-registry-repo-'));
const planPath = path.join(repoRoot, 'workflow.json');
fs.writeFileSync(planPath, JSON.stringify({
sessionName: 'workflow-visual-proof',
repoRoot,
coordinationRoot: path.join(repoRoot, '.claude', 'orchestration')
}));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-registry-home-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-z9y8x7w6-session.tmp'),
'# History Session\n\n**Branch:** feat/history\n'
);
try {
withHome(homeDir, () => {
const registry = createAdapterRegistry({
adapters: [
createDmuxTmuxAdapter({
loadStateStoreImpl: () => null,
collectSessionSnapshotImpl: () => ({
sessionName: 'workflow-visual-proof',
coordinationDir: path.join(repoRoot, '.claude', 'orchestration', 'workflow-visual-proof'),
repoRoot,
targetType: 'plan',
sessionActive: false,
paneCount: 0,
workerCount: 0,
workerStates: {},
panes: [],
workers: []
})
}),
createClaudeHistoryAdapter({ loadStateStoreImpl: () => null })
]
});
const dmuxSnapshot = registry.open(planPath, { cwd: repoRoot }).getSnapshot();
const claudeSnapshot = registry.open('claude:latest', { cwd: repoRoot }).getSnapshot();
assert.strictEqual(dmuxSnapshot.adapterId, 'dmux-tmux');
assert.strictEqual(claudeSnapshot.adapterId, 'claude-history');
});
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
test('adapter registry resolves structured target types into the correct adapter', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-typed-repo-'));
const planPath = path.join(repoRoot, 'workflow.json');
fs.writeFileSync(planPath, JSON.stringify({
sessionName: 'workflow-typed-proof',
repoRoot,
coordinationRoot: path.join(repoRoot, '.claude', 'orchestration')
}));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-typed-home-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-z9y8x7w6-session.tmp'),
'# Typed History Session\n\n**Branch:** feat/typed-targets\n'
);
try {
withHome(homeDir, () => {
const registry = createAdapterRegistry({
adapters: [
createDmuxTmuxAdapter({
loadStateStoreImpl: () => null,
collectSessionSnapshotImpl: () => ({
sessionName: 'workflow-typed-proof',
coordinationDir: path.join(repoRoot, '.claude', 'orchestration', 'workflow-typed-proof'),
repoRoot,
targetType: 'plan',
sessionActive: true,
paneCount: 0,
workerCount: 0,
workerStates: {},
panes: [],
workers: []
})
}),
createClaudeHistoryAdapter({ loadStateStoreImpl: () => null })
]
});
const dmuxSnapshot = registry.open({ type: 'plan', value: planPath }, { cwd: repoRoot }).getSnapshot();
const claudeSnapshot = registry.open({ type: 'claude-history', value: 'latest' }, { cwd: repoRoot }).getSnapshot();
assert.strictEqual(dmuxSnapshot.adapterId, 'dmux-tmux');
assert.strictEqual(dmuxSnapshot.session.sourceTarget.type, 'plan');
assert.strictEqual(claudeSnapshot.adapterId, 'claude-history');
assert.strictEqual(claudeSnapshot.session.sourceTarget.type, 'claude-history');
assert.strictEqual(claudeSnapshot.workers[0].branch, 'feat/typed-targets');
});
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
test('default registry forwards a nested state-store writer to adapters', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-registry-home-'));
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-z9y8x7w6-session.tmp'),
'# History Session\n\n**Branch:** feat/history\n'
);
const stateStore = {
sessions: {
persisted: [],
persistCanonicalSessionSnapshot(snapshot, metadata) {
this.persisted.push({ snapshot, metadata });
}
}
};
try {
withHome(homeDir, () => {
const snapshot = inspectSessionTarget('claude:latest', {
cwd: process.cwd(),
stateStore
});
assert.strictEqual(snapshot.adapterId, 'claude-history');
assert.strictEqual(stateStore.sessions.persisted.length, 1);
assert.strictEqual(stateStore.sessions.persisted[0].snapshot.adapterId, 'claude-history');
assert.strictEqual(stateStore.sessions.persisted[0].metadata.sessionId, snapshot.session.id);
});
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
test('adapter registry lists adapter metadata and target types', () => {
const registry = createAdapterRegistry();
const adapters = registry.listAdapters();
const ids = adapters.map(adapter => adapter.id);
assert.ok(ids.includes('claude-history'));
assert.ok(ids.includes('dmux-tmux'));
assert.ok(
adapters.some(adapter => adapter.id === 'claude-history' && adapter.targetTypes.includes('claude-history')),
'claude-history should advertise its canonical target type'
);
assert.ok(
adapters.some(adapter => adapter.id === 'dmux-tmux' && adapter.targetTypes.includes('plan')),
'dmux-tmux should advertise plan targets'
);
});
test('persistence only falls back when the state-store module is missing', () => {
const snapshot = {
schemaVersion: 'ecc.session.v1',
adapterId: 'claude-history',
session: {
id: 'a1b2c3d4',
kind: 'history',
state: 'recorded',
repoRoot: null,
sourceTarget: {
type: 'claude-history',
value: 'latest'
}
},
workers: [{
id: 'a1b2c3d4',
label: 'Session Review',
state: 'recorded',
health: 'healthy',
branch: null,
worktree: null,
runtime: {
kind: 'claude-session',
command: 'claude',
pid: null,
active: false,
dead: true
},
intent: {
objective: 'Session Review',
seedPaths: []
},
outputs: {
summary: [],
validation: [],
remainingRisks: []
},
artifacts: {
sessionFile: '/tmp/session.tmp',
context: null
}
}],
aggregates: {
workerCount: 1,
states: {
recorded: 1
},
healths: {
healthy: 1
}
}
};
const loadError = new Error('state-store bootstrap failed');
loadError.code = 'ERR_STATE_STORE_BOOT';
assert.throws(() => {
persistCanonicalSnapshot(snapshot, {
loadStateStoreImpl() {
throw loadError;
}
});
}, /state-store bootstrap failed/);
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
'use strict';
const assert = require('assert');
const { splitShellSegments } = require('../../scripts/lib/shell-split');
console.log('=== Testing shell-split.js ===\n');
let passed = 0;
let failed = 0;
function test(desc, fn) {
try {
fn();
console.log(`${desc}`);
passed++;
} catch (e) {
console.log(`${desc}: ${e.message}`);
failed++;
}
}
// Basic operators
console.log('Basic operators:');
test('&& splits into two segments', () => {
assert.deepStrictEqual(splitShellSegments('echo hi && echo bye'), ['echo hi', 'echo bye']);
});
test('|| splits into two segments', () => {
assert.deepStrictEqual(splitShellSegments('echo hi || echo bye'), ['echo hi', 'echo bye']);
});
test('; splits into two segments', () => {
assert.deepStrictEqual(splitShellSegments('echo hi; echo bye'), ['echo hi', 'echo bye']);
});
test('single & splits (background)', () => {
assert.deepStrictEqual(splitShellSegments('sleep 1 & echo hi'), ['sleep 1', 'echo hi']);
});
// Redirection operators should NOT split
console.log('\nRedirection operators (should NOT split):');
test('2>&1 stays as one segment', () => {
const segs = splitShellSegments('cmd 2>&1 | grep error');
assert.strictEqual(segs.length, 1);
});
test('&> stays as one segment', () => {
const segs = splitShellSegments('cmd &> /dev/null');
assert.strictEqual(segs.length, 1);
});
test('>& stays as one segment', () => {
const segs = splitShellSegments('cmd >& /dev/null');
assert.strictEqual(segs.length, 1);
});
// Quoting
console.log('\nQuoting:');
test('double-quoted && not split', () => {
const segs = splitShellSegments('tmux new -d "cd /app && echo hi"');
assert.strictEqual(segs.length, 1);
});
test('single-quoted && not split', () => {
const segs = splitShellSegments("tmux new -d 'cd /app && echo hi'");
assert.strictEqual(segs.length, 1);
});
test('double-quoted ; not split', () => {
const segs = splitShellSegments('echo "hello; world"');
assert.strictEqual(segs.length, 1);
});
// Escaped quotes
console.log('\nEscaped quotes:');
test('escaped double quote inside double quotes', () => {
const segs = splitShellSegments('echo "hello \\"world\\"" && echo bye');
assert.strictEqual(segs.length, 2);
});
test('escaped single quote inside single quotes', () => {
const segs = splitShellSegments("echo 'hello \\'world\\'' && echo bye");
assert.strictEqual(segs.length, 2);
});
// Escaped operators outside quotes
console.log('\nEscaped operators outside quotes:');
test('escaped && outside quotes not split', () => {
const segs = splitShellSegments('tmux new-session -d bash -lc cd /app \\&\\& npm run dev');
assert.strictEqual(segs.length, 1);
});
test('escaped ; outside quotes not split', () => {
const segs = splitShellSegments('echo hello \\; echo bye');
assert.strictEqual(segs.length, 1);
});
// Complex real-world cases
console.log('\nReal-world cases:');
test('tmux new-session with quoted compound command', () => {
const segs = splitShellSegments('tmux new-session -d -s dev "cd /app && npm run dev"');
assert.strictEqual(segs.length, 1);
assert.ok(segs[0].includes('tmux'));
assert.ok(segs[0].includes('npm run dev'));
});
test('chained: tmux ls then bare dev', () => {
const segs = splitShellSegments('tmux ls; npm run dev');
assert.strictEqual(segs.length, 2);
assert.strictEqual(segs[1], 'npm run dev');
});
test('background dev server', () => {
const segs = splitShellSegments('npm run dev & echo started');
assert.strictEqual(segs.length, 2);
assert.strictEqual(segs[0], 'npm run dev');
});
test('empty string returns empty array', () => {
assert.deepStrictEqual(splitShellSegments(''), []);
});
test('single command no operators', () => {
assert.deepStrictEqual(splitShellSegments('npm run dev'), ['npm run dev']);
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

View File

@@ -0,0 +1,454 @@
/**
* Tests for skill health dashboard.
*
* Run with: node tests/lib/skill-dashboard.test.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const dashboard = require('../../scripts/lib/skill-evolution/dashboard');
const versioning = require('../../scripts/lib/skill-evolution/versioning');
const _provenance = require('../../scripts/lib/skill-evolution/provenance');
const HEALTH_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'skills-health.js');
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 cleanupTempDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function createSkill(skillRoot, name, content) {
const skillDir = path.join(skillRoot, name);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
return skillDir;
}
function appendJsonl(filePath, rows) {
const lines = rows.map(row => JSON.stringify(row)).join('\n');
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${lines}\n`);
}
function runCli(args) {
return spawnSync(process.execPath, [HEALTH_SCRIPT, ...args], {
encoding: 'utf8',
});
}
function runTests() {
console.log('\n=== Testing skill dashboard ===\n');
let passed = 0;
let failed = 0;
const repoRoot = createTempDir('skill-dashboard-repo-');
const homeDir = createTempDir('skill-dashboard-home-');
const skillsRoot = path.join(repoRoot, 'skills');
const learnedRoot = path.join(homeDir, '.claude', 'skills', 'learned');
const importedRoot = path.join(homeDir, '.claude', 'skills', 'imported');
const runsFile = path.join(homeDir, '.claude', 'state', 'skill-runs.jsonl');
const now = '2026-03-15T12:00:00.000Z';
fs.mkdirSync(skillsRoot, { recursive: true });
fs.mkdirSync(learnedRoot, { recursive: true });
fs.mkdirSync(importedRoot, { recursive: true });
try {
console.log('Chart primitives:');
if (test('sparkline maps float values to Unicode block characters', () => {
const result = dashboard.sparkline([1, 0.5, 0]);
assert.strictEqual(result.length, 3);
assert.strictEqual(result[0], '\u2588');
assert.strictEqual(result[2], '\u2581');
})) passed++; else failed++;
if (test('sparkline returns empty string for empty array', () => {
assert.strictEqual(dashboard.sparkline([]), '');
})) passed++; else failed++;
if (test('sparkline renders null values as empty block', () => {
const result = dashboard.sparkline([null, 0.5, null]);
assert.strictEqual(result[0], '\u2591');
assert.strictEqual(result[2], '\u2591');
assert.strictEqual(result.length, 3);
})) passed++; else failed++;
if (test('horizontalBar renders correct fill ratio', () => {
const result = dashboard.horizontalBar(5, 10, 10);
const filled = (result.match(/\u2588/g) || []).length;
const empty = (result.match(/\u2591/g) || []).length;
assert.strictEqual(filled, 5);
assert.strictEqual(empty, 5);
assert.strictEqual(result.length, 10);
})) passed++; else failed++;
if (test('horizontalBar handles zero value', () => {
const result = dashboard.horizontalBar(0, 10, 10);
const filled = (result.match(/\u2588/g) || []).length;
assert.strictEqual(filled, 0);
assert.strictEqual(result.length, 10);
})) passed++; else failed++;
if (test('panelBox renders box-drawing characters with title', () => {
const result = dashboard.panelBox('Test Panel', ['line one', 'line two'], 30);
assert.match(result, /\u250C/);
assert.match(result, /\u2510/);
assert.match(result, /\u2514/);
assert.match(result, /\u2518/);
assert.match(result, /Test Panel/);
assert.match(result, /line one/);
assert.match(result, /line two/);
})) passed++; else failed++;
console.log('\nTime-series bucketing:');
if (test('bucketByDay groups records into daily bins', () => {
const nowMs = Date.parse(now);
const records = [
{ skill_id: 'alpha', outcome: 'success', recorded_at: '2026-03-15T10:00:00.000Z' },
{ skill_id: 'alpha', outcome: 'failure', recorded_at: '2026-03-15T08:00:00.000Z' },
{ skill_id: 'alpha', outcome: 'success', recorded_at: '2026-03-14T10:00:00.000Z' },
];
const buckets = dashboard.bucketByDay(records, nowMs, 3);
assert.strictEqual(buckets.length, 3);
const todayBucket = buckets[buckets.length - 1];
assert.strictEqual(todayBucket.runs, 2);
assert.strictEqual(todayBucket.rate, 0.5);
})) passed++; else failed++;
if (test('bucketByDay returns null rate for empty days', () => {
const nowMs = Date.parse(now);
const buckets = dashboard.bucketByDay([], nowMs, 5);
assert.strictEqual(buckets.length, 5);
for (const bucket of buckets) {
assert.strictEqual(bucket.rate, null);
assert.strictEqual(bucket.runs, 0);
}
})) passed++; else failed++;
console.log('\nPanel renderers:');
const alphaSkillDir = createSkill(skillsRoot, 'alpha', '# Alpha\n');
const betaSkillDir = createSkill(learnedRoot, 'beta', '# Beta\n');
versioning.createVersion(alphaSkillDir, {
timestamp: '2026-03-14T11:00:00.000Z',
author: 'observer',
reason: 'bootstrap',
});
fs.writeFileSync(path.join(alphaSkillDir, 'SKILL.md'), '# Alpha v2\n');
versioning.createVersion(alphaSkillDir, {
timestamp: '2026-03-15T11:00:00.000Z',
author: 'observer',
reason: 'accepted-amendment',
});
versioning.createVersion(betaSkillDir, {
timestamp: '2026-03-14T11:00:00.000Z',
author: 'observer',
reason: 'bootstrap',
});
const { appendFile } = require('../../scripts/lib/utils');
const alphaAmendmentsPath = path.join(alphaSkillDir, '.evolution', 'amendments.jsonl');
appendFile(alphaAmendmentsPath, JSON.stringify({
event: 'proposal',
status: 'pending',
created_at: '2026-03-15T07:00:00.000Z',
}) + '\n');
appendJsonl(runsFile, [
{
skill_id: 'alpha',
skill_version: 'v2',
task_description: 'Success task',
outcome: 'success',
failure_reason: null,
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'accepted',
recorded_at: '2026-03-14T10:00:00.000Z',
},
{
skill_id: 'alpha',
skill_version: 'v2',
task_description: 'Failed task',
outcome: 'failure',
failure_reason: 'Regression',
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'rejected',
recorded_at: '2026-03-13T10:00:00.000Z',
},
{
skill_id: 'alpha',
skill_version: 'v1',
task_description: 'Older success',
outcome: 'success',
failure_reason: null,
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'accepted',
recorded_at: '2026-02-20T10:00:00.000Z',
},
{
skill_id: 'beta',
skill_version: 'v1',
task_description: 'Beta success',
outcome: 'success',
failure_reason: null,
tokens_used: 90,
duration_ms: 800,
user_feedback: 'accepted',
recorded_at: '2026-03-15T09:00:00.000Z',
},
{
skill_id: 'beta',
skill_version: 'v1',
task_description: 'Beta failure',
outcome: 'failure',
failure_reason: 'Bad import',
tokens_used: 90,
duration_ms: 800,
user_feedback: 'corrected',
recorded_at: '2026-02-20T09:00:00.000Z',
},
]);
const testRecords = [
{ skill_id: 'alpha', outcome: 'success', failure_reason: null, recorded_at: '2026-03-14T10:00:00.000Z' },
{ skill_id: 'alpha', outcome: 'failure', failure_reason: 'Regression', recorded_at: '2026-03-13T10:00:00.000Z' },
{ skill_id: 'alpha', outcome: 'success', failure_reason: null, recorded_at: '2026-02-20T10:00:00.000Z' },
{ skill_id: 'beta', outcome: 'success', failure_reason: null, recorded_at: '2026-03-15T09:00:00.000Z' },
{ skill_id: 'beta', outcome: 'failure', failure_reason: 'Bad import', recorded_at: '2026-02-20T09:00:00.000Z' },
];
if (test('renderSuccessRatePanel produces one row per skill with sparklines', () => {
const skills = [{ skill_id: 'alpha' }, { skill_id: 'beta' }];
const result = dashboard.renderSuccessRatePanel(testRecords, skills, { now });
assert.ok(result.text.includes('Success Rate'));
assert.ok(result.data.skills.length >= 2);
const alpha = result.data.skills.find(s => s.skill_id === 'alpha');
assert.ok(alpha);
assert.ok(Array.isArray(alpha.daily_rates));
assert.strictEqual(alpha.daily_rates.length, 30);
assert.ok(typeof alpha.sparkline === 'string');
assert.ok(alpha.sparkline.length > 0);
})) passed++; else failed++;
if (test('renderFailureClusterPanel groups failures by reason', () => {
const failureRecords = [
{ skill_id: 'alpha', outcome: 'failure', failure_reason: 'Regression' },
{ skill_id: 'alpha', outcome: 'failure', failure_reason: 'Regression' },
{ skill_id: 'beta', outcome: 'failure', failure_reason: 'Bad import' },
{ skill_id: 'alpha', outcome: 'success', failure_reason: null },
];
const result = dashboard.renderFailureClusterPanel(failureRecords);
assert.ok(result.text.includes('Failure Patterns'));
assert.strictEqual(result.data.clusters.length, 2);
assert.strictEqual(result.data.clusters[0].pattern, 'regression');
assert.strictEqual(result.data.clusters[0].count, 2);
assert.strictEqual(result.data.total_failures, 3);
})) passed++; else failed++;
if (test('renderAmendmentPanel lists pending amendments', () => {
const skillsById = new Map();
skillsById.set('alpha', { skill_id: 'alpha', skill_dir: alphaSkillDir });
const result = dashboard.renderAmendmentPanel(skillsById);
assert.ok(result.text.includes('Pending Amendments'));
assert.ok(result.data.total >= 1);
assert.ok(result.data.amendments.some(a => a.skill_id === 'alpha'));
})) passed++; else failed++;
if (test('renderVersionTimelinePanel shows version history', () => {
const skillsById = new Map();
skillsById.set('alpha', { skill_id: 'alpha', skill_dir: alphaSkillDir });
skillsById.set('beta', { skill_id: 'beta', skill_dir: betaSkillDir });
const result = dashboard.renderVersionTimelinePanel(skillsById);
assert.ok(result.text.includes('Version History'));
assert.ok(result.data.skills.length >= 1);
const alphaVersions = result.data.skills.find(s => s.skill_id === 'alpha');
assert.ok(alphaVersions);
assert.ok(alphaVersions.versions.length >= 2);
})) passed++; else failed++;
console.log('\nFull dashboard:');
if (test('renderDashboard produces all four panels', () => {
const result = dashboard.renderDashboard({
skillsRoot,
learnedRoot,
importedRoot,
homeDir,
runsFilePath: runsFile,
now,
warnThreshold: 0.1,
});
assert.ok(result.text.includes('ECC Skill Health Dashboard'));
assert.ok(result.text.includes('Success Rate'));
assert.ok(result.text.includes('Failure Patterns'));
assert.ok(result.text.includes('Pending Amendments'));
assert.ok(result.text.includes('Version History'));
assert.ok(result.data.generated_at === now);
assert.ok(result.data.summary);
assert.ok(result.data.panels['success-rate']);
assert.ok(result.data.panels['failures']);
assert.ok(result.data.panels['amendments']);
assert.ok(result.data.panels['versions']);
})) passed++; else failed++;
if (test('renderDashboard supports single panel selection', () => {
const result = dashboard.renderDashboard({
skillsRoot,
learnedRoot,
importedRoot,
homeDir,
runsFilePath: runsFile,
now,
panel: 'failures',
});
assert.ok(result.text.includes('Failure Patterns'));
assert.ok(!result.text.includes('Version History'));
assert.ok(result.data.panels['failures']);
assert.ok(!result.data.panels['versions']);
})) passed++; else failed++;
if (test('renderDashboard rejects unknown panel names', () => {
assert.throws(() => {
dashboard.renderDashboard({
skillsRoot,
learnedRoot,
importedRoot,
homeDir,
runsFilePath: runsFile,
now,
panel: 'nonexistent',
});
}, /Unknown panel/);
})) passed++; else failed++;
console.log('\nCLI integration:');
if (test('CLI --dashboard --json returns valid JSON with all panels', () => {
const result = runCli([
'--dashboard',
'--json',
'--skills-root', skillsRoot,
'--learned-root', learnedRoot,
'--imported-root', importedRoot,
'--home', homeDir,
'--runs-file', runsFile,
'--now', now,
]);
assert.strictEqual(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim());
assert.ok(payload.panels);
assert.ok(payload.panels['success-rate']);
assert.ok(payload.panels['failures']);
assert.ok(payload.summary);
})) passed++; else failed++;
if (test('CLI --panel failures --json returns only the failures panel', () => {
const result = runCli([
'--dashboard',
'--panel', 'failures',
'--json',
'--skills-root', skillsRoot,
'--learned-root', learnedRoot,
'--imported-root', importedRoot,
'--home', homeDir,
'--runs-file', runsFile,
'--now', now,
]);
assert.strictEqual(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim());
assert.ok(payload.panels['failures']);
assert.ok(!payload.panels['versions']);
})) passed++; else failed++;
if (test('CLI --help mentions --dashboard', () => {
const result = runCli(['--help']);
assert.strictEqual(result.status, 0);
assert.match(result.stdout, /--dashboard/);
assert.match(result.stdout, /--panel/);
})) passed++; else failed++;
console.log('\nEdge cases:');
if (test('dashboard renders gracefully with no execution records', () => {
const emptyRunsFile = path.join(homeDir, '.claude', 'state', 'empty-runs.jsonl');
fs.mkdirSync(path.dirname(emptyRunsFile), { recursive: true });
fs.writeFileSync(emptyRunsFile, '', 'utf8');
const emptySkillsRoot = path.join(repoRoot, 'empty-skills');
fs.mkdirSync(emptySkillsRoot, { recursive: true });
const result = dashboard.renderDashboard({
skillsRoot: emptySkillsRoot,
learnedRoot: path.join(homeDir, '.claude', 'skills', 'empty-learned'),
importedRoot: path.join(homeDir, '.claude', 'skills', 'empty-imported'),
homeDir,
runsFilePath: emptyRunsFile,
now,
});
assert.ok(result.text.includes('ECC Skill Health Dashboard'));
assert.ok(result.text.includes('No failure patterns detected'));
assert.strictEqual(result.data.summary.total_skills, 0);
})) passed++; else failed++;
if (test('failure cluster panel handles all successes', () => {
const successRecords = [
{ skill_id: 'alpha', outcome: 'success', failure_reason: null },
{ skill_id: 'beta', outcome: 'success', failure_reason: null },
];
const result = dashboard.renderFailureClusterPanel(successRecords);
assert.strictEqual(result.data.clusters.length, 0);
assert.strictEqual(result.data.total_failures, 0);
assert.ok(result.text.includes('No failure patterns detected'));
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
} finally {
cleanupTempDir(repoRoot);
cleanupTempDir(homeDir);
}
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,536 @@
/**
* Tests for skill evolution helpers.
*
* Run with: node tests/lib/skill-evolution.test.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const provenance = require('../../scripts/lib/skill-evolution/provenance');
const versioning = require('../../scripts/lib/skill-evolution/versioning');
const tracker = require('../../scripts/lib/skill-evolution/tracker');
const health = require('../../scripts/lib/skill-evolution/health');
const skillEvolution = require('../../scripts/lib/skill-evolution');
const HEALTH_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'skills-health.js');
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 cleanupTempDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function createSkill(skillRoot, name, content) {
const skillDir = path.join(skillRoot, name);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
return skillDir;
}
function appendJsonl(filePath, rows) {
const lines = rows.map(row => JSON.stringify(row)).join('\n');
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${lines}\n`);
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function runCli(args, options = {}) {
return spawnSync(process.execPath, [HEALTH_SCRIPT, ...args], {
encoding: 'utf8',
env: {
...process.env,
...(options.env || {}),
},
});
}
function runTests() {
console.log('\n=== Testing skill evolution ===\n');
let passed = 0;
let failed = 0;
const repoRoot = createTempDir('skill-evolution-repo-');
const homeDir = createTempDir('skill-evolution-home-');
const skillsRoot = path.join(repoRoot, 'skills');
const learnedRoot = path.join(homeDir, '.claude', 'skills', 'learned');
const importedRoot = path.join(homeDir, '.claude', 'skills', 'imported');
const runsFile = path.join(homeDir, '.claude', 'state', 'skill-runs.jsonl');
const now = '2026-03-15T12:00:00.000Z';
fs.mkdirSync(skillsRoot, { recursive: true });
fs.mkdirSync(learnedRoot, { recursive: true });
fs.mkdirSync(importedRoot, { recursive: true });
try {
console.log('Provenance:');
if (test('classifies curated, learned, and imported skill directories', () => {
const curatedSkillDir = createSkill(skillsRoot, 'curated-alpha', '# Curated\n');
const learnedSkillDir = createSkill(learnedRoot, 'learned-beta', '# Learned\n');
const importedSkillDir = createSkill(importedRoot, 'imported-gamma', '# Imported\n');
const roots = provenance.getSkillRoots({ repoRoot, homeDir });
assert.strictEqual(roots.curated, skillsRoot);
assert.strictEqual(roots.learned, learnedRoot);
assert.strictEqual(roots.imported, importedRoot);
assert.strictEqual(
provenance.classifySkillPath(curatedSkillDir, { repoRoot, homeDir }),
provenance.SKILL_TYPES.CURATED
);
assert.strictEqual(
provenance.classifySkillPath(learnedSkillDir, { repoRoot, homeDir }),
provenance.SKILL_TYPES.LEARNED
);
assert.strictEqual(
provenance.classifySkillPath(importedSkillDir, { repoRoot, homeDir }),
provenance.SKILL_TYPES.IMPORTED
);
assert.strictEqual(
provenance.requiresProvenance(curatedSkillDir, { repoRoot, homeDir }),
false
);
assert.strictEqual(
provenance.requiresProvenance(learnedSkillDir, { repoRoot, homeDir }),
true
);
})) passed++; else failed++;
if (test('writes and validates provenance metadata for non-curated skills', () => {
const importedSkillDir = createSkill(importedRoot, 'imported-delta', '# Imported\n');
const provenanceRecord = {
source: 'https://example.com/skills/imported-delta',
created_at: '2026-03-15T10:00:00.000Z',
confidence: 0.86,
author: 'external-importer',
};
const writeResult = provenance.writeProvenance(importedSkillDir, provenanceRecord, {
repoRoot,
homeDir,
});
assert.strictEqual(writeResult.path, path.join(importedSkillDir, '.provenance.json'));
assert.deepStrictEqual(readJson(writeResult.path), provenanceRecord);
assert.deepStrictEqual(
provenance.readProvenance(importedSkillDir, { repoRoot, homeDir }),
provenanceRecord
);
assert.throws(
() => provenance.writeProvenance(importedSkillDir, {
source: 'bad',
created_at: '2026-03-15T10:00:00.000Z',
author: 'external-importer',
}, { repoRoot, homeDir }),
/confidence/
);
assert.throws(
() => provenance.readProvenance(path.join(learnedRoot, 'missing-provenance'), {
repoRoot,
homeDir,
required: true,
}),
/Missing provenance metadata/
);
})) passed++; else failed++;
if (test('exports the consolidated module surface from index.js', () => {
assert.strictEqual(skillEvolution.provenance, provenance);
assert.strictEqual(skillEvolution.versioning, versioning);
assert.strictEqual(skillEvolution.tracker, tracker);
assert.strictEqual(skillEvolution.health, health);
assert.strictEqual(typeof skillEvolution.collectSkillHealth, 'function');
assert.strictEqual(typeof skillEvolution.recordSkillExecution, 'function');
})) passed++; else failed++;
console.log('\nVersioning:');
if (test('creates version snapshots and evolution logs for a skill', () => {
const skillDir = createSkill(skillsRoot, 'alpha', '# Alpha v1\n');
const versionOne = versioning.createVersion(skillDir, {
timestamp: '2026-03-15T11:00:00.000Z',
reason: 'bootstrap',
author: 'observer',
});
assert.strictEqual(versionOne.version, 1);
assert.ok(fs.existsSync(path.join(skillDir, '.versions', 'v1.md')));
assert.ok(fs.existsSync(path.join(skillDir, '.evolution', 'observations.jsonl')));
assert.ok(fs.existsSync(path.join(skillDir, '.evolution', 'inspections.jsonl')));
assert.ok(fs.existsSync(path.join(skillDir, '.evolution', 'amendments.jsonl')));
assert.strictEqual(versioning.getCurrentVersion(skillDir), 1);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Alpha v2\n');
const versionTwo = versioning.createVersion(skillDir, {
timestamp: '2026-03-16T11:00:00.000Z',
reason: 'accepted-amendment',
author: 'observer',
});
assert.strictEqual(versionTwo.version, 2);
assert.deepStrictEqual(
versioning.listVersions(skillDir).map(entry => entry.version),
[1, 2]
);
const amendments = versioning.getEvolutionLog(skillDir, 'amendments');
assert.strictEqual(amendments.length, 2);
assert.strictEqual(amendments[0].event, 'snapshot');
assert.strictEqual(amendments[1].version, 2);
})) passed++; else failed++;
if (test('rolls back to a previous snapshot without losing history', () => {
const skillDir = path.join(skillsRoot, 'alpha');
const rollback = versioning.rollbackTo(skillDir, 1, {
timestamp: '2026-03-17T11:00:00.000Z',
author: 'maintainer',
reason: 'restore known-good version',
});
assert.strictEqual(rollback.version, 3);
assert.strictEqual(
fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'),
'# Alpha v1\n'
);
assert.deepStrictEqual(
versioning.listVersions(skillDir).map(entry => entry.version),
[1, 2, 3]
);
assert.strictEqual(versioning.getCurrentVersion(skillDir), 3);
const amendments = versioning.getEvolutionLog(skillDir, 'amendments');
const rollbackEntry = amendments[amendments.length - 1];
assert.strictEqual(rollbackEntry.event, 'rollback');
assert.strictEqual(rollbackEntry.target_version, 1);
assert.strictEqual(rollbackEntry.version, 3);
})) passed++; else failed++;
console.log('\nTracking:');
if (test('records skill execution rows to JSONL fallback storage', () => {
const result = tracker.recordSkillExecution({
skill_id: 'alpha',
skill_version: 'v3',
task_description: 'Fix flaky tests',
outcome: 'partial',
failure_reason: 'One integration test still flakes',
tokens_used: 812,
duration_ms: 4400,
user_feedback: 'corrected',
recorded_at: '2026-03-15T11:30:00.000Z',
}, {
runsFilePath: runsFile,
});
assert.strictEqual(result.storage, 'jsonl');
assert.strictEqual(result.path, runsFile);
const records = tracker.readSkillExecutionRecords({ runsFilePath: runsFile });
assert.strictEqual(records.length, 1);
assert.strictEqual(records[0].skill_id, 'alpha');
assert.strictEqual(records[0].task_description, 'Fix flaky tests');
assert.strictEqual(records[0].outcome, 'partial');
})) passed++; else failed++;
if (test('falls back to JSONL when a state-store adapter is unavailable', () => {
const result = tracker.recordSkillExecution({
skill_id: 'beta',
skill_version: 'v1',
task_description: 'Import external skill',
outcome: 'success',
failure_reason: null,
tokens_used: 215,
duration_ms: 900,
user_feedback: 'accepted',
recorded_at: '2026-03-15T11:35:00.000Z',
}, {
runsFilePath: runsFile,
stateStore: {
recordSkillExecution() {
throw new Error('state store offline');
},
},
});
assert.strictEqual(result.storage, 'jsonl');
assert.strictEqual(tracker.readSkillExecutionRecords({ runsFilePath: runsFile }).length, 2);
})) passed++; else failed++;
if (test('ignores malformed JSONL rows when reading execution records', () => {
const malformedRunsFile = path.join(homeDir, '.claude', 'state', 'malformed-skill-runs.jsonl');
fs.writeFileSync(
malformedRunsFile,
`${JSON.stringify({
skill_id: 'alpha',
skill_version: 'v3',
task_description: 'Good row',
outcome: 'success',
failure_reason: null,
tokens_used: 1,
duration_ms: 1,
user_feedback: 'accepted',
recorded_at: '2026-03-15T11:45:00.000Z',
})}\n{bad-json}\n`,
'utf8'
);
const records = tracker.readSkillExecutionRecords({ runsFilePath: malformedRunsFile });
assert.strictEqual(records.length, 1);
assert.strictEqual(records[0].skill_id, 'alpha');
})) passed++; else failed++;
if (test('preserves zero-valued telemetry fields during normalization', () => {
const record = tracker.normalizeExecutionRecord({
skill_id: 'zero-telemetry',
skill_version: 'v1',
task_description: 'No-op hook',
outcome: 'success',
tokens_used: 0,
duration_ms: 0,
user_feedback: 'accepted',
recorded_at: '2026-03-15T11:40:00.000Z',
});
assert.strictEqual(record.tokens_used, 0);
assert.strictEqual(record.duration_ms, 0);
})) passed++; else failed++;
console.log('\nHealth:');
if (test('computes per-skill health metrics and flags declining skills', () => {
const betaSkillDir = createSkill(learnedRoot, 'beta', '# Beta v1\n');
provenance.writeProvenance(betaSkillDir, {
source: 'observer://session/123',
created_at: '2026-03-14T10:00:00.000Z',
confidence: 0.72,
author: 'observer',
}, {
repoRoot,
homeDir,
});
versioning.createVersion(betaSkillDir, {
timestamp: '2026-03-14T11:00:00.000Z',
author: 'observer',
reason: 'bootstrap',
});
appendJsonl(path.join(skillsRoot, 'alpha', '.evolution', 'amendments.jsonl'), [
{
event: 'proposal',
status: 'pending',
created_at: '2026-03-15T07:00:00.000Z',
},
]);
appendJsonl(runsFile, [
{
skill_id: 'alpha',
skill_version: 'v3',
task_description: 'Recent success',
outcome: 'success',
failure_reason: null,
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'accepted',
recorded_at: '2026-03-14T10:00:00.000Z',
},
{
skill_id: 'alpha',
skill_version: 'v3',
task_description: 'Recent failure',
outcome: 'failure',
failure_reason: 'Regression',
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'rejected',
recorded_at: '2026-03-13T10:00:00.000Z',
},
{
skill_id: 'alpha',
skill_version: 'v2',
task_description: 'Prior success',
outcome: 'success',
failure_reason: null,
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'accepted',
recorded_at: '2026-03-06T10:00:00.000Z',
},
{
skill_id: 'alpha',
skill_version: 'v1',
task_description: 'Older success',
outcome: 'success',
failure_reason: null,
tokens_used: 100,
duration_ms: 1000,
user_feedback: 'accepted',
recorded_at: '2026-02-24T10:00:00.000Z',
},
{
skill_id: 'beta',
skill_version: 'v1',
task_description: 'Recent success',
outcome: 'success',
failure_reason: null,
tokens_used: 90,
duration_ms: 800,
user_feedback: 'accepted',
recorded_at: '2026-03-15T09:00:00.000Z',
},
{
skill_id: 'beta',
skill_version: 'v1',
task_description: 'Older failure',
outcome: 'failure',
failure_reason: 'Bad import',
tokens_used: 90,
duration_ms: 800,
user_feedback: 'corrected',
recorded_at: '2026-02-20T09:00:00.000Z',
},
]);
const report = health.collectSkillHealth({
repoRoot,
homeDir,
runsFilePath: runsFile,
now,
warnThreshold: 0.1,
});
const alpha = report.skills.find(skill => skill.skill_id === 'alpha');
const beta = report.skills.find(skill => skill.skill_id === 'beta');
assert.ok(alpha);
assert.ok(beta);
assert.strictEqual(alpha.current_version, 'v3');
assert.strictEqual(alpha.pending_amendments, 1);
assert.strictEqual(alpha.success_rate_7d, 0.5);
assert.strictEqual(alpha.success_rate_30d, 0.75);
assert.strictEqual(alpha.failure_trend, 'worsening');
assert.strictEqual(alpha.declining, true);
assert.strictEqual(beta.failure_trend, 'improving');
const summary = health.summarizeHealthReport(report);
assert.deepStrictEqual(summary, {
total_skills: 6,
healthy_skills: 5,
declining_skills: 1,
});
const human = health.formatHealthReport(report, { json: false });
assert.match(human, /alpha/);
assert.match(human, /worsening/);
assert.match(
human,
new RegExp(`Skills: ${summary.total_skills} total, ${summary.healthy_skills} healthy, ${summary.declining_skills} declining`)
);
})) passed++; else failed++;
if (test('treats an unsnapshotted SKILL.md as v1 and orders last_run by actual time', () => {
const gammaSkillDir = createSkill(skillsRoot, 'gamma', '# Gamma v1\n');
const offsetRunsFile = path.join(homeDir, '.claude', 'state', 'offset-skill-runs.jsonl');
appendJsonl(offsetRunsFile, [
{
skill_id: 'gamma',
skill_version: 'v1',
task_description: 'Offset timestamp run',
outcome: 'success',
failure_reason: null,
tokens_used: 10,
duration_ms: 100,
user_feedback: 'accepted',
recorded_at: '2026-03-15T00:00:00+02:00',
},
{
skill_id: 'gamma',
skill_version: 'v1',
task_description: 'UTC timestamp run',
outcome: 'success',
failure_reason: null,
tokens_used: 11,
duration_ms: 110,
user_feedback: 'accepted',
recorded_at: '2026-03-14T23:30:00Z',
},
]);
const report = health.collectSkillHealth({
repoRoot,
homeDir,
runsFilePath: offsetRunsFile,
now,
warnThreshold: 0.1,
});
const gamma = report.skills.find(skill => skill.skill_id === path.basename(gammaSkillDir));
assert.ok(gamma);
assert.strictEqual(gamma.current_version, 'v1');
assert.strictEqual(gamma.last_run, '2026-03-14T23:30:00Z');
})) passed++; else failed++;
if (test('CLI emits JSON health output for standalone integration', () => {
const result = runCli([
'--json',
'--skills-root', skillsRoot,
'--learned-root', learnedRoot,
'--imported-root', importedRoot,
'--home', homeDir,
'--runs-file', runsFile,
'--now', now,
'--warn-threshold', '0.1',
]);
assert.strictEqual(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim());
assert.ok(Array.isArray(payload.skills));
assert.strictEqual(payload.skills[0].skill_id, 'alpha');
assert.strictEqual(payload.skills[0].declining, true);
})) passed++; else failed++;
if (test('CLI shows help and rejects missing option values', () => {
const helpResult = runCli(['--help']);
assert.strictEqual(helpResult.status, 0);
assert.match(helpResult.stdout, /--learned-root <path>/);
assert.match(helpResult.stdout, /--imported-root <path>/);
const errorResult = runCli(['--skills-root']);
assert.strictEqual(errorResult.status, 1);
assert.match(errorResult.stderr, /Missing value for --skills-root/);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
} finally {
cleanupTempDir(repoRoot);
cleanupTempDir(homeDir);
}
}
runTests();

View File

@@ -0,0 +1,186 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
appendSkillObservation,
createSkillObservation,
getSkillObservationsPath,
readSkillObservations
} = require('../../scripts/lib/skill-improvement/observations');
const { buildSkillHealthReport } = require('../../scripts/lib/skill-improvement/health');
const { proposeSkillAmendment } = require('../../scripts/lib/skill-improvement/amendify');
const { buildSkillEvaluationScaffold } = require('../../scripts/lib/skill-improvement/evaluate');
console.log('=== Testing skill-improvement ===\n');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed += 1;
} catch (error) {
console.log(`${name}: ${error.message}`);
failed += 1;
}
}
function makeProjectRoot(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
test('observation layer writes and reads structured skill outcomes', () => {
const projectRoot = makeProjectRoot('ecc-skill-observe-');
try {
const observation = createSkillObservation({
task: 'Fix flaky Playwright test',
skill: {
id: 'e2e-testing',
path: 'skills/e2e-testing/SKILL.md'
},
success: false,
error: 'playwright timeout',
feedback: 'Timed out waiting for locator',
sessionId: 'sess-1234'
});
appendSkillObservation(observation, { projectRoot });
const records = readSkillObservations({ projectRoot });
assert.strictEqual(records.length, 1);
assert.strictEqual(records[0].schemaVersion, 'ecc.skill-observation.v1');
assert.strictEqual(records[0].task, 'Fix flaky Playwright test');
assert.strictEqual(records[0].skill.id, 'e2e-testing');
assert.strictEqual(records[0].outcome.success, false);
assert.strictEqual(records[0].outcome.error, 'playwright timeout');
assert.strictEqual(getSkillObservationsPath({ projectRoot }), path.join(projectRoot, '.claude', 'ecc', 'skills', 'observations.jsonl'));
} finally {
cleanup(projectRoot);
}
});
test('health inspector traces recurring failures for a skill across runs', () => {
const projectRoot = makeProjectRoot('ecc-skill-health-');
try {
[
createSkillObservation({
task: 'Ship Next.js auth middleware',
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
success: false,
error: 'missing csrf guidance',
feedback: 'Did not mention CSRF'
}),
createSkillObservation({
task: 'Harden Next.js auth middleware',
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
success: false,
error: 'missing csrf guidance',
feedback: 'Repeated omission'
}),
createSkillObservation({
task: 'Review payment webhook security',
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
success: true
})
].forEach(record => appendSkillObservation(record, { projectRoot }));
const report = buildSkillHealthReport(readSkillObservations({ projectRoot }), {
minFailureCount: 2
});
const skill = report.skills.find(entry => entry.skill.id === 'security-review');
assert.ok(skill, 'security-review should appear in the report');
assert.strictEqual(skill.totalRuns, 3);
assert.strictEqual(skill.failures, 2);
assert.strictEqual(skill.status, 'failing');
assert.strictEqual(skill.recurringErrors[0].error, 'missing csrf guidance');
assert.strictEqual(skill.recurringErrors[0].count, 2);
} finally {
cleanup(projectRoot);
}
});
test('amendify proposes SKILL.md patch content from failure evidence', () => {
const records = [
createSkillObservation({
task: 'Add API rate limiting',
skill: { id: 'api-design', path: 'skills/api-design/SKILL.md' },
success: false,
error: 'missing rate limiting guidance',
feedback: 'No rate-limit section'
}),
createSkillObservation({
task: 'Design public API error envelopes',
skill: { id: 'api-design', path: 'skills/api-design/SKILL.md' },
success: false,
error: 'missing error response examples',
feedback: 'Need explicit examples'
})
];
const proposal = proposeSkillAmendment('api-design', records);
assert.strictEqual(proposal.schemaVersion, 'ecc.skill-amendment-proposal.v1');
assert.strictEqual(proposal.skill.id, 'api-design');
assert.strictEqual(proposal.status, 'proposed');
assert.ok(proposal.patch.preview.includes('## Failure-Driven Amendments'));
assert.ok(proposal.patch.preview.includes('rate limiting'));
assert.ok(proposal.patch.preview.includes('error response'));
});
test('evaluation scaffold compares amended and baseline performance', () => {
const records = [
createSkillObservation({
task: 'Fix flaky login test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
success: false,
variant: 'baseline'
}),
createSkillObservation({
task: 'Fix flaky checkout test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
success: true,
variant: 'baseline'
}),
createSkillObservation({
task: 'Fix flaky login test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
success: true,
variant: 'amended',
amendmentId: 'amend-1'
}),
createSkillObservation({
task: 'Fix flaky checkout test',
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
success: true,
variant: 'amended',
amendmentId: 'amend-1'
})
];
const evaluation = buildSkillEvaluationScaffold('e2e-testing', records, {
amendmentId: 'amend-1',
minimumRunsPerVariant: 2
});
assert.strictEqual(evaluation.schemaVersion, 'ecc.skill-evaluation.v1');
assert.strictEqual(evaluation.baseline.runs, 2);
assert.strictEqual(evaluation.amended.runs, 2);
assert.strictEqual(evaluation.delta.successRate, 0.5);
assert.strictEqual(evaluation.recommendation, 'promote-amendment');
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

View File

@@ -0,0 +1,489 @@
/**
* Tests for the SQLite-backed ECC state store and CLI commands.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const {
createStateStore,
resolveStateStorePath,
} = require('../../scripts/lib/state-store');
const ECC_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
const STATUS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'status.js');
const SESSIONS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'sessions-cli.js');
async function test(name, fn) {
try {
await 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 cleanupTempDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function runNode(scriptPath, args = [], options = {}) {
return spawnSync('node', [scriptPath, ...args], {
encoding: 'utf8',
cwd: options.cwd || process.cwd(),
env: {
...process.env,
...(options.env || {}),
},
});
}
function parseJson(stdout) {
return JSON.parse(stdout.trim());
}
async function seedStore(dbPath) {
const store = await createStateStore({ dbPath });
store.upsertSession({
id: 'session-active',
adapterId: 'dmux-tmux',
harness: 'claude',
state: 'active',
repoRoot: '/tmp/ecc-repo',
startedAt: '2026-03-15T08:00:00.000Z',
endedAt: null,
snapshot: {
schemaVersion: 'ecc.session.v1',
adapterId: 'dmux-tmux',
session: {
id: 'session-active',
kind: 'orchestrated',
state: 'active',
repoRoot: '/tmp/ecc-repo',
},
workers: [
{
id: 'worker-1',
label: 'Worker 1',
state: 'active',
branch: 'feat/state-store',
worktree: '/tmp/ecc-repo/.worktrees/worker-1',
},
{
id: 'worker-2',
label: 'Worker 2',
state: 'idle',
branch: 'feat/state-store',
worktree: '/tmp/ecc-repo/.worktrees/worker-2',
},
],
aggregates: {
workerCount: 2,
states: {
active: 1,
idle: 1,
},
},
},
});
store.upsertSession({
id: 'session-recorded',
adapterId: 'claude-history',
harness: 'claude',
state: 'recorded',
repoRoot: '/tmp/ecc-repo',
startedAt: '2026-03-14T18:00:00.000Z',
endedAt: '2026-03-14T19:00:00.000Z',
snapshot: {
schemaVersion: 'ecc.session.v1',
adapterId: 'claude-history',
session: {
id: 'session-recorded',
kind: 'history',
state: 'recorded',
repoRoot: '/tmp/ecc-repo',
},
workers: [
{
id: 'worker-hist',
label: 'History Worker',
state: 'recorded',
branch: 'main',
worktree: '/tmp/ecc-repo',
},
],
aggregates: {
workerCount: 1,
states: {
recorded: 1,
},
},
},
});
store.insertSkillRun({
id: 'skill-run-1',
skillId: 'tdd-workflow',
skillVersion: '1.0.0',
sessionId: 'session-active',
taskDescription: 'Write store tests',
outcome: 'success',
failureReason: null,
tokensUsed: 1200,
durationMs: 3500,
userFeedback: 'useful',
createdAt: '2026-03-15T08:05:00.000Z',
});
store.insertSkillRun({
id: 'skill-run-2',
skillId: 'security-review',
skillVersion: '1.0.0',
sessionId: 'session-active',
taskDescription: 'Review state-store design',
outcome: 'failed',
failureReason: 'timeout',
tokensUsed: 800,
durationMs: 1800,
userFeedback: null,
createdAt: '2026-03-15T08:06:00.000Z',
});
store.insertSkillRun({
id: 'skill-run-3',
skillId: 'code-reviewer',
skillVersion: '1.0.0',
sessionId: 'session-recorded',
taskDescription: 'Inspect CLI formatting',
outcome: 'success',
failureReason: null,
tokensUsed: 500,
durationMs: 900,
userFeedback: 'clear',
createdAt: '2026-03-15T08:07:00.000Z',
});
store.insertSkillRun({
id: 'skill-run-4',
skillId: 'planner',
skillVersion: '1.0.0',
sessionId: 'session-recorded',
taskDescription: 'Outline ECC 2.0 work',
outcome: 'unknown',
failureReason: null,
tokensUsed: 300,
durationMs: 500,
userFeedback: null,
createdAt: '2026-03-15T08:08:00.000Z',
});
store.upsertSkillVersion({
skillId: 'tdd-workflow',
version: '1.0.0',
contentHash: 'abc123',
amendmentReason: 'initial',
promotedAt: '2026-03-10T00:00:00.000Z',
rolledBackAt: null,
});
store.insertDecision({
id: 'decision-1',
sessionId: 'session-active',
title: 'Use SQLite for durable state',
rationale: 'Need queryable local state for ECC control plane',
alternatives: ['json-files', 'memory-only'],
supersedes: null,
status: 'active',
createdAt: '2026-03-15T08:09:00.000Z',
});
store.upsertInstallState({
targetId: 'claude-home',
targetRoot: '/tmp/home/.claude',
profile: 'developer',
modules: ['rules-core', 'orchestration'],
operations: [
{
kind: 'copy-file',
destinationPath: '/tmp/home/.claude/agents/planner.md',
},
],
installedAt: '2026-03-15T07:00:00.000Z',
sourceVersion: '1.8.0',
});
store.insertGovernanceEvent({
id: 'gov-1',
sessionId: 'session-active',
eventType: 'policy-review-required',
payload: {
severity: 'warning',
owner: 'security-reviewer',
},
resolvedAt: null,
resolution: null,
createdAt: '2026-03-15T08:10:00.000Z',
});
store.insertGovernanceEvent({
id: 'gov-2',
sessionId: 'session-recorded',
eventType: 'decision-accepted',
payload: {
severity: 'info',
},
resolvedAt: '2026-03-15T08:11:00.000Z',
resolution: 'accepted',
createdAt: '2026-03-15T08:09:30.000Z',
});
store.close();
}
async function runTests() {
console.log('\n=== Testing state-store ===\n');
let passed = 0;
let failed = 0;
if (await test('creates the default state.db path and applies migrations idempotently', async () => {
const homeDir = createTempDir('ecc-state-home-');
try {
const expectedPath = path.join(homeDir, '.claude', 'ecc', 'state.db');
assert.strictEqual(resolveStateStorePath({ homeDir }), expectedPath);
const firstStore = await createStateStore({ homeDir });
const firstMigrations = firstStore.getAppliedMigrations();
firstStore.close();
assert.strictEqual(firstMigrations.length, 1);
assert.strictEqual(firstMigrations[0].version, 1);
assert.ok(fs.existsSync(expectedPath));
const secondStore = await createStateStore({ homeDir });
const secondMigrations = secondStore.getAppliedMigrations();
secondStore.close();
assert.strictEqual(secondMigrations.length, 1);
assert.strictEqual(secondMigrations[0].version, 1);
} finally {
cleanupTempDir(homeDir);
}
})) passed += 1; else failed += 1;
if (await test('preserves SQLite special database names like :memory:', async () => {
const tempDir = createTempDir('ecc-state-memory-');
const previousCwd = process.cwd();
try {
process.chdir(tempDir);
assert.strictEqual(resolveStateStorePath({ dbPath: ':memory:' }), ':memory:');
const store = await createStateStore({ dbPath: ':memory:' });
assert.strictEqual(store.dbPath, ':memory:');
assert.strictEqual(store.getAppliedMigrations().length, 1);
store.close();
assert.ok(!fs.existsSync(path.join(tempDir, ':memory:')));
} finally {
process.chdir(previousCwd);
cleanupTempDir(tempDir);
}
})) passed += 1; else failed += 1;
if (await test('stores sessions and returns detailed session views with workers, skill runs, and decisions', async () => {
const testDir = createTempDir('ecc-state-db-');
const dbPath = path.join(testDir, 'state.db');
try {
await seedStore(dbPath);
const store = await createStateStore({ dbPath });
const listResult = store.listRecentSessions({ limit: 10 });
const detail = store.getSessionDetail('session-active');
store.close();
assert.strictEqual(listResult.totalCount, 2);
assert.strictEqual(listResult.sessions[0].id, 'session-active');
assert.strictEqual(detail.session.id, 'session-active');
assert.strictEqual(detail.workers.length, 2);
assert.strictEqual(detail.skillRuns.length, 2);
assert.strictEqual(detail.decisions.length, 1);
assert.deepStrictEqual(detail.decisions[0].alternatives, ['json-files', 'memory-only']);
} finally {
cleanupTempDir(testDir);
}
})) passed += 1; else failed += 1;
if (await test('builds a status snapshot with active sessions, skill rates, install health, and pending governance', async () => {
const testDir = createTempDir('ecc-state-db-');
const dbPath = path.join(testDir, 'state.db');
try {
await seedStore(dbPath);
const store = await createStateStore({ dbPath });
const status = store.getStatus();
store.close();
assert.strictEqual(status.activeSessions.activeCount, 1);
assert.strictEqual(status.activeSessions.sessions[0].id, 'session-active');
assert.strictEqual(status.skillRuns.summary.totalCount, 4);
assert.strictEqual(status.skillRuns.summary.successCount, 2);
assert.strictEqual(status.skillRuns.summary.failureCount, 1);
assert.strictEqual(status.skillRuns.summary.unknownCount, 1);
assert.strictEqual(status.installHealth.status, 'healthy');
assert.strictEqual(status.installHealth.totalCount, 1);
assert.strictEqual(status.governance.pendingCount, 1);
assert.strictEqual(status.governance.events[0].id, 'gov-1');
} finally {
cleanupTempDir(testDir);
}
})) passed += 1; else failed += 1;
if (await test('validates entity payloads before writing to the database', async () => {
const testDir = createTempDir('ecc-state-db-');
const dbPath = path.join(testDir, 'state.db');
try {
const store = await createStateStore({ dbPath });
assert.throws(() => {
store.upsertSession({
id: '',
adapterId: 'dmux-tmux',
harness: 'claude',
state: 'active',
repoRoot: '/tmp/repo',
startedAt: '2026-03-15T08:00:00.000Z',
endedAt: null,
snapshot: {},
});
}, /Invalid session/);
assert.throws(() => {
store.insertDecision({
id: 'decision-invalid',
sessionId: 'missing-session',
title: 'Reject non-array alternatives',
rationale: 'alternatives must be an array',
alternatives: { unexpected: true },
supersedes: null,
status: 'active',
createdAt: '2026-03-15T08:15:00.000Z',
});
}, /Invalid decision/);
assert.throws(() => {
store.upsertInstallState({
targetId: 'claude-home',
targetRoot: '/tmp/home/.claude',
profile: 'developer',
modules: 'rules-core',
operations: [],
installedAt: '2026-03-15T07:00:00.000Z',
sourceVersion: '1.8.0',
});
}, /Invalid installState/);
store.close();
} finally {
cleanupTempDir(testDir);
}
})) passed += 1; else failed += 1;
if (await test('status CLI supports human-readable and --json output', async () => {
const testDir = createTempDir('ecc-state-cli-');
const dbPath = path.join(testDir, 'state.db');
try {
await seedStore(dbPath);
const jsonResult = runNode(STATUS_SCRIPT, ['--db', dbPath, '--json']);
assert.strictEqual(jsonResult.status, 0, jsonResult.stderr);
const jsonPayload = parseJson(jsonResult.stdout);
assert.strictEqual(jsonPayload.activeSessions.activeCount, 1);
assert.strictEqual(jsonPayload.governance.pendingCount, 1);
const humanResult = runNode(STATUS_SCRIPT, ['--db', dbPath]);
assert.strictEqual(humanResult.status, 0, humanResult.stderr);
assert.match(humanResult.stdout, /Active sessions: 1/);
assert.match(humanResult.stdout, /Skill runs \(last 20\):/);
assert.match(humanResult.stdout, /Install health: healthy/);
assert.match(humanResult.stdout, /Pending governance events: 1/);
} finally {
cleanupTempDir(testDir);
}
})) passed += 1; else failed += 1;
if (await test('sessions CLI supports list and detail views in human-readable and --json output', async () => {
const testDir = createTempDir('ecc-state-cli-');
const dbPath = path.join(testDir, 'state.db');
try {
await seedStore(dbPath);
const listJsonResult = runNode(SESSIONS_SCRIPT, ['--db', dbPath, '--json']);
assert.strictEqual(listJsonResult.status, 0, listJsonResult.stderr);
const listPayload = parseJson(listJsonResult.stdout);
assert.strictEqual(listPayload.totalCount, 2);
assert.strictEqual(listPayload.sessions[0].id, 'session-active');
const detailJsonResult = runNode(SESSIONS_SCRIPT, ['session-active', '--db', dbPath, '--json']);
assert.strictEqual(detailJsonResult.status, 0, detailJsonResult.stderr);
const detailPayload = parseJson(detailJsonResult.stdout);
assert.strictEqual(detailPayload.session.id, 'session-active');
assert.strictEqual(detailPayload.workers.length, 2);
assert.strictEqual(detailPayload.skillRuns.length, 2);
assert.strictEqual(detailPayload.decisions.length, 1);
const detailHumanResult = runNode(SESSIONS_SCRIPT, ['session-active', '--db', dbPath]);
assert.strictEqual(detailHumanResult.status, 0, detailHumanResult.stderr);
assert.match(detailHumanResult.stdout, /Session: session-active/);
assert.match(detailHumanResult.stdout, /Workers: 2/);
assert.match(detailHumanResult.stdout, /Skill runs: 2/);
assert.match(detailHumanResult.stdout, /Decisions: 1/);
} finally {
cleanupTempDir(testDir);
}
})) passed += 1; else failed += 1;
if (await test('ecc CLI delegates the new status and sessions subcommands', async () => {
const testDir = createTempDir('ecc-state-cli-');
const dbPath = path.join(testDir, 'state.db');
try {
await seedStore(dbPath);
const statusResult = runNode(ECC_SCRIPT, ['status', '--db', dbPath, '--json']);
assert.strictEqual(statusResult.status, 0, statusResult.stderr);
const statusPayload = parseJson(statusResult.stdout);
assert.strictEqual(statusPayload.activeSessions.activeCount, 1);
const sessionsResult = runNode(ECC_SCRIPT, ['sessions', 'session-active', '--db', dbPath, '--json']);
assert.strictEqual(sessionsResult.status, 0, sessionsResult.stderr);
const sessionsPayload = parseJson(sessionsResult.stdout);
assert.strictEqual(sessionsPayload.session.id, 'session-active');
assert.strictEqual(sessionsPayload.skillRuns.length, 2);
} finally {
cleanupTempDir(testDir);
}
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,423 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
slugify,
renderTemplate,
buildOrchestrationPlan,
executePlan,
materializePlan,
normalizeSeedPaths,
overlaySeedPaths
} = require('../../scripts/lib/tmux-worktree-orchestrator');
console.log('=== Testing tmux-worktree-orchestrator.js ===\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++;
}
}
console.log('Helpers:');
test('slugify normalizes mixed punctuation and casing', () => {
assert.strictEqual(slugify('Feature Audit: Docs + Tmux'), 'feature-audit-docs-tmux');
});
test('renderTemplate replaces supported placeholders', () => {
const rendered = renderTemplate('run {worker_name} in {worktree_path}', {
worker_name: 'Docs Fixer',
worktree_path: '/tmp/repo-worker'
});
assert.strictEqual(rendered, 'run Docs Fixer in /tmp/repo-worker');
});
test('renderTemplate rejects unknown placeholders', () => {
assert.throws(
() => renderTemplate('missing {unknown}', { worker_name: 'docs' }),
/Unknown template variable/
);
});
console.log('\nPlan generation:');
test('buildOrchestrationPlan creates worktrees, branches, and tmux commands', () => {
const repoRoot = path.join('/tmp', 'ecc');
const plan = buildOrchestrationPlan({
repoRoot,
sessionName: 'Skill Audit',
baseRef: 'main',
launcherCommand: 'codex exec --cwd {worktree_path} --task-file {task_file}',
workers: [
{ name: 'Docs A', task: 'Fix skills 1-4' },
{ name: 'Docs B', task: 'Fix skills 5-8' }
]
});
assert.strictEqual(plan.sessionName, 'skill-audit');
assert.strictEqual(plan.workerPlans.length, 2);
assert.strictEqual(plan.workerPlans[0].branchName, 'orchestrator-skill-audit-docs-a');
assert.strictEqual(plan.workerPlans[1].branchName, 'orchestrator-skill-audit-docs-b');
assert.deepStrictEqual(
plan.workerPlans[0].gitArgs.slice(0, 4),
['worktree', 'add', '-b', 'orchestrator-skill-audit-docs-a'],
'Should create branch-backed worktrees'
);
assert.ok(
plan.workerPlans[0].worktreePath.endsWith(path.join('ecc-skill-audit-docs-a')),
'Should create sibling worktree path'
);
assert.ok(
plan.workerPlans[0].taskFilePath.endsWith(path.join('.orchestration', 'skill-audit', 'docs-a', 'task.md')),
'Should create per-worker task file'
);
assert.ok(
plan.workerPlans[0].handoffFilePath.endsWith(path.join('.orchestration', 'skill-audit', 'docs-a', 'handoff.md')),
'Should create per-worker handoff file'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(plan.workerPlans[0].taskFilePath),
'Launch command should interpolate task file'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(plan.workerPlans[0].worktreePath),
'Launch command should interpolate worktree path'
);
assert.ok(
plan.tmuxCommands.some(command => command.args.includes('split-window')),
'Should include tmux split commands'
);
assert.ok(
plan.tmuxCommands.some(command => command.args.includes('select-layout')),
'Should include tiled layout command'
);
});
test('buildOrchestrationPlan requires at least one worker', () => {
assert.throws(
() => buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'empty',
launcherCommand: 'codex exec --task-file {task_file}',
workers: []
}),
/at least one worker/
);
});
test('buildOrchestrationPlan normalizes global and worker seed paths', () => {
const plan = buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'seeded',
launcherCommand: 'echo run',
seedPaths: ['scripts/orchestrate-worktrees.js', './.claude/plan/workflow-e2e-test.json'],
workers: [
{
name: 'Docs',
task: 'Update docs',
seedPaths: ['commands/multi-workflow.md']
}
]
});
assert.deepStrictEqual(plan.workerPlans[0].seedPaths, [
'scripts/orchestrate-worktrees.js',
'.claude/plan/workflow-e2e-test.json',
'commands/multi-workflow.md'
]);
});
test('buildOrchestrationPlan rejects worker names that collapse to the same slug', () => {
assert.throws(
() => buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'duplicates',
launcherCommand: 'echo run',
workers: [
{ name: 'Docs A', task: 'Fix skill docs' },
{ name: 'Docs/A', task: 'Fix tests' }
]
}),
/unique slugs/
);
});
test('buildOrchestrationPlan exposes shell-safe launcher aliases alongside raw defaults', () => {
const repoRoot = path.join('/tmp', 'My Repo');
const plan = buildOrchestrationPlan({
repoRoot,
sessionName: 'Spacing Audit',
launcherCommand: 'bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh} {worker_name_sh} {worker_name}',
workers: [{ name: 'Docs Fixer', task: 'Update docs' }]
});
const quote = value => `'${String(value).replace(/'/g, `'\\''`)}'`;
const resolvedRepoRoot = plan.workerPlans[0].repoRoot;
assert.ok(
plan.workerPlans[0].launchCommand.includes(`bash ${quote(resolvedRepoRoot)}/scripts/orchestrate-codex-worker.sh`),
'repo_root_sh should provide a shell-safe path'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(quote(plan.workerPlans[0].taskFilePath)),
'task_file_sh should provide a shell-safe path'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(`${quote(plan.workerPlans[0].workerName)} ${plan.workerPlans[0].workerName}`),
'raw defaults should remain available alongside shell-safe aliases'
);
});
test('buildOrchestrationPlan shell-quotes the orchestration banner command', () => {
const repoRoot = path.join('/tmp', "O'Hare Repo");
const plan = buildOrchestrationPlan({
repoRoot,
sessionName: 'Quote Audit',
launcherCommand: 'echo run',
workers: [{ name: 'Docs', task: 'Update docs' }]
});
const quote = value => `'${String(value).replace(/'/g, `'\\''`)}'`;
const bannerCommand = plan.tmuxCommands[1].args[3];
assert.strictEqual(
bannerCommand,
`printf '%s\\n' ${quote(`Session: ${plan.sessionName}`)} ${quote(`Coordination: ${plan.coordinationDir}`)}`,
'Banner command should quote coordination paths safely for tmux send-keys'
);
});
test('normalizeSeedPaths rejects paths outside the repo root', () => {
assert.throws(
() => normalizeSeedPaths(['../outside.txt'], '/tmp/ecc'),
/inside repoRoot/
);
});
test('materializePlan keeps worker instructions inside the worktree boundary', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orchestrator-test-'));
try {
const plan = buildOrchestrationPlan({
repoRoot: tempRoot,
coordinationRoot: path.join(tempRoot, '.claude', 'orchestration'),
sessionName: 'Workflow E2E',
launcherCommand: 'bash {repo_root}/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}',
workers: [{ name: 'Docs', task: 'Update the workflow docs.' }]
});
materializePlan(plan);
const taskFile = fs.readFileSync(plan.workerPlans[0].taskFilePath, 'utf8');
assert.ok(
taskFile.includes('Report results in your final response.'),
'Task file should tell the worker to report in stdout'
);
assert.ok(
taskFile.includes('Do not spawn subagents or external agents for this task.'),
'Task file should keep nested workers single-session'
);
assert.ok(
!taskFile.includes('Write results and handoff notes to'),
'Task file should not require writing handoff files outside the worktree'
);
assert.ok(
!taskFile.includes('Update `'),
'Task file should not instruct the nested worker to update orchestration status files'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('overlaySeedPaths copies local overlays into the worker worktree', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orchestrator-overlay-'));
const repoRoot = path.join(tempRoot, 'repo');
const worktreePath = path.join(tempRoot, 'worktree');
try {
fs.mkdirSync(path.join(repoRoot, 'scripts'), { recursive: true });
fs.mkdirSync(path.join(repoRoot, '.claude', 'plan'), { recursive: true });
fs.mkdirSync(path.join(worktreePath, 'scripts'), { recursive: true });
fs.writeFileSync(
path.join(repoRoot, 'scripts', 'orchestrate-worktrees.js'),
'local-version\n',
'utf8'
);
fs.writeFileSync(
path.join(repoRoot, '.claude', 'plan', 'workflow-e2e-test.json'),
'{"seeded":true}\n',
'utf8'
);
fs.writeFileSync(
path.join(worktreePath, 'scripts', 'orchestrate-worktrees.js'),
'head-version\n',
'utf8'
);
overlaySeedPaths({
repoRoot,
seedPaths: [
'scripts/orchestrate-worktrees.js',
'.claude/plan/workflow-e2e-test.json'
],
worktreePath
});
assert.strictEqual(
fs.readFileSync(path.join(worktreePath, 'scripts', 'orchestrate-worktrees.js'), 'utf8'),
'local-version\n'
);
assert.strictEqual(
fs.readFileSync(path.join(worktreePath, '.claude', 'plan', 'workflow-e2e-test.json'), 'utf8'),
'{"seeded":true}\n'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('executePlan rolls back partial setup when orchestration fails mid-run', () => {
const plan = {
repoRoot: '/tmp/ecc',
sessionName: 'rollback-test',
coordinationDir: '/tmp/ecc/.orchestration/rollback-test',
replaceExisting: false,
workerPlans: [
{
workerName: 'Docs',
workerSlug: 'docs',
worktreePath: '/tmp/ecc-rollback-docs',
seedPaths: ['commands/orchestrate.md'],
gitArgs: ['worktree', 'add', '-b', 'orchestrator-rollback-test-docs', '/tmp/ecc-rollback-docs', 'HEAD'],
launchCommand: 'echo run'
}
]
};
const calls = [];
const rollbackCalls = [];
assert.throws(
() => executePlan(plan, {
spawnSync(program, args) {
calls.push({ type: 'spawnSync', program, args });
if (program === 'tmux' && args[0] === 'has-session') {
return { status: 1, stdout: '', stderr: '' };
}
throw new Error(`Unexpected spawnSync call: ${program} ${args.join(' ')}`);
},
runCommand(program, args) {
calls.push({ type: 'runCommand', program, args });
if (program === 'git' && args[0] === 'rev-parse') {
return { status: 0, stdout: 'true\n', stderr: '' };
}
if (program === 'tmux' && args[0] === '-V') {
return { status: 0, stdout: 'tmux 3.4\n', stderr: '' };
}
if (program === 'git' && args[0] === 'worktree') {
return { status: 0, stdout: '', stderr: '' };
}
throw new Error(`Unexpected runCommand call: ${program} ${args.join(' ')}`);
},
materializePlan(receivedPlan) {
calls.push({ type: 'materializePlan', receivedPlan });
},
overlaySeedPaths() {
throw new Error('overlay failed');
},
rollbackCreatedResources(receivedPlan, createdState) {
rollbackCalls.push({ receivedPlan, createdState });
}
}),
/overlay failed/
);
assert.deepStrictEqual(
rollbackCalls.map(call => call.receivedPlan),
[plan],
'executePlan should invoke rollback on failure'
);
assert.deepStrictEqual(
rollbackCalls[0].createdState.workerPlans,
plan.workerPlans,
'executePlan should only roll back resources created before the failure'
);
assert.ok(
calls.some(call => call.type === 'runCommand' && call.program === 'git' && call.args[0] === 'worktree'),
'executePlan should attempt setup before rolling back'
);
});
test('executePlan does not mark pre-existing resources for rollback when worktree creation fails', () => {
const plan = {
repoRoot: '/tmp/ecc',
sessionName: 'rollback-existing',
coordinationDir: '/tmp/ecc/.orchestration/rollback-existing',
replaceExisting: false,
workerPlans: [
{
workerName: 'Docs',
workerSlug: 'docs',
worktreePath: '/tmp/ecc-existing-docs',
seedPaths: [],
gitArgs: ['worktree', 'add', '-b', 'orchestrator-rollback-existing-docs', '/tmp/ecc-existing-docs', 'HEAD'],
launchCommand: 'echo run',
branchName: 'orchestrator-rollback-existing-docs'
}
]
};
const rollbackCalls = [];
assert.throws(
() => executePlan(plan, {
spawnSync(program, args) {
if (program === 'tmux' && args[0] === 'has-session') {
return { status: 1, stdout: '', stderr: '' };
}
throw new Error(`Unexpected spawnSync call: ${program} ${args.join(' ')}`);
},
runCommand(program, args) {
if (program === 'git' && args[0] === 'rev-parse') {
return { status: 0, stdout: 'true\n', stderr: '' };
}
if (program === 'tmux' && args[0] === '-V') {
return { status: 0, stdout: 'tmux 3.4\n', stderr: '' };
}
if (program === 'git' && args[0] === 'worktree') {
throw new Error('branch already exists');
}
throw new Error(`Unexpected runCommand call: ${program} ${args.join(' ')}`);
},
materializePlan() {},
rollbackCreatedResources(receivedPlan, createdState) {
rollbackCalls.push({ receivedPlan, createdState });
}
}),
/branch already exists/
);
assert.deepStrictEqual(
rollbackCalls[0].createdState.workerPlans,
[],
'Failures before creation should not schedule any worker resources for rollback'
);
assert.strictEqual(
rollbackCalls[0].createdState.sessionCreated,
false,
'Failures before tmux session creation should not mark a session for rollback'
);
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
/**
* Tests for .opencode/opencode.json local file references.
*
* Run with: node tests/opencode-config.test.js
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const repoRoot = path.join(__dirname, '..');
const opencodeDir = path.join(repoRoot, '.opencode');
const configPath = path.join(opencodeDir, 'opencode.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
let passed = 0;
let failed = 0;
if (
test('plugin paths do not duplicate the .opencode directory', () => {
const plugins = config.plugin || [];
for (const pluginPath of plugins) {
assert.ok(!pluginPath.includes('.opencode/'), `Plugin path should be config-relative, got: ${pluginPath}`);
assert.ok(fs.existsSync(path.resolve(opencodeDir, pluginPath)), `Plugin path should resolve from .opencode/: ${pluginPath}`);
}
})
)
passed++;
else failed++;
if (
test('file references are config-relative and resolve to existing files', () => {
const refs = [];
function walk(value) {
if (typeof value === 'string') {
const matches = value.matchAll(/\{file:([^}]+)\}/g);
for (const match of matches) {
refs.push(match[1]);
}
return;
}
if (Array.isArray(value)) {
value.forEach(walk);
return;
}
if (value && typeof value === 'object') {
Object.values(value).forEach(walk);
}
}
walk(config);
assert.ok(refs.length > 0, 'Expected to find file references in opencode.json');
for (const ref of refs) {
assert.ok(!ref.startsWith('.opencode/'), `File ref should not duplicate .opencode/: ${ref}`);
assert.ok(fs.existsSync(path.resolve(opencodeDir, ref)), `File ref should resolve from .opencode/: ${ref}`);
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,118 @@
#!/usr/bin/env node
/**
* Run all tests
*
* Usage: node tests/run-all.js
*/
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const testsDir = __dirname;
const repoRoot = path.resolve(testsDir, '..');
const TEST_GLOB = 'tests/**/*.test.js';
function matchesTestGlob(relativePath) {
const normalized = relativePath.split(path.sep).join('/');
if (typeof path.matchesGlob === 'function') {
return path.matchesGlob(normalized, TEST_GLOB);
}
return /^tests\/(?:.+\/)?[^/]+\.test\.js$/.test(normalized);
}
function walkFiles(dir, acc = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkFiles(fullPath, acc);
} else if (entry.isFile()) {
acc.push(fullPath);
}
}
return acc;
}
function discoverTestFiles() {
return walkFiles(testsDir)
.map(fullPath => path.relative(repoRoot, fullPath))
.filter(matchesTestGlob)
.map(repoRelativePath => path.relative(testsDir, path.join(repoRoot, repoRelativePath)))
.sort();
}
const testFiles = discoverTestFiles();
const BOX_W = 58; // inner width between ║ delimiters
const boxLine = s => `${s.padEnd(BOX_W)}`;
console.log('╔' + '═'.repeat(BOX_W) + '╗');
console.log(boxLine(' Everything Claude Code - Test Suite'));
console.log('╚' + '═'.repeat(BOX_W) + '╝');
console.log();
if (testFiles.length === 0) {
console.log(`✗ No test files matched ${TEST_GLOB}`);
process.exit(1);
}
let totalPassed = 0;
let totalFailed = 0;
let totalTests = 0;
for (const testFile of testFiles) {
const testPath = path.join(testsDir, testFile);
const displayPath = testFile.split(path.sep).join('/');
if (!fs.existsSync(testPath)) {
console.log(`⚠ Skipping ${displayPath} (file not found)`);
continue;
}
console.log(`\n━━━ Running ${displayPath} ━━━`);
const result = spawnSync('node', [testPath], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
const stdout = result.stdout || '';
const stderr = result.stderr || '';
// Show both stdout and stderr so hook warnings are visible
if (stdout) console.log(stdout);
if (stderr) console.log(stderr);
// Parse results from combined output
const combined = stdout + stderr;
const passedMatch = combined.match(/Passed:\s*(\d+)/);
const failedMatch = combined.match(/Failed:\s*(\d+)/);
if (passedMatch) totalPassed += parseInt(passedMatch[1], 10);
if (failedMatch) totalFailed += parseInt(failedMatch[1], 10);
if (result.error) {
console.log(`${displayPath} failed to start: ${result.error.message}`);
totalFailed += failedMatch ? 0 : 1;
continue;
}
if (result.status !== 0) {
console.log(`${displayPath} exited with status ${result.status}`);
totalFailed += failedMatch ? 0 : 1;
}
}
totalTests = totalPassed + totalFailed;
console.log('\n╔' + '═'.repeat(BOX_W) + '╗');
console.log(boxLine(' Final Results'));
console.log('╠' + '═'.repeat(BOX_W) + '╣');
console.log(boxLine(` Total Tests: ${String(totalTests).padStart(4)}`));
console.log(boxLine(` Passed: ${String(totalPassed).padStart(4)}`));
console.log(boxLine(` Failed: ${String(totalFailed).padStart(4)} ${totalFailed > 0 ? '✗' : ' '}`));
console.log('╚' + '═'.repeat(BOX_W) + '╝');
process.exit(totalFailed > 0 ? 1 : 0);

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();

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();

View File

@@ -0,0 +1,94 @@
/**
* 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 repoRoot = path.join(__dirname, '..', '..');
const installScript = path.join(repoRoot, 'scripts', 'codex', 'install-global-git-hooks.sh');
const installSource = fs.readFileSync(installScript, 'utf8');
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 toBashPath(filePath) {
if (process.platform !== 'win32') {
return filePath;
}
return String(filePath)
.replace(/^([A-Za-z]):/, (_, driveLetter) => `/${driveLetter.toLowerCase()}`)
.replace(/\\/g, '/');
}
function runBash(scriptPath, args = [], env = {}, cwd = repoRoot) {
return spawnSync('bash', [toBashPath(scriptPath), ...args], {
cwd,
env: {
...process.env,
...env
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
}
let passed = 0;
let failed = 0;
if (
test('install-global-git-hooks.sh does not use eval and executes argv directly', () => {
assert.ok(!installSource.includes('eval "$*"'), 'Expected installer to avoid eval');
assert.ok(installSource.includes(' "$@"'), 'Expected installer to execute argv directly');
assert.ok(installSource.includes(`printf ' %q' "$@"`), 'Expected dry-run logging to shell-escape argv');
})
)
passed++;
else failed++;
if (
test('install-global-git-hooks.sh handles shell-sensitive hook paths without shell injection', () => {
const homeDir = createTempDir('codex-hooks-home-');
const weirdHooksDir = path.join(homeDir, "git-hooks 'quoted' & spaced");
try {
const result = runBash(installScript, [], {
HOME: toBashPath(homeDir),
ECC_GLOBAL_HOOKS_DIR: toBashPath(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++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

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();

View File

@@ -0,0 +1,167 @@
/**
* 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/);
}],
['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 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 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');
}],
['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 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>/);
}],
['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();

View File

@@ -0,0 +1,86 @@
/**
* Tests for scripts/harness-audit.js
*/
const assert = require('assert');
const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'harness-audit.js');
function run(args = []) {
const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return stdout;
}
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('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-03-16');
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'));
})) 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):'));
assert.ok(output.includes('Top 3 Actions:') || output.includes('Checks:'));
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,426 @@
/**
* 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 SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
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 env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
...(options.env || {}),
};
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 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', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', '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', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', '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', 'common', 'coding-style.md')
)),
'Should record common rule file operation'
);
} 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.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript-testing.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', '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 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 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('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('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', '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('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.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 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', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', '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', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', '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();

View File

@@ -0,0 +1,180 @@
/**
* 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('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();

View File

@@ -0,0 +1,117 @@
/**
* 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++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,93 @@
/**
* 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++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

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();

View File

@@ -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);

View File

@@ -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();

View File

@@ -0,0 +1,313 @@
/**
* 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 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 env = {
...process.env,
HOME: options.homeDir || process.env.HOME,
};
try {
const stdout = execFileSync('node', [scriptPath, ...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 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(parsed.results[0].repairedPaths.includes(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();

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();

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();

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();

View File

@@ -0,0 +1,80 @@
/**
* 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');
assert.ok(source.includes('run_or_echo rm -rf "$dest"'), 'rm should use argv form');
assert.ok(source.includes('run_or_echo cp -R "$skill_dir" "$dest"'), 'recursive copy should use argv form');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,286 @@
/**
* 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 {
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: 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 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: 10000,
});
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();