gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.97f5583d9
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/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +2 -8
- 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 +42 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
- package/dist/resources/extensions/gsd/auto/session.js +12 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +16 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +25 -2
- package/dist/resources/extensions/gsd/auto-prompts.js +14 -0
- 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 +51 -53
- package/dist/resources/extensions/gsd/auto.js +55 -27
- 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 +51 -5
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +34 -2
- package/dist/resources/extensions/gsd/clean-root-preflight.js +93 -0
- 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/init-wizard.js +15 -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/evidence-collector.js +96 -0
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +13 -5
- package/dist/resources/extensions/gsd/safety/safety-harness.js +5 -1
- 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/uok/plan-v2.js +20 -3
- 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 +10 -10
- 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 +10 -10
- 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 +7 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +41 -4
- 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/mcp-server.test.ts +30 -0
- 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 +62 -10
- 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-auth.test.js +1 -1
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +27 -4
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/minimax-tool-name.test.js +80 -0
- package/packages/pi-ai/dist/providers/minimax-tool-name.test.js.map +1 -0
- 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-auth.test.ts +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +26 -5
- package/packages/pi-ai/src/providers/anthropic.ts +9 -3
- package/packages/pi-ai/src/providers/minimax-tool-name.test.ts +98 -0
- 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/components/chat-frame.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.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/components/chat-frame.ts +6 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
- 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/loop-deps.ts +13 -0
- package/src/resources/extensions/gsd/auto/phases.ts +66 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
- package/src/resources/extensions/gsd/auto/session.ts +22 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +16 -3
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +28 -1
- 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 +62 -63
- package/src/resources/extensions/gsd/auto.ts +58 -27
- 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 +53 -5
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +35 -2
- package/src/resources/extensions/gsd/clean-root-preflight.ts +111 -0
- 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/init-wizard.ts +15 -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/evidence-collector.ts +119 -0
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +17 -4
- package/src/resources/extensions/gsd/safety/safety-harness.ts +9 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +186 -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/custom-engine-loop-integration.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +1 -1
- 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 +58 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
- 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/journal-integration.test.ts +2 -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-exec-gate-loop.test.ts +272 -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/safety-harness-false-positives.test.ts +205 -0
- 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/uok-plan-v2-wiring.test.ts +23 -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/uok/plan-v2.ts +26 -3
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -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/{ecSsu49rxxcpbNmVP4mLD → lLdDRDspgYzfz0bJAmUSz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → lLdDRDspgYzfz0bJAmUSz}/_ssgManifest.js +0 -0
|
@@ -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
|
});
|
|
@@ -378,3 +378,59 @@ test("#2505: back-to-back merges preserve queued CONTEXT files", () => {
|
|
|
378
378
|
rmSync(repo, { recursive: true, force: true });
|
|
379
379
|
}
|
|
380
380
|
});
|
|
381
|
+
|
|
382
|
+
// #4573: When `.gsd` is a gitignored symlink (ADR-002 layout) and the project
|
|
383
|
+
// `.gitignore` contains `.gsd`, `git stash push --include-untracked -- <pathspec>`
|
|
384
|
+
// fatals with "The following paths are ignored by one of your .gitignore files".
|
|
385
|
+
// The prior tests used a symlinked `.gsd` but no `.gitignore`, so this failure
|
|
386
|
+
// mode was invisible to CI. Fixture must include BOTH the symlink AND the
|
|
387
|
+
// ignore rule to reproduce the bug on pre-fix code.
|
|
388
|
+
test("#4573: gitignored .gsd symlink does not break pre-merge stash", () => {
|
|
389
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "wt-4573-ignored-symlink-")));
|
|
390
|
+
const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "wt-4573-state-")));
|
|
391
|
+
try {
|
|
392
|
+
run("git init", repo);
|
|
393
|
+
run("git config user.email test@test.com", repo);
|
|
394
|
+
run("git config user.name Test", repo);
|
|
395
|
+
writeFileSync(join(repo, "README.md"), "# test\n");
|
|
396
|
+
// Matches what BASELINE_PATTERNS in gitignore.ts writes for real projects.
|
|
397
|
+
writeFileSync(join(repo, ".gitignore"), ".gsd\n.gsd-id\n");
|
|
398
|
+
symlinkSync(stateDir, join(repo, ".gsd"));
|
|
399
|
+
run("git add README.md .gitignore", repo);
|
|
400
|
+
run("git commit -m init", repo);
|
|
401
|
+
run("git branch -M main", repo);
|
|
402
|
+
|
|
403
|
+
const wtPath = createAutoWorktree(repo, "M001");
|
|
404
|
+
const worktreeName = wtPath.replaceAll("\\", "/").split("/").pop() || "M001";
|
|
405
|
+
const sliceBranch = `slice/${worktreeName}/S01`;
|
|
406
|
+
run(`git checkout -b "${sliceBranch}"`, wtPath);
|
|
407
|
+
writeFileSync(join(wtPath, "app.ts"), "export const app = true;\n");
|
|
408
|
+
run("git add app.ts", wtPath);
|
|
409
|
+
run('git commit -m "add feature"', wtPath);
|
|
410
|
+
run("git checkout milestone/M001", wtPath);
|
|
411
|
+
run(`git merge --no-ff "${sliceBranch}" -m "merge S01"`, wtPath);
|
|
412
|
+
|
|
413
|
+
// Dirty a tracked file so the pre-merge stash branch actually runs.
|
|
414
|
+
writeFileSync(join(repo, "README.md"), "# test\n\nDirty.\n");
|
|
415
|
+
|
|
416
|
+
const result = mergeMilestoneToMain(
|
|
417
|
+
repo,
|
|
418
|
+
"M001",
|
|
419
|
+
makeRoadmap("M001", "Feature", [{ id: "S01", title: "Feature" }]),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
assert.ok(
|
|
423
|
+
result.commitMessage.includes("GSD-Milestone: M001"),
|
|
424
|
+
"merge must succeed despite gitignored .gsd symlink",
|
|
425
|
+
);
|
|
426
|
+
assert.ok(existsSync(join(repo, "app.ts")), "milestone code merged to main");
|
|
427
|
+
assert.equal(
|
|
428
|
+
lstatSync(join(repo, ".gsd")).isSymbolicLink(),
|
|
429
|
+
true,
|
|
430
|
+
".gsd symlink remains in place",
|
|
431
|
+
);
|
|
432
|
+
} finally {
|
|
433
|
+
rmSync(repo, { recursive: true, force: true });
|
|
434
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
@@ -102,7 +102,8 @@ describe("token-counter: estimateTokensForProvider", () => {
|
|
|
102
102
|
|
|
103
103
|
describe("token-counter: backward compatibility", () => {
|
|
104
104
|
it("countTokensSync returns heuristic estimate when tiktoken is not loaded", () => {
|
|
105
|
-
// Without tiktoken loaded, countTokensSync falls back to
|
|
105
|
+
// Without tiktoken loaded, countTokensSync falls back to estimateTokensForProvider.
|
|
106
|
+
// With no provider (defaults to "unknown", ratio 4.0): ceil(100/4) = 25.
|
|
106
107
|
const text = "A".repeat(100);
|
|
107
108
|
const result = countTokensSync(text);
|
|
108
109
|
// Either tiktoken is loaded (exact count) or heuristic (ceil(100/4) = 25)
|
|
@@ -127,3 +128,106 @@ describe("token-counter: backward compatibility", () => {
|
|
|
127
128
|
assert.equal(result, 0);
|
|
128
129
|
});
|
|
129
130
|
});
|
|
131
|
+
|
|
132
|
+
// ─── provider-aware fallback (issue #4529) ───────────────────────────────────
|
|
133
|
+
// Regression tests: countTokens/countTokensSync must use provider-specific
|
|
134
|
+
// ratios for their heuristic fallback, not a hardcoded GPT-4o / 4 divisor.
|
|
135
|
+
|
|
136
|
+
describe("token-counter: provider-aware heuristic fallback", () => {
|
|
137
|
+
// These tests exercise the heuristic path (no tiktoken or before init).
|
|
138
|
+
// We call estimateTokensForProvider directly to validate expected values,
|
|
139
|
+
// then verify countTokens/countTokensSync return the same values when
|
|
140
|
+
// tiktoken is unavailable.
|
|
141
|
+
|
|
142
|
+
it("countTokensSync uses anthropic ratio (3.5) when provider is 'anthropic'", () => {
|
|
143
|
+
const text = "A".repeat(350);
|
|
144
|
+
// anthropic: ceil(350 / 3.5) = 100
|
|
145
|
+
// openai/unknown: ceil(350 / 4.0) = 88
|
|
146
|
+
// These are different — the provider must matter.
|
|
147
|
+
const anthropicEstimate = estimateTokensForProvider(text, "anthropic");
|
|
148
|
+
const unknownEstimate = estimateTokensForProvider(text, "unknown");
|
|
149
|
+
assert.equal(anthropicEstimate, 100, "anthropic ratio should give 100 tokens for 350 chars");
|
|
150
|
+
assert.equal(unknownEstimate, 88, "unknown ratio should give 88 tokens for 350 chars");
|
|
151
|
+
assert.notEqual(
|
|
152
|
+
anthropicEstimate,
|
|
153
|
+
unknownEstimate,
|
|
154
|
+
"anthropic and unknown estimates must differ — if they are equal the provider is being ignored",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Actually call countTokensSync with the anthropic provider.
|
|
158
|
+
// When tiktoken is not loaded, this must return the provider-aware estimate (100).
|
|
159
|
+
// When tiktoken is loaded, it returns the tiktoken count (which is also > 0 and
|
|
160
|
+
// will be in the range [88, 120] for 350 "A" characters with cl100k_base).
|
|
161
|
+
const syncResult = countTokensSync(text, "anthropic");
|
|
162
|
+
assert.ok(typeof syncResult === "number", "countTokensSync must return a number");
|
|
163
|
+
assert.ok(syncResult > 0, "countTokensSync must return a positive count");
|
|
164
|
+
// If tiktoken is unavailable the result must exactly match the anthropic heuristic.
|
|
165
|
+
// If tiktoken is available we cannot assert the exact value, but we know it will
|
|
166
|
+
// not equal the unknown-provider heuristic (88) for 350 identical characters.
|
|
167
|
+
const tiktokenAvailable = syncResult !== anthropicEstimate;
|
|
168
|
+
if (!tiktokenAvailable) {
|
|
169
|
+
assert.equal(
|
|
170
|
+
syncResult,
|
|
171
|
+
anthropicEstimate,
|
|
172
|
+
`countTokensSync with 'anthropic' provider must return ${anthropicEstimate} (not the unknown-provider value ${unknownEstimate}) when tiktoken is unavailable`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("countTokens uses anthropic ratio when provider='anthropic' and tiktoken unavailable", async () => {
|
|
178
|
+
const text = "A".repeat(350);
|
|
179
|
+
// anthropic heuristic: ceil(350 / 3.5) = 100
|
|
180
|
+
// unknown/hardcoded-4 heuristic: ceil(350 / 4.0) = 88
|
|
181
|
+
const anthropicEstimate = estimateTokensForProvider(text, "anthropic"); // 100
|
|
182
|
+
const unknownEstimate = estimateTokensForProvider(text, "unknown"); // 88
|
|
183
|
+
const hardcodedFallback = Math.ceil(text.length / 4); // 88
|
|
184
|
+
|
|
185
|
+
// The anthropic heuristic must produce more tokens than the old /4 fallback.
|
|
186
|
+
assert.equal(anthropicEstimate, 100, "anthropic heuristic should give 100 tokens for 350 chars");
|
|
187
|
+
assert.ok(
|
|
188
|
+
anthropicEstimate > hardcodedFallback,
|
|
189
|
+
`anthropic estimate (${anthropicEstimate}) must exceed the hardcoded /4 fallback (${hardcodedFallback})`,
|
|
190
|
+
);
|
|
191
|
+
assert.notEqual(
|
|
192
|
+
anthropicEstimate,
|
|
193
|
+
unknownEstimate,
|
|
194
|
+
"anthropic and unknown estimates must differ — provider is being ignored if equal",
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Call countTokens with the anthropic provider and verify the result.
|
|
198
|
+
const result = await countTokens(text, "anthropic");
|
|
199
|
+
assert.ok(typeof result === "number", "should return a number");
|
|
200
|
+
assert.ok(result > 0, "should return a positive token count");
|
|
201
|
+
|
|
202
|
+
// When tiktoken is unavailable: result must equal the anthropic heuristic (100),
|
|
203
|
+
// NOT the unknown-provider heuristic (88). This is the core regression guard for #4529.
|
|
204
|
+
// When tiktoken IS available: result is the tiktoken count, which will differ from
|
|
205
|
+
// the heuristic — but it must never equal the wrong (unknown) heuristic for this text.
|
|
206
|
+
if (result === anthropicEstimate || result === unknownEstimate) {
|
|
207
|
+
// We are on the heuristic path — assert the correct provider ratio was used.
|
|
208
|
+
assert.equal(
|
|
209
|
+
result,
|
|
210
|
+
anthropicEstimate,
|
|
211
|
+
`countTokens with 'anthropic' provider returned ${result} but expected ${anthropicEstimate} (anthropic heuristic) not ${unknownEstimate} (unknown/hardcoded heuristic)`,
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
// tiktoken is active — result is an exact BPE count.
|
|
215
|
+
// For 350 identical "A" characters cl100k_base produces a count in [80, 130].
|
|
216
|
+
assert.ok(
|
|
217
|
+
result >= 80 && result <= 130,
|
|
218
|
+
`tiktoken count ${result} for 350 "A" chars is outside expected range [80, 130]`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("countTokens with provider='anthropic' yields more tokens than provider='openai' (heuristic)", () => {
|
|
224
|
+
const text = "A".repeat(400);
|
|
225
|
+
// anthropic: ceil(400/3.5) = 115, openai: ceil(400/4.0) = 100
|
|
226
|
+
const anthropic = estimateTokensForProvider(text, "anthropic");
|
|
227
|
+
const openai = estimateTokensForProvider(text, "openai");
|
|
228
|
+
assert.ok(
|
|
229
|
+
anthropic > openai,
|
|
230
|
+
`anthropic estimate (${anthropic}) must exceed openai estimate (${openai}) for same text`,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isToolCompatibleWithProvider,
|
|
15
15
|
filterToolsForProvider,
|
|
16
16
|
adjustToolSet,
|
|
17
|
+
GROQ_MAX_TOOLS,
|
|
17
18
|
} from "../model-router.js";
|
|
18
19
|
|
|
19
20
|
import {
|
|
@@ -197,3 +198,109 @@ describe("adjustToolSet", () => {
|
|
|
197
198
|
assert.deepEqual(removedTools, ["mcp_complex"]);
|
|
198
199
|
});
|
|
199
200
|
});
|
|
201
|
+
|
|
202
|
+
// ─── GROQ_MAX_TOOLS constant ─────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
describe("GROQ_MAX_TOOLS", () => {
|
|
205
|
+
test("equals 128", () => {
|
|
206
|
+
assert.equal(GROQ_MAX_TOOLS, 128);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ─── Groq tool-count cap (#4376) ────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe("filterToolsForProvider — Groq 128-tool cap", () => {
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
resetToolCompatibilityRegistry();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("does not cap when provider is not groq", () => {
|
|
218
|
+
const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
|
|
219
|
+
const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions");
|
|
220
|
+
assert.equal(compatible.length, 200);
|
|
221
|
+
assert.equal(filtered.length, 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("does not cap when <= 128 tools with groq provider", () => {
|
|
225
|
+
const toolNames = Array.from({ length: 128 }, (_, i) => `tool_${i}`);
|
|
226
|
+
const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
227
|
+
assert.equal(compatible.length, 128);
|
|
228
|
+
assert.equal(filtered.length, 0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("caps to 128 when >128 tools with groq provider", () => {
|
|
232
|
+
const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
|
|
233
|
+
const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
234
|
+
assert.equal(compatible.length, 128);
|
|
235
|
+
assert.equal(filtered.length, 72);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("keeps the first 128 tools when capping", () => {
|
|
239
|
+
const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
|
|
240
|
+
const { compatible } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
241
|
+
assert.equal(compatible[0], "tool_0");
|
|
242
|
+
assert.equal(compatible[127], "tool_127");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("trimmed tools appear in filtered list", () => {
|
|
246
|
+
const toolNames = Array.from({ length: 130 }, (_, i) => `tool_${i}`);
|
|
247
|
+
const { filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
248
|
+
assert.equal(filtered.length, 2);
|
|
249
|
+
assert.equal(filtered[0], "tool_128");
|
|
250
|
+
assert.equal(filtered[1], "tool_129");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("emits a warning when tools are trimmed", () => {
|
|
254
|
+
const warnings: string[] = [];
|
|
255
|
+
const original = console.warn;
|
|
256
|
+
console.warn = (...args: unknown[]) => { warnings.push(String(args[0])); };
|
|
257
|
+
try {
|
|
258
|
+
const toolNames = Array.from({ length: 129 }, (_, i) => `tool_${i}`);
|
|
259
|
+
filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
260
|
+
assert.equal(warnings.length, 1);
|
|
261
|
+
assert.ok(warnings[0].includes("128"), "warning mentions Groq limit");
|
|
262
|
+
} finally {
|
|
263
|
+
console.warn = original;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("does not warn when tools are at the limit", () => {
|
|
268
|
+
const warnings: string[] = [];
|
|
269
|
+
const original = console.warn;
|
|
270
|
+
console.warn = (...args: unknown[]) => { warnings.push(String(args[0])); };
|
|
271
|
+
try {
|
|
272
|
+
const toolNames = Array.from({ length: 128 }, (_, i) => `tool_${i}`);
|
|
273
|
+
filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
274
|
+
assert.equal(warnings.length, 0);
|
|
275
|
+
} finally {
|
|
276
|
+
console.warn = original;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("adjustToolSet — Groq 128-tool cap", () => {
|
|
282
|
+
beforeEach(() => {
|
|
283
|
+
resetToolCompatibilityRegistry();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("caps to 128 tools when provider is groq and >128 tools active", () => {
|
|
287
|
+
const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
|
|
288
|
+
const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions", "groq");
|
|
289
|
+
assert.equal(result.length, 128);
|
|
290
|
+
assert.equal(removedTools.length, 22);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("does not cap for non-groq providers even with >128 tools", () => {
|
|
294
|
+
const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
|
|
295
|
+
const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions", "openai");
|
|
296
|
+
assert.equal(result.length, 150);
|
|
297
|
+
assert.equal(removedTools.length, 0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("does not cap when provider is omitted", () => {
|
|
301
|
+
const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
|
|
302
|
+
const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions");
|
|
303
|
+
assert.equal(result.length, 150);
|
|
304
|
+
assert.equal(removedTools.length, 0);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -109,6 +109,29 @@ test("plan-v2 gate fails closed for execution phase when finalized context is mi
|
|
|
109
109
|
assert.match(compiled.reason ?? "", /CONTEXT\.md/i);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
test("plan-v2 gate accepts finalized context from project-root fallback", () => {
|
|
113
|
+
const projectRoot = createBasePath();
|
|
114
|
+
const worktreeBase = createBasePath();
|
|
115
|
+
seedGraphRows();
|
|
116
|
+
|
|
117
|
+
writeMilestoneFile(projectRoot, "CONTEXT", "Finalized context in project root.");
|
|
118
|
+
writeMilestoneFile(worktreeBase, "CONTEXT-DRAFT", "Draft context in worktree.");
|
|
119
|
+
|
|
120
|
+
const prevProjectRoot = process.env.GSD_PROJECT_ROOT;
|
|
121
|
+
process.env.GSD_PROJECT_ROOT = projectRoot;
|
|
122
|
+
try {
|
|
123
|
+
const compiled = ensurePlanV2Graph(worktreeBase, buildState("executing"));
|
|
124
|
+
assert.equal(compiled.ok, true);
|
|
125
|
+
assert.equal(compiled.finalizedContextIncluded, true);
|
|
126
|
+
} finally {
|
|
127
|
+
if (prevProjectRoot === undefined) {
|
|
128
|
+
delete process.env.GSD_PROJECT_ROOT;
|
|
129
|
+
} else {
|
|
130
|
+
process.env.GSD_PROJECT_ROOT = prevProjectRoot;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
112
135
|
test("plan-v2 compiler writes pipeline metadata for clarify/research/draft stages", () => {
|
|
113
136
|
const basePath = createBasePath();
|
|
114
137
|
seedGraphRows();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
_getAdapter,
|
|
12
12
|
insertGateRow,
|
|
13
13
|
} from "../gsd-db.ts";
|
|
14
|
-
import { markDepthVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
|
|
14
|
+
import { markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot } from "../bootstrap/write-gate.ts";
|
|
15
15
|
import {
|
|
16
16
|
executeCompleteMilestone,
|
|
17
17
|
executePlanMilestone,
|
|
@@ -742,3 +742,66 @@ test("executeSummarySave leaves sibling CONTEXT-DRAFT intact for non-CONTEXT art
|
|
|
742
742
|
cleanup(base);
|
|
743
743
|
}
|
|
744
744
|
});
|
|
745
|
+
|
|
746
|
+
test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file is deleted (#4343)", async () => {
|
|
747
|
+
const base = makeTmpBase();
|
|
748
|
+
const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
749
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
750
|
+
try {
|
|
751
|
+
openTestDb(base);
|
|
752
|
+
clearDiscussionFlowState();
|
|
753
|
+
|
|
754
|
+
// First call: CONTEXT artifact without depth verification → HARD BLOCK
|
|
755
|
+
const blocked = await inProjectDir(base, () => executeSummarySave({
|
|
756
|
+
milestone_id: "M001",
|
|
757
|
+
artifact_type: "CONTEXT",
|
|
758
|
+
content: "# Context\n\ncontent",
|
|
759
|
+
}, base));
|
|
760
|
+
assert.equal(blocked.isError, true, "should be blocked without depth verification");
|
|
761
|
+
assert.match(
|
|
762
|
+
blocked.content[0].text,
|
|
763
|
+
/HARD BLOCK/,
|
|
764
|
+
"blocked result should mention HARD BLOCK",
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
// Verify the state file was written (persist mode is active)
|
|
768
|
+
const stateFilePath = join(base, ".gsd", "runtime", "write-gate-state.json");
|
|
769
|
+
// The state file may or may not exist at this point (block doesn't write state).
|
|
770
|
+
// Write a fake state file simulating stale persisted block state.
|
|
771
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
772
|
+
writeFileSync(stateFilePath, JSON.stringify({
|
|
773
|
+
verifiedDepthMilestones: [],
|
|
774
|
+
activeQueuePhase: false,
|
|
775
|
+
pendingGateId: "depth_verification_M001",
|
|
776
|
+
}));
|
|
777
|
+
|
|
778
|
+
// User deletes the state file to reset the block
|
|
779
|
+
unlinkSync(stateFilePath);
|
|
780
|
+
assert.ok(!existsSync(stateFilePath), "state file deleted");
|
|
781
|
+
|
|
782
|
+
// The snapshot loaded after deletion should be clean (no pending gate, no block)
|
|
783
|
+
const snapshot = loadWriteGateSnapshot(base);
|
|
784
|
+
assert.equal(snapshot.pendingGateId, null, "pendingGateId should be null after file deletion");
|
|
785
|
+
assert.deepEqual(snapshot.verifiedDepthMilestones, [], "verifiedDepthMilestones should be empty after file deletion");
|
|
786
|
+
|
|
787
|
+
// Depth-verify and re-attempt: should succeed after deletion clears stale state
|
|
788
|
+
markDepthVerified("M001", base);
|
|
789
|
+
|
|
790
|
+
const unblocked = await inProjectDir(base, () => executeSummarySave({
|
|
791
|
+
milestone_id: "M001",
|
|
792
|
+
artifact_type: "CONTEXT",
|
|
793
|
+
content: "# Context\n\nfinal content",
|
|
794
|
+
}, base));
|
|
795
|
+
assert.equal(unblocked.isError, undefined, "should not be blocked after depth verification");
|
|
796
|
+
assert.equal(unblocked.details.operation, "save_summary");
|
|
797
|
+
} finally {
|
|
798
|
+
if (originalEnv === undefined) {
|
|
799
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
800
|
+
} else {
|
|
801
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
802
|
+
}
|
|
803
|
+
clearDiscussionFlowState();
|
|
804
|
+
closeDatabase();
|
|
805
|
+
cleanup(base);
|
|
806
|
+
}
|
|
807
|
+
});
|