@aaroncql/pim-agent 0.0.1 → 0.2.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 (84) hide show
  1. package/README.md +94 -66
  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/apply-patch/coordinator.ts +49 -0
  6. package/src/extensions/apply-patch/executor.ts +566 -0
  7. package/src/extensions/apply-patch/index.ts +74 -0
  8. package/src/extensions/apply-patch/matcher.ts +66 -0
  9. package/src/extensions/apply-patch/model.ts +34 -0
  10. package/src/extensions/apply-patch/parser.ts +381 -0
  11. package/src/extensions/apply-patch/render.ts +261 -0
  12. package/src/extensions/apply-patch/schema.ts +43 -0
  13. package/src/extensions/apply-patch/types.ts +30 -0
  14. package/src/extensions/bash/index.ts +3 -3
  15. package/src/extensions/edit/index.ts +2 -1
  16. package/src/extensions/glob/index.ts +3 -1
  17. package/src/extensions/glob/schema.ts +2 -1
  18. package/src/extensions/grep/index.ts +3 -1
  19. package/src/extensions/grep/render.ts +18 -4
  20. package/src/extensions/grep/schema.ts +1 -1
  21. package/src/extensions/read/index.ts +36 -9
  22. package/src/extensions/read/render.ts +31 -3
  23. package/src/extensions/subagent/index.ts +4 -1
  24. package/src/extensions/todo/index.ts +4 -3
  25. package/src/extensions/web-search/index.ts +2 -1
  26. package/src/extensions/write/index.ts +2 -1
  27. package/src/shared/PatchSummary.ts +82 -0
  28. package/src/telegram/Renderer.ts +190 -4
  29. package/src/extensions/bash/capture.test.ts +0 -126
  30. package/src/extensions/bash/format.test.ts +0 -240
  31. package/src/extensions/bash/run.test.ts +0 -262
  32. package/src/extensions/command-picker/ranker.test.ts +0 -46
  33. package/src/extensions/edit/edit.test.ts +0 -285
  34. package/src/extensions/file-picker/catalog.test.ts +0 -263
  35. package/src/extensions/file-picker/index.test.ts +0 -168
  36. package/src/extensions/file-picker/ranker.test.ts +0 -94
  37. package/src/extensions/footer/git.test.ts +0 -76
  38. package/src/extensions/footer/index.test.ts +0 -161
  39. package/src/extensions/footer/segments.test.ts +0 -164
  40. package/src/extensions/glob/glob.test.ts +0 -171
  41. package/src/extensions/glob/index.test.ts +0 -68
  42. package/src/extensions/glob/render.test.ts +0 -126
  43. package/src/extensions/grep/grep.test.ts +0 -387
  44. package/src/extensions/grep/index.test.ts +0 -68
  45. package/src/extensions/grep/render.test.ts +0 -269
  46. package/src/extensions/read/read.test.ts +0 -177
  47. package/src/extensions/read/render.test.ts +0 -61
  48. package/src/extensions/subagent/index.test.ts +0 -44
  49. package/src/extensions/subagent/render.test.ts +0 -292
  50. package/src/extensions/subagent/subagent.test.ts +0 -315
  51. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  52. package/src/extensions/todo/index.test.ts +0 -244
  53. package/src/extensions/todo/render.test.ts +0 -180
  54. package/src/extensions/todo/todo.test.ts +0 -222
  55. package/src/extensions/tps/index.test.ts +0 -254
  56. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  57. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  58. package/src/extensions/web-fetch/render.test.ts +0 -56
  59. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  60. package/src/extensions/web-search/render.test.ts +0 -21
  61. package/src/extensions/web-search/search.test.ts +0 -53
  62. package/src/extensions/working-indicator/index.test.ts +0 -21
  63. package/src/extensions/write/render.test.ts +0 -64
  64. package/src/extensions/write/write.test.ts +0 -108
  65. package/src/shared/DiffLines.test.ts +0 -193
  66. package/src/shared/DiffRenderer.test.ts +0 -206
  67. package/src/shared/EditMatcher.test.ts +0 -123
  68. package/src/shared/FileScanner.test.ts +0 -158
  69. package/src/shared/FuzzyMatcher.test.ts +0 -114
  70. package/src/shared/GitignoreFilter.test.ts +0 -64
  71. package/src/shared/Lines.test.ts +0 -25
  72. package/src/shared/McpClient.test.ts +0 -235
  73. package/src/shared/OutputBudget.test.ts +0 -99
  74. package/src/shared/Paths.test.ts +0 -51
  75. package/src/shared/PimSettings.test.ts +0 -90
  76. package/src/shared/Renderer.test.ts +0 -190
  77. package/src/shared/SpillCache.test.ts +0 -94
  78. package/src/shared/Tools.test.ts +0 -392
  79. package/src/telegram/Config.test.ts +0 -275
  80. package/src/telegram/Markdown.test.ts +0 -143
  81. package/src/telegram/Renderer.test.ts +0 -216
  82. package/src/telegram/SessionRegistry.test.ts +0 -89
  83. package/src/telegram/TaskScheduler.test.ts +0 -278
  84. 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
- });