* test(#4145): regression rows for hash-matching prefix-less pristine baselines RED skeleton: src/pristine-baseline.cts exports findPristineByHash as a null-returning stub so the new rows fail behaviorally, not at require time. Failing-first rows: verifier resolution (no_baseline must drop to 0 when an exact-hash orphan exists), findPristineByHash unit row, and the two saveLocalPatches relocation rows. Negative-space rows pin today's behavior: missing baselines still report ok_no_baseline, mismatching orphans are never adopted or deleted, canonical precedence and the #3657 drift posture are untouched. * fix(#4145): resolve gsd-pristine/ baselines by recorded hash, relocate orphans Both pristine readers joined the manifest-keyed path strictly, so a snapshot stored without the gsd-core/ prefix (an earlier release's writer) was reported as ok_no_baseline by the verifier and pushed into regeneration by saveLocalPatches — where incoming-release candidates can never satisfy the recorded outgoing hash, leaving the correct baseline permanently unconsumed. - src/pristine-baseline.cts (new, ADR-457): shared findPristineByHash — deterministic sorted scan of gsd-pristine/, exact sha-256 equality with the recorded pristine_hashes entry (the same authority the #3657 drift guard trusts), symlink-skipping, canonical path excluded via skipRel. - verify-reapply-patches.cjs verifyFile(): on canonical miss with a recorded hash, adopt byte-identical content found anywhere under gsd-pristine/ before reporting OK_NO_BASELINE. Drift posture (#3657), canonical precedence, and the frozen REASON/report shapes are untouched; the verifier stays read-only. - install.js saveLocalPatches(): preserve-check rescue — relocate a hash-matching orphan to the canonical path (copy, hash-verify, then remove the orphan) so the state self-heals on the next update instead of repeating forever. Honest accounting: new non-overlapping rescued counter. - Workflow doc: one-sentence note on hash-based snapshot resolution. - Derived ripples: INVENTORY-MANIFEST.json regen, eslint ignore + .gitignore entries for the compiled artifact, seedFixture mkdir fix in the new rows. Emitted-Drift-Ack-Growth: reapply-patches.md — one-sentence note on hash-based pristine snapshot resolution (#4145) * fix(#4145): review follow-up — orphan scan never consumes a canonical path Adversarial review finding: with two modified files sharing byte-identical outgoing content, recoverOrphanedPristine could adopt the OTHER file's canonical pristine as its rescue source — relocating it (copy + delete at its home path) and ping-ponging the single baseline between the two files across updates. findPristineByHash's skip parameter now accepts a Set, and saveLocalPatches passes the normalized manifest keys so every canonical path is excluded; only genuine non-canonical orphans are eligible for removal (no strict-join reader ever consults those). Adds the canonical-theft regression row, a Set-skip unit assertion, and tightens the workflow doc sentence the same pass flagged as overstated. * fix(#4145): INVENTORY roster row + symlink-fixture correction Two leftovers from the ab17b7a1e5 bench run, both root-caused: - docs/INVENTORY.md roster row for cli_modules/pristine-baseline.cjs (#3762 gate: every manifest entry carries a row). - The findPristineByHash symlink unit fixture placed its symlink target INSIDE the scanned root, so the walk legitimately matched the real target file. The implementation skips the symlink itself; the fixture now keeps the target outside the scanned tree so the assertion tests what it claims. * changeset(#4145): fixed fragment for pristine baseline hash resolution --------- Co-authored-by: gsd-agent <agent@gsd.local>
This commit is contained in:
@@ -4013,3 +4013,227 @@ describe('Bug #4086: saveLocalPatches resolves skills/ manifest keys at the runt
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded regression block — #4145 (self-heal side). saveLocalPatches'
|
||||
// preserve-check resolved gsd-pristine/ entries strictly by the manifest-keyed
|
||||
// path, so a hash-matching snapshot stored without the gsd-core/ prefix was
|
||||
// pushed into regeneration from the incoming release; when upstream changed
|
||||
// the file, the candidate hash-mismatched and was discarded. The correct
|
||||
// baseline was never consumed and never pruned — the state repeated on every
|
||||
// future update. The fix rescues exact-recorded-hash orphans by relocating
|
||||
// them to the canonical path.
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe('folded:bug-4145-saveLocalPatches-orphan-rescue', () => {
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe, beforeEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const INSTALL = require(path.join(ROOT, 'bin', 'install.js'));
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
|
||||
function sha256(content) {
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
describe('Bug #4145: saveLocalPatches rescues hash-matching orphaned pristine snapshots', () => {
|
||||
let tmpDir;
|
||||
let configDir;
|
||||
let newSrcDir;
|
||||
let pristineDir;
|
||||
|
||||
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
|
||||
const OLD_PRISTINE = '# Old Release Content\nThis is the outgoing pristine.\n';
|
||||
const NEW_RELEASE = '# New Release Content\nUpstream rewrote this file wholesale in v2.\n';
|
||||
const USER_MODIFIED = OLD_PRISTINE + '## User addition\nUser customization here.\n';
|
||||
|
||||
beforeEach((t) => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-slp-'));
|
||||
configDir = path.join(tmpDir, 'config');
|
||||
newSrcDir = path.join(tmpDir, 'new-release-src');
|
||||
pristineDir = path.join(configDir, 'gsd-pristine');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.mkdirSync(newSrcDir, { recursive: true });
|
||||
t.after(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
});
|
||||
|
||||
function seedFixture({ orphanRel, orphanContent, canonicalContent, newReleaseContent }) {
|
||||
fs.mkdirSync(path.dirname(path.join(configDir, FILE)), { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, FILE), USER_MODIFIED);
|
||||
fs.writeFileSync(
|
||||
path.join(configDir, MANIFEST_NAME),
|
||||
JSON.stringify({ version: '1.0.0', files: { [FILE]: sha256(OLD_PRISTINE) } }, null, 2),
|
||||
);
|
||||
if (canonicalContent !== undefined) {
|
||||
fs.mkdirSync(path.dirname(path.join(pristineDir, FILE)), { recursive: true });
|
||||
fs.writeFileSync(path.join(pristineDir, FILE), canonicalContent);
|
||||
}
|
||||
if (orphanRel !== undefined) {
|
||||
fs.mkdirSync(path.dirname(path.join(pristineDir, orphanRel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(pristineDir, orphanRel), orphanContent);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(path.join(newSrcDir, FILE)), { recursive: true });
|
||||
fs.writeFileSync(path.join(newSrcDir, FILE), newReleaseContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core regression (self-heal): the hash-matching snapshot sits at
|
||||
* bin/lib/frontmatter.cjs — without the gsd-core/ segment. The new release
|
||||
* changed the file upstream, so regeneration candidates are discarded.
|
||||
* After the fix the orphan is relocated to the canonical manifest-keyed
|
||||
* path and the unprefixed copy no longer lingers.
|
||||
*/
|
||||
test('#4145: saveLocalPatches relocates a hash-matching unprefixed orphan to the canonical pristine path', () => {
|
||||
const orphanRel = 'bin/lib/frontmatter.cjs';
|
||||
seedFixture({ orphanRel, orphanContent: OLD_PRISTINE, newReleaseContent: NEW_RELEASE });
|
||||
|
||||
INSTALL.saveLocalPatches(configDir, {
|
||||
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
|
||||
});
|
||||
|
||||
const canonical = path.join(pristineDir, FILE);
|
||||
assert.ok(fs.existsSync(canonical), 'canonical prefixed pristine must exist after the update');
|
||||
assert.equal(sha256(fs.readFileSync(canonical, 'utf8')), sha256(OLD_PRISTINE),
|
||||
'relocated baseline must carry the outgoing (recorded-hash) bytes, not new-release bytes');
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, orphanRel)), false,
|
||||
'the unprefixed orphan must not linger once relocated');
|
||||
});
|
||||
|
||||
/**
|
||||
* Stale canonical (new-release bytes) + hash-matching orphan elsewhere:
|
||||
* the stale entry is removed by the #3407 path and then rescued from the
|
||||
* orphan — the file must not end in no-baseline limbo.
|
||||
*/
|
||||
test('#4145: rescues after stale-canonical removal when a hash-matching orphan exists', () => {
|
||||
seedFixture({
|
||||
orphanRel: 'legacy/frontmatter.cjs',
|
||||
orphanContent: OLD_PRISTINE,
|
||||
canonicalContent: NEW_RELEASE, // stale — hash mismatch
|
||||
newReleaseContent: NEW_RELEASE,
|
||||
});
|
||||
|
||||
INSTALL.saveLocalPatches(configDir, {
|
||||
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
|
||||
});
|
||||
|
||||
const canonical = path.join(pristineDir, FILE);
|
||||
assert.ok(fs.existsSync(canonical), 'canonical pristine must exist after stale removal + rescue');
|
||||
assert.equal(sha256(fs.readFileSync(canonical, 'utf8')), sha256(OLD_PRISTINE),
|
||||
'rescued baseline must carry the recorded-hash bytes');
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, 'legacy', 'frontmatter.cjs')), false,
|
||||
'the orphan must be consumed by the relocation');
|
||||
});
|
||||
|
||||
/** Negative space: no orphan, upstream changed — regeneration discard (#3407) is unchanged. */
|
||||
test('#4145: leaves the baseline absent when no orphan exists and upstream changed', () => {
|
||||
seedFixture({ newReleaseContent: NEW_RELEASE });
|
||||
|
||||
INSTALL.saveLocalPatches(configDir, {
|
||||
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
|
||||
});
|
||||
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, FILE)), false,
|
||||
'no hash-matching source exists — the baseline must stay absent (over-broad/no-baseline fallback)');
|
||||
});
|
||||
|
||||
/** Negative space: a mismatching orphan is neither adopted nor deleted. */
|
||||
test('#4145: never adopts nor deletes a hash-mismatching orphan', () => {
|
||||
const orphanRel = 'bin/lib/frontmatter.cjs';
|
||||
seedFixture({ orphanRel, orphanContent: NEW_RELEASE, newReleaseContent: NEW_RELEASE });
|
||||
|
||||
INSTALL.saveLocalPatches(configDir, {
|
||||
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
|
||||
});
|
||||
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, FILE)), false,
|
||||
'mismatching bytes must not be written to the canonical pristine path');
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, orphanRel)), true,
|
||||
'pruning files the recorded hashes do not vouch for is not this fix\'s job');
|
||||
});
|
||||
|
||||
/**
|
||||
* Review finding (fix follow-up): two modified files sharing byte-identical
|
||||
* outgoing content. The orphan scan must never consume a path that is
|
||||
* another manifest file's canonical pristine path — otherwise the rescue
|
||||
* would relocate a correct canonical away from its owner and the two files
|
||||
* would ping-pong it between updates. Only genuine non-canonical orphans
|
||||
* are eligible.
|
||||
*/
|
||||
test('#4145: does not steal a byte-identical canonical belonging to another modified file', () => {
|
||||
const FILE_B = 'gsd-core/bin/lib/other-file.cjs';
|
||||
const SHARED_OLD = '# Shared Old Content\nByte-identical across two manifest files.\n';
|
||||
// A and B are both user-modified on top of byte-identical outgoing stock,
|
||||
// so both manifest records carry the SAME pristine hash.
|
||||
fs.mkdirSync(path.dirname(path.join(configDir, FILE_B)), { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, FILE), SHARED_OLD + '## User addition A\nCustom A.\n');
|
||||
fs.writeFileSync(path.join(configDir, FILE_B), SHARED_OLD + '## User addition B\nCustom B.\n');
|
||||
fs.writeFileSync(
|
||||
path.join(configDir, MANIFEST_NAME),
|
||||
JSON.stringify({
|
||||
version: '1.0.0',
|
||||
files: { [FILE]: sha256(SHARED_OLD), [FILE_B]: sha256(SHARED_OLD) },
|
||||
}, null, 2),
|
||||
);
|
||||
// A (processed first) has the ALREADY-correct canonical holding the shared
|
||||
// old bytes. B has no canonical and no orphan — B's only possible hash
|
||||
// match is A's canonical. Without the canonical skip set, B's rescue would
|
||||
// copy A's canonical to B's path and then DELETE A's canonical.
|
||||
fs.mkdirSync(path.dirname(path.join(pristineDir, FILE)), { recursive: true });
|
||||
fs.writeFileSync(path.join(pristineDir, FILE), SHARED_OLD);
|
||||
fs.mkdirSync(path.dirname(path.join(newSrcDir, FILE)), { recursive: true });
|
||||
fs.writeFileSync(path.join(newSrcDir, FILE), NEW_RELEASE);
|
||||
fs.mkdirSync(path.dirname(path.join(newSrcDir, FILE_B)), { recursive: true });
|
||||
fs.writeFileSync(path.join(newSrcDir, FILE_B), NEW_RELEASE);
|
||||
|
||||
INSTALL.saveLocalPatches(configDir, {
|
||||
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
|
||||
});
|
||||
|
||||
// A's canonical must survive untouched — never stolen to become B's.
|
||||
assert.ok(fs.existsSync(path.join(pristineDir, FILE)),
|
||||
'the byte-identical canonical of the earlier-processed file must survive');
|
||||
assert.equal(sha256(fs.readFileSync(path.join(pristineDir, FILE), 'utf8')), sha256(SHARED_OLD));
|
||||
// B gains no baseline from A's canonical (falls to regeneration instead).
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, FILE_B)), false,
|
||||
'a canonical path of another file must never be relocated as the rescue source');
|
||||
});
|
||||
|
||||
/** Preserve-path lock: an already-correct canonical stays put; the preserve loop ignores the orphan. */
|
||||
test('#4145: preserves an already-correct canonical and leaves a coexisting identical orphan in place', () => {
|
||||
const orphanRel = 'bin/lib/frontmatter.cjs';
|
||||
seedFixture({
|
||||
orphanRel,
|
||||
orphanContent: OLD_PRISTINE,
|
||||
canonicalContent: OLD_PRISTINE, // already correct
|
||||
newReleaseContent: NEW_RELEASE,
|
||||
});
|
||||
|
||||
INSTALL.saveLocalPatches(configDir, {
|
||||
packageSrc: newSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true,
|
||||
});
|
||||
|
||||
const canonical = path.join(pristineDir, FILE);
|
||||
assert.ok(fs.existsSync(canonical));
|
||||
assert.equal(sha256(fs.readFileSync(canonical, 'utf8')), sha256(OLD_PRISTINE),
|
||||
'preserved canonical must be byte-identical to before the run');
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, orphanRel)), true,
|
||||
'the preserve path must not disturb unrelated files');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1170,3 +1170,272 @@ describe('Bug #4086: verifyFile resolves skills entries at the runtime skills ro
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded regression block — #4145 (a hash-matching gsd-pristine/ baseline
|
||||
// stored without the gsd-core/ prefix is never resolved). verifyFile() joined
|
||||
// the manifest-keyed path strictly; when stat missed and a hash was recorded
|
||||
// it reported OK_NO_BASELINE even though byte-correct content sat elsewhere
|
||||
// under gsd-pristine/. The fix consults the recorded pristine_hashes — the
|
||||
// same authority the #3657 drift guard trusts — and adopts an exact-hash
|
||||
// match found anywhere in the tree.
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe('folded:bug-4145-pristine-prefix-resolution', () => {
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SCRIPT = path.join(ROOT, 'gsd-core', 'bin', 'verify-reapply-patches.cjs');
|
||||
const { REASON } = require(SCRIPT);
|
||||
const { findPristineByHash } = require(
|
||||
path.join(ROOT, 'gsd-core', 'bin', 'lib', 'pristine-baseline.cjs'),
|
||||
);
|
||||
|
||||
let tmpRoot;
|
||||
let patchesDir;
|
||||
let configDir;
|
||||
let pristineDir;
|
||||
|
||||
function sha256(content) {
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
function writeFile(absPath, content) {
|
||||
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
||||
fs.writeFileSync(absPath, content);
|
||||
}
|
||||
|
||||
function writeBackupMeta(pristine_hashes) {
|
||||
writeFile(path.join(patchesDir, 'backup-meta.json'), JSON.stringify({ pristine_hashes }, null, 2));
|
||||
}
|
||||
|
||||
function resetFixture() {
|
||||
for (const dir of [patchesDir, configDir, pristineDir]) {
|
||||
cleanup(dir);
|
||||
}
|
||||
fs.mkdirSync(patchesDir);
|
||||
fs.mkdirSync(configDir);
|
||||
fs.mkdirSync(pristineDir);
|
||||
}
|
||||
|
||||
function runVerifier() {
|
||||
const r = runNode([
|
||||
SCRIPT,
|
||||
'--patches-dir', patchesDir,
|
||||
'--config-dir', configDir,
|
||||
'--pristine-dir', pristineDir,
|
||||
'--json',
|
||||
], { timeoutMs: 30_000 });
|
||||
return {
|
||||
status: r.exitCode,
|
||||
report: r.stdout && r.stdout.length ? JSON.parse(r.stdout) : null,
|
||||
};
|
||||
}
|
||||
|
||||
before(() => {
|
||||
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-'));
|
||||
patchesDir = path.join(tmpRoot, 'patches');
|
||||
configDir = path.join(tmpRoot, 'installed');
|
||||
pristineDir = path.join(tmpRoot, 'pristine');
|
||||
resetFixture();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanup(tmpRoot);
|
||||
});
|
||||
|
||||
describe('Bug #4145: hash-matching prefix-less pristine baseline is resolved', () => {
|
||||
/**
|
||||
* Core regression. The manifest key is `gsd-core/bin/lib/frontmatter.cjs`
|
||||
* but the snapshot sits at `bin/lib/frontmatter.cjs` — one segment away
|
||||
* from the joined path. Its SHA-256 equals the recorded pristine_hashes
|
||||
* entry. The upstream release replaced the file wholesale and only the
|
||||
* user's line survived the merge, so a verifier that recovered the
|
||||
* baseline computes exactly one user-added line (present → exit 0,
|
||||
* no_baseline 0), while the pre-fix run reported ok_no_baseline.
|
||||
*/
|
||||
test('#4145: resolves a hash-matching prefix-less pristine baseline instead of reporting ok_no_baseline', () => {
|
||||
resetFixture();
|
||||
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
|
||||
const OLD_PRISTINE =
|
||||
'outgoing pristine stock line one with substantial content\n' +
|
||||
'outgoing pristine stock line two also substantial content\n';
|
||||
const USER_LINE = 'user customization line that must survive the reapply merge';
|
||||
const backupContent = OLD_PRISTINE + USER_LINE + '\n';
|
||||
const installedContent =
|
||||
'incoming upstream replacement line with substantial content\n' + USER_LINE + '\n';
|
||||
|
||||
writeBackupMeta({ [FILE]: sha256(OLD_PRISTINE) });
|
||||
writeFile(path.join(patchesDir, FILE), backupContent);
|
||||
writeFile(path.join(configDir, FILE), installedContent);
|
||||
// The orphan: same bytes, stored WITHOUT the gsd-core/ prefix.
|
||||
writeFile(path.join(pristineDir, 'bin', 'lib', 'frontmatter.cjs'), OLD_PRISTINE);
|
||||
|
||||
const { status, report } = runVerifier();
|
||||
|
||||
assert.equal(status, 0, `expected exit 0; report=${JSON.stringify(report)}`);
|
||||
assert.equal(report.no_baseline, 0, 'a hash-matching baseline was on disk — it must be resolved');
|
||||
assert.deepEqual(report.no_baseline_files, []);
|
||||
assert.equal(report.failures, 0);
|
||||
const r0 = report.results[0];
|
||||
assert.equal(r0.status, 'ok');
|
||||
assert.notEqual(r0.reason, REASON.OK_NO_BASELINE);
|
||||
});
|
||||
|
||||
/** Negative space: nothing anywhere under gsd-pristine/ matches the record. */
|
||||
test('#4145: still reports ok_no_baseline when the recorded hash matches nothing under gsd-pristine', () => {
|
||||
resetFixture();
|
||||
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
|
||||
const backupContent =
|
||||
'upstream line present in the backup of the outgoing release\n' +
|
||||
'model: sonnet — the user customisation line in the backup file\n';
|
||||
const installedContent =
|
||||
'replacement upstream line in the newer release version\n' +
|
||||
'model: sonnet — the user customisation line in the backup file\n';
|
||||
|
||||
writeBackupMeta({ [FILE]: 'deadbeef00000000000000000000000000000000000000000000000000000001' });
|
||||
writeFile(path.join(patchesDir, FILE), backupContent);
|
||||
writeFile(path.join(configDir, FILE), installedContent);
|
||||
// No pristine file anywhere.
|
||||
|
||||
const { status, report } = runVerifier();
|
||||
|
||||
assert.equal(status, 0, 'no-baseline is advisory, never a failure');
|
||||
assert.equal(report.no_baseline, 1);
|
||||
assert.equal(report.results[0].reason, REASON.OK_NO_BASELINE);
|
||||
});
|
||||
|
||||
/**
|
||||
* Negative space: an orphan whose bytes do NOT hash to the record is never
|
||||
* adopted — only exact recorded-hash matches are accepted.
|
||||
*/
|
||||
test('#4145: never adopts a hash-mismatching orphan — only exact recorded-hash matches', () => {
|
||||
resetFixture();
|
||||
const FILE = 'gsd-core/bin/lib/frontmatter.cjs';
|
||||
const OLD_PRISTINE = 'outgoing pristine bytes that the record hashes\n';
|
||||
const OTHER_CONTENT = 'some other release snapshot with different bytes\n';
|
||||
|
||||
writeBackupMeta({ [FILE]: sha256(OLD_PRISTINE) });
|
||||
writeFile(path.join(patchesDir, FILE), 'outgoing pristine bytes that the record hashes\nuser line\n');
|
||||
writeFile(path.join(configDir, FILE), 'user line\n');
|
||||
// Orphan exists but hashes to something else.
|
||||
writeFile(path.join(pristineDir, 'bin', 'lib', 'frontmatter.cjs'), OTHER_CONTENT);
|
||||
|
||||
const { status, report } = runVerifier();
|
||||
|
||||
assert.equal(status, 0);
|
||||
assert.equal(report.no_baseline, 1, 'a mismatching orphan is not a baseline');
|
||||
assert.equal(report.results[0].reason, REASON.OK_NO_BASELINE);
|
||||
});
|
||||
|
||||
/**
|
||||
* Precedence lock: the canonical prefixed path still resolves exactly as
|
||||
* today even when an identical-content orphan also exists — the strict join
|
||||
* stays first, and the verifier (read-only) leaves the orphan untouched.
|
||||
*/
|
||||
test('#4145: prefixed canonical baseline resolves exactly as today when an identical orphan also exists', () => {
|
||||
resetFixture();
|
||||
const FILE = 'gsd-core/workflows/execute-phase.md';
|
||||
const pristineContent = 'stock workflow line long enough to pass the significance threshold\n';
|
||||
const droppedLine = 'user workflow customisation that was lost in the merge operation';
|
||||
const backupContent = pristineContent + droppedLine + '\n';
|
||||
const installedContent = pristineContent; // user line dropped — real failure
|
||||
|
||||
writeBackupMeta({ [FILE]: sha256(pristineContent) });
|
||||
writeFile(path.join(patchesDir, FILE), backupContent);
|
||||
writeFile(path.join(configDir, FILE), installedContent);
|
||||
writeFile(path.join(pristineDir, FILE), pristineContent);
|
||||
const orphanPath = path.join(pristineDir, 'workflows', 'execute-phase.md');
|
||||
writeFile(orphanPath, pristineContent);
|
||||
|
||||
const { status, report } = runVerifier();
|
||||
|
||||
assert.equal(status, 1, 'the dropped user line must still be caught via the canonical baseline');
|
||||
const r0 = report.results[0];
|
||||
assert.equal(r0.status, 'fail');
|
||||
assert.equal(r0.reason, REASON.FAIL_USER_LINES_MISSING);
|
||||
assert.ok(r0.missing.includes(droppedLine));
|
||||
// Read-only verifier: the orphan is never relocated or pruned by a verify run.
|
||||
assert.equal(fs.existsSync(orphanPath), true, 'verifier must not mutate gsd-pristine/');
|
||||
});
|
||||
|
||||
/**
|
||||
* Drift-path lock: a hash-MISMATCHING canonical snapshot still reports
|
||||
* OK_PRISTINE_DRIFT_DETECTED (#3657) — the recovery scan must not reach the
|
||||
* drift case.
|
||||
*/
|
||||
test('#4145: canonical-path drift still reports ok_pristine_drift_detected even when a hash-matching orphan exists', () => {
|
||||
resetFixture();
|
||||
const FILE = 'gsd-core/agents/gsd-executor.md';
|
||||
const oldPristine = 'old pristine line that was present when backup was captured\n';
|
||||
const newPristine = 'refreshed upstream line in the newer pristine snapshot\n';
|
||||
const userLine = 'user customisation line that should be preserved across updates';
|
||||
|
||||
writeBackupMeta({ [FILE]: sha256(oldPristine) });
|
||||
writeFile(path.join(patchesDir, FILE), oldPristine + userLine + '\n');
|
||||
writeFile(path.join(configDir, FILE), newPristine + userLine + '\n');
|
||||
writeFile(path.join(pristineDir, FILE), newPristine); // canonical drifted
|
||||
// A hash-matching orphan exists elsewhere — drift must still win.
|
||||
writeFile(path.join(pristineDir, 'agents', 'gsd-executor.md'), oldPristine);
|
||||
|
||||
const { status, report } = runVerifier();
|
||||
|
||||
assert.equal(status, 0);
|
||||
const r0 = report.results[0];
|
||||
assert.equal(r0.reason, REASON.OK_PRISTINE_DRIFT_DETECTED,
|
||||
`expected the untouched #3657 drift posture; got ${r0.reason}`);
|
||||
assert.equal(report.drifted, 1);
|
||||
});
|
||||
|
||||
/** Module unit: deterministic sorted-first match, symlink skip, absent dir. */
|
||||
test('#4145: findPristineByHash returns the sorted-first match, skips symlinks, and null on an absent dir', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-unit-'));
|
||||
const symRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-sym-'));
|
||||
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4145-out-'));
|
||||
try {
|
||||
const contentA = 'identical bytes that two snapshots happen to share\n';
|
||||
const hashA = sha256(contentA);
|
||||
writeFile(path.join(root, 'zz-dir', 'late-match.md'), contentA);
|
||||
writeFile(path.join(root, 'aa.txt'), contentA);
|
||||
|
||||
assert.equal(findPristineByHash(root, hashA), 'aa.txt',
|
||||
'sorted-first match wins deterministically');
|
||||
assert.equal(findPristineByHash(root, hashA, 'aa.txt'), 'zz-dir/late-match.md',
|
||||
'skipRel is never returned');
|
||||
assert.equal(findPristineByHash(root, hashA, new Set(['aa.txt', 'zz-dir/late-match.md'])), null,
|
||||
'every member of a skip Set is excluded (canonical-path protection)');
|
||||
assert.equal(findPristineByHash(root, sha256('no such content anywhere here\n')), null,
|
||||
'no match resolves to null');
|
||||
assert.equal(findPristineByHash(path.join(root, 'absent'), hashA), null,
|
||||
'absent dir resolves to null');
|
||||
|
||||
// A symlink is never followed, even when its target would hash-match.
|
||||
// The target lives OUTSIDE symRoot so the only hashable entry inside the
|
||||
// scanned tree is the symlink itself.
|
||||
const outsideTarget = path.join(outsideRoot, 'outside-target.md');
|
||||
fs.writeFileSync(outsideTarget, contentA);
|
||||
fs.symlinkSync(outsideTarget, path.join(symRoot, 'sym.md'));
|
||||
assert.equal(findPristineByHash(symRoot, hashA), null,
|
||||
'symlinked candidates are skipped, not followed');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
cleanup(symRoot);
|
||||
cleanup(outsideRoot);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user