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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-leak-guards.test.ts — Tests for #611 memory leak fixes.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that module-level state accumulators are properly bounded
|
|
5
|
+
* and cleared to prevent OOM during long-running auto-mode sessions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
import { saveActivityLog, clearActivityLogState } from "../activity-log.ts";
|
|
15
|
+
import { clearPathCache } from "../paths.ts";
|
|
16
|
+
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
function createCtx(entries: unknown[]) {
|
|
19
|
+
return { sessionManager: { getEntries: () => entries } } as unknown as ExtensionContext;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── activity-log: clearActivityLogState ─────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
test("clearActivityLogState resets dedup state so identical saves write again", () => {
|
|
25
|
+
clearActivityLogState();
|
|
26
|
+
const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-test-"));
|
|
27
|
+
try {
|
|
28
|
+
const entries = [{ role: "assistant", content: "test entry" }];
|
|
29
|
+
const ctx = createCtx(entries);
|
|
30
|
+
|
|
31
|
+
// First save
|
|
32
|
+
saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
|
|
33
|
+
|
|
34
|
+
const actDir = join(baseDir, ".gsd", "activity");
|
|
35
|
+
assert.equal(readdirSync(actDir).length, 1, "first save creates one file");
|
|
36
|
+
|
|
37
|
+
// Same content, same unit — deduped
|
|
38
|
+
saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
|
|
39
|
+
assert.equal(readdirSync(actDir).length, 1, "dedup prevents duplicate write");
|
|
40
|
+
|
|
41
|
+
// Clear state
|
|
42
|
+
clearActivityLogState();
|
|
43
|
+
|
|
44
|
+
// Same content again — after clear, writes again (fresh state)
|
|
45
|
+
saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
|
|
46
|
+
assert.equal(readdirSync(actDir).length, 2, "after clear, dedup state is reset");
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─── activity-log: streaming JSONL write ────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
test("saveActivityLog writes valid JSONL via streaming", () => {
|
|
55
|
+
clearActivityLogState();
|
|
56
|
+
const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-jsonl-"));
|
|
57
|
+
try {
|
|
58
|
+
const entries = [
|
|
59
|
+
{ type: "message", message: { role: "user", content: "hello" } },
|
|
60
|
+
{ type: "message", message: { role: "assistant", content: "world" } },
|
|
61
|
+
{ type: "message", message: { role: "user", content: "test" } },
|
|
62
|
+
];
|
|
63
|
+
const ctx = createCtx(entries);
|
|
64
|
+
|
|
65
|
+
saveActivityLog(ctx, baseDir, "execute-task", "M002/S01/T01");
|
|
66
|
+
|
|
67
|
+
const actDir = join(baseDir, ".gsd", "activity");
|
|
68
|
+
const files = readdirSync(actDir);
|
|
69
|
+
assert.equal(files.length, 1, "one file written");
|
|
70
|
+
|
|
71
|
+
const content = readFileSync(join(actDir, files[0]), "utf-8");
|
|
72
|
+
const lines = content.trim().split("\n");
|
|
73
|
+
assert.equal(lines.length, 3, "three JSONL lines");
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
assert.doesNotThrow(() => JSON.parse(line), `line is valid JSON`);
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─── paths.ts: directory cache bounds ───────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
test("clearPathCache does not throw", () => {
|
|
86
|
+
assert.doesNotThrow(() => clearPathCache(), "clearPathCache should not throw");
|
|
87
|
+
});
|
|
@@ -68,6 +68,34 @@ async function main(): Promise<void> {
|
|
|
68
68
|
assertTrue(warnings[0].includes("merge_to_main"), "warning mentions merge_to_main");
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
console.log("\n=== git.commit_docs ===");
|
|
72
|
+
|
|
73
|
+
// Valid boolean values accepted
|
|
74
|
+
{
|
|
75
|
+
const { preferences, errors } = validatePreferences({ git: { commit_docs: false } });
|
|
76
|
+
assertEq(errors.length, 0, "commit_docs: false — no errors");
|
|
77
|
+
assertEq(preferences.git?.commit_docs, false, "commit_docs: false — value preserved");
|
|
78
|
+
}
|
|
79
|
+
{
|
|
80
|
+
const { preferences, errors } = validatePreferences({ git: { commit_docs: true } });
|
|
81
|
+
assertEq(errors.length, 0, "commit_docs: true — no errors");
|
|
82
|
+
assertEq(preferences.git?.commit_docs, true, "commit_docs: true — value preserved");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Invalid type produces error
|
|
86
|
+
{
|
|
87
|
+
const { errors } = validatePreferences({ git: { commit_docs: "no" as any } });
|
|
88
|
+
assertTrue(errors.length > 0, "commit_docs: string — produces error");
|
|
89
|
+
assertTrue(errors[0].includes("commit_docs"), "commit_docs: string — error mentions commit_docs");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Undefined passes through without issue
|
|
93
|
+
{
|
|
94
|
+
const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
|
|
95
|
+
assertEq(errors.length, 0, "commit_docs: undefined — no errors");
|
|
96
|
+
assertEq(preferences.git?.commit_docs, undefined, "commit_docs: undefined — not set");
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
report();
|
|
72
100
|
}
|
|
73
101
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preferences-wizard-fields.test.ts — Validates that all wizard-configurable
|
|
3
|
+
* preference fields are properly validated and round-trip through the schema.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
7
|
+
import { validatePreferences } from "../preferences.ts";
|
|
8
|
+
import type { GSDPreferences } from "../preferences.ts";
|
|
9
|
+
|
|
10
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
11
|
+
|
|
12
|
+
async function main(): Promise<void> {
|
|
13
|
+
console.log("\n=== budget fields validate correctly ===");
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
const { preferences, errors } = validatePreferences({
|
|
17
|
+
budget_ceiling: 25.50,
|
|
18
|
+
budget_enforcement: "warn",
|
|
19
|
+
context_pause_threshold: 80,
|
|
20
|
+
});
|
|
21
|
+
assertEq(errors.length, 0, "valid budget fields produce no errors");
|
|
22
|
+
assertEq(preferences.budget_ceiling, 25.50, "budget_ceiling passes through");
|
|
23
|
+
assertEq(preferences.budget_enforcement, "warn", "budget_enforcement passes through");
|
|
24
|
+
assertEq(preferences.context_pause_threshold, 80, "context_pause_threshold passes through");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
const { preferences, errors } = validatePreferences({
|
|
29
|
+
budget_enforcement: "pause",
|
|
30
|
+
});
|
|
31
|
+
assertEq(errors.length, 0, "budget_enforcement 'pause' is valid");
|
|
32
|
+
assertEq(preferences.budget_enforcement, "pause", "pause passes through");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
const { preferences, errors } = validatePreferences({
|
|
37
|
+
budget_enforcement: "halt",
|
|
38
|
+
});
|
|
39
|
+
assertEq(errors.length, 0, "budget_enforcement 'halt' is valid");
|
|
40
|
+
assertEq(preferences.budget_enforcement, "halt", "halt passes through");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
const { errors } = validatePreferences({
|
|
45
|
+
budget_enforcement: "invalid",
|
|
46
|
+
} as unknown as GSDPreferences);
|
|
47
|
+
assertTrue(errors.some(e => e.includes("budget_enforcement")), "invalid budget_enforcement rejected");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log("\n=== notification fields validate correctly ===");
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
const { preferences, errors } = validatePreferences({
|
|
54
|
+
notifications: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
on_complete: false,
|
|
57
|
+
on_error: true,
|
|
58
|
+
on_budget: true,
|
|
59
|
+
on_milestone: false,
|
|
60
|
+
on_attention: true,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
assertEq(errors.length, 0, "valid notifications produce no errors");
|
|
64
|
+
assertEq(preferences.notifications?.enabled, true, "notifications.enabled passes through");
|
|
65
|
+
assertEq(preferences.notifications?.on_complete, false, "notifications.on_complete passes through");
|
|
66
|
+
assertEq(preferences.notifications?.on_milestone, false, "notifications.on_milestone passes through");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
const { errors } = validatePreferences({
|
|
71
|
+
notifications: "invalid",
|
|
72
|
+
} as unknown as GSDPreferences);
|
|
73
|
+
assertTrue(errors.some(e => e.includes("notifications")), "invalid notifications rejected");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log("\n=== git fields validate correctly ===");
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
const { preferences, errors } = validatePreferences({
|
|
80
|
+
git: {
|
|
81
|
+
auto_push: true,
|
|
82
|
+
push_branches: false,
|
|
83
|
+
remote: "upstream",
|
|
84
|
+
snapshots: true,
|
|
85
|
+
pre_merge_check: "auto",
|
|
86
|
+
commit_type: "feat",
|
|
87
|
+
main_branch: "develop",
|
|
88
|
+
merge_strategy: "squash",
|
|
89
|
+
isolation: "branch",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
assertEq(errors.length, 0, "valid git fields produce no errors");
|
|
93
|
+
assertEq(preferences.git?.auto_push, true, "git.auto_push passes through");
|
|
94
|
+
assertEq(preferences.git?.push_branches, false, "git.push_branches passes through");
|
|
95
|
+
assertEq(preferences.git?.remote, "upstream", "git.remote passes through");
|
|
96
|
+
assertEq(preferences.git?.snapshots, true, "git.snapshots passes through");
|
|
97
|
+
assertEq(preferences.git?.pre_merge_check, "auto", "git.pre_merge_check passes through");
|
|
98
|
+
assertEq(preferences.git?.commit_type, "feat", "git.commit_type passes through");
|
|
99
|
+
assertEq(preferences.git?.main_branch, "develop", "git.main_branch passes through");
|
|
100
|
+
assertEq(preferences.git?.merge_strategy, "squash", "git.merge_strategy passes through");
|
|
101
|
+
assertEq(preferences.git?.isolation, "branch", "git.isolation passes through");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log("\n=== uat_dispatch validates correctly ===");
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
const { preferences, errors } = validatePreferences({ uat_dispatch: true });
|
|
108
|
+
assertEq(errors.length, 0, "valid uat_dispatch produces no errors");
|
|
109
|
+
assertEq(preferences.uat_dispatch, true, "uat_dispatch true passes through");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
const { preferences, errors } = validatePreferences({ uat_dispatch: false });
|
|
114
|
+
assertEq(errors.length, 0, "valid uat_dispatch false produces no errors");
|
|
115
|
+
assertEq(preferences.uat_dispatch, false, "uat_dispatch false passes through");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log("\n=== unique_milestone_ids validates correctly ===");
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
const { preferences, errors } = validatePreferences({ unique_milestone_ids: true });
|
|
122
|
+
assertEq(errors.length, 0, "valid unique_milestone_ids produces no errors");
|
|
123
|
+
assertEq(preferences.unique_milestone_ids, true, "unique_milestone_ids passes through");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log("\n=== all wizard fields together produce no errors ===");
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
const fullPrefs: GSDPreferences = {
|
|
130
|
+
version: 1,
|
|
131
|
+
models: { research: "claude-opus-4-6", planning: "claude-sonnet-4-6" },
|
|
132
|
+
auto_supervisor: { soft_timeout_minutes: 15, idle_timeout_minutes: 5, hard_timeout_minutes: 25 },
|
|
133
|
+
git: {
|
|
134
|
+
main_branch: "main",
|
|
135
|
+
auto_push: true,
|
|
136
|
+
push_branches: false,
|
|
137
|
+
remote: "origin",
|
|
138
|
+
snapshots: true,
|
|
139
|
+
pre_merge_check: "auto",
|
|
140
|
+
commit_type: "feat",
|
|
141
|
+
merge_strategy: "squash",
|
|
142
|
+
isolation: "worktree",
|
|
143
|
+
},
|
|
144
|
+
skill_discovery: "suggest",
|
|
145
|
+
unique_milestone_ids: false,
|
|
146
|
+
budget_ceiling: 50,
|
|
147
|
+
budget_enforcement: "pause",
|
|
148
|
+
context_pause_threshold: 75,
|
|
149
|
+
notifications: {
|
|
150
|
+
enabled: true,
|
|
151
|
+
on_complete: true,
|
|
152
|
+
on_error: true,
|
|
153
|
+
on_budget: true,
|
|
154
|
+
on_milestone: true,
|
|
155
|
+
on_attention: true,
|
|
156
|
+
},
|
|
157
|
+
uat_dispatch: false,
|
|
158
|
+
};
|
|
159
|
+
const { errors, warnings } = validatePreferences(fullPrefs);
|
|
160
|
+
const unknownWarnings = warnings.filter(w => w.includes("unknown"));
|
|
161
|
+
assertEq(errors.length, 0, "full wizard prefs produce no errors");
|
|
162
|
+
assertEq(unknownWarnings.length, 0, "full wizard prefs produce no unknown-key warnings");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
report();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main();
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
loadQueueOrder,
|
|
7
|
+
saveQueueOrder,
|
|
8
|
+
sortByQueueOrder,
|
|
9
|
+
pruneQueueOrder,
|
|
10
|
+
validateQueueOrder,
|
|
11
|
+
} from '../queue-order.ts';
|
|
12
|
+
import { createTestContext } from './test-helpers.ts';
|
|
13
|
+
|
|
14
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
15
|
+
|
|
16
|
+
// ─── Fixture Helpers ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function createFixtureBase(): string {
|
|
19
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-queue-order-'));
|
|
20
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
21
|
+
return base;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cleanup(base: string): void {
|
|
25
|
+
rmSync(base, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// sortByQueueOrder
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
console.log('\n=== sortByQueueOrder ===');
|
|
33
|
+
|
|
34
|
+
// Null order → default milestoneIdSort
|
|
35
|
+
{
|
|
36
|
+
const result = sortByQueueOrder(['M003', 'M001', 'M002'], null);
|
|
37
|
+
assertEq(result, ['M001', 'M002', 'M003'], 'null order falls back to numeric sort');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Custom order → exact sequence
|
|
41
|
+
{
|
|
42
|
+
const result = sortByQueueOrder(['M001', 'M002', 'M003'], ['M003', 'M001', 'M002']);
|
|
43
|
+
assertEq(result, ['M003', 'M001', 'M002'], 'custom order produces exact sequence');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Custom order with new IDs → appended at end in numeric order
|
|
47
|
+
{
|
|
48
|
+
const result = sortByQueueOrder(['M001', 'M002', 'M003', 'M004'], ['M003', 'M001']);
|
|
49
|
+
assertEq(result, ['M003', 'M001', 'M002', 'M004'], 'new IDs appended in numeric order');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Custom order with deleted IDs → silently skipped
|
|
53
|
+
{
|
|
54
|
+
const result = sortByQueueOrder(['M001', 'M003'], ['M003', 'M002', 'M001']);
|
|
55
|
+
assertEq(result, ['M003', 'M001'], 'deleted IDs in order are skipped');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Empty custom order → all IDs in numeric order
|
|
59
|
+
{
|
|
60
|
+
const result = sortByQueueOrder(['M002', 'M001'], []);
|
|
61
|
+
assertEq(result, ['M001', 'M002'], 'empty custom order falls back to numeric sort');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
+
// loadQueueOrder / saveQueueOrder
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
console.log('\n=== loadQueueOrder / saveQueueOrder ===');
|
|
69
|
+
|
|
70
|
+
// Load returns null when file doesn't exist
|
|
71
|
+
{
|
|
72
|
+
const base = createFixtureBase();
|
|
73
|
+
assertEq(loadQueueOrder(base), null, 'returns null when file missing');
|
|
74
|
+
cleanup(base);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Save then load round-trip
|
|
78
|
+
{
|
|
79
|
+
const base = createFixtureBase();
|
|
80
|
+
saveQueueOrder(base, ['M003', 'M001', 'M002']);
|
|
81
|
+
const loaded = loadQueueOrder(base);
|
|
82
|
+
assertEq(loaded, ['M003', 'M001', 'M002'], 'round-trip preserves order');
|
|
83
|
+
|
|
84
|
+
// Verify file contains updatedAt
|
|
85
|
+
const raw = JSON.parse(readFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), 'utf-8'));
|
|
86
|
+
assertTrue(typeof raw.updatedAt === 'string' && raw.updatedAt.length > 0, 'file contains updatedAt');
|
|
87
|
+
|
|
88
|
+
cleanup(base);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Load returns null on corrupt JSON
|
|
92
|
+
{
|
|
93
|
+
const base = createFixtureBase();
|
|
94
|
+
writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), 'not json');
|
|
95
|
+
assertEq(loadQueueOrder(base), null, 'returns null on corrupt JSON');
|
|
96
|
+
cleanup(base);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Load returns null when order field is not an array
|
|
100
|
+
{
|
|
101
|
+
const base = createFixtureBase();
|
|
102
|
+
writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), '{"order": "invalid"}');
|
|
103
|
+
assertEq(loadQueueOrder(base), null, 'returns null when order is not array');
|
|
104
|
+
cleanup(base);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
+
// pruneQueueOrder
|
|
109
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
console.log('\n=== pruneQueueOrder ===');
|
|
112
|
+
|
|
113
|
+
// Prune removes invalid IDs
|
|
114
|
+
{
|
|
115
|
+
const base = createFixtureBase();
|
|
116
|
+
saveQueueOrder(base, ['M001', 'M002', 'M003']);
|
|
117
|
+
pruneQueueOrder(base, ['M001', 'M003']);
|
|
118
|
+
assertEq(loadQueueOrder(base), ['M001', 'M003'], 'prune removes invalid IDs');
|
|
119
|
+
cleanup(base);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Prune no-ops when file doesn't exist
|
|
123
|
+
{
|
|
124
|
+
const base = createFixtureBase();
|
|
125
|
+
pruneQueueOrder(base, ['M001']); // should not throw
|
|
126
|
+
assertTrue(!existsSync(join(base, '.gsd', 'QUEUE-ORDER.json')), 'prune does not create file');
|
|
127
|
+
cleanup(base);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Prune no-ops when all IDs are valid
|
|
131
|
+
{
|
|
132
|
+
const base = createFixtureBase();
|
|
133
|
+
saveQueueOrder(base, ['M001', 'M002']);
|
|
134
|
+
pruneQueueOrder(base, ['M001', 'M002', 'M003']);
|
|
135
|
+
assertEq(loadQueueOrder(base), ['M001', 'M002'], 'prune is no-op when all valid');
|
|
136
|
+
cleanup(base);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
140
|
+
// validateQueueOrder
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
console.log('\n=== validateQueueOrder ===');
|
|
144
|
+
|
|
145
|
+
// Valid order with no dependencies
|
|
146
|
+
{
|
|
147
|
+
const depsMap = new Map<string, string[]>();
|
|
148
|
+
const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
|
|
149
|
+
assertTrue(result.valid, 'valid when no dependencies');
|
|
150
|
+
assertEq(result.violations.length, 0, 'no violations');
|
|
151
|
+
assertEq(result.redundant.length, 0, 'no redundancies');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Dependency violation: M002 before M001, but M002 depends on M001
|
|
155
|
+
{
|
|
156
|
+
const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
|
|
157
|
+
const result = validateQueueOrder(['M002', 'M001'], depsMap, new Set());
|
|
158
|
+
assertTrue(!result.valid, 'invalid when dep violated');
|
|
159
|
+
assertEq(result.violations.length, 1, 'one violation');
|
|
160
|
+
assertEq(result.violations[0].type, 'would_block', 'violation type is would_block');
|
|
161
|
+
assertEq(result.violations[0].milestone, 'M002', 'violation milestone is M002');
|
|
162
|
+
assertEq(result.violations[0].dependsOn, 'M001', 'violation dep is M001');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Redundant dependency: M002 depends on M001, M001 comes first in order
|
|
166
|
+
{
|
|
167
|
+
const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
|
|
168
|
+
const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
|
|
169
|
+
assertTrue(result.valid, 'valid when dep satisfied by position');
|
|
170
|
+
assertEq(result.redundant.length, 1, 'one redundancy');
|
|
171
|
+
assertEq(result.redundant[0].milestone, 'M002', 'redundant milestone is M002');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Completed dep is always satisfied
|
|
175
|
+
{
|
|
176
|
+
const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
|
|
177
|
+
const result = validateQueueOrder(['M002'], depsMap, new Set(['M001']));
|
|
178
|
+
assertTrue(result.valid, 'valid when dep is already completed');
|
|
179
|
+
assertEq(result.violations.length, 0, 'no violations for completed dep');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Missing dependency
|
|
183
|
+
{
|
|
184
|
+
const depsMap = new Map<string, string[]>([['M002', ['M099']]]);
|
|
185
|
+
const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
|
|
186
|
+
assertTrue(!result.valid, 'invalid when dep does not exist');
|
|
187
|
+
assertEq(result.violations[0].type, 'missing_dep', 'violation type is missing_dep');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Circular dependency
|
|
191
|
+
{
|
|
192
|
+
const depsMap = new Map<string, string[]>([
|
|
193
|
+
['M001', ['M002']],
|
|
194
|
+
['M002', ['M001']],
|
|
195
|
+
]);
|
|
196
|
+
const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
|
|
197
|
+
assertTrue(!result.valid, 'invalid on circular dependency');
|
|
198
|
+
const circularViolation = result.violations.find(v => v.type === 'circular');
|
|
199
|
+
assertTrue(!!circularViolation, 'circular violation detected');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
report();
|