gsd-pi 2.12.0 → 2.13.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/dist/cli.js +18 -1
- package/dist/resource-loader.d.ts +2 -0
- package/dist/resource-loader.js +36 -1
- package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/dist/resources/extensions/gsd/auto.ts +222 -11
- package/dist/resources/extensions/gsd/doctor.ts +195 -1
- package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/dist/resources/extensions/gsd/git-service.ts +11 -0
- package/dist/resources/extensions/gsd/preferences.ts +17 -1
- package/dist/resources/extensions/gsd/prompts/system.md +32 -29
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +1 -1
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
- package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
- package/packages/pi-tui/dist/components/editor.d.ts +11 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +64 -6
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +71 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/src/resources/extensions/gsd/auto.ts +222 -11
- package/src/resources/extensions/gsd/doctor.ts +195 -1
- package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/src/resources/extensions/gsd/git-service.ts +11 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -1
- package/src/resources/extensions/gsd/prompts/system.md +32 -29
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/src/resources/extensions/search-the-web/native-search.ts +15 -10
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-self-heal.test.ts — Integration tests for git self-healing utilities.
|
|
3
|
+
*
|
|
4
|
+
* Uses real temporary git repos with deliberately broken state.
|
|
5
|
+
* No mocks — exercises actual git operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { existsSync, mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { rmSync } from "node:fs";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import {
|
|
15
|
+
abortAndReset,
|
|
16
|
+
withMergeHeal,
|
|
17
|
+
recoverCheckout,
|
|
18
|
+
formatGitError,
|
|
19
|
+
MergeConflictError,
|
|
20
|
+
} from "../git-self-heal.js";
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function makeTempRepo(): string {
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-self-heal-"));
|
|
26
|
+
execSync("git init", { cwd: dir, stdio: "pipe" });
|
|
27
|
+
execSync("git config user.email 'test@test.com'", { cwd: dir, stdio: "pipe" });
|
|
28
|
+
execSync("git config user.name 'Test'", { cwd: dir, stdio: "pipe" });
|
|
29
|
+
writeFileSync(join(dir, "README.md"), "# init\n");
|
|
30
|
+
execSync("git add -A && git commit -m 'init'", { cwd: dir, stdio: "pipe" });
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cleanup(dir: string) {
|
|
35
|
+
try {
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── abortAndReset ───────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
console.log("── abortAndReset ──");
|
|
45
|
+
|
|
46
|
+
// Test: leftover MERGE_HEAD
|
|
47
|
+
{
|
|
48
|
+
const dir = makeTempRepo();
|
|
49
|
+
try {
|
|
50
|
+
// Create a conflicting branch
|
|
51
|
+
execSync("git checkout -b feature", { cwd: dir, stdio: "pipe" });
|
|
52
|
+
writeFileSync(join(dir, "file.txt"), "feature content\n");
|
|
53
|
+
execSync("git add -A && git commit -m 'feature'", { cwd: dir, stdio: "pipe" });
|
|
54
|
+
execSync("git checkout master 2>/dev/null || git checkout main", { cwd: dir, stdio: "pipe" });
|
|
55
|
+
writeFileSync(join(dir, "file.txt"), "main content\n");
|
|
56
|
+
execSync("git add -A && git commit -m 'main change'", { cwd: dir, stdio: "pipe" });
|
|
57
|
+
|
|
58
|
+
// Create a merge conflict → MERGE_HEAD will exist
|
|
59
|
+
try {
|
|
60
|
+
execSync("git merge feature", { cwd: dir, stdio: "pipe" });
|
|
61
|
+
} catch {
|
|
62
|
+
// expected conflict
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
assert.ok(existsSync(join(dir, ".git", "MERGE_HEAD")), "MERGE_HEAD should exist before abort");
|
|
66
|
+
|
|
67
|
+
const result = abortAndReset(dir);
|
|
68
|
+
assert.ok(result.cleaned.some((s) => s.includes("aborted merge")), "should report aborted merge");
|
|
69
|
+
assert.ok(!existsSync(join(dir, ".git", "MERGE_HEAD")), "MERGE_HEAD should be gone after abort");
|
|
70
|
+
|
|
71
|
+
console.log(" ✓ cleans up leftover MERGE_HEAD");
|
|
72
|
+
} finally {
|
|
73
|
+
cleanup(dir);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Test: leftover SQUASH_MSG (no MERGE_HEAD)
|
|
78
|
+
{
|
|
79
|
+
const dir = makeTempRepo();
|
|
80
|
+
try {
|
|
81
|
+
// Manually create a SQUASH_MSG to simulate leftover state
|
|
82
|
+
writeFileSync(join(dir, ".git", "SQUASH_MSG"), "leftover squash message\n");
|
|
83
|
+
|
|
84
|
+
const result = abortAndReset(dir);
|
|
85
|
+
assert.ok(result.cleaned.some((s) => s.includes("SQUASH_MSG")), "should report SQUASH_MSG removal");
|
|
86
|
+
assert.ok(!existsSync(join(dir, ".git", "SQUASH_MSG")), "SQUASH_MSG should be gone");
|
|
87
|
+
|
|
88
|
+
console.log(" ✓ cleans up leftover SQUASH_MSG");
|
|
89
|
+
} finally {
|
|
90
|
+
cleanup(dir);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Test: clean state (no-op)
|
|
95
|
+
{
|
|
96
|
+
const dir = makeTempRepo();
|
|
97
|
+
try {
|
|
98
|
+
const result = abortAndReset(dir);
|
|
99
|
+
assert.deepStrictEqual(result.cleaned, [], "clean repo should produce empty cleaned array");
|
|
100
|
+
|
|
101
|
+
console.log(" ✓ no-op on clean state");
|
|
102
|
+
} finally {
|
|
103
|
+
cleanup(dir);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── withMergeHeal ───────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
console.log("── withMergeHeal ──");
|
|
110
|
+
|
|
111
|
+
// Test: transient failure succeeds on retry
|
|
112
|
+
{
|
|
113
|
+
const dir = makeTempRepo();
|
|
114
|
+
try {
|
|
115
|
+
let callCount = 0;
|
|
116
|
+
const result = withMergeHeal(dir, () => {
|
|
117
|
+
callCount++;
|
|
118
|
+
if (callCount === 1) throw new Error("transient git error");
|
|
119
|
+
return "success";
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.strictEqual(result, "success", "should return mergeFn result on retry");
|
|
123
|
+
assert.strictEqual(callCount, 2, "should have called mergeFn twice");
|
|
124
|
+
|
|
125
|
+
console.log(" ✓ transient failure succeeds on retry");
|
|
126
|
+
} finally {
|
|
127
|
+
cleanup(dir);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Test: real conflict escalates immediately (no retry)
|
|
132
|
+
{
|
|
133
|
+
const dir = makeTempRepo();
|
|
134
|
+
try {
|
|
135
|
+
// Set up a real merge conflict
|
|
136
|
+
execSync("git checkout -b conflict-branch", { cwd: dir, stdio: "pipe" });
|
|
137
|
+
writeFileSync(join(dir, "conflict.txt"), "branch A\n");
|
|
138
|
+
execSync("git add -A && git commit -m 'branch A'", { cwd: dir, stdio: "pipe" });
|
|
139
|
+
execSync("git checkout master 2>/dev/null || git checkout main", { cwd: dir, stdio: "pipe" });
|
|
140
|
+
writeFileSync(join(dir, "conflict.txt"), "branch B\n");
|
|
141
|
+
execSync("git add -A && git commit -m 'branch B'", { cwd: dir, stdio: "pipe" });
|
|
142
|
+
|
|
143
|
+
let callCount = 0;
|
|
144
|
+
try {
|
|
145
|
+
withMergeHeal(dir, () => {
|
|
146
|
+
callCount++;
|
|
147
|
+
// Actually perform the conflicting merge
|
|
148
|
+
execSync("git merge conflict-branch", { cwd: dir, stdio: "pipe" });
|
|
149
|
+
});
|
|
150
|
+
assert.fail("should have thrown MergeConflictError");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
assert.ok(err instanceof MergeConflictError, `should throw MergeConflictError, got ${(err as Error).constructor.name}`);
|
|
153
|
+
assert.strictEqual(callCount, 1, "should NOT retry on real conflict");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(" ✓ real conflict escalates immediately without retry");
|
|
157
|
+
} finally {
|
|
158
|
+
cleanup(dir);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── recoverCheckout ─────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
console.log("── recoverCheckout ──");
|
|
165
|
+
|
|
166
|
+
// Test: dirty index recovery
|
|
167
|
+
{
|
|
168
|
+
const dir = makeTempRepo();
|
|
169
|
+
try {
|
|
170
|
+
// Create a branch to checkout to
|
|
171
|
+
execSync("git checkout -b target-branch", { cwd: dir, stdio: "pipe" });
|
|
172
|
+
execSync("git checkout master 2>/dev/null || git checkout main", { cwd: dir, stdio: "pipe" });
|
|
173
|
+
|
|
174
|
+
// Dirty the index
|
|
175
|
+
writeFileSync(join(dir, "README.md"), "dirty changes\n");
|
|
176
|
+
execSync("git add README.md", { cwd: dir, stdio: "pipe" });
|
|
177
|
+
|
|
178
|
+
// Normal checkout would complain about dirty index
|
|
179
|
+
recoverCheckout(dir, "target-branch");
|
|
180
|
+
|
|
181
|
+
const branch = execSync("git branch --show-current", { cwd: dir, encoding: "utf-8" }).trim();
|
|
182
|
+
assert.strictEqual(branch, "target-branch", "should be on target branch after recovery");
|
|
183
|
+
|
|
184
|
+
console.log(" ✓ recovers checkout with dirty index");
|
|
185
|
+
} finally {
|
|
186
|
+
cleanup(dir);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Test: non-existent branch throws with context
|
|
191
|
+
{
|
|
192
|
+
const dir = makeTempRepo();
|
|
193
|
+
try {
|
|
194
|
+
try {
|
|
195
|
+
recoverCheckout(dir, "nonexistent-branch");
|
|
196
|
+
assert.fail("should have thrown");
|
|
197
|
+
} catch (err) {
|
|
198
|
+
assert.ok((err as Error).message.includes("recoverCheckout failed"), "should include context in error");
|
|
199
|
+
assert.ok((err as Error).message.includes("nonexistent-branch"), "should mention branch name");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(" ✓ throws with context for non-existent branch");
|
|
203
|
+
} finally {
|
|
204
|
+
cleanup(dir);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── formatGitError ──────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
console.log("── formatGitError ──");
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
const cases: Array<{ input: string; shouldContain: string; label: string }> = [
|
|
214
|
+
{ input: "CONFLICT (content): Merge conflict in file.ts", shouldContain: "/gsd doctor", label: "merge conflict" },
|
|
215
|
+
{ input: "error: pathspec 'foo' did not match any file(s)", shouldContain: "/gsd doctor", label: "checkout failure" },
|
|
216
|
+
{ input: "HEAD detached at abc123", shouldContain: "/gsd doctor", label: "detached HEAD" },
|
|
217
|
+
{ input: "Unable to create '/path/.git/index.lock': File exists", shouldContain: "/gsd doctor", label: "lock file" },
|
|
218
|
+
{ input: "fatal: not a git repository", shouldContain: "/gsd doctor", label: "not a repo" },
|
|
219
|
+
{ input: "some unknown error", shouldContain: "/gsd doctor", label: "unknown error" },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
for (const { input, shouldContain, label } of cases) {
|
|
223
|
+
const result = formatGitError(input);
|
|
224
|
+
assert.ok(result.includes(shouldContain), `${label}: should suggest /gsd doctor`);
|
|
225
|
+
console.log(` ✓ ${label} → suggests /gsd doctor`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Test with Error object
|
|
229
|
+
const result = formatGitError(new Error("CONFLICT in merge"));
|
|
230
|
+
assert.ok(result.includes("/gsd doctor"), "should handle Error objects");
|
|
231
|
+
console.log(" ✓ handles Error objects");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log("\n✅ All git-self-heal tests passed");
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isolation-resolver.test.ts -- Tests for shouldUseWorktreeIsolation resolver.
|
|
3
|
+
*
|
|
4
|
+
* Tests three resolution paths:
|
|
5
|
+
* 1. Explicit git.isolation preference overrides everything
|
|
6
|
+
* 2. Legacy detection: existing gsd/*\/* branches = branch mode
|
|
7
|
+
* 3. Default: new project = worktree mode
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
import { shouldUseWorktreeIsolation } from "../auto-worktree.ts";
|
|
16
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
17
|
+
|
|
18
|
+
const { assertEq, report } = createTestContext();
|
|
19
|
+
|
|
20
|
+
function run(command: string, cwd: string): string {
|
|
21
|
+
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createTempRepo(): string {
|
|
25
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "iso-resolver-test-")));
|
|
26
|
+
run("git init", dir);
|
|
27
|
+
run("git config user.email test@test.com", dir);
|
|
28
|
+
run("git config user.name Test", dir);
|
|
29
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
30
|
+
run("git add .", dir);
|
|
31
|
+
run("git commit -m init", dir);
|
|
32
|
+
run("git branch -M main", dir);
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main(): Promise<void> {
|
|
37
|
+
const savedCwd = process.cwd();
|
|
38
|
+
|
|
39
|
+
console.log("\n=== shouldUseWorktreeIsolation ===");
|
|
40
|
+
|
|
41
|
+
// Test 1: New project with no gsd branches → defaults to worktree (true)
|
|
42
|
+
{
|
|
43
|
+
const dir = createTempRepo();
|
|
44
|
+
try {
|
|
45
|
+
const result = shouldUseWorktreeIsolation(dir);
|
|
46
|
+
assertEq(result, true, "new project defaults to worktree isolation");
|
|
47
|
+
} finally {
|
|
48
|
+
process.chdir(savedCwd);
|
|
49
|
+
rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Test 2: Legacy project with gsd/*/* branches → returns false (branch mode)
|
|
54
|
+
{
|
|
55
|
+
const dir = createTempRepo();
|
|
56
|
+
try {
|
|
57
|
+
// Create a legacy gsd/*/* branch
|
|
58
|
+
run("git checkout -b gsd/M001/S01", dir);
|
|
59
|
+
writeFileSync(join(dir, "slice.md"), "# S01\n");
|
|
60
|
+
run("git add .", dir);
|
|
61
|
+
run("git commit -m 'slice work'", dir);
|
|
62
|
+
run("git checkout main", dir);
|
|
63
|
+
|
|
64
|
+
const result = shouldUseWorktreeIsolation(dir);
|
|
65
|
+
assertEq(result, false, "legacy project with gsd branches → branch mode");
|
|
66
|
+
} finally {
|
|
67
|
+
process.chdir(savedCwd);
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Test 3: Explicit preference override -- isolation: "worktree"
|
|
73
|
+
{
|
|
74
|
+
const dir = createTempRepo();
|
|
75
|
+
try {
|
|
76
|
+
// Create legacy branches that would normally trigger branch mode
|
|
77
|
+
run("git checkout -b gsd/M001/S01", dir);
|
|
78
|
+
writeFileSync(join(dir, "slice.md"), "# S01\n");
|
|
79
|
+
run("git add .", dir);
|
|
80
|
+
run("git commit -m 'slice work'", dir);
|
|
81
|
+
run("git checkout main", dir);
|
|
82
|
+
|
|
83
|
+
const result = shouldUseWorktreeIsolation(dir, { isolation: "worktree" });
|
|
84
|
+
assertEq(result, true, "explicit isolation: worktree overrides legacy detection");
|
|
85
|
+
} finally {
|
|
86
|
+
process.chdir(savedCwd);
|
|
87
|
+
rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Test 4: Explicit preference override -- isolation: "branch"
|
|
92
|
+
{
|
|
93
|
+
const dir = createTempRepo();
|
|
94
|
+
try {
|
|
95
|
+
// No legacy branches -- would normally default to worktree
|
|
96
|
+
const result = shouldUseWorktreeIsolation(dir, { isolation: "branch" });
|
|
97
|
+
assertEq(result, false, "explicit isolation: branch overrides default");
|
|
98
|
+
} finally {
|
|
99
|
+
process.chdir(savedCwd);
|
|
100
|
+
rmSync(dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
report();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preferences-git.test.ts — Validates git.isolation and git.merge_to_main preference fields.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
6
|
+
import { validatePreferences } from "../preferences.ts";
|
|
7
|
+
|
|
8
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
9
|
+
|
|
10
|
+
async function main(): Promise<void> {
|
|
11
|
+
console.log("\n=== git.isolation validation ===");
|
|
12
|
+
|
|
13
|
+
// Valid values
|
|
14
|
+
{
|
|
15
|
+
const { preferences, errors } = validatePreferences({ git: { isolation: "worktree" } });
|
|
16
|
+
assertEq(errors.length, 0, "isolation: worktree — no errors");
|
|
17
|
+
assertEq(preferences.git?.isolation, "worktree", "isolation: worktree — value preserved");
|
|
18
|
+
}
|
|
19
|
+
{
|
|
20
|
+
const { preferences, errors } = validatePreferences({ git: { isolation: "branch" } });
|
|
21
|
+
assertEq(errors.length, 0, "isolation: branch — no errors");
|
|
22
|
+
assertEq(preferences.git?.isolation, "branch", "isolation: branch — value preserved");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Invalid values
|
|
26
|
+
{
|
|
27
|
+
const { errors } = validatePreferences({ git: { isolation: "invalid" } });
|
|
28
|
+
assertTrue(errors.length > 0, "isolation: invalid — produces error");
|
|
29
|
+
assertTrue(errors[0].includes("isolation"), "isolation: invalid — error mentions isolation");
|
|
30
|
+
}
|
|
31
|
+
{
|
|
32
|
+
const { errors } = validatePreferences({ git: { isolation: 42 } });
|
|
33
|
+
assertTrue(errors.length > 0, "isolation: number — produces error");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Undefined passes through
|
|
37
|
+
{
|
|
38
|
+
const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
|
|
39
|
+
assertEq(errors.length, 0, "isolation: undefined — no errors");
|
|
40
|
+
assertEq(preferences.git?.isolation, undefined, "isolation: undefined — not set");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log("\n=== git.merge_to_main validation ===");
|
|
44
|
+
|
|
45
|
+
// Valid values
|
|
46
|
+
{
|
|
47
|
+
const { preferences, errors } = validatePreferences({ git: { merge_to_main: "milestone" } });
|
|
48
|
+
assertEq(errors.length, 0, "merge_to_main: milestone — no errors");
|
|
49
|
+
assertEq(preferences.git?.merge_to_main, "milestone", "merge_to_main: milestone — value preserved");
|
|
50
|
+
}
|
|
51
|
+
{
|
|
52
|
+
const { preferences, errors } = validatePreferences({ git: { merge_to_main: "slice" } });
|
|
53
|
+
assertEq(errors.length, 0, "merge_to_main: slice — no errors");
|
|
54
|
+
assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main: slice — value preserved");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Invalid values
|
|
58
|
+
{
|
|
59
|
+
const { errors } = validatePreferences({ git: { merge_to_main: "invalid" } });
|
|
60
|
+
assertTrue(errors.length > 0, "merge_to_main: invalid — produces error");
|
|
61
|
+
assertTrue(errors[0].includes("merge_to_main"), "merge_to_main: invalid — error mentions merge_to_main");
|
|
62
|
+
}
|
|
63
|
+
{
|
|
64
|
+
const { errors } = validatePreferences({ git: { merge_to_main: false } });
|
|
65
|
+
assertTrue(errors.length > 0, "merge_to_main: boolean — produces error");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Undefined passes through
|
|
69
|
+
{
|
|
70
|
+
const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
|
|
71
|
+
assertEq(errors.length, 0, "merge_to_main: undefined — no errors");
|
|
72
|
+
assertEq(preferences.git?.merge_to_main, undefined, "merge_to_main: undefined — not set");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log("\n=== both fields together ===");
|
|
76
|
+
{
|
|
77
|
+
const { preferences, errors } = validatePreferences({
|
|
78
|
+
git: { isolation: "worktree", merge_to_main: "slice" },
|
|
79
|
+
});
|
|
80
|
+
assertEq(errors.length, 0, "both fields valid — no errors");
|
|
81
|
+
assertEq(preferences.git?.isolation, "worktree", "isolation preserved");
|
|
82
|
+
assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main preserved");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
report();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main();
|