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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Compression — unit tests for M004/S02.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that prompt builders respect inlineLevel parameter by
|
|
5
|
+
* inspecting the auto-prompts.ts source for level-aware gating.
|
|
6
|
+
* Cannot call builders directly due to @gsd/pi-coding-agent import
|
|
7
|
+
* resolution — uses source-level structural verification instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const promptsSrc = readFileSync(join(__dirname, "..", "auto-prompts.ts"), "utf-8");
|
|
18
|
+
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// inlineLevel Parameter Presence
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
const BUILDERS_WITH_LEVEL = [
|
|
24
|
+
"buildPlanMilestonePrompt",
|
|
25
|
+
"buildPlanSlicePrompt",
|
|
26
|
+
"buildExecuteTaskPrompt",
|
|
27
|
+
"buildCompleteSlicePrompt",
|
|
28
|
+
"buildCompleteMilestonePrompt",
|
|
29
|
+
"buildReassessRoadmapPrompt",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const builder of BUILDERS_WITH_LEVEL) {
|
|
33
|
+
test(`compression: ${builder} accepts inlineLevel parameter`, () => {
|
|
34
|
+
// Find the function signature
|
|
35
|
+
const sigRegex = new RegExp(`export async function ${builder}\\([^)]*level\\?: InlineLevel`);
|
|
36
|
+
assert.ok(
|
|
37
|
+
sigRegex.test(promptsSrc),
|
|
38
|
+
`${builder} should have level?: InlineLevel parameter`,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
// Default Level Resolution
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
test("compression: builders default to resolveInlineLevel() when no level passed", () => {
|
|
48
|
+
const defaultPattern = /const inlineLevel = level \?\? resolveInlineLevel\(\)/g;
|
|
49
|
+
const matches = promptsSrc.match(defaultPattern);
|
|
50
|
+
assert.ok(matches, "should have resolveInlineLevel() fallback");
|
|
51
|
+
assert.ok(
|
|
52
|
+
matches.length >= BUILDERS_WITH_LEVEL.length,
|
|
53
|
+
`should have ${BUILDERS_WITH_LEVEL.length} fallback instances, found ${matches?.length}`,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// Minimal Level — Template Reduction
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
test("compression: buildExecuteTaskPrompt minimal drops decisions template", () => {
|
|
62
|
+
// In the execute-task builder, minimal should only inline task-summary, not decisions
|
|
63
|
+
assert.ok(
|
|
64
|
+
promptsSrc.includes('inlineLevel === "minimal"') &&
|
|
65
|
+
promptsSrc.includes('inlineTemplate("task-summary"'),
|
|
66
|
+
"execute-task should conditionally include decisions template based on level",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("compression: buildExecuteTaskPrompt minimal truncates prior summaries", () => {
|
|
71
|
+
assert.ok(
|
|
72
|
+
promptsSrc.includes('inlineLevel === "minimal" && priorSummaries.length > 1'),
|
|
73
|
+
"execute-task should limit prior summaries for minimal level",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("compression: buildPlanMilestonePrompt minimal drops project/requirements/decisions files", () => {
|
|
78
|
+
// The plan-milestone builder should gate root file inlining on inlineLevel
|
|
79
|
+
assert.ok(
|
|
80
|
+
promptsSrc.includes('inlineLevel !== "minimal"') &&
|
|
81
|
+
promptsSrc.includes('inlineGsdRootFile(base, "project.md"'),
|
|
82
|
+
"plan-milestone should conditionally include project.md based on level",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("compression: buildPlanMilestonePrompt minimal drops extra templates", () => {
|
|
87
|
+
// Full inlines 5 templates, minimal should inline fewer
|
|
88
|
+
assert.ok(
|
|
89
|
+
promptsSrc.includes('if (inlineLevel === "full")') &&
|
|
90
|
+
promptsSrc.includes('inlineTemplate("secrets-manifest"'),
|
|
91
|
+
"plan-milestone should only include secrets-manifest template at full level",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
96
|
+
// Complete-Slice Level Gating
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
|
|
99
|
+
test("compression: buildCompleteSlicePrompt minimal drops requirements", () => {
|
|
100
|
+
// Find the complete-slice section and verify requirements gating
|
|
101
|
+
const completeSliceIdx = promptsSrc.indexOf("buildCompleteSlicePrompt");
|
|
102
|
+
const nextBuilder = promptsSrc.indexOf("buildCompleteMilestonePrompt");
|
|
103
|
+
const completeSliceBlock = promptsSrc.slice(completeSliceIdx, nextBuilder);
|
|
104
|
+
assert.ok(
|
|
105
|
+
completeSliceBlock.includes('inlineLevel !== "minimal"'),
|
|
106
|
+
"complete-slice should gate requirements inlining on level",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("compression: buildCompleteSlicePrompt minimal drops UAT template", () => {
|
|
111
|
+
const completeSliceIdx = promptsSrc.indexOf("buildCompleteSlicePrompt");
|
|
112
|
+
const nextBuilder = promptsSrc.indexOf("buildCompleteMilestonePrompt");
|
|
113
|
+
const completeSliceBlock = promptsSrc.slice(completeSliceIdx, nextBuilder);
|
|
114
|
+
assert.ok(
|
|
115
|
+
completeSliceBlock.includes('inlineLevel !== "minimal"') &&
|
|
116
|
+
completeSliceBlock.includes('inlineTemplate("uat"'),
|
|
117
|
+
"complete-slice should conditionally include UAT template based on level",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
|
+
// Complete-Milestone Level Gating
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
124
|
+
|
|
125
|
+
test("compression: buildCompleteMilestonePrompt minimal drops root GSD files", () => {
|
|
126
|
+
const completeMilestoneIdx = promptsSrc.indexOf("buildCompleteMilestonePrompt");
|
|
127
|
+
const nextBuilder = promptsSrc.indexOf("buildReplanSlicePrompt");
|
|
128
|
+
const block = promptsSrc.slice(completeMilestoneIdx, nextBuilder);
|
|
129
|
+
assert.ok(
|
|
130
|
+
block.includes('inlineLevel !== "minimal"') &&
|
|
131
|
+
block.includes('inlineGsdRootFile(base, "requirements.md"'),
|
|
132
|
+
"complete-milestone should gate root file inlining on level",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
// Reassess-Roadmap Level Gating
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
test("compression: buildReassessRoadmapPrompt minimal drops project/requirements/decisions", () => {
|
|
141
|
+
const reassessIdx = promptsSrc.indexOf("buildReassessRoadmapPrompt");
|
|
142
|
+
const block = promptsSrc.slice(reassessIdx, reassessIdx + 1500);
|
|
143
|
+
assert.ok(
|
|
144
|
+
block.includes('inlineLevel !== "minimal"'),
|
|
145
|
+
"reassess-roadmap should gate file inlining on level",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
150
|
+
// Full Level — No Regression
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
152
|
+
|
|
153
|
+
test("compression: full level preserves all templates and files (no regression)", () => {
|
|
154
|
+
// Verify the key template names are still present in the source
|
|
155
|
+
const expectedTemplates = [
|
|
156
|
+
"roadmap", "decisions", "plan", "task-plan", "secrets-manifest",
|
|
157
|
+
"task-summary", "slice-summary", "uat", "milestone-summary",
|
|
158
|
+
];
|
|
159
|
+
for (const tpl of expectedTemplates) {
|
|
160
|
+
assert.ok(
|
|
161
|
+
promptsSrc.includes(`inlineTemplate("${tpl}"`),
|
|
162
|
+
`template "${tpl}" should still be present in auto-prompts.ts`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
168
|
+
// Import Verification
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
170
|
+
|
|
171
|
+
test("compression: auto-prompts.ts imports resolveInlineLevel and InlineLevel", () => {
|
|
172
|
+
assert.ok(
|
|
173
|
+
promptsSrc.includes("resolveInlineLevel"),
|
|
174
|
+
"should import resolveInlineLevel from preferences",
|
|
175
|
+
);
|
|
176
|
+
assert.ok(
|
|
177
|
+
promptsSrc.includes("InlineLevel"),
|
|
178
|
+
"should import InlineLevel type from types",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
@@ -303,6 +303,105 @@ async function main(): Promise<void> {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// ─── Test Group 7: unique-id-deps ──────────────────────────────────────
|
|
307
|
+
// M004-0zjrg0 is complete, M005-b0m2hl depends_on M004-0zjrg0 → M005 should activate.
|
|
308
|
+
// Regression: parseContextDependsOn() used .toUpperCase(), converting "M004-0zjrg0"
|
|
309
|
+
// to "M004-0ZJRG0", breaking the case-sensitive lookup in completeMilestoneIds.
|
|
310
|
+
console.log('\n=== unique-id-deps: unique milestone IDs with lowercase hex suffix ===');
|
|
311
|
+
{
|
|
312
|
+
const base = createFixtureBase();
|
|
313
|
+
try {
|
|
314
|
+
// M004-0zjrg0: complete (all slices done + SUMMARY present)
|
|
315
|
+
writeRoadmap(base, 'M004-0zjrg0', `# M004-0zjrg0: First Unique Milestone
|
|
316
|
+
|
|
317
|
+
**Vision:** Complete milestone with unique ID.
|
|
318
|
+
|
|
319
|
+
## Slices
|
|
320
|
+
|
|
321
|
+
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
322
|
+
> After this: Done.
|
|
323
|
+
`);
|
|
324
|
+
writeMilestoneSummary(base, 'M004-0zjrg0', '# M004-0zjrg0 Summary\n\nComplete.');
|
|
325
|
+
|
|
326
|
+
// M005-b0m2hl: depends on M004-0zjrg0 (lowercase hex suffix)
|
|
327
|
+
writeContext(base, 'M005-b0m2hl', 'depends_on: [M004-0zjrg0]');
|
|
328
|
+
|
|
329
|
+
const state = await deriveState(base);
|
|
330
|
+
|
|
331
|
+
assertEq(state.registry.find(e => e.id === 'M004-0zjrg0')?.status, 'complete',
|
|
332
|
+
'unique-id-deps: M004-0zjrg0 is complete');
|
|
333
|
+
assertEq(state.registry.find(e => e.id === 'M005-b0m2hl')?.status, 'active',
|
|
334
|
+
'unique-id-deps: M005-b0m2hl is active (dep on M004-0zjrg0 met)');
|
|
335
|
+
assertEq(state.activeMilestone?.id, 'M005-b0m2hl',
|
|
336
|
+
'unique-id-deps: activeMilestone is M005-b0m2hl');
|
|
337
|
+
assertTrue(state.phase !== 'blocked',
|
|
338
|
+
'unique-id-deps: phase is not blocked');
|
|
339
|
+
} finally {
|
|
340
|
+
cleanup(base);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Test Group 8: unique-id-deps-blocked ─────────────────────────────
|
|
345
|
+
// M004-0zjrg0 is NOT complete, M005-b0m2hl depends_on M004-0zjrg0 → M005 should be pending
|
|
346
|
+
console.log('\n=== unique-id-deps-blocked: unique ID dep not yet met ===');
|
|
347
|
+
{
|
|
348
|
+
const base = createFixtureBase();
|
|
349
|
+
try {
|
|
350
|
+
// M004-0zjrg0: incomplete (slice not done)
|
|
351
|
+
writeRoadmap(base, 'M004-0zjrg0', `# M004-0zjrg0: Incomplete Unique Milestone
|
|
352
|
+
|
|
353
|
+
**Vision:** Still in progress.
|
|
354
|
+
|
|
355
|
+
## Slices
|
|
356
|
+
|
|
357
|
+
- [ ] **S01: In Progress** \`risk:low\` \`depends:[]\`
|
|
358
|
+
> After this: Done.
|
|
359
|
+
`);
|
|
360
|
+
writeSlicePlan(base, 'M004-0zjrg0', 'S01', `# S01: In Progress
|
|
361
|
+
|
|
362
|
+
**Goal:** Test dep blocking with unique IDs.
|
|
363
|
+
|
|
364
|
+
## Tasks
|
|
365
|
+
|
|
366
|
+
- [ ] **T01: Work** \`est:15m\`
|
|
367
|
+
Still doing work.
|
|
368
|
+
`);
|
|
369
|
+
|
|
370
|
+
// M005-b0m2hl: depends on M004-0zjrg0 (still incomplete)
|
|
371
|
+
writeContext(base, 'M005-b0m2hl', 'depends_on: [M004-0zjrg0]');
|
|
372
|
+
|
|
373
|
+
const state = await deriveState(base);
|
|
374
|
+
|
|
375
|
+
assertEq(state.activeMilestone?.id, 'M004-0zjrg0',
|
|
376
|
+
'unique-id-deps-blocked: activeMilestone is M004-0zjrg0');
|
|
377
|
+
assertEq(state.registry.find(e => e.id === 'M005-b0m2hl')?.status, 'pending',
|
|
378
|
+
'unique-id-deps-blocked: M005-b0m2hl is pending (dep not met)');
|
|
379
|
+
} finally {
|
|
380
|
+
cleanup(base);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Test Group 9: parseContextDependsOn preserves case ───────────────
|
|
385
|
+
// Direct unit test: verify the parsed dep ID matches the input exactly
|
|
386
|
+
console.log('\n=== parseContextDependsOn: preserves case of unique IDs ===');
|
|
387
|
+
{
|
|
388
|
+
const { parseContextDependsOn } = await import('../files.ts');
|
|
389
|
+
|
|
390
|
+
const deps1 = parseContextDependsOn('---\ndepends_on: [M004-0zjrg0]\n---\n');
|
|
391
|
+
assertEq(deps1[0], 'M004-0zjrg0',
|
|
392
|
+
'parseContextDependsOn preserves lowercase hex suffix');
|
|
393
|
+
|
|
394
|
+
const deps2 = parseContextDependsOn('---\ndepends_on: [M001, M004-abc123]\n---\n');
|
|
395
|
+
assertEq(deps2[0], 'M001', 'preserves classic uppercase ID');
|
|
396
|
+
assertEq(deps2[1], 'M004-abc123', 'preserves mixed-case unique ID');
|
|
397
|
+
|
|
398
|
+
const deps3 = parseContextDependsOn('---\ndepends_on: []\n---\n');
|
|
399
|
+
assertEq(deps3.length, 0, 'empty deps returns empty array');
|
|
400
|
+
|
|
401
|
+
const deps4 = parseContextDependsOn(null);
|
|
402
|
+
assertEq(deps4.length, 0, 'null content returns empty array');
|
|
403
|
+
}
|
|
404
|
+
|
|
306
405
|
report();
|
|
307
406
|
}
|
|
308
407
|
|
|
@@ -1020,6 +1020,138 @@ async function main(): Promise<void> {
|
|
|
1020
1020
|
rmSync(repo, { recursive: true, force: true });
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
|
+
// ─── commit_docs: false — smartStage excludes .gsd/ ──────────────────
|
|
1024
|
+
|
|
1025
|
+
console.log("\n=== commit_docs: false — smartStage excludes .gsd/ ===");
|
|
1026
|
+
|
|
1027
|
+
{
|
|
1028
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-commit-docs-"));
|
|
1029
|
+
run("git init -b main", repo);
|
|
1030
|
+
run("git config user.email test@test.com", repo);
|
|
1031
|
+
run("git config user.name Test", repo);
|
|
1032
|
+
writeFileSync(join(repo, "README.md"), "init");
|
|
1033
|
+
run("git add -A && git commit -m init", repo);
|
|
1034
|
+
|
|
1035
|
+
// Create .gsd/ planning files + a normal source file
|
|
1036
|
+
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
|
1037
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M001", "ROADMAP.md"), "# Roadmap");
|
|
1038
|
+
writeFileSync(join(repo, ".gsd", "preferences.md"), "---\nversion: 1\n---");
|
|
1039
|
+
writeFileSync(join(repo, "src.ts"), "const x = 1;");
|
|
1040
|
+
|
|
1041
|
+
// With commit_docs: false, smartStage should exclude .gsd/
|
|
1042
|
+
const svc = new GitServiceImpl(repo, { commit_docs: false });
|
|
1043
|
+
const msg = svc.commit({ message: "test commit" });
|
|
1044
|
+
assertTrue(msg !== null, "commit_docs=false: commit succeeds with non-.gsd files");
|
|
1045
|
+
|
|
1046
|
+
// .gsd/ files should NOT be in the commit
|
|
1047
|
+
const committed = run("git show --name-only HEAD", repo);
|
|
1048
|
+
assertTrue(!committed.includes(".gsd/"), "commit_docs=false: .gsd/ files not in commit");
|
|
1049
|
+
assertTrue(committed.includes("src.ts"), "commit_docs=false: source files ARE in commit");
|
|
1050
|
+
|
|
1051
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ─── commit_docs: true (default) — smartStage includes .gsd/ ────────
|
|
1055
|
+
|
|
1056
|
+
console.log("\n=== commit_docs: true — smartStage includes .gsd/ ===");
|
|
1057
|
+
|
|
1058
|
+
{
|
|
1059
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-commit-docs-default-"));
|
|
1060
|
+
run("git init -b main", repo);
|
|
1061
|
+
run("git config user.email test@test.com", repo);
|
|
1062
|
+
run("git config user.name Test", repo);
|
|
1063
|
+
writeFileSync(join(repo, "README.md"), "init");
|
|
1064
|
+
run("git add -A && git commit -m init", repo);
|
|
1065
|
+
|
|
1066
|
+
mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
|
|
1067
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M001", "ROADMAP.md"), "# Roadmap");
|
|
1068
|
+
writeFileSync(join(repo, "src.ts"), "const x = 1;");
|
|
1069
|
+
|
|
1070
|
+
// Default behavior (commit_docs not set) — .gsd/ files ARE committed
|
|
1071
|
+
const svc = new GitServiceImpl(repo);
|
|
1072
|
+
const msg = svc.commit({ message: "test commit" });
|
|
1073
|
+
assertTrue(msg !== null, "commit_docs=default: commit succeeds");
|
|
1074
|
+
|
|
1075
|
+
const committed = run("git show --name-only HEAD", repo);
|
|
1076
|
+
assertTrue(committed.includes(".gsd/"), "commit_docs=default: .gsd/ files ARE in commit");
|
|
1077
|
+
assertTrue(committed.includes("src.ts"), "commit_docs=default: source files in commit");
|
|
1078
|
+
|
|
1079
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ─── writeIntegrationBranch: commitDocs false skips commit ──────────
|
|
1083
|
+
|
|
1084
|
+
console.log("\n=== writeIntegrationBranch: commitDocs false skips commit ===");
|
|
1085
|
+
|
|
1086
|
+
{
|
|
1087
|
+
const repo = initBranchTestRepo();
|
|
1088
|
+
const commitsBefore = run("git rev-list --count HEAD", repo);
|
|
1089
|
+
|
|
1090
|
+
writeIntegrationBranch(repo, "M001", "f-123-new-thing", { commitDocs: false });
|
|
1091
|
+
|
|
1092
|
+
// File should still be written to disk
|
|
1093
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-123-new-thing",
|
|
1094
|
+
"commitDocs=false: metadata file exists on disk");
|
|
1095
|
+
|
|
1096
|
+
// But no new commit should have been created
|
|
1097
|
+
const commitsAfter = run("git rev-list --count HEAD", repo);
|
|
1098
|
+
assertEq(commitsBefore, commitsAfter,
|
|
1099
|
+
"commitDocs=false: no git commit created for integration branch");
|
|
1100
|
+
|
|
1101
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ─── ensureGitignore: commit_docs false adds blanket .gsd/ ──────────
|
|
1105
|
+
|
|
1106
|
+
console.log("\n=== ensureGitignore: commit_docs false ===");
|
|
1107
|
+
|
|
1108
|
+
{
|
|
1109
|
+
const { ensureGitignore } = await import("../gitignore.ts");
|
|
1110
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-gitignore-commit-docs-"));
|
|
1111
|
+
|
|
1112
|
+
// When commit_docs is false, should add blanket .gsd/ to gitignore
|
|
1113
|
+
const modified = ensureGitignore(repo, { commitDocs: false });
|
|
1114
|
+
assertTrue(modified, "commit_docs=false: gitignore was modified");
|
|
1115
|
+
|
|
1116
|
+
const { readFileSync } = await import("node:fs");
|
|
1117
|
+
const content = readFileSync(join(repo, ".gitignore"), "utf-8");
|
|
1118
|
+
assertTrue(content.includes(".gsd/"), "commit_docs=false: .gitignore contains blanket .gsd/");
|
|
1119
|
+
assertTrue(content.includes("commit_docs: false"), "commit_docs=false: .gitignore contains explanatory comment");
|
|
1120
|
+
|
|
1121
|
+
// Should NOT contain individual runtime patterns (those are subsumed by blanket .gsd/)
|
|
1122
|
+
// But it's OK if it does — the blanket .gsd/ covers everything
|
|
1123
|
+
|
|
1124
|
+
// Idempotent — calling again doesn't add duplicates
|
|
1125
|
+
const modified2 = ensureGitignore(repo, { commitDocs: false });
|
|
1126
|
+
assertTrue(!modified2, "commit_docs=false: second call is idempotent");
|
|
1127
|
+
|
|
1128
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// ─── ensureGitignore: commit_docs true removes blanket .gsd/ ────────
|
|
1132
|
+
|
|
1133
|
+
console.log("\n=== ensureGitignore: commit_docs true self-heals ===");
|
|
1134
|
+
|
|
1135
|
+
{
|
|
1136
|
+
const { ensureGitignore } = await import("../gitignore.ts");
|
|
1137
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-gitignore-selfheal-"));
|
|
1138
|
+
|
|
1139
|
+
// Start with a gitignore that has a blanket .gsd/ (e.g., user switched setting)
|
|
1140
|
+
writeFileSync(join(repo, ".gitignore"), ".gsd/\n");
|
|
1141
|
+
|
|
1142
|
+
const modified = ensureGitignore(repo, { commitDocs: true });
|
|
1143
|
+
assertTrue(modified, "commit_docs=true: gitignore was modified");
|
|
1144
|
+
|
|
1145
|
+
const { readFileSync } = await import("node:fs");
|
|
1146
|
+
const content = readFileSync(join(repo, ".gitignore"), "utf-8");
|
|
1147
|
+
// Blanket .gsd/ should be removed
|
|
1148
|
+
const lines = content.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#"));
|
|
1149
|
+
assertTrue(!lines.includes(".gsd/"), "commit_docs=true: blanket .gsd/ was removed");
|
|
1150
|
+
assertTrue(!lines.includes(".gsd"), "commit_docs=true: blanket .gsd was removed");
|
|
1151
|
+
|
|
1152
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1023
1155
|
report();
|
|
1024
1156
|
}
|
|
1025
1157
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight tool tracking tests — verifies that markToolStart/markToolEnd
|
|
3
|
+
* correctly manage the in-flight tools set used by the idle watchdog to
|
|
4
|
+
* distinguish "agent waiting on long-running tool" from "agent is idle".
|
|
5
|
+
*
|
|
6
|
+
* Background: The idle watchdog checks every 15s for agent progress. Without
|
|
7
|
+
* in-flight tool tracking, agents waiting on await_job or async_bash (which
|
|
8
|
+
* can run 20+ minutes for evaluations, deployments, test suites) are falsely
|
|
9
|
+
* declared idle and interrupted by recovery steering messages.
|
|
10
|
+
*
|
|
11
|
+
* The fix hooks tool_execution_start/end events to track active tool calls.
|
|
12
|
+
* When tools are in-flight, the watchdog resets lastProgressAt instead of
|
|
13
|
+
* triggering idle recovery.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { markToolStart, markToolEnd, isAutoActive } from "../auto.ts";
|
|
17
|
+
import { createTestContext } from './test-helpers.ts';
|
|
18
|
+
|
|
19
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
20
|
+
|
|
21
|
+
// ═══ markToolStart / markToolEnd basic behavior ═════════════════════════════
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
console.log("\n=== markToolStart: no-op when auto-mode is not active ===");
|
|
25
|
+
// When auto-mode is not active, markToolStart should silently ignore
|
|
26
|
+
// (the guard `if (!active) return` prevents set pollution outside auto-mode)
|
|
27
|
+
assertTrue(!isAutoActive(), "auto-mode should not be active in tests");
|
|
28
|
+
markToolStart("tool-1");
|
|
29
|
+
// We can't directly inspect the set, but markToolEnd should be a safe no-op
|
|
30
|
+
markToolEnd("tool-1");
|
|
31
|
+
// If we got here without error, the guard works
|
|
32
|
+
assertTrue(true, "markToolStart/markToolEnd are safe no-ops when inactive");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
console.log("\n=== markToolEnd: no-op for unknown toolCallId ===");
|
|
37
|
+
// Set.delete on non-existent key is a no-op — verify no crash
|
|
38
|
+
markToolEnd("nonexistent-tool-call-id");
|
|
39
|
+
assertTrue(true, "markToolEnd handles unknown IDs gracefully");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
console.log("\n=== markToolEnd: idempotent — double-end does not crash ===");
|
|
44
|
+
markToolEnd("some-id");
|
|
45
|
+
markToolEnd("some-id");
|
|
46
|
+
assertTrue(true, "double markToolEnd is safe");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ═══ Integration contract: expected exports from auto.ts ═════════════════════
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
console.log("\n=== auto.ts exports markToolStart and markToolEnd ===");
|
|
53
|
+
assertEq(typeof markToolStart, "function", "markToolStart should be a function");
|
|
54
|
+
assertEq(typeof markToolEnd, "function", "markToolEnd should be a function");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
console.log("\n=== markToolStart accepts string toolCallId ===");
|
|
59
|
+
// Verify the function signature handles string input without error
|
|
60
|
+
// (when inactive, this is a no-op but should not throw)
|
|
61
|
+
try {
|
|
62
|
+
markToolStart("toolu_01ABC123");
|
|
63
|
+
assertTrue(true, "accepts standard Claude tool call ID format");
|
|
64
|
+
} catch (e) {
|
|
65
|
+
assertTrue(false, `should not throw: ${e}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
console.log("\n=== markToolEnd accepts string toolCallId ===");
|
|
71
|
+
try {
|
|
72
|
+
markToolEnd("toolu_01ABC123");
|
|
73
|
+
assertTrue(true, "accepts standard Claude tool call ID format");
|
|
74
|
+
} catch (e) {
|
|
75
|
+
assertTrue(false, `should not throw: ${e}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
report();
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for KNOWLEDGE.md integration.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - KNOWLEDGE is registered in GSD_ROOT_FILES
|
|
6
|
+
* - resolveGsdRootFile resolves KNOWLEDGE paths correctly
|
|
7
|
+
* - inlineGsdRootFile works with the KNOWLEDGE key
|
|
8
|
+
* - before_agent_start hook includes/omits knowledge block appropriately
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
|
|
17
|
+
import { inlineGsdRootFile } from '../auto-prompts.ts';
|
|
18
|
+
import { appendKnowledge } from '../files.ts';
|
|
19
|
+
|
|
20
|
+
// ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
|
|
21
|
+
|
|
22
|
+
test('knowledge: KNOWLEDGE key exists in GSD_ROOT_FILES', () => {
|
|
23
|
+
assert.ok('KNOWLEDGE' in GSD_ROOT_FILES, 'GSD_ROOT_FILES should have KNOWLEDGE key');
|
|
24
|
+
assert.strictEqual(GSD_ROOT_FILES.KNOWLEDGE, 'KNOWLEDGE.md');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ─── resolveGsdRootFile resolves KNOWLEDGE.md ───────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('knowledge: resolveGsdRootFile returns canonical path when KNOWLEDGE.md exists', () => {
|
|
30
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
31
|
+
const gsdDir = join(tmp, '.gsd');
|
|
32
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
33
|
+
writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n');
|
|
34
|
+
|
|
35
|
+
const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
|
|
36
|
+
assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
|
|
37
|
+
|
|
38
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('knowledge: resolveGsdRootFile resolves when legacy knowledge.md exists', () => {
|
|
42
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
43
|
+
const gsdDir = join(tmp, '.gsd');
|
|
44
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
45
|
+
writeFileSync(join(gsdDir, 'knowledge.md'), '# Project Knowledge\n');
|
|
46
|
+
|
|
47
|
+
const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
|
|
48
|
+
// On case-insensitive filesystems (macOS), canonical path matches;
|
|
49
|
+
// on case-sensitive (Linux), legacy path matches. Either is valid.
|
|
50
|
+
const canonical = join(gsdDir, 'KNOWLEDGE.md');
|
|
51
|
+
const legacy = join(gsdDir, 'knowledge.md');
|
|
52
|
+
assert.ok(
|
|
53
|
+
resolved === canonical || resolved === legacy,
|
|
54
|
+
`resolved path should be canonical or legacy, got: ${resolved}`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('knowledge: resolveGsdRootFile returns canonical path when file does not exist', () => {
|
|
61
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
62
|
+
const gsdDir = join(tmp, '.gsd');
|
|
63
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
|
|
66
|
+
assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
|
|
67
|
+
|
|
68
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ─── inlineGsdRootFile works with knowledge.md ─────────────────────────────
|
|
72
|
+
|
|
73
|
+
test('knowledge: inlineGsdRootFile returns content when KNOWLEDGE.md exists', async () => {
|
|
74
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
75
|
+
const gsdDir = join(tmp, '.gsd');
|
|
76
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
77
|
+
writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n\n## Rules\n\nK001: Use real DB');
|
|
78
|
+
|
|
79
|
+
const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
|
|
80
|
+
assert.ok(result !== null, 'should return content');
|
|
81
|
+
assert.ok(result!.includes('Project Knowledge'), 'should include label');
|
|
82
|
+
assert.ok(result!.includes('K001'), 'should include knowledge content');
|
|
83
|
+
|
|
84
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('knowledge: inlineGsdRootFile returns null when KNOWLEDGE.md does not exist', async () => {
|
|
88
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
89
|
+
const gsdDir = join(tmp, '.gsd');
|
|
90
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
|
|
93
|
+
assert.strictEqual(result, null, 'should return null when file does not exist');
|
|
94
|
+
|
|
95
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── appendKnowledge creates file and appends entries ──────────────────────
|
|
99
|
+
|
|
100
|
+
test('knowledge: appendKnowledge creates KNOWLEDGE.md with rule when file does not exist', async () => {
|
|
101
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
102
|
+
const gsdDir = join(tmp, '.gsd');
|
|
103
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
await appendKnowledge(tmp, 'rule', 'Use real DB for integration tests', 'M001/S01');
|
|
106
|
+
|
|
107
|
+
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
|
108
|
+
assert.ok(content.includes('# Project Knowledge'), 'should have header');
|
|
109
|
+
assert.ok(content.includes('K001'), 'should have K001 id');
|
|
110
|
+
assert.ok(content.includes('Use real DB for integration tests'), 'should have rule text');
|
|
111
|
+
assert.ok(content.includes('M001/S01'), 'should have scope');
|
|
112
|
+
|
|
113
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('knowledge: appendKnowledge appends to existing KNOWLEDGE.md with auto-incrementing ID', async () => {
|
|
117
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
118
|
+
const gsdDir = join(tmp, '.gsd');
|
|
119
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
120
|
+
|
|
121
|
+
// Create initial file with one rule
|
|
122
|
+
await appendKnowledge(tmp, 'rule', 'First rule', 'M001');
|
|
123
|
+
// Add second rule
|
|
124
|
+
await appendKnowledge(tmp, 'rule', 'Second rule', 'M001/S02');
|
|
125
|
+
|
|
126
|
+
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
|
127
|
+
assert.ok(content.includes('K001'), 'should have K001');
|
|
128
|
+
assert.ok(content.includes('K002'), 'should have K002');
|
|
129
|
+
assert.ok(content.includes('First rule'), 'should have first rule');
|
|
130
|
+
assert.ok(content.includes('Second rule'), 'should have second rule');
|
|
131
|
+
|
|
132
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('knowledge: appendKnowledge handles pattern type', async () => {
|
|
136
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
137
|
+
const gsdDir = join(tmp, '.gsd');
|
|
138
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
139
|
+
|
|
140
|
+
await appendKnowledge(tmp, 'pattern', 'Middleware chain for auth', 'M001');
|
|
141
|
+
|
|
142
|
+
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
|
143
|
+
assert.ok(content.includes('P001'), 'should have P001 id');
|
|
144
|
+
assert.ok(content.includes('Middleware chain for auth'), 'should have pattern text');
|
|
145
|
+
|
|
146
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('knowledge: appendKnowledge handles lesson type', async () => {
|
|
150
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
|
151
|
+
const gsdDir = join(tmp, '.gsd');
|
|
152
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
153
|
+
|
|
154
|
+
await appendKnowledge(tmp, 'lesson', 'API timeout on large payloads', 'M002');
|
|
155
|
+
|
|
156
|
+
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
|
157
|
+
assert.ok(content.includes('L001'), 'should have L001 id');
|
|
158
|
+
assert.ok(content.includes('API timeout on large payloads'), 'should have lesson text');
|
|
159
|
+
|
|
160
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
161
|
+
});
|