gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/dist/app-paths.js +1 -1
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +636 -594
- package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
- package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
- package/dist/resources/extensions/gsd/auto-start.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +4 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +48 -9
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +149 -38
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +3 -86
- package/dist/resources/extensions/gsd/index.js +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
- package/dist/resources/extensions/gsd/preferences.js +22 -11
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +42 -23
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +4 -1
- package/dist/resources/extensions/remote-questions/store.js +4 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +6 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
- package/src/resources/extensions/gsd/auto-loop.ts +526 -545
- package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
- package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
- package/src/resources/extensions/gsd/auto-start.ts +11 -1
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +5 -3
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +51 -11
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +177 -44
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +3 -89
- package/src/resources/extensions/gsd/index.ts +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +4 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
- package/src/resources/extensions/gsd/preferences.ts +25 -11
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +39 -21
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/types.ts +18 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +5 -1
- package/src/resources/extensions/remote-questions/store.ts +5 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/shared/frontmatter.ts +1 -1
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resolveAgentEnd,
|
|
8
8
|
runUnit,
|
|
9
9
|
autoLoop,
|
|
10
|
+
detectStuck,
|
|
10
11
|
_resetPendingResolve,
|
|
11
12
|
_setActiveSession,
|
|
12
13
|
isSessionSwitchInFlight,
|
|
@@ -37,9 +38,6 @@ function makeMockSession(opts?: {
|
|
|
37
38
|
const session = {
|
|
38
39
|
active: true,
|
|
39
40
|
verbose: false,
|
|
40
|
-
sessionSwitchInFlight: false,
|
|
41
|
-
pendingResolve: null,
|
|
42
|
-
pendingAgentEndQueue: [],
|
|
43
41
|
cmdCtx: {
|
|
44
42
|
newSession: () => {
|
|
45
43
|
opts?.onNewSessionStart?.(session);
|
|
@@ -96,7 +94,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
96
94
|
const ctx = makeMockCtx();
|
|
97
95
|
const pi = makeMockPi();
|
|
98
96
|
const s = makeMockSession();
|
|
99
|
-
_setActiveSession(s);
|
|
100
97
|
const event = makeEvent();
|
|
101
98
|
|
|
102
99
|
// Start runUnit — it will create the promise and send a message,
|
|
@@ -108,7 +105,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
108
105
|
"task",
|
|
109
106
|
"T01",
|
|
110
107
|
"do stuff",
|
|
111
|
-
undefined,
|
|
112
108
|
);
|
|
113
109
|
|
|
114
110
|
// Give the microtask queue a tick so runUnit reaches the await
|
|
@@ -122,44 +118,35 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
122
118
|
assert.deepEqual(result.event, event);
|
|
123
119
|
});
|
|
124
120
|
|
|
125
|
-
test("resolveAgentEnd
|
|
121
|
+
test("resolveAgentEnd drops event when no promise is pending", () => {
|
|
126
122
|
_resetPendingResolve();
|
|
127
|
-
const s = makeMockSession();
|
|
128
|
-
_setActiveSession(s);
|
|
129
123
|
|
|
130
|
-
// Should not throw —
|
|
124
|
+
// Should not throw — event is dropped (logged as warning)
|
|
131
125
|
assert.doesNotThrow(() => {
|
|
132
126
|
resolveAgentEnd(makeEvent());
|
|
133
127
|
});
|
|
134
|
-
assert.equal(s.pendingAgentEndQueue.length, 1, "event should be queued");
|
|
135
128
|
});
|
|
136
129
|
|
|
137
|
-
test("double resolveAgentEnd only resolves once (second is
|
|
130
|
+
test("double resolveAgentEnd only resolves once (second is dropped)", async () => {
|
|
138
131
|
_resetPendingResolve();
|
|
139
132
|
|
|
140
133
|
const ctx = makeMockCtx();
|
|
141
134
|
const pi = makeMockPi();
|
|
142
135
|
const s = makeMockSession();
|
|
143
|
-
_setActiveSession(s);
|
|
144
136
|
const event1 = makeEvent([{ id: 1 }]);
|
|
145
137
|
const event2 = makeEvent([{ id: 2 }]);
|
|
146
138
|
|
|
147
|
-
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
139
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
148
140
|
|
|
149
141
|
await new Promise((r) => setTimeout(r, 10));
|
|
150
142
|
|
|
151
143
|
// First resolve — should work
|
|
152
144
|
resolveAgentEnd(event1);
|
|
153
145
|
|
|
154
|
-
// Second resolve — should be
|
|
146
|
+
// Second resolve — should be dropped (no pending resolver)
|
|
155
147
|
assert.doesNotThrow(() => {
|
|
156
148
|
resolveAgentEnd(event2);
|
|
157
149
|
});
|
|
158
|
-
assert.equal(
|
|
159
|
-
s.pendingAgentEndQueue.length,
|
|
160
|
-
1,
|
|
161
|
-
"second event should be queued",
|
|
162
|
-
);
|
|
163
150
|
|
|
164
151
|
const result = await resultPromise;
|
|
165
152
|
assert.equal(result.status, "completed");
|
|
@@ -174,7 +161,7 @@ test("runUnit returns cancelled when session creation fails", async () => {
|
|
|
174
161
|
const pi = makeMockPi();
|
|
175
162
|
const s = makeMockSession({ newSessionThrows: "connection refused" });
|
|
176
163
|
|
|
177
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
164
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
178
165
|
|
|
179
166
|
assert.equal(result.status, "cancelled");
|
|
180
167
|
assert.equal(result.event, undefined);
|
|
@@ -190,7 +177,7 @@ test("runUnit returns cancelled when session creation times out", async () => {
|
|
|
190
177
|
// Session returns cancelled: true (simulates the timeout race outcome)
|
|
191
178
|
const s = makeMockSession({ newSessionResult: { cancelled: true } });
|
|
192
179
|
|
|
193
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
180
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
194
181
|
|
|
195
182
|
assert.equal(result.status, "cancelled");
|
|
196
183
|
assert.equal(result.event, undefined);
|
|
@@ -205,35 +192,31 @@ test("runUnit returns cancelled when s.active is false before sendMessage", asyn
|
|
|
205
192
|
const s = makeMockSession();
|
|
206
193
|
s.active = false;
|
|
207
194
|
|
|
208
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
195
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
209
196
|
|
|
210
197
|
assert.equal(result.status, "cancelled");
|
|
211
198
|
assert.equal(pi.calls.length, 0);
|
|
212
199
|
});
|
|
213
200
|
|
|
214
|
-
test("runUnit only arms
|
|
201
|
+
test("runUnit only arms resolve after newSession completes", async () => {
|
|
215
202
|
_resetPendingResolve();
|
|
216
203
|
|
|
217
204
|
let sawSwitchFlag = false;
|
|
218
|
-
let sawPendingResolve: unknown = "unset";
|
|
219
205
|
|
|
220
206
|
const ctx = makeMockCtx();
|
|
221
207
|
const pi = makeMockPi();
|
|
222
208
|
const s = makeMockSession({
|
|
223
209
|
newSessionDelayMs: 20,
|
|
224
|
-
onNewSessionStart: (
|
|
225
|
-
sawSwitchFlag =
|
|
226
|
-
sawPendingResolve = session.pendingResolve;
|
|
210
|
+
onNewSessionStart: () => {
|
|
211
|
+
sawSwitchFlag = isSessionSwitchInFlight();
|
|
227
212
|
},
|
|
228
213
|
});
|
|
229
|
-
_setActiveSession(s);
|
|
230
214
|
|
|
231
|
-
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
215
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
232
216
|
|
|
233
217
|
await new Promise((r) => setTimeout(r, 30));
|
|
234
218
|
|
|
235
219
|
assert.equal(sawSwitchFlag, true, "session switch guard should be active during newSession");
|
|
236
|
-
assert.equal(sawPendingResolve, null, "pendingResolve should not be armed before newSession completes");
|
|
237
220
|
assert.equal(isSessionSwitchInFlight(), false, "session switch guard should clear after newSession settles");
|
|
238
221
|
|
|
239
222
|
resolveAgentEnd(makeEvent());
|
|
@@ -275,24 +258,23 @@ test("auto-loop.ts contains a while keyword", () => {
|
|
|
275
258
|
);
|
|
276
259
|
});
|
|
277
260
|
|
|
278
|
-
test("auto-loop.ts one-shot pattern:
|
|
261
|
+
test("auto-loop.ts one-shot pattern: _currentResolve is nulled before calling resolver", () => {
|
|
279
262
|
const src = readFileSync(
|
|
280
263
|
resolve(import.meta.dirname, "..", "auto-loop.ts"),
|
|
281
264
|
"utf-8",
|
|
282
265
|
);
|
|
283
266
|
// The one-shot pattern requires: save ref, null the variable, then call
|
|
284
|
-
// Look for the pattern: s.pendingResolve = null appearing before r(
|
|
285
267
|
const resolveBlock = src.slice(
|
|
286
268
|
src.indexOf("export function resolveAgentEnd"),
|
|
287
269
|
src.indexOf("export function resolveAgentEnd") + 600,
|
|
288
270
|
);
|
|
289
|
-
const nullIdx = resolveBlock.indexOf("
|
|
271
|
+
const nullIdx = resolveBlock.indexOf("_currentResolve = null");
|
|
290
272
|
const callIdx = resolveBlock.indexOf("r({");
|
|
291
|
-
assert.ok(nullIdx > 0, "should null
|
|
273
|
+
assert.ok(nullIdx > 0, "should null _currentResolve in resolveAgentEnd");
|
|
292
274
|
assert.ok(callIdx > 0, "should call resolver in resolveAgentEnd");
|
|
293
275
|
assert.ok(
|
|
294
276
|
nullIdx < callIdx,
|
|
295
|
-
"
|
|
277
|
+
"_currentResolve should be nulled before calling the resolver (one-shot)",
|
|
296
278
|
);
|
|
297
279
|
});
|
|
298
280
|
|
|
@@ -462,8 +444,6 @@ function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
|
|
|
462
444
|
pendingQuickTasks: [],
|
|
463
445
|
sidecarQueue: [],
|
|
464
446
|
autoModeStartModel: null,
|
|
465
|
-
pendingResolve: null,
|
|
466
|
-
pendingAgentEndQueue: [],
|
|
467
447
|
unitDispatchCount: new Map<string, number>(),
|
|
468
448
|
unitLifetimeDispatches: new Map<string, number>(),
|
|
469
449
|
unitRecoveryCount: new Map<string, number>(),
|
|
@@ -1063,7 +1043,7 @@ test("handleAgentEnd in auto.ts is a thin wrapper calling resolveAgentEnd", () =
|
|
|
1063
1043
|
|
|
1064
1044
|
// ── Stuck counter tests ──────────────────────────────────────────────────────
|
|
1065
1045
|
|
|
1066
|
-
test("stuck
|
|
1046
|
+
test("stuck detection: stops when sliding window detects same unit 3 consecutive times", async () => {
|
|
1067
1047
|
_resetPendingResolve();
|
|
1068
1048
|
|
|
1069
1049
|
const ctx = makeMockCtx();
|
|
@@ -1098,20 +1078,15 @@ test("stuck counter: stops when deriveState returns same unit 5 consecutive time
|
|
|
1098
1078
|
|
|
1099
1079
|
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1100
1080
|
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
// Actually: iteration 1 sets lastDerivedUnit (sameUnitCount=0).
|
|
1109
|
-
// Iteration 2: derivedKey === lastDerivedUnit → sameUnitCount=1.
|
|
1110
|
-
// Iteration 3: sameUnitCount=2. Iteration 4: sameUnitCount=3.
|
|
1111
|
-
// Iteration 5: sameUnitCount=4. Iteration 6: sameUnitCount=5 → stop.
|
|
1112
|
-
// So we need to resolve 5 agent_end events (iterations 1-5 each run a unit).
|
|
1081
|
+
// Sliding window: iteration 1 pushes [A], iteration 2 pushes [A,A],
|
|
1082
|
+
// iteration 3 pushes [A,A,A] → Rule 2 fires (3 consecutive) → Level 1 recovery.
|
|
1083
|
+
// Level 1 invalidates caches and continues. Iteration 4 pushes [A,A,A,A] →
|
|
1084
|
+
// Rule 2 fires again → Level 2 hard stop.
|
|
1085
|
+
// Iterations 1-3 each run a unit (3 resolves needed). Iteration 3 triggers
|
|
1086
|
+
// Level 1 (cache invalidation + continue). Iteration 4 triggers Level 2 (stop
|
|
1087
|
+
// before runUnit), so no 4th resolve needed.
|
|
1113
1088
|
|
|
1114
|
-
for (let i = 0; i <
|
|
1089
|
+
for (let i = 0; i < 3; i++) {
|
|
1115
1090
|
await new Promise((r) => setTimeout(r, 30));
|
|
1116
1091
|
resolveAgentEnd(makeEvent());
|
|
1117
1092
|
}
|
|
@@ -1126,17 +1101,13 @@ test("stuck counter: stops when deriveState returns same unit 5 consecutive time
|
|
|
1126
1101
|
stopReason.includes("Stuck"),
|
|
1127
1102
|
`stop reason should mention 'Stuck', got: ${stopReason}`,
|
|
1128
1103
|
);
|
|
1129
|
-
assert.ok(
|
|
1130
|
-
stopReason.includes("execute-task"),
|
|
1131
|
-
"stop reason should include unitType",
|
|
1132
|
-
);
|
|
1133
1104
|
assert.ok(
|
|
1134
1105
|
stopReason.includes("M001/S01/T01"),
|
|
1135
1106
|
"stop reason should include unitId",
|
|
1136
1107
|
);
|
|
1137
1108
|
});
|
|
1138
1109
|
|
|
1139
|
-
test("stuck
|
|
1110
|
+
test("stuck detection: window resets recovery when deriveState returns a different unit", async () => {
|
|
1140
1111
|
_resetPendingResolve();
|
|
1141
1112
|
|
|
1142
1113
|
const ctx = makeMockCtx();
|
|
@@ -1197,10 +1168,11 @@ test("stuck counter: resets when deriveState returns a different unit", async ()
|
|
|
1197
1168
|
|
|
1198
1169
|
await loopPromise;
|
|
1199
1170
|
|
|
1200
|
-
//
|
|
1171
|
+
// Level 1 recovery fires on iteration 3 (cache invalidation + continue),
|
|
1172
|
+
// then iteration 4 derives T02 — no Level 2 hard stop.
|
|
1201
1173
|
assert.ok(
|
|
1202
1174
|
!stopCalled,
|
|
1203
|
-
"stopAuto should NOT have been called —
|
|
1175
|
+
"stopAuto should NOT have been called — different unit broke stuck pattern",
|
|
1204
1176
|
);
|
|
1205
1177
|
assert.ok(
|
|
1206
1178
|
deriveCallCount >= 4,
|
|
@@ -1208,7 +1180,7 @@ test("stuck counter: resets when deriveState returns a different unit", async ()
|
|
|
1208
1180
|
);
|
|
1209
1181
|
});
|
|
1210
1182
|
|
|
1211
|
-
test("stuck
|
|
1183
|
+
test("stuck detection: does not push to window during verification retry", async () => {
|
|
1212
1184
|
_resetPendingResolve();
|
|
1213
1185
|
|
|
1214
1186
|
const ctx = makeMockCtx();
|
|
@@ -1270,10 +1242,10 @@ test("stuck counter: does not increment during verification retry", async () =>
|
|
|
1270
1242
|
await loopPromise;
|
|
1271
1243
|
|
|
1272
1244
|
// Even though same unit was derived 4 times, verification retries should
|
|
1273
|
-
// not
|
|
1245
|
+
// not push to the sliding window, so stuck detection should not have fired
|
|
1274
1246
|
assert.ok(
|
|
1275
1247
|
!stopReason.includes("Stuck"),
|
|
1276
|
-
`stuck
|
|
1248
|
+
`stuck detection should not fire during verification retries, got: ${stopReason}`,
|
|
1277
1249
|
);
|
|
1278
1250
|
assert.equal(
|
|
1279
1251
|
verifyCallCount,
|
|
@@ -1282,24 +1254,106 @@ test("stuck counter: does not increment during verification retry", async () =>
|
|
|
1282
1254
|
);
|
|
1283
1255
|
});
|
|
1284
1256
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1257
|
+
// ── detectStuck unit tests ────────────────────────────────────────────────────
|
|
1258
|
+
|
|
1259
|
+
test("detectStuck: returns null for fewer than 2 entries", () => {
|
|
1260
|
+
assert.equal(detectStuck([]), null);
|
|
1261
|
+
assert.equal(detectStuck([{ key: "A" }]), null);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
test("detectStuck: Rule 1 — same error twice in a row", () => {
|
|
1265
|
+
const result = detectStuck([
|
|
1266
|
+
{ key: "A", error: "ENOENT: file not found" },
|
|
1267
|
+
{ key: "A", error: "ENOENT: file not found" },
|
|
1268
|
+
]);
|
|
1269
|
+
assert.ok(result?.stuck, "should detect same error repeated");
|
|
1270
|
+
assert.ok(result?.reason.includes("Same error repeated"));
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("detectStuck: Rule 1 — different errors do not trigger", () => {
|
|
1274
|
+
const result = detectStuck([
|
|
1275
|
+
{ key: "A", error: "ENOENT: file not found" },
|
|
1276
|
+
{ key: "A", error: "EACCES: permission denied" },
|
|
1277
|
+
]);
|
|
1278
|
+
assert.equal(result, null);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
test("detectStuck: Rule 2 — same unit 3 consecutive times", () => {
|
|
1282
|
+
const result = detectStuck([
|
|
1283
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
1284
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
1285
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
1286
|
+
]);
|
|
1287
|
+
assert.ok(result?.stuck);
|
|
1288
|
+
assert.ok(result?.reason.includes("3 consecutive times"));
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("detectStuck: Rule 2 — 2 consecutive does not trigger", () => {
|
|
1292
|
+
assert.equal(detectStuck([
|
|
1293
|
+
{ key: "A" },
|
|
1294
|
+
{ key: "A" },
|
|
1295
|
+
]), null);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test("detectStuck: Rule 3 — oscillation A→B→A→B", () => {
|
|
1299
|
+
const result = detectStuck([
|
|
1300
|
+
{ key: "A" },
|
|
1301
|
+
{ key: "B" },
|
|
1302
|
+
{ key: "A" },
|
|
1303
|
+
{ key: "B" },
|
|
1304
|
+
]);
|
|
1305
|
+
assert.ok(result?.stuck);
|
|
1306
|
+
assert.ok(result?.reason.includes("Oscillation"));
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
test("detectStuck: Rule 3 — non-oscillation pattern A→B→C→B", () => {
|
|
1310
|
+
assert.equal(detectStuck([
|
|
1311
|
+
{ key: "A" },
|
|
1312
|
+
{ key: "B" },
|
|
1313
|
+
{ key: "C" },
|
|
1314
|
+
{ key: "B" },
|
|
1315
|
+
]), null);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("detectStuck: Rule 1 takes priority over Rule 2 when both match", () => {
|
|
1319
|
+
const result = detectStuck([
|
|
1320
|
+
{ key: "A", error: "test error" },
|
|
1321
|
+
{ key: "A", error: "test error" },
|
|
1322
|
+
{ key: "A", error: "test error" },
|
|
1323
|
+
]);
|
|
1324
|
+
assert.ok(result?.stuck);
|
|
1325
|
+
// Rule 1 fires first
|
|
1326
|
+
assert.ok(result?.reason.includes("Same error repeated"));
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test("detectStuck: truncates long error strings", () => {
|
|
1330
|
+
const longError = "x".repeat(500);
|
|
1331
|
+
const result = detectStuck([
|
|
1332
|
+
{ key: "A", error: longError },
|
|
1333
|
+
{ key: "A", error: longError },
|
|
1334
|
+
]);
|
|
1335
|
+
assert.ok(result?.stuck);
|
|
1336
|
+
assert.ok(result!.reason.length < 300, "reason should be truncated");
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test("stuck detection: logs debug output with stuck-detected phase", () => {
|
|
1340
|
+
// Structural test: verify the auto-loop.ts source contains
|
|
1341
|
+
// stuck-detected and stuck-counter-reset debug log phases, plus detectStuck
|
|
1288
1342
|
const src = readFileSync(
|
|
1289
1343
|
resolve(import.meta.dirname, "..", "auto-loop.ts"),
|
|
1290
1344
|
"utf-8",
|
|
1291
1345
|
);
|
|
1292
1346
|
assert.ok(
|
|
1293
1347
|
src.includes('"stuck-detected"'),
|
|
1294
|
-
"auto-loop.ts must log phase: 'stuck-detected' when stuck
|
|
1348
|
+
"auto-loop.ts must log phase: 'stuck-detected' when stuck detection fires",
|
|
1295
1349
|
);
|
|
1296
1350
|
assert.ok(
|
|
1297
1351
|
src.includes('"stuck-counter-reset"'),
|
|
1298
|
-
"auto-loop.ts must log phase: 'stuck-counter-reset' when
|
|
1352
|
+
"auto-loop.ts must log phase: 'stuck-counter-reset' when recovery resets on new unit",
|
|
1299
1353
|
);
|
|
1300
1354
|
assert.ok(
|
|
1301
|
-
src.includes("
|
|
1302
|
-
"auto-loop.ts must
|
|
1355
|
+
src.includes("detectStuck"),
|
|
1356
|
+
"auto-loop.ts must use detectStuck for sliding window analysis",
|
|
1303
1357
|
);
|
|
1304
1358
|
});
|
|
1305
1359
|
|
|
@@ -242,9 +242,10 @@ async function main(): Promise<void> {
|
|
|
242
242
|
const remoteLog = run("git log --oneline main", bareDir);
|
|
243
243
|
assertTrue(remoteLog.includes("feat(M040)"), "milestone commit reachable on remote after manual push");
|
|
244
244
|
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
245
|
+
// Temp-repo prefs may or may not be discoverable depending on process cwd and
|
|
246
|
+
// current preference-loading behavior. The important contract is that remote
|
|
247
|
+
// push mechanics work and the returned value reflects what happened.
|
|
248
|
+
assertTrue(typeof result.pushed === "boolean", "pushed flag remains boolean");
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
// ─── Test 5: Auto-resolve .gsd/ state file conflicts (#530) ───────
|
|
@@ -779,6 +779,49 @@ slice: S01
|
|
|
779
779
|
}
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
+
// ─── Test: unchecked roadmap slices + summary → complete (summary is terminal) ────
|
|
783
|
+
console.log('\n=== unchecked roadmap slices + summary → complete (summary is terminal) ===');
|
|
784
|
+
{
|
|
785
|
+
const base = createFixtureBase();
|
|
786
|
+
try {
|
|
787
|
+
// M001: roadmap has unchecked slices but a summary exists — should be complete
|
|
788
|
+
writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Already done.\n\n## Slices\n\n- [ ] **S01: Unchecked slice** \`risk:low\` \`depends:[]\`\n > Work was done but checkbox never ticked.\n- [ ] **S02: Another unchecked** \`risk:low\` \`depends:[]\`\n > Same.\n`);
|
|
789
|
+
writeMilestoneSummary(base, 'M001', '---\nid: M001\n---\n\n# M001: First Milestone\n\n**Completed despite unchecked roadmap.**');
|
|
790
|
+
// M002: genuinely incomplete — should be the active milestone
|
|
791
|
+
writeRoadmap(base, 'M002', `# M002: Active Milestone\n\n**Vision:** Do stuff.\n\n## Slices\n\n- [ ] **S01: Work slice** \`risk:low\` \`depends:[]\`\n > Needs work.\n`);
|
|
792
|
+
|
|
793
|
+
const state = await deriveState(base);
|
|
794
|
+
const m001Entry = state.registry.find(e => e.id === 'M001');
|
|
795
|
+
assertEq(m001Entry?.status, 'complete', 'M001 with unchecked roadmap + summary is complete');
|
|
796
|
+
assertEq(state.activeMilestone?.id, 'M002', 'active milestone is M002, not M001');
|
|
797
|
+
} finally {
|
|
798
|
+
cleanup(base);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ─── Test: unchecked roadmap + summary counts toward completeMilestoneIds (deps) ────
|
|
803
|
+
console.log('\n=== unchecked roadmap + summary satisfies dependency ===');
|
|
804
|
+
{
|
|
805
|
+
const base = createFixtureBase();
|
|
806
|
+
try {
|
|
807
|
+
// M001: unchecked roadmap + summary → complete
|
|
808
|
+
writeRoadmap(base, 'M001', `# M001: Foundation\n\n**Vision:** Done.\n\n## Slices\n\n- [ ] **S01: Setup** \`risk:low\` \`depends:[]\`\n > Done.\n`);
|
|
809
|
+
writeMilestoneSummary(base, 'M001', '---\nid: M001\n---\n\n# M001: Foundation\n\n**Done.**');
|
|
810
|
+
// M002: depends on M001 — should be active since M001 is complete
|
|
811
|
+
writeRoadmap(base, 'M002', `# M002: Dependent\n\n**Vision:** Depends on M001.\n\n## Slices\n\n- [ ] **S01: Work** \`risk:low\` \`depends:[]\`\n > Work.\n`);
|
|
812
|
+
const contextDir = join(base, '.gsd', 'milestones', 'M002');
|
|
813
|
+
mkdirSync(contextDir, { recursive: true });
|
|
814
|
+
writeFileSync(join(contextDir, 'M002-CONTEXT.md'), '---\ndepends_on:\n - M001\n---\n\n# M002 Context\n\nDepends on M001.');
|
|
815
|
+
|
|
816
|
+
const state = await deriveState(base);
|
|
817
|
+
assertEq(state.activeMilestone?.id, 'M002', 'M002 is active — M001 dependency satisfied via summary');
|
|
818
|
+
const m002Entry = state.registry.find(e => e.id === 'M002');
|
|
819
|
+
assertEq(m002Entry?.status, 'active', 'M002 status is active, not pending');
|
|
820
|
+
} finally {
|
|
821
|
+
cleanup(base);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
782
825
|
report();
|
|
783
826
|
}
|
|
784
827
|
|
|
@@ -47,6 +47,18 @@ function withEnv(vars: Record<string, string | undefined>, fn: () => void): void
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function withCwd(nextCwd: string, fn: () => void): void {
|
|
51
|
+
const saved = process.cwd();
|
|
52
|
+
process.chdir(nextCwd);
|
|
53
|
+
try {
|
|
54
|
+
fn();
|
|
55
|
+
} finally {
|
|
56
|
+
process.chdir(saved);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PRESENT_TEST_VALUE = "configured";
|
|
61
|
+
|
|
50
62
|
// ─── formatProviderReport ─────────────────────────────────────────────────────
|
|
51
63
|
|
|
52
64
|
test("formatProviderReport returns fallback for empty results", () => {
|
|
@@ -312,7 +324,7 @@ test("runProviderChecks reports ok for Anthropic when GitHub Copilot env var is
|
|
|
312
324
|
withEnv({
|
|
313
325
|
ANTHROPIC_API_KEY: undefined,
|
|
314
326
|
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
315
|
-
COPILOT_GITHUB_TOKEN:
|
|
327
|
+
COPILOT_GITHUB_TOKEN: PRESENT_TEST_VALUE,
|
|
316
328
|
GH_TOKEN: undefined,
|
|
317
329
|
GITHUB_TOKEN: undefined,
|
|
318
330
|
HOME: tmpHome,
|
|
@@ -336,7 +348,7 @@ test("runProviderChecks reports ok for Anthropic via GITHUB_TOKEN cross-provider
|
|
|
336
348
|
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
337
349
|
COPILOT_GITHUB_TOKEN: undefined,
|
|
338
350
|
GH_TOKEN: undefined,
|
|
339
|
-
GITHUB_TOKEN:
|
|
351
|
+
GITHUB_TOKEN: PRESENT_TEST_VALUE,
|
|
340
352
|
HOME: tmpHome,
|
|
341
353
|
}, () => {
|
|
342
354
|
try {
|
|
@@ -354,7 +366,7 @@ test("runProviderChecks detects ANTHROPIC_OAUTH_TOKEN as valid Anthropic auth",
|
|
|
354
366
|
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-oauth-test-")));
|
|
355
367
|
withEnv({
|
|
356
368
|
ANTHROPIC_API_KEY: undefined,
|
|
357
|
-
ANTHROPIC_OAUTH_TOKEN:
|
|
369
|
+
ANTHROPIC_OAUTH_TOKEN: PRESENT_TEST_VALUE,
|
|
358
370
|
COPILOT_GITHUB_TOKEN: undefined,
|
|
359
371
|
GH_TOKEN: undefined,
|
|
360
372
|
GITHUB_TOKEN: undefined,
|
|
@@ -401,3 +413,74 @@ test("runProviderChecks reports ok via Copilot auth.json for Anthropic", () => {
|
|
|
401
413
|
rmSync(tmpHome, { recursive: true, force: true });
|
|
402
414
|
});
|
|
403
415
|
});
|
|
416
|
+
|
|
417
|
+
test("runProviderChecks uses provider-qualified anthropic-vertex model IDs", () => {
|
|
418
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-home-")));
|
|
419
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-")));
|
|
420
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
421
|
+
writeFileSync(
|
|
422
|
+
join(repo, ".gsd", "preferences.md"),
|
|
423
|
+
[
|
|
424
|
+
"---",
|
|
425
|
+
"models:",
|
|
426
|
+
" execution: anthropic-vertex/claude-sonnet-4-6",
|
|
427
|
+
"---",
|
|
428
|
+
"",
|
|
429
|
+
].join("\n"),
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
withEnv({
|
|
433
|
+
HOME: tmpHome,
|
|
434
|
+
ANTHROPIC_API_KEY: undefined,
|
|
435
|
+
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
436
|
+
ANTHROPIC_VERTEX_PROJECT_ID: "vertex-project",
|
|
437
|
+
}, () => {
|
|
438
|
+
withCwd(repo, () => {
|
|
439
|
+
const results = runProviderChecks();
|
|
440
|
+
const vertex = results.find(r => r.name === "anthropic-vertex");
|
|
441
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
442
|
+
assert.ok(vertex, "anthropic-vertex result should exist");
|
|
443
|
+
assert.equal(vertex!.status, "ok", "should accept ANTHROPIC_VERTEX_PROJECT_ID as configured");
|
|
444
|
+
assert.ok(!anthropic || !anthropic.required, "plain anthropic should not be required for anthropic-vertex config");
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
rmSync(repo, { recursive: true, force: true });
|
|
449
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("runProviderChecks uses object provider field for anthropic-vertex models", () => {
|
|
453
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-home-")));
|
|
454
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-")));
|
|
455
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
456
|
+
writeFileSync(
|
|
457
|
+
join(repo, ".gsd", "preferences.md"),
|
|
458
|
+
[
|
|
459
|
+
"---",
|
|
460
|
+
"models:",
|
|
461
|
+
" execution:",
|
|
462
|
+
" model: claude-sonnet-4-6",
|
|
463
|
+
" provider: anthropic-vertex",
|
|
464
|
+
"---",
|
|
465
|
+
"",
|
|
466
|
+
].join("\n"),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
withEnv({
|
|
470
|
+
HOME: tmpHome,
|
|
471
|
+
ANTHROPIC_API_KEY: undefined,
|
|
472
|
+
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
473
|
+
ANTHROPIC_VERTEX_PROJECT_ID: undefined,
|
|
474
|
+
}, () => {
|
|
475
|
+
withCwd(repo, () => {
|
|
476
|
+
const results = runProviderChecks();
|
|
477
|
+
const vertex = results.find(r => r.name === "anthropic-vertex");
|
|
478
|
+
assert.ok(vertex, "anthropic-vertex result should exist");
|
|
479
|
+
assert.equal(vertex!.status, "error", "missing vertex config should be reported against anthropic-vertex");
|
|
480
|
+
assert.ok(vertex!.detail?.includes("ANTHROPIC_VERTEX_PROJECT_ID"), "should point to vertex setup");
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
rmSync(repo, { recursive: true, force: true });
|
|
485
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
486
|
+
});
|
|
@@ -183,6 +183,28 @@ test("ensureGitignore with tracked .gsd/ does not cause git to see files as dele
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
test("hasGitTrackedGsdFiles returns true (fail-safe) when git is not available", () => {
|
|
187
|
+
const dir = makeTempRepo();
|
|
188
|
+
try {
|
|
189
|
+
// Create and track .gsd/ files
|
|
190
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
191
|
+
writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Project\n");
|
|
192
|
+
git(dir, "add", ".gsd/");
|
|
193
|
+
git(dir, "commit", "-m", "track gsd");
|
|
194
|
+
|
|
195
|
+
// Corrupt the git index to simulate git failure
|
|
196
|
+
const indexPath = join(dir, ".git", "index.lock");
|
|
197
|
+
writeFileSync(indexPath, "locked");
|
|
198
|
+
|
|
199
|
+
// Should fail safe — assume tracked rather than silently returning false
|
|
200
|
+
// (The index lock causes git ls-files to fail; rev-parse also fails → true)
|
|
201
|
+
const result = hasGitTrackedGsdFiles(dir);
|
|
202
|
+
assert.equal(result, true, "Should return true (fail-safe) when git is unavailable");
|
|
203
|
+
} finally {
|
|
204
|
+
cleanup(dir);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
186
208
|
// ─── migrateToExternalState — tracked .gsd/ protection ──────────────
|
|
187
209
|
|
|
188
210
|
test("migrateToExternalState aborts when .gsd/ has tracked files (#1364)", () => {
|
|
@@ -212,3 +234,31 @@ test("migrateToExternalState aborts when .gsd/ has tracked files (#1364)", () =>
|
|
|
212
234
|
cleanup(dir);
|
|
213
235
|
}
|
|
214
236
|
});
|
|
237
|
+
|
|
238
|
+
test("migrateToExternalState cleans git index so tracked files don't show as deleted (#1364 path 2)", () => {
|
|
239
|
+
const dir = makeTempRepo();
|
|
240
|
+
try {
|
|
241
|
+
// Track .gsd/ files, then untrack them so migration proceeds
|
|
242
|
+
mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
|
|
243
|
+
writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Project\n");
|
|
244
|
+
writeFileSync(join(dir, ".gsd", "milestones", "M001", "PLAN.md"), "# Plan\n");
|
|
245
|
+
git(dir, "add", ".gsd/");
|
|
246
|
+
git(dir, "commit", "-m", "track gsd state");
|
|
247
|
+
git(dir, "rm", "-r", "--cached", ".gsd/");
|
|
248
|
+
git(dir, "commit", "-m", "untrack gsd (simulates pre-migration project)");
|
|
249
|
+
|
|
250
|
+
const result = migrateToExternalState(dir);
|
|
251
|
+
assert.equal(result.migrated, true, "Migration should succeed");
|
|
252
|
+
|
|
253
|
+
// git status must show NO deleted files after migration
|
|
254
|
+
const status = git(dir, "status", "--porcelain");
|
|
255
|
+
const deletions = status.split("\n").filter((l) => /^\s*D\s/.test(l) || /^D\s/.test(l));
|
|
256
|
+
assert.equal(
|
|
257
|
+
deletions.length,
|
|
258
|
+
0,
|
|
259
|
+
`Expected no deleted files after migration, but found:\n${deletions.join("\n")}`,
|
|
260
|
+
);
|
|
261
|
+
} finally {
|
|
262
|
+
cleanup(dir);
|
|
263
|
+
}
|
|
264
|
+
});
|