test(gsd): gap-fill tests for single-writer engine v2 modules

62 new tests across 6 files covering the modules introduced in the v2
single-writer discipline layer that had no test coverage:

- write-intercept.test.ts (15): isBlockedStateFile path matching for
  STATE.md (blocked) vs other .gsd/ files (allowed), BLOCKED_WRITE_ERROR
- sync-lock.test.ts (7): acquireSyncLock/releaseSyncLock including
  lock file creation, round-trip, and stale lock override
- workflow-events.test.ts (15): appendEvent (creates dir, valid JSONL,
  deterministic hash), readEvents (empty, parse, skip corrupted),
  findForkPoint (edge cases), compactMilestoneEvents (archive/truncate)
- workflow-manifest.test.ts (8): snapshotState, writeManifest,
  readManifest (null/parse/version guard), bootstrapFromManifest
  round-trip restore
- workflow-projections.test.ts (17): renderPlanContent pure function —
  H1/Goal/Demo/Tasks structure, [x]/[ ] checkboxes, Estimate/Files/
  Verify/Duration sublines, task ordering
- post-mutation-hook.test.ts (5): regression — verifies that after
  handleCompleteTask, event-log.jsonl and state-manifest.json are
  both written by the post-mutation hook; also confirms hook failures
  are non-fatal (handler still returns success)

All 62 tests pass. Zero regressions introduced.
This commit is contained in:
Jeremy McSpadden 2026-03-24 23:46:36 -05:00 committed by Lex Christopherson
parent 1c0cca4f76
commit eab3851a56
6 changed files with 931 additions and 0 deletions

View file

@ -0,0 +1,171 @@
// GSD Extension — post-mutation hook regression tests
// Verifies that after a successful handleCompleteTask call, the post-mutation
// hook fires: event-log.jsonl and state-manifest.json are both written.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { openDatabase, closeDatabase } from '../gsd-db.ts';
import { handleCompleteTask } from '../tools/complete-task.ts';
import { readEvents } from '../workflow-events.ts';
import { readManifest } from '../workflow-manifest.ts';
function tempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-post-hook-'));
}
function cleanupDir(dirPath: string): void {
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
}
/** Create a minimal project directory with a PLAN.md for complete-task to find. */
function createProject(basePath: string): void {
const sliceDir = path.join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01');
const tasksDir = path.join(sliceDir, 'tasks');
fs.mkdirSync(tasksDir, { recursive: true });
fs.writeFileSync(path.join(sliceDir, 'S01-PLAN.md'), `# S01: Test Slice
## Tasks
- [ ] **T01: Test task** \`est:30m\`
- Do: Implement the thing
- Verify: Run tests
- [ ] **T02: Second task** \`est:1h\`
- Do: Implement more
- Verify: Run more tests
`);
}
function makeCompleteTaskParams() {
return {
taskId: 'T01',
sliceId: 'S01',
milestoneId: 'M001',
oneLiner: 'Implemented auth middleware',
narrative: 'Added JWT validation middleware with proper error handling.',
verification: 'Ran npm test — all tests pass.',
deviations: 'None.',
knownIssues: 'None.',
keyFiles: ['src/middleware/auth.ts'],
keyDecisions: [],
blockerDiscovered: false,
verificationEvidence: [
{ command: 'npm test', exitCode: 0, verdict: '✅ pass', durationMs: 2500 },
],
};
}
// ─── Post-mutation hook: event log ───────────────────────────────────────
test('post-mutation-hook: event-log.jsonl exists after handleCompleteTask', async () => {
const base = tempDir();
const dbPath = path.join(base, 'test.db');
openDatabase(dbPath);
createProject(base);
try {
const result = await handleCompleteTask(makeCompleteTaskParams(), base);
assert.ok(!('error' in result), `handler should succeed, got: ${JSON.stringify(result)}`);
const logPath = path.join(base, '.gsd', 'event-log.jsonl');
assert.ok(fs.existsSync(logPath), 'event-log.jsonl should exist after handler completes');
} finally {
closeDatabase();
cleanupDir(base);
}
});
test('post-mutation-hook: event log contains complete-task event with correct params', async () => {
const base = tempDir();
const dbPath = path.join(base, 'test.db');
openDatabase(dbPath);
createProject(base);
try {
await handleCompleteTask(makeCompleteTaskParams(), base);
const logPath = path.join(base, '.gsd', 'event-log.jsonl');
const events = readEvents(logPath);
assert.ok(events.length > 0, 'event log should have at least one event');
const ev = events.find((e) => e.cmd === 'complete-task');
assert.ok(ev !== undefined, 'should have a complete-task event');
assert.strictEqual((ev!.params as { milestoneId?: string }).milestoneId, 'M001');
assert.strictEqual((ev!.params as { sliceId?: string }).sliceId, 'S01');
assert.strictEqual((ev!.params as { taskId?: string }).taskId, 'T01');
assert.strictEqual(ev!.actor, 'agent');
} finally {
closeDatabase();
cleanupDir(base);
}
});
// ─── Post-mutation hook: manifest ────────────────────────────────────────
test('post-mutation-hook: state-manifest.json exists after handleCompleteTask', async () => {
const base = tempDir();
const dbPath = path.join(base, 'test.db');
openDatabase(dbPath);
createProject(base);
try {
const result = await handleCompleteTask(makeCompleteTaskParams(), base);
assert.ok(!('error' in result), `handler should succeed, got: ${JSON.stringify(result)}`);
const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist after handler completes');
} finally {
closeDatabase();
cleanupDir(base);
}
});
test('post-mutation-hook: manifest has version 1 and includes completed task', async () => {
const base = tempDir();
const dbPath = path.join(base, 'test.db');
openDatabase(dbPath);
createProject(base);
try {
await handleCompleteTask(makeCompleteTaskParams(), base);
const manifest = readManifest(base);
assert.ok(manifest !== null, 'manifest should be readable');
assert.strictEqual(manifest!.version, 1);
const task = manifest!.tasks.find((t) => t.id === 'T01');
assert.ok(task !== undefined, 'T01 should appear in manifest');
assert.strictEqual(task!.status, 'complete');
assert.strictEqual(task!.milestone_id, 'M001');
assert.strictEqual(task!.slice_id, 'S01');
} finally {
closeDatabase();
cleanupDir(base);
}
});
// ─── Post-mutation hook: non-fatal on hook failure ───────────────────────
test('post-mutation-hook: handler still returns success even if projections dir is missing', async () => {
// basePath with NO .gsd directory — projections will fail to find milestones
// but handler should still return a result (not throw)
const base = tempDir();
const dbPath = path.join(base, 'test.db');
openDatabase(dbPath);
// Create tasks dir but NO plan file (projections will soft-fail)
const tasksDir = path.join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks');
fs.mkdirSync(tasksDir, { recursive: true });
try {
const result = await handleCompleteTask(makeCompleteTaskParams(), base);
// Handler should succeed (post-hook failures are non-fatal)
assert.ok(!('error' in result), `handler should not propagate hook errors, got: ${JSON.stringify(result)}`);
} finally {
closeDatabase();
cleanupDir(base);
}
});

View file

@ -0,0 +1,122 @@
// GSD Extension — sync-lock unit tests
// Tests acquireSyncLock() and releaseSyncLock().
import test from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { acquireSyncLock, releaseSyncLock } from '../sync-lock.ts';
function tempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-sync-lock-'));
}
function cleanupDir(dirPath: string): void {
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
}
// ─── acquireSyncLock ─────────────────────────────────────────────────────
test('sync-lock: acquireSyncLock returns { acquired: true } when no lock exists', () => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
try {
const result = acquireSyncLock(base);
assert.strictEqual(result.acquired, true);
} finally {
cleanupDir(base);
}
});
test('sync-lock: acquireSyncLock creates lock file at .gsd/sync.lock', () => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
try {
acquireSyncLock(base);
const lockPath = path.join(base, '.gsd', 'sync.lock');
assert.ok(fs.existsSync(lockPath), 'sync.lock should exist after acquire');
} finally {
cleanupDir(base);
}
});
test('sync-lock: lock file contains pid and acquired_at fields', () => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
try {
acquireSyncLock(base);
const lockPath = path.join(base, '.gsd', 'sync.lock');
const content = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
assert.strictEqual(typeof content.pid, 'number');
assert.strictEqual(typeof content.acquired_at, 'string');
} finally {
cleanupDir(base);
}
});
// ─── releaseSyncLock ─────────────────────────────────────────────────────
test('sync-lock: releaseSyncLock removes lock file', () => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
try {
acquireSyncLock(base);
const lockPath = path.join(base, '.gsd', 'sync.lock');
assert.ok(fs.existsSync(lockPath), 'lock file should exist before release');
releaseSyncLock(base);
assert.ok(!fs.existsSync(lockPath), 'lock file should not exist after release');
} finally {
cleanupDir(base);
}
});
test('sync-lock: releaseSyncLock is a no-op when no lock file exists', () => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
try {
// Should not throw
releaseSyncLock(base);
} finally {
cleanupDir(base);
}
});
// ─── acquire → release → re-acquire round-trip ───────────────────────────
test('sync-lock: can re-acquire after release', () => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
try {
const r1 = acquireSyncLock(base);
assert.strictEqual(r1.acquired, true, 'first acquire should succeed');
releaseSyncLock(base);
const r2 = acquireSyncLock(base);
assert.strictEqual(r2.acquired, true, 're-acquire after release should succeed');
releaseSyncLock(base);
} finally {
cleanupDir(base);
}
});
// ─── stale lock override ─────────────────────────────────────────────────
test('sync-lock: overrides stale lock file (mtime backdated)', (t) => {
const base = tempDir();
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
const lockPath = path.join(base, '.gsd', 'sync.lock');
try {
// Write a lock file with a very old mtime (simulating staleness)
fs.writeFileSync(lockPath, JSON.stringify({ pid: 99999, acquired_at: new Date(0).toISOString() }));
// Backdate mtime by 2 minutes
const staleTime = new Date(Date.now() - 120_000);
fs.utimesSync(lockPath, staleTime, staleTime);
// Should override stale lock and acquire
const result = acquireSyncLock(base, 500);
assert.strictEqual(result.acquired, true, 'should acquire over stale lock');
releaseSyncLock(base);
} finally {
cleanupDir(base);
}
});

View file

@ -0,0 +1,205 @@
// GSD Extension — workflow-events unit tests
// Tests appendEvent, readEvents, findForkPoint, compactMilestoneEvents.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import {
appendEvent,
readEvents,
findForkPoint,
compactMilestoneEvents,
type WorkflowEvent,
} from '../workflow-events.ts';
function tempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-events-'));
}
function cleanupDir(dirPath: string): void {
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
}
function makeEvent(cmd: string, params: Record<string, unknown> = {}): Omit<WorkflowEvent, 'hash'> {
return { cmd, params, ts: new Date().toISOString(), actor: 'agent' };
}
// ─── appendEvent ─────────────────────────────────────────────────────────
test('workflow-events: appendEvent creates .gsd dir and event-log.jsonl', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
assert.ok(fs.existsSync(path.join(base, '.gsd', 'event-log.jsonl')));
} finally {
cleanupDir(base);
}
});
test('workflow-events: appendEvent writes valid JSON line', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
const content = fs.readFileSync(path.join(base, '.gsd', 'event-log.jsonl'), 'utf-8');
const lines = content.trim().split('\n');
assert.strictEqual(lines.length, 1);
const parsed = JSON.parse(lines[0]!) as WorkflowEvent;
assert.strictEqual(parsed.cmd, 'complete-task');
assert.strictEqual(parsed.actor, 'agent');
assert.strictEqual(typeof parsed.hash, 'string');
assert.strictEqual(parsed.hash.length, 16);
} finally {
cleanupDir(base);
}
});
test('workflow-events: appendEvent appends multiple events', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { taskId: 'T01' }));
appendEvent(base, makeEvent('complete-slice', { sliceId: 'S01' }));
const events = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
assert.strictEqual(events.length, 2);
assert.strictEqual(events[0]!.cmd, 'complete-task');
assert.strictEqual(events[1]!.cmd, 'complete-slice');
} finally {
cleanupDir(base);
}
});
test('workflow-events: same cmd+params → same hash (deterministic)', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('plan-task', { milestoneId: 'M001', sliceId: 'S01' }));
appendEvent(base, makeEvent('plan-task', { milestoneId: 'M001', sliceId: 'S01' }));
const events = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
assert.strictEqual(events[0]!.hash, events[1]!.hash, 'identical cmd+params produce identical hash');
} finally {
cleanupDir(base);
}
});
test('workflow-events: different params → different hash', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { taskId: 'T01' }));
appendEvent(base, makeEvent('complete-task', { taskId: 'T02' }));
const events = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
assert.notStrictEqual(events[0]!.hash, events[1]!.hash, 'different params produce different hash');
} finally {
cleanupDir(base);
}
});
// ─── readEvents ──────────────────────────────────────────────────────────
test('workflow-events: readEvents returns [] for non-existent file', () => {
const result = readEvents('/nonexistent/path/event-log.jsonl');
assert.deepStrictEqual(result, []);
});
test('workflow-events: readEvents skips corrupted lines', () => {
const base = tempDir();
try {
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
const logPath = path.join(base, '.gsd', 'event-log.jsonl');
// Write a valid line, a corrupted line, and another valid line
fs.writeFileSync(logPath,
'{"cmd":"complete-task","params":{},"ts":"2026-01-01T00:00:00Z","hash":"abcd1234abcd1234","actor":"agent"}\n' +
'NOT VALID JSON {{{{\n' +
'{"cmd":"plan-task","params":{},"ts":"2026-01-01T00:00:01Z","hash":"1234abcd1234abcd","actor":"system"}\n',
);
const events = readEvents(logPath);
assert.strictEqual(events.length, 2, 'should return 2 valid events, skipping the corrupted line');
assert.strictEqual(events[0]!.cmd, 'complete-task');
assert.strictEqual(events[1]!.cmd, 'plan-task');
} finally {
cleanupDir(base);
}
});
// ─── findForkPoint ───────────────────────────────────────────────────────
test('workflow-events: findForkPoint returns -1 for two empty logs', () => {
assert.strictEqual(findForkPoint([], []), -1);
});
test('workflow-events: findForkPoint returns -1 when first events differ', () => {
const e1 = { cmd: 'a', params: {}, ts: '', hash: 'hash1', actor: 'agent' } as WorkflowEvent;
const e2 = { cmd: 'b', params: {}, ts: '', hash: 'hash2', actor: 'agent' } as WorkflowEvent;
assert.strictEqual(findForkPoint([e1], [e2]), -1);
});
test('workflow-events: findForkPoint returns 0 when only first event is common', () => {
const common = { cmd: 'a', params: {}, ts: '', hash: 'hash1', actor: 'agent' } as WorkflowEvent;
const eA = { cmd: 'b', params: {}, ts: '', hash: 'hash2', actor: 'agent' } as WorkflowEvent;
const eB = { cmd: 'c', params: {}, ts: '', hash: 'hash3', actor: 'agent' } as WorkflowEvent;
// logA: [common, eA], logB: [common, eB]
assert.strictEqual(findForkPoint([common, eA], [common, eB]), 0);
});
test('workflow-events: findForkPoint returns last common index for prefix relationship', () => {
const e1 = { cmd: 'a', params: {}, ts: '', hash: 'h1', actor: 'agent' } as WorkflowEvent;
const e2 = { cmd: 'b', params: {}, ts: '', hash: 'h2', actor: 'agent' } as WorkflowEvent;
const e3 = { cmd: 'c', params: {}, ts: '', hash: 'h3', actor: 'agent' } as WorkflowEvent;
// logA is a prefix of logB → fork point is last index of logA
assert.strictEqual(findForkPoint([e1, e2], [e1, e2, e3]), 1);
});
test('workflow-events: findForkPoint handles equal logs', () => {
const e1 = { cmd: 'a', params: {}, ts: '', hash: 'h1', actor: 'agent' } as WorkflowEvent;
const e2 = { cmd: 'b', params: {}, ts: '', hash: 'h2', actor: 'agent' } as WorkflowEvent;
assert.strictEqual(findForkPoint([e1, e2], [e1, e2]), 1);
});
// ─── compactMilestoneEvents ──────────────────────────────────────────────
test('workflow-events: compactMilestoneEvents returns { archived: 0 } when no matching events', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M002', taskId: 'T01' }));
const result = compactMilestoneEvents(base, 'M001');
assert.strictEqual(result.archived, 0);
} finally {
cleanupDir(base);
}
});
test('workflow-events: compactMilestoneEvents archives milestone events', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T02' }));
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M002', taskId: 'T03' }));
const result = compactMilestoneEvents(base, 'M001');
assert.strictEqual(result.archived, 2, 'should archive 2 M001 events');
// Archive file should exist
const archivePath = path.join(base, '.gsd', 'event-log-M001.jsonl.archived');
assert.ok(fs.existsSync(archivePath), 'archive file should exist');
const archived = readEvents(archivePath);
assert.strictEqual(archived.length, 2, 'archive file should have 2 events');
// Active log should retain only M002 event
const active = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
assert.strictEqual(active.length, 1, 'active log should have 1 remaining event');
assert.strictEqual((active[0]!.params as { milestoneId?: string }).milestoneId, 'M002');
} finally {
cleanupDir(base);
}
});
test('workflow-events: compactMilestoneEvents empties active log when all events are from milestone', () => {
const base = tempDir();
try {
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
compactMilestoneEvents(base, 'M001');
const active = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
assert.strictEqual(active.length, 0, 'active log should be empty after full compact');
} finally {
cleanupDir(base);
}
});

View file

@ -0,0 +1,186 @@
// GSD Extension — workflow-manifest unit tests
// Tests writeManifest, readManifest, snapshotState, bootstrapFromManifest.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import {
openDatabase,
closeDatabase,
insertMilestone,
insertSlice,
insertTask,
} from '../gsd-db.ts';
import {
writeManifest,
readManifest,
snapshotState,
bootstrapFromManifest,
} from '../workflow-manifest.ts';
function tempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-manifest-'));
}
function tempDbPath(base: string): string {
return path.join(base, 'test.db');
}
function cleanupDir(dirPath: string): void {
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
}
// ─── readManifest: no file ────────────────────────────────────────────────
test('workflow-manifest: readManifest returns null when file does not exist', () => {
const base = tempDir();
try {
const result = readManifest(base);
assert.strictEqual(result, null);
} finally {
cleanupDir(base);
}
});
// ─── writeManifest + readManifest round-trip ─────────────────────────────
test('workflow-manifest: writeManifest creates state-manifest.json with version 1', () => {
const base = tempDir();
openDatabase(tempDbPath(base));
try {
writeManifest(base);
const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist');
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
assert.strictEqual(raw.version, 1);
} finally {
closeDatabase();
cleanupDir(base);
}
});
test('workflow-manifest: readManifest parses manifest written by writeManifest', () => {
const base = tempDir();
openDatabase(tempDbPath(base));
try {
writeManifest(base);
const manifest = readManifest(base);
assert.ok(manifest !== null);
assert.strictEqual(manifest!.version, 1);
assert.ok(typeof manifest!.exported_at === 'string');
assert.ok(Array.isArray(manifest!.milestones));
assert.ok(Array.isArray(manifest!.slices));
assert.ok(Array.isArray(manifest!.tasks));
assert.ok(Array.isArray(manifest!.decisions));
assert.ok(Array.isArray(manifest!.verification_evidence));
} finally {
closeDatabase();
cleanupDir(base);
}
});
// ─── snapshotState: captures DB rows ─────────────────────────────────────
test('workflow-manifest: snapshotState includes inserted milestone', () => {
const base = tempDir();
openDatabase(tempDbPath(base));
try {
insertMilestone({ id: 'M001', title: 'Auth Milestone' });
const snap = snapshotState();
assert.strictEqual(snap.version, 1);
const m = snap.milestones.find((r) => r.id === 'M001');
assert.ok(m !== undefined, 'M001 should appear in snapshot');
assert.strictEqual(m!.title, 'Auth Milestone');
} finally {
closeDatabase();
cleanupDir(base);
}
});
test('workflow-manifest: snapshotState captures tasks', () => {
const base = tempDir();
openDatabase(tempDbPath(base));
try {
insertMilestone({ id: 'M001' });
insertSlice({ id: 'S01', milestoneId: 'M001' });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Do thing', status: 'complete' });
const snap = snapshotState();
const t = snap.tasks.find((r) => r.id === 'T01');
assert.ok(t !== undefined, 'T01 should appear in snapshot');
assert.strictEqual(t!.status, 'complete');
} finally {
closeDatabase();
cleanupDir(base);
}
});
// ─── bootstrapFromManifest ────────────────────────────────────────────────
test('workflow-manifest: bootstrapFromManifest returns false when no manifest file', () => {
const base = tempDir();
openDatabase(tempDbPath(base));
try {
const result = bootstrapFromManifest(base);
assert.strictEqual(result, false);
} finally {
closeDatabase();
cleanupDir(base);
}
});
test('workflow-manifest: bootstrapFromManifest restores DB from manifest (round-trip)', () => {
const base = tempDir();
openDatabase(tempDbPath(base));
try {
// Insert data and write manifest
insertMilestone({ id: 'M001', title: 'Restored Milestone' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Restored Slice' });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Restored Task', status: 'complete' });
writeManifest(base);
closeDatabase();
// Open a fresh DB and bootstrap from manifest
const newDbPath = path.join(base, 'new.db');
openDatabase(newDbPath);
const result = bootstrapFromManifest(base);
assert.strictEqual(result, true, 'bootstrapFromManifest should return true');
// Verify restored state
const snap = snapshotState();
const m = snap.milestones.find((r) => r.id === 'M001');
assert.ok(m !== undefined, 'M001 should be restored');
assert.strictEqual(m!.title, 'Restored Milestone');
const s = snap.slices.find((r) => r.id === 'S01');
assert.ok(s !== undefined, 'S01 should be restored');
const t = snap.tasks.find((r) => r.id === 'T01');
assert.ok(t !== undefined, 'T01 should be restored');
assert.strictEqual(t!.status, 'complete');
} finally {
closeDatabase();
cleanupDir(base);
}
});
// ─── readManifest: version check ─────────────────────────────────────────
test('workflow-manifest: readManifest throws on unsupported version', () => {
const base = tempDir();
try {
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
fs.writeFileSync(
path.join(base, '.gsd', 'state-manifest.json'),
JSON.stringify({ version: 99, exported_at: '', milestones: [], slices: [], tasks: [], decisions: [], verification_evidence: [] }),
);
assert.throws(
() => readManifest(base),
/Unsupported manifest version/,
'should throw on version mismatch',
);
} finally {
cleanupDir(base);
}
});

View file

@ -0,0 +1,170 @@
// GSD Extension — workflow-projections unit tests
// Tests the pure rendering functions (no DB required).
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderPlanContent } from '../workflow-projections.ts';
import type { SliceRow, TaskRow } from '../gsd-db.ts';
// ─── Test fixtures ────────────────────────────────────────────────────────
function makeSlice(overrides: Partial<SliceRow> = {}): SliceRow {
return {
id: 'S01',
milestone_id: 'M001',
title: 'Auth Layer',
status: 'active',
risk: 'high',
depends: [],
demo: 'Login flow works end-to-end',
goal: 'Implement JWT authentication',
full_summary_md: '',
full_uat_md: '',
success_criteria: '',
proof_level: '',
integration_closure: '',
observability_impact: '',
created_at: '2026-01-01T00:00:00Z',
completed_at: null,
sequence: 1,
replan_triggered_at: null,
...overrides,
};
}
function makeTask(overrides: Partial<TaskRow> = {}): TaskRow {
return {
id: 'T01',
slice_id: 'S01',
milestone_id: 'M001',
title: 'Create JWT middleware',
status: 'pending',
description: 'Implement JWT validation middleware',
estimate: '2h',
files: ['src/middleware/auth.ts'],
verify: 'npm test src/middleware/auth.test.ts',
one_liner: '',
narrative: '',
verification_result: '',
duration: '',
completed_at: null,
blocker_discovered: false,
deviations: '',
known_issues: '',
key_files: [],
key_decisions: [],
full_summary_md: '',
inputs: [],
expected_output: [],
observability_impact: '',
sequence: 1,
...overrides,
};
}
// ─── renderPlanContent: structure ────────────────────────────────────────
test('workflow-projections: renderPlanContent starts with H1 containing slice id and title', () => {
const content = renderPlanContent(makeSlice(), []);
assert.ok(content.startsWith('# S01: Auth Layer'), `expected H1, got: ${content.slice(0, 60)}`);
});
test('workflow-projections: renderPlanContent includes Goal line', () => {
const content = renderPlanContent(makeSlice(), []);
assert.ok(content.includes('**Goal:** Implement JWT authentication'));
});
test('workflow-projections: renderPlanContent includes Demo line', () => {
const content = renderPlanContent(makeSlice(), []);
assert.ok(content.includes('**Demo:** After this: Login flow works end-to-end'));
});
test('workflow-projections: renderPlanContent falls back to TBD when goal and full_summary_md are empty', () => {
const slice = makeSlice({ goal: '', full_summary_md: '' });
const content = renderPlanContent(slice, []);
assert.ok(content.includes('**Goal:** TBD'));
});
test('workflow-projections: renderPlanContent falls back to full_summary_md when goal is empty', () => {
const slice = makeSlice({ goal: '', full_summary_md: 'Fallback goal text' });
const content = renderPlanContent(slice, []);
assert.ok(content.includes('**Goal:** Fallback goal text'));
});
test('workflow-projections: renderPlanContent includes ## Tasks section', () => {
const content = renderPlanContent(makeSlice(), []);
assert.ok(content.includes('## Tasks'));
});
// ─── renderPlanContent: task checkboxes ──────────────────────────────────
test('workflow-projections: pending task renders with [ ] checkbox', () => {
const task = makeTask({ status: 'pending' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes('- [ ] **T01:**'), `expected unchecked, got: ${content}`);
});
test('workflow-projections: done task renders with [x] checkbox', () => {
const task = makeTask({ status: 'done' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes('- [x] **T01:**'), `expected checked, got: ${content}`);
});
test('workflow-projections: non-done status renders with [ ] checkbox', () => {
const task = makeTask({ status: 'complete' }); // 'complete' ≠ 'done' → unchecked
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes('- [ ] **T01:**'));
});
// ─── renderPlanContent: task sublines ────────────────────────────────────
test('workflow-projections: task with estimate renders Estimate subline', () => {
const task = makeTask({ estimate: '2h' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes(' - Estimate: 2h'));
});
test('workflow-projections: task with empty estimate omits Estimate subline', () => {
const task = makeTask({ estimate: '' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(!content.includes(' - Estimate:'));
});
test('workflow-projections: task with files renders Files subline', () => {
const task = makeTask({ files: ['src/auth.ts', 'src/auth.test.ts'] });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes(' - Files: src/auth.ts, src/auth.test.ts'));
});
test('workflow-projections: task with empty files array omits Files subline', () => {
const task = makeTask({ files: [] });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(!content.includes(' - Files:'));
});
test('workflow-projections: task with verify renders Verify subline', () => {
const task = makeTask({ verify: 'npm test' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes(' - Verify: npm test'));
});
test('workflow-projections: task with no verify omits Verify subline', () => {
const task = makeTask({ verify: '' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(!content.includes(' - Verify:'));
});
test('workflow-projections: task with duration renders Duration subline', () => {
const task = makeTask({ duration: '45m' });
const content = renderPlanContent(makeSlice(), [task]);
assert.ok(content.includes(' - Duration: 45m'));
});
test('workflow-projections: multiple tasks rendered in order', () => {
const t1 = makeTask({ id: 'T01', title: 'First task', sequence: 1 });
const t2 = makeTask({ id: 'T02', title: 'Second task', sequence: 2 });
const content = renderPlanContent(makeSlice(), [t1, t2]);
const idxT1 = content.indexOf('**T01:**');
const idxT2 = content.indexOf('**T02:**');
assert.ok(idxT1 < idxT2, 'T01 should appear before T02');
});

View file

@ -0,0 +1,77 @@
// GSD Extension — write-intercept unit tests
// Tests isBlockedStateFile() and BLOCKED_WRITE_ERROR constant.
import test from 'node:test';
import assert from 'node:assert/strict';
import { isBlockedStateFile, BLOCKED_WRITE_ERROR } from '../write-intercept.ts';
// ─── isBlockedStateFile: blocked paths ───────────────────────────────────
test('write-intercept: blocks unix .gsd/STATE.md path', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/STATE.md'), true);
});
test('write-intercept: blocks relative path with dir prefix before .gsd/STATE.md', () => {
// The regex requires a path separator before .gsd — bare '.gsd/STATE.md' is not blocked
// but 'project/.gsd/STATE.md' is (has separator before .gsd)
assert.strictEqual(isBlockedStateFile('project/.gsd/STATE.md'), true);
});
test('write-intercept: does NOT block bare .gsd/STATE.md without leading separator', () => {
// Regex requires [/\\] before .gsd — bare relative path has no such separator
assert.strictEqual(isBlockedStateFile('.gsd/STATE.md'), false);
});
test('write-intercept: blocks nested project .gsd/STATE.md path', () => {
assert.strictEqual(isBlockedStateFile('/Users/dev/my-project/.gsd/STATE.md'), true);
});
test('write-intercept: blocks .gsd/projects/<name>/STATE.md (symlinked projects path)', () => {
assert.strictEqual(isBlockedStateFile('/home/user/.gsd/projects/my-project/STATE.md'), true);
});
// ─── isBlockedStateFile: allowed paths ───────────────────────────────────
test('write-intercept: allows .gsd/ROADMAP.md', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/ROADMAP.md'), false);
});
test('write-intercept: allows .gsd/PLAN.md', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/PLAN.md'), false);
});
test('write-intercept: allows .gsd/REQUIREMENTS.md', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/REQUIREMENTS.md'), false);
});
test('write-intercept: allows .gsd/SUMMARY.md', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/SUMMARY.md'), false);
});
test('write-intercept: allows .gsd/PROJECT.md', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/PROJECT.md'), false);
});
test('write-intercept: allows regular source files', () => {
assert.strictEqual(isBlockedStateFile('/project/src/index.ts'), false);
});
test('write-intercept: allows slice plan files', () => {
assert.strictEqual(isBlockedStateFile('/project/.gsd/milestones/M001/slices/S01/S01-PLAN.md'), false);
});
test('write-intercept: does not block files named STATE.md outside .gsd/', () => {
assert.strictEqual(isBlockedStateFile('/project/docs/STATE.md'), false);
});
// ─── BLOCKED_WRITE_ERROR: content ────────────────────────────────────────
test('write-intercept: BLOCKED_WRITE_ERROR is a non-empty string', () => {
assert.strictEqual(typeof BLOCKED_WRITE_ERROR, 'string');
assert.ok(BLOCKED_WRITE_ERROR.length > 0);
});
test('write-intercept: BLOCKED_WRITE_ERROR mentions engine tool calls', () => {
assert.ok(BLOCKED_WRITE_ERROR.includes('gsd_complete_task'), 'should mention gsd_complete_task');
assert.ok(BLOCKED_WRITE_ERROR.includes('engine tool calls'), 'should mention engine tool calls');
});