gsd-pi 2.45.0-dev.fdcf73c → 2.46.0-dev.5c2b8ed
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/resources/extensions/remote-questions/config.js +42 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
- 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 +19 -19
- 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/remote-questions.test.ts +84 -0
- 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/src/resources/extensions/remote-questions/config.ts +45 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → NtM_pwvGTaXTeqiByOv3A}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → NtM_pwvGTaXTeqiByOv3A}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// GSD — projection renderer regression tests
|
|
2
|
+
// Verifies that "done" vs "complete" status mismatch doesn't recur.
|
|
3
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
4
|
+
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
|
|
8
|
+
import { renderPlanContent, renderRoadmapContent } from '../workflow-projections.ts';
|
|
9
|
+
import type { SliceRow, TaskRow } from '../gsd-db.ts';
|
|
10
|
+
|
|
11
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeSliceRow(overrides?: Partial<SliceRow>): SliceRow {
|
|
14
|
+
return {
|
|
15
|
+
milestone_id: 'M001',
|
|
16
|
+
id: 'S01',
|
|
17
|
+
title: 'Test Slice',
|
|
18
|
+
status: 'pending',
|
|
19
|
+
risk: 'medium',
|
|
20
|
+
depends: [],
|
|
21
|
+
demo: 'Demo.',
|
|
22
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
23
|
+
completed_at: null,
|
|
24
|
+
full_summary_md: '',
|
|
25
|
+
full_uat_md: '',
|
|
26
|
+
goal: 'Test goal',
|
|
27
|
+
success_criteria: '',
|
|
28
|
+
proof_level: '',
|
|
29
|
+
integration_closure: '',
|
|
30
|
+
observability_impact: '',
|
|
31
|
+
sequence: 0,
|
|
32
|
+
replan_triggered_at: null,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeTaskRow(overrides?: Partial<TaskRow>): TaskRow {
|
|
38
|
+
return {
|
|
39
|
+
milestone_id: 'M001',
|
|
40
|
+
slice_id: 'S01',
|
|
41
|
+
id: 'T01',
|
|
42
|
+
title: 'Test Task',
|
|
43
|
+
status: 'pending',
|
|
44
|
+
one_liner: '',
|
|
45
|
+
narrative: '',
|
|
46
|
+
verification_result: '',
|
|
47
|
+
duration: '',
|
|
48
|
+
completed_at: null,
|
|
49
|
+
blocker_discovered: false,
|
|
50
|
+
deviations: '',
|
|
51
|
+
known_issues: '',
|
|
52
|
+
key_files: [],
|
|
53
|
+
key_decisions: [],
|
|
54
|
+
full_summary_md: '',
|
|
55
|
+
full_plan_md: '',
|
|
56
|
+
description: 'Test description',
|
|
57
|
+
estimate: '30m',
|
|
58
|
+
files: ['src/test.ts'],
|
|
59
|
+
verify: 'npm test',
|
|
60
|
+
inputs: [],
|
|
61
|
+
expected_output: [],
|
|
62
|
+
observability_impact: '',
|
|
63
|
+
sequence: 0,
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makeMilestoneRow() {
|
|
69
|
+
return {
|
|
70
|
+
id: 'M001',
|
|
71
|
+
title: 'Test Milestone',
|
|
72
|
+
status: 'active',
|
|
73
|
+
depends_on: [],
|
|
74
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
75
|
+
completed_at: null,
|
|
76
|
+
vision: 'Test vision',
|
|
77
|
+
success_criteria: [],
|
|
78
|
+
key_risks: [],
|
|
79
|
+
proof_strategy: [],
|
|
80
|
+
verification_contract: '',
|
|
81
|
+
verification_integration: '',
|
|
82
|
+
verification_operational: '',
|
|
83
|
+
verification_uat: '',
|
|
84
|
+
definition_of_done: [],
|
|
85
|
+
requirement_coverage: '',
|
|
86
|
+
boundary_map_markdown: '',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── renderPlanContent: checkbox regression ──────────────────────────────
|
|
91
|
+
|
|
92
|
+
test('renderPlanContent: task with status "complete" renders [x] checkbox', () => {
|
|
93
|
+
const slice = makeSliceRow();
|
|
94
|
+
const tasks = [makeTaskRow({ id: 'T01', status: 'complete', title: 'Completed Task' })];
|
|
95
|
+
|
|
96
|
+
const content = renderPlanContent(slice, tasks);
|
|
97
|
+
|
|
98
|
+
assert.match(content, /\[x\]\s+\*\*T01:/, 'complete task should have [x] checkbox');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('renderPlanContent: task with status "done" renders [x] checkbox', () => {
|
|
102
|
+
const slice = makeSliceRow();
|
|
103
|
+
const tasks = [makeTaskRow({ id: 'T01', status: 'done', title: 'Done Task' })];
|
|
104
|
+
|
|
105
|
+
const content = renderPlanContent(slice, tasks);
|
|
106
|
+
|
|
107
|
+
assert.match(content, /\[x\]\s+\*\*T01:/, 'done task should have [x] checkbox');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('renderPlanContent: task with status "pending" renders [ ] checkbox', () => {
|
|
111
|
+
const slice = makeSliceRow();
|
|
112
|
+
const tasks = [makeTaskRow({ id: 'T01', status: 'pending', title: 'Pending Task' })];
|
|
113
|
+
|
|
114
|
+
const content = renderPlanContent(slice, tasks);
|
|
115
|
+
|
|
116
|
+
assert.match(content, /\[ \]\s+\*\*T01:/, 'pending task should have [ ] checkbox');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('renderPlanContent: mixed statuses render correct checkboxes', () => {
|
|
120
|
+
const slice = makeSliceRow();
|
|
121
|
+
const tasks = [
|
|
122
|
+
makeTaskRow({ id: 'T01', status: 'complete', title: 'Done One' }),
|
|
123
|
+
makeTaskRow({ id: 'T02', status: 'pending', title: 'Pending One' }),
|
|
124
|
+
makeTaskRow({ id: 'T03', status: 'done', title: 'Done Two' }),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const content = renderPlanContent(slice, tasks);
|
|
128
|
+
|
|
129
|
+
assert.match(content, /\[x\]\s+\*\*T01:/, 'T01 (complete) should be checked');
|
|
130
|
+
assert.match(content, /\[ \]\s+\*\*T02:/, 'T02 (pending) should be unchecked');
|
|
131
|
+
assert.match(content, /\[x\]\s+\*\*T03:/, 'T03 (done) should be checked');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── renderPlanContent: format regression (parsePlan compatibility) ──────
|
|
135
|
+
|
|
136
|
+
test('renderPlanContent: format matches parsePlan regex **ID: title**', () => {
|
|
137
|
+
const slice = makeSliceRow();
|
|
138
|
+
const tasks = [makeTaskRow({ id: 'T01', status: 'pending', title: 'My Task' })];
|
|
139
|
+
|
|
140
|
+
const content = renderPlanContent(slice, tasks);
|
|
141
|
+
|
|
142
|
+
// parsePlan expects: **T01: My Task** (both ID and title inside bold)
|
|
143
|
+
// NOT: **T01:** My Task (only ID in bold)
|
|
144
|
+
assert.match(content, /\*\*T01: My Task\*\*/, 'ID and title should both be inside bold markers');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ─── renderRoadmapContent: status regression ─────────────────────────────
|
|
148
|
+
|
|
149
|
+
test('renderRoadmapContent: slice with status "complete" shows ✅', () => {
|
|
150
|
+
const milestone = makeMilestoneRow();
|
|
151
|
+
const slices = [makeSliceRow({ id: 'S01', status: 'complete' })];
|
|
152
|
+
|
|
153
|
+
const content = renderRoadmapContent(milestone, slices);
|
|
154
|
+
|
|
155
|
+
assert.ok(content.includes('✅'), 'complete slice should show ✅');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('renderRoadmapContent: slice with status "done" shows ✅', () => {
|
|
159
|
+
const milestone = makeMilestoneRow();
|
|
160
|
+
const slices = [makeSliceRow({ id: 'S01', status: 'done' })];
|
|
161
|
+
|
|
162
|
+
const content = renderRoadmapContent(milestone, slices);
|
|
163
|
+
|
|
164
|
+
assert.ok(content.includes('✅'), 'done slice should show ✅');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('renderRoadmapContent: slice with status "pending" shows ⬜', () => {
|
|
168
|
+
const milestone = makeMilestoneRow();
|
|
169
|
+
const slices = [makeSliceRow({ id: 'S01', status: 'pending' })];
|
|
170
|
+
|
|
171
|
+
const content = renderRoadmapContent(milestone, slices);
|
|
172
|
+
|
|
173
|
+
assert.ok(content.includes('⬜'), 'pending slice should show ⬜');
|
|
174
|
+
});
|
|
@@ -58,17 +58,18 @@ test("guided-resume-task prompt preserves recovery state until work is supersede
|
|
|
58
58
|
assert.doesNotMatch(prompt, /Delete the continue file after reading it/i);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
// ─── Prompt migration: execute-task →
|
|
61
|
+
// ─── Prompt migration: execute-task → gsd_complete_task ───────────────
|
|
62
62
|
|
|
63
|
-
test("execute-task prompt references
|
|
63
|
+
test("execute-task prompt references gsd_complete_task tool", () => {
|
|
64
64
|
const prompt = readPrompt("execute-task");
|
|
65
|
-
assert.match(prompt, /
|
|
65
|
+
assert.match(prompt, /gsd_complete_task/);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
test("execute-task prompt
|
|
68
|
+
test("execute-task prompt instructs writing task summary before tool call", () => {
|
|
69
69
|
const prompt = readPrompt("execute-task");
|
|
70
|
-
//
|
|
71
|
-
assert.
|
|
70
|
+
// The prompt instructs writing the summary file AND calling the tool
|
|
71
|
+
assert.match(prompt, /\{\{taskSummaryPath\}\}/);
|
|
72
|
+
assert.match(prompt, /gsd_complete_task/);
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
test("execute-task prompt does not instruct LLM to toggle checkboxes manually", () => {
|
|
@@ -93,12 +94,11 @@ test("guided-execute-task prompt does not instruct manual file write", () => {
|
|
|
93
94
|
assert.doesNotMatch(prompt, /Write `?\{\{taskId\}\}-SUMMARY\.md`?.*mark it done/i);
|
|
94
95
|
});
|
|
95
96
|
|
|
96
|
-
// ─── Prompt migration: complete-slice →
|
|
97
|
-
// These tests are for T02 — expected to fail until that task runs.
|
|
97
|
+
// ─── Prompt migration: complete-slice → gsd_complete_slice ────────────
|
|
98
98
|
|
|
99
|
-
test("complete-slice prompt references
|
|
99
|
+
test("complete-slice prompt references gsd_complete_slice tool", () => {
|
|
100
100
|
const prompt = readPrompt("complete-slice");
|
|
101
|
-
assert.match(prompt, /
|
|
101
|
+
assert.match(prompt, /gsd_complete_slice/);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
test("complete-slice prompt does not instruct LLM to toggle checkboxes manually", () => {
|
|
@@ -111,10 +111,12 @@ test("guided-complete-slice prompt references gsd_slice_complete tool", () => {
|
|
|
111
111
|
assert.match(prompt, /gsd_slice_complete/);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
test("complete-slice prompt
|
|
114
|
+
test("complete-slice prompt instructs writing summary and UAT files before tool call", () => {
|
|
115
115
|
const prompt = readPrompt("complete-slice");
|
|
116
|
-
|
|
117
|
-
assert.
|
|
116
|
+
// The prompt instructs writing the summary AND UAT files, then calling the tool
|
|
117
|
+
assert.match(prompt, /\{\{sliceSummaryPath\}\}/);
|
|
118
|
+
assert.match(prompt, /\{\{sliceUatPath\}\}/);
|
|
119
|
+
assert.match(prompt, /gsd_complete_slice/);
|
|
118
120
|
});
|
|
119
121
|
|
|
120
122
|
test("complete-slice prompt preserves decisions and knowledge review steps", () => {
|
|
@@ -127,7 +129,6 @@ test("complete-slice prompt still contains template variables for context", () =
|
|
|
127
129
|
const prompt = readPrompt("complete-slice");
|
|
128
130
|
assert.match(prompt, /\{\{sliceSummaryPath\}\}/);
|
|
129
131
|
assert.match(prompt, /\{\{sliceUatPath\}\}/);
|
|
130
|
-
assert.match(prompt, /\{\{roadmapPath\}\}/);
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
test("plan-milestone prompt references DB-backed planning tool and explicitly forbids manual roadmap writes", () => {
|
|
@@ -640,3 +640,87 @@ test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
|
|
|
640
640
|
"sendPrompt should set threadUrl to the constructed message URL",
|
|
641
641
|
);
|
|
642
642
|
});
|
|
643
|
+
|
|
644
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
645
|
+
// Auth.json Token Hydration Tests
|
|
646
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
647
|
+
|
|
648
|
+
test("config source-level: hydrateRemoteTokensFromAuth is called before env check in resolveRemoteConfig", () => {
|
|
649
|
+
const configSrc = readFileSync(
|
|
650
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
651
|
+
"utf-8",
|
|
652
|
+
);
|
|
653
|
+
// Find the body of resolveRemoteConfig by slicing from its declaration to the next export function.
|
|
654
|
+
const resolveStart = configSrc.indexOf("export function resolveRemoteConfig()");
|
|
655
|
+
const resolveEnd = configSrc.indexOf("\nexport function", resolveStart + 1);
|
|
656
|
+
const resolveFnBody = configSrc.slice(resolveStart, resolveEnd);
|
|
657
|
+
|
|
658
|
+
const hydrationIdx = resolveFnBody.indexOf("hydrateRemoteTokensFromAuth()");
|
|
659
|
+
const envCheckIdx = resolveFnBody.indexOf("process.env[ENV_KEYS[");
|
|
660
|
+
assert.ok(hydrationIdx !== -1, "hydrateRemoteTokensFromAuth() should be called inside resolveRemoteConfig");
|
|
661
|
+
assert.ok(envCheckIdx !== -1, "process.env[ENV_KEYS[ lookup should exist inside resolveRemoteConfig");
|
|
662
|
+
assert.ok(hydrationIdx < envCheckIdx, "hydration call should appear before the process.env env-key lookup");
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("config source-level: hydrateRemoteTokensFromAuth is called in getRemoteConfigStatus", () => {
|
|
666
|
+
const configSrc = readFileSync(
|
|
667
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
668
|
+
"utf-8",
|
|
669
|
+
);
|
|
670
|
+
const statusFnIdx = configSrc.indexOf("export function getRemoteConfigStatus()");
|
|
671
|
+
const hydrationInStatus = configSrc.indexOf("hydrateRemoteTokensFromAuth()", statusFnIdx);
|
|
672
|
+
assert.ok(hydrationInStatus > statusFnIdx, "hydrateRemoteTokensFromAuth should be called inside getRemoteConfigStatus");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("config source-level: AUTH_PROVIDER_ENV_MAP covers all three remote channels", () => {
|
|
676
|
+
const configSrc = readFileSync(
|
|
677
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
678
|
+
"utf-8",
|
|
679
|
+
);
|
|
680
|
+
assert.ok(configSrc.includes("discord_bot"), "AUTH_PROVIDER_ENV_MAP should include discord_bot");
|
|
681
|
+
assert.ok(configSrc.includes("slack_bot"), "AUTH_PROVIDER_ENV_MAP should include slack_bot");
|
|
682
|
+
assert.ok(configSrc.includes("telegram_bot"), "AUTH_PROVIDER_ENV_MAP should include telegram_bot");
|
|
683
|
+
assert.ok(configSrc.includes("DISCORD_BOT_TOKEN"), "should map discord_bot to DISCORD_BOT_TOKEN");
|
|
684
|
+
assert.ok(configSrc.includes("SLACK_BOT_TOKEN"), "should map slack_bot to SLACK_BOT_TOKEN");
|
|
685
|
+
assert.ok(configSrc.includes("TELEGRAM_BOT_TOKEN"), "should map telegram_bot to TELEGRAM_BOT_TOKEN");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("config source-level: hydration skips env vars already set", () => {
|
|
689
|
+
const configSrc = readFileSync(
|
|
690
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
691
|
+
"utf-8",
|
|
692
|
+
);
|
|
693
|
+
// The guard that skips already-set vars must be present.
|
|
694
|
+
assert.ok(
|
|
695
|
+
configSrc.includes("!process.env[envVar]"),
|
|
696
|
+
"hydrateRemoteTokensFromAuth should skip env vars that are already populated",
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("resolveRemoteConfig returns null when preferences are absent (no env side-effects)", () => {
|
|
701
|
+
// Guard: ensure that with no prefs configured, resolveRemoteConfig returns null cleanly.
|
|
702
|
+
// This exercises the hydration path without auth.json present (it should no-op silently).
|
|
703
|
+
const savedHome = process.env.HOME;
|
|
704
|
+
const savedUserProfile = process.env.USERPROFILE;
|
|
705
|
+
const savedDiscord = process.env.DISCORD_BOT_TOKEN;
|
|
706
|
+
const savedSlack = process.env.SLACK_BOT_TOKEN;
|
|
707
|
+
const savedTelegram = process.env.TELEGRAM_BOT_TOKEN;
|
|
708
|
+
try {
|
|
709
|
+
// Point HOME to a nonexistent dir so auth.json lookup finds nothing.
|
|
710
|
+
process.env.HOME = "/tmp/gsd-no-such-home-for-test";
|
|
711
|
+
process.env.USERPROFILE = "/tmp/gsd-no-such-home-for-test";
|
|
712
|
+
delete process.env.DISCORD_BOT_TOKEN;
|
|
713
|
+
delete process.env.SLACK_BOT_TOKEN;
|
|
714
|
+
delete process.env.TELEGRAM_BOT_TOKEN;
|
|
715
|
+
|
|
716
|
+
const result = resolveRemoteConfig();
|
|
717
|
+
// With no prefs file, result is null — not an exception.
|
|
718
|
+
assert.equal(result, null, "resolveRemoteConfig should return null when no preferences are configured");
|
|
719
|
+
} finally {
|
|
720
|
+
process.env.HOME = savedHome;
|
|
721
|
+
process.env.USERPROFILE = savedUserProfile;
|
|
722
|
+
if (savedDiscord !== undefined) process.env.DISCORD_BOT_TOKEN = savedDiscord;
|
|
723
|
+
if (savedSlack !== undefined) process.env.SLACK_BOT_TOKEN = savedSlack;
|
|
724
|
+
if (savedTelegram !== undefined) process.env.TELEGRAM_BOT_TOKEN = savedTelegram;
|
|
725
|
+
}
|
|
726
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// GSD — reopen-slice handler 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, mkdirSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
openDatabase,
|
|
12
|
+
closeDatabase,
|
|
13
|
+
insertMilestone,
|
|
14
|
+
insertSlice,
|
|
15
|
+
insertTask,
|
|
16
|
+
getSlice,
|
|
17
|
+
getSliceTasks,
|
|
18
|
+
} from '../gsd-db.ts';
|
|
19
|
+
import { handleReopenSlice } from '../tools/reopen-slice.ts';
|
|
20
|
+
|
|
21
|
+
function makeTmpBase(): string {
|
|
22
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-reopen-slice-'));
|
|
23
|
+
mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
|
|
24
|
+
return base;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanup(base: string): void {
|
|
28
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
29
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function seedCompleteSlice(): void {
|
|
33
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
|
|
34
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'complete' });
|
|
35
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Task One', status: 'complete' });
|
|
36
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Task Two', status: 'complete' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Success path ────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
test('handleReopenSlice: resets a complete slice to in_progress and all tasks to pending', async () => {
|
|
42
|
+
const base = makeTmpBase();
|
|
43
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
44
|
+
try {
|
|
45
|
+
seedCompleteSlice();
|
|
46
|
+
|
|
47
|
+
const result = await handleReopenSlice({
|
|
48
|
+
milestoneId: 'M001',
|
|
49
|
+
sliceId: 'S01',
|
|
50
|
+
reason: 'need to redo after requirements change',
|
|
51
|
+
}, base);
|
|
52
|
+
|
|
53
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
54
|
+
assert.equal(result.sliceId, 'S01');
|
|
55
|
+
assert.equal(result.tasksReset, 2, 'should report 2 tasks reset');
|
|
56
|
+
|
|
57
|
+
const slice = getSlice('M001', 'S01');
|
|
58
|
+
assert.ok(slice, 'slice should still exist');
|
|
59
|
+
assert.equal(slice!.status, 'in_progress', 'slice status should be in_progress');
|
|
60
|
+
|
|
61
|
+
const tasks = getSliceTasks('M001', 'S01');
|
|
62
|
+
assert.equal(tasks.length, 2, 'both tasks should still exist');
|
|
63
|
+
assert.ok(tasks.every(t => t.status === 'pending'), 'all tasks should be pending');
|
|
64
|
+
} finally {
|
|
65
|
+
cleanup(base);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('handleReopenSlice: works with a single task', async () => {
|
|
70
|
+
const base = makeTmpBase();
|
|
71
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
72
|
+
try {
|
|
73
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
74
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
75
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
76
|
+
|
|
77
|
+
const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: 'S01' }, base);
|
|
78
|
+
|
|
79
|
+
assert.ok(!('error' in result));
|
|
80
|
+
assert.equal(result.tasksReset, 1);
|
|
81
|
+
} finally {
|
|
82
|
+
cleanup(base);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── Failure paths ───────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
test('handleReopenSlice: rejects empty sliceId', async () => {
|
|
89
|
+
const base = makeTmpBase();
|
|
90
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
91
|
+
try {
|
|
92
|
+
const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: '' }, base);
|
|
93
|
+
assert.ok('error' in result);
|
|
94
|
+
assert.match(result.error, /sliceId/);
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(base);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('handleReopenSlice: rejects non-existent milestone', async () => {
|
|
101
|
+
const base = makeTmpBase();
|
|
102
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
103
|
+
try {
|
|
104
|
+
const result = await handleReopenSlice({ milestoneId: 'M999', sliceId: 'S01' }, base);
|
|
105
|
+
assert.ok('error' in result);
|
|
106
|
+
assert.match(result.error, /milestone not found/);
|
|
107
|
+
} finally {
|
|
108
|
+
cleanup(base);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('handleReopenSlice: rejects slice in a closed milestone', async () => {
|
|
113
|
+
const base = makeTmpBase();
|
|
114
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
115
|
+
try {
|
|
116
|
+
insertMilestone({ id: 'M001', title: 'Done', status: 'complete' });
|
|
117
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
118
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
119
|
+
|
|
120
|
+
const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: 'S01' }, base);
|
|
121
|
+
assert.ok('error' in result);
|
|
122
|
+
assert.match(result.error, /closed milestone/);
|
|
123
|
+
} finally {
|
|
124
|
+
cleanup(base);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('handleReopenSlice: rejects reopening a slice that is not complete', async () => {
|
|
129
|
+
const base = makeTmpBase();
|
|
130
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
131
|
+
try {
|
|
132
|
+
insertMilestone({ id: 'M001', title: 'Active', status: 'active' });
|
|
133
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', status: 'in_progress' });
|
|
134
|
+
|
|
135
|
+
const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: 'S01' }, base);
|
|
136
|
+
assert.ok('error' in result);
|
|
137
|
+
assert.match(result.error, /not complete/);
|
|
138
|
+
} finally {
|
|
139
|
+
cleanup(base);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('handleReopenSlice: rejects non-existent slice', async () => {
|
|
144
|
+
const base = makeTmpBase();
|
|
145
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
146
|
+
try {
|
|
147
|
+
insertMilestone({ id: 'M001', title: 'Active', status: 'active' });
|
|
148
|
+
|
|
149
|
+
const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: 'S99' }, base);
|
|
150
|
+
assert.ok('error' in result);
|
|
151
|
+
assert.match(result.error, /slice not found/);
|
|
152
|
+
} finally {
|
|
153
|
+
cleanup(base);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// GSD — reopen-task handler 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, mkdirSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
openDatabase,
|
|
12
|
+
closeDatabase,
|
|
13
|
+
insertMilestone,
|
|
14
|
+
insertSlice,
|
|
15
|
+
insertTask,
|
|
16
|
+
getTask,
|
|
17
|
+
} from '../gsd-db.ts';
|
|
18
|
+
import { handleReopenTask } from '../tools/reopen-task.ts';
|
|
19
|
+
|
|
20
|
+
function makeTmpBase(): string {
|
|
21
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-reopen-task-'));
|
|
22
|
+
mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cleanup(base: string): void {
|
|
27
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
28
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function seedCompleteTask(): void {
|
|
32
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
|
|
33
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'in_progress' });
|
|
34
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Task One', status: 'complete' });
|
|
35
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Task Two', status: 'pending' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Success path ────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
test('handleReopenTask: resets a complete task to pending', async () => {
|
|
41
|
+
const base = makeTmpBase();
|
|
42
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
43
|
+
try {
|
|
44
|
+
seedCompleteTask();
|
|
45
|
+
|
|
46
|
+
const result = await handleReopenTask({
|
|
47
|
+
milestoneId: 'M001',
|
|
48
|
+
sliceId: 'S01',
|
|
49
|
+
taskId: 'T01',
|
|
50
|
+
reason: 'verification failed after merge',
|
|
51
|
+
}, base);
|
|
52
|
+
|
|
53
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
54
|
+
assert.equal(result.taskId, 'T01');
|
|
55
|
+
|
|
56
|
+
const task = getTask('M001', 'S01', 'T01');
|
|
57
|
+
assert.ok(task, 'task should still exist');
|
|
58
|
+
assert.equal(task!.status, 'pending', 'task status should be reset to pending');
|
|
59
|
+
} finally {
|
|
60
|
+
cleanup(base);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('handleReopenTask: does not affect other tasks in the slice', async () => {
|
|
65
|
+
const base = makeTmpBase();
|
|
66
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
67
|
+
try {
|
|
68
|
+
seedCompleteTask();
|
|
69
|
+
|
|
70
|
+
await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T01' }, base);
|
|
71
|
+
|
|
72
|
+
const t02 = getTask('M001', 'S01', 'T02');
|
|
73
|
+
assert.ok(t02, 'T02 should still exist');
|
|
74
|
+
assert.equal(t02!.status, 'pending', 'T02 status should be unchanged');
|
|
75
|
+
} finally {
|
|
76
|
+
cleanup(base);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ─── Failure paths ───────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
test('handleReopenTask: rejects empty taskId', async () => {
|
|
83
|
+
const base = makeTmpBase();
|
|
84
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
85
|
+
try {
|
|
86
|
+
const result = await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: '' }, base);
|
|
87
|
+
assert.ok('error' in result);
|
|
88
|
+
assert.match(result.error, /taskId/);
|
|
89
|
+
} finally {
|
|
90
|
+
cleanup(base);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('handleReopenTask: rejects non-existent milestone', async () => {
|
|
95
|
+
const base = makeTmpBase();
|
|
96
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
97
|
+
try {
|
|
98
|
+
const result = await handleReopenTask({ milestoneId: 'M999', sliceId: 'S01', taskId: 'T01' }, base);
|
|
99
|
+
assert.ok('error' in result);
|
|
100
|
+
assert.match(result.error, /milestone not found/);
|
|
101
|
+
} finally {
|
|
102
|
+
cleanup(base);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('handleReopenTask: rejects task in a closed milestone', async () => {
|
|
107
|
+
const base = makeTmpBase();
|
|
108
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
109
|
+
try {
|
|
110
|
+
insertMilestone({ id: 'M001', title: 'Done', status: 'complete' });
|
|
111
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
112
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
113
|
+
|
|
114
|
+
const result = await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T01' }, base);
|
|
115
|
+
assert.ok('error' in result);
|
|
116
|
+
assert.match(result.error, /closed milestone/);
|
|
117
|
+
} finally {
|
|
118
|
+
cleanup(base);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('handleReopenTask: rejects task inside a closed slice', async () => {
|
|
123
|
+
const base = makeTmpBase();
|
|
124
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
125
|
+
try {
|
|
126
|
+
insertMilestone({ id: 'M001', title: 'Active', status: 'active' });
|
|
127
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
128
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
|
|
129
|
+
|
|
130
|
+
const result = await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T01' }, base);
|
|
131
|
+
assert.ok('error' in result);
|
|
132
|
+
assert.match(result.error, /closed slice/);
|
|
133
|
+
} finally {
|
|
134
|
+
cleanup(base);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('handleReopenTask: rejects reopening a task that is not complete', async () => {
|
|
139
|
+
const base = makeTmpBase();
|
|
140
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
141
|
+
try {
|
|
142
|
+
seedCompleteTask();
|
|
143
|
+
|
|
144
|
+
const result = await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T02' }, base);
|
|
145
|
+
assert.ok('error' in result);
|
|
146
|
+
assert.match(result.error, /not complete/);
|
|
147
|
+
} finally {
|
|
148
|
+
cleanup(base);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('handleReopenTask: rejects non-existent task', async () => {
|
|
153
|
+
const base = makeTmpBase();
|
|
154
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
155
|
+
try {
|
|
156
|
+
insertMilestone({ id: 'M001', title: 'Active', status: 'active' });
|
|
157
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', status: 'in_progress' });
|
|
158
|
+
|
|
159
|
+
const result = await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T99' }, base);
|
|
160
|
+
assert.ok('error' in result);
|
|
161
|
+
assert.match(result.error, /task not found/);
|
|
162
|
+
} finally {
|
|
163
|
+
cleanup(base);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
@@ -103,7 +103,7 @@ describe('session-lock-regression', async () => {
|
|
|
103
103
|
try {
|
|
104
104
|
acquireSessionLock(base);
|
|
105
105
|
|
|
106
|
-
updateSessionLock(base, 'execute-task', 'M001/S01/T01',
|
|
106
|
+
updateSessionLock(base, 'execute-task', 'M001/S01/T01', '/tmp/session.json');
|
|
107
107
|
|
|
108
108
|
const data = readSessionLockData(base);
|
|
109
109
|
assert.ok(data !== null, 'lock data readable after update');
|
|
@@ -111,7 +111,6 @@ describe('session-lock-regression', async () => {
|
|
|
111
111
|
assert.deepStrictEqual(data.pid, process.pid, 'lock data has correct PID');
|
|
112
112
|
assert.deepStrictEqual(data.unitType, 'execute-task', 'lock data has correct unit type');
|
|
113
113
|
assert.deepStrictEqual(data.unitId, 'M001/S01/T01', 'lock data has correct unit ID');
|
|
114
|
-
assert.deepStrictEqual(data.completedUnits, 5, 'lock data has correct completed count');
|
|
115
114
|
assert.deepStrictEqual(data.sessionFile, '/tmp/session.json', 'lock data has session file');
|
|
116
115
|
}
|
|
117
116
|
|
|
@@ -136,7 +135,6 @@ describe('session-lock-regression', async () => {
|
|
|
136
135
|
unitType: 'execute-task',
|
|
137
136
|
unitId: 'M001/S01/T01',
|
|
138
137
|
unitStartedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
139
|
-
completedUnits: 3,
|
|
140
138
|
};
|
|
141
139
|
writeFileSync(lockFile, JSON.stringify(staleLock, null, 2));
|
|
142
140
|
|
|
@@ -233,7 +231,6 @@ describe('session-lock-regression', async () => {
|
|
|
233
231
|
unitType: 'execute-task',
|
|
234
232
|
unitId: 'M001/S01/T01',
|
|
235
233
|
unitStartedAt: new Date().toISOString(),
|
|
236
|
-
completedUnits: 0,
|
|
237
234
|
}, null, 2));
|
|
238
235
|
|
|
239
236
|
const status = getSessionLockStatus(base);
|