gsd-pi 2.61.0-dev.7aed0bf → 2.62.0-dev.a987556
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/ask-user-questions.js +47 -3
- package/dist/resources/extensions/gsd/auto-start.js +11 -6
- package/dist/resources/extensions/gsd/auto-timers.js +8 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
- package/dist/resources/extensions/gsd/auto.js +24 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
- package/dist/resources/extensions/gsd/db-writer.js +64 -28
- package/dist/resources/extensions/gsd/preferences-models.js +74 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
- package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
- package/dist/resources/extensions/gsd/skill-health.js +7 -3
- package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/cli.ts +1 -1
- package/packages/mcp-server/src/index.ts +15 -1
- package/packages/mcp-server/src/readers/captures.ts +119 -0
- package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
- package/packages/mcp-server/src/readers/index.ts +16 -0
- package/packages/mcp-server/src/readers/knowledge.ts +111 -0
- package/packages/mcp-server/src/readers/metrics.ts +118 -0
- package/packages/mcp-server/src/readers/paths.ts +217 -0
- package/packages/mcp-server/src/readers/readers.test.ts +509 -0
- package/packages/mcp-server/src/readers/roadmap.ts +263 -0
- package/packages/mcp-server/src/readers/state.ts +223 -0
- package/packages/mcp-server/src/server.ts +134 -3
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
- package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +60 -4
- package/src/resources/extensions/gsd/auto-start.ts +11 -6
- package/src/resources/extensions/gsd/auto-timers.ts +8 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +25 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
- package/src/resources/extensions/gsd/db-writer.ts +67 -30
- package/src/resources/extensions/gsd/preferences-models.ts +78 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
- package/src/resources/extensions/gsd/skill-health.ts +7 -3
- package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
- package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
- /package/dist/web/standalone/.next/static/{b7FOoMHaUb3FPoLNbxar4 → AsCFY1___4XTI6GkTQkFb}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{b7FOoMHaUb3FPoLNbxar4 → AsCFY1___4XTI6GkTQkFb}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for model config isolation between concurrent instances (#650, #1065)
|
|
2
|
+
* Tests for model config isolation between concurrent instances (#650, #1065)
|
|
3
|
+
* and GSD preferences override of settings.json defaults (#3517).
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
@@ -155,3 +156,76 @@ describe("session model recovery on error (#1065)", () => {
|
|
|
155
156
|
"Recovery should be skipped when no session model was captured");
|
|
156
157
|
});
|
|
157
158
|
});
|
|
159
|
+
|
|
160
|
+
// ─── GSD Preferences override settings.json (#3517) ─────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("GSD preferences override settings.json for session model (#3517)", () => {
|
|
163
|
+
it("preferredModel takes priority over ctx.model when both are available", () => {
|
|
164
|
+
// Simulates auto-start.ts logic: preferredModel ?? ctx.model snapshot
|
|
165
|
+
const preferredModel = { provider: "openai-codex", id: "gpt-5.4" };
|
|
166
|
+
const ctxModel = { provider: "claude-code", id: "claude-sonnet-4-6" };
|
|
167
|
+
|
|
168
|
+
const startModelSnapshot = preferredModel
|
|
169
|
+
?? { provider: ctxModel.provider, id: ctxModel.id };
|
|
170
|
+
|
|
171
|
+
assert.equal(startModelSnapshot.provider, "openai-codex",
|
|
172
|
+
"preferredModel provider should win over ctx.model");
|
|
173
|
+
assert.equal(startModelSnapshot.id, "gpt-5.4",
|
|
174
|
+
"preferredModel id should win over ctx.model");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("falls back to ctx.model when no GSD preferences are configured", () => {
|
|
178
|
+
const preferredModel: { provider: string; id: string } | undefined = undefined;
|
|
179
|
+
const ctxModel = { provider: "claude-code", id: "claude-sonnet-4-6" };
|
|
180
|
+
|
|
181
|
+
const startModelSnapshot = preferredModel
|
|
182
|
+
?? { provider: ctxModel.provider, id: ctxModel.id };
|
|
183
|
+
|
|
184
|
+
assert.equal(startModelSnapshot.provider, "claude-code",
|
|
185
|
+
"should fall back to ctx.model provider when no preferences");
|
|
186
|
+
assert.equal(startModelSnapshot.id, "claude-sonnet-4-6",
|
|
187
|
+
"should fall back to ctx.model id when no preferences");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("handles null ctx.model with no preferences gracefully", () => {
|
|
191
|
+
const preferredModel: { provider: string; id: string } | undefined = undefined;
|
|
192
|
+
// Use a function to prevent TS from narrowing to `never` in the ternary
|
|
193
|
+
function getCtxModel(): { provider: string; id: string } | null { return null; }
|
|
194
|
+
const ctxModel = getCtxModel();
|
|
195
|
+
|
|
196
|
+
const startModelSnapshot = preferredModel
|
|
197
|
+
?? (ctxModel ? { provider: ctxModel.provider, id: ctxModel.id } : null);
|
|
198
|
+
|
|
199
|
+
assert.equal(startModelSnapshot, null,
|
|
200
|
+
"should be null when neither preferences nor ctx.model exist");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("bare model ID uses session provider when available", () => {
|
|
204
|
+
// Simulates: PREFERENCES.md has "gpt-5.4" (no provider), session is openai-codex
|
|
205
|
+
const preferredModel = { provider: "openai-codex", id: "gpt-5.4" }; // from resolveDefaultSessionModel("openai-codex")
|
|
206
|
+
const ctxModel = { provider: "openai-codex", id: "claude-sonnet-4-6" };
|
|
207
|
+
|
|
208
|
+
const startModelSnapshot = preferredModel
|
|
209
|
+
?? { provider: ctxModel.provider, id: ctxModel.id };
|
|
210
|
+
|
|
211
|
+
assert.equal(startModelSnapshot.provider, "openai-codex");
|
|
212
|
+
assert.equal(startModelSnapshot.id, "gpt-5.4",
|
|
213
|
+
"bare model ID from preferences should still override ctx.model");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("stale settings.json does not leak when preferences are set", () => {
|
|
217
|
+
// Scenario: settings.json has claude-code, PREFERENCES.md has openai-codex
|
|
218
|
+
const settingsJsonDefault = { provider: "claude-code", id: "claude-sonnet-4-6" };
|
|
219
|
+
const preferencesModel = { provider: "openai-codex", id: "gpt-5.4" };
|
|
220
|
+
|
|
221
|
+
// auto-start.ts captures preferredModel first, which preempts settingsJsonDefault
|
|
222
|
+
const startModelSnapshot = preferencesModel ?? settingsJsonDefault;
|
|
223
|
+
|
|
224
|
+
assert.equal(startModelSnapshot.provider, "openai-codex",
|
|
225
|
+
"PREFERENCES.md must override stale settings.json provider");
|
|
226
|
+
assert.equal(startModelSnapshot.id, "gpt-5.4",
|
|
227
|
+
"PREFERENCES.md must override stale settings.json model");
|
|
228
|
+
assert.notEqual(startModelSnapshot.provider, settingsJsonDefault.provider,
|
|
229
|
+
"settings.json provider must NOT leak through");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// GSD Extension - Steer Worktree Path Resolution Test
|
|
2
|
+
// Regression test for #3476: /gsd steer must write overrides to the worktree .gsd/,
|
|
3
|
+
// not the project root .gsd/, when a worktree is active.
|
|
4
|
+
|
|
5
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtempSync, mkdirSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { appendOverride, loadActiveOverrides } from "../files.ts";
|
|
11
|
+
import { getAutoWorktreePath } from "../auto-worktree.ts";
|
|
12
|
+
|
|
13
|
+
describe("steer worktree path resolution (#3476)", () => {
|
|
14
|
+
let projectRoot: string;
|
|
15
|
+
let worktreePath: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
projectRoot = mkdtempSync(join(tmpdir(), "gsd-steer-wt-"));
|
|
19
|
+
mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
|
|
20
|
+
|
|
21
|
+
// Simulate a worktree with its own .gsd directory
|
|
22
|
+
worktreePath = join(projectRoot, ".gsd", "worktrees", "M001");
|
|
23
|
+
mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("appendOverride writes to worktree .gsd/ when worktree path is used", async () => {
|
|
31
|
+
await appendOverride(worktreePath, "Use Postgres instead of SQLite", "M001/S01/T01");
|
|
32
|
+
|
|
33
|
+
// Override should be in the worktree .gsd/
|
|
34
|
+
const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md");
|
|
35
|
+
assert.ok(existsSync(wtOverrides), "override file exists in worktree .gsd/");
|
|
36
|
+
|
|
37
|
+
const content = readFileSync(wtOverrides, "utf-8");
|
|
38
|
+
assert.ok(content.includes("Use Postgres instead of SQLite"), "override content is correct");
|
|
39
|
+
|
|
40
|
+
// Override should NOT be in the project root .gsd/
|
|
41
|
+
const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
|
|
42
|
+
assert.ok(!existsSync(rootOverrides), "no override file in project root .gsd/");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("loadActiveOverrides reads from worktree .gsd/ when worktree path is used", async () => {
|
|
46
|
+
await appendOverride(worktreePath, "Switch to JWT auth", "M001/S02/T01");
|
|
47
|
+
|
|
48
|
+
// Loading from worktree should find the override
|
|
49
|
+
const wtOverrides = await loadActiveOverrides(worktreePath);
|
|
50
|
+
assert.equal(wtOverrides.length, 1, "one active override in worktree");
|
|
51
|
+
assert.equal(wtOverrides[0].change, "Switch to JWT auth");
|
|
52
|
+
|
|
53
|
+
// Loading from project root should find nothing
|
|
54
|
+
const rootOverrides = await loadActiveOverrides(projectRoot);
|
|
55
|
+
assert.equal(rootOverrides.length, 0, "no overrides in project root");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("appendOverride falls back to project root when no worktree exists", async () => {
|
|
59
|
+
await appendOverride(projectRoot, "Use Redis cache", "M001/S01/T01");
|
|
60
|
+
|
|
61
|
+
const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
|
|
62
|
+
assert.ok(existsSync(rootOverrides), "override file exists in project root .gsd/");
|
|
63
|
+
|
|
64
|
+
const content = readFileSync(rootOverrides, "utf-8");
|
|
65
|
+
assert.ok(content.includes("Use Redis cache"), "override content is correct");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("getAutoWorktreePath returns null for worktree without valid .git file", () => {
|
|
69
|
+
// The worktree directory exists but has no .git file — this is an inactive/
|
|
70
|
+
// leftover worktree. getAutoWorktreePath must return null so handleSteer
|
|
71
|
+
// does not route overrides to a dead worktree.
|
|
72
|
+
const result = getAutoWorktreePath(projectRoot, "M001");
|
|
73
|
+
assert.equal(result, null, "returns null for worktree without .git file");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("override routing: inactive worktree directory should not receive overrides", async () => {
|
|
77
|
+
// Simulate the handleSteer path-resolution logic:
|
|
78
|
+
// When no auto-mode is running, even if a worktree dir exists,
|
|
79
|
+
// overrides must go to the project root.
|
|
80
|
+
const autoRunning = false; // no live session
|
|
81
|
+
const wtPath = autoRunning ? getAutoWorktreePath(projectRoot, "M001") : null;
|
|
82
|
+
const targetPath = wtPath ?? projectRoot;
|
|
83
|
+
|
|
84
|
+
await appendOverride(targetPath, "Should go to project root", "M001/S01/T01");
|
|
85
|
+
|
|
86
|
+
const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
|
|
87
|
+
const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md");
|
|
88
|
+
|
|
89
|
+
assert.ok(existsSync(rootOverrides), "override written to project root");
|
|
90
|
+
assert.ok(!existsSync(wtOverrides), "override NOT written to inactive worktree");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("override routing: active worktree with valid .git should receive overrides", async () => {
|
|
94
|
+
// Simulate the handleSteer path-resolution logic with active auto-mode.
|
|
95
|
+
// getAutoWorktreePath requires a valid .git file, so even with autoRunning=true,
|
|
96
|
+
// it returns null for our test worktree (no real .git). This confirms the
|
|
97
|
+
// double-gate: both autoRunning AND valid worktree must be true.
|
|
98
|
+
const autoRunning = true;
|
|
99
|
+
const wtPath = autoRunning ? getAutoWorktreePath(projectRoot, "M001") : null;
|
|
100
|
+
const targetPath = wtPath ?? projectRoot;
|
|
101
|
+
|
|
102
|
+
// Without a valid .git file, falls back to project root
|
|
103
|
+
await appendOverride(targetPath, "Falls back without .git", "M001/S01/T01");
|
|
104
|
+
|
|
105
|
+
const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
|
|
106
|
+
assert.ok(existsSync(rootOverrides), "override written to project root (no valid .git in worktree)");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -136,17 +136,30 @@ console.log('\n── Loop guard: nested args are not stripped ──');
|
|
|
136
136
|
assert.deepStrictEqual(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Truly identical nested calls should still be detected
|
|
139
|
+
// Truly identical nested calls should still be detected.
|
|
140
|
+
// ask_user_questions has a strict threshold of 1, so the 2nd identical call is blocked.
|
|
141
|
+
resetToolCallLoopGuard();
|
|
142
|
+
const first = checkToolCallLoop('ask_user_questions', {
|
|
143
|
+
questions: [{ id: 'same', question: 'Same?' }],
|
|
144
|
+
});
|
|
145
|
+
assert.ok(first.block === false, 'First ask_user_questions call should be allowed');
|
|
146
|
+
const blocked = checkToolCallLoop('ask_user_questions', {
|
|
147
|
+
questions: [{ id: 'same', question: 'Same?' }],
|
|
148
|
+
});
|
|
149
|
+
assert.ok(blocked.block === true, '2nd identical ask_user_questions call should be blocked (strict threshold)');
|
|
150
|
+
|
|
151
|
+
// Non-strict tools still allow up to 4 identical calls
|
|
140
152
|
resetToolCallLoopGuard();
|
|
141
153
|
for (let i = 1; i <= 4; i++) {
|
|
142
|
-
checkToolCallLoop('
|
|
154
|
+
const r = checkToolCallLoop('web_search', {
|
|
143
155
|
questions: [{ id: 'same', question: 'Same?' }],
|
|
144
156
|
});
|
|
157
|
+
assert.ok(r.block === false, `web_search call ${i} should be allowed (normal threshold)`);
|
|
145
158
|
}
|
|
146
|
-
const
|
|
159
|
+
const blockedNormal = checkToolCallLoop('web_search', {
|
|
147
160
|
questions: [{ id: 'same', question: 'Same?' }],
|
|
148
161
|
});
|
|
149
|
-
assert.ok(
|
|
162
|
+
assert.ok(blockedNormal.block === true, '5th identical web_search call should be blocked');
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -100,8 +100,87 @@ describe('worktree-db-respawn-truncation (#2815)', async () => {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// ─── 3.
|
|
104
|
-
console.log('\n=== 3.
|
|
103
|
+
// ─── 3. WAL/SHM sidecar files cleaned up when empty DB is deleted (#2478) ──
|
|
104
|
+
console.log('\n=== 3. orphaned WAL/SHM cleaned up alongside empty gsd.db (#2478) ===');
|
|
105
|
+
{
|
|
106
|
+
const mainBase = createBase('main');
|
|
107
|
+
const wtBase = createBase('wt');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
111
|
+
mkdirSync(m001Dir, { recursive: true });
|
|
112
|
+
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
113
|
+
|
|
114
|
+
// Create an empty (0-byte) gsd.db plus orphaned WAL and SHM files —
|
|
115
|
+
// this is the exact state that causes Node 24 node:sqlite CPU spin (#2478).
|
|
116
|
+
const wtGsd = join(wtBase, '.gsd');
|
|
117
|
+
writeFileSync(join(wtGsd, 'gsd.db'), '');
|
|
118
|
+
writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(605672, 0xAA));
|
|
119
|
+
writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(32768, 0xBB));
|
|
120
|
+
|
|
121
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db')), 'gsd.db exists before sync');
|
|
122
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'gsd.db-wal exists before sync');
|
|
123
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'gsd.db-shm exists before sync');
|
|
124
|
+
|
|
125
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
126
|
+
|
|
127
|
+
assert.ok(
|
|
128
|
+
!existsSync(join(wtGsd, 'gsd.db')),
|
|
129
|
+
'#2478: empty gsd.db must be deleted',
|
|
130
|
+
);
|
|
131
|
+
assert.ok(
|
|
132
|
+
!existsSync(join(wtGsd, 'gsd.db-wal')),
|
|
133
|
+
'#2478: orphaned gsd.db-wal must be deleted alongside gsd.db',
|
|
134
|
+
);
|
|
135
|
+
assert.ok(
|
|
136
|
+
!existsSync(join(wtGsd, 'gsd.db-shm')),
|
|
137
|
+
'#2478: orphaned gsd.db-shm must be deleted alongside gsd.db',
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
cleanup(mainBase);
|
|
141
|
+
cleanup(wtBase);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── 4. Orphaned WAL/SHM cleaned up even when gsd.db already missing (#2478) ──
|
|
146
|
+
console.log('\n=== 4. orphaned WAL/SHM cleaned up even without gsd.db (#2478) ===');
|
|
147
|
+
{
|
|
148
|
+
const mainBase = createBase('main');
|
|
149
|
+
const wtBase = createBase('wt');
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
153
|
+
mkdirSync(m001Dir, { recursive: true });
|
|
154
|
+
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
155
|
+
|
|
156
|
+
// Orphaned WAL/SHM with NO gsd.db at all — can happen from a previous
|
|
157
|
+
// partial cleanup. These must still be cleaned up.
|
|
158
|
+
const wtGsd = join(wtBase, '.gsd');
|
|
159
|
+
writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(1024, 0xAA));
|
|
160
|
+
writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(1024, 0xBB));
|
|
161
|
+
|
|
162
|
+
assert.ok(!existsSync(join(wtGsd, 'gsd.db')), 'gsd.db does not exist');
|
|
163
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'orphaned gsd.db-wal exists');
|
|
164
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'orphaned gsd.db-shm exists');
|
|
165
|
+
|
|
166
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
167
|
+
|
|
168
|
+
assert.ok(
|
|
169
|
+
!existsSync(join(wtGsd, 'gsd.db-wal')),
|
|
170
|
+
'#2478: orphaned gsd.db-wal must be deleted even without main db file',
|
|
171
|
+
);
|
|
172
|
+
assert.ok(
|
|
173
|
+
!existsSync(join(wtGsd, 'gsd.db-shm')),
|
|
174
|
+
'#2478: orphaned gsd.db-shm must be deleted even without main db file',
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
cleanup(mainBase);
|
|
178
|
+
cleanup(wtBase);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── 5. Milestone artifacts still synced when DB is preserved ────────
|
|
183
|
+
console.log('\n=== 5. milestone artifacts still synced even when DB preserved ===');
|
|
105
184
|
{
|
|
106
185
|
const mainBase = createBase('main');
|
|
107
186
|
const wtBase = createBase('wt');
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* triggers a follow-up free-text input prompt via ctx.ui.input().
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe, it } from "node:test";
|
|
13
|
+
import { describe, it, beforeEach } from "node:test";
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
15
|
|
|
16
16
|
// The ask-user-questions extension registers a tool via pi.registerTool().
|
|
17
17
|
// We capture that registration and call execute() directly with a mock context.
|
|
18
18
|
import AskUserQuestions from "../../ask-user-questions.js";
|
|
19
|
+
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
|
|
19
20
|
|
|
20
21
|
interface CapturedTool {
|
|
21
22
|
name: string;
|
|
@@ -73,6 +74,10 @@ function makeMockCtx(opts: {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
describe("ask-user-questions RPC fallback free-text", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
resetAskUserQuestionsCache();
|
|
79
|
+
});
|
|
80
|
+
|
|
76
81
|
it("prompts for free-text input when user selects 'None of the above'", async () => {
|
|
77
82
|
const tool = captureTool();
|
|
78
83
|
const { ctx, selectCalls, inputCalls } = makeMockCtx({
|
|
File without changes
|
|
File without changes
|