mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-15 12:45:29 +02:00
fix: preserve snapshot paths and reject malformed skill tracking rows
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"id": "hand-seeded",
|
||||
"source": "manual-curation-pending-regeneration"
|
||||
},
|
||||
"registryDigest": "7144a76d4b920efacf5a8d678ec6757ea34410834c82f4e226b6a32af6f8c540",
|
||||
"registryDigest": "cb89b3434da17c00988095077f61208ab81c2f0611f0edbd5c90d3672bce7ac1",
|
||||
"schemaVersion": 1,
|
||||
"triggers": {
|
||||
"skill:agent-runtime-gateway-smoke-test": [
|
||||
|
||||
@@ -103,7 +103,10 @@ function readJsonl(filePath) {
|
||||
.reduce((rows, line) => {
|
||||
try {
|
||||
const record = JSON.parse(line);
|
||||
if (record && typeof record === 'object' && !Array.isArray(record)) {
|
||||
if (record && typeof record === 'object' && !Array.isArray(record)
|
||||
&& typeof record.skill_id === 'string' && record.skill_id.trim().length > 0
|
||||
&& VALID_OUTCOMES.has(record.outcome)
|
||||
&& typeof record.recorded_at === 'string' && !Number.isNaN(Date.parse(record.recorded_at))) {
|
||||
rows.push(record);
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -100,7 +100,14 @@ function listVersions(skillPath) {
|
||||
}
|
||||
|
||||
const filePath = path.join(versionsDir, fileName);
|
||||
const stats = fs.statSync(filePath);
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(filePath);
|
||||
} catch (error) {
|
||||
// A dangling link still occupies a path, but is not a readable snapshot.
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
}
|
||||
if (!stats.isFile()) {
|
||||
return null;
|
||||
}
|
||||
@@ -146,7 +153,10 @@ function readJsonl(filePath) {
|
||||
.filter(Boolean)
|
||||
.reduce((rows, line) => {
|
||||
try {
|
||||
rows.push(JSON.parse(line));
|
||||
const record = JSON.parse(line);
|
||||
if (record && typeof record === 'object' && !Array.isArray(record)) {
|
||||
rows.push(record);
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed rows so the log remains append-only and resilient.
|
||||
}
|
||||
@@ -163,12 +173,18 @@ function createVersion(skillPath, options = {}) {
|
||||
ensureSkillVersioning(skillPath);
|
||||
|
||||
const versions = listVersions(skillPath);
|
||||
const nextVersion = versions.length === 0 ? 1 : versions[versions.length - 1].version + 1;
|
||||
const snapshotPath = path.join(getVersionsDir(skillPath), `v${nextVersion}.md`);
|
||||
const versionsDir = getVersionsDir(skillPath);
|
||||
const occupiedPaths = new Set(fs.readdirSync(versionsDir));
|
||||
let nextVersion = versions.length === 0 ? 1 : versions[versions.length - 1].version + 1;
|
||||
// Listing excludes non-files, but their paths remain user-owned and occupied.
|
||||
while (occupiedPaths.has(`v${nextVersion}.md`)) {
|
||||
nextVersion += 1;
|
||||
}
|
||||
const snapshotPath = path.join(versionsDir, `v${nextVersion}.md`);
|
||||
const skillContent = fs.readFileSync(skillFilePath, 'utf8');
|
||||
const createdAt = options.timestamp || new Date().toISOString();
|
||||
|
||||
fs.writeFileSync(snapshotPath, skillContent, 'utf8');
|
||||
fs.writeFileSync(snapshotPath, skillContent, { encoding: 'utf8', flag: 'wx' });
|
||||
appendEvolutionRecord(skillPath, 'amendments', {
|
||||
event: 'snapshot',
|
||||
version: nextVersion,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const versioning = require('../../scripts/lib/skill-evolution/versioning');
|
||||
const health = require('../../scripts/lib/skill-evolution/health');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function createFixture() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-evolution-storage-'));
|
||||
const skillDir = path.join(root, 'skills', 'demo');
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Original\n');
|
||||
return { root, skillDir, versionsDir: versioning.getVersionsDir(skillDir) };
|
||||
}
|
||||
|
||||
function test(name, fn) {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
fn(fixture);
|
||||
passed += 1;
|
||||
console.log(` ✓ ${name}`);
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
console.error(` ✗ ${name}: ${error.message}`);
|
||||
} finally {
|
||||
fs.rmSync(fixture.root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('snapshot creation preserves occupied directories and links plus earlier snapshots', fixture => {
|
||||
const { root, skillDir, versionsDir } = fixture;
|
||||
versioning.createVersion(skillDir);
|
||||
fs.mkdirSync(path.join(versionsDir, 'v2.md'));
|
||||
fs.writeFileSync(path.join(versionsDir, 'v2.md', 'keep.txt'), 'preserve');
|
||||
if (process.platform !== 'win32') {
|
||||
fs.symlinkSync(path.join(root, 'missing'), path.join(versionsDir, 'v3.md'));
|
||||
}
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Updated\n');
|
||||
const snapshot = versioning.createVersion(skillDir);
|
||||
assert.equal(snapshot.version, process.platform === 'win32' ? 3 : 4);
|
||||
assert.equal(fs.readFileSync(snapshot.path, 'utf8'), '# Updated\n');
|
||||
assert.equal(fs.readFileSync(path.join(versionsDir, 'v1.md'), 'utf8'), '# Original\n');
|
||||
assert.equal(fs.readFileSync(path.join(versionsDir, 'v2.md', 'keep.txt'), 'utf8'), 'preserve');
|
||||
assert.equal(versioning.getEvolutionLog(skillDir, 'amendments').at(-1).version, snapshot.version);
|
||||
if (process.platform !== 'win32') {
|
||||
assert.ok(fs.lstatSync(path.join(versionsDir, 'v3.md')).isSymbolicLink());
|
||||
fs.symlinkSync(path.join(versionsDir, 'v1.md'), path.join(versionsDir, 'v5.md'));
|
||||
assert.deepEqual(versioning.listVersions(skillDir).map(entry => entry.version), [1, 4, 5]);
|
||||
assert.equal(versioning.getCurrentVersion(skillDir), 5);
|
||||
}
|
||||
});
|
||||
|
||||
test('health skips malformed amendment rows while retaining a valid pending proposal', fixture => {
|
||||
const { root, skillDir } = fixture;
|
||||
versioning.ensureSkillVersioning(skillDir);
|
||||
fs.writeFileSync(versioning.getEvolutionLogPath(skillDir, 'amendments'),
|
||||
'null\n[]\n42\n{bad}\n{"event":"proposal","status":"pending"}\n');
|
||||
const report = health.collectSkillHealth({
|
||||
skillsRoot: path.join(root, 'skills'), homeDir: root, now: '2026-03-15T12:00:00.000Z',
|
||||
});
|
||||
assert.equal(report.skills.find(skill => skill.skill_id === 'demo').pending_amendments, 1);
|
||||
});
|
||||
|
||||
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exitCode = failed > 0 ? 1 : 0;
|
||||
@@ -320,7 +320,7 @@ function runTests() {
|
||||
duration_ms: 1,
|
||||
user_feedback: 'accepted',
|
||||
recorded_at: '2026-03-15T11:45:00.000Z',
|
||||
})}\n{bad-json}\nnull\n42\nfalse\n"not a record"\n[]\n`,
|
||||
})}\n{bad-json}\nnull\n42\nfalse\n"not a record"\n[]\n{}\n{"skill_id":42}\n{"skill_id":""}\n{"skill_id":"alpha","outcome":"invalid","recorded_at":"2026-03-15T11:45:00.000Z"}\n{"skill_id":"alpha","outcome":"success","recorded_at":"bad-date"}\n`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user