test: add Phase C coverage for drift-aware ensureAgenticDocsScaffold
Phase C (automatic silent sync) had no dedicated tests when committed. Added 8 cases covering: - ensureAgenticDocsScaffold on empty dir creates files with markers - old-version pending marker silently re-renders to current - editing-drift file left untouched - legacy unmarked file matched against archive promoted to pending - migrateLegacyScaffold idempotency Total scaffold test count: 41 (was 33). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d01b2f0b7f
commit
14b5c2b12c
2 changed files with 230 additions and 3 deletions
|
|
@ -173,7 +173,7 @@ describe("detectScaffoldDrift", () => {
|
|||
});
|
||||
|
||||
describe("migrateLegacyScaffold", () => {
|
||||
test("returns empty arrays in Phase B (no archive yet)", () => {
|
||||
test("on empty dir, both migrated and skipped are empty", () => {
|
||||
const dir = makeTmp();
|
||||
try {
|
||||
const result = migrateLegacyScaffold(dir);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
/**
|
||||
* Tests for scaffold-versioning (ADR-021 Phase A).
|
||||
* Tests for scaffold-versioning (ADR-021 Phase A + Phase C).
|
||||
*
|
||||
* Covers: marker parse/format round-trip, stamp file behavior (new and replace),
|
||||
* Phase A: marker parse/format round-trip, stamp file behavior (new and replace),
|
||||
* body hash determinism, manifest read/write/dedup.
|
||||
*
|
||||
* Phase C: drift-aware ensureAgenticDocsScaffold, migrateLegacyScaffold against
|
||||
* the seeded SCAFFOLD_VERSION_ARCHIVE, and the silent-path edge cases.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
|
|
@ -16,6 +20,14 @@ import { tmpdir } from "node:os";
|
|||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, test } from "node:test";
|
||||
|
||||
import {
|
||||
ensureAgenticDocsScaffold,
|
||||
SCAFFOLD_FILES,
|
||||
} from "../agentic-docs-scaffold.ts";
|
||||
import {
|
||||
migrateLegacyScaffold,
|
||||
SCAFFOLD_VERSION_ARCHIVE,
|
||||
} from "../scaffold-drift.ts";
|
||||
import {
|
||||
bodyHash,
|
||||
extractMarker,
|
||||
|
|
@ -313,3 +325,218 @@ describe("scaffold manifest", () => {
|
|||
assert.equal(m.applied.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Phase C: drift-aware ensureAgenticDocsScaffold ──────────────────────
|
||||
|
||||
/** AGENTS.md template entry — used by Phase C tests. Asserted to exist. */
|
||||
function getAgentsMdTemplate() {
|
||||
const file = SCAFFOLD_FILES.find((f) => f.path === "AGENTS.md");
|
||||
if (!file) throw new Error("AGENTS.md missing from SCAFFOLD_FILES");
|
||||
return file;
|
||||
}
|
||||
|
||||
describe("Phase C: ensureAgenticDocsScaffold drift-aware sync", () => {
|
||||
let dir: string;
|
||||
let prevVersion: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = makeTmp();
|
||||
// Pin SF_VERSION so the archive seeding and stamp behaviour are
|
||||
// deterministic regardless of host environment.
|
||||
prevVersion = process.env.SF_VERSION;
|
||||
process.env.SF_VERSION = "9.9.9";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
if (prevVersion === undefined) delete process.env.SF_VERSION;
|
||||
else process.env.SF_VERSION = prevVersion;
|
||||
});
|
||||
|
||||
test("on empty dir, creates all SCAFFOLD_FILES with markers and manifest entries", () => {
|
||||
ensureAgenticDocsScaffold(dir);
|
||||
|
||||
// Every entry in SCAFFOLD_FILES exists on disk.
|
||||
for (const file of SCAFFOLD_FILES) {
|
||||
assert.ok(
|
||||
existsSync(join(dir, file.path)),
|
||||
`expected ${file.path} to exist`,
|
||||
);
|
||||
}
|
||||
|
||||
// Markdown templates have first-line markers stamped to current version.
|
||||
const agents = getAgentsMdTemplate();
|
||||
const { marker } = extractMarker(join(dir, agents.path));
|
||||
assert.ok(marker, "AGENTS.md should be stamped");
|
||||
assert.equal(marker?.version, "9.9.9");
|
||||
assert.equal(marker?.state, "pending");
|
||||
|
||||
// Manifest captures every (markered + non-markered) scaffold file.
|
||||
const manifest = readScaffoldManifest(dir);
|
||||
assert.equal(manifest.applied.length, SCAFFOLD_FILES.length);
|
||||
});
|
||||
|
||||
test("on dir with old-version pending marker, silently re-renders to current", () => {
|
||||
const agents = getAgentsMdTemplate();
|
||||
const target = join(dir, agents.path);
|
||||
|
||||
// Plant the file with current template body but stamped to an older version.
|
||||
writeFileSync(target, agents.content, "utf-8");
|
||||
stampScaffoldFile(target, agents.path, "0.1.0", "pending");
|
||||
|
||||
// Confirm pre-state.
|
||||
const before = extractMarker(target).marker;
|
||||
assert.equal(before?.version, "0.1.0");
|
||||
|
||||
ensureAgenticDocsScaffold(dir);
|
||||
|
||||
const after = extractMarker(target).marker;
|
||||
assert.equal(after?.version, "9.9.9", "marker should be restamped");
|
||||
assert.equal(after?.state, "pending");
|
||||
// Body should match the current template content.
|
||||
const { body } = extractMarker(target);
|
||||
assert.equal(body, agents.content);
|
||||
});
|
||||
|
||||
test("on dir with editing-drift file (marker + diverged body), leaves untouched", () => {
|
||||
const agents = getAgentsMdTemplate();
|
||||
const target = join(dir, agents.path);
|
||||
|
||||
// Stamp at old version then user edited the body so the hash diverges.
|
||||
writeFileSync(target, agents.content, "utf-8");
|
||||
stampScaffoldFile(target, agents.path, "0.1.0", "pending");
|
||||
// Re-write with edited body that no longer matches stamp's hash.
|
||||
const stamped = readFileSync(target, "utf-8");
|
||||
const newlineIdx = stamped.indexOf("\n");
|
||||
const markerLine = stamped.slice(0, newlineIdx);
|
||||
const userEditedBody = "USER WROTE THIS\n";
|
||||
writeFileSync(target, `${markerLine}\n${userEditedBody}`, "utf-8");
|
||||
|
||||
ensureAgenticDocsScaffold(dir);
|
||||
|
||||
const { marker, body } = extractMarker(target);
|
||||
// Marker version should NOT be bumped — Phase C must not touch
|
||||
// editing-drift files.
|
||||
assert.equal(marker?.version, "0.1.0");
|
||||
assert.equal(body, userEditedBody);
|
||||
});
|
||||
|
||||
test("on dir with marker-less verbatim-template file, promotes to pending and stamps", () => {
|
||||
const agents = getAgentsMdTemplate();
|
||||
const target = join(dir, agents.path);
|
||||
|
||||
// Plant the file with the **current** template body and no marker —
|
||||
// this simulates a project that pre-dates ADR-021 Phase A but is
|
||||
// still on the current SF release.
|
||||
writeFileSync(target, agents.content, "utf-8");
|
||||
assert.equal(extractMarker(target).marker, null);
|
||||
|
||||
ensureAgenticDocsScaffold(dir);
|
||||
|
||||
const { marker, body } = extractMarker(target);
|
||||
assert.ok(marker, "file should be stamped after legacy migration");
|
||||
assert.equal(marker?.template, agents.path);
|
||||
assert.equal(marker?.state, "pending");
|
||||
// Body unchanged — the migration path stamps without rewriting.
|
||||
assert.equal(body, agents.content);
|
||||
|
||||
// Manifest contains the file at the matched (current) version.
|
||||
const manifest = readScaffoldManifest(dir);
|
||||
const entry = manifest.applied.find((e) => e.path === agents.path);
|
||||
assert.ok(entry, "manifest should record the migrated file");
|
||||
assert.equal(entry?.version, "9.9.9");
|
||||
});
|
||||
|
||||
test("on dir with marker-less customized file (no archive match), leaves untouched", () => {
|
||||
const agents = getAgentsMdTemplate();
|
||||
const target = join(dir, agents.path);
|
||||
|
||||
const customised = "# My Custom AGENTS.md\n\nNothing like the template.\n";
|
||||
writeFileSync(target, customised, "utf-8");
|
||||
|
||||
ensureAgenticDocsScaffold(dir);
|
||||
|
||||
// File body unchanged, no marker stamped.
|
||||
assert.equal(readFileSync(target, "utf-8"), customised);
|
||||
assert.equal(extractMarker(target).marker, null);
|
||||
|
||||
// Manifest must not record it.
|
||||
const manifest = readScaffoldManifest(dir);
|
||||
const entry = manifest.applied.find((e) => e.path === agents.path);
|
||||
assert.equal(entry, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Phase C: migrateLegacyScaffold", () => {
|
||||
let dir: string;
|
||||
let prevVersion: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = makeTmp();
|
||||
prevVersion = process.env.SF_VERSION;
|
||||
process.env.SF_VERSION = "9.9.9";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
if (prevVersion === undefined) delete process.env.SF_VERSION;
|
||||
else process.env.SF_VERSION = prevVersion;
|
||||
});
|
||||
|
||||
test("returns matched + skipped lists correctly", () => {
|
||||
const agents = getAgentsMdTemplate();
|
||||
const arch = SCAFFOLD_FILES.find((f) => f.path === "ARCHITECTURE.md");
|
||||
assert.ok(arch);
|
||||
|
||||
// AGENTS.md: verbatim current template, no marker → should be migrated.
|
||||
writeFileSync(join(dir, agents.path), agents.content, "utf-8");
|
||||
|
||||
// ARCHITECTURE.md: customised, no marker → should be skipped.
|
||||
mkdirSync(join(dir, "."), { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, arch.path),
|
||||
"# Custom Architecture\n\nNot the template.\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = migrateLegacyScaffold(dir);
|
||||
|
||||
assert.ok(
|
||||
result.migrated.includes(agents.path),
|
||||
"AGENTS.md should be in migrated list",
|
||||
);
|
||||
assert.ok(
|
||||
result.skipped.includes(arch.path),
|
||||
"ARCHITECTURE.md should be in skipped list",
|
||||
);
|
||||
// Files not present on disk should appear in neither list.
|
||||
assert.ok(!result.migrated.includes("docs/RELIABILITY.md"));
|
||||
assert.ok(!result.skipped.includes("docs/RELIABILITY.md"));
|
||||
});
|
||||
|
||||
test("seeds SCAFFOLD_VERSION_ARCHIVE with current ship version on first call", () => {
|
||||
// Trigger seeding.
|
||||
migrateLegacyScaffold(dir);
|
||||
const agents = getAgentsMdTemplate();
|
||||
const archive = SCAFFOLD_VERSION_ARCHIVE[agents.path];
|
||||
assert.ok(archive, "archive should have entries for AGENTS.md");
|
||||
assert.ok(
|
||||
archive.some((e) => e.hash === bodyHash(agents.content)),
|
||||
"archive should contain the current AGENTS.md body hash",
|
||||
);
|
||||
});
|
||||
|
||||
test("idempotent — second invocation finds nothing to migrate", () => {
|
||||
const agents = getAgentsMdTemplate();
|
||||
writeFileSync(join(dir, agents.path), agents.content, "utf-8");
|
||||
|
||||
const first = migrateLegacyScaffold(dir);
|
||||
assert.ok(first.migrated.includes(agents.path));
|
||||
|
||||
const second = migrateLegacyScaffold(dir);
|
||||
assert.ok(
|
||||
!second.migrated.includes(agents.path),
|
||||
"second migration should not re-migrate already-stamped file",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue