gsd-pi 2.66.1-dev.0df32ec → 2.66.1-dev.3cea7ac
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/ask-user-questions.js +79 -11
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +4 -3
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -3
- package/dist/resources/extensions/gsd/auto/loop.js +13 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -4
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
- package/dist/resources/extensions/gsd/auto/session.js +1 -1
- package/dist/resources/extensions/gsd/auto-dashboard.js +65 -15
- package/dist/resources/extensions/gsd/auto-dispatch.js +30 -28
- package/dist/resources/extensions/gsd/auto-prompts.js +6 -6
- package/dist/resources/extensions/gsd/auto-recovery.js +11 -12
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +18 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +59 -5
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +8 -5
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +186 -14
- package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
- package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +10 -4
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -1
- package/dist/resources/extensions/gsd/detection.js +6 -0
- package/dist/resources/extensions/gsd/files.js +19 -2
- package/dist/resources/extensions/gsd/guided-flow.js +12 -8
- package/dist/resources/extensions/gsd/index.js +1 -1
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +2 -0
- package/dist/resources/extensions/gsd/parsers-legacy.js +3 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
- package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
- package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +2 -1
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.js +27 -26
- package/dist/resources/extensions/gsd/workflow-reconcile.js +46 -7
- package/dist/resources/extensions/remote-questions/manager.js +8 -0
- package/dist/resources/extensions/shared/interview-ui.js +10 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +4 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/utils/json-parse.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/json-parse.js +11 -1
- package/packages/pi-ai/dist/utils/json-parse.js.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +60 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/json-parse.test.js +14 -0
- package/packages/pi-ai/dist/utils/tests/json-parse.test.js.map +1 -0
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +10 -0
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +4 -3
- package/packages/pi-ai/src/utils/json-parse.ts +11 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +69 -1
- package/packages/pi-ai/src/utils/tests/json-parse.test.ts +17 -0
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
- 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 +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -2
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js +13 -0
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +35 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -0
- package/packages/pi-tui/dist/__tests__/tui.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/tui.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js +43 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +9 -7
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.js +54 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -0
- package/packages/pi-tui/dist/components/editor.d.ts +3 -1
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +14 -3
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.js +6 -0
- package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +8 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/autocomplete.test.ts +15 -0
- package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +43 -0
- package/packages/pi-tui/src/__tests__/tui.test.ts +50 -0
- package/packages/pi-tui/src/autocomplete.ts +9 -7
- package/packages/pi-tui/src/components/__tests__/editor.test.ts +64 -0
- package/packages/pi-tui/src/components/editor.ts +14 -3
- package/packages/pi-tui/src/stdin-buffer.ts +7 -0
- package/packages/pi-tui/src/tui.ts +9 -0
- package/src/resources/extensions/ask-user-questions.ts +103 -11
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +4 -3
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -3
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +17 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +18 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -1
- package/src/resources/extensions/gsd/auto/loop.ts +14 -1
- package/src/resources/extensions/gsd/auto/phases.ts +10 -5
- package/src/resources/extensions/gsd/auto/run-unit.ts +14 -2
- package/src/resources/extensions/gsd/auto/session.ts +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +76 -16
- package/src/resources/extensions/gsd/auto-dispatch.ts +36 -35
- package/src/resources/extensions/gsd/auto-prompts.ts +5 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -15
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +27 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +67 -6
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +11 -8
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +209 -16
- package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
- package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -6
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +11 -4
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +3 -1
- package/src/resources/extensions/gsd/detection.ts +6 -0
- package/src/resources/extensions/gsd/files.ts +21 -2
- package/src/resources/extensions/gsd/guided-flow.ts +15 -8
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +2 -0
- package/src/resources/extensions/gsd/parsers-legacy.ts +3 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
- package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +4 -1
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +52 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +50 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +53 -13
- package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +71 -2
- package/src/resources/extensions/gsd/tests/parsers.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/queue-execution-guard.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/workflow-reconcile.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +210 -35
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -27
- package/src/resources/extensions/gsd/workflow-reconcile.ts +59 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -0
- package/src/resources/extensions/shared/interview-ui.ts +13 -0
- /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_ssgManifest.js +0 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formatRelativeTime,
|
|
10
10
|
type HealthWidgetData,
|
|
11
11
|
} from "../health-widget-core.ts";
|
|
12
|
+
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
12
13
|
|
|
13
14
|
function makeTempDir(prefix: string): string {
|
|
14
15
|
const dir = join(
|
|
@@ -177,3 +178,47 @@ test("detectHealthWidgetProjectState: metrics file alone does not imply project"
|
|
|
177
178
|
);
|
|
178
179
|
assert.equal(detectHealthWidgetProjectState(dir), "initialized");
|
|
179
180
|
});
|
|
181
|
+
|
|
182
|
+
test("session_start bootstraps the health widget alongside notifications", async (t) => {
|
|
183
|
+
const dir = makeTempDir("bootstrap");
|
|
184
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
185
|
+
|
|
186
|
+
const originalCwd = process.cwd();
|
|
187
|
+
process.chdir(dir);
|
|
188
|
+
t.after(() => {
|
|
189
|
+
process.chdir(originalCwd);
|
|
190
|
+
cleanup(dir);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const widgets: string[] = [];
|
|
194
|
+
const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
|
|
195
|
+
const pi = {
|
|
196
|
+
on(event: string, handler: (event: unknown, ctx: any) => Promise<void> | void) {
|
|
197
|
+
handlers.set(event, handler);
|
|
198
|
+
},
|
|
199
|
+
} as any;
|
|
200
|
+
|
|
201
|
+
registerHooks(pi);
|
|
202
|
+
const sessionStart = handlers.get("session_start");
|
|
203
|
+
assert.ok(sessionStart, "session_start handler is registered");
|
|
204
|
+
|
|
205
|
+
await sessionStart!({}, {
|
|
206
|
+
hasUI: true,
|
|
207
|
+
ui: {
|
|
208
|
+
notify: () => {},
|
|
209
|
+
setStatus: () => {},
|
|
210
|
+
setWorkingMessage: () => {},
|
|
211
|
+
onTerminalInput: () => () => {},
|
|
212
|
+
setWidget: (key: string) => {
|
|
213
|
+
widgets.push(key);
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
sessionManager: {
|
|
217
|
+
getSessionId: () => null,
|
|
218
|
+
},
|
|
219
|
+
model: null,
|
|
220
|
+
} as any);
|
|
221
|
+
|
|
222
|
+
assert.ok(widgets.includes("gsd-health"), "health widget is bootstrapped");
|
|
223
|
+
assert.ok(widgets.includes("gsd-notifications"), "notification widget still boots");
|
|
224
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, chmodSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
resolveExpectedArtifactPath,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
diagnoseExpectedArtifact,
|
|
12
13
|
buildLoopRemediationSteps,
|
|
13
14
|
hasImplementationArtifacts,
|
|
15
|
+
reconcileMergeState,
|
|
14
16
|
} from "../../auto-recovery.ts";
|
|
15
17
|
import { parseRoadmap, parsePlan } from "../../parsers-legacy.ts";
|
|
16
18
|
import { parseTaskPlanFile, clearParseCache } from "../../files.ts";
|
|
@@ -669,8 +671,6 @@ test("#793: invalidateAllCaches clears all caches so deriveState sees fresh disk
|
|
|
669
671
|
|
|
670
672
|
// ─── hasImplementationArtifacts (#1703) ───────────────────────────────────
|
|
671
673
|
|
|
672
|
-
import { execFileSync } from "node:child_process";
|
|
673
|
-
|
|
674
674
|
function makeGitBase(): string {
|
|
675
675
|
const base = join(tmpdir(), `gsd-test-git-${randomUUID()}`);
|
|
676
676
|
mkdirSync(base, { recursive: true });
|
|
@@ -745,9 +745,6 @@ test("verifyExpectedArtifact complete-milestone fails with only .gsd/ files (#17
|
|
|
745
745
|
|
|
746
746
|
// ─── reconcileMergeState: silent nativeCommit failure (#2542) ─────────────
|
|
747
747
|
|
|
748
|
-
import { reconcileMergeState } from "../../auto-recovery.ts";
|
|
749
|
-
import { chmodSync } from "node:fs";
|
|
750
|
-
|
|
751
748
|
function makeMockCtx(): { ctx: any; notifications: Array<{ msg: string; level: string }> } {
|
|
752
749
|
const notifications: Array<{ msg: string; level: string }> = [];
|
|
753
750
|
const ctx = {
|
|
@@ -760,7 +757,7 @@ function makeMockCtx(): { ctx: any; notifications: Array<{ msg: string; level: s
|
|
|
760
757
|
return { ctx, notifications };
|
|
761
758
|
}
|
|
762
759
|
|
|
763
|
-
test("reconcileMergeState returns
|
|
760
|
+
test("reconcileMergeState returns blocked and notifies error when nativeCommit fails (#2542)", (t) => {
|
|
764
761
|
const base = makeGitBase();
|
|
765
762
|
t.after(() => cleanup(base));
|
|
766
763
|
|
|
@@ -786,9 +783,7 @@ test("reconcileMergeState returns false and notifies error when nativeCommit fai
|
|
|
786
783
|
const { ctx, notifications } = makeMockCtx();
|
|
787
784
|
const result = reconcileMergeState(base, ctx);
|
|
788
785
|
|
|
789
|
-
|
|
790
|
-
// (Currently it silently swallows the error and returns true — this test should FAIL before the fix)
|
|
791
|
-
assert.equal(result, false, "reconcileMergeState should return false when nativeCommit fails");
|
|
786
|
+
assert.equal(result, "blocked", "reconcileMergeState should return blocked when nativeCommit fails");
|
|
792
787
|
const errorNotifications = notifications.filter(n => n.level === "error");
|
|
793
788
|
assert.ok(errorNotifications.length > 0, "should notify an error when nativeCommit fails");
|
|
794
789
|
assert.ok(
|
|
@@ -797,18 +792,63 @@ test("reconcileMergeState returns false and notifies error when nativeCommit fai
|
|
|
797
792
|
);
|
|
798
793
|
});
|
|
799
794
|
|
|
800
|
-
test("reconcileMergeState returns
|
|
801
|
-
// When there's no MERGE_HEAD or SQUASH_MSG, reconcileMergeState returns false (no dirty state)
|
|
795
|
+
test("reconcileMergeState returns clean when no merge state present", (t) => {
|
|
802
796
|
const base = makeGitBase();
|
|
803
797
|
t.after(() => cleanup(base));
|
|
804
798
|
|
|
805
799
|
const { ctx, notifications } = makeMockCtx();
|
|
806
800
|
const result = reconcileMergeState(base, ctx);
|
|
807
801
|
|
|
808
|
-
assert.equal(result,
|
|
802
|
+
assert.equal(result, "clean", "should return clean when no merge state exists");
|
|
809
803
|
assert.equal(notifications.length, 0, "should not notify when no merge state present");
|
|
810
804
|
});
|
|
811
805
|
|
|
806
|
+
test("reconcileMergeState blocks and preserves unresolved code conflicts", (t) => {
|
|
807
|
+
const base = makeGitBase();
|
|
808
|
+
t.after(() => cleanup(base));
|
|
809
|
+
|
|
810
|
+
writeFileSync(join(base, "conflict.txt"), "base\n");
|
|
811
|
+
execFileSync("git", ["add", "conflict.txt"], { cwd: base, stdio: "ignore" });
|
|
812
|
+
execFileSync("git", ["commit", "-m", "add conflict base"], { cwd: base, stdio: "ignore" });
|
|
813
|
+
|
|
814
|
+
execFileSync("git", ["checkout", "-b", "feature"], { cwd: base, stdio: "ignore" });
|
|
815
|
+
writeFileSync(join(base, "conflict.txt"), "feature\n");
|
|
816
|
+
execFileSync("git", ["add", "conflict.txt"], { cwd: base, stdio: "ignore" });
|
|
817
|
+
execFileSync("git", ["commit", "-m", "feature change"], { cwd: base, stdio: "ignore" });
|
|
818
|
+
|
|
819
|
+
execFileSync("git", ["checkout", "main"], { cwd: base, stdio: "ignore" });
|
|
820
|
+
writeFileSync(join(base, "conflict.txt"), "main\n");
|
|
821
|
+
execFileSync("git", ["add", "conflict.txt"], { cwd: base, stdio: "ignore" });
|
|
822
|
+
execFileSync("git", ["commit", "-m", "main change"], { cwd: base, stdio: "ignore" });
|
|
823
|
+
|
|
824
|
+
let mergeFailed = false;
|
|
825
|
+
try {
|
|
826
|
+
execFileSync("git", ["merge", "--no-ff", "feature"], { cwd: base, stdio: "ignore" });
|
|
827
|
+
} catch {
|
|
828
|
+
mergeFailed = true;
|
|
829
|
+
}
|
|
830
|
+
assert.equal(mergeFailed, true, "merge should produce a conflict");
|
|
831
|
+
assert.ok(existsSync(join(base, ".git", "MERGE_HEAD")), "MERGE_HEAD should remain present before reconcile");
|
|
832
|
+
|
|
833
|
+
const beforeContents = readFileSync(join(base, "conflict.txt"), "utf8");
|
|
834
|
+
assert.match(beforeContents, /<<<<<<<|=======|>>>>>>>/, "fixture should contain conflict markers");
|
|
835
|
+
|
|
836
|
+
const { ctx, notifications } = makeMockCtx();
|
|
837
|
+
const result = reconcileMergeState(base, ctx);
|
|
838
|
+
|
|
839
|
+
assert.equal(result, "blocked", "code conflicts should block reconciliation");
|
|
840
|
+
assert.ok(existsSync(join(base, ".git", "MERGE_HEAD")), "MERGE_HEAD should be preserved for manual resolution");
|
|
841
|
+
assert.equal(
|
|
842
|
+
readFileSync(join(base, "conflict.txt"), "utf8"),
|
|
843
|
+
beforeContents,
|
|
844
|
+
"reconcile should preserve the conflicted file contents",
|
|
845
|
+
);
|
|
846
|
+
assert.ok(
|
|
847
|
+
notifications.some((n) => n.level === "error" && n.msg.includes("manual conflict resolution is preserved")),
|
|
848
|
+
"should notify that auto-mode paused and preserved manual work",
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
|
|
812
852
|
test("verifyExpectedArtifact complete-milestone passes with impl files (#1703)", (t) => {
|
|
813
853
|
const base = makeGitBase();
|
|
814
854
|
t.after(() => cleanup(base));
|
package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts
CHANGED
|
@@ -360,8 +360,8 @@ describe("session management", () => {
|
|
|
360
360
|
assert.equal(s.unitRecoveryCount.size, 0, "recovery counts cleared");
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
-
test("NEW_SESSION_TIMEOUT_MS is
|
|
364
|
-
assert.equal(NEW_SESSION_TIMEOUT_MS,
|
|
363
|
+
test("NEW_SESSION_TIMEOUT_MS is 120 seconds", () => {
|
|
364
|
+
assert.equal(NEW_SESSION_TIMEOUT_MS, 120_000, "session timeout should be 120s");
|
|
365
365
|
});
|
|
366
366
|
|
|
367
367
|
test("MAX_UNIT_DISPATCHES limits retries for a single unit", () => {
|
|
@@ -72,7 +72,7 @@ function makeMockDeps(
|
|
|
72
72
|
getCurrentBranch: () => "main",
|
|
73
73
|
autoWorktreeBranch: () => "auto/M001",
|
|
74
74
|
resolveMilestoneFile: () => null,
|
|
75
|
-
reconcileMergeState: () =>
|
|
75
|
+
reconcileMergeState: () => "clean",
|
|
76
76
|
getLedger: () => ({ units: [] }),
|
|
77
77
|
getProjectTotals: () => ({ cost: 0 }),
|
|
78
78
|
formatCost: (c: number) => `$${c.toFixed(2)}`,
|
|
@@ -273,9 +273,9 @@ test('Scenario 2: Fully complete project — deriveState phase', async () => {
|
|
|
273
273
|
invalidateAllCaches();
|
|
274
274
|
const state = await deriveState(base);
|
|
275
275
|
assert.deepStrictEqual(state.phase, 'complete', 'complete: deriveState phase is complete (validation + summary written by migration)');
|
|
276
|
-
|
|
277
|
-
assert.ok(state.
|
|
278
|
-
assert.deepStrictEqual(state.
|
|
276
|
+
assert.equal(state.activeMilestone, null, 'complete: deriveState has no activeMilestone');
|
|
277
|
+
assert.ok(state.lastCompletedMilestone !== null, 'complete: deriveState exposes lastCompletedMilestone');
|
|
278
|
+
assert.deepStrictEqual(state.lastCompletedMilestone!.id, 'M001', 'complete: deriveState lastCompletedMilestone is M001');
|
|
279
279
|
|
|
280
280
|
// generatePreview for complete project
|
|
281
281
|
const preview = generatePreview(project);
|
|
@@ -292,4 +292,3 @@ test('Scenario 2: Fully complete project — deriveState phase', async () => {
|
|
|
292
292
|
rmSync(base, { recursive: true, force: true });
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
|
-
|
|
@@ -57,4 +57,25 @@ describe("parallel-monitor-overlay", () => {
|
|
|
57
57
|
assert.ok(closed, "pressing q should trigger onClose");
|
|
58
58
|
overlay2.dispose();
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
it("ParallelMonitorOverlay clamps scrollOffset during render", async () => {
|
|
62
|
+
const mod = await import("../parallel-monitor-overlay.js");
|
|
63
|
+
|
|
64
|
+
const mockTui = { requestRender: () => {} };
|
|
65
|
+
const mockTheme = {
|
|
66
|
+
fg: (_color: string, text: string) => text,
|
|
67
|
+
bold: (text: string) => text,
|
|
68
|
+
};
|
|
69
|
+
const overlay = new mod.ParallelMonitorOverlay(
|
|
70
|
+
mockTui,
|
|
71
|
+
mockTheme as any,
|
|
72
|
+
() => {},
|
|
73
|
+
"/nonexistent/path",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
(overlay as any).scrollOffset = 999;
|
|
77
|
+
overlay.render(80);
|
|
78
|
+
assert.equal((overlay as any).scrollOffset, 0, "empty overlays clamp scroll to zero");
|
|
79
|
+
overlay.dispose();
|
|
80
|
+
});
|
|
60
81
|
});
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* Verifies the dispatch rule and prompt builder exist with correct structure.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import test from "node:test";
|
|
7
|
+
import test, { afterEach } from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
9
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { join, dirname } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
|
|
14
|
+
import { resolveDispatch } from "../auto-dispatch.ts";
|
|
15
|
+
|
|
13
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
17
|
|
|
15
18
|
const dispatchSrc = readFileSync(join(__dirname, "..", "auto-dispatch.ts"), "utf-8");
|
|
@@ -17,6 +20,47 @@ const promptsSrc = readFileSync(join(__dirname, "..", "auto-prompts.ts"), "utf-8
|
|
|
17
20
|
const templatePath = join(__dirname, "..", "prompts", "parallel-research-slices.md");
|
|
18
21
|
const templateSrc = readFileSync(templatePath, "utf-8");
|
|
19
22
|
|
|
23
|
+
const tmpDirs: string[] = [];
|
|
24
|
+
|
|
25
|
+
function makeTmpProject(): string {
|
|
26
|
+
const base = mkdtempSync(join(tmpdir(), "parallel-research-"));
|
|
27
|
+
tmpDirs.push(base);
|
|
28
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
29
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
30
|
+
writeFileSync(
|
|
31
|
+
join(milestoneDir, "M001-ROADMAP.md"),
|
|
32
|
+
[
|
|
33
|
+
"# M001: Parallel Research Milestone",
|
|
34
|
+
"",
|
|
35
|
+
"**Vision:** Research-ready slices.",
|
|
36
|
+
"",
|
|
37
|
+
"**Success Criteria:**",
|
|
38
|
+
"- Research both slices",
|
|
39
|
+
"",
|
|
40
|
+
"## Slices",
|
|
41
|
+
"",
|
|
42
|
+
"- [ ] **S01: Alpha** `risk:low` `depends:[]`",
|
|
43
|
+
"- [ ] **S02: Beta** `risk:low` `depends:[]`",
|
|
44
|
+
"",
|
|
45
|
+
"## Boundary Map",
|
|
46
|
+
"",
|
|
47
|
+
].join("\n"),
|
|
48
|
+
"utf-8",
|
|
49
|
+
);
|
|
50
|
+
return base;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
for (const dir of tmpDirs) {
|
|
55
|
+
try {
|
|
56
|
+
rmSync(dir, { recursive: true, force: true });
|
|
57
|
+
} catch {
|
|
58
|
+
// Best-effort cleanup only.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
tmpDirs.length = 0;
|
|
62
|
+
});
|
|
63
|
+
|
|
20
64
|
// ─── Dispatch rule ────────────────────────────────────────────────────────
|
|
21
65
|
|
|
22
66
|
test("dispatch: parallel-research-slices rule exists", () => {
|
|
@@ -75,3 +119,28 @@ test("template: validate-milestone uses parallel reviewers", () => {
|
|
|
75
119
|
"validate-milestone should dispatch 3 parallel reviewers",
|
|
76
120
|
);
|
|
77
121
|
});
|
|
122
|
+
|
|
123
|
+
test("resolveDispatch prefers parallel research when multiple slices are ready", async () => {
|
|
124
|
+
const base = makeTmpProject();
|
|
125
|
+
|
|
126
|
+
const action = await resolveDispatch({
|
|
127
|
+
basePath: base,
|
|
128
|
+
mid: "M001",
|
|
129
|
+
midTitle: "Parallel Research Milestone",
|
|
130
|
+
state: {
|
|
131
|
+
phase: "planning",
|
|
132
|
+
activeMilestone: { id: "M001", title: "Parallel Research Milestone", status: "active" },
|
|
133
|
+
activeSlice: { id: "S01", title: "Alpha" },
|
|
134
|
+
activeTask: null,
|
|
135
|
+
registry: [],
|
|
136
|
+
blockers: [],
|
|
137
|
+
} as any,
|
|
138
|
+
prefs: undefined,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
assert.equal(action.action, "dispatch");
|
|
142
|
+
if (action.action === "dispatch") {
|
|
143
|
+
assert.equal(action.unitType, "research-slice");
|
|
144
|
+
assert.equal(action.unitId, "M001/parallel-research");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
@@ -703,6 +703,31 @@ Widget description.
|
|
|
703
703
|
assert.deepStrictEqual(p.tasks[0].title, 'Build the widget', 'em-dash heading T01 title');
|
|
704
704
|
});
|
|
705
705
|
|
|
706
|
+
test('parsePlan: filename subheadings do not become task ids', () => {
|
|
707
|
+
const content = `# S15: Filename Headings
|
|
708
|
+
|
|
709
|
+
**Goal:** Ignore file-reference subheadings inside task descriptions.
|
|
710
|
+
**Demo:** Only real task ids are parsed.
|
|
711
|
+
|
|
712
|
+
## Tasks
|
|
713
|
+
|
|
714
|
+
- [ ] **T01: First task** \`est:10m\`
|
|
715
|
+
Implement the feature.
|
|
716
|
+
|
|
717
|
+
### constraints.py — \`add_off_request_tiered()\`
|
|
718
|
+
- preserve behavior
|
|
719
|
+
|
|
720
|
+
### annotations.py — \`annotate()\`
|
|
721
|
+
- keep metadata
|
|
722
|
+
`;
|
|
723
|
+
|
|
724
|
+
const p = parsePlan(content);
|
|
725
|
+
assert.deepStrictEqual(p.tasks.map((task) => task.id), ['T01'], 'filename subheadings should not create extra tasks');
|
|
726
|
+
assert.deepStrictEqual(p.tasks[0].title, 'First task', 'real task should still parse normally');
|
|
727
|
+
assert.ok(p.tasks[0].description.includes('preserve behavior'), 'detail lines under filename subheadings should remain attached to the task');
|
|
728
|
+
assert.ok(p.tasks[0].description.includes('keep metadata'), 'later detail lines should also remain attached to the task');
|
|
729
|
+
});
|
|
730
|
+
|
|
706
731
|
test('parsePlan: mixed checkbox and heading-style tasks', () => {
|
|
707
732
|
const content = `# S14: Mixed Format
|
|
708
733
|
|
|
@@ -51,6 +51,12 @@ test("guided discussion prompts avoid wrap-up prompts after every round", () =>
|
|
|
51
51
|
assert.doesNotMatch(slicePrompt, /I think I have a solid picture of this slice\. Ready to wrap up/i);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test("guided milestone discussion scopes depth verification to the milestone id", () => {
|
|
55
|
+
const prompt = readPrompt("guided-discuss-milestone");
|
|
56
|
+
assert.match(prompt, /depth_verification_\{\{milestoneId\}\}/, "depth verification id should include the milestone id");
|
|
57
|
+
assert.doesNotMatch(prompt, /depth_verification_confirm" — this enables the write-gate downstream/i, "legacy global depth gate wording should be gone");
|
|
58
|
+
});
|
|
59
|
+
|
|
54
60
|
test("guided-resume-task prompt preserves recovery state until work is superseded", () => {
|
|
55
61
|
const prompt = readPrompt("guided-resume-task");
|
|
56
62
|
assert.match(prompt, /Do \*\*not\*\* delete the continue file immediately/i);
|
|
@@ -188,7 +194,8 @@ test("validate-milestone prompt dispatches parallel reviewers", () => {
|
|
|
188
194
|
assert.match(prompt, /Reviewer C/);
|
|
189
195
|
assert.match(prompt, /Requirements Coverage/);
|
|
190
196
|
assert.match(prompt, /Cross-Slice Integration/);
|
|
191
|
-
assert.match(prompt, /
|
|
197
|
+
assert.match(prompt, /Assessment & Acceptance Criteria/);
|
|
198
|
+
assert.match(prompt, /assessment evidence/i);
|
|
192
199
|
});
|
|
193
200
|
|
|
194
201
|
// ─── Prompt migration: replan-slice → gsd_replan_slice ────────────────
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* (e) write/edit to source path → block
|
|
14
14
|
* (f) bash command → block (could execute work)
|
|
15
15
|
* (g) registered GSD tools (gsd_milestone_generate_id, gsd_summary_save) → pass
|
|
16
|
+
* (h) unknown custom tools → block
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import test from 'node:test';
|
|
@@ -155,3 +156,11 @@ test('queue-guard: allows web search and library tools during queue mode', () =>
|
|
|
155
156
|
const r4 = shouldBlockQueueExecution('fetch_page', '', true);
|
|
156
157
|
assert.strictEqual(r4.block, false, 'fetch_page should pass');
|
|
157
158
|
});
|
|
159
|
+
|
|
160
|
+
// ─── Scenario 10: Unknown custom tools are blocked during queue mode ──
|
|
161
|
+
|
|
162
|
+
test('queue-guard: blocks unknown custom tools during queue mode', () => {
|
|
163
|
+
const result = shouldBlockQueueExecution('custom_codegen_tool', '', true);
|
|
164
|
+
assert.strictEqual(result.block, true, 'unknown custom tools should be blocked');
|
|
165
|
+
assert.ok(result.reason, 'should explain the queue restriction');
|
|
166
|
+
});
|
|
@@ -101,6 +101,25 @@ test("parseTaskPlanIO handles multiple backtick tokens on one line", () => {
|
|
|
101
101
|
assert.deepEqual(io.outputFiles, ["src/c.ts"]);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
test("parseTaskPlanIO strips inline descriptions from backtick-wrapped file references", () => {
|
|
105
|
+
const content = `# T01: Described Paths
|
|
106
|
+
|
|
107
|
+
## Inputs
|
|
108
|
+
|
|
109
|
+
- \`src/config.ts — existing configuration\`
|
|
110
|
+
- \`src/flags.ts - feature flags\`
|
|
111
|
+
|
|
112
|
+
## Expected Output
|
|
113
|
+
|
|
114
|
+
- \`definitions/ac-audit.md — current state of AC CRM\`
|
|
115
|
+
- \`docs/runbook.md - update deployment notes\`
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const io = parseTaskPlanIO(content);
|
|
119
|
+
assert.deepEqual(io.inputFiles, ["src/config.ts", "src/flags.ts"]);
|
|
120
|
+
assert.deepEqual(io.outputFiles, ["definitions/ac-audit.md", "docs/runbook.md"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
104
123
|
// ─── deriveTaskGraph ──────────────────────────────────────────────────────
|
|
105
124
|
|
|
106
125
|
test("deriveTaskGraph: linear chain T01→T02→T03", () => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { registerShortcuts } from "../bootstrap/register-shortcuts.ts";
|
|
8
|
+
|
|
9
|
+
function makeTempDir(prefix: string): string {
|
|
10
|
+
const dir = join(
|
|
11
|
+
tmpdir(),
|
|
12
|
+
`gsd-register-shortcuts-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
13
|
+
);
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cleanup(dir: string): void {
|
|
19
|
+
try {
|
|
20
|
+
rmSync(dir, { recursive: true, force: true });
|
|
21
|
+
} catch {
|
|
22
|
+
// best-effort
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("dashboard shortcut resolves the project root instead of the current worktree path", async (t) => {
|
|
27
|
+
const projectRoot = makeTempDir("project");
|
|
28
|
+
const worktreeRoot = join(projectRoot, ".gsd", "worktrees", "M001");
|
|
29
|
+
mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
|
|
30
|
+
mkdirSync(worktreeRoot, { recursive: true });
|
|
31
|
+
|
|
32
|
+
const originalCwd = process.cwd();
|
|
33
|
+
process.chdir(worktreeRoot);
|
|
34
|
+
t.after(() => {
|
|
35
|
+
process.chdir(originalCwd);
|
|
36
|
+
cleanup(projectRoot);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let capturedHandler: ((ctx: any) => Promise<void>) | null = null;
|
|
40
|
+
const shortcuts: Array<{ description: string; handler: (ctx: any) => Promise<void> }> = [];
|
|
41
|
+
const pi = {
|
|
42
|
+
registerShortcut: (_key: unknown, shortcut: { description: string; handler: (ctx: any) => Promise<void> }) => {
|
|
43
|
+
shortcuts.push(shortcut);
|
|
44
|
+
if (!capturedHandler) {
|
|
45
|
+
capturedHandler = shortcut.handler;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
registerShortcuts(pi);
|
|
51
|
+
assert.ok(capturedHandler, "dashboard shortcut is registered");
|
|
52
|
+
const dashboardShortcut = shortcuts[0];
|
|
53
|
+
assert.ok(dashboardShortcut, "dashboard shortcut is captured");
|
|
54
|
+
|
|
55
|
+
let customCalls = 0;
|
|
56
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
57
|
+
await dashboardShortcut.handler({
|
|
58
|
+
hasUI: true,
|
|
59
|
+
ui: {
|
|
60
|
+
custom: async () => {
|
|
61
|
+
customCalls++;
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
notify: (message: string, type?: string) => {
|
|
65
|
+
notices.push({ message, type });
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.ok(customCalls > 0, "shortcut opens the dashboard overlay when project root is resolved");
|
|
71
|
+
assert.equal(notices.length, 0, "shortcut does not fall back to the missing-.gsd warning");
|
|
72
|
+
assert.equal(shortcuts.length, 3, "all GSD shortcuts are still registered");
|
|
73
|
+
});
|
|
@@ -760,6 +760,104 @@ test("ask-user-questions source-level: tryRemoteQuestions is called before the h
|
|
|
760
760
|
);
|
|
761
761
|
});
|
|
762
762
|
|
|
763
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
764
|
+
// Race model tests (#3810) — local TUI races against remote channel
|
|
765
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
766
|
+
|
|
767
|
+
test("ask-user-questions source-level: raceRemoteAndLocal function exists", () => {
|
|
768
|
+
const src = readFileSync(
|
|
769
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
770
|
+
"utf-8",
|
|
771
|
+
);
|
|
772
|
+
assert.ok(
|
|
773
|
+
src.includes("async function raceRemoteAndLocal("),
|
|
774
|
+
"raceRemoteAndLocal helper should exist for racing local TUI against remote channel",
|
|
775
|
+
);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("ask-user-questions source-level: race path uses isRemoteConfigured for routing", () => {
|
|
779
|
+
const src = readFileSync(
|
|
780
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
781
|
+
"utf-8",
|
|
782
|
+
);
|
|
783
|
+
assert.ok(
|
|
784
|
+
src.includes("isRemoteConfigured()"),
|
|
785
|
+
"execute() should call isRemoteConfigured() for lightweight routing decision",
|
|
786
|
+
);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("ask-user-questions source-level: race path checks both hasRemote and ctx.hasUI", () => {
|
|
790
|
+
// Regression: #3810 — the race should only activate when BOTH remote and local UI
|
|
791
|
+
// are available. Headless mode should still use remote-only, and no-remote should
|
|
792
|
+
// use local-only.
|
|
793
|
+
const src = readFileSync(
|
|
794
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
795
|
+
"utf-8",
|
|
796
|
+
);
|
|
797
|
+
assert.ok(
|
|
798
|
+
src.includes("hasRemote && ctx.hasUI"),
|
|
799
|
+
"Race path should require both remote configured and local UI available",
|
|
800
|
+
);
|
|
801
|
+
assert.ok(
|
|
802
|
+
src.includes("hasRemote && !ctx.hasUI"),
|
|
803
|
+
"Headless path should handle remote-only when no local UI",
|
|
804
|
+
);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("ask-user-questions source-level: race treats remote timeout as non-win", () => {
|
|
808
|
+
// Regression: the whole point of the race is that a remote timeout should NOT
|
|
809
|
+
// block the local TUI. The race helper must filter out timed_out results.
|
|
810
|
+
const src = readFileSync(
|
|
811
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
812
|
+
"utf-8",
|
|
813
|
+
);
|
|
814
|
+
const raceFnStart = src.indexOf("async function raceRemoteAndLocal(");
|
|
815
|
+
const raceFnEnd = src.indexOf("\n}", raceFnStart);
|
|
816
|
+
const raceFnBody = src.slice(raceFnStart, raceFnEnd);
|
|
817
|
+
assert.ok(
|
|
818
|
+
raceFnBody.includes("timed_out"),
|
|
819
|
+
"raceRemoteAndLocal should check for timed_out in remote results",
|
|
820
|
+
);
|
|
821
|
+
assert.ok(
|
|
822
|
+
raceFnBody.includes("details?.error"),
|
|
823
|
+
"raceRemoteAndLocal should check for error in remote results",
|
|
824
|
+
);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test("ask-user-questions source-level: race uses AbortController to cancel loser", () => {
|
|
828
|
+
const src = readFileSync(
|
|
829
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
830
|
+
"utf-8",
|
|
831
|
+
);
|
|
832
|
+
assert.ok(
|
|
833
|
+
src.includes("new AbortController()"),
|
|
834
|
+
"Race path should create an AbortController for cancellation",
|
|
835
|
+
);
|
|
836
|
+
assert.ok(
|
|
837
|
+
src.includes("controller.abort()"),
|
|
838
|
+
"raceRemoteAndLocal should abort the controller to cancel the losing side",
|
|
839
|
+
);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("manager source-level: isRemoteConfigured export exists", () => {
|
|
843
|
+
const src = readFileSync(
|
|
844
|
+
join(__dirname, "..", "..", "remote-questions", "manager.ts"),
|
|
845
|
+
"utf-8",
|
|
846
|
+
);
|
|
847
|
+
assert.ok(
|
|
848
|
+
src.includes("export function isRemoteConfigured()"),
|
|
849
|
+
"manager.ts should export isRemoteConfigured for lightweight config checking",
|
|
850
|
+
);
|
|
851
|
+
// Must delegate to resolveRemoteConfig — no separate config parsing
|
|
852
|
+
const fnStart = src.indexOf("export function isRemoteConfigured()");
|
|
853
|
+
const fnEnd = src.indexOf("\n}", fnStart);
|
|
854
|
+
const fnBody = src.slice(fnStart, fnEnd);
|
|
855
|
+
assert.ok(
|
|
856
|
+
fnBody.includes("resolveRemoteConfig()"),
|
|
857
|
+
"isRemoteConfigured should delegate to resolveRemoteConfig",
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
763
861
|
test("config source-level: removeProviderToken uses auth.remove not auth.set with empty key", () => {
|
|
764
862
|
const commandSrc = readFileSync(
|
|
765
863
|
join(__dirname, "..", "..", "remote-questions", "remote-command.ts"),
|
|
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
|
|
7
7
|
const { deriveState } = await import("../state.js");
|
|
8
8
|
|
|
9
|
-
test("deriveState reports
|
|
9
|
+
test("deriveState reports the last completed milestone when all milestone slices are done", async () => {
|
|
10
10
|
const base = mkdtempSync(join(tmpdir(), "gsd-smart-entry-complete-"));
|
|
11
11
|
|
|
12
12
|
try {
|
|
@@ -31,7 +31,7 @@ test("deriveState reports complete when all milestone slices are done", async ()
|
|
|
31
31
|
|
|
32
32
|
const state = await deriveState(base);
|
|
33
33
|
assert.equal(state.phase, "complete");
|
|
34
|
-
assert.equal(state.
|
|
34
|
+
assert.equal(state.lastCompletedMilestone?.id, "M001");
|
|
35
35
|
} finally {
|
|
36
36
|
rmSync(base, { recursive: true, force: true });
|
|
37
37
|
}
|