gsd-pi 2.73.0-dev.27730dc → 2.73.0-dev.e1c09f2
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.js +1 -5
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +7 -18
- package/dist/resources/extensions/gsd/crash-recovery.js +0 -51
- package/dist/resources/extensions/gsd/gsd-db.js +2 -36
- package/dist/resources/extensions/gsd/milestone-actions.js +1 -19
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- 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 +1 -1
- 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 +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto.ts +0 -5
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +7 -19
- package/src/resources/extensions/gsd/crash-recovery.ts +0 -59
- package/src/resources/extensions/gsd/gsd-db.ts +2 -52
- package/src/resources/extensions/gsd/milestone-actions.ts +1 -19
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -59
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +0 -64
- package/dist/resources/extensions/gsd/bootstrap/crash-log.js +0 -31
- package/src/resources/extensions/gsd/bootstrap/crash-log.ts +0 -32
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +0 -235
- /package/dist/web/standalone/.next/static/{jNiH700EcljeLnbQ2_RCv → _XD_gUDcZNBbWV5rI8RgS}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{jNiH700EcljeLnbQ2_RCv → _XD_gUDcZNBbWV5rI8RgS}/_ssgManifest.js +0 -0
|
@@ -11,9 +11,6 @@ import { registerJournalTools } from "./journal-tools.js";
|
|
|
11
11
|
import { registerQueryTools } from "./query-tools.js";
|
|
12
12
|
import { registerHooks } from "./register-hooks.js";
|
|
13
13
|
import { registerShortcuts } from "./register-shortcuts.js";
|
|
14
|
-
import { writeCrashLog } from "./crash-log.js";
|
|
15
|
-
|
|
16
|
-
export { writeCrashLog } from "./crash-log.js";
|
|
17
14
|
|
|
18
15
|
export function handleRecoverableExtensionProcessError(err: Error): boolean {
|
|
19
16
|
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
|
|
@@ -36,25 +33,16 @@ export function handleRecoverableExtensionProcessError(err: Error): boolean {
|
|
|
36
33
|
function installEpipeGuard(): void {
|
|
37
34
|
if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
|
|
38
35
|
const _gsdEpipeGuard = (err: Error): void => {
|
|
39
|
-
if (handleRecoverableExtensionProcessError(err))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
process.
|
|
36
|
+
if (handleRecoverableExtensionProcessError(err)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Log unhandled errors instead of re-throwing — throwing inside an
|
|
40
|
+
// uncaughtException handler is a fatal double-fault in Node.js (#3163).
|
|
41
|
+
process.stderr.write(`[gsd] uncaught extension error (non-fatal): ${err.message}\n`);
|
|
42
|
+
if (err.stack) process.stderr.write(`${err.stack}\n`);
|
|
45
43
|
};
|
|
46
44
|
process.on("uncaughtException", _gsdEpipeGuard);
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
if (!process.listeners("unhandledRejection").some((listener) => listener.name === "_gsdRejectionGuard")) {
|
|
50
|
-
const _gsdRejectionGuard = (reason: unknown, _promise: Promise<unknown>): void => {
|
|
51
|
-
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
52
|
-
if (handleRecoverableExtensionProcessError(err)) return;
|
|
53
|
-
writeCrashLog(err, "unhandledRejection");
|
|
54
|
-
process.exit(1);
|
|
55
|
-
};
|
|
56
|
-
process.on("unhandledRejection", _gsdRejectionGuard);
|
|
57
|
-
}
|
|
58
46
|
}
|
|
59
47
|
|
|
60
48
|
export function registerGsdExtension(pi: ExtensionAPI): void {
|
|
@@ -15,7 +15,6 @@ import { join } from "node:path";
|
|
|
15
15
|
import { gsdRoot } from "./paths.js";
|
|
16
16
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
17
17
|
import { effectiveLockFile } from "./session-lock.js";
|
|
18
|
-
import { emitJournalEvent, queryJournal } from "./journal.js";
|
|
19
18
|
|
|
20
19
|
export interface LockData {
|
|
21
20
|
pid: number;
|
|
@@ -119,61 +118,3 @@ export function formatCrashInfo(lock: LockData): string {
|
|
|
119
118
|
|
|
120
119
|
return lines.join("\n");
|
|
121
120
|
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Emit a synthetic unit-end event for a unit that crashed without emitting its own.
|
|
125
|
-
*
|
|
126
|
-
* Queries the journal to find the most recent unit-start for the crashed unit.
|
|
127
|
-
* If a matching unit-end already exists (e.g. the hard timeout fired), this is a
|
|
128
|
-
* no-op. Called during crash recovery, before clearing the stale lock.
|
|
129
|
-
*
|
|
130
|
-
* Addresses the gap reported in #3348 where `unit-start` was emitted but no
|
|
131
|
-
* `unit-end` followed — side effects landed but the worker died before closeout.
|
|
132
|
-
*/
|
|
133
|
-
export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
|
|
134
|
-
// Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
|
|
135
|
-
if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const all = queryJournal(basePath);
|
|
139
|
-
|
|
140
|
-
// Find the most recent unit-start for this unitId
|
|
141
|
-
const starts = all.filter(
|
|
142
|
-
(e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId,
|
|
143
|
-
);
|
|
144
|
-
if (starts.length === 0) return;
|
|
145
|
-
|
|
146
|
-
const lastStart = starts[starts.length - 1];
|
|
147
|
-
|
|
148
|
-
// Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
|
|
149
|
-
const alreadyClosed = all.some(
|
|
150
|
-
(e) =>
|
|
151
|
-
e.eventType === "unit-end" &&
|
|
152
|
-
e.data?.unitId === lock.unitId &&
|
|
153
|
-
e.causedBy?.flowId === lastStart.flowId &&
|
|
154
|
-
e.causedBy?.seq === lastStart.seq,
|
|
155
|
-
);
|
|
156
|
-
if (alreadyClosed) return;
|
|
157
|
-
|
|
158
|
-
// Find the highest seq in this flow for monotonic ordering
|
|
159
|
-
const maxSeq = all
|
|
160
|
-
.filter((e) => e.flowId === lastStart.flowId)
|
|
161
|
-
.reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
|
|
162
|
-
|
|
163
|
-
emitJournalEvent(basePath, {
|
|
164
|
-
ts: new Date().toISOString(),
|
|
165
|
-
flowId: lastStart.flowId,
|
|
166
|
-
seq: maxSeq + 1,
|
|
167
|
-
eventType: "unit-end",
|
|
168
|
-
data: {
|
|
169
|
-
unitType: lock.unitType,
|
|
170
|
-
unitId: lock.unitId,
|
|
171
|
-
status: "crash-recovered",
|
|
172
|
-
artifactVerified: false,
|
|
173
|
-
},
|
|
174
|
-
causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
|
|
175
|
-
});
|
|
176
|
-
} catch {
|
|
177
|
-
// Never throw from crash recovery path — journal failure must not block recovery
|
|
178
|
-
}
|
|
179
|
-
}
|
|
@@ -1564,23 +1564,6 @@ export interface TaskRow {
|
|
|
1564
1564
|
sequence: number;
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
1567
|
-
function parseTaskArrayColumn(raw: unknown): string[] {
|
|
1568
|
-
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
1569
|
-
|
|
1570
|
-
try {
|
|
1571
|
-
const parsed = JSON.parse(raw);
|
|
1572
|
-
if (Array.isArray(parsed)) return parsed.map((value) => String(value));
|
|
1573
|
-
if (parsed === null || parsed === undefined || parsed === "") return [];
|
|
1574
|
-
return [String(parsed)];
|
|
1575
|
-
} catch {
|
|
1576
|
-
// Older/corrupt rows may contain comma-separated strings instead of JSON.
|
|
1577
|
-
return raw
|
|
1578
|
-
.split(",")
|
|
1579
|
-
.map((value) => value.trim())
|
|
1580
|
-
.filter(Boolean);
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
1567
|
function rowToTask(row: Record<string, unknown>): TaskRow {
|
|
1585
1568
|
const parseTaskArray = (value: unknown): string[] => {
|
|
1586
1569
|
if (Array.isArray(value)) {
|
|
@@ -1620,8 +1603,8 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
|
|
|
1620
1603
|
blocker_discovered: (row["blocker_discovered"] as number) === 1,
|
|
1621
1604
|
deviations: row["deviations"] as string,
|
|
1622
1605
|
known_issues: row["known_issues"] as string,
|
|
1623
|
-
key_files:
|
|
1624
|
-
key_decisions:
|
|
1606
|
+
key_files: JSON.parse((row["key_files"] as string) || "[]"),
|
|
1607
|
+
key_decisions: JSON.parse((row["key_decisions"] as string) || "[]"),
|
|
1625
1608
|
full_summary_md: row["full_summary_md"] as string,
|
|
1626
1609
|
description: (row["description"] as string) ?? "",
|
|
1627
1610
|
estimate: (row["estimate"] as string) ?? "",
|
|
@@ -2217,39 +2200,6 @@ export function deleteSlice(milestoneId: string, sliceId: string): void {
|
|
|
2217
2200
|
});
|
|
2218
2201
|
}
|
|
2219
2202
|
|
|
2220
|
-
export function deleteMilestone(milestoneId: string): void {
|
|
2221
|
-
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
2222
|
-
transaction(() => {
|
|
2223
|
-
currentDb!.prepare(
|
|
2224
|
-
`DELETE FROM verification_evidence WHERE milestone_id = :mid`,
|
|
2225
|
-
).run({ ":mid": milestoneId });
|
|
2226
|
-
currentDb!.prepare(
|
|
2227
|
-
`DELETE FROM quality_gates WHERE milestone_id = :mid`,
|
|
2228
|
-
).run({ ":mid": milestoneId });
|
|
2229
|
-
currentDb!.prepare(
|
|
2230
|
-
`DELETE FROM tasks WHERE milestone_id = :mid`,
|
|
2231
|
-
).run({ ":mid": milestoneId });
|
|
2232
|
-
currentDb!.prepare(
|
|
2233
|
-
`DELETE FROM slice_dependencies WHERE milestone_id = :mid`,
|
|
2234
|
-
).run({ ":mid": milestoneId });
|
|
2235
|
-
currentDb!.prepare(
|
|
2236
|
-
`DELETE FROM slices WHERE milestone_id = :mid`,
|
|
2237
|
-
).run({ ":mid": milestoneId });
|
|
2238
|
-
currentDb!.prepare(
|
|
2239
|
-
`DELETE FROM replan_history WHERE milestone_id = :mid`,
|
|
2240
|
-
).run({ ":mid": milestoneId });
|
|
2241
|
-
currentDb!.prepare(
|
|
2242
|
-
`DELETE FROM assessments WHERE milestone_id = :mid`,
|
|
2243
|
-
).run({ ":mid": milestoneId });
|
|
2244
|
-
currentDb!.prepare(
|
|
2245
|
-
`DELETE FROM artifacts WHERE milestone_id = :mid`,
|
|
2246
|
-
).run({ ":mid": milestoneId });
|
|
2247
|
-
currentDb!.prepare(
|
|
2248
|
-
`DELETE FROM milestones WHERE id = :mid`,
|
|
2249
|
-
).run({ ":mid": milestoneId });
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
2203
|
export function updateSliceFields(milestoneId: string, sliceId: string, fields: {
|
|
2254
2204
|
title?: string;
|
|
2255
2205
|
risk?: string;
|
|
@@ -20,8 +20,7 @@ import {
|
|
|
20
20
|
} from "./paths.js";
|
|
21
21
|
import { invalidateAllCaches } from "./cache.js";
|
|
22
22
|
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
23
|
-
import {
|
|
24
|
-
import { removeWorktree } from "./worktree-manager.js";
|
|
23
|
+
import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
25
24
|
import { logWarning } from "./workflow-logger.js";
|
|
26
25
|
|
|
27
26
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
@@ -111,15 +110,6 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
|
|
|
111
110
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
112
111
|
if (!mDir || !existsSync(mDir)) return false;
|
|
113
112
|
|
|
114
|
-
try {
|
|
115
|
-
removeWorktree(basePath, milestoneId, {
|
|
116
|
-
branch: `milestone/${milestoneId}`,
|
|
117
|
-
deleteBranch: true,
|
|
118
|
-
});
|
|
119
|
-
} catch (err) {
|
|
120
|
-
logWarning("engine", `discardMilestone worktree cleanup failed for ${milestoneId}: ${(err as Error).message}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
113
|
rmSync(mDir, { recursive: true, force: true });
|
|
124
114
|
|
|
125
115
|
// Prune from queue order if present
|
|
@@ -128,14 +118,6 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
|
|
|
128
118
|
saveQueueOrder(basePath, order.filter(id => id !== milestoneId));
|
|
129
119
|
}
|
|
130
120
|
|
|
131
|
-
if (isDbAvailable()) {
|
|
132
|
-
try {
|
|
133
|
-
deleteMilestone(milestoneId);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
logWarning("engine", `discardMilestone DB cleanup failed for ${milestoneId}: ${(err as Error).message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
121
|
invalidateAllCaches();
|
|
140
122
|
return true;
|
|
141
123
|
}
|
|
@@ -15,14 +15,10 @@ import {
|
|
|
15
15
|
getRequirementById,
|
|
16
16
|
getActiveDecisions,
|
|
17
17
|
getActiveRequirements,
|
|
18
|
+
getTask,
|
|
18
19
|
transaction,
|
|
19
20
|
_getAdapter,
|
|
20
21
|
_resetProvider,
|
|
21
|
-
insertMilestone,
|
|
22
|
-
insertSlice,
|
|
23
|
-
insertTask,
|
|
24
|
-
getTask,
|
|
25
|
-
getSliceTasks,
|
|
26
22
|
} from '../gsd-db.ts';
|
|
27
23
|
|
|
28
24
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -464,60 +460,6 @@ describe('gsd-db', () => {
|
|
|
464
460
|
assert.ok(!wasDbOpenAttempted(), 'wasDbOpenAttempted should reset after closeDatabase');
|
|
465
461
|
});
|
|
466
462
|
|
|
467
|
-
test('gsd-db: rowToTask tolerates corrupt comma-separated task arrays', () => {
|
|
468
|
-
openDatabase(':memory:');
|
|
469
|
-
insertMilestone({ id: 'M001', status: 'active' });
|
|
470
|
-
insertSlice({ milestoneId: 'M001', id: 'S01', status: 'active' });
|
|
471
|
-
insertTask({
|
|
472
|
-
milestoneId: 'M001',
|
|
473
|
-
sliceId: 'S01',
|
|
474
|
-
id: 'T01',
|
|
475
|
-
title: 'Recover corrupt arrays',
|
|
476
|
-
planning: {
|
|
477
|
-
description: 'desc',
|
|
478
|
-
estimate: 'small',
|
|
479
|
-
files: ['src/original.ts'],
|
|
480
|
-
verify: 'npm test',
|
|
481
|
-
inputs: ['docs/original.md'],
|
|
482
|
-
expectedOutput: ['dist/original.md'],
|
|
483
|
-
observabilityImpact: '',
|
|
484
|
-
},
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
const adapter = _getAdapter()!;
|
|
488
|
-
adapter.prepare(
|
|
489
|
-
`UPDATE tasks
|
|
490
|
-
SET files = ?, inputs = ?, expected_output = ?, key_files = ?, key_decisions = ?
|
|
491
|
-
WHERE milestone_id = ? AND slice_id = ? AND id = ?`,
|
|
492
|
-
).run(
|
|
493
|
-
'src-erf/Models/foo.cs, src-erf/Models/bar.cs',
|
|
494
|
-
'docs/input-a.md, docs/input-b.md',
|
|
495
|
-
'dist/out-a.md, dist/out-b.md',
|
|
496
|
-
'src/resources/extensions/gsd/gsd-db.ts, src/resources/extensions/gsd/state.ts',
|
|
497
|
-
'"decision-1"',
|
|
498
|
-
'M001',
|
|
499
|
-
'S01',
|
|
500
|
-
'T01',
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
const task = getTask('M001', 'S01', 'T01');
|
|
504
|
-
assert.ok(task, 'getTask should still return the corrupt row');
|
|
505
|
-
assert.deepStrictEqual(task!.files, ['src-erf/Models/foo.cs', 'src-erf/Models/bar.cs']);
|
|
506
|
-
assert.deepStrictEqual(task!.inputs, ['docs/input-a.md', 'docs/input-b.md']);
|
|
507
|
-
assert.deepStrictEqual(task!.expected_output, ['dist/out-a.md', 'dist/out-b.md']);
|
|
508
|
-
assert.deepStrictEqual(
|
|
509
|
-
task!.key_files,
|
|
510
|
-
['src/resources/extensions/gsd/gsd-db.ts', 'src/resources/extensions/gsd/state.ts'],
|
|
511
|
-
);
|
|
512
|
-
assert.deepStrictEqual(task!.key_decisions, ['decision-1']);
|
|
513
|
-
|
|
514
|
-
const sliceTasks = getSliceTasks('M001', 'S01');
|
|
515
|
-
assert.equal(sliceTasks.length, 1, 'getSliceTasks should also survive corrupt rows');
|
|
516
|
-
assert.deepStrictEqual(sliceTasks[0]!.files, task!.files);
|
|
517
|
-
|
|
518
|
-
closeDatabase();
|
|
519
|
-
});
|
|
520
|
-
|
|
521
463
|
// ─── Final Report ──────────────────────────────────────────────────────────
|
|
522
464
|
|
|
523
465
|
});
|
|
@@ -3,22 +3,10 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
7
6
|
|
|
8
7
|
import { deriveState, invalidateStateCache, getActiveMilestoneId } from '../state.ts';
|
|
9
8
|
import { clearPathCache } from '../paths.ts';
|
|
10
9
|
import { parkMilestone, unparkMilestone, discardMilestone, isParked, getParkedReason } from '../milestone-actions.ts';
|
|
11
|
-
import {
|
|
12
|
-
closeDatabase,
|
|
13
|
-
getMilestone,
|
|
14
|
-
getMilestoneSlices,
|
|
15
|
-
getSliceTasks,
|
|
16
|
-
insertMilestone,
|
|
17
|
-
insertSlice,
|
|
18
|
-
insertTask,
|
|
19
|
-
openDatabase,
|
|
20
|
-
} from "../gsd-db.ts";
|
|
21
|
-
import { createWorktree } from "../worktree-manager.ts";
|
|
22
10
|
|
|
23
11
|
|
|
24
12
|
|
|
@@ -72,29 +60,9 @@ function createMilestone(base: string, mid: string, opts?: { withRoadmap?: boole
|
|
|
72
60
|
}
|
|
73
61
|
|
|
74
62
|
function cleanup(base: string): void {
|
|
75
|
-
try {
|
|
76
|
-
closeDatabase();
|
|
77
|
-
} catch {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
63
|
rmSync(base, { recursive: true, force: true });
|
|
81
64
|
}
|
|
82
65
|
|
|
83
|
-
function run(cmd: string, cwd: string): string {
|
|
84
|
-
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function initGitRepo(base: string): void {
|
|
88
|
-
writeFileSync(join(base, "README.md"), "# test\n", "utf-8");
|
|
89
|
-
writeFileSync(join(base, ".gsd", "STATE.md"), "# State\n", "utf-8");
|
|
90
|
-
run("git init", base);
|
|
91
|
-
run("git config user.email test@test.com", base);
|
|
92
|
-
run("git config user.name Test", base);
|
|
93
|
-
run("git add .", base);
|
|
94
|
-
run('git commit -m "init"', base);
|
|
95
|
-
run("git branch -M main", base);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
66
|
function clearCaches(): void {
|
|
99
67
|
clearPathCache();
|
|
100
68
|
invalidateStateCache();
|
|
@@ -326,38 +294,6 @@ test('discardMilestone updates queue order', () => {
|
|
|
326
294
|
}
|
|
327
295
|
});
|
|
328
296
|
|
|
329
|
-
test('discardMilestone removes DB rows, worktree, and milestone branch', () => {
|
|
330
|
-
const base = createFixtureBase();
|
|
331
|
-
try {
|
|
332
|
-
createMilestone(base, 'M001', { withRoadmap: true });
|
|
333
|
-
initGitRepo(base);
|
|
334
|
-
clearCaches();
|
|
335
|
-
|
|
336
|
-
assert.ok(openDatabase(join(base, '.gsd', 'gsd.db')), 'database opens');
|
|
337
|
-
insertMilestone({ id: 'M001', title: 'Discard me', status: 'active' });
|
|
338
|
-
insertSlice({ milestoneId: 'M001', id: 'S01', title: 'Only slice', status: 'pending' });
|
|
339
|
-
insertTask({ milestoneId: 'M001', sliceId: 'S01', id: 'T01', title: 'Only task', status: 'pending' });
|
|
340
|
-
|
|
341
|
-
const wt = createWorktree(base, 'M001', { branch: 'milestone/M001' });
|
|
342
|
-
assert.ok(existsSync(wt.path), 'worktree exists before discard');
|
|
343
|
-
assert.ok(run('git branch', base).includes('milestone/M001'), 'milestone branch exists before discard');
|
|
344
|
-
assert.ok(getMilestone('M001'), 'milestone exists in DB before discard');
|
|
345
|
-
assert.equal(getMilestoneSlices('M001').length, 1, 'slice exists in DB before discard');
|
|
346
|
-
assert.equal(getSliceTasks('M001', 'S01').length, 1, 'task exists in DB before discard');
|
|
347
|
-
|
|
348
|
-
const success = discardMilestone(base, 'M001');
|
|
349
|
-
assert.ok(success, 'discardMilestone returns true');
|
|
350
|
-
|
|
351
|
-
assert.equal(getMilestone('M001'), null, 'milestone row removed from DB');
|
|
352
|
-
assert.equal(getMilestoneSlices('M001').length, 0, 'slice rows removed from DB');
|
|
353
|
-
assert.equal(getSliceTasks('M001', 'S01').length, 0, 'task rows removed from DB');
|
|
354
|
-
assert.ok(!existsSync(wt.path), 'worktree removed after discard');
|
|
355
|
-
assert.ok(!run('git branch', base).includes('milestone/M001'), 'milestone branch removed after discard');
|
|
356
|
-
} finally {
|
|
357
|
-
cleanup(base);
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
297
|
// ─── Test 12: All milestones parked → no active milestone ─────────────
|
|
362
298
|
test('All milestones parked → no active', async () => {
|
|
363
299
|
const base = createFixtureBase();
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* crash-log.ts — Write crash diagnostics to ~/.gsd/crash/<timestamp>.log
|
|
3
|
-
*
|
|
4
|
-
* Zero cross-dependencies: only uses Node.js built-ins so it can be imported
|
|
5
|
-
* safely from uncaughtException / unhandledRejection handlers and from tests
|
|
6
|
-
* without pulling in the full extension dependency tree.
|
|
7
|
-
*/
|
|
8
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
/**
|
|
12
|
-
* Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
|
|
13
|
-
* Never throws — must be safe to call from any error handler.
|
|
14
|
-
*/
|
|
15
|
-
export function writeCrashLog(err, source) {
|
|
16
|
-
try {
|
|
17
|
-
const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
|
|
18
|
-
mkdirSync(crashDir, { recursive: true });
|
|
19
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
20
|
-
const logPath = join(crashDir, `${ts}.log`);
|
|
21
|
-
const lines = [
|
|
22
|
-
`[gsd] ${source}: ${err.message}`,
|
|
23
|
-
`timestamp: ${new Date().toISOString()}`,
|
|
24
|
-
`pid: ${process.pid}`,
|
|
25
|
-
err.stack ?? "(no stack trace available)",
|
|
26
|
-
"",
|
|
27
|
-
];
|
|
28
|
-
appendFileSync(logPath, lines.join("\n"));
|
|
29
|
-
}
|
|
30
|
-
catch { /* never throw from crash handler */ }
|
|
31
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* crash-log.ts — Write crash diagnostics to ~/.gsd/crash/<timestamp>.log
|
|
3
|
-
*
|
|
4
|
-
* Zero cross-dependencies: only uses Node.js built-ins so it can be imported
|
|
5
|
-
* safely from uncaughtException / unhandledRejection handlers and from tests
|
|
6
|
-
* without pulling in the full extension dependency tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
|
|
15
|
-
* Never throws — must be safe to call from any error handler.
|
|
16
|
-
*/
|
|
17
|
-
export function writeCrashLog(err: Error, source: string): void {
|
|
18
|
-
try {
|
|
19
|
-
const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
|
|
20
|
-
mkdirSync(crashDir, { recursive: true });
|
|
21
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
22
|
-
const logPath = join(crashDir, `${ts}.log`);
|
|
23
|
-
const lines = [
|
|
24
|
-
`[gsd] ${source}: ${err.message}`,
|
|
25
|
-
`timestamp: ${new Date().toISOString()}`,
|
|
26
|
-
`pid: ${process.pid}`,
|
|
27
|
-
err.stack ?? "(no stack trace available)",
|
|
28
|
-
"",
|
|
29
|
-
];
|
|
30
|
-
appendFileSync(logPath, lines.join("\n"));
|
|
31
|
-
} catch { /* never throw from crash handler */ }
|
|
32
|
-
}
|