mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
fix: validate rollback allocation before changing live skill
This commit is contained in:
2 files changed
+48
-5
No files matched your search
@@ -169,9 +169,7 @@ function getEvolutionLog(skillPath, logType) {
|
||||
return readJsonl(getEvolutionLogPath(skillPath, logType));
|
||||
}
|
||||
|
||||
function createVersion(skillPath, options = {}) {
|
||||
const skillFilePath = ensureSkillExists(skillPath);
|
||||
|
||||
function allocateSnapshotVersion(skillPath) {
|
||||
const versions = listVersions(skillPath);
|
||||
const versionsDir = getVersionsDir(skillPath);
|
||||
const occupiedPaths = new Set(fs.existsSync(versionsDir) ? fs.readdirSync(versionsDir) : []);
|
||||
@@ -183,6 +181,13 @@ function createVersion(skillPath, options = {}) {
|
||||
if (!Number.isSafeInteger(nextVersion)) {
|
||||
throw new Error('Skill snapshot version range is exhausted; existing snapshots were preserved');
|
||||
}
|
||||
return nextVersion;
|
||||
}
|
||||
|
||||
function createVersion(skillPath, options = {}) {
|
||||
const skillFilePath = ensureSkillExists(skillPath);
|
||||
const nextVersion = allocateSnapshotVersion(skillPath);
|
||||
const versionsDir = getVersionsDir(skillPath);
|
||||
ensureSkillVersioning(skillPath);
|
||||
const snapshotPath = path.join(versionsDir, `v${nextVersion}.md`);
|
||||
const skillContent = fs.readFileSync(skillFilePath, 'utf8');
|
||||
@@ -207,12 +212,11 @@ function createVersion(skillPath, options = {}) {
|
||||
|
||||
function rollbackTo(skillPath, targetVersion, options = {}) {
|
||||
const normalizedTargetVersion = Number(targetVersion);
|
||||
if (!Number.isInteger(normalizedTargetVersion) || normalizedTargetVersion <= 0) {
|
||||
if (!Number.isSafeInteger(normalizedTargetVersion) || normalizedTargetVersion <= 0) {
|
||||
throw new Error(`Invalid target version: ${targetVersion}`);
|
||||
}
|
||||
|
||||
ensureSkillExists(skillPath);
|
||||
ensureSkillVersioning(skillPath);
|
||||
|
||||
const targetPath = path.join(getVersionsDir(skillPath), `v${normalizedTargetVersion}.md`);
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
@@ -221,6 +225,7 @@ function rollbackTo(skillPath, targetVersion, options = {}) {
|
||||
|
||||
const currentVersion = getCurrentVersion(skillPath);
|
||||
const targetContent = fs.readFileSync(targetPath, 'utf8');
|
||||
allocateSnapshotVersion(skillPath);
|
||||
fs.writeFileSync(getSkillFilePath(skillPath), targetContent, 'utf8');
|
||||
|
||||
const createdVersion = createVersion(skillPath, {
|
||||
|
||||
@@ -102,5 +102,43 @@ test('an occupied final safe snapshot path refuses creation while preserving the
|
||||
assert.equal(fs.existsSync(versioning.getEvolutionDir(skillDir)), false);
|
||||
});
|
||||
|
||||
test('exhausted rollback preserves unsnapshotted content, snapshot files and evolution state', fixture => {
|
||||
const { skillDir, versionsDir } = fixture;
|
||||
fs.mkdirSync(versionsDir);
|
||||
const previous = path.join(versionsDir, 'v1.md');
|
||||
const last = path.join(versionsDir, `v${Number.MAX_SAFE_INTEGER}.md`);
|
||||
fs.writeFileSync(previous, '# Previous\n');
|
||||
fs.writeFileSync(last, '# Latest snapshot\n');
|
||||
const assertPreserved = () => {
|
||||
assert.equal(fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'), '# Original\n');
|
||||
assert.equal(fs.readFileSync(previous, 'utf8'), '# Previous\n');
|
||||
assert.equal(fs.readFileSync(last, 'utf8'), '# Latest snapshot\n');
|
||||
assert.equal(fs.readdirSync(versionsDir).length, 2);
|
||||
};
|
||||
assert.throws(() => versioning.rollbackTo(skillDir, 1), /snapshot version range is exhausted/);
|
||||
assertPreserved();
|
||||
assert.equal(fs.existsSync(versioning.getEvolutionDir(skillDir)), false);
|
||||
versioning.ensureSkillVersioning(skillDir);
|
||||
const logs = versioning.EVOLUTION_LOG_TYPES.map(type => versioning.getEvolutionLogPath(skillDir, type));
|
||||
for (const file of logs) fs.writeFileSync(file, '{"event":"preserve"}\n');
|
||||
assert.throws(() => versioning.rollbackTo(skillDir, 1), /snapshot version range is exhausted/);
|
||||
assertPreserved();
|
||||
for (const file of logs) assert.equal(fs.readFileSync(file, 'utf8'), '{"event":"preserve"}\n');
|
||||
});
|
||||
|
||||
test('unsafe rollback targets preserve the live skill and excluded snapshots', fixture => {
|
||||
const { skillDir, versionsDir } = fixture;
|
||||
fs.mkdirSync(versionsDir);
|
||||
const invalid = path.join(versionsDir, 'v9007199254740992.md');
|
||||
fs.writeFileSync(invalid, '# Excluded snapshot\n');
|
||||
for (const target of [9007199254740992, '9007199254740992']) {
|
||||
assert.throws(() => versioning.rollbackTo(skillDir, target), /Invalid target version/);
|
||||
assert.equal(fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'), '# Original\n');
|
||||
assert.equal(fs.readFileSync(invalid, 'utf8'), '# Excluded snapshot\n');
|
||||
assert.equal(fs.readdirSync(versionsDir).length, 1);
|
||||
assert.equal(fs.existsSync(versioning.getEvolutionDir(skillDir)), false);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exitCode = failed > 0 ? 1 : 0;
|
||||
Reference in new issue
Block a user