mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 05:03:28 +08:00
merge: dmux worktree (selective install, orchestration, observer fixes)
This commit is contained in:
@@ -14,6 +14,10 @@ const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const validatorsDir = path.join(__dirname, '..', '..', 'scripts', 'ci');
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const modulesSchemaPath = path.join(repoRoot, 'schemas', 'install-modules.schema.json');
|
||||
const profilesSchemaPath = path.join(repoRoot, 'schemas', 'install-profiles.schema.json');
|
||||
const componentsSchemaPath = path.join(repoRoot, 'schemas', 'install-components.schema.json');
|
||||
|
||||
// Test helpers
|
||||
function test(name, fn) {
|
||||
@@ -36,6 +40,18 @@ function cleanupTestDir(testDir) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
||||
}
|
||||
|
||||
function writeInstallComponentsManifest(testDir, components) {
|
||||
writeJson(path.join(testDir, 'manifests', 'install-components.json'), {
|
||||
version: 1,
|
||||
components,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a validator script via a wrapper that overrides its directory constant.
|
||||
* This allows testing error cases without modifying real project files.
|
||||
@@ -2164,6 +2180,369 @@ function runTests() {
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ==========================================
|
||||
// validate-install-manifests.js
|
||||
// ==========================================
|
||||
console.log('\nvalidate-install-manifests.js:');
|
||||
|
||||
if (test('passes on real project install manifests', () => {
|
||||
const result = runValidator('validate-install-manifests');
|
||||
assert.strictEqual(result.code, 0, `Should pass, got stderr: ${result.stderr}`);
|
||||
assert.ok(result.stdout.includes('Validated'), 'Should output validation count');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('exits 0 when install manifests do not exist', () => {
|
||||
const testDir = createTestDir();
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-profiles.json')
|
||||
});
|
||||
assert.strictEqual(result.code, 0, 'Should skip when manifests are missing');
|
||||
assert.ok(result.stdout.includes('skipping'), 'Should say skipping');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('fails on invalid install manifest JSON', () => {
|
||||
const testDir = createTestDir();
|
||||
const manifestsDir = path.join(testDir, 'manifests');
|
||||
fs.mkdirSync(manifestsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(manifestsDir, 'install-modules.json'), '{ invalid json');
|
||||
writeJson(path.join(manifestsDir, 'install-profiles.json'), {
|
||||
version: 1,
|
||||
profiles: {}
|
||||
});
|
||||
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(manifestsDir, 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(manifestsDir, 'install-profiles.json'),
|
||||
COMPONENTS_MANIFEST_PATH: path.join(manifestsDir, 'install-components.json'),
|
||||
MODULES_SCHEMA_PATH: modulesSchemaPath,
|
||||
PROFILES_SCHEMA_PATH: profilesSchemaPath,
|
||||
COMPONENTS_SCHEMA_PATH: componentsSchemaPath
|
||||
});
|
||||
assert.strictEqual(result.code, 1, 'Should fail on invalid JSON');
|
||||
assert.ok(result.stderr.includes('Invalid JSON'), 'Should report invalid JSON');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('fails when install module references a missing path', () => {
|
||||
const testDir = createTestDir();
|
||||
writeJson(path.join(testDir, 'manifests', 'install-modules.json'), {
|
||||
version: 1,
|
||||
modules: [
|
||||
{
|
||||
id: 'rules-core',
|
||||
kind: 'rules',
|
||||
description: 'Rules',
|
||||
paths: ['rules'],
|
||||
targets: ['claude'],
|
||||
dependencies: [],
|
||||
defaultInstall: true,
|
||||
cost: 'light',
|
||||
stability: 'stable'
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
kind: 'skills',
|
||||
description: 'Security',
|
||||
paths: ['skills/security-review'],
|
||||
targets: ['codex'],
|
||||
dependencies: [],
|
||||
defaultInstall: false,
|
||||
cost: 'medium',
|
||||
stability: 'stable'
|
||||
}
|
||||
]
|
||||
});
|
||||
writeJson(path.join(testDir, 'manifests', 'install-profiles.json'), {
|
||||
version: 1,
|
||||
profiles: {
|
||||
core: { description: 'Core', modules: ['rules-core'] },
|
||||
developer: { description: 'Developer', modules: ['rules-core'] },
|
||||
security: { description: 'Security', modules: ['rules-core', 'security'] },
|
||||
research: { description: 'Research', modules: ['rules-core'] },
|
||||
full: { description: 'Full', modules: ['rules-core', 'security'] }
|
||||
}
|
||||
});
|
||||
writeInstallComponentsManifest(testDir, [
|
||||
{
|
||||
id: 'baseline:rules',
|
||||
family: 'baseline',
|
||||
description: 'Rules',
|
||||
modules: ['rules-core']
|
||||
},
|
||||
{
|
||||
id: 'capability:security',
|
||||
family: 'capability',
|
||||
description: 'Security',
|
||||
modules: ['security']
|
||||
}
|
||||
]);
|
||||
fs.mkdirSync(path.join(testDir, 'rules'), { recursive: true });
|
||||
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-profiles.json'),
|
||||
COMPONENTS_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-components.json'),
|
||||
MODULES_SCHEMA_PATH: modulesSchemaPath,
|
||||
PROFILES_SCHEMA_PATH: profilesSchemaPath,
|
||||
COMPONENTS_SCHEMA_PATH: componentsSchemaPath
|
||||
});
|
||||
assert.strictEqual(result.code, 1, 'Should fail when a referenced path is missing');
|
||||
assert.ok(result.stderr.includes('references missing path'), 'Should report missing path');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('fails when two install modules claim the same path', () => {
|
||||
const testDir = createTestDir();
|
||||
writeJson(path.join(testDir, 'manifests', 'install-modules.json'), {
|
||||
version: 1,
|
||||
modules: [
|
||||
{
|
||||
id: 'agents-core',
|
||||
kind: 'agents',
|
||||
description: 'Agents',
|
||||
paths: ['agents'],
|
||||
targets: ['codex'],
|
||||
dependencies: [],
|
||||
defaultInstall: true,
|
||||
cost: 'light',
|
||||
stability: 'stable'
|
||||
},
|
||||
{
|
||||
id: 'commands-core',
|
||||
kind: 'commands',
|
||||
description: 'Commands',
|
||||
paths: ['agents'],
|
||||
targets: ['codex'],
|
||||
dependencies: [],
|
||||
defaultInstall: true,
|
||||
cost: 'light',
|
||||
stability: 'stable'
|
||||
}
|
||||
]
|
||||
});
|
||||
writeJson(path.join(testDir, 'manifests', 'install-profiles.json'), {
|
||||
version: 1,
|
||||
profiles: {
|
||||
core: { description: 'Core', modules: ['agents-core', 'commands-core'] },
|
||||
developer: { description: 'Developer', modules: ['agents-core', 'commands-core'] },
|
||||
security: { description: 'Security', modules: ['agents-core', 'commands-core'] },
|
||||
research: { description: 'Research', modules: ['agents-core', 'commands-core'] },
|
||||
full: { description: 'Full', modules: ['agents-core', 'commands-core'] }
|
||||
}
|
||||
});
|
||||
writeInstallComponentsManifest(testDir, [
|
||||
{
|
||||
id: 'baseline:agents',
|
||||
family: 'baseline',
|
||||
description: 'Agents',
|
||||
modules: ['agents-core']
|
||||
},
|
||||
{
|
||||
id: 'baseline:commands',
|
||||
family: 'baseline',
|
||||
description: 'Commands',
|
||||
modules: ['commands-core']
|
||||
}
|
||||
]);
|
||||
fs.mkdirSync(path.join(testDir, 'agents'), { recursive: true });
|
||||
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-profiles.json'),
|
||||
COMPONENTS_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-components.json'),
|
||||
MODULES_SCHEMA_PATH: modulesSchemaPath,
|
||||
PROFILES_SCHEMA_PATH: profilesSchemaPath,
|
||||
COMPONENTS_SCHEMA_PATH: componentsSchemaPath
|
||||
});
|
||||
assert.strictEqual(result.code, 1, 'Should fail on duplicate claimed paths');
|
||||
assert.ok(result.stderr.includes('claimed by both'), 'Should report duplicate path claims');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('fails when an install profile references an unknown module', () => {
|
||||
const testDir = createTestDir();
|
||||
writeJson(path.join(testDir, 'manifests', 'install-modules.json'), {
|
||||
version: 1,
|
||||
modules: [
|
||||
{
|
||||
id: 'rules-core',
|
||||
kind: 'rules',
|
||||
description: 'Rules',
|
||||
paths: ['rules'],
|
||||
targets: ['claude'],
|
||||
dependencies: [],
|
||||
defaultInstall: true,
|
||||
cost: 'light',
|
||||
stability: 'stable'
|
||||
}
|
||||
]
|
||||
});
|
||||
writeJson(path.join(testDir, 'manifests', 'install-profiles.json'), {
|
||||
version: 1,
|
||||
profiles: {
|
||||
core: { description: 'Core', modules: ['rules-core'] },
|
||||
developer: { description: 'Developer', modules: ['rules-core'] },
|
||||
security: { description: 'Security', modules: ['rules-core'] },
|
||||
research: { description: 'Research', modules: ['rules-core'] },
|
||||
full: { description: 'Full', modules: ['rules-core', 'ghost-module'] }
|
||||
}
|
||||
});
|
||||
writeInstallComponentsManifest(testDir, [
|
||||
{
|
||||
id: 'baseline:rules',
|
||||
family: 'baseline',
|
||||
description: 'Rules',
|
||||
modules: ['rules-core']
|
||||
}
|
||||
]);
|
||||
fs.mkdirSync(path.join(testDir, 'rules'), { recursive: true });
|
||||
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-profiles.json'),
|
||||
COMPONENTS_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-components.json'),
|
||||
MODULES_SCHEMA_PATH: modulesSchemaPath,
|
||||
PROFILES_SCHEMA_PATH: profilesSchemaPath,
|
||||
COMPONENTS_SCHEMA_PATH: componentsSchemaPath
|
||||
});
|
||||
assert.strictEqual(result.code, 1, 'Should fail on unknown profile module');
|
||||
assert.ok(result.stderr.includes('references unknown module ghost-module'),
|
||||
'Should report unknown module reference');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('passes on a valid standalone install manifest fixture', () => {
|
||||
const testDir = createTestDir();
|
||||
writeJson(path.join(testDir, 'manifests', 'install-modules.json'), {
|
||||
version: 1,
|
||||
modules: [
|
||||
{
|
||||
id: 'rules-core',
|
||||
kind: 'rules',
|
||||
description: 'Rules',
|
||||
paths: ['rules'],
|
||||
targets: ['claude'],
|
||||
dependencies: [],
|
||||
defaultInstall: true,
|
||||
cost: 'light',
|
||||
stability: 'stable'
|
||||
},
|
||||
{
|
||||
id: 'orchestration',
|
||||
kind: 'orchestration',
|
||||
description: 'Orchestration',
|
||||
paths: ['scripts/orchestrate-worktrees.js'],
|
||||
targets: ['codex'],
|
||||
dependencies: ['rules-core'],
|
||||
defaultInstall: false,
|
||||
cost: 'medium',
|
||||
stability: 'beta'
|
||||
}
|
||||
]
|
||||
});
|
||||
writeJson(path.join(testDir, 'manifests', 'install-profiles.json'), {
|
||||
version: 1,
|
||||
profiles: {
|
||||
core: { description: 'Core', modules: ['rules-core'] },
|
||||
developer: { description: 'Developer', modules: ['rules-core', 'orchestration'] },
|
||||
security: { description: 'Security', modules: ['rules-core'] },
|
||||
research: { description: 'Research', modules: ['rules-core'] },
|
||||
full: { description: 'Full', modules: ['rules-core', 'orchestration'] }
|
||||
}
|
||||
});
|
||||
writeInstallComponentsManifest(testDir, [
|
||||
{
|
||||
id: 'baseline:rules',
|
||||
family: 'baseline',
|
||||
description: 'Rules',
|
||||
modules: ['rules-core']
|
||||
},
|
||||
{
|
||||
id: 'capability:orchestration',
|
||||
family: 'capability',
|
||||
description: 'Orchestration',
|
||||
modules: ['orchestration']
|
||||
}
|
||||
]);
|
||||
fs.mkdirSync(path.join(testDir, 'rules'), { recursive: true });
|
||||
fs.mkdirSync(path.join(testDir, 'scripts'), { recursive: true });
|
||||
fs.writeFileSync(path.join(testDir, 'scripts', 'orchestrate-worktrees.js'), '#!/usr/bin/env node\n');
|
||||
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-profiles.json'),
|
||||
COMPONENTS_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-components.json'),
|
||||
MODULES_SCHEMA_PATH: modulesSchemaPath,
|
||||
PROFILES_SCHEMA_PATH: profilesSchemaPath,
|
||||
COMPONENTS_SCHEMA_PATH: componentsSchemaPath
|
||||
});
|
||||
assert.strictEqual(result.code, 0, `Should pass valid fixture, got stderr: ${result.stderr}`);
|
||||
assert.ok(result.stdout.includes('Validated 2 install modules, 2 install components, and 5 profiles'),
|
||||
'Should report validated install manifest counts');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('fails when an install component references an unknown module', () => {
|
||||
const testDir = createTestDir();
|
||||
writeJson(path.join(testDir, 'manifests', 'install-modules.json'), {
|
||||
version: 1,
|
||||
modules: [
|
||||
{
|
||||
id: 'rules-core',
|
||||
kind: 'rules',
|
||||
description: 'Rules',
|
||||
paths: ['rules'],
|
||||
targets: ['claude'],
|
||||
dependencies: [],
|
||||
defaultInstall: true,
|
||||
cost: 'light',
|
||||
stability: 'stable'
|
||||
}
|
||||
]
|
||||
});
|
||||
writeJson(path.join(testDir, 'manifests', 'install-profiles.json'), {
|
||||
version: 1,
|
||||
profiles: {
|
||||
core: { description: 'Core', modules: ['rules-core'] },
|
||||
developer: { description: 'Developer', modules: ['rules-core'] },
|
||||
security: { description: 'Security', modules: ['rules-core'] },
|
||||
research: { description: 'Research', modules: ['rules-core'] },
|
||||
full: { description: 'Full', modules: ['rules-core'] }
|
||||
}
|
||||
});
|
||||
writeInstallComponentsManifest(testDir, [
|
||||
{
|
||||
id: 'capability:security',
|
||||
family: 'capability',
|
||||
description: 'Security',
|
||||
modules: ['ghost-module']
|
||||
}
|
||||
]);
|
||||
fs.mkdirSync(path.join(testDir, 'rules'), { recursive: true });
|
||||
|
||||
const result = runValidatorWithDirs('validate-install-manifests', {
|
||||
REPO_ROOT: testDir,
|
||||
MODULES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-modules.json'),
|
||||
PROFILES_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-profiles.json'),
|
||||
COMPONENTS_MANIFEST_PATH: path.join(testDir, 'manifests', 'install-components.json'),
|
||||
MODULES_SCHEMA_PATH: modulesSchemaPath,
|
||||
PROFILES_SCHEMA_PATH: profilesSchemaPath,
|
||||
COMPONENTS_SCHEMA_PATH: componentsSchemaPath
|
||||
});
|
||||
assert.strictEqual(result.code, 1, 'Should fail on unknown component module');
|
||||
assert.ok(result.stderr.includes('references unknown module ghost-module'),
|
||||
'Should report unknown component module');
|
||||
cleanupTestDir(testDir);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Summary
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
@@ -88,9 +88,7 @@ function runShellScript(scriptPath, args = [], input = '', env = {}, cwd = proce
|
||||
|
||||
// Create a temporary test directory
|
||||
function createTestDir() {
|
||||
const testDir = path.join(os.tmpdir(), `hooks-test-${Date.now()}`);
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
return testDir;
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-'));
|
||||
}
|
||||
|
||||
// Clean up test directory
|
||||
@@ -98,65 +96,6 @@ function cleanupTestDir(testDir) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function normalizeComparablePath(targetPath) {
|
||||
if (!targetPath) return '';
|
||||
|
||||
let normalizedPath = String(targetPath).trim().replace(/\\/g, '/');
|
||||
|
||||
if (/^\/[a-zA-Z]\//.test(normalizedPath)) {
|
||||
normalizedPath = `${normalizedPath[1]}:/${normalizedPath.slice(3)}`;
|
||||
}
|
||||
|
||||
if (/^[a-zA-Z]:\//.test(normalizedPath)) {
|
||||
normalizedPath = `${normalizedPath[0].toUpperCase()}:${normalizedPath.slice(2)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
normalizedPath = fs.realpathSync(normalizedPath);
|
||||
} catch {
|
||||
// Fall through to string normalization when the path cannot be resolved directly.
|
||||
}
|
||||
|
||||
return path.normalize(normalizedPath).replace(/\\/g, '/').replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
||||
}
|
||||
|
||||
function pathsReferToSameLocation(leftPath, rightPath) {
|
||||
const normalizedLeftPath = normalizeComparablePath(leftPath);
|
||||
const normalizedRightPath = normalizeComparablePath(rightPath);
|
||||
|
||||
if (!normalizedLeftPath || !normalizedRightPath) return false;
|
||||
if (normalizedLeftPath === normalizedRightPath) return true;
|
||||
|
||||
try {
|
||||
const leftStats = fs.statSync(normalizedLeftPath);
|
||||
const rightStats = fs.statSync(normalizedRightPath);
|
||||
return leftStats.dev === rightStats.dev && leftStats.ino === rightStats.ino;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createObservePayload(projectDir, overrides = {}) {
|
||||
return JSON.stringify({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo hello' },
|
||||
tool_response: 'ok',
|
||||
session_id: 'session-123',
|
||||
cwd: projectDir,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
function listObservationFiles(homeDir) {
|
||||
const projectsDir = path.join(homeDir, '.claude', 'homunculus', 'projects');
|
||||
if (!fs.existsSync(projectsDir)) return [];
|
||||
|
||||
return fs
|
||||
.readdirSync(projectsDir)
|
||||
.map(projectId => path.join(projectsDir, projectId, 'observations.jsonl'))
|
||||
.filter(observationsPath => fs.existsSync(observationsPath));
|
||||
}
|
||||
|
||||
function createCommandShim(binDir, baseName, logFile) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
@@ -214,7 +153,6 @@ async function runTests() {
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
|
||||
const scriptsDir = path.join(__dirname, '..', '..', 'scripts', 'hooks');
|
||||
|
||||
@@ -2219,6 +2157,8 @@ async function runTests() {
|
||||
assert.ok(observerLoopSource.includes('ECC_OBSERVER_MAX_TURNS'), 'observer-loop should allow max-turn overrides');
|
||||
assert.ok(observerLoopSource.includes('max_turns="${ECC_OBSERVER_MAX_TURNS:-10}"'), 'observer-loop should default to 10 turns');
|
||||
assert.ok(!observerLoopSource.includes('--max-turns 3'), 'observer-loop should not hardcode a 3-turn limit');
|
||||
assert.ok(observerLoopSource.includes('ECC_SKIP_OBSERVE=1'), 'observer-loop should suppress observe.sh for automated sessions');
|
||||
assert.ok(observerLoopSource.includes('ECC_HOOK_PROFILE=minimal'), 'observer-loop should run automated analysis with the minimal hook profile');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -2252,11 +2192,7 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - detect-project writes project metadata to the registry and project directory');
|
||||
console.log(' (skipped — bash script paths are not Windows-compatible)');
|
||||
skipped++;
|
||||
} else if (
|
||||
if (
|
||||
await asyncTest('detect-project writes project metadata to the registry and project directory', async () => {
|
||||
const testRoot = createTestDir();
|
||||
const homeDir = path.join(testRoot, 'home');
|
||||
@@ -2293,9 +2229,9 @@ async function runTests() {
|
||||
|
||||
assert.strictEqual(code, 0, `detect-project should source cleanly, stderr: ${stderr}`);
|
||||
|
||||
const [projectId] = stdout.trim().split(/\r?\n/);
|
||||
const [projectId, projectDir] = stdout.trim().split(/\r?\n/);
|
||||
const registryPath = path.join(homeDir, '.claude', 'homunculus', 'projects.json');
|
||||
const projectMetadataPath = path.join(homeDir, '.claude', 'homunculus', 'projects', projectId, 'project.json');
|
||||
const projectMetadataPath = path.join(projectDir, 'project.json');
|
||||
|
||||
assert.ok(projectId, 'detect-project should emit a project id');
|
||||
assert.ok(fs.existsSync(registryPath), 'projects.json should be created');
|
||||
@@ -2307,13 +2243,7 @@ async function runTests() {
|
||||
assert.ok(registry[projectId], 'registry should contain the detected project');
|
||||
assert.strictEqual(metadata.id, projectId, 'project.json should include the detected id');
|
||||
assert.strictEqual(metadata.name, path.basename(repoDir), 'project.json should include the repo name');
|
||||
const normalizedMetadataRoot = normalizeComparablePath(metadata.root);
|
||||
const normalizedRepoDir = normalizeComparablePath(repoDir);
|
||||
assert.ok(normalizedMetadataRoot, 'project.json should include a non-empty repo root');
|
||||
assert.ok(
|
||||
pathsReferToSameLocation(normalizedMetadataRoot, normalizedRepoDir),
|
||||
`project.json should include the repo root (expected ${normalizedRepoDir}, got ${normalizedMetadataRoot})`,
|
||||
);
|
||||
assert.strictEqual(fs.realpathSync(metadata.root), fs.realpathSync(repoDir), 'project.json should include the repo root');
|
||||
assert.strictEqual(metadata.remote, 'https://github.com/example/ecc-test.git', 'project.json should include the sanitized remote');
|
||||
assert.ok(metadata.created_at, 'project.json should include created_at');
|
||||
assert.ok(metadata.last_seen, 'project.json should include last_seen');
|
||||
@@ -2325,22 +2255,6 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('continuous-learning-v2 observer scripts share prompt guard config and start-observer supports reset', () => {
|
||||
const observeSource = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh'), 'utf8');
|
||||
const startObserverSource = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'agents', 'start-observer.sh'), 'utf8');
|
||||
const detectProjectSource = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'scripts', 'detect-project.sh'), 'utf8');
|
||||
|
||||
assert.ok(detectProjectSource.includes('CLV2_OBSERVER_PROMPT_PATTERN='), 'detect-project.sh should export a shared observer prompt pattern');
|
||||
assert.ok(observeSource.includes('CLV2_OBSERVER_PROMPT_PATTERN'), 'observe.sh should use the shared observer prompt pattern');
|
||||
assert.ok(startObserverSource.includes('CLV2_OBSERVER_PROMPT_PATTERN'), 'start-observer.sh should use the shared observer prompt pattern');
|
||||
assert.ok(startObserverSource.includes('--reset'), 'start-observer.sh should document or support an explicit reset flag');
|
||||
assert.ok(!startObserverSource.includes('.observer.tmp.log'), 'start-observer.sh should not leave the observer writing to a temp log file');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh falls back to legacy output fields when tool_response is null', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
@@ -2378,197 +2292,74 @@ async function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh does not trip the observer lock for generic Awaiting output', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
if (await asyncTest('observe.sh skips automated sessions before project detection side effects', async () => {
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
const payload = JSON.stringify({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo waiting' },
|
||||
tool_response: 'Awaiting build completion from CI',
|
||||
session_id: 'session-awaiting-generic',
|
||||
cwd: projectDir
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], payload, {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should not fail closed for generic Awaiting output, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(projectDir, '.observer.lock')), 'generic Awaiting output should not create the observer lock sentinel');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh writes a scrubbed sentinel when confirmation prompts are detected', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
const payload = JSON.stringify({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo guarded' },
|
||||
tool_response: 'Awaiting user confirmation before proceeding. token=supersecretvalue123456',
|
||||
session_id: 'session-awaiting-confirmation',
|
||||
cwd: projectDir
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], payload, {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir
|
||||
}, projectDir);
|
||||
|
||||
const sentinelPath = path.join(projectDir, '.observer.lock');
|
||||
assert.strictEqual(result.code, 2, `observe.sh should fail closed when a confirmation prompt is detected, stderr: ${result.stderr}`);
|
||||
assert.ok(fs.existsSync(sentinelPath), 'confirmation prompts should create the observer lock sentinel');
|
||||
|
||||
const sentinelContent = fs.readFileSync(sentinelPath, 'utf8');
|
||||
assert.ok(/confirmation|permission/i.test(sentinelContent), 'sentinel should record the reason it was created');
|
||||
assert.ok(!sentinelContent.includes('supersecretvalue123456'), 'sentinel should not persist raw secrets from observer output');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips non-cli entrypoints without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-non-cli' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
CLAUDE_CODE_ENTRYPOINT: 'sdk'
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for non-cli entrypoints, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'non-cli entrypoints should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'non-cli entrypoints should not write observations');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips ECC_SKIP_OBSERVE sessions without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-skip-env' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
ECC_SKIP_OBSERVE: '1'
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully when ECC_SKIP_OBSERVE=1, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'ECC_SKIP_OBSERVE should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'ECC_SKIP_OBSERVE should suppress observation writes');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips subagent payloads with agent_id without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(
|
||||
observePath,
|
||||
['post'],
|
||||
createObservePayload(projectDir, { session_id: 'session-agent', agent_id: 'agent-123' }),
|
||||
{
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir
|
||||
const cases = [
|
||||
{
|
||||
name: 'non-cli entrypoint',
|
||||
env: { CLAUDE_CODE_ENTRYPOINT: 'mcp' }
|
||||
},
|
||||
{
|
||||
name: 'minimal hook profile',
|
||||
env: { CLAUDE_CODE_ENTRYPOINT: 'cli', ECC_HOOK_PROFILE: 'minimal' }
|
||||
},
|
||||
{
|
||||
name: 'cooperative skip env',
|
||||
env: { CLAUDE_CODE_ENTRYPOINT: 'cli', ECC_SKIP_OBSERVE: '1' }
|
||||
},
|
||||
{
|
||||
name: 'subagent payload',
|
||||
env: { CLAUDE_CODE_ENTRYPOINT: 'cli' },
|
||||
payload: { agent_id: 'agent-123' }
|
||||
},
|
||||
{
|
||||
name: 'cwd skip path',
|
||||
env: {
|
||||
CLAUDE_CODE_ENTRYPOINT: 'cli',
|
||||
ECC_OBSERVE_SKIP_PATHS: ' observer-sessions , .claude-mem '
|
||||
},
|
||||
projectDir
|
||||
);
|
||||
cwdSuffix: path.join('observer-sessions', 'worker')
|
||||
}
|
||||
];
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for subagent sessions, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'subagent sessions should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'subagent sessions should not write observations');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
for (const testCase of cases) {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
|
||||
if (await asyncTest('observe.sh skips default observer-session paths without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectRoot = createTestDir();
|
||||
const projectDir = path.join(projectRoot, 'observer-sessions-worker');
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
try {
|
||||
const cwd = testCase.cwdSuffix ? path.join(projectDir, testCase.cwdSuffix) : projectDir;
|
||||
fs.mkdirSync(cwd, { recursive: true });
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-default-skip-path' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir
|
||||
}, projectDir);
|
||||
const payload = JSON.stringify({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo hello' },
|
||||
tool_response: 'ok',
|
||||
session_id: `session-${testCase.name.replace(/[^a-z0-9]+/gi, '-')}`,
|
||||
cwd,
|
||||
...(testCase.payload || {})
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for default skip paths, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'default skip paths should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'default skip paths should suppress observation writes');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
const result = await runShellScript(observePath, ['post'], payload, {
|
||||
HOME: homeDir,
|
||||
...testCase.env
|
||||
}, projectDir);
|
||||
|
||||
if (await asyncTest('observe.sh trims custom skip-path patterns before matching', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectRoot = createTestDir();
|
||||
const projectDir = path.join(projectRoot, 'custom-observer-session');
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
assert.strictEqual(result.code, 0, `${testCase.name} should exit successfully, stderr: ${result.stderr}`);
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-custom-skip-path' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
ECC_OBSERVE_SKIP_PATHS: ' custom-observer-session , , '
|
||||
}, projectDir);
|
||||
const homunculusDir = path.join(homeDir, '.claude', 'homunculus');
|
||||
const registryPath = path.join(homunculusDir, 'projects.json');
|
||||
const projectsDir = path.join(homunculusDir, 'projects');
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for custom skip paths, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'custom skip paths should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'trimmed custom skip paths should suppress observation writes');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
assert.ok(!fs.existsSync(registryPath), `${testCase.name} should not create projects.json`);
|
||||
|
||||
if (await asyncTest('observe.sh ignores empty skip-path entries so normal paths still record observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-empty-skip-paths' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
ECC_OBSERVE_SKIP_PATHS: ' , , '
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully when skip-path entries are empty, stderr: ${result.stderr}`);
|
||||
const observationFiles = listObservationFiles(homeDir);
|
||||
assert.strictEqual(observationFiles.length, 1, 'empty skip-path entries should not suppress normal observations');
|
||||
|
||||
const observations = fs.readFileSync(observationFiles[0], 'utf8').trim().split('\n').filter(Boolean);
|
||||
assert.ok(observations.length > 0, 'normal sessions should still append observations when skip-path entries are empty');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
const projectEntries = fs.existsSync(projectsDir)
|
||||
? fs.readdirSync(projectsDir).filter(entry => fs.statSync(path.join(projectsDir, entry)).isDirectory())
|
||||
: [];
|
||||
assert.strictEqual(projectEntries.length, 0, `${testCase.name} should not create project directories`);
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -4600,12 +4391,12 @@ async function runTests() {
|
||||
// ── Round 74: session-start.js main().catch handler ──
|
||||
console.log('\nRound 74: session-start.js (main catch — unrecoverable error):');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - session-start exits 0 with error message when HOME is non-directory');
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
skipped++;
|
||||
} else if (
|
||||
if (
|
||||
await asyncTest('session-start exits 0 with error message when HOME is non-directory', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
return;
|
||||
}
|
||||
// HOME=/dev/null makes ensureDir(sessionsDir) throw ENOTDIR,
|
||||
// which propagates to main().catch — the top-level error boundary
|
||||
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||
@@ -4622,12 +4413,12 @@ async function runTests() {
|
||||
// ── Round 75: pre-compact.js main().catch handler ──
|
||||
console.log('\nRound 75: pre-compact.js (main catch — unrecoverable error):');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - pre-compact exits 0 with error message when HOME is non-directory');
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
skipped++;
|
||||
} else if (
|
||||
if (
|
||||
await asyncTest('pre-compact exits 0 with error message when HOME is non-directory', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
return;
|
||||
}
|
||||
// HOME=/dev/null makes ensureDir(sessionsDir) throw ENOTDIR,
|
||||
// which propagates to main().catch — the top-level error boundary
|
||||
const result = await runScript(path.join(scriptsDir, 'pre-compact.js'), '', {
|
||||
@@ -4644,12 +4435,12 @@ async function runTests() {
|
||||
// ── Round 75: session-end.js main().catch handler ──
|
||||
console.log('\nRound 75: session-end.js (main catch — unrecoverable error):');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - session-end exits 0 with error message when HOME is non-directory');
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
skipped++;
|
||||
} else if (
|
||||
if (
|
||||
await asyncTest('session-end exits 0 with error message when HOME is non-directory', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
return;
|
||||
}
|
||||
// HOME=/dev/null makes ensureDir(sessionsDir) throw ENOTDIR inside main(),
|
||||
// which propagates to runMain().catch — the top-level error boundary
|
||||
const result = await runScript(path.join(scriptsDir, 'session-end.js'), '{}', {
|
||||
@@ -4666,12 +4457,12 @@ async function runTests() {
|
||||
// ── Round 76: evaluate-session.js main().catch handler ──
|
||||
console.log('\nRound 76: evaluate-session.js (main catch — unrecoverable error):');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - evaluate-session exits 0 with error message when HOME is non-directory');
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
skipped++;
|
||||
} else if (
|
||||
if (
|
||||
await asyncTest('evaluate-session exits 0 with error message when HOME is non-directory', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
return;
|
||||
}
|
||||
// HOME=/dev/null makes ensureDir(learnedSkillsPath) throw ENOTDIR,
|
||||
// which propagates to main().catch — the top-level error boundary
|
||||
const result = await runScript(path.join(scriptsDir, 'evaluate-session.js'), '{}', {
|
||||
@@ -4688,12 +4479,12 @@ async function runTests() {
|
||||
// ── Round 76: suggest-compact.js main().catch handler ──
|
||||
console.log('\nRound 76: suggest-compact.js (main catch — double-failure):');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - suggest-compact exits 0 with error when TMPDIR is non-directory');
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
skipped++;
|
||||
} else if (
|
||||
if (
|
||||
await asyncTest('suggest-compact exits 0 with error when TMPDIR is non-directory', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — /dev/null not available on Windows)');
|
||||
return;
|
||||
}
|
||||
// TMPDIR=/dev/null causes openSync to fail (ENOTDIR), then the catch
|
||||
// fallback writeFile also fails, propagating to main().catch
|
||||
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
|
||||
@@ -4806,20 +4597,13 @@ async function runTests() {
|
||||
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');
|
||||
const summaryMatch = content.match(
|
||||
/<!-- ECC:SUMMARY:START -->([\s\S]*?)<!-- ECC:SUMMARY:END -->/
|
||||
);
|
||||
// The real string message should appear
|
||||
assert.ok(content.includes('Real user message'), 'Should include the string content user message');
|
||||
assert.ok(summaryMatch, 'Should include a generated summary block');
|
||||
const summaryBlock = summaryMatch[1];
|
||||
// Numeric/boolean/object content should NOT appear as task bullets
|
||||
assert.ok(
|
||||
!summaryBlock.includes('\n- 42\n'),
|
||||
'Numeric content should be skipped (else branch → empty string → filtered)'
|
||||
);
|
||||
assert.ok(!summaryBlock.includes('\n- true\n'), 'Boolean content should be skipped');
|
||||
assert.ok(!summaryBlock.includes('[object Object]'), 'Object content should be skipped');
|
||||
// Numeric/boolean/object content should NOT appear as task bullets.
|
||||
// The full file may legitimately contain "42" in timestamps like 03:42.
|
||||
assert.ok(!content.includes('\n- 42\n'), 'Numeric content should not be rendered as a task bullet');
|
||||
assert.ok(!content.includes('\n- true\n'), 'Boolean content should not be rendered as a task bullet');
|
||||
assert.ok(!content.includes('\n- [object Object]\n'), 'Object content should not be stringified into a task bullet');
|
||||
} finally {
|
||||
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||
}
|
||||
@@ -5175,8 +4959,7 @@ Some random content without the expected ### Context to Load section
|
||||
console.log('\n=== Test Results ===');
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
console.log(`Skipped: ${skipped}`);
|
||||
console.log(`Total: ${passed + failed + skipped}\n`);
|
||||
console.log(`Total: ${passed + failed}\n`);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
117
tests/lib/install-config.test.js
Normal file
117
tests/lib/install-config.test.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Tests for scripts/lib/install/config.js
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
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('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();
|
||||
357
tests/lib/install-lifecycle.test.js
Normal file
357
tests/lib/install-lifecycle.test.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 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,
|
||||
} = 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++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
196
tests/lib/install-manifests.test.js
Normal file
196
tests/lib/install-manifests.test.js
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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,
|
||||
listInstallModules,
|
||||
listInstallProfiles,
|
||||
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 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('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 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('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('throws when a dependency does not support the requested 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'] }
|
||||
}
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' }),
|
||||
/does not support target claude/
|
||||
);
|
||||
cleanupTestRepo(repoRoot);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
147
tests/lib/install-request.test.js
Normal file
147
tests/lib/install-request.test.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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',
|
||||
'--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.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');
|
||||
assert.strictEqual(request.target, 'claude');
|
||||
assert.deepStrictEqual(request.languages, ['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.languages, []);
|
||||
})) 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('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();
|
||||
139
tests/lib/install-state.test.js
Normal file
139
tests/lib/install-state.test.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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('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++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
110
tests/lib/install-targets.test.js
Normal file
110
tests/lib/install-targets.test.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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 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 = '/repo/ecc';
|
||||
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 => operation.sourceRelativePath === 'rules');
|
||||
|
||||
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 rules scaffold operation');
|
||||
assert.strictEqual(preserved.strategy, 'preserve-relative-path');
|
||||
assert.strictEqual(preserved.destinationPath, path.join(projectRoot, '.cursor', 'rules'));
|
||||
})) 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();
|
||||
@@ -7,6 +7,7 @@ const path = require('path');
|
||||
|
||||
const {
|
||||
buildSessionSnapshot,
|
||||
listTmuxPanes,
|
||||
loadWorkerSnapshots,
|
||||
parseWorkerHandoff,
|
||||
parseWorkerStatus,
|
||||
@@ -109,25 +110,6 @@ test('parseWorkerHandoff also supports bold section headers', () => {
|
||||
assert.deepStrictEqual(handoff.remainingRisks, ['No runtime screenshot']);
|
||||
});
|
||||
|
||||
test('parseWorkerHandoff accepts legacy verification and follow-up headings', () => {
|
||||
const handoff = parseWorkerHandoff([
|
||||
'# Handoff',
|
||||
'',
|
||||
'## Summary',
|
||||
'- Worker completed successfully',
|
||||
'',
|
||||
'## Tests / Verification',
|
||||
'- Ran tests',
|
||||
'',
|
||||
'## Follow-ups',
|
||||
'- Re-run screenshots after deploy'
|
||||
].join('\n'));
|
||||
|
||||
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
|
||||
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
|
||||
assert.deepStrictEqual(handoff.remainingRisks, ['Re-run screenshots after deploy']);
|
||||
});
|
||||
|
||||
test('loadWorkerSnapshots reads coordination worker directories', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-session-'));
|
||||
const coordinationDir = path.join(tempRoot, 'coordination');
|
||||
@@ -205,6 +187,16 @@ test('buildSessionSnapshot merges tmux panes with worker metadata', () => {
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -223,92 +215,7 @@ test('resolveSnapshotTarget handles plan files and direct session names', () =>
|
||||
|
||||
const fromSession = resolveSnapshotTarget('workflow-visual-proof', repoRoot);
|
||||
assert.strictEqual(fromSession.targetType, 'session');
|
||||
assert.ok(fromSession.coordinationDir.endsWith(path.join('.orchestration', 'workflow-visual-proof')));
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('resolveSnapshotTarget normalizes plan session names and defaults to the repo name', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
|
||||
const repoRoot = path.join(tempRoot, 'My Repo');
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
|
||||
const namedPlanPath = path.join(repoRoot, 'named-plan.json');
|
||||
const defaultPlanPath = path.join(repoRoot, 'default-plan.json');
|
||||
|
||||
fs.writeFileSync(namedPlanPath, JSON.stringify({
|
||||
sessionName: 'Workflow Visual Proof',
|
||||
repoRoot
|
||||
}));
|
||||
fs.writeFileSync(defaultPlanPath, JSON.stringify({ repoRoot }));
|
||||
|
||||
try {
|
||||
const namedPlan = resolveSnapshotTarget(namedPlanPath, repoRoot);
|
||||
assert.strictEqual(namedPlan.sessionName, 'workflow-visual-proof');
|
||||
assert.ok(namedPlan.coordinationDir.endsWith(path.join('.orchestration', 'workflow-visual-proof')));
|
||||
|
||||
const defaultPlan = resolveSnapshotTarget(defaultPlanPath, repoRoot);
|
||||
assert.strictEqual(defaultPlan.sessionName, 'my-repo');
|
||||
assert.ok(defaultPlan.coordinationDir.endsWith(path.join('.orchestration', 'my-repo')));
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('resolveSnapshotTarget rejects malformed plan files and invalid config fields', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
|
||||
const repoRoot = path.join(tempRoot, 'repo');
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
|
||||
const invalidJsonPath = path.join(repoRoot, 'invalid-json.json');
|
||||
const blankFieldsPath = path.join(repoRoot, 'blank-fields.json');
|
||||
const invalidSessionNamePath = path.join(repoRoot, 'invalid-session.json');
|
||||
const invalidRepoRootPath = path.join(repoRoot, 'invalid-repo-root.json');
|
||||
const invalidCoordinationRootPath = path.join(repoRoot, 'invalid-coordination-root.json');
|
||||
|
||||
fs.writeFileSync(invalidJsonPath, '{not valid json');
|
||||
fs.writeFileSync(blankFieldsPath, JSON.stringify({
|
||||
sessionName: ' ',
|
||||
repoRoot: ' ',
|
||||
coordinationRoot: ' '
|
||||
}));
|
||||
fs.writeFileSync(invalidSessionNamePath, JSON.stringify({
|
||||
sessionName: 42,
|
||||
repoRoot
|
||||
}));
|
||||
fs.writeFileSync(invalidRepoRootPath, JSON.stringify({
|
||||
sessionName: 'workflow',
|
||||
repoRoot: ['not-a-string']
|
||||
}));
|
||||
fs.writeFileSync(invalidCoordinationRootPath, JSON.stringify({
|
||||
sessionName: 'workflow',
|
||||
repoRoot,
|
||||
coordinationRoot: false
|
||||
}));
|
||||
|
||||
try {
|
||||
const blankFields = resolveSnapshotTarget(blankFieldsPath, repoRoot);
|
||||
assert.strictEqual(blankFields.sessionName, 'repo');
|
||||
assert.strictEqual(blankFields.repoRoot, repoRoot);
|
||||
assert.ok(blankFields.coordinationDir.endsWith(path.join('.orchestration', 'repo')));
|
||||
|
||||
assert.throws(
|
||||
() => resolveSnapshotTarget(invalidJsonPath, repoRoot),
|
||||
/Invalid orchestration plan JSON/
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveSnapshotTarget(invalidSessionNamePath, repoRoot),
|
||||
/sessionName must be a string when provided/
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveSnapshotTarget(invalidRepoRootPath, repoRoot),
|
||||
/repoRoot must be a string when provided/
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveSnapshotTarget(invalidCoordinationRootPath, repoRoot),
|
||||
/coordinationRoot must be a string when provided/
|
||||
);
|
||||
assert.ok(fromSession.coordinationDir.endsWith(path.join('.claude', 'orchestration', 'workflow-visual-proof')));
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
214
tests/lib/session-adapters.test.js
Normal file
214
tests/lib/session-adapters.test.js
Normal file
@@ -0,0 +1,214 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { createClaudeHistoryAdapter } = require('../../scripts/lib/session-adapters/claude-history');
|
||||
const { createDmuxTmuxAdapter } = require('../../scripts/lib/session-adapters/dmux-tmux');
|
||||
const { createAdapterRegistry } = 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;
|
||||
process.env.HOME = homeDir;
|
||||
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
if (typeof previousHome === 'string') {
|
||||
process.env.HOME = previousHome;
|
||||
} else {
|
||||
delete process.env.HOME;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('dmux adapter normalizes orchestration snapshots into canonical form', () => {
|
||||
const adapter = createDmuxTmuxAdapter({
|
||||
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: '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: ['No screenshot yet']
|
||||
},
|
||||
files: {
|
||||
status: '/tmp/status.md',
|
||||
task: '/tmp/task.md',
|
||||
handoff: '/tmp/handoff.md'
|
||||
},
|
||||
pane: {
|
||||
paneId: '%95',
|
||||
title: 'seed-check'
|
||||
}
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
const snapshot = adapter.open('workflow-visual-proof').getSnapshot();
|
||||
|
||||
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.sourceTarget.type, 'session');
|
||||
assert.strictEqual(snapshot.aggregates.workerCount, 1);
|
||||
assert.strictEqual(snapshot.workers[0].runtime.kind, 'tmux-pane');
|
||||
assert.strictEqual(snapshot.workers[0].outputs.remainingRisks[0], 'No screenshot yet');
|
||||
});
|
||||
|
||||
test('claude-history adapter loads the latest recorded session', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-adapter-home-'));
|
||||
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();
|
||||
const snapshot = adapter.open('claude:latest').getSnapshot();
|
||||
|
||||
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.strictEqual(snapshot.workers[0].artifacts.sessionFile, sessionPath);
|
||||
assert.ok(snapshot.workers[0].outputs.summary.includes('Build snapshot prototype'));
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { 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({
|
||||
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()
|
||||
]
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
if (failed > 0) process.exit(1);
|
||||
@@ -57,7 +57,7 @@ test('buildOrchestrationPlan creates worktrees, branches, and tmux commands', ()
|
||||
repoRoot,
|
||||
sessionName: 'Skill Audit',
|
||||
baseRef: 'main',
|
||||
launcherCommand: 'codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}',
|
||||
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' }
|
||||
@@ -137,46 +137,6 @@ test('buildOrchestrationPlan normalizes global and worker seed paths', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
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('normalizeSeedPaths rejects paths outside the repo root', () => {
|
||||
assert.throws(
|
||||
() => normalizeSeedPaths(['../outside.txt'], '/tmp/ecc'),
|
||||
@@ -184,17 +144,6 @@ test('normalizeSeedPaths rejects paths outside the repo root', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeSeedPaths rejects repo root and git metadata paths', () => {
|
||||
assert.throws(
|
||||
() => normalizeSeedPaths(['.'], '/tmp/ecc'),
|
||||
/must not target the repo root/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeSeedPaths(['.git/config'], '/tmp/ecc'),
|
||||
/must not target git metadata/
|
||||
);
|
||||
});
|
||||
|
||||
test('materializePlan keeps worker instructions inside the worktree boundary', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orchestrator-test-'));
|
||||
|
||||
@@ -203,25 +152,18 @@ test('materializePlan keeps worker instructions inside the worktree boundary', (
|
||||
repoRoot: tempRoot,
|
||||
coordinationRoot: path.join(tempRoot, '.claude', 'orchestration'),
|
||||
sessionName: 'Workflow E2E',
|
||||
launcherCommand: 'bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}',
|
||||
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');
|
||||
const handoffFile = fs.readFileSync(plan.workerPlans[0].handoffFilePath, '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('## Summary') &&
|
||||
taskFile.includes('## Validation') &&
|
||||
taskFile.includes('## Remaining Risks'),
|
||||
'Task file should require parser-compatible headings'
|
||||
);
|
||||
assert.ok(
|
||||
taskFile.includes('Do not spawn subagents or external agents for this task.'),
|
||||
'Task file should keep nested workers single-session'
|
||||
@@ -234,18 +176,6 @@ test('materializePlan keeps worker instructions inside the worktree boundary', (
|
||||
!taskFile.includes('Update `'),
|
||||
'Task file should not instruct the nested worker to update orchestration status files'
|
||||
);
|
||||
assert.ok(
|
||||
handoffFile.includes('## Summary') &&
|
||||
handoffFile.includes('## Validation') &&
|
||||
handoffFile.includes('## Remaining Risks'),
|
||||
'Handoff placeholder should seed parser-compatible headings'
|
||||
);
|
||||
assert.ok(
|
||||
!handoffFile.includes('## Files Changed') &&
|
||||
!handoffFile.includes('## Tests / Verification') &&
|
||||
!handoffFile.includes('## Follow-ups'),
|
||||
'Handoff placeholder should not use legacy headings'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
190
tests/scripts/doctor.test.js
Normal file
190
tests/scripts/doctor.test.js
Normal 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();
|
||||
139
tests/scripts/ecc.test.js
Normal file
139
tests/scripts/ecc.test.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 = {}) {
|
||||
return spawnSync('node', [SCRIPT, ...args], {
|
||||
encoding: 'utf8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
...(options.env || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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, /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');
|
||||
assert.deepStrictEqual(payload.plan.languages, ['typescript']);
|
||||
}],
|
||||
['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');
|
||||
assert.deepStrictEqual(payload.plan.languages, ['typescript']);
|
||||
}],
|
||||
['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 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/);
|
||||
}],
|
||||
['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();
|
||||
309
tests/scripts/install-apply.test.js
Normal file
309
tests/scripts/install-apply.test.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 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 rulesDir = path.join(homeDir, '.claude', 'rules');
|
||||
assert.ok(fs.existsSync(path.join(rulesDir, 'common', 'coding-style.md')));
|
||||
assert.ok(fs.existsSync(path.join(rulesDir, 'typescript', 'testing.md')));
|
||||
|
||||
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.ok(
|
||||
state.operations.some(operation => (
|
||||
operation.destinationPath === path.join(rulesDir, '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', 'hooks.json')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'article-writing', '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.ok(
|
||||
state.operations.some(operation => (
|
||||
operation.destinationPath === path.join(normalizedProjectDir, '.cursor', 'hooks', 'session-start.js')
|
||||
)),
|
||||
'Should record hook 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', 'code-review.md')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'article-writing', 'SKILL.md')));
|
||||
|
||||
const statePath = path.join(projectDir, '.agent', 'ecc-install-state.json');
|
||||
const state = readJson(statePath);
|
||||
assert.strictEqual(state.target.id, 'antigravity-project');
|
||||
assert.ok(
|
||||
state.operations.some(operation => (
|
||||
operation.destinationPath.endsWith(path.join('.agent', 'workflows', 'code-review.md'))
|
||||
)),
|
||||
'Should record workflow 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(!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, '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 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('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++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
154
tests/scripts/install-plan.test.js
Normal file
154
tests/scripts/install-plan.test.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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 = []) {
|
||||
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 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('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();
|
||||
87
tests/scripts/install-sh.test.js
Normal file
87
tests/scripts/install-sh.test.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 (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();
|
||||
139
tests/scripts/list-installed.test.js
Normal file
139
tests/scripts/list-installed.test.js
Normal 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();
|
||||
@@ -1,89 +1,76 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 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 { parseArgs } = require('../../scripts/orchestration-status');
|
||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'orchestration-status.js');
|
||||
|
||||
console.log('=== Testing orchestration-status.js ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(desc, fn) {
|
||||
function run(args = [], options = {}) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${desc}`);
|
||||
passed++;
|
||||
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) {
|
||||
console.log(` ✗ ${desc}: ${error.message}`);
|
||||
failed++;
|
||||
return {
|
||||
code: error.status || 1,
|
||||
stdout: error.stdout || '',
|
||||
stderr: error.stderr || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test('parseArgs reads a target with an optional write path', () => {
|
||||
assert.deepStrictEqual(
|
||||
parseArgs([
|
||||
'node',
|
||||
'scripts/orchestration-status.js',
|
||||
'workflow-visual-proof',
|
||||
'--write',
|
||||
'/tmp/snapshot.json'
|
||||
]),
|
||||
{
|
||||
target: 'workflow-visual-proof',
|
||||
writePath: '/tmp/snapshot.json'
|
||||
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++;
|
||||
|
||||
test('parseArgs does not treat the write path as the target', () => {
|
||||
assert.deepStrictEqual(
|
||||
parseArgs([
|
||||
'node',
|
||||
'scripts/orchestration-status.js',
|
||||
'--write',
|
||||
'/tmp/snapshot.json',
|
||||
'workflow-visual-proof'
|
||||
]),
|
||||
{
|
||||
target: 'workflow-visual-proof',
|
||||
writePath: '/tmp/snapshot.json'
|
||||
}
|
||||
);
|
||||
});
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
test('parseArgs rejects missing write values and unknown flags', () => {
|
||||
assert.throws(
|
||||
() => parseArgs([
|
||||
'node',
|
||||
'scripts/orchestration-status.js',
|
||||
'workflow-visual-proof',
|
||||
'--write'
|
||||
]),
|
||||
/--write requires an output path/
|
||||
);
|
||||
assert.throws(
|
||||
() => parseArgs([
|
||||
'node',
|
||||
'scripts/orchestration-status.js',
|
||||
'workflow-visual-proof',
|
||||
'--unknown'
|
||||
]),
|
||||
/Unknown flag/
|
||||
);
|
||||
});
|
||||
|
||||
test('parseArgs rejects multiple positional targets', () => {
|
||||
assert.throws(
|
||||
() => parseArgs([
|
||||
'node',
|
||||
'scripts/orchestration-status.js',
|
||||
'first',
|
||||
'second'
|
||||
]),
|
||||
/Expected a single session name or plan path/
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
if (failed > 0) process.exit(1);
|
||||
runTests();
|
||||
|
||||
159
tests/scripts/repair.test.js
Normal file
159
tests/scripts/repair.test.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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, '..', '..');
|
||||
|
||||
function createTempDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function cleanup(dirPath) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
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 managed files and refreshes install-state', () => {
|
||||
const homeDir = createTempDir('repair-home-');
|
||||
const projectRoot = createTempDir('repair-project-');
|
||||
|
||||
try {
|
||||
const installResult = runNode(INSTALL_SCRIPT, ['--target', 'cursor', '--modules', 'platform-configs'], {
|
||||
cwd: projectRoot,
|
||||
homeDir,
|
||||
});
|
||||
assert.strictEqual(installResult.code, 0, installResult.stderr);
|
||||
|
||||
const cursorRoot = path.join(projectRoot, '.cursor');
|
||||
const managedPath = path.join(cursorRoot, 'hooks.json');
|
||||
const statePath = path.join(cursorRoot, 'ecc-install-state.json');
|
||||
const managedRealPath = fs.realpathSync(cursorRoot);
|
||||
const expectedManagedPath = path.join(managedRealPath, 'hooks.json');
|
||||
const expectedContent = fs.readFileSync(path.join(REPO_ROOT, '.cursor', 'hooks.json'), 'utf8');
|
||||
const installedAtBefore = JSON.parse(fs.readFileSync(statePath, 'utf8')).installedAt;
|
||||
|
||||
fs.writeFileSync(managedPath, '{"drifted":true}\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(expectedManagedPath));
|
||||
assert.strictEqual(fs.readFileSync(managedPath, 'utf8'), expectedContent);
|
||||
|
||||
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 files', () => {
|
||||
const homeDir = createTempDir('repair-home-');
|
||||
const projectRoot = createTempDir('repair-project-');
|
||||
|
||||
try {
|
||||
const installResult = runNode(INSTALL_SCRIPT, ['--target', 'cursor', '--modules', 'platform-configs'], {
|
||||
cwd: projectRoot,
|
||||
homeDir,
|
||||
});
|
||||
assert.strictEqual(installResult.code, 0, installResult.stderr);
|
||||
|
||||
const cursorRoot = path.join(projectRoot, '.cursor');
|
||||
const managedPath = path.join(cursorRoot, 'hooks.json');
|
||||
const managedRealPath = fs.realpathSync(cursorRoot);
|
||||
const expectedManagedPath = path.join(managedRealPath, 'hooks.json');
|
||||
const driftedContent = '{"drifted":true}\n';
|
||||
fs.writeFileSync(managedPath, driftedContent);
|
||||
|
||||
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(expectedManagedPath));
|
||||
assert.strictEqual(fs.readFileSync(managedPath, 'utf8'), driftedContent);
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
116
tests/scripts/session-inspect.test.js
Normal file
116
tests/scripts/session-inspect.test.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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 SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'session-inspect.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(),
|
||||
env: {
|
||||
...process.env,
|
||||
...(options.env || {})
|
||||
}
|
||||
});
|
||||
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('prints canonical JSON for claude history targets', () => {
|
||||
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/session-inspect\n'
|
||||
);
|
||||
|
||||
const result = run(['claude:latest'], {
|
||||
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.kind, 'history');
|
||||
assert.strictEqual(payload.workers[0].branch, 'feat/session-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++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
133
tests/scripts/uninstall.test.js
Normal file
133
tests/scripts/uninstall.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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 UNINSTALL_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'uninstall.js');
|
||||
|
||||
function createTempDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function cleanup(dirPath) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
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 uninstall.js ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('removes managed files and keeps unrelated files', () => {
|
||||
const homeDir = createTempDir('uninstall-home-');
|
||||
const projectRoot = createTempDir('uninstall-project-');
|
||||
|
||||
try {
|
||||
const installResult = runNode(INSTALL_SCRIPT, ['--target', 'cursor', '--modules', 'platform-configs'], {
|
||||
cwd: projectRoot,
|
||||
homeDir,
|
||||
});
|
||||
assert.strictEqual(installResult.code, 0, installResult.stderr);
|
||||
|
||||
const cursorRoot = path.join(projectRoot, '.cursor');
|
||||
const managedPath = path.join(cursorRoot, 'hooks.json');
|
||||
const statePath = path.join(cursorRoot, 'ecc-install-state.json');
|
||||
const unrelatedPath = path.join(cursorRoot, 'custom-user-note.txt');
|
||||
fs.writeFileSync(unrelatedPath, 'leave me alone');
|
||||
|
||||
const uninstallResult = runNode(UNINSTALL_SCRIPT, ['--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('supports dry-run without removing files', () => {
|
||||
const homeDir = createTempDir('uninstall-home-');
|
||||
const projectRoot = createTempDir('uninstall-project-');
|
||||
|
||||
try {
|
||||
const installResult = runNode(INSTALL_SCRIPT, ['--target', 'cursor', '--modules', 'platform-configs'], {
|
||||
cwd: projectRoot,
|
||||
homeDir,
|
||||
});
|
||||
assert.strictEqual(installResult.code, 0, installResult.stderr);
|
||||
|
||||
const cursorRoot = path.join(projectRoot, '.cursor');
|
||||
const managedPath = path.join(cursorRoot, 'hooks.json');
|
||||
const statePath = path.join(cursorRoot, 'ecc-install-state.json');
|
||||
|
||||
const uninstallResult = runNode(UNINSTALL_SCRIPT, ['--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.length > 0);
|
||||
assert.ok(fs.existsSync(managedPath));
|
||||
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();
|
||||
Reference in New Issue
Block a user