mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-16 13:15:36 +02:00
157 lines
6.4 KiB
JavaScript
Executable File
157 lines
6.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
const saveScript = path.join(repoRoot, 'skills', 'skill-stocktake', 'scripts', 'save-results.sh');
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(description, fn) {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-stocktake-save-'));
|
|
try {
|
|
fn(path.join(tempRoot, 'results.json'));
|
|
console.log(` ✓ ${description}`);
|
|
passed++;
|
|
} catch (error) {
|
|
console.log(` ✗ ${description}: ${error.message}`);
|
|
failed++;
|
|
} finally {
|
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function save(resultsPath, input, args = []) {
|
|
return spawnSync('bash', [saveScript, resultsPath, ...args], {
|
|
encoding: 'utf8',
|
|
input: typeof input === 'string' ? input : JSON.stringify(input),
|
|
env: process.env,
|
|
});
|
|
}
|
|
|
|
function readResults(resultsPath) {
|
|
const output = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
|
|
assert.match(output.evaluated_at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
assert.ok(Math.abs(Date.now() - Date.parse(output.evaluated_at)) < 60000);
|
|
return output;
|
|
}
|
|
|
|
console.log('\nSkill stocktake save-results tests:');
|
|
|
|
if (process.platform === 'win32') {
|
|
console.log(' ↷ Bash integration cases skipped on Windows');
|
|
} else {
|
|
for (const mode of [undefined, 'quick']) {
|
|
test(`default save merges ${mode || 'legacy'} results and retains unchanged skills`, resultsPath => {
|
|
const unchanged = { verdict: 'Keep', reason: 'unchanged skill' };
|
|
fs.writeFileSync(resultsPath, JSON.stringify({
|
|
evaluated_at: '2000-01-01T00:00:00Z',
|
|
mode: 'full',
|
|
batch_progress: { completed: 1, total: 2 },
|
|
skills: { unchanged, updated: { verdict: 'Improve' } },
|
|
}));
|
|
const input = {
|
|
skills: { updated: { verdict: 'Keep' }, added: { verdict: 'Keep' } },
|
|
...(mode ? { mode } : {}),
|
|
};
|
|
const result = save(resultsPath, input);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
const output = readResults(resultsPath);
|
|
assert.deepStrictEqual(output.skills, { unchanged, ...input.skills });
|
|
assert.strictEqual(output.mode, mode || 'full');
|
|
assert.deepStrictEqual(output.batch_progress, { completed: 1, total: 2 });
|
|
});
|
|
}
|
|
|
|
test('full-mode intermediate saves still merge completed batches for resume', resultsPath => {
|
|
const completed = { verdict: 'Keep' };
|
|
fs.writeFileSync(resultsPath, JSON.stringify({
|
|
mode: 'full', batch_progress: { completed: 1, total: 2 }, skills: { completed },
|
|
}));
|
|
const input = {
|
|
mode: 'full', batch_progress: { completed: 2, total: 2 }, skills: { next: { verdict: 'Improve' } },
|
|
};
|
|
const result = save(resultsPath, input);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
const output = readResults(resultsPath);
|
|
assert.deepStrictEqual(output.skills, { completed, ...input.skills });
|
|
assert.deepStrictEqual(output.batch_progress, input.batch_progress);
|
|
});
|
|
|
|
test('explicit replacement saves only the live inventory and drops obsolete metadata', resultsPath => {
|
|
fs.writeFileSync(resultsPath, JSON.stringify({
|
|
evaluated_at: '2000-01-01T00:00:00Z',
|
|
mode: 'quick',
|
|
batch_progress: { completed: 1, total: 2 },
|
|
obsolete_summary: 'old cache',
|
|
skills: {
|
|
'~/.claude/skills/.trash/deleted/SKILL.md': { verdict: 'Keep' },
|
|
'~/.claude/skills/removed/SKILL.md': { verdict: 'Keep' },
|
|
},
|
|
}));
|
|
const input = {
|
|
mode: 'full',
|
|
evaluated_at: '1999-01-01T00:00:00Z',
|
|
skills: { '~/.claude/skills/live/SKILL.md': { verdict: 'Keep' } },
|
|
};
|
|
const result = save(resultsPath, input, ['--replace']);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
const output = readResults(resultsPath);
|
|
assert.deepStrictEqual(output, { ...input, evaluated_at: output.evaluated_at });
|
|
});
|
|
|
|
test('explicit replacement with an empty inventory removes all cached skills', resultsPath => {
|
|
fs.writeFileSync(resultsPath, JSON.stringify({
|
|
skills: { stale: { verdict: 'Keep' } }, batch_progress: { completed: 1, total: 2 },
|
|
}));
|
|
const input = { mode: 'full', skills: {} };
|
|
const result = save(resultsPath, input, ['--replace']);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
const output = readResults(resultsPath);
|
|
assert.deepStrictEqual(output, { ...input, evaluated_at: output.evaluated_at });
|
|
});
|
|
|
|
test('invalid JSON during replacement preserves the existing results byte for byte', resultsPath => {
|
|
const original = '{\n "skills": {"keep": {"verdict": "Keep"}}\n}\n';
|
|
fs.writeFileSync(resultsPath, original);
|
|
const result = save(resultsPath, '{"skills":', ['--replace']);
|
|
assert.notStrictEqual(result.status, 0);
|
|
assert.match(result.stderr, /not valid JSON/);
|
|
assert.strictEqual(fs.readFileSync(resultsPath, 'utf8'), original);
|
|
assert.deepStrictEqual(fs.readdirSync(path.dirname(resultsPath)), ['results.json']);
|
|
});
|
|
|
|
test('replacement rejects absent, non-object, or multiple inventories without altering the cache', resultsPath => {
|
|
const original = '{\n "skills": {"keep": {"verdict": "Keep"}}\n}\n';
|
|
const invalidInputs = ['', 'null', '[]', '{}', '{"skills":[]}', '{"skills":null}',
|
|
'{"skills":{}}\n{"skills":{}}'];
|
|
for (const input of invalidInputs) {
|
|
fs.writeFileSync(resultsPath, original);
|
|
const result = save(resultsPath, input, ['--replace']);
|
|
assert.notStrictEqual(result.status, 0, `accepted invalid replacement: ${JSON.stringify(input)}`);
|
|
assert.strictEqual(fs.readFileSync(resultsPath, 'utf8'), original);
|
|
assert.deepStrictEqual(fs.readdirSync(path.dirname(resultsPath)), ['results.json']);
|
|
}
|
|
});
|
|
|
|
for (const args of [[], ['--replace']]) {
|
|
test(`${args.length ? 'replacement' : 'default'} save bootstraps a new results file`, resultsPath => {
|
|
const input = { mode: 'full', skills: { live: { verdict: 'Keep' } } };
|
|
const result = save(resultsPath, input, args);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
const output = readResults(resultsPath);
|
|
assert.deepStrictEqual(output, { ...input, evaluated_at: output.evaluated_at });
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(`\nPassed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|