@aaroncql/pim-agent 0.0.1 → 0.1.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.
Files changed (60) hide show
  1. package/README.md +19 -8
  2. package/bin/pim.ts +55 -3
  3. package/package.json +20 -5
  4. package/src/extensions/_init/index.ts +3 -2
  5. package/src/extensions/bash/capture.test.ts +0 -126
  6. package/src/extensions/bash/format.test.ts +0 -240
  7. package/src/extensions/bash/run.test.ts +0 -262
  8. package/src/extensions/command-picker/ranker.test.ts +0 -46
  9. package/src/extensions/edit/edit.test.ts +0 -285
  10. package/src/extensions/file-picker/catalog.test.ts +0 -263
  11. package/src/extensions/file-picker/index.test.ts +0 -168
  12. package/src/extensions/file-picker/ranker.test.ts +0 -94
  13. package/src/extensions/footer/git.test.ts +0 -76
  14. package/src/extensions/footer/index.test.ts +0 -161
  15. package/src/extensions/footer/segments.test.ts +0 -164
  16. package/src/extensions/glob/glob.test.ts +0 -171
  17. package/src/extensions/glob/index.test.ts +0 -68
  18. package/src/extensions/glob/render.test.ts +0 -126
  19. package/src/extensions/grep/grep.test.ts +0 -387
  20. package/src/extensions/grep/index.test.ts +0 -68
  21. package/src/extensions/grep/render.test.ts +0 -269
  22. package/src/extensions/read/read.test.ts +0 -177
  23. package/src/extensions/read/render.test.ts +0 -61
  24. package/src/extensions/subagent/index.test.ts +0 -44
  25. package/src/extensions/subagent/render.test.ts +0 -292
  26. package/src/extensions/subagent/subagent.test.ts +0 -315
  27. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  28. package/src/extensions/todo/index.test.ts +0 -244
  29. package/src/extensions/todo/render.test.ts +0 -180
  30. package/src/extensions/todo/todo.test.ts +0 -222
  31. package/src/extensions/tps/index.test.ts +0 -254
  32. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  33. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  34. package/src/extensions/web-fetch/render.test.ts +0 -56
  35. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  36. package/src/extensions/web-search/render.test.ts +0 -21
  37. package/src/extensions/web-search/search.test.ts +0 -53
  38. package/src/extensions/working-indicator/index.test.ts +0 -21
  39. package/src/extensions/write/render.test.ts +0 -64
  40. package/src/extensions/write/write.test.ts +0 -108
  41. package/src/shared/DiffLines.test.ts +0 -193
  42. package/src/shared/DiffRenderer.test.ts +0 -206
  43. package/src/shared/EditMatcher.test.ts +0 -123
  44. package/src/shared/FileScanner.test.ts +0 -158
  45. package/src/shared/FuzzyMatcher.test.ts +0 -114
  46. package/src/shared/GitignoreFilter.test.ts +0 -64
  47. package/src/shared/Lines.test.ts +0 -25
  48. package/src/shared/McpClient.test.ts +0 -235
  49. package/src/shared/OutputBudget.test.ts +0 -99
  50. package/src/shared/Paths.test.ts +0 -51
  51. package/src/shared/PimSettings.test.ts +0 -90
  52. package/src/shared/Renderer.test.ts +0 -190
  53. package/src/shared/SpillCache.test.ts +0 -94
  54. package/src/shared/Tools.test.ts +0 -392
  55. package/src/telegram/Config.test.ts +0 -275
  56. package/src/telegram/Markdown.test.ts +0 -143
  57. package/src/telegram/Renderer.test.ts +0 -216
  58. package/src/telegram/SessionRegistry.test.ts +0 -89
  59. package/src/telegram/TaskScheduler.test.ts +0 -278
  60. package/src/telegram/TaskTool.test.ts +0 -179
@@ -1,263 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import {
6
- loadAbsolute,
7
- loadRelative,
8
- type GitSpawnResult,
9
- type GitSpawner,
10
- } from "./catalog";
11
-
12
- let workspace: string;
13
-
14
- beforeEach(async () => {
15
- workspace = await mkdtemp(join(tmpdir(), "pim-file-catalog-"));
16
- });
17
-
18
- afterEach(async () => {
19
- await rm(workspace, { force: true, recursive: true });
20
- });
21
-
22
- const failingSpawner: GitSpawner = async () => ({
23
- exitCode: 1,
24
- stdout: "",
25
- });
26
-
27
- const succeedingSpawner = (files: readonly string[]): GitSpawner => {
28
- return async () => ({
29
- exitCode: 0,
30
- stdout: files.join("\n"),
31
- });
32
- };
33
-
34
- describe("loadRelative — fast path (git ls-files)", () => {
35
- test("returns the listed files as forward-slash relative paths", async () => {
36
- const candidates = await loadRelative({
37
- root: workspace,
38
- gitSpawner: succeedingSpawner([
39
- "src/foo.ts",
40
- "src/bar/baz.ts",
41
- "README.md",
42
- ]),
43
- });
44
-
45
- const paths = candidates.map((c) => c.displayPath);
46
- expect(paths).toContain("README.md");
47
- expect(paths).toContain("src/foo.ts");
48
- expect(paths).toContain("src/bar/baz.ts");
49
- for (const candidate of candidates) {
50
- expect(candidate.isDirectory).toBe(false);
51
- expect(candidate.insertPath).toBe(candidate.displayPath);
52
- expect(candidate.matchHaystack).toBe(candidate.displayPath);
53
- }
54
- });
55
-
56
- test("sorts ascending by relative path", async () => {
57
- const candidates = await loadRelative({
58
- root: workspace,
59
- gitSpawner: succeedingSpawner(["zeta.ts", "alpha.ts", "mu.ts"]),
60
- });
61
-
62
- expect(candidates.map((c) => c.displayPath)).toEqual([
63
- "alpha.ts",
64
- "mu.ts",
65
- "zeta.ts",
66
- ]);
67
- });
68
-
69
- test("limit truncates after sort", async () => {
70
- const candidates = await loadRelative({
71
- root: workspace,
72
- gitSpawner: succeedingSpawner(["c.ts", "a.ts", "b.ts", "d.ts"]),
73
- limit: 2,
74
- });
75
-
76
- expect(candidates.map((c) => c.displayPath)).toEqual(["a.ts", "b.ts"]);
77
- });
78
- });
79
-
80
- describe("loadRelative — fallback (Bun.Glob)", () => {
81
- test("walks the directory and skips gitignored entries", async () => {
82
- await writeFile(join(workspace, ".gitignore"), "ignored.txt\n");
83
- await writeFile(join(workspace, "kept.ts"), "kept");
84
- await writeFile(join(workspace, "ignored.txt"), "ignored");
85
- await mkdir(join(workspace, "nested"), { recursive: true });
86
- await writeFile(join(workspace, "nested", "deep.ts"), "deep");
87
- await mkdir(join(workspace, "node_modules"), { recursive: true });
88
- await writeFile(join(workspace, "node_modules", "junk.js"), "junk");
89
-
90
- const candidates = await loadRelative({
91
- root: workspace,
92
- gitSpawner: failingSpawner,
93
- });
94
-
95
- const paths = candidates.map((c) => c.displayPath);
96
- expect(paths).toContain("kept.ts");
97
- expect(paths).toContain("nested/deep.ts");
98
- expect(paths).not.toContain("ignored.txt");
99
- expect(paths.some((p) => p.includes("node_modules"))).toBe(false);
100
- });
101
-
102
- test("empty workspace produces an empty list", async () => {
103
- const candidates = await loadRelative({
104
- root: workspace,
105
- gitSpawner: failingSpawner,
106
- });
107
-
108
- expect(candidates).toEqual([]);
109
- });
110
- });
111
-
112
- describe("loadRelative — coalescing", () => {
113
- test("two concurrent calls with the same root share one spawn", async () => {
114
- let invocations = 0;
115
- const spawner: GitSpawner = async () => {
116
- invocations += 1;
117
- await new Promise((resolveSleep) => setTimeout(resolveSleep, 10));
118
- return { exitCode: 0, stdout: "x.ts\n" } satisfies GitSpawnResult;
119
- };
120
-
121
- const [a, b] = await Promise.all([
122
- loadRelative({ root: workspace, gitSpawner: spawner }),
123
- loadRelative({ root: workspace, gitSpawner: spawner }),
124
- ]);
125
-
126
- expect(invocations).toBe(1);
127
- expect(a).toBe(b);
128
- });
129
-
130
- test("subsequent call after settle re-runs the spawner", async () => {
131
- let invocations = 0;
132
- const spawner: GitSpawner = async () => {
133
- invocations += 1;
134
- return { exitCode: 0, stdout: "x.ts\n" };
135
- };
136
-
137
- await loadRelative({ root: workspace, gitSpawner: spawner });
138
- await loadRelative({ root: workspace, gitSpawner: spawner });
139
-
140
- expect(invocations).toBe(2);
141
- });
142
- });
143
-
144
- describe("loadAbsolute", () => {
145
- test("anchor at exact directory lists its children", async () => {
146
- await mkdir(join(workspace, "sub"), { recursive: true });
147
- await writeFile(join(workspace, "sub", "alpha.ts"), "a");
148
- await writeFile(join(workspace, "sub", "beta.ts"), "b");
149
- await mkdir(join(workspace, "sub", "child"), { recursive: true });
150
-
151
- const result = await loadAbsolute({
152
- query: `${join(workspace, "sub")}/`,
153
- });
154
-
155
- expect(result.residualQuery).toBe("");
156
- const names = result.candidates.map((c) => c.matchHaystack);
157
- expect(names).toEqual(["child", "alpha.ts", "beta.ts"]);
158
- const child = result.candidates.find((c) => c.matchHaystack === "child");
159
- expect(child?.isDirectory).toBe(true);
160
- const alpha = result.candidates.find((c) => c.matchHaystack === "alpha.ts");
161
- expect(alpha?.isDirectory).toBe(false);
162
- expect(alpha?.insertPath).toBe(join(workspace, "sub", "alpha.ts"));
163
- });
164
-
165
- test("anchor at deepest existing prefix produces residual", async () => {
166
- await mkdir(join(workspace, "sub"), { recursive: true });
167
- await writeFile(join(workspace, "sub", "partial.ts"), "p");
168
-
169
- const result = await loadAbsolute({
170
- query: `${join(workspace, "sub")}/parti`,
171
- });
172
-
173
- expect(result.residualQuery).toBe("parti");
174
- expect(result.candidates.map((c) => c.matchHaystack)).toEqual([
175
- "partial.ts",
176
- ]);
177
- });
178
-
179
- test("walks past a file that is an exact prefix to the parent directory", async () => {
180
- await mkdir(join(workspace, "etc"), { recursive: true });
181
- await writeFile(join(workspace, "etc", "passwd"), "fake");
182
-
183
- const result = await loadAbsolute({
184
- query: `${join(workspace, "etc", "passwd")}/foo`,
185
- });
186
-
187
- expect(result.residualQuery).toBe("passwd/foo");
188
- expect(result.candidates.map((c) => c.matchHaystack)).toEqual(["passwd"]);
189
- });
190
-
191
- test("normalizes .. segments before walking", async () => {
192
- await mkdir(join(workspace, "alpha"), { recursive: true });
193
- await writeFile(join(workspace, "alpha", "x.ts"), "x");
194
- await mkdir(join(workspace, "beta"), { recursive: true });
195
-
196
- const result = await loadAbsolute({
197
- query: `${join(workspace, "beta")}/../alpha/`,
198
- });
199
-
200
- expect(result.residualQuery).toBe("");
201
- expect(result.candidates.map((c) => c.matchHaystack)).toEqual(["x.ts"]);
202
- });
203
-
204
- test("nonexistent suffix walks up to the deepest existing ancestor", async () => {
205
- await mkdir(join(workspace, "real"), { recursive: true });
206
- await writeFile(join(workspace, "real", "kept.ts"), "k");
207
-
208
- const result = await loadAbsolute({
209
- query: `${join(workspace, "real")}/nope/foo`,
210
- });
211
-
212
- expect(result.residualQuery).toBe("nope/foo");
213
- expect(result.candidates.map((c) => c.matchHaystack)).toEqual(["kept.ts"]);
214
- });
215
-
216
- test("symlinked directory is treated as a directory anchor", async () => {
217
- await mkdir(join(workspace, "real"), { recursive: true });
218
- await writeFile(join(workspace, "real", "x.ts"), "x");
219
- await symlink(join(workspace, "real"), join(workspace, "link"));
220
-
221
- const result = await loadAbsolute({
222
- query: `${join(workspace, "link")}/`,
223
- });
224
-
225
- expect(result.candidates.map((c) => c.matchHaystack)).toEqual(["x.ts"]);
226
- });
227
-
228
- test("dotfiles hidden unless residual starts with a dot", async () => {
229
- await mkdir(join(workspace, "sub"), { recursive: true });
230
- await writeFile(join(workspace, "sub", "visible.ts"), "v");
231
- await writeFile(join(workspace, "sub", ".hidden"), "h");
232
-
233
- const without = await loadAbsolute({
234
- query: `${join(workspace, "sub")}/`,
235
- });
236
- expect(without.candidates.map((c) => c.matchHaystack)).toEqual([
237
- "visible.ts",
238
- ]);
239
-
240
- const withDot = await loadAbsolute({
241
- query: `${join(workspace, "sub")}/.h`,
242
- });
243
- expect(withDot.candidates.map((c) => c.matchHaystack)).toContain(".hidden");
244
- expect(withDot.residualQuery).toBe(".h");
245
- });
246
-
247
- test("directories sort before files within the same anchor", async () => {
248
- await mkdir(join(workspace, "sub"), { recursive: true });
249
- await writeFile(join(workspace, "sub", "z-file.ts"), "z");
250
- await mkdir(join(workspace, "sub", "a-dir"), { recursive: true });
251
- await writeFile(join(workspace, "sub", "a-file.ts"), "a");
252
-
253
- const result = await loadAbsolute({
254
- query: `${join(workspace, "sub")}/`,
255
- });
256
-
257
- expect(result.candidates.map((c) => c.matchHaystack)).toEqual([
258
- "a-dir",
259
- "a-file.ts",
260
- "z-file.ts",
261
- ]);
262
- });
263
- });
@@ -1,168 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { AutocompleteProvider } from "@earendil-works/pi-tui";
6
- import type { FileCandidate } from "./catalog";
7
- import { createFilePickerProviderFactory } from "./index";
8
-
9
- const file = (path: string): FileCandidate => ({
10
- insertPath: path,
11
- displayPath: path,
12
- matchHaystack: path,
13
- isDirectory: false,
14
- });
15
-
16
- const currentProvider: AutocompleteProvider = {
17
- async getSuggestions() {
18
- return null;
19
- },
20
-
21
- applyCompletion(lines, cursorLine, cursorCol) {
22
- return { lines, cursorLine, cursorCol };
23
- },
24
- };
25
-
26
- const autocompleteOptions = (): { readonly signal: AbortSignal } => ({
27
- signal: new AbortController().signal,
28
- });
29
-
30
- const flushPromises = async (): Promise<void> => {
31
- await Promise.resolve();
32
- await Promise.resolve();
33
- };
34
-
35
- test("relative @ autocomplete refreshes in the background after using the session cache", async () => {
36
- let catalog: readonly FileCandidate[] = [file("old.ts")];
37
- let loads = 0;
38
- const factory = createFilePickerProviderFactory({
39
- loadRelativeCatalog: async () => {
40
- loads += 1;
41
- return catalog;
42
- },
43
- });
44
- const provider = factory(currentProvider);
45
-
46
- await flushPromises();
47
- expect(loads).toBe(1);
48
-
49
- catalog = [file("new.ts")];
50
- const stale = await provider.getSuggestions(
51
- ["@new"],
52
- 0,
53
- 4,
54
- autocompleteOptions()
55
- );
56
-
57
- expect(stale).toBeNull();
58
- expect(loads).toBe(2);
59
-
60
- await flushPromises();
61
- const fresh = await provider.getSuggestions(
62
- ["@new"],
63
- 0,
64
- 4,
65
- autocompleteOptions()
66
- );
67
-
68
- expect(fresh?.items.map((item) => item.value)).toContain("@new.ts");
69
- });
70
-
71
- test("relative catalog cache survives provider rebuilds", async () => {
72
- let catalog: readonly FileCandidate[] = [file("old.ts")];
73
- const factory = createFilePickerProviderFactory({
74
- loadRelativeCatalog: async () => catalog,
75
- });
76
- const firstProvider = factory(currentProvider);
77
-
78
- await flushPromises();
79
- catalog = [file("new.ts")];
80
- await firstProvider.getSuggestions(["@new"], 0, 4, autocompleteOptions());
81
- await flushPromises();
82
-
83
- const rebuiltProvider = factory(currentProvider);
84
- const fresh = await rebuiltProvider.getSuggestions(
85
- ["@new"],
86
- 0,
87
- 4,
88
- autocompleteOptions()
89
- );
90
-
91
- expect(fresh?.items.map((item) => item.value)).toContain("@new.ts");
92
- });
93
-
94
- test("relative catalog refreshes are coalesced", async () => {
95
- let resolveLoad: ((catalog: readonly FileCandidate[]) => void) | undefined;
96
- let loads = 0;
97
- const factory = createFilePickerProviderFactory({
98
- loadRelativeCatalog: () => {
99
- loads += 1;
100
- return new Promise((resolve) => {
101
- resolveLoad = resolve;
102
- });
103
- },
104
- });
105
- const provider = factory(currentProvider);
106
-
107
- await provider.getSuggestions(["@a"], 0, 2, autocompleteOptions());
108
- await provider.getSuggestions(["@ab"], 0, 3, autocompleteOptions());
109
-
110
- expect(loads).toBe(1);
111
- resolveLoad?.([file("ab.ts")]);
112
- await flushPromises();
113
- });
114
-
115
- test("refresh failure preserves the last good relative cache", async () => {
116
- let shouldFail = false;
117
- const factory = createFilePickerProviderFactory({
118
- loadRelativeCatalog: async () => {
119
- if (shouldFail) {
120
- throw new Error("boom");
121
- }
122
- return [file("old.ts")];
123
- },
124
- });
125
- const provider = factory(currentProvider);
126
-
127
- await flushPromises();
128
- shouldFail = true;
129
- await provider.getSuggestions(["@old"], 0, 4, autocompleteOptions());
130
- await flushPromises();
131
-
132
- const result = await provider.getSuggestions(
133
- ["@old"],
134
- 0,
135
- 4,
136
- autocompleteOptions()
137
- );
138
-
139
- expect(result?.items.map((item) => item.value)).toContain("@old.ts");
140
- });
141
-
142
- test("absolute @ autocomplete also refreshes the relative catalog", async () => {
143
- const workspace = await mkdtemp(join(tmpdir(), "pim-file-picker-absolute-"));
144
- try {
145
- let loads = 0;
146
- const factory = createFilePickerProviderFactory({
147
- loadRelativeCatalog: async () => {
148
- loads += 1;
149
- return [file("old.ts")];
150
- },
151
- });
152
- const provider = factory(currentProvider);
153
-
154
- await flushPromises();
155
- expect(loads).toBe(1);
156
-
157
- await provider.getSuggestions(
158
- [`@${workspace}`],
159
- 0,
160
- workspace.length + 1,
161
- autocompleteOptions()
162
- );
163
-
164
- expect(loads).toBe(2);
165
- } finally {
166
- await rm(workspace, { force: true, recursive: true });
167
- }
168
- });
@@ -1,94 +0,0 @@
1
- import { afterEach, beforeEach, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { FileCandidate } from "./catalog";
6
- import { rank } from "./ranker";
7
-
8
- let workspace: string;
9
-
10
- beforeEach(async () => {
11
- workspace = await mkdtemp(join(tmpdir(), "pim-file-ranker-"));
12
- });
13
-
14
- afterEach(async () => {
15
- await rm(workspace, { force: true, recursive: true });
16
- });
17
-
18
- const file = (path: string): FileCandidate => ({
19
- insertPath: path,
20
- displayPath: path,
21
- matchHaystack: path,
22
- isDirectory: false,
23
- });
24
-
25
- test("relative query with no cache returns undefined (fallback)", async () => {
26
- const result = await rank("foo", { cachedRelative: undefined });
27
-
28
- expect(result).toBeUndefined();
29
- });
30
-
31
- test("relative query with cache fuzzy-ranks the cached catalog", async () => {
32
- const cachedRelative: readonly FileCandidate[] = [
33
- file("src/util/log.ts"),
34
- file("src/util/clock.ts"),
35
- file("README.md"),
36
- ];
37
-
38
- const result = await rank("clk", { cachedRelative });
39
-
40
- expect(result).toBeDefined();
41
- expect(result?.[0]?.value).toBe("src/util/clock.ts");
42
- });
43
-
44
- test("empty relative query returns cached catalog in given order", async () => {
45
- const cachedRelative: readonly FileCandidate[] = [
46
- file("a.ts"),
47
- file("b.ts"),
48
- file("c.ts"),
49
- ];
50
-
51
- const result = await rank("", {
52
- cachedRelative,
53
- limit: 2,
54
- });
55
-
56
- expect(result?.map((item) => item.value)).toEqual(["a.ts", "b.ts"]);
57
- });
58
-
59
- test("absolute query lists the resolved directory", async () => {
60
- await mkdir(join(workspace, "sub"), { recursive: true });
61
- await writeFile(join(workspace, "sub", "alpha.ts"), "a");
62
- await writeFile(join(workspace, "sub", "beta.ts"), "b");
63
-
64
- const result = await rank(`${join(workspace, "sub")}/`, {
65
- cachedRelative: undefined,
66
- });
67
-
68
- expect(result?.map((item) => item.label)).toEqual(["alpha.ts", "beta.ts"]);
69
- });
70
-
71
- test("absolute query with residual fuzzy-ranks within the directory", async () => {
72
- await mkdir(join(workspace, "sub"), { recursive: true });
73
- await writeFile(join(workspace, "sub", "alpha.ts"), "a");
74
- await writeFile(join(workspace, "sub", "beta.ts"), "b");
75
-
76
- const result = await rank(`${join(workspace, "sub")}/be`, {
77
- cachedRelative: undefined,
78
- });
79
-
80
- expect(result?.[0]?.label).toBe("beta.ts");
81
- });
82
-
83
- test("directory candidates carry trailing slash in value and label", async () => {
84
- await mkdir(join(workspace, "sub"), { recursive: true });
85
- await mkdir(join(workspace, "sub", "child"), { recursive: true });
86
-
87
- const result = await rank(`${join(workspace, "sub")}/`, {
88
- cachedRelative: undefined,
89
- });
90
-
91
- const child = result?.find((item) => item.label === "child/");
92
- expect(child).toBeDefined();
93
- expect(child?.value.endsWith("/child/")).toBe(true);
94
- });
@@ -1,76 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterAll, describe, expect, test } from "bun:test";
5
- import { EMPTY_GIT, fetchGitStatus, parseGitStatus } from "./git";
6
-
7
- const tempRoots: string[] = [];
8
-
9
- const tempRoot = async (): Promise<string> => {
10
- const root = await mkdtemp(join(tmpdir(), "pim-footer-git-"));
11
- tempRoots.push(root);
12
- return root;
13
- };
14
-
15
- afterAll(async () => {
16
- await Promise.all(
17
- tempRoots.map((root) => rm(root, { force: true, recursive: true }))
18
- );
19
- });
20
-
21
- describe("parseGitStatus", () => {
22
- test("parses clean branch status", () => {
23
- expect(
24
- parseGitStatus(
25
- [
26
- "# branch.oid 123456",
27
- "# branch.head main",
28
- "# branch.upstream origin/main",
29
- "# branch.ab +0 -0",
30
- ].join("\n")
31
- )
32
- ).toEqual({
33
- branch: "main",
34
- dirty: false,
35
- ahead: 0,
36
- behind: 0,
37
- });
38
- });
39
-
40
- test("parses dirty state and ahead/behind counts", () => {
41
- expect(
42
- parseGitStatus(
43
- [
44
- "# branch.oid 123456",
45
- "# branch.head feature/footer",
46
- "# branch.upstream origin/feature/footer",
47
- "# branch.ab +12 -3",
48
- "1 .M N... 100644 100644 100644 abc abc src/file.ts",
49
- "? scratch.txt",
50
- ].join("\n")
51
- )
52
- ).toEqual({
53
- branch: "feature/footer",
54
- dirty: true,
55
- ahead: 12,
56
- behind: 3,
57
- });
58
- });
59
-
60
- test("labels detached heads explicitly", () => {
61
- expect(parseGitStatus("# branch.head (detached)\n")).toEqual({
62
- branch: "detached",
63
- dirty: false,
64
- ahead: 0,
65
- behind: 0,
66
- });
67
- });
68
- });
69
-
70
- describe("fetchGitStatus", () => {
71
- test("returns empty git state outside a git repository", async () => {
72
- const root = await tempRoot();
73
-
74
- expect(await fetchGitStatus(root)).toEqual(EMPTY_GIT);
75
- });
76
- });