gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5
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/README.md +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +73 -30
- package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
- package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
- package/dist/resources/extensions/gsd/auto-start.js +3 -3
- package/dist/resources/extensions/gsd/auto-verification.js +17 -4
- package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
- package/dist/resources/extensions/gsd/auto.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
- package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
- package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
- package/dist/resources/extensions/gsd/git-service.js +6 -2
- package/dist/resources/extensions/gsd/gsd-db.js +20 -6
- package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
- package/dist/resources/extensions/gsd/guided-flow.js +95 -116
- package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
- package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
- package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
- package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
- package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
- package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
- package/dist/resources/extensions/gsd/status-guards.js +7 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- 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/api/browse-directories/route.js +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/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/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
- package/packages/pi-tui/dist/terminal.d.ts +2 -0
- package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal.js +12 -0
- package/packages/pi-tui/dist/terminal.js.map +1 -1
- package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
- package/packages/pi-tui/src/terminal.ts +11 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
- package/src/resources/extensions/gsd/auto/phases.ts +83 -37
- package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
- package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
- package/src/resources/extensions/gsd/auto-start.ts +2 -3
- package/src/resources/extensions/gsd/auto-verification.ts +22 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
- package/src/resources/extensions/gsd/auto.ts +8 -2
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
- package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
- package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
- package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/git-service.ts +6 -3
- package/src/resources/extensions/gsd/gsd-db.ts +18 -6
- package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
- package/src/resources/extensions/gsd/guided-flow.ts +128 -133
- package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
- package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
- package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
- package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -4
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
- package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
- package/src/resources/extensions/gsd/status-guards.ts +8 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
- package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
- package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
- package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
- /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Regression tests for DB authority over markdown projections.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
closeDatabase,
|
|
12
|
+
getAllMilestones,
|
|
13
|
+
getSliceTasks,
|
|
14
|
+
insertMilestone,
|
|
15
|
+
insertRequirement,
|
|
16
|
+
insertSlice,
|
|
17
|
+
openDatabase,
|
|
18
|
+
} from "../gsd-db.ts";
|
|
19
|
+
import { migrateHierarchyToDb } from "../md-importer.ts";
|
|
20
|
+
import { checkMarkdownHierarchyAgainstDb } from "../migration-auto-check.ts";
|
|
21
|
+
import { queryDecisions } from "../context-store.ts";
|
|
22
|
+
import { deriveStateFromDb, invalidateStateCache } from "../state.ts";
|
|
23
|
+
import type { Requirement } from "../types.ts";
|
|
24
|
+
|
|
25
|
+
function makeBase(prefix = "gsd-db-authority-"): string {
|
|
26
|
+
const base = mkdtempSync(join(tmpdir(), prefix));
|
|
27
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
28
|
+
return base;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanup(base: string): void {
|
|
32
|
+
try {
|
|
33
|
+
closeDatabase();
|
|
34
|
+
} catch {
|
|
35
|
+
/* noop */
|
|
36
|
+
}
|
|
37
|
+
rmSync(base, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function openProjectDb(base: string): void {
|
|
41
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function activeRequirement(id: string): Requirement {
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
class: "functional",
|
|
48
|
+
status: "active",
|
|
49
|
+
description: `${id} from DB`,
|
|
50
|
+
why: "DB authority regression fixture",
|
|
51
|
+
source: "test",
|
|
52
|
+
primary_owner: "M999/S01",
|
|
53
|
+
supporting_slices: "",
|
|
54
|
+
validation: "derive state",
|
|
55
|
+
notes: "",
|
|
56
|
+
full_content: `${id} from DB`,
|
|
57
|
+
superseded_by: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("DB authority: PROJECT.md and QUEUE-ORDER projections do not choose runtime milestone", async (t) => {
|
|
62
|
+
const base = makeBase();
|
|
63
|
+
t.after(() => cleanup(base));
|
|
64
|
+
|
|
65
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(base, ".gsd", "PROJECT.md"),
|
|
68
|
+
[
|
|
69
|
+
"# Projection Project",
|
|
70
|
+
"",
|
|
71
|
+
"## Milestone Sequence",
|
|
72
|
+
"- [ ] M001: Projection Only -- should not become active",
|
|
73
|
+
"",
|
|
74
|
+
].join("\n"),
|
|
75
|
+
);
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(base, ".gsd", "QUEUE-ORDER.json"),
|
|
78
|
+
JSON.stringify({ order: ["M001", "M999"], updatedAt: new Date().toISOString() }),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
openProjectDb(base);
|
|
82
|
+
insertMilestone({ id: "M999", title: "DB Milestone", status: "active" });
|
|
83
|
+
insertSlice({ id: "S01", milestoneId: "M999", title: "DB Slice", status: "pending", risk: "low", depends: [], demo: "DB demo", sequence: 1 });
|
|
84
|
+
|
|
85
|
+
invalidateStateCache();
|
|
86
|
+
const state = await deriveStateFromDb(base);
|
|
87
|
+
|
|
88
|
+
assert.equal(state.activeMilestone?.id, "M999");
|
|
89
|
+
assert.equal(state.registry.some((entry) => entry.id === "M001"), false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("DB authority: REQUIREMENTS.md and DECISIONS.md projections do not populate DB reads", async (t) => {
|
|
93
|
+
const base = makeBase();
|
|
94
|
+
t.after(() => cleanup(base));
|
|
95
|
+
|
|
96
|
+
writeFileSync(
|
|
97
|
+
join(base, ".gsd", "REQUIREMENTS.md"),
|
|
98
|
+
[
|
|
99
|
+
"# Requirements",
|
|
100
|
+
"",
|
|
101
|
+
"## Active",
|
|
102
|
+
"### R001 - Projection-only requirement",
|
|
103
|
+
"- Class: functional",
|
|
104
|
+
"- Status: active",
|
|
105
|
+
"",
|
|
106
|
+
].join("\n"),
|
|
107
|
+
);
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(base, ".gsd", "DECISIONS.md"),
|
|
110
|
+
[
|
|
111
|
+
"# Decisions",
|
|
112
|
+
"",
|
|
113
|
+
"| # | When / Context | Scope | Decision | Choice | Rationale | Revisable | Made By |",
|
|
114
|
+
"|---|----------------|-------|----------|--------|-----------|----------|---------|",
|
|
115
|
+
"| D001 | Now | global | Projection-only decision | Ignore | DB is authority | Yes | human |",
|
|
116
|
+
"",
|
|
117
|
+
].join("\n"),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
openProjectDb(base);
|
|
121
|
+
insertMilestone({ id: "M999", title: "DB Milestone", status: "active" });
|
|
122
|
+
|
|
123
|
+
invalidateStateCache();
|
|
124
|
+
const state = await deriveStateFromDb(base);
|
|
125
|
+
|
|
126
|
+
assert.deepEqual(state.requirements, {
|
|
127
|
+
active: 0,
|
|
128
|
+
validated: 0,
|
|
129
|
+
deferred: 0,
|
|
130
|
+
outOfScope: 0,
|
|
131
|
+
blocked: 0,
|
|
132
|
+
total: 0,
|
|
133
|
+
});
|
|
134
|
+
assert.deepEqual(queryDecisions(), []);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("DB authority: DB requirements remain canonical when REQUIREMENTS.md disagrees", async (t) => {
|
|
138
|
+
const base = makeBase();
|
|
139
|
+
t.after(() => cleanup(base));
|
|
140
|
+
|
|
141
|
+
writeFileSync(
|
|
142
|
+
join(base, ".gsd", "REQUIREMENTS.md"),
|
|
143
|
+
[
|
|
144
|
+
"# Requirements",
|
|
145
|
+
"",
|
|
146
|
+
"## Active",
|
|
147
|
+
"### R999 - Projection-only requirement",
|
|
148
|
+
"- Class: functional",
|
|
149
|
+
"- Status: active",
|
|
150
|
+
"",
|
|
151
|
+
].join("\n"),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
openProjectDb(base);
|
|
155
|
+
insertMilestone({ id: "M999", title: "DB Milestone", status: "active" });
|
|
156
|
+
insertRequirement(activeRequirement("R001"));
|
|
157
|
+
|
|
158
|
+
invalidateStateCache();
|
|
159
|
+
const state = await deriveStateFromDb(base);
|
|
160
|
+
|
|
161
|
+
assert.ok(state.requirements);
|
|
162
|
+
assert.equal(state.requirements.active, 1);
|
|
163
|
+
assert.equal(state.requirements.total, 1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("explicit markdown import remains opt-in and is not run by startup mismatch check", async (t) => {
|
|
167
|
+
const base = makeBase();
|
|
168
|
+
t.after(() => cleanup(base));
|
|
169
|
+
|
|
170
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
171
|
+
const sliceDir = join(milestoneDir, "slices", "S01");
|
|
172
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
173
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
174
|
+
writeFileSync(
|
|
175
|
+
join(milestoneDir, "M001-ROADMAP.md"),
|
|
176
|
+
[
|
|
177
|
+
"# M001: Imported Explicitly",
|
|
178
|
+
"",
|
|
179
|
+
"**Vision:** Explicit recovery import only",
|
|
180
|
+
"",
|
|
181
|
+
"## Slices",
|
|
182
|
+
"- [ ] **S01: Slice** `risk:low` `depends:[]`",
|
|
183
|
+
"",
|
|
184
|
+
].join("\n"),
|
|
185
|
+
);
|
|
186
|
+
writeFileSync(
|
|
187
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
188
|
+
[
|
|
189
|
+
"# S01: Slice",
|
|
190
|
+
"",
|
|
191
|
+
"**Goal:** prove explicit import",
|
|
192
|
+
"",
|
|
193
|
+
"## Tasks",
|
|
194
|
+
"- [ ] **T01: Task** `est:5m`",
|
|
195
|
+
"",
|
|
196
|
+
].join("\n"),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
openProjectDb(base);
|
|
200
|
+
const check = await checkMarkdownHierarchyAgainstDb(base);
|
|
201
|
+
assert.equal(check.action, "recovery-required");
|
|
202
|
+
assert.equal(getAllMilestones().length, 0, "startup mismatch check must not import markdown");
|
|
203
|
+
|
|
204
|
+
const imported = migrateHierarchyToDb(base);
|
|
205
|
+
assert.deepEqual(imported, { milestones: 1, slices: 1, tasks: 1 });
|
|
206
|
+
assert.equal(getAllMilestones().length, 1);
|
|
207
|
+
assert.equal(getSliceTasks("M001", "S01").length, 1);
|
|
208
|
+
});
|
|
@@ -10,6 +10,7 @@ import assert from "node:assert/strict";
|
|
|
10
10
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
13
14
|
|
|
14
15
|
import { DISPATCH_RULES, type DispatchContext } from "../auto-dispatch.ts";
|
|
15
16
|
import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
|
|
@@ -23,6 +24,14 @@ function makeBase(): string {
|
|
|
23
24
|
return base;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function initGitRepo(base: string): void {
|
|
28
|
+
execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
|
|
29
|
+
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base, stdio: "ignore" });
|
|
30
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: base, stdio: "ignore" });
|
|
31
|
+
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
32
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd: base, stdio: "ignore" });
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
function buildDispatchCtx(basePath: string): DispatchContext {
|
|
27
36
|
return {
|
|
28
37
|
basePath,
|
|
@@ -76,6 +85,24 @@ describe("completing-milestone dispatch guard (#4324)", () => {
|
|
|
76
85
|
assert.equal(result?.unitType, "complete-milestone");
|
|
77
86
|
assert.equal(result?.unitId, "M001");
|
|
78
87
|
});
|
|
88
|
+
|
|
89
|
+
test("dispatches complete-milestone when only .gsd/ files exist in git history (#5097)", async () => {
|
|
90
|
+
base = makeBase();
|
|
91
|
+
rmSync(join(base, "implementation.txt"), { force: true });
|
|
92
|
+
initGitRepo(base);
|
|
93
|
+
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\n");
|
|
94
|
+
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
95
|
+
execFileSync("git", ["commit", "-m", "chore: planning artifacts only"], { cwd: base, stdio: "ignore" });
|
|
96
|
+
|
|
97
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
98
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
99
|
+
|
|
100
|
+
const result = await rule.match(buildDispatchCtx(base));
|
|
101
|
+
|
|
102
|
+
assert.equal(result?.action, "dispatch");
|
|
103
|
+
assert.equal(result?.unitType, "complete-milestone");
|
|
104
|
+
assert.equal(result?.unitId, "M001");
|
|
105
|
+
});
|
|
79
106
|
});
|
|
80
107
|
|
|
81
108
|
describe("complete phase dispatch guard (#5683)", () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Regression tests for doctor repair of empty milestone worktrees.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { runGSDDoctor } from "../doctor.ts";
|
|
12
|
+
import { createWorktree, worktreePath } from "../worktree-manager.ts";
|
|
13
|
+
|
|
14
|
+
function runGit(args: string[], cwd: string): string {
|
|
15
|
+
return execFileSync("git", args, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeRepo(): string {
|
|
23
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-empty-worktree-"));
|
|
24
|
+
runGit(["init", "-b", "main"], base);
|
|
25
|
+
runGit(["config", "user.name", "Test User"], base);
|
|
26
|
+
runGit(["config", "user.email", "test@example.com"], base);
|
|
27
|
+
writeFileSync(join(base, "package.json"), "{\"scripts\":{}}\n", "utf-8");
|
|
28
|
+
runGit(["add", "."], base);
|
|
29
|
+
runGit(["commit", "-m", "chore: init"], base);
|
|
30
|
+
return base;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("doctor fix recreates an empty registered milestone worktree", async (t) => {
|
|
34
|
+
const base = makeRepo();
|
|
35
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
36
|
+
|
|
37
|
+
createWorktree(base, "M001", { branch: "milestone/M001" });
|
|
38
|
+
const wtPath = worktreePath(base, "M001");
|
|
39
|
+
writeFileSync(join(wtPath, "milestone-note.txt"), "worktree branch content\n", "utf-8");
|
|
40
|
+
runGit(["add", "milestone-note.txt"], wtPath);
|
|
41
|
+
runGit(["commit", "-m", "test: add milestone content"], wtPath);
|
|
42
|
+
for (const entry of readdirSync(wtPath)) {
|
|
43
|
+
if (entry === ".git") continue;
|
|
44
|
+
rmSync(join(wtPath, entry), { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
assert.ok(existsSync(join(wtPath, ".git")), "test setup keeps registered worktree marker");
|
|
47
|
+
assert.equal(existsSync(join(wtPath, "package.json")), false, "test setup removes project content");
|
|
48
|
+
|
|
49
|
+
const report = await runGSDDoctor(base, {
|
|
50
|
+
fix: true,
|
|
51
|
+
fixLevel: "all",
|
|
52
|
+
isolationMode: "worktree",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.ok(
|
|
56
|
+
report.issues.some((issue) => issue.code === "worktree_empty_with_project_content"),
|
|
57
|
+
"doctor reports the empty worktree",
|
|
58
|
+
);
|
|
59
|
+
assert.ok(
|
|
60
|
+
report.fixesApplied.some((fix) => fix.includes("recreated empty worktree")),
|
|
61
|
+
"doctor applies the repair",
|
|
62
|
+
);
|
|
63
|
+
assert.ok(existsSync(join(wtPath, "package.json")), "worktree content is restored");
|
|
64
|
+
assert.ok(existsSync(join(wtPath, "milestone-note.txt")), "branch content is restored");
|
|
65
|
+
});
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
checkpointDatabase,
|
|
38
38
|
refreshOpenDatabaseFromDisk,
|
|
39
39
|
tryCreateMemoriesFts,
|
|
40
|
+
_isLikelyWslDrvFsPathForTest,
|
|
40
41
|
} from '../gsd-db.ts';
|
|
41
42
|
import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
|
|
42
43
|
|
|
@@ -345,6 +346,16 @@ describe('gsd-db', () => {
|
|
|
345
346
|
});
|
|
346
347
|
});
|
|
347
348
|
|
|
349
|
+
test('gsd-db: detects WSL DrvFs mount paths for conservative pragmas', () => {
|
|
350
|
+
withPlatform('linux', () => {
|
|
351
|
+
assert.equal(_isLikelyWslDrvFsPathForTest('/mnt/d/code/project/.gsd/gsd.db'), true);
|
|
352
|
+
assert.equal(_isLikelyWslDrvFsPathForTest('/tmp/gsd.db'), false);
|
|
353
|
+
});
|
|
354
|
+
withPlatform('darwin', () => {
|
|
355
|
+
assert.equal(_isLikelyWslDrvFsPathForTest('/mnt/d/code/project/.gsd/gsd.db'), false);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
348
359
|
test('gsd-db: transaction rollback on error', () => {
|
|
349
360
|
openDatabase(':memory:');
|
|
350
361
|
|
|
@@ -34,6 +34,8 @@ test("guided project prompt renders compact interview and artifact guidance", as
|
|
|
34
34
|
assert.match(prompt, /depth_verification_project_confirm/);
|
|
35
35
|
assert.match(prompt, /artifact_type: "PROJECT"/);
|
|
36
36
|
assert.match(prompt, /omit `milestone_id`/);
|
|
37
|
+
assert.match(prompt, /do not write the projection directly/i);
|
|
38
|
+
assert.doesNotMatch(prompt, /then write `.gsd\/PROJECT\.md`/);
|
|
37
39
|
assert.match(prompt, /Do NOT use `artifact_type: "CONTEXT"` and do NOT pass `milestone_id: "PROJECT"`/);
|
|
38
40
|
assert.match(prompt, /\*\*Complexity:\*\* simple/);
|
|
39
41
|
assert.match(prompt, /\*\*Complexity:\*\* complex/);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// GSD-2 — Guided workflow dispatch project-root tests.
|
|
2
|
+
// Verifies smart entry dispatch uses the explicit project root instead of cwd.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
_dispatchWorkflowForTest,
|
|
12
|
+
resolveGuidedDispatchProjectRoot,
|
|
13
|
+
} from "../guided-flow.ts";
|
|
14
|
+
|
|
15
|
+
test("guided dispatch falls back to cwd only when no project root is supplied", () => {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
assert.equal(resolveGuidedDispatchProjectRoot(), cwd);
|
|
18
|
+
assert.equal(resolveGuidedDispatchProjectRoot("/tmp/explicit-root"), "/tmp/explicit-root");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("guided dispatch passes the explicit project root through model and compatibility checks", async () => {
|
|
22
|
+
const explicitRoot = mkdtempSync(join(tmpdir(), "gsd-guided-root-explicit-"));
|
|
23
|
+
const otherRoot = mkdtempSync(join(tmpdir(), "gsd-guided-root-cwd-"));
|
|
24
|
+
const workflowPath = join(explicitRoot, "GSD-WORKFLOW.md");
|
|
25
|
+
const originalWorkflowPath = process.env.GSD_WORKFLOW_PATH;
|
|
26
|
+
const originalCwd = process.cwd();
|
|
27
|
+
const seen = {
|
|
28
|
+
prefsRoot: "",
|
|
29
|
+
modelRoot: "",
|
|
30
|
+
compatibilityRoot: "",
|
|
31
|
+
sent: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ctx = {
|
|
35
|
+
model: { provider: "local-provider" },
|
|
36
|
+
modelRegistry: {
|
|
37
|
+
getProviderAuthMode: () => "apiKey",
|
|
38
|
+
},
|
|
39
|
+
ui: {
|
|
40
|
+
notify: () => {},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const pi = {
|
|
45
|
+
getActiveTools: () => ["gsd_plan_slice"],
|
|
46
|
+
setActiveTools: () => {},
|
|
47
|
+
sendMessage: () => {
|
|
48
|
+
seen.sent = true;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(workflowPath, "# Workflow\n", "utf-8");
|
|
54
|
+
process.env.GSD_WORKFLOW_PATH = workflowPath;
|
|
55
|
+
process.chdir(otherRoot);
|
|
56
|
+
|
|
57
|
+
await _dispatchWorkflowForTest(
|
|
58
|
+
pi as any,
|
|
59
|
+
"Plan the slice.",
|
|
60
|
+
"gsd-run",
|
|
61
|
+
ctx as any,
|
|
62
|
+
"plan-slice",
|
|
63
|
+
{
|
|
64
|
+
basePath: explicitRoot,
|
|
65
|
+
deps: {
|
|
66
|
+
loadPreferences: (projectRoot?: string) => {
|
|
67
|
+
seen.prefsRoot = projectRoot ?? "";
|
|
68
|
+
return { preferences: {} } as any;
|
|
69
|
+
},
|
|
70
|
+
selectModel: async (
|
|
71
|
+
_ctx: unknown,
|
|
72
|
+
_pi: unknown,
|
|
73
|
+
_unitType: string,
|
|
74
|
+
_unitId: string,
|
|
75
|
+
projectRoot: string,
|
|
76
|
+
) => {
|
|
77
|
+
seen.modelRoot = projectRoot;
|
|
78
|
+
return { routing: null, appliedModel: null };
|
|
79
|
+
},
|
|
80
|
+
getTransportSupportError: (
|
|
81
|
+
_provider: string | undefined,
|
|
82
|
+
_requiredTools: string[],
|
|
83
|
+
options?: { projectRoot?: string },
|
|
84
|
+
) => {
|
|
85
|
+
seen.compatibilityRoot = options?.projectRoot ?? "";
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assert.equal(seen.prefsRoot, explicitRoot);
|
|
93
|
+
assert.equal(seen.modelRoot, explicitRoot);
|
|
94
|
+
assert.equal(seen.compatibilityRoot, explicitRoot);
|
|
95
|
+
assert.equal(seen.sent, true);
|
|
96
|
+
} finally {
|
|
97
|
+
process.chdir(originalCwd);
|
|
98
|
+
if (originalWorkflowPath === undefined) {
|
|
99
|
+
delete process.env.GSD_WORKFLOW_PATH;
|
|
100
|
+
} else {
|
|
101
|
+
process.env.GSD_WORKFLOW_PATH = originalWorkflowPath;
|
|
102
|
+
}
|
|
103
|
+
rmSync(explicitRoot, { recursive: true, force: true });
|
|
104
|
+
rmSync(otherRoot, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
@@ -21,6 +21,15 @@ import {
|
|
|
21
21
|
checkAutoStartAfterDiscuss,
|
|
22
22
|
} from "../guided-flow.ts";
|
|
23
23
|
|
|
24
|
+
function pendingInput(basePath: string, milestoneId: string) {
|
|
25
|
+
return {
|
|
26
|
+
basePath,
|
|
27
|
+
milestoneId,
|
|
28
|
+
ctx: { ui: { notify: () => undefined } } as any,
|
|
29
|
+
pi: { setActiveTools: () => undefined, getActiveTools: () => [] } as any,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
25
34
|
|
|
26
35
|
describe("#2985 Bug 3 — concurrent discuss sessions must be independent", () => {
|
|
@@ -34,13 +43,11 @@ describe("#2985 Bug 3 — concurrent discuss sessions must be independent", () =
|
|
|
34
43
|
const projectB = "/projects/beta";
|
|
35
44
|
|
|
36
45
|
setPendingAutoStart(projectA, {
|
|
37
|
-
|
|
38
|
-
milestoneId: "M001-aaa111",
|
|
46
|
+
...pendingInput(projectA, "M001-aaa111"),
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
setPendingAutoStart(projectB, {
|
|
42
|
-
|
|
43
|
-
milestoneId: "M002-bbb222",
|
|
50
|
+
...pendingInput(projectB, "M002-bbb222"),
|
|
44
51
|
});
|
|
45
52
|
|
|
46
53
|
// Both sessions should be retrievable
|
|
@@ -55,8 +62,8 @@ describe("#2985 Bug 3 — concurrent discuss sessions must be independent", () =
|
|
|
55
62
|
const projectA = "/projects/alpha";
|
|
56
63
|
const projectB = "/projects/beta";
|
|
57
64
|
|
|
58
|
-
setPendingAutoStart(projectA,
|
|
59
|
-
setPendingAutoStart(projectB,
|
|
65
|
+
setPendingAutoStart(projectA, pendingInput(projectA, "M001-aaa111"));
|
|
66
|
+
setPendingAutoStart(projectB, pendingInput(projectB, "M002-bbb222"));
|
|
60
67
|
|
|
61
68
|
// Clear only projectA
|
|
62
69
|
clearPendingAutoStart(projectA);
|
|
@@ -72,8 +79,8 @@ describe("#2985 Bug 4 — getDiscussionMilestoneId must be keyed by basePath", (
|
|
|
72
79
|
});
|
|
73
80
|
|
|
74
81
|
test("getDiscussionMilestoneId(basePath) returns correct milestone for each project", () => {
|
|
75
|
-
setPendingAutoStart("/proj/a",
|
|
76
|
-
setPendingAutoStart("/proj/b",
|
|
82
|
+
setPendingAutoStart("/proj/a", pendingInput("/proj/a", "M001"));
|
|
83
|
+
setPendingAutoStart("/proj/b", pendingInput("/proj/b", "M002"));
|
|
77
84
|
|
|
78
85
|
assert.equal(getDiscussionMilestoneId("/proj/a"), "M001");
|
|
79
86
|
assert.equal(getDiscussionMilestoneId("/proj/b"), "M002");
|
|
@@ -81,8 +88,8 @@ describe("#2985 Bug 4 — getDiscussionMilestoneId must be keyed by basePath", (
|
|
|
81
88
|
});
|
|
82
89
|
|
|
83
90
|
test("getDiscussionMilestoneId() without basePath returns null when multiple sessions exist", () => {
|
|
84
|
-
setPendingAutoStart("/proj/a",
|
|
85
|
-
setPendingAutoStart("/proj/b",
|
|
91
|
+
setPendingAutoStart("/proj/a", pendingInput("/proj/a", "M001"));
|
|
92
|
+
setPendingAutoStart("/proj/b", pendingInput("/proj/b", "M002"));
|
|
86
93
|
|
|
87
94
|
// Without a key, the function should not blindly return the first entry
|
|
88
95
|
const result = getDiscussionMilestoneId();
|
|
@@ -92,7 +99,7 @@ describe("#2985 Bug 4 — getDiscussionMilestoneId must be keyed by basePath", (
|
|
|
92
99
|
});
|
|
93
100
|
|
|
94
101
|
test("getDiscussionMilestoneId() without basePath returns the milestone when only one session", () => {
|
|
95
|
-
setPendingAutoStart("/proj/a",
|
|
102
|
+
setPendingAutoStart("/proj/a", pendingInput("/proj/a", "M001"));
|
|
96
103
|
|
|
97
104
|
// With only one session, backward compat — return it
|
|
98
105
|
const result = getDiscussionMilestoneId();
|
|
@@ -129,3 +136,44 @@ test("checkAutoStartAfterDiscuss ignores missing manifest for single-milestone d
|
|
|
129
136
|
rmSync(base, { recursive: true, force: true });
|
|
130
137
|
}
|
|
131
138
|
});
|
|
139
|
+
|
|
140
|
+
test("checkAutoStartAfterDiscuss(basePath) selects the matching pending entry when multiple sessions exist", () => {
|
|
141
|
+
const projectA = mkdtempSync(join(tmpdir(), "gsd-auto-start-project-a-"));
|
|
142
|
+
const projectB = mkdtempSync(join(tmpdir(), "gsd-auto-start-project-b-"));
|
|
143
|
+
|
|
144
|
+
function writeReadyArtifacts(base: string, milestoneId: string): void {
|
|
145
|
+
const gsdDir = join(base, ".gsd");
|
|
146
|
+
const milestoneDir = join(gsdDir, "milestones", milestoneId);
|
|
147
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
148
|
+
writeFileSync(join(gsdDir, "PROJECT.md"), `# Project\n\n| ${milestoneId} | Milestone | active |\n`);
|
|
149
|
+
writeFileSync(join(gsdDir, "STATE.md"), "# State\n");
|
|
150
|
+
writeFileSync(join(milestoneDir, `${milestoneId}-CONTEXT.md`), "# Context\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
clearPendingAutoStart();
|
|
155
|
+
writeReadyArtifacts(projectA, "M001");
|
|
156
|
+
writeReadyArtifacts(projectB, "M002");
|
|
157
|
+
setPendingAutoStart(projectA, {
|
|
158
|
+
basePath: projectA,
|
|
159
|
+
milestoneId: "M001",
|
|
160
|
+
ctx: { ui: { notify: () => undefined } } as any,
|
|
161
|
+
pi: { setActiveTools: () => undefined, getActiveTools: () => [] } as any,
|
|
162
|
+
});
|
|
163
|
+
setPendingAutoStart(projectB, {
|
|
164
|
+
basePath: projectB,
|
|
165
|
+
milestoneId: "M002",
|
|
166
|
+
ctx: { ui: { notify: () => undefined } } as any,
|
|
167
|
+
pi: { setActiveTools: () => undefined, getActiveTools: () => [] } as any,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.equal(checkAutoStartAfterDiscuss(), false, "ambiguous pending sessions should not auto-start");
|
|
171
|
+
assert.equal(checkAutoStartAfterDiscuss(projectB), true, "explicit basePath should select projectB");
|
|
172
|
+
assert.equal(getDiscussionMilestoneId(projectA), "M001", "projectA should remain pending");
|
|
173
|
+
assert.equal(getDiscussionMilestoneId(projectB), null, "projectB should be cleared after start");
|
|
174
|
+
} finally {
|
|
175
|
+
clearPendingAutoStart();
|
|
176
|
+
rmSync(projectA, { recursive: true, force: true });
|
|
177
|
+
rmSync(projectB, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// GSD-2 — Guided Unit Tool Contract tests.
|
|
2
|
+
// Verifies guided workflow turns use manifest tool policy without auto-mode state.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
11
|
+
import {
|
|
12
|
+
clearGuidedUnitContext,
|
|
13
|
+
getGuidedUnitContext,
|
|
14
|
+
setGuidedUnitContext,
|
|
15
|
+
} from "../guided-unit-context.ts";
|
|
16
|
+
|
|
17
|
+
test("guided Unit context applies Tool Contract policy when auto-mode has no current Unit", async () => {
|
|
18
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-guided-tool-contract-"));
|
|
19
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
20
|
+
const pi = {
|
|
21
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
22
|
+
const existing = handlers.get(event) ?? [];
|
|
23
|
+
existing.push(handler);
|
|
24
|
+
handlers.set(event, existing);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
clearGuidedUnitContext();
|
|
30
|
+
registerHooks(pi as any, []);
|
|
31
|
+
setGuidedUnitContext(basePath, "plan-slice");
|
|
32
|
+
|
|
33
|
+
let blockResult: { block?: boolean; reason?: string } | undefined;
|
|
34
|
+
for (const handler of handlers.get("tool_call") ?? []) {
|
|
35
|
+
const result = await handler({
|
|
36
|
+
toolName: "edit",
|
|
37
|
+
input: {
|
|
38
|
+
path: join(basePath, "src", "main.ts"),
|
|
39
|
+
},
|
|
40
|
+
}, { cwd: basePath });
|
|
41
|
+
if (result?.block) {
|
|
42
|
+
blockResult = result;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assert.equal(blockResult?.block, true);
|
|
48
|
+
assert.match(blockResult?.reason ?? "", /plan-slice|ToolsPolicy|not allowed|blocked/i);
|
|
49
|
+
} finally {
|
|
50
|
+
clearGuidedUnitContext();
|
|
51
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("guided Unit context can be cleared by project root", () => {
|
|
56
|
+
clearGuidedUnitContext();
|
|
57
|
+
setGuidedUnitContext("/project/a", "plan-slice");
|
|
58
|
+
setGuidedUnitContext("/project/b", "complete-slice");
|
|
59
|
+
|
|
60
|
+
clearGuidedUnitContext("/project/a");
|
|
61
|
+
|
|
62
|
+
assert.equal(getGuidedUnitContext("/project/a"), null);
|
|
63
|
+
assert.equal(getGuidedUnitContext("/project/b")?.unitType, "complete-slice");
|
|
64
|
+
clearGuidedUnitContext();
|
|
65
|
+
});
|