mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
* Add install.ps1 PowerShell wrapper and tests Add a Windows-native PowerShell wrapper (install.ps1) that resolves symlinks and delegates to the Node-based installer runtime. Update README with PowerShell usage examples and cross-platform npx entrypoint guidance. Point the ecc-install bin to the Node installer (scripts/install-apply.js) in package.json (and refresh package-lock), include install.ps1 in package files, and add tests: a new install-ps1.test.js and a tweak to install-sh.test.js to skip on Windows. These changes provide native Windows installer support while keeping npm-compatible cross-platform invocation. * Improve tests for Windows HOME/USERPROFILE Make tests more cross-platform by ensuring HOME and USERPROFILE are kept in sync and by normalizing test file paths for display. - tests/lib/session-adapters.test.js: set USERPROFILE when temporarily setting HOME and restore previous USERPROFILE on teardown. - tests/run-all.js: use a normalized displayPath (forward-slash separated) for logging and error messages so output is consistent across platforms. - tests/scripts/ecc.test.js & tests/scripts/session-inspect.test.js: build envOverrides from options.env and add HOME <-> USERPROFILE fallbacks so spawned child processes receive both variables when only one is provided. These changes prevent test failures and inconsistent logs on Windows where USERPROFILE is used instead of HOME. * Fix Windows paths and test flakiness Improve cross-platform behavior and test stability. - Remove unused createLegacyInstallPlan import from install-lifecycle.js. - Change resolveInstallConfigPath to use path.normalize(path.join(cwd, configPath)) to produce normalized relative paths. - Tests: add toBashPath and normalizedRelativePath helpers to normalize Windows paths for bash and comparisons. - Make cleanupTestDir retry rmSync on transient Windows errors (EPERM/EBUSY/ENOTEMPTY) with short backoff using sleepMs. - Ensure spawned test processes receive USERPROFILE and convert repo/detect paths to bash format when invoking bash. These changes reduce Windows-specific failures and flakiness in the test suite and tidy up a small unused import.
301 lines
12 KiB
JavaScript
301 lines
12 KiB
JavaScript
/**
|
|
* Tests for scripts/session-inspect.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const { getFallbackSessionRecordingPath } = require('../../scripts/lib/session-adapters/canonical-session');
|
|
|
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'session-inspect.js');
|
|
|
|
function run(args = [], options = {}) {
|
|
const envOverrides = {
|
|
...(options.env || {})
|
|
};
|
|
|
|
if (typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)) {
|
|
envOverrides.USERPROFILE = envOverrides.HOME;
|
|
}
|
|
|
|
if (typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)) {
|
|
envOverrides.HOME = envOverrides.USERPROFILE;
|
|
}
|
|
|
|
try {
|
|
const stdout = execFileSync('node', [SCRIPT, ...args], {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: 10000,
|
|
cwd: options.cwd || process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
...envOverrides
|
|
}
|
|
});
|
|
return { code: 0, stdout, stderr: '' };
|
|
} catch (error) {
|
|
return {
|
|
code: error.status || 1,
|
|
stdout: error.stdout || '',
|
|
stderr: error.stderr || '',
|
|
};
|
|
}
|
|
}
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` \u2713 ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing session-inspect.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
if (test('shows usage when no target is provided', () => {
|
|
const result = run();
|
|
assert.strictEqual(result.code, 1);
|
|
assert.ok(result.stdout.includes('Usage:'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('lists registered adapters', () => {
|
|
const result = run(['--list-adapters']);
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.ok(Array.isArray(payload.adapters));
|
|
assert.ok(payload.adapters.some(adapter => adapter.id === 'claude-history'));
|
|
assert.ok(payload.adapters.some(adapter => adapter.id === 'dmux-tmux'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('prints canonical JSON for claude history targets', () => {
|
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
|
|
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-recordings-'));
|
|
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
|
|
try {
|
|
fs.writeFileSync(
|
|
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
|
|
'# Inspect Session\n\n**Branch:** feat/session-inspect\n'
|
|
);
|
|
|
|
const result = run(['claude:latest'], {
|
|
env: {
|
|
HOME: homeDir,
|
|
ECC_SESSION_RECORDING_DIR: recordingDir
|
|
}
|
|
});
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
const recordingPath = getFallbackSessionRecordingPath(payload, { recordingDir });
|
|
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
|
|
assert.strictEqual(payload.adapterId, 'claude-history');
|
|
assert.strictEqual(payload.session.kind, 'history');
|
|
assert.strictEqual(payload.workers[0].branch, 'feat/session-inspect');
|
|
assert.strictEqual(persisted.latest.adapterId, 'claude-history');
|
|
assert.strictEqual(persisted.history.length, 1);
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
fs.rmSync(recordingDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('supports explicit target types for structured registry routing', () => {
|
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
|
|
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
|
|
try {
|
|
fs.writeFileSync(
|
|
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
|
|
'# Inspect Session\n\n**Branch:** feat/typed-inspect\n'
|
|
);
|
|
|
|
const result = run(['latest', '--target-type', 'claude-history'], {
|
|
env: { HOME: homeDir }
|
|
});
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.strictEqual(payload.adapterId, 'claude-history');
|
|
assert.strictEqual(payload.session.sourceTarget.type, 'claude-history');
|
|
assert.strictEqual(payload.workers[0].branch, 'feat/typed-inspect');
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('writes snapshot JSON to disk when --write is provided', () => {
|
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
|
|
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-out-'));
|
|
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
|
|
const outputPath = path.join(outputDir, 'snapshot.json');
|
|
|
|
try {
|
|
fs.writeFileSync(
|
|
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
|
|
'# Inspect Session\n\n**Branch:** feat/session-inspect\n'
|
|
);
|
|
|
|
const result = run(['claude:latest', '--write', outputPath], {
|
|
env: { HOME: homeDir }
|
|
});
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
assert.ok(fs.existsSync(outputPath));
|
|
const written = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
assert.strictEqual(written.adapterId, 'claude-history');
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('inspects skill health from recorded observations', () => {
|
|
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-skills-'));
|
|
const observationsDir = path.join(projectRoot, '.claude', 'ecc', 'skills');
|
|
fs.mkdirSync(observationsDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(observationsDir, 'observations.jsonl'),
|
|
[
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-1',
|
|
timestamp: '2026-03-14T12:00:00.000Z',
|
|
task: 'Review auth middleware',
|
|
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
|
|
outcome: { success: false, status: 'failure', error: 'missing csrf guidance', feedback: 'Need CSRF coverage' },
|
|
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-1' }
|
|
}),
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-2',
|
|
timestamp: '2026-03-14T12:05:00.000Z',
|
|
task: 'Review auth middleware',
|
|
skill: { id: 'security-review', path: 'skills/security-review/SKILL.md' },
|
|
outcome: { success: false, status: 'failure', error: 'missing csrf guidance', feedback: null },
|
|
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-2' }
|
|
})
|
|
].join('\n') + '\n'
|
|
);
|
|
|
|
try {
|
|
const result = run(['skills:health'], { cwd: projectRoot });
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.strictEqual(payload.schemaVersion, 'ecc.skill-health.v1');
|
|
assert.ok(payload.skills.some(skill => skill.skill.id === 'security-review'));
|
|
} finally {
|
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('proposes skill amendments through session-inspect', () => {
|
|
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-amend-'));
|
|
const observationsDir = path.join(projectRoot, '.claude', 'ecc', 'skills');
|
|
fs.mkdirSync(observationsDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(observationsDir, 'observations.jsonl'),
|
|
[
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-1',
|
|
timestamp: '2026-03-14T12:00:00.000Z',
|
|
task: 'Add rate limiting',
|
|
skill: { id: 'api-design', path: 'skills/api-design/SKILL.md' },
|
|
outcome: { success: false, status: 'failure', error: 'missing rate limiting guidance', feedback: 'Need rate limiting examples' },
|
|
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-1' }
|
|
})
|
|
].join('\n') + '\n'
|
|
);
|
|
|
|
try {
|
|
const result = run(['skills:amendify', '--skill', 'api-design'], { cwd: projectRoot });
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.strictEqual(payload.schemaVersion, 'ecc.skill-amendment-proposal.v1');
|
|
assert.strictEqual(payload.skill.id, 'api-design');
|
|
assert.ok(payload.patch.preview.includes('Failure-Driven Amendments'));
|
|
} finally {
|
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('builds skill evaluation scaffolding through session-inspect', () => {
|
|
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-eval-'));
|
|
const observationsDir = path.join(projectRoot, '.claude', 'ecc', 'skills');
|
|
fs.mkdirSync(observationsDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(observationsDir, 'observations.jsonl'),
|
|
[
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-1',
|
|
timestamp: '2026-03-14T12:00:00.000Z',
|
|
task: 'Fix flaky login test',
|
|
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
|
|
outcome: { success: false, status: 'failure', error: null, feedback: null },
|
|
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-1' }
|
|
}),
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-2',
|
|
timestamp: '2026-03-14T12:10:00.000Z',
|
|
task: 'Fix flaky checkout test',
|
|
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
|
|
outcome: { success: true, status: 'success', error: null, feedback: null },
|
|
run: { variant: 'baseline', amendmentId: null, sessionId: 'sess-2' }
|
|
}),
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-3',
|
|
timestamp: '2026-03-14T12:20:00.000Z',
|
|
task: 'Fix flaky login test',
|
|
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
|
|
outcome: { success: true, status: 'success', error: null, feedback: null },
|
|
run: { variant: 'amended', amendmentId: 'amend-1', sessionId: 'sess-3' }
|
|
}),
|
|
JSON.stringify({
|
|
schemaVersion: 'ecc.skill-observation.v1',
|
|
observationId: 'obs-4',
|
|
timestamp: '2026-03-14T12:30:00.000Z',
|
|
task: 'Fix flaky checkout test',
|
|
skill: { id: 'e2e-testing', path: 'skills/e2e-testing/SKILL.md' },
|
|
outcome: { success: true, status: 'success', error: null, feedback: null },
|
|
run: { variant: 'amended', amendmentId: 'amend-1', sessionId: 'sess-4' }
|
|
})
|
|
].join('\n') + '\n'
|
|
);
|
|
|
|
try {
|
|
const result = run(['skills:evaluate', '--skill', 'e2e-testing', '--amendment-id', 'amend-1'], { cwd: projectRoot });
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.strictEqual(payload.schemaVersion, 'ecc.skill-evaluation.v1');
|
|
assert.strictEqual(payload.recommendation, 'promote-amendment');
|
|
} finally {
|
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests();
|