gsd-pi 2.45.0-dev.fdcf73c → 2.46.0-dev.cc9d310
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/phases.js +14 -35
- package/dist/resources/extensions/gsd/auto/session.js +0 -11
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +112 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +25 -96
- package/dist/resources/extensions/gsd/auto-start.js +2 -3
- package/dist/resources/extensions/gsd/auto.js +8 -52
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +18 -0
- package/dist/resources/extensions/gsd/commands/context.js +0 -4
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +1 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +2 -4
- package/dist/resources/extensions/gsd/dashboard-overlay.js +0 -44
- package/dist/resources/extensions/gsd/doctor-checks.js +166 -1
- package/dist/resources/extensions/gsd/doctor.js +3 -1
- package/dist/resources/extensions/gsd/gsd-db.js +11 -2
- package/dist/resources/extensions/gsd/guided-flow.js +1 -2
- package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -18
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -15
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/session-lock.js +1 -3
- package/dist/resources/extensions/gsd/state.js +7 -0
- package/dist/resources/extensions/gsd/sync-lock.js +89 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +58 -12
- package/dist/resources/extensions/gsd/tools/complete-slice.js +56 -11
- package/dist/resources/extensions/gsd/tools/complete-task.js +50 -2
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +37 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +30 -1
- package/dist/resources/extensions/gsd/tools/plan-task.js +27 -1
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +32 -2
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +86 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +90 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +32 -2
- package/dist/resources/extensions/gsd/unit-ownership.js +85 -0
- package/dist/resources/extensions/gsd/workflow-events.js +102 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +56 -1
- package/dist/resources/extensions/gsd/workflow-manifest.js +244 -0
- package/dist/resources/extensions/gsd/workflow-migration.js +280 -0
- package/dist/resources/extensions/gsd/workflow-projections.js +373 -0
- package/dist/resources/extensions/gsd/workflow-reconcile.js +411 -0
- package/dist/resources/extensions/gsd/write-intercept.js +84 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -19
- package/src/resources/extensions/gsd/auto/phases.ts +11 -35
- package/src/resources/extensions/gsd/auto/session.ts +0 -18
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +131 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +0 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +25 -106
- package/src/resources/extensions/gsd/auto-start.ts +1 -3
- package/src/resources/extensions/gsd/auto.ts +4 -80
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/commands/context.ts +0 -5
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +1 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +1 -5
- package/src/resources/extensions/gsd/dashboard-overlay.ts +0 -50
- package/src/resources/extensions/gsd/doctor-checks.ts +179 -1
- package/src/resources/extensions/gsd/doctor-types.ts +7 -1
- package/src/resources/extensions/gsd/doctor.ts +4 -1
- package/src/resources/extensions/gsd/gsd-db.ts +11 -2
- package/src/resources/extensions/gsd/guided-flow.ts +1 -2
- package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +5 -21
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -15
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/session-lock.ts +0 -4
- package/src/resources/extensions/gsd/state.ts +8 -0
- package/src/resources/extensions/gsd/sync-lock.ts +94 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +5 -13
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +6 -10
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +264 -228
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +317 -250
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +2 -8
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration-proof.test.ts +15 -24
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +0 -7
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +20 -24
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +15 -14
- package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/reopen-task.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +1 -4
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/sync-lock.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/unit-ownership.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/workflow-events.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +76 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +70 -13
- package/src/resources/extensions/gsd/tools/complete-slice.ts +68 -11
- package/src/resources/extensions/gsd/tools/complete-task.ts +63 -1
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +45 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +38 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +35 -1
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +39 -1
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +125 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +129 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +38 -1
- package/src/resources/extensions/gsd/types.ts +8 -0
- package/src/resources/extensions/gsd/unit-ownership.ts +104 -0
- package/src/resources/extensions/gsd/workflow-events.ts +154 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +51 -1
- package/src/resources/extensions/gsd/workflow-manifest.ts +334 -0
- package/src/resources/extensions/gsd/workflow-migration.ts +345 -0
- package/src/resources/extensions/gsd/workflow-projections.ts +425 -0
- package/src/resources/extensions/gsd/workflow-reconcile.ts +503 -0
- package/src/resources/extensions/gsd/write-intercept.ts +90 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// GSD Extension — sync-lock unit tests
|
|
2
|
+
// Tests acquireSyncLock() and releaseSyncLock().
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import { acquireSyncLock, releaseSyncLock } from '../sync-lock.ts';
|
|
10
|
+
|
|
11
|
+
function tempDir(): string {
|
|
12
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-sync-lock-'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanupDir(dirPath: string): void {
|
|
16
|
+
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── acquireSyncLock ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
test('sync-lock: acquireSyncLock returns { acquired: true } when no lock exists', () => {
|
|
22
|
+
const base = tempDir();
|
|
23
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
24
|
+
try {
|
|
25
|
+
const result = acquireSyncLock(base);
|
|
26
|
+
assert.strictEqual(result.acquired, true);
|
|
27
|
+
} finally {
|
|
28
|
+
cleanupDir(base);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('sync-lock: acquireSyncLock creates lock file at .gsd/sync.lock', () => {
|
|
33
|
+
const base = tempDir();
|
|
34
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
35
|
+
try {
|
|
36
|
+
acquireSyncLock(base);
|
|
37
|
+
const lockPath = path.join(base, '.gsd', 'sync.lock');
|
|
38
|
+
assert.ok(fs.existsSync(lockPath), 'sync.lock should exist after acquire');
|
|
39
|
+
} finally {
|
|
40
|
+
cleanupDir(base);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('sync-lock: lock file contains pid and acquired_at fields', () => {
|
|
45
|
+
const base = tempDir();
|
|
46
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
47
|
+
try {
|
|
48
|
+
acquireSyncLock(base);
|
|
49
|
+
const lockPath = path.join(base, '.gsd', 'sync.lock');
|
|
50
|
+
const content = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
51
|
+
assert.strictEqual(typeof content.pid, 'number');
|
|
52
|
+
assert.strictEqual(typeof content.acquired_at, 'string');
|
|
53
|
+
} finally {
|
|
54
|
+
cleanupDir(base);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── releaseSyncLock ─────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
test('sync-lock: releaseSyncLock removes lock file', () => {
|
|
61
|
+
const base = tempDir();
|
|
62
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
63
|
+
try {
|
|
64
|
+
acquireSyncLock(base);
|
|
65
|
+
const lockPath = path.join(base, '.gsd', 'sync.lock');
|
|
66
|
+
assert.ok(fs.existsSync(lockPath), 'lock file should exist before release');
|
|
67
|
+
releaseSyncLock(base);
|
|
68
|
+
assert.ok(!fs.existsSync(lockPath), 'lock file should not exist after release');
|
|
69
|
+
} finally {
|
|
70
|
+
cleanupDir(base);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('sync-lock: releaseSyncLock is a no-op when no lock file exists', () => {
|
|
75
|
+
const base = tempDir();
|
|
76
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
77
|
+
try {
|
|
78
|
+
// Should not throw
|
|
79
|
+
releaseSyncLock(base);
|
|
80
|
+
} finally {
|
|
81
|
+
cleanupDir(base);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ─── acquire → release → re-acquire round-trip ───────────────────────────
|
|
86
|
+
|
|
87
|
+
test('sync-lock: can re-acquire after release', () => {
|
|
88
|
+
const base = tempDir();
|
|
89
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
90
|
+
try {
|
|
91
|
+
const r1 = acquireSyncLock(base);
|
|
92
|
+
assert.strictEqual(r1.acquired, true, 'first acquire should succeed');
|
|
93
|
+
releaseSyncLock(base);
|
|
94
|
+
const r2 = acquireSyncLock(base);
|
|
95
|
+
assert.strictEqual(r2.acquired, true, 're-acquire after release should succeed');
|
|
96
|
+
releaseSyncLock(base);
|
|
97
|
+
} finally {
|
|
98
|
+
cleanupDir(base);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─── stale lock override ─────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test('sync-lock: overrides stale lock file (mtime backdated)', (t) => {
|
|
105
|
+
const base = tempDir();
|
|
106
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
107
|
+
const lockPath = path.join(base, '.gsd', 'sync.lock');
|
|
108
|
+
try {
|
|
109
|
+
// Write a lock file with a very old mtime (simulating staleness)
|
|
110
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: 99999, acquired_at: new Date(0).toISOString() }));
|
|
111
|
+
// Backdate mtime by 2 minutes
|
|
112
|
+
const staleTime = new Date(Date.now() - 120_000);
|
|
113
|
+
fs.utimesSync(lockPath, staleTime, staleTime);
|
|
114
|
+
|
|
115
|
+
// Should override stale lock and acquire
|
|
116
|
+
const result = acquireSyncLock(base, 500);
|
|
117
|
+
assert.strictEqual(result.acquired, true, 'should acquire over stale lock');
|
|
118
|
+
releaseSyncLock(base);
|
|
119
|
+
} finally {
|
|
120
|
+
cleanupDir(base);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// GSD — unit-ownership tests
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
claimUnit,
|
|
12
|
+
releaseUnit,
|
|
13
|
+
getOwner,
|
|
14
|
+
checkOwnership,
|
|
15
|
+
taskUnitKey,
|
|
16
|
+
sliceUnitKey,
|
|
17
|
+
} from '../unit-ownership.ts';
|
|
18
|
+
|
|
19
|
+
function makeTmpBase(): string {
|
|
20
|
+
return mkdtempSync(join(tmpdir(), 'gsd-ownership-'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanup(base: string): void {
|
|
24
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Key builders ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('taskUnitKey: builds correct key', () => {
|
|
30
|
+
assert.equal(taskUnitKey('M001', 'S01', 'T01'), 'M001/S01/T01');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sliceUnitKey: builds correct key', () => {
|
|
34
|
+
assert.equal(sliceUnitKey('M001', 'S01'), 'M001/S01');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── Claim / get / release ───────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
test('claimUnit: creates claim file and records agent', () => {
|
|
40
|
+
const base = makeTmpBase();
|
|
41
|
+
try {
|
|
42
|
+
claimUnit(base, 'M001/S01/T01', 'executor-01');
|
|
43
|
+
|
|
44
|
+
assert.ok(existsSync(join(base, '.gsd', 'unit-claims.json')), 'claim file should exist');
|
|
45
|
+
assert.equal(getOwner(base, 'M001/S01/T01'), 'executor-01');
|
|
46
|
+
} finally {
|
|
47
|
+
cleanup(base);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('claimUnit: overwrites existing claim (last writer wins)', () => {
|
|
52
|
+
const base = makeTmpBase();
|
|
53
|
+
try {
|
|
54
|
+
claimUnit(base, 'M001/S01/T01', 'executor-01');
|
|
55
|
+
claimUnit(base, 'M001/S01/T01', 'executor-02');
|
|
56
|
+
|
|
57
|
+
assert.equal(getOwner(base, 'M001/S01/T01'), 'executor-02');
|
|
58
|
+
} finally {
|
|
59
|
+
cleanup(base);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('claimUnit: multiple units can be claimed independently', () => {
|
|
64
|
+
const base = makeTmpBase();
|
|
65
|
+
try {
|
|
66
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
67
|
+
claimUnit(base, 'M001/S01/T02', 'agent-b');
|
|
68
|
+
|
|
69
|
+
assert.equal(getOwner(base, 'M001/S01/T01'), 'agent-a');
|
|
70
|
+
assert.equal(getOwner(base, 'M001/S01/T02'), 'agent-b');
|
|
71
|
+
} finally {
|
|
72
|
+
cleanup(base);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('getOwner: returns null when no claim file exists', () => {
|
|
77
|
+
const base = makeTmpBase();
|
|
78
|
+
try {
|
|
79
|
+
assert.equal(getOwner(base, 'M001/S01/T01'), null);
|
|
80
|
+
} finally {
|
|
81
|
+
cleanup(base);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('getOwner: returns null for unclaimed unit', () => {
|
|
86
|
+
const base = makeTmpBase();
|
|
87
|
+
try {
|
|
88
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
89
|
+
assert.equal(getOwner(base, 'M001/S01/T99'), null);
|
|
90
|
+
} finally {
|
|
91
|
+
cleanup(base);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('releaseUnit: removes claim', () => {
|
|
96
|
+
const base = makeTmpBase();
|
|
97
|
+
try {
|
|
98
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
99
|
+
releaseUnit(base, 'M001/S01/T01');
|
|
100
|
+
|
|
101
|
+
assert.equal(getOwner(base, 'M001/S01/T01'), null);
|
|
102
|
+
} finally {
|
|
103
|
+
cleanup(base);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('releaseUnit: no-op for non-existent claim', () => {
|
|
108
|
+
const base = makeTmpBase();
|
|
109
|
+
try {
|
|
110
|
+
// Should not throw
|
|
111
|
+
releaseUnit(base, 'M001/S01/T01');
|
|
112
|
+
} finally {
|
|
113
|
+
cleanup(base);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ─── checkOwnership ──────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
test('checkOwnership: returns null when no actorName provided (opt-in)', () => {
|
|
120
|
+
const base = makeTmpBase();
|
|
121
|
+
try {
|
|
122
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
123
|
+
|
|
124
|
+
// No actorName → ownership not enforced
|
|
125
|
+
assert.equal(checkOwnership(base, 'M001/S01/T01', undefined), null);
|
|
126
|
+
} finally {
|
|
127
|
+
cleanup(base);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('checkOwnership: returns null when no claim file exists', () => {
|
|
132
|
+
const base = makeTmpBase();
|
|
133
|
+
try {
|
|
134
|
+
assert.equal(checkOwnership(base, 'M001/S01/T01', 'agent-a'), null);
|
|
135
|
+
} finally {
|
|
136
|
+
cleanup(base);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('checkOwnership: returns null when unit is unclaimed', () => {
|
|
141
|
+
const base = makeTmpBase();
|
|
142
|
+
try {
|
|
143
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
144
|
+
|
|
145
|
+
// Different unit, unclaimed
|
|
146
|
+
assert.equal(checkOwnership(base, 'M001/S01/T99', 'agent-b'), null);
|
|
147
|
+
} finally {
|
|
148
|
+
cleanup(base);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('checkOwnership: returns null when actor matches owner', () => {
|
|
153
|
+
const base = makeTmpBase();
|
|
154
|
+
try {
|
|
155
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
156
|
+
|
|
157
|
+
assert.equal(checkOwnership(base, 'M001/S01/T01', 'agent-a'), null);
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup(base);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('checkOwnership: returns error string when actor does not match owner', () => {
|
|
164
|
+
const base = makeTmpBase();
|
|
165
|
+
try {
|
|
166
|
+
claimUnit(base, 'M001/S01/T01', 'agent-a');
|
|
167
|
+
|
|
168
|
+
const err = checkOwnership(base, 'M001/S01/T01', 'agent-b');
|
|
169
|
+
assert.ok(err !== null, 'should return error');
|
|
170
|
+
assert.match(err!, /owned by agent-a/);
|
|
171
|
+
assert.match(err!, /not agent-b/);
|
|
172
|
+
} finally {
|
|
173
|
+
cleanup(base);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// GSD Extension — workflow-events unit tests
|
|
2
|
+
// Tests appendEvent, readEvents, findForkPoint, compactMilestoneEvents.
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import {
|
|
10
|
+
appendEvent,
|
|
11
|
+
readEvents,
|
|
12
|
+
findForkPoint,
|
|
13
|
+
compactMilestoneEvents,
|
|
14
|
+
type WorkflowEvent,
|
|
15
|
+
} from '../workflow-events.ts';
|
|
16
|
+
|
|
17
|
+
function tempDir(): string {
|
|
18
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-events-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanupDir(dirPath: string): void {
|
|
22
|
+
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeEvent(cmd: string, params: Record<string, unknown> = {}): Omit<WorkflowEvent, 'hash' | 'session_id'> {
|
|
26
|
+
return { cmd, params, ts: new Date().toISOString(), actor: 'agent' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── appendEvent ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
test('workflow-events: appendEvent creates .gsd dir and event-log.jsonl', () => {
|
|
32
|
+
const base = tempDir();
|
|
33
|
+
try {
|
|
34
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
|
|
35
|
+
assert.ok(fs.existsSync(path.join(base, '.gsd', 'event-log.jsonl')));
|
|
36
|
+
} finally {
|
|
37
|
+
cleanupDir(base);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('workflow-events: appendEvent writes valid JSON line', () => {
|
|
42
|
+
const base = tempDir();
|
|
43
|
+
try {
|
|
44
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
|
|
45
|
+
const content = fs.readFileSync(path.join(base, '.gsd', 'event-log.jsonl'), 'utf-8');
|
|
46
|
+
const lines = content.trim().split('\n');
|
|
47
|
+
assert.strictEqual(lines.length, 1);
|
|
48
|
+
const parsed = JSON.parse(lines[0]!) as WorkflowEvent;
|
|
49
|
+
assert.strictEqual(parsed.cmd, 'complete-task');
|
|
50
|
+
assert.strictEqual(parsed.actor, 'agent');
|
|
51
|
+
assert.strictEqual(typeof parsed.hash, 'string');
|
|
52
|
+
assert.strictEqual(parsed.hash.length, 16);
|
|
53
|
+
} finally {
|
|
54
|
+
cleanupDir(base);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('workflow-events: appendEvent appends multiple events', () => {
|
|
59
|
+
const base = tempDir();
|
|
60
|
+
try {
|
|
61
|
+
appendEvent(base, makeEvent('complete-task', { taskId: 'T01' }));
|
|
62
|
+
appendEvent(base, makeEvent('complete-slice', { sliceId: 'S01' }));
|
|
63
|
+
const events = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
|
|
64
|
+
assert.strictEqual(events.length, 2);
|
|
65
|
+
assert.strictEqual(events[0]!.cmd, 'complete-task');
|
|
66
|
+
assert.strictEqual(events[1]!.cmd, 'complete-slice');
|
|
67
|
+
} finally {
|
|
68
|
+
cleanupDir(base);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('workflow-events: same cmd+params → same hash (deterministic)', () => {
|
|
73
|
+
const base = tempDir();
|
|
74
|
+
try {
|
|
75
|
+
appendEvent(base, makeEvent('plan-task', { milestoneId: 'M001', sliceId: 'S01' }));
|
|
76
|
+
appendEvent(base, makeEvent('plan-task', { milestoneId: 'M001', sliceId: 'S01' }));
|
|
77
|
+
const events = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
|
|
78
|
+
assert.strictEqual(events[0]!.hash, events[1]!.hash, 'identical cmd+params produce identical hash');
|
|
79
|
+
} finally {
|
|
80
|
+
cleanupDir(base);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('workflow-events: different params → different hash', () => {
|
|
85
|
+
const base = tempDir();
|
|
86
|
+
try {
|
|
87
|
+
appendEvent(base, makeEvent('complete-task', { taskId: 'T01' }));
|
|
88
|
+
appendEvent(base, makeEvent('complete-task', { taskId: 'T02' }));
|
|
89
|
+
const events = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
|
|
90
|
+
assert.notStrictEqual(events[0]!.hash, events[1]!.hash, 'different params produce different hash');
|
|
91
|
+
} finally {
|
|
92
|
+
cleanupDir(base);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── readEvents ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test('workflow-events: readEvents returns [] for non-existent file', () => {
|
|
99
|
+
const result = readEvents('/nonexistent/path/event-log.jsonl');
|
|
100
|
+
assert.deepStrictEqual(result, []);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('workflow-events: readEvents skips corrupted lines', () => {
|
|
104
|
+
const base = tempDir();
|
|
105
|
+
try {
|
|
106
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
107
|
+
const logPath = path.join(base, '.gsd', 'event-log.jsonl');
|
|
108
|
+
// Write a valid line, a corrupted line, and another valid line
|
|
109
|
+
fs.writeFileSync(logPath,
|
|
110
|
+
'{"cmd":"complete-task","params":{},"ts":"2026-01-01T00:00:00Z","hash":"abcd1234abcd1234","actor":"agent"}\n' +
|
|
111
|
+
'NOT VALID JSON {{{{\n' +
|
|
112
|
+
'{"cmd":"plan-task","params":{},"ts":"2026-01-01T00:00:01Z","hash":"1234abcd1234abcd","actor":"system"}\n',
|
|
113
|
+
);
|
|
114
|
+
const events = readEvents(logPath);
|
|
115
|
+
assert.strictEqual(events.length, 2, 'should return 2 valid events, skipping the corrupted line');
|
|
116
|
+
assert.strictEqual(events[0]!.cmd, 'complete-task');
|
|
117
|
+
assert.strictEqual(events[1]!.cmd, 'plan-task');
|
|
118
|
+
} finally {
|
|
119
|
+
cleanupDir(base);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── findForkPoint ───────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test('workflow-events: findForkPoint returns -1 for two empty logs', () => {
|
|
126
|
+
assert.strictEqual(findForkPoint([], []), -1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('workflow-events: findForkPoint returns -1 when first events differ', () => {
|
|
130
|
+
const e1 = { cmd: 'a', params: {}, ts: '', hash: 'hash1', actor: 'agent' } as WorkflowEvent;
|
|
131
|
+
const e2 = { cmd: 'b', params: {}, ts: '', hash: 'hash2', actor: 'agent' } as WorkflowEvent;
|
|
132
|
+
assert.strictEqual(findForkPoint([e1], [e2]), -1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('workflow-events: findForkPoint returns 0 when only first event is common', () => {
|
|
136
|
+
const common = { cmd: 'a', params: {}, ts: '', hash: 'hash1', actor: 'agent' } as WorkflowEvent;
|
|
137
|
+
const eA = { cmd: 'b', params: {}, ts: '', hash: 'hash2', actor: 'agent' } as WorkflowEvent;
|
|
138
|
+
const eB = { cmd: 'c', params: {}, ts: '', hash: 'hash3', actor: 'agent' } as WorkflowEvent;
|
|
139
|
+
// logA: [common, eA], logB: [common, eB]
|
|
140
|
+
assert.strictEqual(findForkPoint([common, eA], [common, eB]), 0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('workflow-events: findForkPoint returns last common index for prefix relationship', () => {
|
|
144
|
+
const e1 = { cmd: 'a', params: {}, ts: '', hash: 'h1', actor: 'agent' } as WorkflowEvent;
|
|
145
|
+
const e2 = { cmd: 'b', params: {}, ts: '', hash: 'h2', actor: 'agent' } as WorkflowEvent;
|
|
146
|
+
const e3 = { cmd: 'c', params: {}, ts: '', hash: 'h3', actor: 'agent' } as WorkflowEvent;
|
|
147
|
+
// logA is a prefix of logB → fork point is last index of logA
|
|
148
|
+
assert.strictEqual(findForkPoint([e1, e2], [e1, e2, e3]), 1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('workflow-events: findForkPoint handles equal logs', () => {
|
|
152
|
+
const e1 = { cmd: 'a', params: {}, ts: '', hash: 'h1', actor: 'agent' } as WorkflowEvent;
|
|
153
|
+
const e2 = { cmd: 'b', params: {}, ts: '', hash: 'h2', actor: 'agent' } as WorkflowEvent;
|
|
154
|
+
assert.strictEqual(findForkPoint([e1, e2], [e1, e2]), 1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ─── compactMilestoneEvents ──────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
test('workflow-events: compactMilestoneEvents returns { archived: 0 } when no matching events', () => {
|
|
160
|
+
const base = tempDir();
|
|
161
|
+
try {
|
|
162
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M002', taskId: 'T01' }));
|
|
163
|
+
const result = compactMilestoneEvents(base, 'M001');
|
|
164
|
+
assert.strictEqual(result.archived, 0);
|
|
165
|
+
} finally {
|
|
166
|
+
cleanupDir(base);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('workflow-events: compactMilestoneEvents archives milestone events', () => {
|
|
171
|
+
const base = tempDir();
|
|
172
|
+
try {
|
|
173
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
|
|
174
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T02' }));
|
|
175
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M002', taskId: 'T03' }));
|
|
176
|
+
|
|
177
|
+
const result = compactMilestoneEvents(base, 'M001');
|
|
178
|
+
assert.strictEqual(result.archived, 2, 'should archive 2 M001 events');
|
|
179
|
+
|
|
180
|
+
// Archive file should exist
|
|
181
|
+
const archivePath = path.join(base, '.gsd', 'event-log-M001.jsonl.archived');
|
|
182
|
+
assert.ok(fs.existsSync(archivePath), 'archive file should exist');
|
|
183
|
+
const archived = readEvents(archivePath);
|
|
184
|
+
assert.strictEqual(archived.length, 2, 'archive file should have 2 events');
|
|
185
|
+
|
|
186
|
+
// Active log should retain only M002 event
|
|
187
|
+
const active = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
|
|
188
|
+
assert.strictEqual(active.length, 1, 'active log should have 1 remaining event');
|
|
189
|
+
assert.strictEqual((active[0]!.params as { milestoneId?: string }).milestoneId, 'M002');
|
|
190
|
+
} finally {
|
|
191
|
+
cleanupDir(base);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('workflow-events: compactMilestoneEvents empties active log when all events are from milestone', () => {
|
|
196
|
+
const base = tempDir();
|
|
197
|
+
try {
|
|
198
|
+
appendEvent(base, makeEvent('complete-task', { milestoneId: 'M001', taskId: 'T01' }));
|
|
199
|
+
compactMilestoneEvents(base, 'M001');
|
|
200
|
+
const active = readEvents(path.join(base, '.gsd', 'event-log.jsonl'));
|
|
201
|
+
assert.strictEqual(active.length, 0, 'active log should be empty after full compact');
|
|
202
|
+
} finally {
|
|
203
|
+
cleanupDir(base);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// GSD Extension — workflow-manifest unit tests
|
|
2
|
+
// Tests writeManifest, readManifest, snapshotState, bootstrapFromManifest.
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import {
|
|
10
|
+
openDatabase,
|
|
11
|
+
closeDatabase,
|
|
12
|
+
insertMilestone,
|
|
13
|
+
insertSlice,
|
|
14
|
+
insertTask,
|
|
15
|
+
} from '../gsd-db.ts';
|
|
16
|
+
import {
|
|
17
|
+
writeManifest,
|
|
18
|
+
readManifest,
|
|
19
|
+
snapshotState,
|
|
20
|
+
bootstrapFromManifest,
|
|
21
|
+
} from '../workflow-manifest.ts';
|
|
22
|
+
|
|
23
|
+
function tempDir(): string {
|
|
24
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-manifest-'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tempDbPath(base: string): string {
|
|
28
|
+
return path.join(base, 'test.db');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanupDir(dirPath: string): void {
|
|
32
|
+
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── readManifest: no file ────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
test('workflow-manifest: readManifest returns null when file does not exist', () => {
|
|
38
|
+
const base = tempDir();
|
|
39
|
+
try {
|
|
40
|
+
const result = readManifest(base);
|
|
41
|
+
assert.strictEqual(result, null);
|
|
42
|
+
} finally {
|
|
43
|
+
cleanupDir(base);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── writeManifest + readManifest round-trip ─────────────────────────────
|
|
48
|
+
|
|
49
|
+
test('workflow-manifest: writeManifest creates state-manifest.json with version 1', () => {
|
|
50
|
+
const base = tempDir();
|
|
51
|
+
openDatabase(tempDbPath(base));
|
|
52
|
+
try {
|
|
53
|
+
writeManifest(base);
|
|
54
|
+
const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
|
|
55
|
+
assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist');
|
|
56
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
57
|
+
assert.strictEqual(raw.version, 1);
|
|
58
|
+
} finally {
|
|
59
|
+
closeDatabase();
|
|
60
|
+
cleanupDir(base);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('workflow-manifest: readManifest parses manifest written by writeManifest', () => {
|
|
65
|
+
const base = tempDir();
|
|
66
|
+
openDatabase(tempDbPath(base));
|
|
67
|
+
try {
|
|
68
|
+
writeManifest(base);
|
|
69
|
+
const manifest = readManifest(base);
|
|
70
|
+
assert.ok(manifest !== null);
|
|
71
|
+
assert.strictEqual(manifest!.version, 1);
|
|
72
|
+
assert.ok(typeof manifest!.exported_at === 'string');
|
|
73
|
+
assert.ok(Array.isArray(manifest!.milestones));
|
|
74
|
+
assert.ok(Array.isArray(manifest!.slices));
|
|
75
|
+
assert.ok(Array.isArray(manifest!.tasks));
|
|
76
|
+
assert.ok(Array.isArray(manifest!.decisions));
|
|
77
|
+
assert.ok(Array.isArray(manifest!.verification_evidence));
|
|
78
|
+
} finally {
|
|
79
|
+
closeDatabase();
|
|
80
|
+
cleanupDir(base);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ─── snapshotState: captures DB rows ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
test('workflow-manifest: snapshotState includes inserted milestone', () => {
|
|
87
|
+
const base = tempDir();
|
|
88
|
+
openDatabase(tempDbPath(base));
|
|
89
|
+
try {
|
|
90
|
+
insertMilestone({ id: 'M001', title: 'Auth Milestone' });
|
|
91
|
+
const snap = snapshotState();
|
|
92
|
+
assert.strictEqual(snap.version, 1);
|
|
93
|
+
const m = snap.milestones.find((r) => r.id === 'M001');
|
|
94
|
+
assert.ok(m !== undefined, 'M001 should appear in snapshot');
|
|
95
|
+
assert.strictEqual(m!.title, 'Auth Milestone');
|
|
96
|
+
} finally {
|
|
97
|
+
closeDatabase();
|
|
98
|
+
cleanupDir(base);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('workflow-manifest: snapshotState captures tasks', () => {
|
|
103
|
+
const base = tempDir();
|
|
104
|
+
openDatabase(tempDbPath(base));
|
|
105
|
+
try {
|
|
106
|
+
insertMilestone({ id: 'M001' });
|
|
107
|
+
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
108
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Do thing', status: 'complete' });
|
|
109
|
+
const snap = snapshotState();
|
|
110
|
+
const t = snap.tasks.find((r) => r.id === 'T01');
|
|
111
|
+
assert.ok(t !== undefined, 'T01 should appear in snapshot');
|
|
112
|
+
assert.strictEqual(t!.status, 'complete');
|
|
113
|
+
} finally {
|
|
114
|
+
closeDatabase();
|
|
115
|
+
cleanupDir(base);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── bootstrapFromManifest ────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
test('workflow-manifest: bootstrapFromManifest returns false when no manifest file', () => {
|
|
122
|
+
const base = tempDir();
|
|
123
|
+
openDatabase(tempDbPath(base));
|
|
124
|
+
try {
|
|
125
|
+
const result = bootstrapFromManifest(base);
|
|
126
|
+
assert.strictEqual(result, false);
|
|
127
|
+
} finally {
|
|
128
|
+
closeDatabase();
|
|
129
|
+
cleanupDir(base);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('workflow-manifest: bootstrapFromManifest restores DB from manifest (round-trip)', () => {
|
|
134
|
+
const base = tempDir();
|
|
135
|
+
openDatabase(tempDbPath(base));
|
|
136
|
+
try {
|
|
137
|
+
// Insert data and write manifest
|
|
138
|
+
insertMilestone({ id: 'M001', title: 'Restored Milestone' });
|
|
139
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Restored Slice' });
|
|
140
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Restored Task', status: 'complete' });
|
|
141
|
+
writeManifest(base);
|
|
142
|
+
closeDatabase();
|
|
143
|
+
|
|
144
|
+
// Open a fresh DB and bootstrap from manifest
|
|
145
|
+
const newDbPath = path.join(base, 'new.db');
|
|
146
|
+
openDatabase(newDbPath);
|
|
147
|
+
const result = bootstrapFromManifest(base);
|
|
148
|
+
assert.strictEqual(result, true, 'bootstrapFromManifest should return true');
|
|
149
|
+
|
|
150
|
+
// Verify restored state
|
|
151
|
+
const snap = snapshotState();
|
|
152
|
+
const m = snap.milestones.find((r) => r.id === 'M001');
|
|
153
|
+
assert.ok(m !== undefined, 'M001 should be restored');
|
|
154
|
+
assert.strictEqual(m!.title, 'Restored Milestone');
|
|
155
|
+
|
|
156
|
+
const s = snap.slices.find((r) => r.id === 'S01');
|
|
157
|
+
assert.ok(s !== undefined, 'S01 should be restored');
|
|
158
|
+
|
|
159
|
+
const t = snap.tasks.find((r) => r.id === 'T01');
|
|
160
|
+
assert.ok(t !== undefined, 'T01 should be restored');
|
|
161
|
+
assert.strictEqual(t!.status, 'complete');
|
|
162
|
+
} finally {
|
|
163
|
+
closeDatabase();
|
|
164
|
+
cleanupDir(base);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── readManifest: version check ─────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
test('workflow-manifest: readManifest throws on unsupported version', () => {
|
|
171
|
+
const base = tempDir();
|
|
172
|
+
try {
|
|
173
|
+
fs.mkdirSync(path.join(base, '.gsd'), { recursive: true });
|
|
174
|
+
fs.writeFileSync(
|
|
175
|
+
path.join(base, '.gsd', 'state-manifest.json'),
|
|
176
|
+
JSON.stringify({ version: 99, exported_at: '', milestones: [], slices: [], tasks: [], decisions: [], verification_evidence: [] }),
|
|
177
|
+
);
|
|
178
|
+
assert.throws(
|
|
179
|
+
() => readManifest(base),
|
|
180
|
+
/Unsupported manifest version/,
|
|
181
|
+
'should throw on version mismatch',
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
cleanupDir(base);
|
|
185
|
+
}
|
|
186
|
+
});
|