wdyt 0.1.13 → 0.1.15
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/package.json +1 -1
- package/src/commands/chat.ts +65 -3
- package/src/context/hints.test.ts +135 -0
- package/src/context/hints.ts +264 -0
- package/src/context/index.ts +48 -0
- package/src/context/references.test.ts +341 -0
- package/src/context/references.ts +232 -0
- package/src/context/rereview.test.ts +135 -0
- package/src/context/rereview.ts +204 -0
- package/src/context/symbols.test.ts +550 -0
- package/src/context/symbols.ts +234 -0
- package/src/flow/index.ts +18 -0
- package/src/flow/specs.test.ts +260 -0
- package/src/flow/specs.ts +255 -0
- package/src/git/diff.test.ts +311 -0
- package/src/git/diff.ts +205 -0
- package/src/integration.test.ts +538 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for reference finding module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
findReferences,
|
|
8
|
+
findReferencesForSymbols,
|
|
9
|
+
formatReferences,
|
|
10
|
+
isGitRepository,
|
|
11
|
+
type Reference,
|
|
12
|
+
} from "./references";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { mkdir, writeFile, rm } from "fs/promises";
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
|
|
17
|
+
// Test fixtures directory - using a git-initialized temp directory
|
|
18
|
+
const FIXTURES_DIR = join(import.meta.dir, "..", "..", ".test-fixtures-refs");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper to run git commands
|
|
22
|
+
*/
|
|
23
|
+
async function git(args: string[], cwd: string): Promise<void> {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const process = spawn("git", args, { cwd, stdio: "ignore" });
|
|
26
|
+
process.on("close", (code) => {
|
|
27
|
+
if (code === 0) resolve();
|
|
28
|
+
else reject(new Error(`git ${args.join(" ")} failed with code ${code}`));
|
|
29
|
+
});
|
|
30
|
+
process.on("error", reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("Reference finding", () => {
|
|
35
|
+
// Set up a temp git repo with test files
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
await mkdir(FIXTURES_DIR, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// Initialize git repo
|
|
40
|
+
await git(["init"], FIXTURES_DIR);
|
|
41
|
+
await git(["config", "user.email", "test@test.com"], FIXTURES_DIR);
|
|
42
|
+
await git(["config", "user.name", "Test User"], FIXTURES_DIR);
|
|
43
|
+
|
|
44
|
+
// Create test files
|
|
45
|
+
await writeFile(
|
|
46
|
+
join(FIXTURES_DIR, "utils.ts"),
|
|
47
|
+
`
|
|
48
|
+
export function helper() {
|
|
49
|
+
return "helper";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function otherFunc() {
|
|
53
|
+
return helper();
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await writeFile(
|
|
59
|
+
join(FIXTURES_DIR, "main.ts"),
|
|
60
|
+
`
|
|
61
|
+
import { helper } from './utils';
|
|
62
|
+
|
|
63
|
+
function main() {
|
|
64
|
+
const result = helper();
|
|
65
|
+
console.log(result);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { main };
|
|
69
|
+
`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await writeFile(
|
|
73
|
+
join(FIXTURES_DIR, "test.ts"),
|
|
74
|
+
`
|
|
75
|
+
import { helper } from './utils';
|
|
76
|
+
|
|
77
|
+
test('helper works', () => {
|
|
78
|
+
expect(helper()).toBe('helper');
|
|
79
|
+
});
|
|
80
|
+
`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await writeFile(
|
|
84
|
+
join(FIXTURES_DIR, "noRefs.ts"),
|
|
85
|
+
`
|
|
86
|
+
export function uniqueFunction() {
|
|
87
|
+
return "unique";
|
|
88
|
+
}
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Stage and commit files
|
|
93
|
+
await git(["add", "."], FIXTURES_DIR);
|
|
94
|
+
await git(["commit", "-m", "Initial commit"], FIXTURES_DIR);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterAll(async () => {
|
|
98
|
+
await rm(FIXTURES_DIR, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("findReferences", () => {
|
|
102
|
+
test("finds references to a symbol", async () => {
|
|
103
|
+
const refs = await findReferences({
|
|
104
|
+
symbol: "helper",
|
|
105
|
+
cwd: FIXTURES_DIR,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(refs.length).toBeGreaterThan(0);
|
|
109
|
+
|
|
110
|
+
// Should find references in main.ts and test.ts
|
|
111
|
+
const files = refs.map((r) => r.file);
|
|
112
|
+
expect(files).toContain("main.ts");
|
|
113
|
+
expect(files).toContain("test.ts");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("excludes definition file from results", async () => {
|
|
117
|
+
const refs = await findReferences({
|
|
118
|
+
symbol: "helper",
|
|
119
|
+
definitionFile: "utils.ts",
|
|
120
|
+
cwd: FIXTURES_DIR,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Should not include utils.ts
|
|
124
|
+
const files = refs.map((r) => r.file);
|
|
125
|
+
expect(files).not.toContain("utils.ts");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns line numbers", async () => {
|
|
129
|
+
const refs = await findReferences({
|
|
130
|
+
symbol: "helper",
|
|
131
|
+
cwd: FIXTURES_DIR,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
for (const ref of refs) {
|
|
135
|
+
expect(typeof ref.line).toBe("number");
|
|
136
|
+
expect(ref.line).toBeGreaterThan(0);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("returns context for each reference", async () => {
|
|
141
|
+
const refs = await findReferences({
|
|
142
|
+
symbol: "helper",
|
|
143
|
+
cwd: FIXTURES_DIR,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
for (const ref of refs) {
|
|
147
|
+
expect(typeof ref.context).toBe("string");
|
|
148
|
+
expect(ref.context.length).toBeGreaterThan(0);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("respects limit option", async () => {
|
|
153
|
+
const refs = await findReferences({
|
|
154
|
+
symbol: "helper",
|
|
155
|
+
cwd: FIXTURES_DIR,
|
|
156
|
+
limit: 2,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(refs.length).toBeLessThanOrEqual(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("handles symbols with no references gracefully", async () => {
|
|
163
|
+
const refs = await findReferences({
|
|
164
|
+
symbol: "nonExistentSymbol12345",
|
|
165
|
+
cwd: FIXTURES_DIR,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(refs).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("handles file patterns option", async () => {
|
|
172
|
+
const refs = await findReferences({
|
|
173
|
+
symbol: "helper",
|
|
174
|
+
cwd: FIXTURES_DIR,
|
|
175
|
+
filePatterns: ["*.ts"],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(refs.length).toBeGreaterThan(0);
|
|
179
|
+
|
|
180
|
+
// All results should be .ts files
|
|
181
|
+
for (const ref of refs) {
|
|
182
|
+
expect(ref.file.endsWith(".ts")).toBe(true);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("uses word boundary matching", async () => {
|
|
187
|
+
// Create a file with similar but different symbol names
|
|
188
|
+
await writeFile(
|
|
189
|
+
join(FIXTURES_DIR, "partial.ts"),
|
|
190
|
+
`
|
|
191
|
+
const helperFunc = 1;
|
|
192
|
+
const myhelper = 2;
|
|
193
|
+
const helper = 3;
|
|
194
|
+
`
|
|
195
|
+
);
|
|
196
|
+
await git(["add", "partial.ts"], FIXTURES_DIR);
|
|
197
|
+
await git(["commit", "-m", "Add partial matches file"], FIXTURES_DIR);
|
|
198
|
+
|
|
199
|
+
const refs = await findReferences({
|
|
200
|
+
symbol: "helper",
|
|
201
|
+
cwd: FIXTURES_DIR,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Should only find exact "helper" matches, not "helperFunc" or "myhelper"
|
|
205
|
+
for (const ref of refs) {
|
|
206
|
+
if (ref.file === "partial.ts") {
|
|
207
|
+
// The context should contain "helper" as a whole word
|
|
208
|
+
expect(ref.context).toMatch(/\bhelper\b/);
|
|
209
|
+
// Should not be the helperFunc or myhelper lines
|
|
210
|
+
expect(ref.context).not.toMatch(/helperFunc/);
|
|
211
|
+
expect(ref.context).not.toMatch(/myhelper/);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("findReferencesForSymbols", () => {
|
|
218
|
+
test("finds references for multiple symbols", async () => {
|
|
219
|
+
const results = await findReferencesForSymbols(
|
|
220
|
+
["helper", "main"],
|
|
221
|
+
undefined,
|
|
222
|
+
FIXTURES_DIR
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(results.size).toBe(2);
|
|
226
|
+
expect(results.has("helper")).toBe(true);
|
|
227
|
+
expect(results.has("main")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("excludes definition file for all symbols", async () => {
|
|
231
|
+
const results = await findReferencesForSymbols(
|
|
232
|
+
["helper", "otherFunc"],
|
|
233
|
+
"utils.ts",
|
|
234
|
+
FIXTURES_DIR
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Neither should have utils.ts in results
|
|
238
|
+
for (const [, refs] of results) {
|
|
239
|
+
const files = refs.map((r) => r.file);
|
|
240
|
+
expect(files).not.toContain("utils.ts");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("respects per-symbol limit", async () => {
|
|
245
|
+
const results = await findReferencesForSymbols(
|
|
246
|
+
["helper"],
|
|
247
|
+
undefined,
|
|
248
|
+
FIXTURES_DIR,
|
|
249
|
+
1
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const helperRefs = results.get("helper") ?? [];
|
|
253
|
+
expect(helperRefs.length).toBeLessThanOrEqual(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("handles symbols with no references", async () => {
|
|
257
|
+
const results = await findReferencesForSymbols(
|
|
258
|
+
["helper", "nonExistent12345"],
|
|
259
|
+
undefined,
|
|
260
|
+
FIXTURES_DIR
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(results.get("nonExistent12345")).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("formatReferences", () => {
|
|
268
|
+
test("formats references as file:line:context", () => {
|
|
269
|
+
const refs: Reference[] = [
|
|
270
|
+
{ file: "main.ts", line: 4, context: "const result = helper();" },
|
|
271
|
+
{ file: "test.ts", line: 5, context: "expect(helper()).toBe('helper');" },
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const formatted = formatReferences(refs);
|
|
275
|
+
|
|
276
|
+
expect(formatted).toEqual([
|
|
277
|
+
"main.ts:4:const result = helper();",
|
|
278
|
+
"test.ts:5:expect(helper()).toBe('helper');",
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("handles empty array", () => {
|
|
283
|
+
expect(formatReferences([])).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("isGitRepository", () => {
|
|
288
|
+
test("returns true for git repository", async () => {
|
|
289
|
+
const result = await isGitRepository(FIXTURES_DIR);
|
|
290
|
+
expect(result).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("returns false for non-git directory", async () => {
|
|
294
|
+
const tempDir = join(FIXTURES_DIR, "non-git");
|
|
295
|
+
await mkdir(tempDir, { recursive: true });
|
|
296
|
+
|
|
297
|
+
// This dir is inside the git repo, but let's test with a clearly non-git path
|
|
298
|
+
// Actually, since it's inside the repo, it would still return true
|
|
299
|
+
// Let's use /tmp instead
|
|
300
|
+
const result = await isGitRepository("/tmp");
|
|
301
|
+
// /tmp might or might not be a git repo, but we can at least verify the function runs
|
|
302
|
+
expect(typeof result).toBe("boolean");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("Edge cases", () => {
|
|
307
|
+
test("handles special regex characters in symbol names", async () => {
|
|
308
|
+
// Git grep -w handles word boundaries, but let's make sure special chars work
|
|
309
|
+
// Most special chars wouldn't be valid symbol names anyway
|
|
310
|
+
const refs = await findReferences({
|
|
311
|
+
symbol: "test",
|
|
312
|
+
cwd: FIXTURES_DIR,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Should not throw
|
|
316
|
+
expect(Array.isArray(refs)).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("handles definition file with path prefix", async () => {
|
|
320
|
+
// Test that definition file exclusion works with various path formats
|
|
321
|
+
const refs = await findReferences({
|
|
322
|
+
symbol: "helper",
|
|
323
|
+
definitionFile: "./utils.ts",
|
|
324
|
+
cwd: FIXTURES_DIR,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const files = refs.map((r) => r.file);
|
|
328
|
+
expect(files).not.toContain("utils.ts");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("handles empty symbol", async () => {
|
|
332
|
+
const refs = await findReferences({
|
|
333
|
+
symbol: "",
|
|
334
|
+
cwd: FIXTURES_DIR,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Git grep with empty pattern returns error, we should handle gracefully
|
|
338
|
+
expect(Array.isArray(refs)).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference finding module for wdyt
|
|
3
|
+
*
|
|
4
|
+
* Finds where symbols are used across the codebase using git grep.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
|
|
9
|
+
/** Reference information for a symbol */
|
|
10
|
+
export interface Reference {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
context: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Options for finding references */
|
|
17
|
+
export interface FindReferencesOptions {
|
|
18
|
+
/** The symbol name to search for */
|
|
19
|
+
symbol: string;
|
|
20
|
+
/** File path where the symbol is defined (will be excluded from results) */
|
|
21
|
+
definitionFile?: string;
|
|
22
|
+
/** Working directory (defaults to cwd) */
|
|
23
|
+
cwd?: string;
|
|
24
|
+
/** Maximum number of results to return (default: 20) */
|
|
25
|
+
limit?: number;
|
|
26
|
+
/** File patterns to search (e.g., "*.ts", "*.js") */
|
|
27
|
+
filePatterns?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Default maximum results per symbol */
|
|
31
|
+
const DEFAULT_LIMIT = 20;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute git grep and return raw output
|
|
35
|
+
*/
|
|
36
|
+
async function execGitGrep(
|
|
37
|
+
args: string[],
|
|
38
|
+
cwd: string
|
|
39
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const process = spawn("git", ["grep", ...args], {
|
|
42
|
+
cwd,
|
|
43
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let stdout = "";
|
|
47
|
+
let stderr = "";
|
|
48
|
+
|
|
49
|
+
process.stdout.on("data", (data) => {
|
|
50
|
+
stdout += data.toString();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
process.stderr.on("data", (data) => {
|
|
54
|
+
stderr += data.toString();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
process.on("close", (exitCode) => {
|
|
58
|
+
resolve({ stdout, stderr, exitCode: exitCode ?? 0 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
process.on("error", () => {
|
|
62
|
+
resolve({ stdout: "", stderr: "Failed to spawn git grep", exitCode: 1 });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse git grep output line into Reference object
|
|
69
|
+
*
|
|
70
|
+
* git grep -n output format: file:line:context
|
|
71
|
+
*/
|
|
72
|
+
function parseGrepLine(line: string): Reference | null {
|
|
73
|
+
// Handle format: file:line:context
|
|
74
|
+
// Need to handle Windows paths (C:\...) and colons in context
|
|
75
|
+
const match = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
76
|
+
if (!match) return null;
|
|
77
|
+
|
|
78
|
+
const [, file, lineStr, context] = match;
|
|
79
|
+
const lineNum = parseInt(lineStr, 10);
|
|
80
|
+
|
|
81
|
+
if (isNaN(lineNum)) return null;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
file,
|
|
85
|
+
line: lineNum,
|
|
86
|
+
context: context.trim(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize file path for comparison
|
|
92
|
+
*/
|
|
93
|
+
function normalizePath(filePath: string): string {
|
|
94
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Find references to a symbol across the codebase using git grep
|
|
99
|
+
*
|
|
100
|
+
* @param options - Search options
|
|
101
|
+
* @returns Array of references found
|
|
102
|
+
*/
|
|
103
|
+
export async function findReferences(
|
|
104
|
+
options: FindReferencesOptions
|
|
105
|
+
): Promise<Reference[]> {
|
|
106
|
+
const {
|
|
107
|
+
symbol,
|
|
108
|
+
definitionFile,
|
|
109
|
+
cwd = process.cwd(),
|
|
110
|
+
limit = DEFAULT_LIMIT,
|
|
111
|
+
filePatterns,
|
|
112
|
+
} = options;
|
|
113
|
+
|
|
114
|
+
// Build git grep arguments
|
|
115
|
+
// -w: word boundary matching (whole word only)
|
|
116
|
+
// -n: show line numbers
|
|
117
|
+
const args: string[] = ["-w", "-n", symbol];
|
|
118
|
+
|
|
119
|
+
// Add file patterns if specified
|
|
120
|
+
if (filePatterns && filePatterns.length > 0) {
|
|
121
|
+
args.push("--");
|
|
122
|
+
args.push(...filePatterns);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { stdout, exitCode } = await execGitGrep(args, cwd);
|
|
126
|
+
|
|
127
|
+
// Exit code 1 means no matches found (not an error)
|
|
128
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Parse output lines
|
|
133
|
+
const lines = stdout.split("\n").filter((line) => line.trim() !== "");
|
|
134
|
+
const references: Reference[] = [];
|
|
135
|
+
|
|
136
|
+
const normalizedDefinitionFile = definitionFile
|
|
137
|
+
? normalizePath(definitionFile)
|
|
138
|
+
: null;
|
|
139
|
+
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
const ref = parseGrepLine(line);
|
|
142
|
+
if (!ref) continue;
|
|
143
|
+
|
|
144
|
+
// Exclude the definition file
|
|
145
|
+
if (normalizedDefinitionFile) {
|
|
146
|
+
const normalizedRefFile = normalizePath(ref.file);
|
|
147
|
+
if (
|
|
148
|
+
normalizedRefFile === normalizedDefinitionFile ||
|
|
149
|
+
normalizedRefFile.endsWith(`/${normalizedDefinitionFile}`) ||
|
|
150
|
+
normalizedDefinitionFile.endsWith(`/${normalizedRefFile}`)
|
|
151
|
+
) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
references.push(ref);
|
|
157
|
+
|
|
158
|
+
// Stop if we've reached the limit
|
|
159
|
+
if (references.length >= limit) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return references;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Find references for multiple symbols
|
|
169
|
+
*
|
|
170
|
+
* @param symbols - Array of symbol names
|
|
171
|
+
* @param definitionFile - File where symbols are defined (excluded from results)
|
|
172
|
+
* @param cwd - Working directory
|
|
173
|
+
* @param limitPerSymbol - Maximum references per symbol (default: 10)
|
|
174
|
+
* @returns Map of symbol name to references
|
|
175
|
+
*/
|
|
176
|
+
export async function findReferencesForSymbols(
|
|
177
|
+
symbols: string[],
|
|
178
|
+
definitionFile?: string,
|
|
179
|
+
cwd?: string,
|
|
180
|
+
limitPerSymbol: number = 10
|
|
181
|
+
): Promise<Map<string, Reference[]>> {
|
|
182
|
+
const results = new Map<string, Reference[]>();
|
|
183
|
+
|
|
184
|
+
// Process symbols in parallel for better performance
|
|
185
|
+
const promises = symbols.map(async (symbol) => {
|
|
186
|
+
const refs = await findReferences({
|
|
187
|
+
symbol,
|
|
188
|
+
definitionFile,
|
|
189
|
+
cwd,
|
|
190
|
+
limit: limitPerSymbol,
|
|
191
|
+
});
|
|
192
|
+
return { symbol, refs };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const resolvedResults = await Promise.all(promises);
|
|
196
|
+
|
|
197
|
+
for (const { symbol, refs } of resolvedResults) {
|
|
198
|
+
results.set(symbol, refs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Format references as file:line:context strings
|
|
206
|
+
*
|
|
207
|
+
* @param references - References to format
|
|
208
|
+
* @returns Array of formatted strings
|
|
209
|
+
*/
|
|
210
|
+
export function formatReferences(references: Reference[]): string[] {
|
|
211
|
+
return references.map((ref) => `${ref.file}:${ref.line}:${ref.context}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if git is available and cwd is a git repository
|
|
216
|
+
*/
|
|
217
|
+
export async function isGitRepository(cwd?: string): Promise<boolean> {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const process = spawn("git", ["rev-parse", "--git-dir"], {
|
|
220
|
+
cwd: cwd ?? process.cwd(),
|
|
221
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
process.on("close", (exitCode) => {
|
|
225
|
+
resolve(exitCode === 0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
process.on("error", () => {
|
|
229
|
+
resolve(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for re-review cache-busting module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
buildReReviewPreamble,
|
|
8
|
+
detectReReview,
|
|
9
|
+
recordReview,
|
|
10
|
+
getPreviousReviewState,
|
|
11
|
+
clearReviewState,
|
|
12
|
+
processReReview,
|
|
13
|
+
} from "./rereview";
|
|
14
|
+
|
|
15
|
+
describe("buildReReviewPreamble", () => {
|
|
16
|
+
test("builds preamble with changed files", () => {
|
|
17
|
+
const files = ["src/auth.ts", "src/types.ts"];
|
|
18
|
+
const preamble = buildReReviewPreamble(files, "implementation");
|
|
19
|
+
|
|
20
|
+
expect(preamble).toContain("## IMPORTANT: Re-review After Fixes");
|
|
21
|
+
expect(preamble).toContain("This is a RE-REVIEW");
|
|
22
|
+
expect(preamble).toContain("- src/auth.ts");
|
|
23
|
+
expect(preamble).toContain("- src/types.ts");
|
|
24
|
+
expect(preamble).toContain("implementation review");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("truncates files list at 30 files", () => {
|
|
28
|
+
const files = Array.from({ length: 40 }, (_, i) => `src/file${i}.ts`);
|
|
29
|
+
const preamble = buildReReviewPreamble(files, "plan");
|
|
30
|
+
|
|
31
|
+
expect(preamble).toContain("- src/file0.ts");
|
|
32
|
+
expect(preamble).toContain("- src/file29.ts");
|
|
33
|
+
expect(preamble).not.toContain("- src/file30.ts");
|
|
34
|
+
expect(preamble).toContain("... and 10 more files");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("uses correct review type in message", () => {
|
|
38
|
+
const files = ["src/spec.md"];
|
|
39
|
+
const preamble = buildReReviewPreamble(files, "plan");
|
|
40
|
+
|
|
41
|
+
expect(preamble).toContain("plan review");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("detectReReview", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
clearReviewState();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns false when no chatId and no explicit flag", () => {
|
|
51
|
+
const result = detectReReview({});
|
|
52
|
+
expect(result).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns true when isReReview is explicitly true", () => {
|
|
56
|
+
const result = detectReReview({ isReReview: true });
|
|
57
|
+
expect(result).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns false when chatId has no previous review", () => {
|
|
61
|
+
const result = detectReReview({ chatId: "new-chat-id" });
|
|
62
|
+
expect(result).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("returns true when chatId has previous review", () => {
|
|
66
|
+
recordReview("existing-chat-id", ["file1.ts"]);
|
|
67
|
+
const result = detectReReview({ chatId: "existing-chat-id" });
|
|
68
|
+
expect(result).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("recordReview and getPreviousReviewState", () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
clearReviewState();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("records and retrieves review state", () => {
|
|
78
|
+
const files = ["src/auth.ts", "src/types.ts"];
|
|
79
|
+
recordReview("test-chat-id", files);
|
|
80
|
+
|
|
81
|
+
const state = getPreviousReviewState("test-chat-id");
|
|
82
|
+
expect(state).toBeDefined();
|
|
83
|
+
expect(state?.files).toEqual(files);
|
|
84
|
+
expect(state?.timestamp).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns undefined for unknown chat id", () => {
|
|
88
|
+
const state = getPreviousReviewState("unknown-chat-id");
|
|
89
|
+
expect(state).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("clearReviewState removes all state", () => {
|
|
93
|
+
recordReview("chat-1", ["file1.ts"]);
|
|
94
|
+
recordReview("chat-2", ["file2.ts"]);
|
|
95
|
+
|
|
96
|
+
clearReviewState();
|
|
97
|
+
|
|
98
|
+
expect(getPreviousReviewState("chat-1")).toBeUndefined();
|
|
99
|
+
expect(getPreviousReviewState("chat-2")).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("processReReview", () => {
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
clearReviewState();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("returns isReReview false when not a re-review", async () => {
|
|
109
|
+
const result = await processReReview({});
|
|
110
|
+
expect(result.isReReview).toBe(false);
|
|
111
|
+
expect(result.preamble).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("returns preamble when is re-review", async () => {
|
|
115
|
+
recordReview("prev-chat", ["file.ts"]);
|
|
116
|
+
const result = await processReReview({
|
|
117
|
+
chatId: "prev-chat",
|
|
118
|
+
reviewType: "implementation",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.isReReview).toBe(true);
|
|
122
|
+
expect(result.preamble).toBeDefined();
|
|
123
|
+
expect(result.preamble).toContain("RE-REVIEW");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("uses explicit isReReview flag", async () => {
|
|
127
|
+
const result = await processReReview({
|
|
128
|
+
isReReview: true,
|
|
129
|
+
reviewType: "plan",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.isReReview).toBe(true);
|
|
133
|
+
expect(result.preamble).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
});
|