gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.49d972f
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/cli.js +15 -9
- package/dist/resource-loader.js +80 -8
- package/dist/resources/extensions/gsd/auto-post-unit.ts +9 -4
- package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
- package/dist/resources/extensions/gsd/auto-start.ts +25 -10
- package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
- package/dist/resources/extensions/gsd/auto.ts +67 -22
- package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
- package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
- package/dist/resources/extensions/gsd/commands.ts +22 -28
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
- package/dist/resources/extensions/gsd/doctor.ts +2 -6
- package/dist/resources/extensions/gsd/export.ts +28 -2
- package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
- package/dist/resources/extensions/gsd/index.ts +2 -1
- package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
- package/dist/resources/extensions/gsd/metrics.ts +17 -31
- package/dist/resources/extensions/gsd/paths.ts +0 -8
- package/dist/resources/extensions/gsd/queue-order.ts +10 -11
- package/dist/resources/extensions/gsd/routing-history.ts +13 -17
- package/dist/resources/extensions/gsd/session-lock.ts +284 -0
- package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/dist/resources/extensions/gsd/types.ts +1 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
- package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
- package/dist/resources/extensions/remote-questions/notify.ts +1 -2
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
- package/dist/resources/extensions/remote-questions/types.ts +3 -0
- package/dist/resources/extensions/shared/mod.ts +3 -0
- package/package.json +6 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +14 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +19 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +9 -4
- package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
- package/src/resources/extensions/gsd/auto-start.ts +25 -10
- package/src/resources/extensions/gsd/auto-verification.ts +41 -7
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
- package/src/resources/extensions/gsd/auto.ts +67 -22
- package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
- package/src/resources/extensions/gsd/commands-logs.ts +536 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
- package/src/resources/extensions/gsd/commands.ts +22 -28
- package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
- package/src/resources/extensions/gsd/doctor-types.ts +13 -0
- package/src/resources/extensions/gsd/doctor.ts +2 -6
- package/src/resources/extensions/gsd/export.ts +28 -2
- package/src/resources/extensions/gsd/gsd-db.ts +19 -0
- package/src/resources/extensions/gsd/index.ts +2 -1
- package/src/resources/extensions/gsd/json-persistence.ts +67 -0
- package/src/resources/extensions/gsd/metrics.ts +17 -31
- package/src/resources/extensions/gsd/paths.ts +0 -8
- package/src/resources/extensions/gsd/queue-order.ts +10 -11
- package/src/resources/extensions/gsd/routing-history.ts +13 -17
- package/src/resources/extensions/gsd/session-lock.ts +284 -0
- package/src/resources/extensions/gsd/session-status-io.ts +23 -41
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/src/resources/extensions/gsd/verification-gate.ts +13 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
- package/src/resources/extensions/remote-questions/http-client.ts +76 -0
- package/src/resources/extensions/remote-questions/notify.ts +1 -2
- package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
- package/src/resources/extensions/remote-questions/types.ts +3 -0
- package/src/resources/extensions/shared/mod.ts +3 -0
- package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/dist/resources/extensions/shared/progress-widget.ts +0 -282
- package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
- package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/src/resources/extensions/shared/progress-widget.ts +0 -282
- package/src/resources/extensions/shared/thinking-widget.ts +0 -107
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
acquireSessionLock,
|
|
9
|
+
releaseSessionLock,
|
|
10
|
+
updateSessionLock,
|
|
11
|
+
validateSessionLock,
|
|
12
|
+
readSessionLockData,
|
|
13
|
+
isSessionLockHeld,
|
|
14
|
+
isSessionLockProcessAlive,
|
|
15
|
+
} from "../session-lock.ts";
|
|
16
|
+
|
|
17
|
+
// ─── acquireSessionLock ──────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
test("acquireSessionLock succeeds on empty directory", () => {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
21
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
22
|
+
|
|
23
|
+
const result = acquireSessionLock(dir);
|
|
24
|
+
assert.equal(result.acquired, true, "should acquire lock on empty dir");
|
|
25
|
+
|
|
26
|
+
// Verify lock file was created with correct data
|
|
27
|
+
const lockPath = join(dir, ".gsd", "auto.lock");
|
|
28
|
+
assert.ok(existsSync(lockPath), "auto.lock should exist after acquire");
|
|
29
|
+
|
|
30
|
+
const data = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
31
|
+
assert.equal(data.pid, process.pid, "lock should contain current PID");
|
|
32
|
+
assert.equal(data.unitType, "starting", "initial unit type should be 'starting'");
|
|
33
|
+
|
|
34
|
+
releaseSessionLock(dir);
|
|
35
|
+
rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("acquireSessionLock rejects when another live process holds lock", () => {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
40
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
41
|
+
|
|
42
|
+
// Simulate another process holding the lock by writing a lock with parent PID
|
|
43
|
+
const fakeLockData = {
|
|
44
|
+
pid: process.ppid,
|
|
45
|
+
startedAt: new Date().toISOString(),
|
|
46
|
+
unitType: "execute-task",
|
|
47
|
+
unitId: "M001/S01/T01",
|
|
48
|
+
unitStartedAt: new Date().toISOString(),
|
|
49
|
+
completedUnits: 2,
|
|
50
|
+
};
|
|
51
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(fakeLockData, null, 2));
|
|
52
|
+
|
|
53
|
+
// First acquire to set up proper-lockfile state
|
|
54
|
+
const result1 = acquireSessionLock(dir);
|
|
55
|
+
|
|
56
|
+
// If proper-lockfile is available, it should manage the OS lock.
|
|
57
|
+
// If not (fallback mode), the PID check should detect the live process.
|
|
58
|
+
// Either way, we can't fully simulate another process holding an OS lock
|
|
59
|
+
// from within the same process, so we test the fallback path.
|
|
60
|
+
if (result1.acquired) {
|
|
61
|
+
// We got the lock (proper-lockfile saw no OS lock from another process)
|
|
62
|
+
// This is expected since we're in the same process
|
|
63
|
+
releaseSessionLock(dir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("acquireSessionLock takes over stale lock from dead process", () => {
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
71
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Write a lock from a dead process
|
|
74
|
+
const staleLockData = {
|
|
75
|
+
pid: 9999999,
|
|
76
|
+
startedAt: "2026-03-01T00:00:00Z",
|
|
77
|
+
unitType: "execute-task",
|
|
78
|
+
unitId: "M001/S01/T01",
|
|
79
|
+
unitStartedAt: "2026-03-01T00:00:00Z",
|
|
80
|
+
completedUnits: 0,
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(staleLockData, null, 2));
|
|
83
|
+
|
|
84
|
+
const result = acquireSessionLock(dir);
|
|
85
|
+
assert.equal(result.acquired, true, "should take over lock from dead process");
|
|
86
|
+
|
|
87
|
+
// Verify our PID is now in the lock
|
|
88
|
+
const data = readSessionLockData(dir);
|
|
89
|
+
assert.ok(data, "lock data should exist after acquire");
|
|
90
|
+
assert.equal(data!.pid, process.pid, "lock should contain our PID now");
|
|
91
|
+
|
|
92
|
+
releaseSessionLock(dir);
|
|
93
|
+
rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── releaseSessionLock ─────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test("releaseSessionLock removes the lock file", () => {
|
|
99
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
100
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
101
|
+
|
|
102
|
+
const result = acquireSessionLock(dir);
|
|
103
|
+
assert.equal(result.acquired, true);
|
|
104
|
+
|
|
105
|
+
releaseSessionLock(dir);
|
|
106
|
+
|
|
107
|
+
const lockPath = join(dir, ".gsd", "auto.lock");
|
|
108
|
+
assert.ok(!existsSync(lockPath), "auto.lock should be removed after release");
|
|
109
|
+
|
|
110
|
+
rmSync(dir, { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("releaseSessionLock is safe when no lock exists", () => {
|
|
114
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
115
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
116
|
+
|
|
117
|
+
// Should not throw
|
|
118
|
+
releaseSessionLock(dir);
|
|
119
|
+
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── updateSessionLock ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test("updateSessionLock updates the lock data without re-acquiring", () => {
|
|
126
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
127
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
128
|
+
|
|
129
|
+
const result = acquireSessionLock(dir);
|
|
130
|
+
assert.equal(result.acquired, true);
|
|
131
|
+
|
|
132
|
+
updateSessionLock(dir, "execute-task", "M001/S01/T02", 3, "/tmp/session.jsonl");
|
|
133
|
+
|
|
134
|
+
const data = readSessionLockData(dir);
|
|
135
|
+
assert.ok(data, "lock data should exist after update");
|
|
136
|
+
assert.equal(data!.pid, process.pid, "PID should still be ours");
|
|
137
|
+
assert.equal(data!.unitType, "execute-task", "unit type should be updated");
|
|
138
|
+
assert.equal(data!.unitId, "M001/S01/T02", "unit ID should be updated");
|
|
139
|
+
assert.equal(data!.completedUnits, 3, "completed count should be updated");
|
|
140
|
+
assert.equal(data!.sessionFile, "/tmp/session.jsonl", "session file should be recorded");
|
|
141
|
+
|
|
142
|
+
releaseSessionLock(dir);
|
|
143
|
+
rmSync(dir, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── validateSessionLock ────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
test("validateSessionLock returns true when we hold the lock", () => {
|
|
149
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
150
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
151
|
+
|
|
152
|
+
const result = acquireSessionLock(dir);
|
|
153
|
+
assert.equal(result.acquired, true);
|
|
154
|
+
|
|
155
|
+
assert.equal(validateSessionLock(dir), true, "should validate when we hold the lock");
|
|
156
|
+
|
|
157
|
+
releaseSessionLock(dir);
|
|
158
|
+
rmSync(dir, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("validateSessionLock returns false after release", () => {
|
|
162
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
163
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
164
|
+
|
|
165
|
+
const result = acquireSessionLock(dir);
|
|
166
|
+
assert.equal(result.acquired, true);
|
|
167
|
+
assert.equal(validateSessionLock(dir), true, "should be valid while held");
|
|
168
|
+
|
|
169
|
+
// Release the lock — both OS lock and lock file are removed
|
|
170
|
+
releaseSessionLock(dir);
|
|
171
|
+
|
|
172
|
+
// After release, _lockedPath is cleared and lock file is gone
|
|
173
|
+
assert.equal(isSessionLockHeld(dir), false, "should not be held after release");
|
|
174
|
+
|
|
175
|
+
rmSync(dir, { recursive: true, force: true });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("validateSessionLock returns false when another PID owns the lock", () => {
|
|
179
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
180
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
181
|
+
|
|
182
|
+
// Write lock data with a different PID (parent process)
|
|
183
|
+
const foreignLockData = {
|
|
184
|
+
pid: process.ppid,
|
|
185
|
+
startedAt: new Date().toISOString(),
|
|
186
|
+
unitType: "execute-task",
|
|
187
|
+
unitId: "M001/S01/T01",
|
|
188
|
+
unitStartedAt: new Date().toISOString(),
|
|
189
|
+
completedUnits: 0,
|
|
190
|
+
};
|
|
191
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(foreignLockData, null, 2));
|
|
192
|
+
|
|
193
|
+
// Without holding the OS lock, validate should check PID
|
|
194
|
+
assert.equal(validateSessionLock(dir), false, "should fail when another PID owns lock");
|
|
195
|
+
|
|
196
|
+
rmSync(dir, { recursive: true, force: true });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ─── isSessionLockHeld ──────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
test("isSessionLockHeld returns true after acquire", () => {
|
|
202
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
203
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
204
|
+
|
|
205
|
+
acquireSessionLock(dir);
|
|
206
|
+
assert.equal(isSessionLockHeld(dir), true);
|
|
207
|
+
|
|
208
|
+
releaseSessionLock(dir);
|
|
209
|
+
assert.equal(isSessionLockHeld(dir), false, "should return false after release");
|
|
210
|
+
|
|
211
|
+
rmSync(dir, { recursive: true, force: true });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ─── isSessionLockProcessAlive ──────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
test("isSessionLockProcessAlive returns false for dead PID", () => {
|
|
217
|
+
const data = {
|
|
218
|
+
pid: 9999999,
|
|
219
|
+
startedAt: new Date().toISOString(),
|
|
220
|
+
unitType: "starting",
|
|
221
|
+
unitId: "bootstrap",
|
|
222
|
+
unitStartedAt: new Date().toISOString(),
|
|
223
|
+
completedUnits: 0,
|
|
224
|
+
};
|
|
225
|
+
assert.equal(isSessionLockProcessAlive(data), false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("isSessionLockProcessAlive returns false for own PID (recycled)", () => {
|
|
229
|
+
const data = {
|
|
230
|
+
pid: process.pid,
|
|
231
|
+
startedAt: new Date().toISOString(),
|
|
232
|
+
unitType: "starting",
|
|
233
|
+
unitId: "bootstrap",
|
|
234
|
+
unitStartedAt: new Date().toISOString(),
|
|
235
|
+
completedUnits: 0,
|
|
236
|
+
};
|
|
237
|
+
// Own PID returns false because it means the lock is from a recycled PID
|
|
238
|
+
assert.equal(isSessionLockProcessAlive(data), false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── readSessionLockData ────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
test("readSessionLockData returns null when no lock exists", () => {
|
|
244
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
245
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
246
|
+
|
|
247
|
+
const data = readSessionLockData(dir);
|
|
248
|
+
assert.equal(data, null);
|
|
249
|
+
|
|
250
|
+
rmSync(dir, { recursive: true, force: true });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("readSessionLockData reads existing lock data", () => {
|
|
254
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
255
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
256
|
+
|
|
257
|
+
const lockData = {
|
|
258
|
+
pid: 12345,
|
|
259
|
+
startedAt: "2026-03-18T00:00:00Z",
|
|
260
|
+
unitType: "execute-task",
|
|
261
|
+
unitId: "M001/S01/T01",
|
|
262
|
+
unitStartedAt: "2026-03-18T00:01:00Z",
|
|
263
|
+
completedUnits: 2,
|
|
264
|
+
sessionFile: "/tmp/session.jsonl",
|
|
265
|
+
};
|
|
266
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
267
|
+
|
|
268
|
+
const data = readSessionLockData(dir);
|
|
269
|
+
assert.ok(data, "should read lock data");
|
|
270
|
+
assert.equal(data!.pid, 12345);
|
|
271
|
+
assert.equal(data!.unitType, "execute-task");
|
|
272
|
+
assert.equal(data!.unitId, "M001/S01/T01");
|
|
273
|
+
assert.equal(data!.completedUnits, 2);
|
|
274
|
+
assert.equal(data!.sessionFile, "/tmp/session.jsonl");
|
|
275
|
+
|
|
276
|
+
rmSync(dir, { recursive: true, force: true });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ─── Acquire → Release → Re-Acquire lifecycle ──────────────────────────
|
|
280
|
+
|
|
281
|
+
test("session lock supports acquire → release → re-acquire cycle", () => {
|
|
282
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
283
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
284
|
+
|
|
285
|
+
// First acquire
|
|
286
|
+
const r1 = acquireSessionLock(dir);
|
|
287
|
+
assert.equal(r1.acquired, true, "first acquire should succeed");
|
|
288
|
+
assert.equal(isSessionLockHeld(dir), true);
|
|
289
|
+
|
|
290
|
+
// Release
|
|
291
|
+
releaseSessionLock(dir);
|
|
292
|
+
assert.equal(isSessionLockHeld(dir), false);
|
|
293
|
+
|
|
294
|
+
// Re-acquire
|
|
295
|
+
const r2 = acquireSessionLock(dir);
|
|
296
|
+
assert.equal(r2.acquired, true, "re-acquire after release should succeed");
|
|
297
|
+
assert.equal(isSessionLockHeld(dir), true);
|
|
298
|
+
|
|
299
|
+
releaseSessionLock(dir);
|
|
300
|
+
rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── Lock creates .gsd/ directory if needed ─────────────────────────────
|
|
304
|
+
|
|
305
|
+
test("acquireSessionLock creates .gsd/ if it does not exist", () => {
|
|
306
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
307
|
+
// Do NOT create .gsd/ — let the lock function do it
|
|
308
|
+
|
|
309
|
+
const result = acquireSessionLock(dir);
|
|
310
|
+
assert.equal(result.acquired, true, "should succeed even without .gsd/");
|
|
311
|
+
assert.ok(existsSync(join(dir, ".gsd")), ".gsd/ should be created");
|
|
312
|
+
|
|
313
|
+
releaseSessionLock(dir);
|
|
314
|
+
rmSync(dir, { recursive: true, force: true });
|
|
315
|
+
});
|
|
@@ -290,6 +290,61 @@ test("verifyExpectedArtifact fails when VALIDATION.md is missing", () => {
|
|
|
290
290
|
}
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
+
test("verifyExpectedArtifact rejects VALIDATION with missing frontmatter", () => {
|
|
294
|
+
const base = makeTmpBase();
|
|
295
|
+
try {
|
|
296
|
+
// A VALIDATION file without frontmatter should be treated as incomplete —
|
|
297
|
+
// matching what deriveState expects. Without this, the artifact check passes
|
|
298
|
+
// but deriveState still returns validating-milestone, causing the hard skip loop.
|
|
299
|
+
writeValidation(base, "M001", "# Validation\nNo frontmatter here.");
|
|
300
|
+
clearPathCache();
|
|
301
|
+
clearParseCache();
|
|
302
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
303
|
+
assert.equal(result, false, "VALIDATION without frontmatter should fail verification");
|
|
304
|
+
} finally {
|
|
305
|
+
cleanup(base);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("verifyExpectedArtifact rejects VALIDATION with missing verdict field", () => {
|
|
310
|
+
const base = makeTmpBase();
|
|
311
|
+
try {
|
|
312
|
+
writeValidation(base, "M001", "---\nremediation_round: 0\n---\n\n# Validation");
|
|
313
|
+
clearPathCache();
|
|
314
|
+
clearParseCache();
|
|
315
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
316
|
+
assert.equal(result, false, "VALIDATION without verdict field should fail verification");
|
|
317
|
+
} finally {
|
|
318
|
+
cleanup(base);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("verifyExpectedArtifact rejects VALIDATION with unrecognized verdict", () => {
|
|
323
|
+
const base = makeTmpBase();
|
|
324
|
+
try {
|
|
325
|
+
writeValidation(base, "M001", "---\nverdict: unknown-value\nremediation_round: 0\n---\n\n# Validation");
|
|
326
|
+
clearPathCache();
|
|
327
|
+
clearParseCache();
|
|
328
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
329
|
+
assert.equal(result, false, "VALIDATION with unrecognized verdict should fail verification");
|
|
330
|
+
} finally {
|
|
331
|
+
cleanup(base);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("verifyExpectedArtifact passes VALIDATION with needs-attention verdict", () => {
|
|
336
|
+
const base = makeTmpBase();
|
|
337
|
+
try {
|
|
338
|
+
writeValidation(base, "M001", "---\nverdict: needs-attention\nremediation_round: 0\n---\n\n# Validation\nNeeds attention.");
|
|
339
|
+
clearPathCache();
|
|
340
|
+
clearParseCache();
|
|
341
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
342
|
+
assert.equal(result, true, "VALIDATION with needs-attention verdict should pass verification");
|
|
343
|
+
} finally {
|
|
344
|
+
cleanup(base);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
293
348
|
// ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
|
|
294
349
|
|
|
295
350
|
test("diagnoseExpectedArtifact returns validation path for validate-milestone", () => {
|
|
@@ -58,6 +58,7 @@ test("verification-evidence: writeVerificationJSON writes correct JSON shape", (
|
|
|
58
58
|
stdout: "all good",
|
|
59
59
|
stderr: "",
|
|
60
60
|
durationMs: 2340,
|
|
61
|
+
blocking: true,
|
|
61
62
|
},
|
|
62
63
|
],
|
|
63
64
|
});
|
|
@@ -105,9 +106,9 @@ test("verification-evidence: writeVerificationJSON maps exitCode to verdict corr
|
|
|
105
106
|
const result = makeResult({
|
|
106
107
|
passed: false,
|
|
107
108
|
checks: [
|
|
108
|
-
{ command: "lint", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
109
|
-
{ command: "test", exitCode: 1, stdout: "", stderr: "fail", durationMs: 200 },
|
|
110
|
-
{ command: "audit", exitCode: 2, stdout: "", stderr: "err", durationMs: 300 },
|
|
109
|
+
{ command: "lint", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
110
|
+
{ command: "test", exitCode: 1, stdout: "", stderr: "fail", durationMs: 200, blocking: true },
|
|
111
|
+
{ command: "audit", exitCode: 2, stdout: "", stderr: "err", durationMs: 300, blocking: true },
|
|
111
112
|
],
|
|
112
113
|
});
|
|
113
114
|
|
|
@@ -133,6 +134,7 @@ test("verification-evidence: writeVerificationJSON excludes stdout/stderr from o
|
|
|
133
134
|
stdout: "hello\n",
|
|
134
135
|
stderr: "some warning",
|
|
135
136
|
durationMs: 50,
|
|
137
|
+
blocking: true,
|
|
136
138
|
},
|
|
137
139
|
],
|
|
138
140
|
});
|
|
@@ -181,8 +183,8 @@ test("verification-evidence: writeVerificationJSON uses optional unitId when pro
|
|
|
181
183
|
test("verification-evidence: formatEvidenceTable returns markdown table with correct columns", () => {
|
|
182
184
|
const result = makeResult({
|
|
183
185
|
checks: [
|
|
184
|
-
{ command: "npm run typecheck", exitCode: 0, stdout: "", stderr: "", durationMs: 2340 },
|
|
185
|
-
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "err", durationMs: 1100 },
|
|
186
|
+
{ command: "npm run typecheck", exitCode: 0, stdout: "", stderr: "", durationMs: 2340, blocking: true },
|
|
187
|
+
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "err", durationMs: 1100, blocking: true },
|
|
186
188
|
],
|
|
187
189
|
});
|
|
188
190
|
|
|
@@ -214,9 +216,9 @@ test("verification-evidence: formatEvidenceTable returns no-checks message for e
|
|
|
214
216
|
test("verification-evidence: formatEvidenceTable formats duration as seconds with 1 decimal", () => {
|
|
215
217
|
const result = makeResult({
|
|
216
218
|
checks: [
|
|
217
|
-
{ command: "fast", exitCode: 0, stdout: "", stderr: "", durationMs: 150 },
|
|
218
|
-
{ command: "slow", exitCode: 0, stdout: "", stderr: "", durationMs: 2340 },
|
|
219
|
-
{ command: "zero", exitCode: 0, stdout: "", stderr: "", durationMs: 0 },
|
|
219
|
+
{ command: "fast", exitCode: 0, stdout: "", stderr: "", durationMs: 150, blocking: true },
|
|
220
|
+
{ command: "slow", exitCode: 0, stdout: "", stderr: "", durationMs: 2340, blocking: true },
|
|
221
|
+
{ command: "zero", exitCode: 0, stdout: "", stderr: "", durationMs: 0, blocking: true },
|
|
220
222
|
],
|
|
221
223
|
});
|
|
222
224
|
|
|
@@ -230,8 +232,8 @@ test("verification-evidence: formatEvidenceTable uses ✅/❌ emoji for pass/fai
|
|
|
230
232
|
const result = makeResult({
|
|
231
233
|
passed: false,
|
|
232
234
|
checks: [
|
|
233
|
-
{ command: "pass-cmd", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
234
|
-
{ command: "fail-cmd", exitCode: 1, stdout: "", stderr: "", durationMs: 200 },
|
|
235
|
+
{ command: "pass-cmd", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
236
|
+
{ command: "fail-cmd", exitCode: 1, stdout: "", stderr: "", durationMs: 200, blocking: true },
|
|
235
237
|
],
|
|
236
238
|
});
|
|
237
239
|
|
|
@@ -335,8 +337,8 @@ test("verification-evidence: integration — VerificationResult → JSON → tab
|
|
|
335
337
|
const result = makeResult({
|
|
336
338
|
passed: false,
|
|
337
339
|
checks: [
|
|
338
|
-
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500 },
|
|
339
|
-
{ command: "npm run test:unit", exitCode: 1, stdout: "", stderr: "1 failed", durationMs: 3200 },
|
|
340
|
+
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500, blocking: true },
|
|
341
|
+
{ command: "npm run test:unit", exitCode: 1, stdout: "", stderr: "1 failed", durationMs: 3200, blocking: true },
|
|
340
342
|
],
|
|
341
343
|
discoverySource: "package-json",
|
|
342
344
|
});
|
|
@@ -390,7 +392,7 @@ test("verification-evidence: writeVerificationJSON with retryAttempt and maxRetr
|
|
|
390
392
|
const result = makeResult({
|
|
391
393
|
passed: false,
|
|
392
394
|
checks: [
|
|
393
|
-
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "error", durationMs: 300 },
|
|
395
|
+
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "error", durationMs: 300, blocking: true },
|
|
394
396
|
],
|
|
395
397
|
});
|
|
396
398
|
|
|
@@ -415,7 +417,7 @@ test("verification-evidence: writeVerificationJSON without retry params omits re
|
|
|
415
417
|
const result = makeResult({
|
|
416
418
|
passed: true,
|
|
417
419
|
checks: [
|
|
418
|
-
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
|
|
420
|
+
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
|
|
419
421
|
],
|
|
420
422
|
});
|
|
421
423
|
|
|
@@ -441,7 +443,7 @@ test("verification-evidence: writeVerificationJSON includes runtimeErrors when p
|
|
|
441
443
|
const result = makeResult({
|
|
442
444
|
passed: false,
|
|
443
445
|
checks: [
|
|
444
|
-
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
|
|
446
|
+
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
|
|
445
447
|
],
|
|
446
448
|
runtimeErrors: [
|
|
447
449
|
{ source: "bg-shell", severity: "crash", message: "Server crashed", blocking: true },
|
|
@@ -473,7 +475,7 @@ test("verification-evidence: writeVerificationJSON omits runtimeErrors when abse
|
|
|
473
475
|
const result = makeResult({
|
|
474
476
|
passed: true,
|
|
475
477
|
checks: [
|
|
476
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50 },
|
|
478
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50, blocking: true },
|
|
477
479
|
],
|
|
478
480
|
});
|
|
479
481
|
|
|
@@ -512,7 +514,7 @@ test("verification-evidence: formatEvidenceTable appends runtime errors section"
|
|
|
512
514
|
const result = makeResult({
|
|
513
515
|
passed: false,
|
|
514
516
|
checks: [
|
|
515
|
-
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
517
|
+
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
516
518
|
],
|
|
517
519
|
runtimeErrors: [
|
|
518
520
|
{ source: "bg-shell", severity: "crash", message: "Server crashed with SIGKILL", blocking: true },
|
|
@@ -537,7 +539,7 @@ test("verification-evidence: formatEvidenceTable omits runtime errors section wh
|
|
|
537
539
|
const result = makeResult({
|
|
538
540
|
passed: true,
|
|
539
541
|
checks: [
|
|
540
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200 },
|
|
542
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200, blocking: true },
|
|
541
543
|
],
|
|
542
544
|
});
|
|
543
545
|
|
|
@@ -552,7 +554,7 @@ test("verification-evidence: formatEvidenceTable truncates runtime error message
|
|
|
552
554
|
const result = makeResult({
|
|
553
555
|
passed: false,
|
|
554
556
|
checks: [
|
|
555
|
-
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
557
|
+
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
556
558
|
],
|
|
557
559
|
runtimeErrors: [
|
|
558
560
|
{ source: "bg-shell", severity: "error", message: longMessage, blocking: false },
|
|
@@ -598,7 +600,7 @@ test("verification-evidence: writeVerificationJSON includes auditWarnings when p
|
|
|
598
600
|
const result = makeResult({
|
|
599
601
|
passed: true,
|
|
600
602
|
checks: [
|
|
601
|
-
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
|
|
603
|
+
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
|
|
602
604
|
],
|
|
603
605
|
auditWarnings: SAMPLE_AUDIT_WARNINGS,
|
|
604
606
|
});
|
|
@@ -627,7 +629,7 @@ test("verification-evidence: writeVerificationJSON omits auditWarnings when abse
|
|
|
627
629
|
const result = makeResult({
|
|
628
630
|
passed: true,
|
|
629
631
|
checks: [
|
|
630
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50 },
|
|
632
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50, blocking: true },
|
|
631
633
|
],
|
|
632
634
|
});
|
|
633
635
|
|
|
@@ -666,7 +668,7 @@ test("verification-evidence: formatEvidenceTable appends audit warnings section"
|
|
|
666
668
|
const result = makeResult({
|
|
667
669
|
passed: true,
|
|
668
670
|
checks: [
|
|
669
|
-
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
671
|
+
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
670
672
|
],
|
|
671
673
|
auditWarnings: SAMPLE_AUDIT_WARNINGS,
|
|
672
674
|
});
|
|
@@ -689,7 +691,7 @@ test("verification-evidence: formatEvidenceTable omits audit warnings section wh
|
|
|
689
691
|
const result = makeResult({
|
|
690
692
|
passed: true,
|
|
691
693
|
checks: [
|
|
692
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200 },
|
|
694
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200, blocking: true },
|
|
693
695
|
],
|
|
694
696
|
});
|
|
695
697
|
|
|
@@ -705,7 +707,7 @@ test("verification-evidence: integration — VerificationResult with auditWarnin
|
|
|
705
707
|
const result = makeResult({
|
|
706
708
|
passed: true,
|
|
707
709
|
checks: [
|
|
708
|
-
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500 },
|
|
710
|
+
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500, blocking: true },
|
|
709
711
|
],
|
|
710
712
|
auditWarnings: [
|
|
711
713
|
{
|