From 14b5c2b12cbd79b8d5e0e46aa4ddcb4d36064120 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 00:43:19 +0200 Subject: [PATCH] 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 --- .../sf/tests/scaffold-drift.test.ts | 2 +- .../sf/tests/scaffold-versioning.test.ts | 231 +++++++++++++++++- 2 files changed, 230 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/sf/tests/scaffold-drift.test.ts b/src/resources/extensions/sf/tests/scaffold-drift.test.ts index 3615d483f..987546d32 100644 --- a/src/resources/extensions/sf/tests/scaffold-drift.test.ts +++ b/src/resources/extensions/sf/tests/scaffold-drift.test.ts @@ -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); diff --git a/src/resources/extensions/sf/tests/scaffold-versioning.test.ts b/src/resources/extensions/sf/tests/scaffold-versioning.test.ts index 6b0826371..a237ce0bf 100644 --- a/src/resources/extensions/sf/tests/scaffold-versioning.test.ts +++ b/src/resources/extensions/sf/tests/scaffold-versioning.test.ts @@ -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", + ); + }); +});