gsd-pi 2.73.1-dev.47ed32a → 2.73.1-dev.49397e5
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/cli-web-branch.d.ts +4 -3
- package/dist/cli-web-branch.js +10 -7
- package/dist/cli.js +99 -206
- package/dist/headless-query.js +4 -1
- package/dist/logo.d.ts +1 -1
- package/dist/logo.js +1 -1
- package/dist/onboarding.js +59 -53
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +59 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
- package/dist/resources/extensions/gsd/auto/phases.js +60 -10
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +93 -57
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-verification.js +88 -3
- package/dist/resources/extensions/gsd/auto.js +29 -10
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
- package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
- package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
- package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
- package/dist/resources/extensions/gsd/commands-do.js +79 -0
- package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
- package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
- package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
- package/dist/resources/extensions/gsd/commands-ship.js +187 -0
- package/dist/resources/extensions/gsd/db-writer.js +3 -5
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +321 -0
- package/dist/resources/extensions/gsd/index.js +15 -2
- package/dist/resources/extensions/gsd/md-importer.js +3 -4
- package/dist/resources/extensions/gsd/memory-store.js +19 -51
- package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
- package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
- package/dist/resources/extensions/gsd/notification-widget.js +2 -2
- package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
- package/dist/resources/extensions/gsd/state.js +66 -15
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
- package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
- package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
- package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
- package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
- package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
- package/dist/tsconfig.extensions.tsbuildinfo +1 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- 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/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -3
- package/packages/mcp-server/tsconfig.json +1 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
- package/packages/native/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-agent-core/tsconfig.json +1 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-ai/tsconfig.json +1 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +10 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +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 +5 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +12 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -1
- package/packages/pi-coding-agent/tsconfig.json +1 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js +30 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +1 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +22 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +38 -0
- package/packages/pi-tui/src/tui.ts +20 -3
- package/packages/pi-tui/tsconfig.json +1 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
- package/packages/rpc-client/tsconfig.json +1 -0
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +95 -1
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +88 -0
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
- package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
- package/src/resources/extensions/gsd/auto/phases.ts +90 -10
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +107 -58
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
- package/src/resources/extensions/gsd/auto-verification.ts +98 -3
- package/src/resources/extensions/gsd/auto.ts +33 -14
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
- package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
- package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
- package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
- package/src/resources/extensions/gsd/commands-do.ts +109 -0
- package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
- package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
- package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
- package/src/resources/extensions/gsd/commands-ship.ts +219 -0
- package/src/resources/extensions/gsd/db-writer.ts +3 -5
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +467 -0
- package/src/resources/extensions/gsd/index.ts +18 -2
- package/src/resources/extensions/gsd/md-importer.ts +3 -5
- package/src/resources/extensions/gsd/memory-store.ts +31 -62
- package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
- package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
- package/src/resources/extensions/gsd/notification-widget.ts +2 -2
- package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
- package/src/resources/extensions/gsd/state.ts +80 -17
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
- package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
- package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
- package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
- package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
- package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
- package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
- /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTmpBase(): string {
|
|
11
|
+
const base = join(tmpdir(), `gsd-backlog-test-${randomUUID()}`);
|
|
12
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cleanup(base: string): void {
|
|
17
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function backlogPath(base: string): string {
|
|
21
|
+
return join(base, ".gsd", "BACKLOG.md");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeBacklog(base: string, content: string): void {
|
|
25
|
+
writeFileSync(backlogPath(base), content, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readBacklog(base: string): string {
|
|
29
|
+
return readFileSync(backlogPath(base), "utf-8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Test the parsing/writing logic inline since the handler requires runtime context
|
|
33
|
+
|
|
34
|
+
interface BacklogItem {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
done: boolean;
|
|
38
|
+
note: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseBacklog(content: string): BacklogItem[] {
|
|
42
|
+
const items: BacklogItem[] = [];
|
|
43
|
+
for (const line of content.split("\n")) {
|
|
44
|
+
const match = line.match(/^- \[([ x])\] (999\.\d+) — (.+?)(?:\s*\((.+)\))?$/);
|
|
45
|
+
if (match) {
|
|
46
|
+
items.push({
|
|
47
|
+
id: match[2],
|
|
48
|
+
title: match[3].trim(),
|
|
49
|
+
done: match[1] === "x",
|
|
50
|
+
note: match[4] ?? "",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return items;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatBacklog(items: BacklogItem[]): string {
|
|
58
|
+
const lines = ["# Backlog\n"];
|
|
59
|
+
for (const item of items) {
|
|
60
|
+
const check = item.done ? "x" : " ";
|
|
61
|
+
const note = item.note ? ` (${item.note})` : "";
|
|
62
|
+
lines.push(`- [${check}] ${item.id} — ${item.title}${note}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Tests ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
test("backlog: parse empty file returns empty array", () => {
|
|
71
|
+
const items = parseBacklog("");
|
|
72
|
+
assert.equal(items.length, 0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("backlog: parse valid entries", () => {
|
|
76
|
+
const content = `# Backlog
|
|
77
|
+
|
|
78
|
+
- [ ] 999.1 — OAuth support (added 2026-03-23)
|
|
79
|
+
- [x] 999.2 — Rate limiting (promoted 2026-03-24)
|
|
80
|
+
- [ ] 999.3 — Dark mode`;
|
|
81
|
+
|
|
82
|
+
const items = parseBacklog(content);
|
|
83
|
+
assert.equal(items.length, 3);
|
|
84
|
+
assert.equal(items[0].id, "999.1");
|
|
85
|
+
assert.equal(items[0].title, "OAuth support");
|
|
86
|
+
assert.equal(items[0].done, false);
|
|
87
|
+
assert.equal(items[0].note, "added 2026-03-23");
|
|
88
|
+
|
|
89
|
+
assert.equal(items[1].id, "999.2");
|
|
90
|
+
assert.equal(items[1].done, true);
|
|
91
|
+
assert.equal(items[1].note, "promoted 2026-03-24");
|
|
92
|
+
|
|
93
|
+
assert.equal(items[2].id, "999.3");
|
|
94
|
+
assert.equal(items[2].title, "Dark mode");
|
|
95
|
+
assert.equal(items[2].note, "");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("backlog: format roundtrips correctly", () => {
|
|
99
|
+
const items: BacklogItem[] = [
|
|
100
|
+
{ id: "999.1", title: "OAuth support", done: false, note: "added 2026-03-23" },
|
|
101
|
+
{ id: "999.2", title: "Rate limiting", done: true, note: "promoted 2026-03-24" },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const formatted = formatBacklog(items);
|
|
105
|
+
const parsed = parseBacklog(formatted);
|
|
106
|
+
|
|
107
|
+
assert.equal(parsed.length, 2);
|
|
108
|
+
assert.equal(parsed[0].id, "999.1");
|
|
109
|
+
assert.equal(parsed[0].title, "OAuth support");
|
|
110
|
+
assert.equal(parsed[1].done, true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("backlog: write and read from disk", () => {
|
|
114
|
+
const base = makeTmpBase();
|
|
115
|
+
try {
|
|
116
|
+
const items: BacklogItem[] = [
|
|
117
|
+
{ id: "999.1", title: "Test item", done: false, note: "added 2026-03-23" },
|
|
118
|
+
];
|
|
119
|
+
writeBacklog(base, formatBacklog(items));
|
|
120
|
+
|
|
121
|
+
assert.ok(existsSync(backlogPath(base)));
|
|
122
|
+
const content = readBacklog(base);
|
|
123
|
+
assert.ok(content.includes("999.1"));
|
|
124
|
+
assert.ok(content.includes("Test item"));
|
|
125
|
+
} finally {
|
|
126
|
+
cleanup(base);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("backlog: next ID increments correctly", () => {
|
|
131
|
+
const items: BacklogItem[] = [
|
|
132
|
+
{ id: "999.1", title: "First", done: false, note: "" },
|
|
133
|
+
{ id: "999.2", title: "Second", done: false, note: "" },
|
|
134
|
+
{ id: "999.5", title: "Fifth", done: false, note: "" },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
let maxNum = 0;
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
const match = item.id.match(/^999\.(\d+)$/);
|
|
140
|
+
if (match) {
|
|
141
|
+
const num = parseInt(match[1], 10);
|
|
142
|
+
if (num > maxNum) maxNum = num;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const nextId = `999.${maxNum + 1}`;
|
|
146
|
+
assert.equal(nextId, "999.6");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("backlog: empty backlog returns no items", () => {
|
|
150
|
+
const base = makeTmpBase();
|
|
151
|
+
try {
|
|
152
|
+
// No BACKLOG.md exists
|
|
153
|
+
assert.ok(!existsSync(backlogPath(base)));
|
|
154
|
+
// Would return empty array
|
|
155
|
+
} finally {
|
|
156
|
+
cleanup(base);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// ─── Mock dispatcher to capture routed commands ─────────────────────────
|
|
5
|
+
|
|
6
|
+
let lastRouted: string | null = null;
|
|
7
|
+
let lastQuick: string | null = null;
|
|
8
|
+
|
|
9
|
+
const mockCtx = {
|
|
10
|
+
ui: {
|
|
11
|
+
notify: (_msg: string, _level: string) => {},
|
|
12
|
+
},
|
|
13
|
+
} as any;
|
|
14
|
+
|
|
15
|
+
// We test the keyword matching logic directly since the handler imports
|
|
16
|
+
// the dispatcher dynamically (which requires the full extension runtime).
|
|
17
|
+
|
|
18
|
+
// Inline the route-matching logic from commands-do.ts for unit testing.
|
|
19
|
+
interface Route {
|
|
20
|
+
keywords: string[];
|
|
21
|
+
command: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ROUTES: Route[] = [
|
|
25
|
+
{ keywords: ["progress", "status", "dashboard", "how far", "where are we"], command: "status" },
|
|
26
|
+
{ keywords: ["auto", "autonomous", "run all", "keep going", "start auto"], command: "auto" },
|
|
27
|
+
{ keywords: ["stop", "halt", "abort"], command: "stop" },
|
|
28
|
+
{ keywords: ["pause", "break", "take a break"], command: "pause" },
|
|
29
|
+
{ keywords: ["history", "past", "what happened", "previous"], command: "history" },
|
|
30
|
+
{ keywords: ["doctor", "health", "diagnose", "check health"], command: "doctor" },
|
|
31
|
+
{ keywords: ["clean up", "cleanup", "remove old", "prune", "tidy"], command: "cleanup" },
|
|
32
|
+
{ keywords: ["ship", "pull request", "create pr", "open pr", "merge"], command: "ship" },
|
|
33
|
+
{ keywords: ["discuss", "talk about", "architecture", "design"], command: "discuss" },
|
|
34
|
+
{ keywords: ["undo", "revert", "rollback", "take back"], command: "undo" },
|
|
35
|
+
{ keywords: ["skip", "skip task", "skip this"], command: "skip" },
|
|
36
|
+
{ keywords: ["visualize", "viz", "graph", "chart", "show graph"], command: "visualize" },
|
|
37
|
+
{ keywords: ["capture", "note", "idea", "thought", "remember"], command: "capture" },
|
|
38
|
+
{ keywords: ["inspect", "database", "sqlite", "db state"], command: "inspect" },
|
|
39
|
+
{ keywords: ["session report", "session summary", "cost summary", "how much"], command: "session-report" },
|
|
40
|
+
{ keywords: ["backlog", "parking lot", "later", "someday"], command: "backlog" },
|
|
41
|
+
{ keywords: ["add tests", "write tests", "generate tests", "test coverage"], command: "add-tests" },
|
|
42
|
+
{ keywords: ["next", "step", "next step", "what's next"], command: "next" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
interface MatchResult {
|
|
46
|
+
command: string;
|
|
47
|
+
remainingArgs: string;
|
|
48
|
+
score: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function matchRoute(input: string): MatchResult | null {
|
|
52
|
+
const lower = input.toLowerCase();
|
|
53
|
+
let bestMatch: MatchResult | null = null;
|
|
54
|
+
|
|
55
|
+
for (const route of ROUTES) {
|
|
56
|
+
for (const keyword of route.keywords) {
|
|
57
|
+
if (lower.includes(keyword)) {
|
|
58
|
+
const score = keyword.length;
|
|
59
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
60
|
+
const idx = lower.indexOf(keyword);
|
|
61
|
+
const remaining = (input.slice(0, idx) + input.slice(idx + keyword.length)).trim();
|
|
62
|
+
bestMatch = { command: route.command, remainingArgs: remaining, score };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return bestMatch;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Tests ──────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
test("/gsd do: routes 'show me progress' to status", () => {
|
|
74
|
+
const match = matchRoute("show me progress");
|
|
75
|
+
assert.ok(match);
|
|
76
|
+
assert.equal(match.command, "status");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("/gsd do: routes 'run autonomously' to auto", () => {
|
|
80
|
+
const match = matchRoute("run autonomously");
|
|
81
|
+
assert.ok(match);
|
|
82
|
+
assert.equal(match.command, "auto");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("/gsd do: routes 'clean up old branches' to cleanup", () => {
|
|
86
|
+
const match = matchRoute("clean up old branches");
|
|
87
|
+
assert.ok(match);
|
|
88
|
+
assert.equal(match.command, "cleanup");
|
|
89
|
+
assert.equal(match.remainingArgs, "old branches");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("/gsd do: routes 'create pr for milestone' to ship", () => {
|
|
93
|
+
const match = matchRoute("create pr for milestone");
|
|
94
|
+
assert.ok(match);
|
|
95
|
+
assert.equal(match.command, "ship");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("/gsd do: routes 'add tests for S03' to add-tests", () => {
|
|
99
|
+
const match = matchRoute("add tests for S03");
|
|
100
|
+
assert.ok(match);
|
|
101
|
+
assert.equal(match.command, "add-tests");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("/gsd do: routes 'what is next' to next", () => {
|
|
105
|
+
const match = matchRoute("what's next");
|
|
106
|
+
assert.ok(match);
|
|
107
|
+
assert.equal(match.command, "next");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("/gsd do: returns null for unrecognized input", () => {
|
|
111
|
+
const match = matchRoute("florbinate the gizmo");
|
|
112
|
+
assert.equal(match, null);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("/gsd do: prefers longer keyword match", () => {
|
|
116
|
+
// "check health" (12 chars) should beat "health" (6 chars)
|
|
117
|
+
const match = matchRoute("check health of the system");
|
|
118
|
+
assert.ok(match);
|
|
119
|
+
assert.equal(match.command, "doctor");
|
|
120
|
+
assert.ok(match.score >= 12);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("/gsd do: routes 'session report' to session-report", () => {
|
|
124
|
+
const match = matchRoute("show me the session report");
|
|
125
|
+
assert.ok(match);
|
|
126
|
+
assert.equal(match.command, "session-report");
|
|
127
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// Test the filtering logic used by /gsd pr-branch.
|
|
5
|
+
// Full integration requires git operations, so we test the path filtering.
|
|
6
|
+
|
|
7
|
+
test("pr-branch: identifies .gsd/ paths", () => {
|
|
8
|
+
const files = [
|
|
9
|
+
".gsd/milestones/M001/ROADMAP.md",
|
|
10
|
+
".gsd/metrics.json",
|
|
11
|
+
"src/main.ts",
|
|
12
|
+
"package.json",
|
|
13
|
+
".planning/PLAN.md",
|
|
14
|
+
"PLAN.md",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const codeFiles = files.filter(
|
|
18
|
+
(f) => !f.startsWith(".gsd/") && !f.startsWith(".planning/") && f !== "PLAN.md",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(codeFiles, ["src/main.ts", "package.json"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("pr-branch: all .gsd/ files returns empty", () => {
|
|
25
|
+
const files = [
|
|
26
|
+
".gsd/milestones/M001/ROADMAP.md",
|
|
27
|
+
".gsd/metrics.json",
|
|
28
|
+
".gsd/BACKLOG.md",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const codeFiles = files.filter(
|
|
32
|
+
(f) => !f.startsWith(".gsd/") && !f.startsWith(".planning/") && f !== "PLAN.md",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
assert.equal(codeFiles.length, 0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("pr-branch: mixed commits with code changes", () => {
|
|
39
|
+
const files = [
|
|
40
|
+
".gsd/milestones/M001/ROADMAP.md",
|
|
41
|
+
"src/auth.ts",
|
|
42
|
+
"src/auth.test.ts",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const hasCodeChanges = files.some(
|
|
46
|
+
(f) => !f.startsWith(".gsd/") && !f.startsWith(".planning/") && f !== "PLAN.md",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
assert.ok(hasCodeChanges);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("pr-branch: --dry-run flag", () => {
|
|
53
|
+
assert.ok("--dry-run".includes("--dry-run"));
|
|
54
|
+
assert.ok(!"--name my-branch".includes("--dry-run"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("pr-branch: --name flag parsing", () => {
|
|
58
|
+
const args = "--name my-clean-pr";
|
|
59
|
+
const nameMatch = args.match(/--name\s+(\S+)/);
|
|
60
|
+
assert.ok(nameMatch);
|
|
61
|
+
assert.equal(nameMatch[1], "my-clean-pr");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("pr-branch: default branch name", () => {
|
|
65
|
+
const currentBranch = "feat/add-auth";
|
|
66
|
+
const prBranch = `pr/${currentBranch}`;
|
|
67
|
+
assert.equal(prBranch, "pr/feat/add-auth");
|
|
68
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// Test the formatting logic used by session-report.
|
|
5
|
+
// The actual handler requires runtime context (metrics module), so we
|
|
6
|
+
// test the core formatting and aggregation patterns.
|
|
7
|
+
|
|
8
|
+
test("session-report: format cost correctly", () => {
|
|
9
|
+
// Simple cost formatting test
|
|
10
|
+
const formatCost = (cost: number): string => {
|
|
11
|
+
if (cost < 0.01) return "<$0.01";
|
|
12
|
+
return `$${cost.toFixed(2)}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
assert.equal(formatCost(0), "<$0.01");
|
|
16
|
+
assert.equal(formatCost(0.005), "<$0.01");
|
|
17
|
+
assert.equal(formatCost(1.5), "$1.50");
|
|
18
|
+
assert.equal(formatCost(10.999), "$11.00");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("session-report: format token count", () => {
|
|
22
|
+
const formatTokenCount = (count: number): string => {
|
|
23
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
24
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
|
|
25
|
+
return String(count);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
assert.equal(formatTokenCount(500), "500");
|
|
29
|
+
assert.equal(formatTokenCount(1500), "1.5K");
|
|
30
|
+
assert.equal(formatTokenCount(1_200_000), "1.2M");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("session-report: aggregate by model", () => {
|
|
34
|
+
interface UnitMetric {
|
|
35
|
+
model: string;
|
|
36
|
+
cost: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const units: UnitMetric[] = [
|
|
40
|
+
{ model: "opus", cost: 1.0 },
|
|
41
|
+
{ model: "opus", cost: 0.8 },
|
|
42
|
+
{ model: "sonnet", cost: 0.3 },
|
|
43
|
+
{ model: "sonnet", cost: 0.5 },
|
|
44
|
+
{ model: "sonnet", cost: 0.2 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const byModel = new Map<string, { count: number; cost: number }>();
|
|
48
|
+
for (const u of units) {
|
|
49
|
+
const existing = byModel.get(u.model) ?? { count: 0, cost: 0 };
|
|
50
|
+
existing.count++;
|
|
51
|
+
existing.cost += u.cost;
|
|
52
|
+
byModel.set(u.model, existing);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const opus = byModel.get("opus")!;
|
|
56
|
+
assert.equal(opus.count, 2);
|
|
57
|
+
assert.ok(Math.abs(opus.cost - 1.8) < 0.01);
|
|
58
|
+
|
|
59
|
+
const sonnet = byModel.get("sonnet")!;
|
|
60
|
+
assert.equal(sonnet.count, 3);
|
|
61
|
+
assert.ok(Math.abs(sonnet.cost - 1.0) < 0.01);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("session-report: --json flag detection", () => {
|
|
65
|
+
const args1 = "--json";
|
|
66
|
+
const args2 = "--save --json";
|
|
67
|
+
const args3 = "something else";
|
|
68
|
+
|
|
69
|
+
assert.ok(args1.includes("--json"));
|
|
70
|
+
assert.ok(args2.includes("--json"));
|
|
71
|
+
assert.ok(!args3.includes("--json"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("session-report: --save flag detection", () => {
|
|
75
|
+
const args1 = "--save";
|
|
76
|
+
const args2 = "--save --json";
|
|
77
|
+
const args3 = "";
|
|
78
|
+
|
|
79
|
+
assert.ok(args1.includes("--save"));
|
|
80
|
+
assert.ok(args2.includes("--save"));
|
|
81
|
+
assert.ok(!args3.includes("--save"));
|
|
82
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// Test the PR content generation logic used by /gsd ship.
|
|
5
|
+
// Full integration requires gh CLI + git, so we test the text generation.
|
|
6
|
+
|
|
7
|
+
test("ship: generates TL;DR format", () => {
|
|
8
|
+
// Simulate generatePRContent output structure
|
|
9
|
+
const milestoneId = "M001";
|
|
10
|
+
const milestoneTitle = "User authentication system";
|
|
11
|
+
|
|
12
|
+
const title = `feat: ${milestoneTitle}`;
|
|
13
|
+
assert.equal(title, "feat: User authentication system");
|
|
14
|
+
assert.ok(title.length < 80); // PR title should be short
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("ship: --dry-run flag detection", () => {
|
|
18
|
+
const args1 = "--dry-run";
|
|
19
|
+
const args2 = "--draft --dry-run";
|
|
20
|
+
const args3 = "--draft";
|
|
21
|
+
|
|
22
|
+
assert.ok(args1.includes("--dry-run"));
|
|
23
|
+
assert.ok(args2.includes("--dry-run"));
|
|
24
|
+
assert.ok(!args3.includes("--dry-run"));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("ship: --base flag parsing", () => {
|
|
28
|
+
const args = "--base develop --draft";
|
|
29
|
+
const baseMatch = args.match(/--base\s+(\S+)/);
|
|
30
|
+
assert.ok(baseMatch);
|
|
31
|
+
assert.equal(baseMatch[1], "develop");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("ship: --base flag absent defaults", () => {
|
|
35
|
+
const args = "--draft";
|
|
36
|
+
const baseMatch = args.match(/--base\s+(\S+)/);
|
|
37
|
+
assert.equal(baseMatch, null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("ship: --force flag detection", () => {
|
|
41
|
+
const args1 = "--force";
|
|
42
|
+
const args2 = "";
|
|
43
|
+
|
|
44
|
+
assert.ok(args1.includes("--force"));
|
|
45
|
+
assert.ok(!args2.includes("--force"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("ship: change type checklist format", () => {
|
|
49
|
+
const checklist = [
|
|
50
|
+
"- [x] `feat` — New feature or capability",
|
|
51
|
+
"- [ ] `fix` — Bug fix",
|
|
52
|
+
"- [ ] `refactor` — Code restructuring",
|
|
53
|
+
"- [ ] `test` — Adding or updating tests",
|
|
54
|
+
"- [ ] `docs` — Documentation only",
|
|
55
|
+
"- [ ] `chore` — Build, CI, or tooling changes",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Verify format matches CONTRIBUTING.md expectations
|
|
59
|
+
for (const line of checklist) {
|
|
60
|
+
assert.match(line, /^- \[[ x]\] `\w+` — .+$/);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("ship: PR body contains required sections", () => {
|
|
65
|
+
const requiredSections = ["## TL;DR", "## Change type"];
|
|
66
|
+
const body = "## TL;DR\n\n**What:** Ship M001\n\n## Change type\n\n- [x] `feat`";
|
|
67
|
+
|
|
68
|
+
for (const section of requiredSections) {
|
|
69
|
+
assert.ok(body.includes(section), `Missing section: ${section}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -217,6 +217,20 @@ describe("workflow command handler", () => {
|
|
|
217
217
|
);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it("preserves quoted workflow run overrides (#4130)", async () => {
|
|
221
|
+
const { parseWorkflowRunArgs } = await import("../commands/handlers/workflow.ts");
|
|
222
|
+
assert.deepStrictEqual(
|
|
223
|
+
parseWorkflowRunArgs('demo-workflow target="multi word target" region=\'us east\''),
|
|
224
|
+
{
|
|
225
|
+
defName: "demo-workflow",
|
|
226
|
+
overrides: {
|
|
227
|
+
target: "multi word target",
|
|
228
|
+
region: "us east",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
220
234
|
it("'/gsd workflow run nonexistent' shows error for missing definition", async () => {
|
|
221
235
|
const { handled, notifications } = await callHandler("workflow run nonexistent-def-12345");
|
|
222
236
|
assert.ok(handled, "should be handled");
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* complete-milestone-false-merge.test.ts — Regression test for #4175.
|
|
3
|
+
*
|
|
4
|
+
* Before the fix, a failed complete-milestone unit could leave a stub
|
|
5
|
+
* SUMMARY blocker placeholder on disk. stopAuto's SUMMARY-presence check
|
|
6
|
+
* then treated the milestone as complete and merged the worktree branch
|
|
7
|
+
* into main — emitting a misleading metadata-only merge warning for a
|
|
8
|
+
* milestone that was never legitimately finished.
|
|
9
|
+
*
|
|
10
|
+
* The fix has three cooperating parts:
|
|
11
|
+
* 1. stopAuto uses DB status (authoritative) instead of SUMMARY presence
|
|
12
|
+
* when the project DB is available.
|
|
13
|
+
* 2. postUnitPreVerification pauses auto-mode for complete-milestone
|
|
14
|
+
* after retries are exhausted instead of writing a blocker placeholder.
|
|
15
|
+
* 3. recoverTimedOutUnit pauses for complete-milestone instead of
|
|
16
|
+
* writing a blocker placeholder.
|
|
17
|
+
*
|
|
18
|
+
* This test guards all three via source inspection so a future refactor
|
|
19
|
+
* cannot silently reintroduce the false-merge path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
const gsdDir = join(import.meta.dirname, "..");
|
|
28
|
+
const autoSrc = readFileSync(join(gsdDir, "auto.ts"), "utf-8");
|
|
29
|
+
const postUnitSrc = readFileSync(join(gsdDir, "auto-post-unit.ts"), "utf-8");
|
|
30
|
+
const timeoutSrc = readFileSync(join(gsdDir, "auto-timeout-recovery.ts"), "utf-8");
|
|
31
|
+
|
|
32
|
+
test("#4175: stopAuto uses DB status as the authoritative milestone-complete signal", () => {
|
|
33
|
+
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
|
|
34
|
+
assert.ok(step4Idx !== -1, "Step 4 comment exists in stopAuto");
|
|
35
|
+
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
|
|
36
|
+
const step4Block = autoSrc.slice(step4Idx, step5Idx);
|
|
37
|
+
|
|
38
|
+
assert.ok(
|
|
39
|
+
step4Block.includes("isDbAvailable()"),
|
|
40
|
+
"Step 4 should branch on isDbAvailable() so DB is consulted when present",
|
|
41
|
+
);
|
|
42
|
+
assert.ok(
|
|
43
|
+
step4Block.includes("getMilestone(s.currentMilestoneId)"),
|
|
44
|
+
"Step 4 should read authoritative milestone status via getMilestone()",
|
|
45
|
+
);
|
|
46
|
+
assert.ok(
|
|
47
|
+
/status\s*===\s*"complete"/.test(step4Block),
|
|
48
|
+
'Step 4 should compare the DB row status to "complete"',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("#4175: stopAuto imports getMilestone from gsd-db", () => {
|
|
53
|
+
assert.ok(
|
|
54
|
+
/import\s*\{[^}]*\bgetMilestone\b[^}]*\}\s*from\s*"\.\/gsd-db\.js"/.test(autoSrc),
|
|
55
|
+
"auto.ts should import getMilestone from ./gsd-db.js",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("#4175: stopAuto still falls back to SUMMARY presence when DB is unavailable", () => {
|
|
60
|
+
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
|
|
61
|
+
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
|
|
62
|
+
const step4Block = autoSrc.slice(step4Idx, step5Idx);
|
|
63
|
+
|
|
64
|
+
assert.ok(
|
|
65
|
+
step4Block.includes("resolveMilestoneFile"),
|
|
66
|
+
"Step 4 should keep SUMMARY-file resolution for DB-unavailable projects",
|
|
67
|
+
);
|
|
68
|
+
assert.ok(
|
|
69
|
+
step4Block.includes("preserveBranch"),
|
|
70
|
+
"Step 4 should still preserve branch for incomplete milestones (fallback path)",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("#4175: postUnitPreVerification pauses complete-milestone after retries exhausted", () => {
|
|
75
|
+
// The pause branch must live inside the retries-exhausted block, above the
|
|
76
|
+
// writeBlockerPlaceholder call — otherwise the stub SUMMARY is still written.
|
|
77
|
+
const retriesExhaustedIdx = postUnitSrc.indexOf(
|
|
78
|
+
"if (attempt > MAX_VERIFICATION_RETRIES)",
|
|
79
|
+
);
|
|
80
|
+
assert.ok(
|
|
81
|
+
retriesExhaustedIdx !== -1,
|
|
82
|
+
"retries-exhausted guard exists in postUnitPreVerification",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const blockerCallIdx = postUnitSrc.indexOf("writeBlockerPlaceholder", retriesExhaustedIdx);
|
|
86
|
+
assert.ok(
|
|
87
|
+
blockerCallIdx !== -1,
|
|
88
|
+
"blocker placeholder call still exists for non-milestone units",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const exhaustedBlock = postUnitSrc.slice(retriesExhaustedIdx, blockerCallIdx);
|
|
92
|
+
|
|
93
|
+
assert.ok(
|
|
94
|
+
/s\.currentUnit\.type\s*===\s*"complete-milestone"/.test(exhaustedBlock),
|
|
95
|
+
"retries-exhausted block should specifically handle complete-milestone",
|
|
96
|
+
);
|
|
97
|
+
assert.ok(
|
|
98
|
+
/pauseAuto\s*\(\s*ctx\s*,\s*pi\s*\)/.test(exhaustedBlock),
|
|
99
|
+
"complete-milestone path should call pauseAuto instead of falling through",
|
|
100
|
+
);
|
|
101
|
+
// The pause branch must return so execution never reaches writeBlockerPlaceholder.
|
|
102
|
+
assert.ok(
|
|
103
|
+
/return\s+"dispatched"\s*;/.test(exhaustedBlock),
|
|
104
|
+
"complete-milestone pause branch should return before the placeholder call",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("#4175: recoverTimedOutUnit pauses complete-milestone instead of writing a blocker placeholder", () => {
|
|
109
|
+
// The complete-milestone pause branch must sit immediately above the
|
|
110
|
+
// "retries exhausted" writeBlockerPlaceholder call so a failed
|
|
111
|
+
// complete-milestone never produces a stub SUMMARY. Anchor on the
|
|
112
|
+
// comment that precedes that specific placeholder call rather than the
|
|
113
|
+
// function's earlier writeBlockerPlaceholder use sites or its import.
|
|
114
|
+
// Use lastIndexOf so we find the final retries-exhausted block in
|
|
115
|
+
// recoverTimedOutUnit, not an earlier helper with the same comment.
|
|
116
|
+
const exhaustedAnchor = "Retries exhausted — write a blocker placeholder";
|
|
117
|
+
const exhaustedIdx = timeoutSrc.lastIndexOf(exhaustedAnchor);
|
|
118
|
+
assert.ok(
|
|
119
|
+
exhaustedIdx !== -1,
|
|
120
|
+
"retries-exhausted blocker-placeholder path still exists for non-milestone units",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const guardIdx = timeoutSrc.lastIndexOf(
|
|
124
|
+
'unitType === "complete-milestone"',
|
|
125
|
+
exhaustedIdx,
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
guardIdx !== -1,
|
|
129
|
+
"complete-milestone guard should appear above the retries-exhausted placeholder call",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const guardBlock = timeoutSrc.slice(guardIdx, exhaustedIdx);
|
|
133
|
+
assert.ok(
|
|
134
|
+
/return\s+"paused"\s*;/.test(guardBlock),
|
|
135
|
+
"complete-milestone guard should return 'paused' before the placeholder call",
|
|
136
|
+
);
|
|
137
|
+
// The guard itself must not call writeBlockerPlaceholder.
|
|
138
|
+
assert.ok(
|
|
139
|
+
!guardBlock.includes("writeBlockerPlaceholder"),
|
|
140
|
+
"complete-milestone guard must not write a blocker placeholder",
|
|
141
|
+
);
|
|
142
|
+
});
|