gsd-pi 2.76.0-dev.76f9a2dc5 → 2.76.0-dev.97807402
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/claude-cli-check.js +32 -3
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
- package/dist/resources/extensions/gsd/auto/phases.js +14 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-start.js +27 -18
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
- package/dist/resources/extensions/gsd/auto.js +13 -17
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/error-classifier.js +10 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +115 -7
- package/dist/resources/extensions/gsd/guided-flow.js +189 -0
- package/dist/resources/extensions/gsd/health-widget.js +4 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/model-router.js +36 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +9 -3
- package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
- package/dist/resources/extensions/gsd/token-counter.js +22 -5
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/dist/resources/skills/write-docs/SKILL.md +2 -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 +14 -14
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
- package/packages/mcp-server/dist/remote-questions.js +732 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +18 -1
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -1
- package/packages/mcp-server/src/remote-questions.test.ts +294 -0
- package/packages/mcp-server/src/remote-questions.ts +916 -0
- package/packages/mcp-server/src/server.ts +19 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.test.json +19 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +16 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
- package/packages/pi-ai/src/providers/simple-options.ts +17 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
- package/src/resources/extensions/gsd/auto/phases.ts +14 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
- package/src/resources/extensions/gsd/auto-start.ts +29 -19
- package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
- package/src/resources/extensions/gsd/auto.ts +12 -17
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/error-classifier.ts +10 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +122 -7
- package/src/resources/extensions/gsd/guided-flow.ts +221 -0
- package/src/resources/extensions/gsd/health-widget.ts +3 -1
- package/src/resources/extensions/gsd/journal.ts +2 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/model-router.ts +42 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
- package/src/resources/extensions/gsd/preferences-types.ts +46 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +13 -2
- package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- 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/parallel-research-dispatch.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
- package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/token-counter.ts +22 -5
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/src/resources/skills/write-docs/SKILL.md +2 -1
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD-2 / guided-flow — regression tests for #4573
|
|
3
|
+
*
|
|
4
|
+
* Covers two recovery paths:
|
|
5
|
+
* - maybeHandleReadyPhraseWithoutFiles: nudge when LLM emits
|
|
6
|
+
* "Milestone M001 ready." without writing CONTEXT.md / ROADMAP.md
|
|
7
|
+
* - maybeHandleEmptyIntentTurn: nudge when LLM narrates intent but
|
|
8
|
+
* emits no tool-use blocks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, beforeEach } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
setPendingAutoStart,
|
|
19
|
+
clearPendingAutoStart,
|
|
20
|
+
maybeHandleReadyPhraseWithoutFiles,
|
|
21
|
+
maybeHandleEmptyIntentTurn,
|
|
22
|
+
resetEmptyTurnCounter,
|
|
23
|
+
} from "../guided-flow.ts";
|
|
24
|
+
|
|
25
|
+
// ─── Test harness ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
interface MockCapture {
|
|
28
|
+
notifies: Array<{ msg: string; level: string }>;
|
|
29
|
+
messages: Array<{ payload: any; options: any }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mkCapture(): MockCapture {
|
|
33
|
+
return { notifies: [], messages: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mkCtx(cap: MockCapture): any {
|
|
37
|
+
return {
|
|
38
|
+
ui: {
|
|
39
|
+
notify: (msg: string, level: string) => {
|
|
40
|
+
cap.notifies.push({ msg, level });
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mkPi(cap: MockCapture, opts: { sendThrows?: boolean } = {}): any {
|
|
47
|
+
return {
|
|
48
|
+
sendMessage: (payload: any, options: any) => {
|
|
49
|
+
if (opts.sendThrows) throw new Error("send failed");
|
|
50
|
+
cap.messages.push({ payload, options });
|
|
51
|
+
},
|
|
52
|
+
setActiveTools: () => undefined,
|
|
53
|
+
getActiveTools: () => [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mkBase(): string {
|
|
58
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-4573-"));
|
|
59
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
60
|
+
return base;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assistantMsg(text: string, opts: { toolUse?: boolean } = {}): any {
|
|
64
|
+
const content: any[] = [];
|
|
65
|
+
if (text) content.push({ type: "text", text });
|
|
66
|
+
if (opts.toolUse) content.push({ type: "tool_use", name: "whatever", input: {} });
|
|
67
|
+
return { role: "assistant", content };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── ready-phrase recovery (Layer 2) ───────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
clearPendingAutoStart();
|
|
75
|
+
resetEmptyTurnCounter();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("no pending entry → no-op", () => {
|
|
79
|
+
const cap = mkCapture();
|
|
80
|
+
const event = { messages: [assistantMsg("Milestone M001 ready.")] };
|
|
81
|
+
const handled = maybeHandleReadyPhraseWithoutFiles(event);
|
|
82
|
+
assert.equal(handled, false);
|
|
83
|
+
assert.equal(cap.messages.length, 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("pending entry, ready phrase, no files → notify + sendMessage", () => {
|
|
87
|
+
const base = mkBase();
|
|
88
|
+
try {
|
|
89
|
+
const cap = mkCapture();
|
|
90
|
+
setPendingAutoStart(base, {
|
|
91
|
+
basePath: base,
|
|
92
|
+
milestoneId: "M001",
|
|
93
|
+
ctx: mkCtx(cap),
|
|
94
|
+
pi: mkPi(cap),
|
|
95
|
+
});
|
|
96
|
+
const handled = maybeHandleReadyPhraseWithoutFiles({
|
|
97
|
+
messages: [assistantMsg("Milestone M001 ready.")],
|
|
98
|
+
});
|
|
99
|
+
assert.equal(handled, true);
|
|
100
|
+
assert.equal(cap.messages.length, 1);
|
|
101
|
+
assert.equal(cap.messages[0].payload.customType, "gsd-ready-no-files");
|
|
102
|
+
assert.equal(cap.messages[0].options.triggerTurn, true);
|
|
103
|
+
assert.ok(
|
|
104
|
+
cap.notifies.some((n) => /rejected/.test(n.msg)),
|
|
105
|
+
"user notified about rejection",
|
|
106
|
+
);
|
|
107
|
+
} finally {
|
|
108
|
+
clearPendingAutoStart();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("retry cap — after MAX_READY_REJECTS the nudge stops and entry clears", () => {
|
|
113
|
+
const base = mkBase();
|
|
114
|
+
try {
|
|
115
|
+
const cap = mkCapture();
|
|
116
|
+
setPendingAutoStart(base, {
|
|
117
|
+
basePath: base,
|
|
118
|
+
milestoneId: "M001",
|
|
119
|
+
ctx: mkCtx(cap),
|
|
120
|
+
pi: mkPi(cap),
|
|
121
|
+
});
|
|
122
|
+
const event = { messages: [assistantMsg("Milestone M001 ready.")] };
|
|
123
|
+
|
|
124
|
+
const first = maybeHandleReadyPhraseWithoutFiles(event);
|
|
125
|
+
const second = maybeHandleReadyPhraseWithoutFiles(event);
|
|
126
|
+
const third = maybeHandleReadyPhraseWithoutFiles(event); // > MAX
|
|
127
|
+
|
|
128
|
+
assert.equal(first, true);
|
|
129
|
+
assert.equal(second, true);
|
|
130
|
+
assert.equal(third, true); // still returns true (handled via give-up)
|
|
131
|
+
assert.equal(cap.messages.length, 2, "only 2 nudges sent (MAX_READY_REJECTS=2)");
|
|
132
|
+
assert.ok(
|
|
133
|
+
cap.notifies.some((n) => /Stopping auto-nudge/.test(n.msg)),
|
|
134
|
+
"gives up with error notify",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// After giving up, a fresh re-entry starts clean
|
|
138
|
+
const fourth = maybeHandleReadyPhraseWithoutFiles(event);
|
|
139
|
+
assert.equal(fourth, false, "pending entry was cleared — nothing to handle");
|
|
140
|
+
} finally {
|
|
141
|
+
clearPendingAutoStart();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("files present → no nudge (happy path already fired)", () => {
|
|
146
|
+
const base = mkBase();
|
|
147
|
+
try {
|
|
148
|
+
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# ctx");
|
|
149
|
+
const cap = mkCapture();
|
|
150
|
+
setPendingAutoStart(base, {
|
|
151
|
+
basePath: base,
|
|
152
|
+
milestoneId: "M001",
|
|
153
|
+
ctx: mkCtx(cap),
|
|
154
|
+
pi: mkPi(cap),
|
|
155
|
+
});
|
|
156
|
+
const handled = maybeHandleReadyPhraseWithoutFiles({
|
|
157
|
+
messages: [assistantMsg("Milestone M001 ready.")],
|
|
158
|
+
});
|
|
159
|
+
assert.equal(handled, false);
|
|
160
|
+
assert.equal(cap.messages.length, 0);
|
|
161
|
+
} finally {
|
|
162
|
+
clearPendingAutoStart();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("last message lacks ready phrase → no-op", () => {
|
|
167
|
+
const base = mkBase();
|
|
168
|
+
try {
|
|
169
|
+
const cap = mkCapture();
|
|
170
|
+
setPendingAutoStart(base, {
|
|
171
|
+
basePath: base,
|
|
172
|
+
milestoneId: "M001",
|
|
173
|
+
ctx: mkCtx(cap),
|
|
174
|
+
pi: mkPi(cap),
|
|
175
|
+
});
|
|
176
|
+
const handled = maybeHandleReadyPhraseWithoutFiles({
|
|
177
|
+
messages: [assistantMsg("Let me think about the slices first.")],
|
|
178
|
+
});
|
|
179
|
+
assert.equal(handled, false);
|
|
180
|
+
assert.equal(cap.messages.length, 0);
|
|
181
|
+
} finally {
|
|
182
|
+
clearPendingAutoStart();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("fresh entry after give-up resets counter", () => {
|
|
187
|
+
const base = mkBase();
|
|
188
|
+
try {
|
|
189
|
+
const cap = mkCapture();
|
|
190
|
+
// First cycle: exhaust cap
|
|
191
|
+
setPendingAutoStart(base, {
|
|
192
|
+
basePath: base,
|
|
193
|
+
milestoneId: "M001",
|
|
194
|
+
ctx: mkCtx(cap),
|
|
195
|
+
pi: mkPi(cap),
|
|
196
|
+
});
|
|
197
|
+
const event = { messages: [assistantMsg("Milestone M001 ready.")] };
|
|
198
|
+
maybeHandleReadyPhraseWithoutFiles(event);
|
|
199
|
+
maybeHandleReadyPhraseWithoutFiles(event);
|
|
200
|
+
maybeHandleReadyPhraseWithoutFiles(event); // clears entry
|
|
201
|
+
|
|
202
|
+
// New /gsd run — re-seeds entry; counter must be 0 again
|
|
203
|
+
cap.messages.length = 0;
|
|
204
|
+
setPendingAutoStart(base, {
|
|
205
|
+
basePath: base,
|
|
206
|
+
milestoneId: "M001",
|
|
207
|
+
ctx: mkCtx(cap),
|
|
208
|
+
pi: mkPi(cap),
|
|
209
|
+
});
|
|
210
|
+
const handled = maybeHandleReadyPhraseWithoutFiles(event);
|
|
211
|
+
assert.equal(handled, true);
|
|
212
|
+
assert.equal(cap.messages.length, 1, "fresh entry fires nudge again");
|
|
213
|
+
} finally {
|
|
214
|
+
clearPendingAutoStart();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ─── empty-turn recovery (Layer 3) ────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
describe("#4573 maybeHandleEmptyIntentTurn", () => {
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
clearPendingAutoStart();
|
|
224
|
+
resetEmptyTurnCounter();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("no pending entry + isAuto false → no-op (interactive discuss is user-driven)", () => {
|
|
228
|
+
const event = { messages: [assistantMsg("I'll write the CONTEXT.md now.")] };
|
|
229
|
+
const handled = maybeHandleEmptyIntentTurn(event, false);
|
|
230
|
+
assert.equal(handled, false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("text-only turn WITHOUT commit phrase → not flagged (legitimate text)", () => {
|
|
234
|
+
const base = mkBase();
|
|
235
|
+
try {
|
|
236
|
+
const cap = mkCapture();
|
|
237
|
+
setPendingAutoStart(base, {
|
|
238
|
+
basePath: base,
|
|
239
|
+
milestoneId: "M001",
|
|
240
|
+
ctx: mkCtx(cap),
|
|
241
|
+
pi: mkPi(cap),
|
|
242
|
+
});
|
|
243
|
+
const handled = maybeHandleEmptyIntentTurn(
|
|
244
|
+
{ messages: [assistantMsg("Here is the roadmap preview — three slices.")] },
|
|
245
|
+
false,
|
|
246
|
+
);
|
|
247
|
+
assert.equal(handled, false);
|
|
248
|
+
assert.equal(cap.messages.length, 0);
|
|
249
|
+
} finally {
|
|
250
|
+
clearPendingAutoStart();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("text-only turn ending in question → treated as user-handoff, not flagged", () => {
|
|
255
|
+
const base = mkBase();
|
|
256
|
+
try {
|
|
257
|
+
const cap = mkCapture();
|
|
258
|
+
setPendingAutoStart(base, {
|
|
259
|
+
basePath: base,
|
|
260
|
+
milestoneId: "M001",
|
|
261
|
+
ctx: mkCtx(cap),
|
|
262
|
+
pi: mkPi(cap),
|
|
263
|
+
});
|
|
264
|
+
const handled = maybeHandleEmptyIntentTurn(
|
|
265
|
+
{ messages: [assistantMsg("Ready to write, or want to adjust?")] },
|
|
266
|
+
false,
|
|
267
|
+
);
|
|
268
|
+
assert.equal(handled, false);
|
|
269
|
+
} finally {
|
|
270
|
+
clearPendingAutoStart();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("commit-intent phrase WITHOUT tool call → nudge fires", () => {
|
|
275
|
+
const base = mkBase();
|
|
276
|
+
try {
|
|
277
|
+
const cap = mkCapture();
|
|
278
|
+
setPendingAutoStart(base, {
|
|
279
|
+
basePath: base,
|
|
280
|
+
milestoneId: "M001",
|
|
281
|
+
ctx: mkCtx(cap),
|
|
282
|
+
pi: mkPi(cap),
|
|
283
|
+
});
|
|
284
|
+
const handled = maybeHandleEmptyIntentTurn(
|
|
285
|
+
{ messages: [assistantMsg("I'll now write the CONTEXT.md file.")] },
|
|
286
|
+
false,
|
|
287
|
+
);
|
|
288
|
+
assert.equal(handled, true);
|
|
289
|
+
assert.equal(cap.messages.length, 1);
|
|
290
|
+
assert.equal(cap.messages[0].payload.customType, "gsd-empty-turn-recovery");
|
|
291
|
+
} finally {
|
|
292
|
+
clearPendingAutoStart();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("commit-intent WITH tool-use block → not flagged", () => {
|
|
297
|
+
const base = mkBase();
|
|
298
|
+
try {
|
|
299
|
+
const cap = mkCapture();
|
|
300
|
+
setPendingAutoStart(base, {
|
|
301
|
+
basePath: base,
|
|
302
|
+
milestoneId: "M001",
|
|
303
|
+
ctx: mkCtx(cap),
|
|
304
|
+
pi: mkPi(cap),
|
|
305
|
+
});
|
|
306
|
+
const handled = maybeHandleEmptyIntentTurn(
|
|
307
|
+
{ messages: [assistantMsg("I'll write the file now.", { toolUse: true })] },
|
|
308
|
+
false,
|
|
309
|
+
);
|
|
310
|
+
assert.equal(handled, false);
|
|
311
|
+
assert.equal(cap.messages.length, 0);
|
|
312
|
+
} finally {
|
|
313
|
+
clearPendingAutoStart();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("ready phrase is NOT treated as empty-turn (handled by other recovery path)", () => {
|
|
318
|
+
const base = mkBase();
|
|
319
|
+
try {
|
|
320
|
+
const cap = mkCapture();
|
|
321
|
+
setPendingAutoStart(base, {
|
|
322
|
+
basePath: base,
|
|
323
|
+
milestoneId: "M001",
|
|
324
|
+
ctx: mkCtx(cap),
|
|
325
|
+
pi: mkPi(cap),
|
|
326
|
+
});
|
|
327
|
+
const handled = maybeHandleEmptyIntentTurn(
|
|
328
|
+
{ messages: [assistantMsg("Milestone M001 ready.")] },
|
|
329
|
+
false,
|
|
330
|
+
);
|
|
331
|
+
assert.equal(handled, false);
|
|
332
|
+
} finally {
|
|
333
|
+
clearPendingAutoStart();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("empty-turn retry cap — stops after MAX_EMPTY_TURN_RETRIES", () => {
|
|
338
|
+
const base = mkBase();
|
|
339
|
+
try {
|
|
340
|
+
const cap = mkCapture();
|
|
341
|
+
setPendingAutoStart(base, {
|
|
342
|
+
basePath: base,
|
|
343
|
+
milestoneId: "M001",
|
|
344
|
+
ctx: mkCtx(cap),
|
|
345
|
+
pi: mkPi(cap),
|
|
346
|
+
});
|
|
347
|
+
const event = { messages: [assistantMsg("I'll write the CONTEXT.md file.")] };
|
|
348
|
+
|
|
349
|
+
maybeHandleEmptyIntentTurn(event, false); // 1
|
|
350
|
+
maybeHandleEmptyIntentTurn(event, false); // 2
|
|
351
|
+
const third = maybeHandleEmptyIntentTurn(event, false); // > cap
|
|
352
|
+
|
|
353
|
+
assert.equal(cap.messages.length, 2, "only 2 nudges sent");
|
|
354
|
+
assert.equal(third, false, "after cap, no further injection");
|
|
355
|
+
assert.ok(
|
|
356
|
+
cap.notifies.some((n) => /Stopping auto-nudge/.test(n.msg)),
|
|
357
|
+
"user notified of give-up",
|
|
358
|
+
);
|
|
359
|
+
} finally {
|
|
360
|
+
clearPendingAutoStart();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("resetEmptyTurnCounter clears state after a successful tool-use turn", () => {
|
|
365
|
+
const base = mkBase();
|
|
366
|
+
try {
|
|
367
|
+
const cap = mkCapture();
|
|
368
|
+
setPendingAutoStart(base, {
|
|
369
|
+
basePath: base,
|
|
370
|
+
milestoneId: "M001",
|
|
371
|
+
ctx: mkCtx(cap),
|
|
372
|
+
pi: mkPi(cap),
|
|
373
|
+
});
|
|
374
|
+
const event = { messages: [assistantMsg("I'll write the CONTEXT.md file.")] };
|
|
375
|
+
|
|
376
|
+
maybeHandleEmptyIntentTurn(event, false); // 1
|
|
377
|
+
maybeHandleEmptyIntentTurn(event, false); // 2 — at cap
|
|
378
|
+
resetEmptyTurnCounter(); // simulate a successful tool-use turn in between
|
|
379
|
+
|
|
380
|
+
cap.messages.length = 0;
|
|
381
|
+
const after = maybeHandleEmptyIntentTurn(event, false);
|
|
382
|
+
assert.equal(after, true, "counter reset — nudge fires again");
|
|
383
|
+
assert.equal(cap.messages.length, 1);
|
|
384
|
+
} finally {
|
|
385
|
+
clearPendingAutoStart();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -45,9 +45,15 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
it('savedTools is restored after sendMessage', () => {
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// #4573: guided-flow.ts now contains multiple `triggerTurn: true` calls
|
|
49
|
+
// (ready-phrase and empty-turn recovery paths). The discuss-flow scoping
|
|
50
|
+
// sendMessage is the one that follows `savedTools = currentTools`, so
|
|
51
|
+
// anchor the search there rather than at the first `triggerTurn: true`.
|
|
52
|
+
const savedToolsAssign = src.indexOf('savedTools = currentTools')
|
|
53
|
+
assert.ok(savedToolsAssign !== -1, 'savedTools = currentTools must exist')
|
|
54
|
+
|
|
55
|
+
const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
|
|
56
|
+
assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
|
|
51
57
|
|
|
52
58
|
// After sendMessage, savedTools should be restored via setActiveTools
|
|
53
59
|
const afterSend = src.slice(sendMsg, sendMsg + 500)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test suite for save_gate_result renderResult.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that renderResult does not print "undefined: undefined" when
|
|
5
|
+
* `details` is empty, and that the error fallback does not produce a
|
|
6
|
+
* duplicated `Error: Error:` prefix when `content[0].text` already starts
|
|
7
|
+
* with `Error:`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { registerDbTools } from '../bootstrap/db-tools.ts';
|
|
13
|
+
|
|
14
|
+
function makeMockPi() {
|
|
15
|
+
const tools: any[] = [];
|
|
16
|
+
return {
|
|
17
|
+
registerTool: (tool: any) => tools.push(tool),
|
|
18
|
+
tools,
|
|
19
|
+
} as any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fakeTheme = {
|
|
23
|
+
fg: (_color: string, text: string) => text,
|
|
24
|
+
bold: (text: string) => text,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function getSaveGateResultTool() {
|
|
28
|
+
const pi = makeMockPi();
|
|
29
|
+
registerDbTools(pi);
|
|
30
|
+
const tool = pi.tools.find((t: any) => t.name === 'gsd_save_gate_result');
|
|
31
|
+
assert.ok(tool, 'gsd_save_gate_result should be registered');
|
|
32
|
+
return tool;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('save_gate_result renderResult falls back to content text when details is empty', () => {
|
|
36
|
+
const tool = getSaveGateResultTool();
|
|
37
|
+
const result = {
|
|
38
|
+
content: [{ type: 'text', text: 'Gate Q3 result saved: verdict=pass' }],
|
|
39
|
+
details: {},
|
|
40
|
+
isError: false,
|
|
41
|
+
};
|
|
42
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
43
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
44
|
+
assert.ok(!text.includes('undefined'), `got: ${text}`);
|
|
45
|
+
assert.ok(
|
|
46
|
+
text.includes('Gate Q3') || text.includes('verdict=pass'),
|
|
47
|
+
`expected content summary — got: ${text}`,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('save_gate_result renderResult uses structured details when present', () => {
|
|
52
|
+
const tool = getSaveGateResultTool();
|
|
53
|
+
const result = {
|
|
54
|
+
content: [{ type: 'text', text: 'Gate Q3 result saved: verdict=flag' }],
|
|
55
|
+
details: { operation: 'save_gate_result', gateId: 'Q3', verdict: 'flag' },
|
|
56
|
+
isError: false,
|
|
57
|
+
};
|
|
58
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
59
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
60
|
+
assert.ok(text.includes('Q3'), `got: ${text}`);
|
|
61
|
+
assert.ok(text.includes('flag'), `got: ${text}`);
|
|
62
|
+
assert.ok(!text.includes('undefined'), `got: ${text}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('save_gate_result renderResult shows error from content when details.error is missing', () => {
|
|
66
|
+
const tool = getSaveGateResultTool();
|
|
67
|
+
const result = {
|
|
68
|
+
content: [{ type: 'text', text: 'Error: Invalid gateId "Z1"' }],
|
|
69
|
+
details: {},
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
73
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
74
|
+
assert.ok(
|
|
75
|
+
text.includes('Invalid gateId') || text.includes('Error'),
|
|
76
|
+
`got: ${text}`,
|
|
77
|
+
);
|
|
78
|
+
assert.ok(!text.includes('undefined'), `got: ${text}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('save_gate_result renderResult does not duplicate Error: prefix', () => {
|
|
82
|
+
const tool = getSaveGateResultTool();
|
|
83
|
+
const result = {
|
|
84
|
+
content: [{ type: 'text', text: 'Error: Invalid gateId "Z1"' }],
|
|
85
|
+
details: {},
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
const rendered = tool.renderResult(result, {}, fakeTheme);
|
|
89
|
+
const text = String(rendered.content ?? rendered.text ?? rendered);
|
|
90
|
+
assert.ok(
|
|
91
|
+
!/Error:\s*Error:/i.test(text),
|
|
92
|
+
`expected a single Error: prefix — got: ${text}`,
|
|
93
|
+
);
|
|
94
|
+
assert.ok(text.includes('Invalid gateId'), `got: ${text}`);
|
|
95
|
+
});
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-start-footer.test.ts
|
|
3
3
|
*
|
|
4
|
-
* Verifies that register-hooks.ts suppresses the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Verifies that register-hooks.ts suppresses the gsd-health widget (not the
|
|
5
|
+
* built-in footer) when isAutoActive() is true, and that setFooter is never
|
|
6
|
+
* called by the extension in either session_start or session_switch.
|
|
7
7
|
*
|
|
8
8
|
* Testing strategy:
|
|
9
|
-
*
|
|
10
|
-
* 1. Source-code regression guard: ensures the guard and setFooter call are
|
|
11
|
-
* structurally present in register-hooks.ts for both event handlers.
|
|
12
|
-
* (node:test does not support mock.module without --experimental-test-module-mocks,
|
|
13
|
-
* so structural analysis is the correct approach here.)
|
|
9
|
+
* 1. Source-code regression guards: structural checks on register-hooks.ts.
|
|
14
10
|
* 2. Behavioral integration test: fires the live session_start handler with a
|
|
15
|
-
* fake ctx when isAutoActive() is false (
|
|
16
|
-
*
|
|
11
|
+
* fake ctx when isAutoActive() is false (default) and confirms neither
|
|
12
|
+
* setFooter nor setWidget("gsd-health") is called.
|
|
17
13
|
*
|
|
18
14
|
* Relates to #4314.
|
|
19
15
|
*/
|
|
@@ -35,16 +31,14 @@ const HOOKS_SOURCE = readFileSync(
|
|
|
35
31
|
|
|
36
32
|
// ─── Source-code regression guards ──────────────────────────────────────────
|
|
37
33
|
|
|
38
|
-
test("register-hooks.ts
|
|
34
|
+
test("register-hooks.ts does NOT import hideFooter", () => {
|
|
39
35
|
assert.ok(
|
|
40
|
-
HOOKS_SOURCE.includes(
|
|
41
|
-
|
|
42
|
-
"register-hooks.ts must import hideFooter from auto-dashboard.js",
|
|
36
|
+
!HOOKS_SOURCE.includes("hideFooter"),
|
|
37
|
+
"register-hooks.ts must not reference hideFooter — footer is no longer swapped in auto mode",
|
|
43
38
|
);
|
|
44
39
|
});
|
|
45
40
|
|
|
46
|
-
test("session_start handler
|
|
47
|
-
// Locate the session_start handler body (up to the next pi.on call)
|
|
41
|
+
test("session_start handler guards initHealthWidget with !isAutoActive()", () => {
|
|
48
42
|
const sessionStartIdx = HOOKS_SOURCE.indexOf('"session_start"');
|
|
49
43
|
assert.ok(sessionStartIdx > -1, "session_start handler must exist");
|
|
50
44
|
|
|
@@ -58,20 +52,23 @@ test("session_start handler calls ctx.ui.setFooter(hideFooter) when isAutoActive
|
|
|
58
52
|
"session_start handler must call isAutoActive()",
|
|
59
53
|
);
|
|
60
54
|
assert.ok(
|
|
61
|
-
sessionStartBody.includes("
|
|
62
|
-
"session_start handler must
|
|
55
|
+
sessionStartBody.includes("initHealthWidget"),
|
|
56
|
+
"session_start handler must reference initHealthWidget",
|
|
57
|
+
);
|
|
58
|
+
assert.ok(
|
|
59
|
+
!sessionStartBody.includes("setFooter"),
|
|
60
|
+
"session_start handler must NOT call setFooter",
|
|
63
61
|
);
|
|
64
62
|
|
|
65
|
-
// Guard must wrap the setFooter call
|
|
66
63
|
const guardIdx = sessionStartBody.indexOf("isAutoActive()");
|
|
67
|
-
const
|
|
64
|
+
const healthIdx = sessionStartBody.indexOf("initHealthWidget");
|
|
68
65
|
assert.ok(
|
|
69
|
-
guardIdx <
|
|
70
|
-
"isAutoActive() guard must appear before
|
|
66
|
+
guardIdx < healthIdx,
|
|
67
|
+
"isAutoActive() guard must appear before initHealthWidget in session_start",
|
|
71
68
|
);
|
|
72
69
|
});
|
|
73
70
|
|
|
74
|
-
test("session_switch handler
|
|
71
|
+
test("session_switch handler suppresses gsd-health when isAutoActive()", () => {
|
|
75
72
|
const sessionSwitchIdx = HOOKS_SOURCE.indexOf('"session_switch"');
|
|
76
73
|
assert.ok(sessionSwitchIdx > -1, "session_switch handler must exist");
|
|
77
74
|
|
|
@@ -85,21 +82,18 @@ test("session_switch handler calls ctx.ui.setFooter(hideFooter) when isAutoActiv
|
|
|
85
82
|
"session_switch handler must call isAutoActive()",
|
|
86
83
|
);
|
|
87
84
|
assert.ok(
|
|
88
|
-
sessionSwitchBody.includes(
|
|
89
|
-
"session_switch handler must call
|
|
85
|
+
sessionSwitchBody.includes('setWidget("gsd-health", undefined)'),
|
|
86
|
+
"session_switch handler must call setWidget(\"gsd-health\", undefined) when auto is active",
|
|
90
87
|
);
|
|
91
|
-
|
|
92
|
-
const guardIdx = sessionSwitchBody.indexOf("isAutoActive()");
|
|
93
|
-
const setFooterIdx = sessionSwitchBody.indexOf("ctx.ui.setFooter(hideFooter)");
|
|
94
88
|
assert.ok(
|
|
95
|
-
|
|
96
|
-
"
|
|
89
|
+
!sessionSwitchBody.includes("setFooter"),
|
|
90
|
+
"session_switch handler must NOT call setFooter",
|
|
97
91
|
);
|
|
98
92
|
});
|
|
99
93
|
|
|
100
|
-
// ─── Behavioral test: setFooter
|
|
94
|
+
// ─── Behavioral test: neither setFooter nor health suppression when auto inactive ─
|
|
101
95
|
|
|
102
|
-
test("session_start does NOT call setFooter when isAutoActive() is false
|
|
96
|
+
test("session_start does NOT call setFooter or suppress gsd-health when isAutoActive() is false", async (t) => {
|
|
103
97
|
const dir = join(
|
|
104
98
|
tmpdir(),
|
|
105
99
|
`gsd-footer-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -114,6 +108,7 @@ test("session_start does NOT call setFooter when isAutoActive() is false (defaul
|
|
|
114
108
|
});
|
|
115
109
|
|
|
116
110
|
let setFooterCallCount = 0;
|
|
111
|
+
let healthWidgetHideCount = 0;
|
|
117
112
|
|
|
118
113
|
const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
|
|
119
114
|
const pi = {
|
|
@@ -137,17 +132,14 @@ test("session_start does NOT call setFooter when isAutoActive() is false (defaul
|
|
|
137
132
|
},
|
|
138
133
|
setWorkingMessage: () => {},
|
|
139
134
|
onTerminalInput: () => () => {},
|
|
140
|
-
setWidget: () => {
|
|
135
|
+
setWidget: (key: string, value: unknown) => {
|
|
136
|
+
if (key === "gsd-health" && value === undefined) healthWidgetHideCount++;
|
|
137
|
+
},
|
|
141
138
|
},
|
|
142
139
|
sessionManager: { getSessionId: () => null },
|
|
143
140
|
model: null,
|
|
144
141
|
} as any);
|
|
145
142
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
assert.equal(
|
|
149
|
-
setFooterCallCount,
|
|
150
|
-
0,
|
|
151
|
-
"setFooter must NOT be called when isAutoActive() returns false",
|
|
152
|
-
);
|
|
143
|
+
assert.equal(setFooterCallCount, 0, "setFooter must NOT be called when isAutoActive() is false");
|
|
144
|
+
assert.equal(healthWidgetHideCount, 0, "gsd-health must NOT be hidden when isAutoActive() is false");
|
|
153
145
|
});
|