gsd-pi 2.16.0 → 2.18.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 +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +177 -25
- package/dist/resources/extensions/gsd/commands.ts +264 -23
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/dist/resources/extensions/gsd/files.ts +129 -3
- package/dist/resources/extensions/gsd/git-service.ts +19 -8
- package/dist/resources/extensions/gsd/gitignore.ts +41 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +44 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +181 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -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/routing-history.ts +290 -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-recovery.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -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/preferences-git.test.ts +28 -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/routing-history.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/dist/resources/extensions/gsd/types.ts +28 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +24 -2
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +493 -13
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +422 -62
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +9 -22
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
- package/packages/pi-ai/src/models.generated.ts +422 -62
- package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
- package/packages/pi-ai/src/providers/google-shared.ts +10 -19
- 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/core/tools/edit-diff.d.ts +7 -7
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
- 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/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.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/core/tools/edit-diff.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
- 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/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +10 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +177 -25
- package/src/resources/extensions/gsd/commands.ts +264 -23
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/src/resources/extensions/gsd/files.ts +129 -3
- package/src/resources/extensions/gsd/git-service.ts +19 -8
- package/src/resources/extensions/gsd/gitignore.ts +41 -2
- package/src/resources/extensions/gsd/guided-flow.ts +247 -10
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +44 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +181 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -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/routing-history.ts +290 -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-recovery.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -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/preferences-git.test.ts +28 -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/routing-history.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/src/resources/extensions/gsd/types.ts +28 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +24 -2
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end integration tests for the Queue Reorder feature.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the full chain: QUEUE-ORDER.json + findMilestoneIds() + deriveState()
|
|
5
|
+
* + depends_on removal from CONTEXT.md files.
|
|
6
|
+
*
|
|
7
|
+
* These tests simulate what happens when a user reorders milestones and confirms:
|
|
8
|
+
* 1. QUEUE-ORDER.json is written with the new order
|
|
9
|
+
* 2. depends_on is removed from CONTEXT.md frontmatter
|
|
10
|
+
* 3. deriveState() picks the correct milestone as active
|
|
11
|
+
* 4. A fresh deriveState() call (simulating new session) also works
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
|
|
18
|
+
import { deriveState, invalidateStateCache } from '../state.ts';
|
|
19
|
+
import { findMilestoneIds } from '../guided-flow.ts';
|
|
20
|
+
import { saveQueueOrder, loadQueueOrder } from '../queue-order.ts';
|
|
21
|
+
import { parseContextDependsOn } from '../files.ts';
|
|
22
|
+
import { createTestContext } from './test-helpers.ts';
|
|
23
|
+
|
|
24
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
25
|
+
|
|
26
|
+
// ─── Fixture Helpers ───────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function createFixtureBase(): string {
|
|
29
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-reorder-e2e-'));
|
|
30
|
+
mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cleanup(base: string): void {
|
|
35
|
+
rmSync(base, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeMilestoneDir(base: string, mid: string): void {
|
|
39
|
+
mkdirSync(join(base, '.gsd', 'milestones', mid), { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeContext(base: string, mid: string, frontmatter: string, body: string = ''): void {
|
|
43
|
+
const dir = join(base, '.gsd', 'milestones', mid);
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
const fm = frontmatter ? `---\n${frontmatter}\n---\n\n` : '';
|
|
46
|
+
writeFileSync(join(dir, `${mid}-CONTEXT.md`), `${fm}# ${mid}: Test\n\n${body}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeCompleteMilestone(base: string, mid: string): void {
|
|
50
|
+
const dir = join(base, '.gsd', 'milestones', mid);
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
52
|
+
writeFileSync(join(dir, `${mid}-ROADMAP.md`), `# ${mid}: Complete
|
|
53
|
+
|
|
54
|
+
**Vision:** Done.
|
|
55
|
+
|
|
56
|
+
## Slices
|
|
57
|
+
|
|
58
|
+
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
59
|
+
> After this: Done.
|
|
60
|
+
`);
|
|
61
|
+
writeFileSync(join(dir, `${mid}-SUMMARY.md`), `# ${mid} Summary\n\nComplete.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readContextFile(base: string, mid: string): string {
|
|
65
|
+
return readFileSync(join(base, '.gsd', 'milestones', mid, `${mid}-CONTEXT.md`), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
// Test: Queue order changes milestone activation
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
console.log('\n=== E2E: queue-order changes active milestone ===');
|
|
73
|
+
{
|
|
74
|
+
const base = createFixtureBase();
|
|
75
|
+
try {
|
|
76
|
+
// Setup: M007 complete, M008 and M009 pending (no context, no roadmap)
|
|
77
|
+
writeCompleteMilestone(base, 'M007');
|
|
78
|
+
writeMilestoneDir(base, 'M008');
|
|
79
|
+
writeContext(base, 'M008', '', 'Multi-Session Parallel Orchestration');
|
|
80
|
+
writeMilestoneDir(base, 'M009');
|
|
81
|
+
writeContext(base, 'M009', '', 'Context-Budget Visibility');
|
|
82
|
+
|
|
83
|
+
// Without custom order: M008 comes first (numeric sort)
|
|
84
|
+
invalidateStateCache();
|
|
85
|
+
const stateBefore = await deriveState(base);
|
|
86
|
+
assertEq(stateBefore.activeMilestone?.id, 'M008', 'before reorder: M008 is active');
|
|
87
|
+
|
|
88
|
+
// Save custom order: M009 before M008
|
|
89
|
+
saveQueueOrder(base, ['M009', 'M008']);
|
|
90
|
+
|
|
91
|
+
// With custom order: M009 should be active
|
|
92
|
+
invalidateStateCache();
|
|
93
|
+
const stateAfter = await deriveState(base);
|
|
94
|
+
assertEq(stateAfter.activeMilestone?.id, 'M009', 'after reorder: M009 is active');
|
|
95
|
+
|
|
96
|
+
// findMilestoneIds respects the order
|
|
97
|
+
const ids = findMilestoneIds(base);
|
|
98
|
+
const m008Idx = ids.indexOf('M008');
|
|
99
|
+
const m009Idx = ids.indexOf('M009');
|
|
100
|
+
assertTrue(m009Idx < m008Idx, 'findMilestoneIds: M009 comes before M008');
|
|
101
|
+
|
|
102
|
+
} finally {
|
|
103
|
+
cleanup(base);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
+
// Test: Reorder + depends_on removal = correct state
|
|
109
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
console.log('\n=== E2E: reorder with depends_on removal ===');
|
|
112
|
+
{
|
|
113
|
+
const base = createFixtureBase();
|
|
114
|
+
try {
|
|
115
|
+
// Setup: M007 complete, M008 depends_on M009, M009 no deps
|
|
116
|
+
writeCompleteMilestone(base, 'M007');
|
|
117
|
+
writeContext(base, 'M008', 'depends_on: [M009]', 'Multi-Session Parallel');
|
|
118
|
+
writeContext(base, 'M009', '', 'Context-Budget Visibility');
|
|
119
|
+
|
|
120
|
+
// Before: M008 depends on M009, so deriveState skips M008, M009 is active
|
|
121
|
+
invalidateStateCache();
|
|
122
|
+
const stateBefore = await deriveState(base);
|
|
123
|
+
assertEq(stateBefore.activeMilestone?.id, 'M009', 'before: M009 active (M008 dep-blocked)');
|
|
124
|
+
|
|
125
|
+
// Simulate reorder confirm: save order M009→M008, remove depends_on from M008
|
|
126
|
+
saveQueueOrder(base, ['M009', 'M008']);
|
|
127
|
+
|
|
128
|
+
// Remove depends_on from M008-CONTEXT.md (simulating what handleQueueReorder does)
|
|
129
|
+
const contextContent = readContextFile(base, 'M008');
|
|
130
|
+
const newContent = contextContent.replace(/---\ndepends_on: \[M009\]\n---\n\n/, '');
|
|
131
|
+
writeFileSync(join(base, '.gsd', 'milestones', 'M008', 'M008-CONTEXT.md'), newContent);
|
|
132
|
+
|
|
133
|
+
// Verify: depends_on is gone
|
|
134
|
+
const updatedContent = readContextFile(base, 'M008');
|
|
135
|
+
const deps = parseContextDependsOn(updatedContent);
|
|
136
|
+
assertEq(deps.length, 0, 'depends_on removed from M008-CONTEXT.md');
|
|
137
|
+
|
|
138
|
+
// Verify: deriveState still picks M009 (it's first in queue order)
|
|
139
|
+
invalidateStateCache();
|
|
140
|
+
const stateAfter = await deriveState(base);
|
|
141
|
+
assertEq(stateAfter.activeMilestone?.id, 'M009', 'after: M009 still active (first in queue)');
|
|
142
|
+
|
|
143
|
+
// Verify: M008 is now pending (not dep-blocked)
|
|
144
|
+
const m008Entry = stateAfter.registry.find(m => m.id === 'M008');
|
|
145
|
+
assertEq(m008Entry?.status, 'pending', 'M008 is pending (not dep-blocked)');
|
|
146
|
+
assertTrue(!m008Entry?.dependsOn || m008Entry.dependsOn.length === 0, 'M008 has no dependsOn');
|
|
147
|
+
|
|
148
|
+
} finally {
|
|
149
|
+
cleanup(base);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// Test: Fresh deriveState (simulating new session) respects queue order
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
console.log('\n=== E2E: fresh session respects queue order ===');
|
|
158
|
+
{
|
|
159
|
+
const base = createFixtureBase();
|
|
160
|
+
try {
|
|
161
|
+
writeCompleteMilestone(base, 'M007');
|
|
162
|
+
writeContext(base, 'M008', '', 'Parallel Orchestration');
|
|
163
|
+
writeContext(base, 'M009', '', 'Budget Visibility');
|
|
164
|
+
|
|
165
|
+
// Save queue order
|
|
166
|
+
saveQueueOrder(base, ['M009', 'M008']);
|
|
167
|
+
|
|
168
|
+
// Simulate fresh session — invalidate all caches
|
|
169
|
+
invalidateStateCache();
|
|
170
|
+
|
|
171
|
+
// Derive state — should read QUEUE-ORDER.json from disk
|
|
172
|
+
const state = await deriveState(base);
|
|
173
|
+
assertEq(state.activeMilestone?.id, 'M009', 'fresh session: M009 is active');
|
|
174
|
+
|
|
175
|
+
// Verify queue order persisted
|
|
176
|
+
const order = loadQueueOrder(base);
|
|
177
|
+
assertEq(order, ['M009', 'M008'], 'QUEUE-ORDER.json persisted correctly');
|
|
178
|
+
|
|
179
|
+
} finally {
|
|
180
|
+
cleanup(base);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
185
|
+
// Test: Queue order with newly added milestones
|
|
186
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
187
|
+
|
|
188
|
+
console.log('\n=== E2E: new milestones appended to queue ===');
|
|
189
|
+
{
|
|
190
|
+
const base = createFixtureBase();
|
|
191
|
+
try {
|
|
192
|
+
writeCompleteMilestone(base, 'M007');
|
|
193
|
+
writeContext(base, 'M008', '', 'Parallel');
|
|
194
|
+
writeContext(base, 'M009', '', 'Visibility');
|
|
195
|
+
|
|
196
|
+
// Custom order only has M009, M008
|
|
197
|
+
saveQueueOrder(base, ['M009', 'M008']);
|
|
198
|
+
|
|
199
|
+
// Add M010 (not in queue order)
|
|
200
|
+
writeContext(base, 'M010', '', 'New feature');
|
|
201
|
+
|
|
202
|
+
invalidateStateCache();
|
|
203
|
+
const ids = findMilestoneIds(base);
|
|
204
|
+
|
|
205
|
+
// M009 first, M008 second, M010 appended at end
|
|
206
|
+
const m009Idx = ids.indexOf('M009');
|
|
207
|
+
const m008Idx = ids.indexOf('M008');
|
|
208
|
+
const m010Idx = ids.indexOf('M010');
|
|
209
|
+
assertTrue(m009Idx < m008Idx, 'M009 before M008');
|
|
210
|
+
assertTrue(m008Idx < m010Idx, 'M008 before M010 (new milestone appended)');
|
|
211
|
+
|
|
212
|
+
// M009 is still active (first non-complete in queue order)
|
|
213
|
+
const state = await deriveState(base);
|
|
214
|
+
assertEq(state.activeMilestone?.id, 'M009', 'M009 still active after M010 added');
|
|
215
|
+
|
|
216
|
+
} finally {
|
|
217
|
+
cleanup(base);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
222
|
+
// Test: No queue order file = default numeric sort (backward compat)
|
|
223
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
+
|
|
225
|
+
console.log('\n=== E2E: backward compat without QUEUE-ORDER.json ===');
|
|
226
|
+
{
|
|
227
|
+
const base = createFixtureBase();
|
|
228
|
+
try {
|
|
229
|
+
writeCompleteMilestone(base, 'M007');
|
|
230
|
+
writeContext(base, 'M008', '', 'Parallel');
|
|
231
|
+
writeContext(base, 'M009', '', 'Visibility');
|
|
232
|
+
|
|
233
|
+
// No QUEUE-ORDER.json — default numeric sort
|
|
234
|
+
invalidateStateCache();
|
|
235
|
+
const state = await deriveState(base);
|
|
236
|
+
assertEq(state.activeMilestone?.id, 'M008', 'no queue order: M008 active (numeric)');
|
|
237
|
+
|
|
238
|
+
const ids = findMilestoneIds(base);
|
|
239
|
+
assertTrue(ids.indexOf('M008') < ids.indexOf('M009'), 'default sort: M008 before M009');
|
|
240
|
+
|
|
241
|
+
} finally {
|
|
242
|
+
cleanup(base);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
247
|
+
// Test: depends_on inline array format removal
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
|
+
|
|
250
|
+
console.log('\n=== E2E: depends_on inline format preserved after partial removal ===');
|
|
251
|
+
{
|
|
252
|
+
const base = createFixtureBase();
|
|
253
|
+
try {
|
|
254
|
+
writeCompleteMilestone(base, 'M007');
|
|
255
|
+
// M008 depends on both M009 and M010
|
|
256
|
+
writeContext(base, 'M008', 'depends_on: [M009, M010]', 'Parallel');
|
|
257
|
+
writeContext(base, 'M009', '', 'Visibility');
|
|
258
|
+
writeContext(base, 'M010', '', 'Other');
|
|
259
|
+
|
|
260
|
+
// Verify both deps are parsed
|
|
261
|
+
const contentBefore = readContextFile(base, 'M008');
|
|
262
|
+
const depsBefore = parseContextDependsOn(contentBefore);
|
|
263
|
+
assertEq(depsBefore.length, 2, 'M008 has 2 deps before');
|
|
264
|
+
|
|
265
|
+
// Simulate removing only M009 dep (keep M010)
|
|
266
|
+
const content = readContextFile(base, 'M008');
|
|
267
|
+
const updated = content.replace('depends_on: [M009, M010]', 'depends_on: [M010]');
|
|
268
|
+
writeFileSync(join(base, '.gsd', 'milestones', 'M008', 'M008-CONTEXT.md'), updated);
|
|
269
|
+
|
|
270
|
+
// Verify only M010 remains
|
|
271
|
+
const contentAfter = readContextFile(base, 'M008');
|
|
272
|
+
const depsAfter = parseContextDependsOn(contentAfter);
|
|
273
|
+
assertEq(depsAfter.length, 1, 'M008 has 1 dep after removal');
|
|
274
|
+
assertEq(depsAfter[0], 'M010', 'remaining dep is M010');
|
|
275
|
+
|
|
276
|
+
} finally {
|
|
277
|
+
cleanup(base);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
report();
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const historySrc = readFileSync(join(__dirname, "..", "routing-history.ts"), "utf-8");
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// Module Exports
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
test("routing-history: exports initRoutingHistory", () => {
|
|
22
|
+
assert.ok(historySrc.includes("export function initRoutingHistory"), "should export initRoutingHistory");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("routing-history: exports recordOutcome", () => {
|
|
26
|
+
assert.ok(historySrc.includes("export function recordOutcome"), "should export recordOutcome");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("routing-history: exports recordFeedback", () => {
|
|
30
|
+
assert.ok(historySrc.includes("export function recordFeedback"), "should export recordFeedback");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("routing-history: exports getAdaptiveTierAdjustment", () => {
|
|
34
|
+
assert.ok(historySrc.includes("export function getAdaptiveTierAdjustment"), "should export getAdaptiveTierAdjustment");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("routing-history: exports resetRoutingHistory", () => {
|
|
38
|
+
assert.ok(historySrc.includes("export function resetRoutingHistory"), "should export resetRoutingHistory");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// Design Constants
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
test("routing-history: uses rolling window of 50 entries", () => {
|
|
46
|
+
assert.ok(historySrc.includes("ROLLING_WINDOW = 50"), "should use 50-entry rolling window");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("routing-history: failure threshold is 20%", () => {
|
|
50
|
+
assert.ok(historySrc.includes("FAILURE_THRESHOLD = 0.20"), "should use 20% failure threshold");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("routing-history: feedback weight is 2x", () => {
|
|
54
|
+
assert.ok(historySrc.includes("FEEDBACK_WEIGHT = 2"), "feedback should count 2x");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// Type Structure
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
test("routing-history: imports ComplexityTier from types.ts", () => {
|
|
62
|
+
assert.ok(
|
|
63
|
+
historySrc.includes('from "./types.js"') && historySrc.includes("ComplexityTier"),
|
|
64
|
+
"should import ComplexityTier from types.ts",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("routing-history: defines RoutingHistoryData interface", () => {
|
|
69
|
+
assert.ok(historySrc.includes("interface RoutingHistoryData"), "should define RoutingHistoryData");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("routing-history: defines FeedbackEntry interface", () => {
|
|
73
|
+
assert.ok(historySrc.includes("interface FeedbackEntry"), "should define FeedbackEntry");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
// Persistence
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
|
|
80
|
+
test("routing-history: persists to routing-history.json", () => {
|
|
81
|
+
assert.ok(historySrc.includes("routing-history.json"), "should persist to routing-history.json");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("routing-history: has save and load functions", () => {
|
|
85
|
+
assert.ok(historySrc.includes("saveHistory") || historySrc.includes("function save"), "should have save");
|
|
86
|
+
assert.ok(historySrc.includes("loadHistory") || historySrc.includes("function load"), "should have load");
|
|
87
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { fork } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import {
|
|
11
|
+
writeLock,
|
|
12
|
+
readCrashLock,
|
|
13
|
+
clearLock,
|
|
14
|
+
isLockProcessAlive,
|
|
15
|
+
} from "../crash-recovery.ts";
|
|
16
|
+
import { stopAutoRemote } from "../auto.ts";
|
|
17
|
+
|
|
18
|
+
function makeTmpBase(): string {
|
|
19
|
+
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
20
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
21
|
+
return base;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cleanup(base: string): void {
|
|
25
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── stopAutoRemote ──────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
test("stopAutoRemote returns found:false when no lock file exists", () => {
|
|
31
|
+
const base = makeTmpBase();
|
|
32
|
+
try {
|
|
33
|
+
const result = stopAutoRemote(base);
|
|
34
|
+
assert.equal(result.found, false);
|
|
35
|
+
assert.equal(result.pid, undefined);
|
|
36
|
+
assert.equal(result.error, undefined);
|
|
37
|
+
} finally {
|
|
38
|
+
cleanup(base);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("stopAutoRemote cleans up stale lock (dead PID) and returns found:false", () => {
|
|
43
|
+
const base = makeTmpBase();
|
|
44
|
+
try {
|
|
45
|
+
// Write a lock with a PID that doesn't exist
|
|
46
|
+
writeLock(base, "execute-task", "M001/S01/T01", 3);
|
|
47
|
+
// Overwrite PID to a dead one
|
|
48
|
+
const lock = readCrashLock(base)!;
|
|
49
|
+
const staleData = { ...lock, pid: 999999999 };
|
|
50
|
+
writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(staleData, null, 2), "utf-8");
|
|
51
|
+
|
|
52
|
+
const result = stopAutoRemote(base);
|
|
53
|
+
assert.equal(result.found, false, "stale lock should not be found as running");
|
|
54
|
+
|
|
55
|
+
// Lock should be cleaned up
|
|
56
|
+
assert.equal(readCrashLock(base), null, "stale lock should be removed");
|
|
57
|
+
} finally {
|
|
58
|
+
cleanup(base);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("stopAutoRemote sends SIGTERM to a live process and returns found:true", async () => {
|
|
63
|
+
const base = makeTmpBase();
|
|
64
|
+
|
|
65
|
+
// Spawn a child process that sleeps, acting as a fake auto-mode session
|
|
66
|
+
const child = fork(
|
|
67
|
+
"-e",
|
|
68
|
+
["process.on('SIGTERM', () => process.exit(0)); setTimeout(() => process.exit(1), 30000);"],
|
|
69
|
+
{ stdio: "ignore", detached: false },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Wait for child to be ready
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
75
|
+
|
|
76
|
+
// Write lock with child's PID
|
|
77
|
+
const lockData = {
|
|
78
|
+
pid: child.pid,
|
|
79
|
+
startedAt: new Date().toISOString(),
|
|
80
|
+
unitType: "execute-task",
|
|
81
|
+
unitId: "M001/S01/T01",
|
|
82
|
+
unitStartedAt: new Date().toISOString(),
|
|
83
|
+
completedUnits: 0,
|
|
84
|
+
};
|
|
85
|
+
writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2), "utf-8");
|
|
86
|
+
|
|
87
|
+
const result = stopAutoRemote(base);
|
|
88
|
+
assert.equal(result.found, true, "should find running auto-mode");
|
|
89
|
+
assert.equal(result.pid, child.pid, "should return the PID");
|
|
90
|
+
|
|
91
|
+
// Wait for child to exit (it should receive SIGTERM)
|
|
92
|
+
const exitCode = await new Promise<number | null>((resolve) => {
|
|
93
|
+
child.on("exit", (code) => resolve(code));
|
|
94
|
+
setTimeout(() => resolve(null), 5000);
|
|
95
|
+
});
|
|
96
|
+
// On Windows, SIGTERM is not interceptable — the process exits with code 1
|
|
97
|
+
// rather than running the handler. Accept either clean exit (0) or forced (1).
|
|
98
|
+
assert.ok(exitCode !== null, "child should have exited after SIGTERM");
|
|
99
|
+
if (process.platform !== "win32") {
|
|
100
|
+
assert.equal(exitCode, 0, "child should have exited cleanly via SIGTERM");
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
try { child.kill("SIGKILL"); } catch { /* already dead */ }
|
|
104
|
+
cleanup(base);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── Lock path: original project root vs worktree ────────────────────────
|
|
109
|
+
|
|
110
|
+
test("lock file should be discoverable at project root, not worktree path", () => {
|
|
111
|
+
const projectRoot = makeTmpBase();
|
|
112
|
+
const worktreePath = join(projectRoot, ".gsd", "worktrees", "M001");
|
|
113
|
+
mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Simulate: auto-mode writes lock to project root (the fix)
|
|
117
|
+
writeLock(projectRoot, "execute-task", "M001/S01/T01", 0);
|
|
118
|
+
|
|
119
|
+
// Second terminal checks project root — should find the lock
|
|
120
|
+
const lock = readCrashLock(projectRoot);
|
|
121
|
+
assert.ok(lock, "lock should be found at project root");
|
|
122
|
+
assert.equal(lock!.unitType, "execute-task");
|
|
123
|
+
|
|
124
|
+
// Worktree path should NOT have a lock
|
|
125
|
+
const worktreeLock = readCrashLock(worktreePath);
|
|
126
|
+
assert.equal(worktreeLock, null, "lock should NOT exist at worktree path");
|
|
127
|
+
} finally {
|
|
128
|
+
cleanup(projectRoot);
|
|
129
|
+
}
|
|
130
|
+
});
|