gsd-pi 2.17.0 → 2.19.0
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 +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +399 -29
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +382 -23
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +132 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -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 +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- 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 +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +399 -29
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +382 -23
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +132 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseSlackReply, parseDiscordResponse, formatForDiscord } from "../../remote-questions/format.ts";
|
|
4
7
|
import { resolveRemoteConfig, isValidChannelId } from "../../remote-questions/config.ts";
|
|
5
8
|
import { sanitizeError } from "../../remote-questions/manager.ts";
|
|
6
9
|
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
7
13
|
test("parseSlackReply handles single-number single-question answers", () => {
|
|
8
14
|
const result = parseSlackReply("2", [{
|
|
9
15
|
id: "choice",
|
|
@@ -153,3 +159,223 @@ test("sanitizeError preserves short safe messages", () => {
|
|
|
153
159
|
assert.equal(sanitizeError("Connection refused"), "Connection refused");
|
|
154
160
|
});
|
|
155
161
|
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
164
|
+
// Discord Parity Tests
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
test("formatForDiscord includes context source in footer when present", () => {
|
|
168
|
+
const prompt = {
|
|
169
|
+
id: "test-1",
|
|
170
|
+
channel: "discord" as const,
|
|
171
|
+
createdAt: Date.now(),
|
|
172
|
+
timeoutAt: Date.now() + 60000,
|
|
173
|
+
pollIntervalMs: 5000,
|
|
174
|
+
context: { source: "auto-mode-dispatch" },
|
|
175
|
+
questions: [{
|
|
176
|
+
id: "q1",
|
|
177
|
+
header: "Confirm",
|
|
178
|
+
question: "Proceed?",
|
|
179
|
+
options: [
|
|
180
|
+
{ label: "Yes", description: "Continue" },
|
|
181
|
+
{ label: "No", description: "Stop" },
|
|
182
|
+
],
|
|
183
|
+
allowMultiple: false,
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const { embeds } = formatForDiscord(prompt);
|
|
188
|
+
assert.equal(embeds.length, 1);
|
|
189
|
+
assert.ok(embeds[0].footer?.text.includes("auto-mode-dispatch"), "footer should include context source");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("formatForDiscord omits source from footer when context is absent", () => {
|
|
193
|
+
const prompt = {
|
|
194
|
+
id: "test-2",
|
|
195
|
+
channel: "discord" as const,
|
|
196
|
+
createdAt: Date.now(),
|
|
197
|
+
timeoutAt: Date.now() + 60000,
|
|
198
|
+
pollIntervalMs: 5000,
|
|
199
|
+
questions: [{
|
|
200
|
+
id: "q1",
|
|
201
|
+
header: "Choice",
|
|
202
|
+
question: "Pick one",
|
|
203
|
+
options: [
|
|
204
|
+
{ label: "A", description: "Alpha" },
|
|
205
|
+
{ label: "B", description: "Beta" },
|
|
206
|
+
],
|
|
207
|
+
allowMultiple: false,
|
|
208
|
+
}],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const { embeds } = formatForDiscord(prompt);
|
|
212
|
+
assert.ok(!embeds[0].footer?.text.includes("Source:"), "footer should not include Source when context absent");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("formatForDiscord multi-question footer includes question position", () => {
|
|
216
|
+
const prompt = {
|
|
217
|
+
id: "test-3",
|
|
218
|
+
channel: "discord" as const,
|
|
219
|
+
createdAt: Date.now(),
|
|
220
|
+
timeoutAt: Date.now() + 60000,
|
|
221
|
+
pollIntervalMs: 5000,
|
|
222
|
+
questions: [
|
|
223
|
+
{
|
|
224
|
+
id: "q1",
|
|
225
|
+
header: "First",
|
|
226
|
+
question: "Pick",
|
|
227
|
+
options: [{ label: "A", description: "a" }],
|
|
228
|
+
allowMultiple: false,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "q2",
|
|
232
|
+
header: "Second",
|
|
233
|
+
question: "Pick",
|
|
234
|
+
options: [{ label: "B", description: "b" }],
|
|
235
|
+
allowMultiple: false,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const { embeds } = formatForDiscord(prompt);
|
|
241
|
+
assert.equal(embeds.length, 2);
|
|
242
|
+
assert.ok(embeds[0].footer?.text.includes("1/2"), "first embed footer should show 1/2");
|
|
243
|
+
assert.ok(embeds[1].footer?.text.includes("2/2"), "second embed footer should show 2/2");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("formatForDiscord single-question generates reaction emojis", () => {
|
|
247
|
+
const prompt = {
|
|
248
|
+
id: "test-4",
|
|
249
|
+
channel: "discord" as const,
|
|
250
|
+
createdAt: Date.now(),
|
|
251
|
+
timeoutAt: Date.now() + 60000,
|
|
252
|
+
pollIntervalMs: 5000,
|
|
253
|
+
questions: [{
|
|
254
|
+
id: "q1",
|
|
255
|
+
header: "Pick",
|
|
256
|
+
question: "Choose",
|
|
257
|
+
options: [
|
|
258
|
+
{ label: "A", description: "a" },
|
|
259
|
+
{ label: "B", description: "b" },
|
|
260
|
+
{ label: "C", description: "c" },
|
|
261
|
+
],
|
|
262
|
+
allowMultiple: false,
|
|
263
|
+
}],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const { reactionEmojis } = formatForDiscord(prompt);
|
|
267
|
+
assert.equal(reactionEmojis.length, 3, "should generate 3 reaction emojis for 3 options");
|
|
268
|
+
assert.equal(reactionEmojis[0], "1️⃣");
|
|
269
|
+
assert.equal(reactionEmojis[1], "2️⃣");
|
|
270
|
+
assert.equal(reactionEmojis[2], "3️⃣");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("formatForDiscord multi-question generates no reaction emojis", () => {
|
|
274
|
+
const prompt = {
|
|
275
|
+
id: "test-5",
|
|
276
|
+
channel: "discord" as const,
|
|
277
|
+
createdAt: Date.now(),
|
|
278
|
+
timeoutAt: Date.now() + 60000,
|
|
279
|
+
pollIntervalMs: 5000,
|
|
280
|
+
questions: [
|
|
281
|
+
{
|
|
282
|
+
id: "q1",
|
|
283
|
+
header: "First",
|
|
284
|
+
question: "Pick",
|
|
285
|
+
options: [{ label: "A", description: "a" }],
|
|
286
|
+
allowMultiple: false,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "q2",
|
|
290
|
+
header: "Second",
|
|
291
|
+
question: "Pick",
|
|
292
|
+
options: [{ label: "B", description: "b" }],
|
|
293
|
+
allowMultiple: false,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const { reactionEmojis } = formatForDiscord(prompt);
|
|
299
|
+
assert.equal(reactionEmojis.length, 0, "multi-question should not generate reaction emojis");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("parseDiscordResponse handles multi-question text reply via semicolons", () => {
|
|
303
|
+
const result = parseDiscordResponse([], "1;2", [
|
|
304
|
+
{
|
|
305
|
+
id: "first",
|
|
306
|
+
header: "First",
|
|
307
|
+
question: "Pick one",
|
|
308
|
+
allowMultiple: false,
|
|
309
|
+
options: [
|
|
310
|
+
{ label: "Alpha", description: "A" },
|
|
311
|
+
{ label: "Beta", description: "B" },
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "second",
|
|
316
|
+
header: "Second",
|
|
317
|
+
question: "Pick one",
|
|
318
|
+
allowMultiple: false,
|
|
319
|
+
options: [
|
|
320
|
+
{ label: "Gamma", description: "G" },
|
|
321
|
+
{ label: "Delta", description: "D" },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
assert.deepEqual(result.answers.first.answers, ["Alpha"]);
|
|
327
|
+
assert.deepEqual(result.answers.second.answers, ["Delta"]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("parseDiscordResponse handles multiple reactions for allowMultiple question", () => {
|
|
331
|
+
const result = parseDiscordResponse(
|
|
332
|
+
[{ emoji: "1️⃣", count: 1 }, { emoji: "3️⃣", count: 1 }],
|
|
333
|
+
null,
|
|
334
|
+
[{
|
|
335
|
+
id: "choice",
|
|
336
|
+
header: "Choice",
|
|
337
|
+
question: "Pick any",
|
|
338
|
+
allowMultiple: true,
|
|
339
|
+
options: [
|
|
340
|
+
{ label: "Alpha", description: "A" },
|
|
341
|
+
{ label: "Beta", description: "B" },
|
|
342
|
+
{ label: "Gamma", description: "G" },
|
|
343
|
+
],
|
|
344
|
+
}],
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
assert.deepEqual(result.answers.choice.answers, ["Alpha", "Gamma"]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("DiscordAdapter source-level: acknowledgeAnswer method exists", () => {
|
|
351
|
+
const adapterSrc = readFileSync(
|
|
352
|
+
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
353
|
+
"utf-8",
|
|
354
|
+
);
|
|
355
|
+
assert.ok(adapterSrc.includes("async acknowledgeAnswer"), "should have acknowledgeAnswer method");
|
|
356
|
+
assert.ok(adapterSrc.includes("✅"), "should use checkmark emoji for acknowledgement");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
|
|
360
|
+
const adapterSrc = readFileSync(
|
|
361
|
+
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
362
|
+
"utf-8",
|
|
363
|
+
);
|
|
364
|
+
assert.ok(adapterSrc.includes("guildId"), "should track guild ID");
|
|
365
|
+
assert.ok(adapterSrc.includes("guild_id"), "should read guild_id from channel info");
|
|
366
|
+
assert.ok(
|
|
367
|
+
adapterSrc.includes("discord.com/channels/"),
|
|
368
|
+
"should construct message URL with guild/channel/message format",
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
|
|
373
|
+
const adapterSrc = readFileSync(
|
|
374
|
+
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
375
|
+
"utf-8",
|
|
376
|
+
);
|
|
377
|
+
assert.ok(
|
|
378
|
+
adapterSrc.includes("threadUrl: messageUrl"),
|
|
379
|
+
"sendPrompt should set threadUrl to the constructed message URL",
|
|
380
|
+
);
|
|
381
|
+
});
|
|
@@ -1,87 +1,240 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Routing History — structural tests for adaptive learning module.
|
|
3
|
-
*
|
|
4
|
-
* Verifies routing-history.ts exports and structure from #579.
|
|
5
|
-
* Uses source-level checks to avoid @gsd/pi-coding-agent import chain.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import test from "node:test";
|
|
9
2
|
import assert from "node:assert/strict";
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
|
-
import { join
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
initRoutingHistory,
|
|
9
|
+
resetRoutingHistory,
|
|
10
|
+
recordOutcome,
|
|
11
|
+
recordFeedback,
|
|
12
|
+
getAdaptiveTierAdjustment,
|
|
13
|
+
clearRoutingHistory,
|
|
14
|
+
getRoutingHistory,
|
|
15
|
+
} from "../routing-history.js";
|
|
16
|
+
|
|
17
|
+
// ─── Test Setup ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function makeTmpDir(): string {
|
|
20
|
+
const dir = join(tmpdir(), `gsd-routing-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
21
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cleanup(dir: string): void {
|
|
26
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
27
|
+
resetRoutingHistory();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── recordOutcome ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
test("recordOutcome tracks success and failure counts", () => {
|
|
33
|
+
const dir = makeTmpDir();
|
|
34
|
+
try {
|
|
35
|
+
initRoutingHistory(dir);
|
|
36
|
+
recordOutcome("execute-task", "standard", true);
|
|
37
|
+
recordOutcome("execute-task", "standard", true);
|
|
38
|
+
recordOutcome("execute-task", "standard", false);
|
|
39
|
+
|
|
40
|
+
const history = getRoutingHistory();
|
|
41
|
+
assert.ok(history);
|
|
42
|
+
const pattern = history.patterns["execute-task"];
|
|
43
|
+
assert.ok(pattern);
|
|
44
|
+
assert.equal(pattern.standard.success, 2);
|
|
45
|
+
assert.equal(pattern.standard.fail, 1);
|
|
46
|
+
} finally {
|
|
47
|
+
cleanup(dir);
|
|
48
|
+
}
|
|
23
49
|
});
|
|
24
50
|
|
|
25
|
-
test("
|
|
26
|
-
|
|
51
|
+
test("recordOutcome tracks tag-specific patterns", () => {
|
|
52
|
+
const dir = makeTmpDir();
|
|
53
|
+
try {
|
|
54
|
+
initRoutingHistory(dir);
|
|
55
|
+
recordOutcome("execute-task", "light", true, ["docs"]);
|
|
56
|
+
|
|
57
|
+
const history = getRoutingHistory();
|
|
58
|
+
assert.ok(history);
|
|
59
|
+
assert.ok(history.patterns["execute-task:docs"]);
|
|
60
|
+
assert.equal(history.patterns["execute-task:docs"].light.success, 1);
|
|
61
|
+
} finally {
|
|
62
|
+
cleanup(dir);
|
|
63
|
+
}
|
|
27
64
|
});
|
|
28
65
|
|
|
29
|
-
test("
|
|
30
|
-
|
|
66
|
+
test("recordOutcome applies rolling window", () => {
|
|
67
|
+
const dir = makeTmpDir();
|
|
68
|
+
try {
|
|
69
|
+
initRoutingHistory(dir);
|
|
70
|
+
// Record 60 successes — should be capped to 50
|
|
71
|
+
for (let i = 0; i < 60; i++) {
|
|
72
|
+
recordOutcome("execute-task", "standard", true);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const history = getRoutingHistory();
|
|
76
|
+
assert.ok(history);
|
|
77
|
+
const total = history.patterns["execute-task"].standard.success +
|
|
78
|
+
history.patterns["execute-task"].standard.fail;
|
|
79
|
+
assert.ok(total <= 50, `total ${total} should be <= 50`);
|
|
80
|
+
} finally {
|
|
81
|
+
cleanup(dir);
|
|
82
|
+
}
|
|
31
83
|
});
|
|
32
84
|
|
|
33
|
-
|
|
34
|
-
|
|
85
|
+
// ─── getAdaptiveTierAdjustment ───────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
test("no adjustment when insufficient data", () => {
|
|
88
|
+
const dir = makeTmpDir();
|
|
89
|
+
try {
|
|
90
|
+
initRoutingHistory(dir);
|
|
91
|
+
recordOutcome("execute-task", "light", false);
|
|
92
|
+
// Only 1 data point — not enough
|
|
93
|
+
const adj = getAdaptiveTierAdjustment("execute-task", "light");
|
|
94
|
+
assert.equal(adj, null);
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(dir);
|
|
97
|
+
}
|
|
35
98
|
});
|
|
36
99
|
|
|
37
|
-
test("
|
|
38
|
-
|
|
100
|
+
test("bumps tier when failure rate exceeds threshold", () => {
|
|
101
|
+
const dir = makeTmpDir();
|
|
102
|
+
try {
|
|
103
|
+
initRoutingHistory(dir);
|
|
104
|
+
// Record high failure rate at light tier
|
|
105
|
+
recordOutcome("execute-task", "light", false);
|
|
106
|
+
recordOutcome("execute-task", "light", false);
|
|
107
|
+
recordOutcome("execute-task", "light", true);
|
|
108
|
+
// 2/3 = 66% failure rate > 20% threshold
|
|
109
|
+
|
|
110
|
+
const adj = getAdaptiveTierAdjustment("execute-task", "light");
|
|
111
|
+
assert.equal(adj, "standard");
|
|
112
|
+
} finally {
|
|
113
|
+
cleanup(dir);
|
|
114
|
+
}
|
|
39
115
|
});
|
|
40
116
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
117
|
+
test("no adjustment when success rate is high", () => {
|
|
118
|
+
const dir = makeTmpDir();
|
|
119
|
+
try {
|
|
120
|
+
initRoutingHistory(dir);
|
|
121
|
+
for (let i = 0; i < 10; i++) {
|
|
122
|
+
recordOutcome("execute-task", "light", true);
|
|
123
|
+
}
|
|
124
|
+
const adj = getAdaptiveTierAdjustment("execute-task", "light");
|
|
125
|
+
assert.equal(adj, null);
|
|
126
|
+
} finally {
|
|
127
|
+
cleanup(dir);
|
|
128
|
+
}
|
|
47
129
|
});
|
|
48
130
|
|
|
49
|
-
test("
|
|
50
|
-
|
|
131
|
+
test("tag-specific patterns take precedence", () => {
|
|
132
|
+
const dir = makeTmpDir();
|
|
133
|
+
try {
|
|
134
|
+
initRoutingHistory(dir);
|
|
135
|
+
// Base pattern has high success rate (tagged calls also count toward base)
|
|
136
|
+
for (let i = 0; i < 15; i++) {
|
|
137
|
+
recordOutcome("execute-task", "light", true);
|
|
138
|
+
}
|
|
139
|
+
// But docs-tagged tasks fail at light
|
|
140
|
+
recordOutcome("execute-task", "light", false, ["docs"]);
|
|
141
|
+
recordOutcome("execute-task", "light", false, ["docs"]);
|
|
142
|
+
recordOutcome("execute-task", "light", true, ["docs"]);
|
|
143
|
+
|
|
144
|
+
// With tags, should bump (docs pattern: 1/3 success = 66% failure)
|
|
145
|
+
const adj = getAdaptiveTierAdjustment("execute-task", "light", ["docs"]);
|
|
146
|
+
assert.equal(adj, "standard");
|
|
147
|
+
|
|
148
|
+
// Without tags, should not bump (base: 16/18 success = 11% failure)
|
|
149
|
+
const adjBase = getAdaptiveTierAdjustment("execute-task", "light");
|
|
150
|
+
assert.equal(adjBase, null);
|
|
151
|
+
} finally {
|
|
152
|
+
cleanup(dir);
|
|
153
|
+
}
|
|
51
154
|
});
|
|
52
155
|
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
// ─── recordFeedback ──────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
test("recordFeedback stores feedback entries", () => {
|
|
159
|
+
const dir = makeTmpDir();
|
|
160
|
+
try {
|
|
161
|
+
initRoutingHistory(dir);
|
|
162
|
+
recordFeedback("execute-task", "M001/S01/T01", "standard", "over");
|
|
163
|
+
|
|
164
|
+
const history = getRoutingHistory();
|
|
165
|
+
assert.ok(history);
|
|
166
|
+
assert.equal(history.feedback.length, 1);
|
|
167
|
+
assert.equal(history.feedback[0].rating, "over");
|
|
168
|
+
assert.equal(history.feedback[0].tier, "standard");
|
|
169
|
+
} finally {
|
|
170
|
+
cleanup(dir);
|
|
171
|
+
}
|
|
55
172
|
});
|
|
56
173
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
174
|
+
test("recordFeedback 'under' increases failure count at tier", () => {
|
|
175
|
+
const dir = makeTmpDir();
|
|
176
|
+
try {
|
|
177
|
+
initRoutingHistory(dir);
|
|
178
|
+
recordFeedback("execute-task", "M001/S01/T01", "light", "under");
|
|
179
|
+
|
|
180
|
+
const history = getRoutingHistory();
|
|
181
|
+
assert.ok(history);
|
|
182
|
+
// "under" adds 2 (FEEDBACK_WEIGHT) failures
|
|
183
|
+
assert.equal(history.patterns["execute-task"].light.fail, 2);
|
|
184
|
+
} finally {
|
|
185
|
+
cleanup(dir);
|
|
186
|
+
}
|
|
70
187
|
});
|
|
71
188
|
|
|
72
|
-
test("
|
|
73
|
-
|
|
189
|
+
test("recordFeedback 'over' increases success count at lower tier", () => {
|
|
190
|
+
const dir = makeTmpDir();
|
|
191
|
+
try {
|
|
192
|
+
initRoutingHistory(dir);
|
|
193
|
+
recordFeedback("execute-task", "M001/S01/T01", "standard", "over");
|
|
194
|
+
|
|
195
|
+
const history = getRoutingHistory();
|
|
196
|
+
assert.ok(history);
|
|
197
|
+
// "over" at standard → adds 2 successes at light
|
|
198
|
+
assert.equal(history.patterns["execute-task"].light.success, 2);
|
|
199
|
+
} finally {
|
|
200
|
+
cleanup(dir);
|
|
201
|
+
}
|
|
74
202
|
});
|
|
75
203
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
204
|
+
// ─── clearRoutingHistory ─────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
test("clearRoutingHistory resets all data", () => {
|
|
207
|
+
const dir = makeTmpDir();
|
|
208
|
+
try {
|
|
209
|
+
initRoutingHistory(dir);
|
|
210
|
+
recordOutcome("execute-task", "light", true);
|
|
211
|
+
clearRoutingHistory(dir);
|
|
212
|
+
|
|
213
|
+
const history = getRoutingHistory();
|
|
214
|
+
assert.ok(history);
|
|
215
|
+
assert.deepEqual(history.patterns, {});
|
|
216
|
+
assert.deepEqual(history.feedback, []);
|
|
217
|
+
} finally {
|
|
218
|
+
cleanup(dir);
|
|
219
|
+
}
|
|
82
220
|
});
|
|
83
221
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
222
|
+
// ─── Persistence ─────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
test("routing history persists to disk and reloads", () => {
|
|
225
|
+
const dir = makeTmpDir();
|
|
226
|
+
try {
|
|
227
|
+
initRoutingHistory(dir);
|
|
228
|
+
recordOutcome("execute-task", "standard", true);
|
|
229
|
+
recordOutcome("execute-task", "standard", true);
|
|
230
|
+
resetRoutingHistory();
|
|
231
|
+
|
|
232
|
+
// Reload from disk
|
|
233
|
+
initRoutingHistory(dir);
|
|
234
|
+
const history = getRoutingHistory();
|
|
235
|
+
assert.ok(history);
|
|
236
|
+
assert.equal(history.patterns["execute-task"].standard.success, 2);
|
|
237
|
+
} finally {
|
|
238
|
+
cleanup(dir);
|
|
239
|
+
}
|
|
87
240
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stale-worktree-cwd.test.ts — Tests for #608 fix.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that when process.cwd() is inside a stale .gsd/worktrees/ path,
|
|
5
|
+
* startAuto escapes back to the project root before proceeding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, mkdirSync, rmSync, existsSync, realpathSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join, sep } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createAutoWorktree,
|
|
17
|
+
teardownAutoWorktree,
|
|
18
|
+
mergeMilestoneToMain,
|
|
19
|
+
} from "../auto-worktree.ts";
|
|
20
|
+
|
|
21
|
+
function run(command: string, cwd: string): string {
|
|
22
|
+
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createTempRepo(): string {
|
|
26
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "stale-wt-test-")));
|
|
27
|
+
run("git init", dir);
|
|
28
|
+
run("git config user.email test@test.com", dir);
|
|
29
|
+
run("git config user.name Test", dir);
|
|
30
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
31
|
+
run("git add .", dir);
|
|
32
|
+
run("git commit -m init", dir);
|
|
33
|
+
run("git branch -M main", dir);
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── escapeStaleWorktree is called by startAuto, test the detection logic ────
|
|
38
|
+
|
|
39
|
+
test("detects stale worktree path and extracts project root", () => {
|
|
40
|
+
// Simulate the path pattern: /project/.gsd/worktrees/M004/...
|
|
41
|
+
const projectRoot = "/Users/test/myproject";
|
|
42
|
+
const stalePath = `${projectRoot}${sep}.gsd${sep}worktrees${sep}M004`;
|
|
43
|
+
|
|
44
|
+
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
45
|
+
const idx = stalePath.indexOf(marker);
|
|
46
|
+
|
|
47
|
+
assert.ok(idx !== -1, "marker found in stale path");
|
|
48
|
+
assert.equal(stalePath.slice(0, idx), projectRoot, "project root extracted correctly");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("does not trigger on normal project path", () => {
|
|
52
|
+
const normalPath = "/Users/test/myproject";
|
|
53
|
+
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
54
|
+
const idx = normalPath.indexOf(marker);
|
|
55
|
+
|
|
56
|
+
assert.equal(idx, -1, "marker not found in normal path");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ─── Integration: mergeMilestoneToMain restores cwd ─────────────────────────
|
|
60
|
+
|
|
61
|
+
test("mergeMilestoneToMain restores cwd to project root", () => {
|
|
62
|
+
const savedCwd = process.cwd();
|
|
63
|
+
let tempDir = "";
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
tempDir = createTempRepo();
|
|
67
|
+
|
|
68
|
+
// Create milestone planning artifacts
|
|
69
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M050");
|
|
70
|
+
mkdirSync(msDir, { recursive: true });
|
|
71
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M050 Context\n");
|
|
72
|
+
const roadmap = [
|
|
73
|
+
"# M050: Test Milestone",
|
|
74
|
+
"**Vision**: testing",
|
|
75
|
+
"## Success Criteria",
|
|
76
|
+
"- It works",
|
|
77
|
+
"## Slices",
|
|
78
|
+
"- [x] S01 — First slice",
|
|
79
|
+
].join("\n");
|
|
80
|
+
writeFileSync(join(msDir, "ROADMAP.md"), roadmap);
|
|
81
|
+
run("git add .", tempDir);
|
|
82
|
+
run("git commit -m \"add milestone\"", tempDir);
|
|
83
|
+
|
|
84
|
+
// Create auto-worktree (enters the worktree dir)
|
|
85
|
+
const wtPath = createAutoWorktree(tempDir, "M050");
|
|
86
|
+
assert.equal(process.cwd(), wtPath, "cwd is in worktree after create");
|
|
87
|
+
|
|
88
|
+
// Add a change in the worktree
|
|
89
|
+
writeFileSync(join(wtPath, "feature.txt"), "new feature\n");
|
|
90
|
+
run("git add .", wtPath);
|
|
91
|
+
run("git commit -m \"feat: add feature\"", wtPath);
|
|
92
|
+
|
|
93
|
+
// Merge back — should restore cwd to tempDir
|
|
94
|
+
mergeMilestoneToMain(tempDir, "M050", roadmap);
|
|
95
|
+
|
|
96
|
+
assert.equal(process.cwd(), tempDir, "cwd restored to project root after merge");
|
|
97
|
+
assert.ok(!existsSync(wtPath), "worktree directory removed after merge");
|
|
98
|
+
} finally {
|
|
99
|
+
process.chdir(savedCwd);
|
|
100
|
+
if (tempDir && existsSync(tempDir)) {
|
|
101
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── Integration: stale worktree directory is detectable ────────────────────
|
|
107
|
+
|
|
108
|
+
test("process.cwd() inside removed worktree is recoverable", () => {
|
|
109
|
+
const savedCwd = process.cwd();
|
|
110
|
+
let tempDir = "";
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
tempDir = createTempRepo();
|
|
114
|
+
|
|
115
|
+
// Create a .gsd/worktrees/M099 directory to simulate stale state
|
|
116
|
+
const staleWtDir = join(tempDir, ".gsd", "worktrees", "M099");
|
|
117
|
+
mkdirSync(staleWtDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
// Enter the stale directory
|
|
120
|
+
process.chdir(staleWtDir);
|
|
121
|
+
const cwdBefore = process.cwd();
|
|
122
|
+
assert.ok(cwdBefore.includes(`${sep}.gsd${sep}worktrees${sep}`), "cwd is inside worktree dir");
|
|
123
|
+
|
|
124
|
+
// Simulate escapeStaleWorktree logic
|
|
125
|
+
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
126
|
+
const idx = cwdBefore.indexOf(marker);
|
|
127
|
+
assert.ok(idx !== -1, "marker found");
|
|
128
|
+
|
|
129
|
+
const projectRoot = cwdBefore.slice(0, idx);
|
|
130
|
+
process.chdir(projectRoot);
|
|
131
|
+
|
|
132
|
+
assert.equal(process.cwd(), tempDir, "successfully escaped to project root");
|
|
133
|
+
} finally {
|
|
134
|
+
process.chdir(savedCwd);
|
|
135
|
+
if (tempDir && existsSync(tempDir)) {
|
|
136
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|