wdyt 0.1.11 → 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 +3 -2
- 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
- package/src/parseExpression.ts +20 -46
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for wdyt with flowctl-compatible interface
|
|
3
|
+
*
|
|
4
|
+
* Tests the full pipeline that flowctl uses:
|
|
5
|
+
* 1. builder "summary" -> creates tab
|
|
6
|
+
* 2. select add "path" -> adds files
|
|
7
|
+
* 3. call chat_send {...} -> generates review context
|
|
8
|
+
*
|
|
9
|
+
* Also tests context enrichment features:
|
|
10
|
+
* - Context hints from changed files
|
|
11
|
+
* - Git diff context injection
|
|
12
|
+
* - Verdict parsing from response
|
|
13
|
+
* - Re-review preamble for continuing chats
|
|
14
|
+
* - Flow-Next spec loading
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from "bun:test";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { mkdirSync, rmSync } from "fs";
|
|
20
|
+
import { $ } from "bun";
|
|
21
|
+
import { ensureState, createTab, getTab, updateTab, updateWindowPaths } from "./state";
|
|
22
|
+
import { builderCommand } from "./commands/builder";
|
|
23
|
+
import { selectAddCommand, selectGetCommand } from "./commands/select";
|
|
24
|
+
import { chatSendCommand } from "./commands/chat";
|
|
25
|
+
import { generateContextHints, formatHints } from "./context/hints";
|
|
26
|
+
import { getGitDiffContext, formatDiffContextXml } from "./git/diff";
|
|
27
|
+
import { buildReReviewPreamble, clearReviewState } from "./context/rereview";
|
|
28
|
+
import { loadTaskSpec, getTaskSpecContext } from "./flow/specs";
|
|
29
|
+
|
|
30
|
+
// Test fixtures directory
|
|
31
|
+
const TEST_DIR = join(import.meta.dir, "..", ".test-integration");
|
|
32
|
+
const TEST_FLOW_DIR = join(TEST_DIR, ".flow");
|
|
33
|
+
|
|
34
|
+
// Sample test files
|
|
35
|
+
const SAMPLE_TS_FILE = `/**
|
|
36
|
+
* Sample TypeScript file for testing
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export interface User {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
email: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createUser(name: string, email: string): User {
|
|
46
|
+
return {
|
|
47
|
+
id: crypto.randomUUID(),
|
|
48
|
+
name,
|
|
49
|
+
email,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateEmail(email: string): boolean {
|
|
54
|
+
return email.includes("@");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class UserService {
|
|
58
|
+
private users: User[] = [];
|
|
59
|
+
|
|
60
|
+
addUser(user: User): void {
|
|
61
|
+
this.users.push(user);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getUser(id: string): User | undefined {
|
|
65
|
+
return this.users.find(u => u.id === id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const SAMPLE_SPEC = `# fn-99-int.1 Integration Test Task
|
|
71
|
+
|
|
72
|
+
## Description
|
|
73
|
+
This is a test task for integration testing.
|
|
74
|
+
|
|
75
|
+
### Requirements
|
|
76
|
+
- Test the full flowctl pipeline
|
|
77
|
+
- Verify context hints generation
|
|
78
|
+
- Check verdict parsing
|
|
79
|
+
|
|
80
|
+
### Acceptance Criteria
|
|
81
|
+
- [ ] builder command creates tabs
|
|
82
|
+
- [ ] select add adds files
|
|
83
|
+
- [ ] chat_send generates context
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
describe("Integration: flowctl-compatible pipeline", () => {
|
|
87
|
+
beforeEach(async () => {
|
|
88
|
+
// Set up test environment
|
|
89
|
+
process.env.XDG_DATA_HOME = TEST_DIR;
|
|
90
|
+
|
|
91
|
+
// Create test directories
|
|
92
|
+
mkdirSync(join(TEST_DIR, "src"), { recursive: true });
|
|
93
|
+
mkdirSync(join(TEST_FLOW_DIR, "tasks"), { recursive: true });
|
|
94
|
+
mkdirSync(join(TEST_FLOW_DIR, "specs"), { recursive: true });
|
|
95
|
+
|
|
96
|
+
// Write test files
|
|
97
|
+
await Bun.write(join(TEST_DIR, "src", "user.ts"), SAMPLE_TS_FILE);
|
|
98
|
+
await Bun.write(join(TEST_FLOW_DIR, "tasks", "fn-99-int.1.md"), SAMPLE_SPEC);
|
|
99
|
+
|
|
100
|
+
// Initialize state
|
|
101
|
+
await ensureState();
|
|
102
|
+
|
|
103
|
+
// Set window root path to TEST_DIR so relative paths resolve correctly
|
|
104
|
+
await updateWindowPaths(1, [TEST_DIR]);
|
|
105
|
+
|
|
106
|
+
// Clear re-review state between tests
|
|
107
|
+
clearReviewState();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
try {
|
|
112
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
113
|
+
} catch {
|
|
114
|
+
// Directory might not exist
|
|
115
|
+
}
|
|
116
|
+
delete process.env.XDG_DATA_HOME;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("Step 1: builder command (setup-review)", () => {
|
|
120
|
+
it("creates a new tab and returns Tab: <uuid>", async () => {
|
|
121
|
+
const result = await builderCommand(1, "test review");
|
|
122
|
+
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(result.data).toBeDefined();
|
|
125
|
+
expect(result.data?.tabId).toBeDefined();
|
|
126
|
+
expect(result.data?.tabId.length).toBe(36); // UUID format
|
|
127
|
+
expect(result.output).toMatch(/^Tab: [a-f0-9-]{36}$/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("accepts --response-type flag (flowctl compatibility)", async () => {
|
|
131
|
+
const result = await builderCommand(1, "test", { "response-type": "markdown" });
|
|
132
|
+
|
|
133
|
+
expect(result.success).toBe(true);
|
|
134
|
+
expect(result.data?.tabId).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Step 2: select add command", () => {
|
|
139
|
+
it("adds files to tab selection", async () => {
|
|
140
|
+
// Create tab first
|
|
141
|
+
const tab = await createTab(1);
|
|
142
|
+
|
|
143
|
+
// Add a file (using relative path - resolved against TEST_DIR)
|
|
144
|
+
const result = await selectAddCommand(1, tab.id, "src/user.ts");
|
|
145
|
+
|
|
146
|
+
expect(result.success).toBe(true);
|
|
147
|
+
expect(result.data?.added).toBe(1);
|
|
148
|
+
expect(result.data?.total).toBe(1);
|
|
149
|
+
|
|
150
|
+
// Verify file was added
|
|
151
|
+
const getResult = await selectGetCommand(1, tab.id);
|
|
152
|
+
expect(getResult.success).toBe(true);
|
|
153
|
+
// getResult.data is { files: string[] }, need to check files array
|
|
154
|
+
expect(getResult.data?.files).toBeDefined();
|
|
155
|
+
expect(getResult.data?.files.length).toBe(1);
|
|
156
|
+
expect(getResult.data?.files[0]).toContain("src/user.ts");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("handles multiple files", async () => {
|
|
160
|
+
const tab = await createTab(1);
|
|
161
|
+
|
|
162
|
+
// Write another test file
|
|
163
|
+
await Bun.write(join(TEST_DIR, "src", "other.ts"), "export const other = 1;");
|
|
164
|
+
|
|
165
|
+
// Add multiple files
|
|
166
|
+
await selectAddCommand(1, tab.id, "src/user.ts");
|
|
167
|
+
await selectAddCommand(1, tab.id, "src/other.ts");
|
|
168
|
+
|
|
169
|
+
const getResult = await selectGetCommand(1, tab.id);
|
|
170
|
+
expect(getResult.success).toBe(true);
|
|
171
|
+
expect(getResult.data?.files).toBeDefined();
|
|
172
|
+
expect(getResult.data?.files.length).toBe(2);
|
|
173
|
+
|
|
174
|
+
// Check that both files are in the list
|
|
175
|
+
const files = getResult.data!.files.join("\n");
|
|
176
|
+
expect(files).toContain("src/user.ts");
|
|
177
|
+
expect(files).toContain("src/other.ts");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("Step 3: chat_send command", () => {
|
|
182
|
+
// Save original PATH and restore after tests
|
|
183
|
+
let originalPath: string | undefined;
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
// Remove claude from PATH to ensure tests don't depend on Claude CLI
|
|
187
|
+
// This makes tests fast and deterministic
|
|
188
|
+
originalPath = process.env.PATH;
|
|
189
|
+
process.env.PATH = "/nonexistent";
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
if (originalPath) {
|
|
194
|
+
process.env.PATH = originalPath;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("generates context XML with prompt and files", async () => {
|
|
199
|
+
// Setup: create tab and add file
|
|
200
|
+
const tab = await createTab(1);
|
|
201
|
+
await updateTab(1, tab.id, {
|
|
202
|
+
prompt: "Review this code",
|
|
203
|
+
selectedFiles: [join(TEST_DIR, "src", "user.ts")],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Send chat (without Claude CLI)
|
|
207
|
+
const result = await chatSendCommand(1, tab.id, JSON.stringify({
|
|
208
|
+
message: "Please review this code for issues",
|
|
209
|
+
mode: "review",
|
|
210
|
+
new_chat: true,
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
// Verify response structure
|
|
214
|
+
expect(result.success).toBe(true);
|
|
215
|
+
expect(result.data).toBeDefined();
|
|
216
|
+
expect(result.data?.id).toBeDefined();
|
|
217
|
+
expect(result.data?.id.length).toBe(36); // UUID format
|
|
218
|
+
expect(result.data?.path).toBeDefined();
|
|
219
|
+
|
|
220
|
+
// Verify output format matches flowctl expectations
|
|
221
|
+
expect(result.output).toContain(`Chat: \`${result.data?.id}\``);
|
|
222
|
+
|
|
223
|
+
// Verify the XML file was created with correct content
|
|
224
|
+
const xmlFile = Bun.file(result.data!.path);
|
|
225
|
+
expect(await xmlFile.exists()).toBe(true);
|
|
226
|
+
const xmlContent = await xmlFile.text();
|
|
227
|
+
expect(xmlContent).toContain('<?xml version="1.0"');
|
|
228
|
+
expect(xmlContent).toContain("<prompt>");
|
|
229
|
+
expect(xmlContent).toContain("Please review this code for issues");
|
|
230
|
+
expect(xmlContent).toContain("<files>");
|
|
231
|
+
expect(xmlContent).toContain("user.ts");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("includes selected_paths from payload", async () => {
|
|
235
|
+
const tab = await createTab(1);
|
|
236
|
+
|
|
237
|
+
// Send with explicit selected_paths
|
|
238
|
+
const result = await chatSendCommand(1, tab.id, JSON.stringify({
|
|
239
|
+
message: "Review these files",
|
|
240
|
+
mode: "review",
|
|
241
|
+
selected_paths: [join(TEST_DIR, "src", "user.ts")],
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
expect(result.success).toBe(true);
|
|
245
|
+
expect(result.data?.id).toBeDefined();
|
|
246
|
+
|
|
247
|
+
// Verify the file was included
|
|
248
|
+
const xmlFile = Bun.file(result.data!.path);
|
|
249
|
+
const xmlContent = await xmlFile.text();
|
|
250
|
+
expect(xmlContent).toContain("user.ts");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("Integration: Context hints generation", () => {
|
|
256
|
+
it("generates hints from TypeScript symbols", async () => {
|
|
257
|
+
const fileContents = new Map([
|
|
258
|
+
["src/user.ts", SAMPLE_TS_FILE],
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const hints = await generateContextHints({
|
|
262
|
+
changedFiles: ["src/user.ts"],
|
|
263
|
+
fileContents,
|
|
264
|
+
cwd: process.cwd(),
|
|
265
|
+
maxHints: 10,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Should return array (may be empty if no external refs)
|
|
269
|
+
expect(Array.isArray(hints)).toBe(true);
|
|
270
|
+
|
|
271
|
+
// Each hint should have required fields
|
|
272
|
+
for (const hint of hints) {
|
|
273
|
+
expect(hint).toHaveProperty("file");
|
|
274
|
+
expect(hint).toHaveProperty("line");
|
|
275
|
+
expect(hint).toHaveProperty("symbol");
|
|
276
|
+
expect(hint).toHaveProperty("refCount");
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("formats hints in flowctl-compatible format", () => {
|
|
281
|
+
const hints = [
|
|
282
|
+
{ file: "src/auth.ts", line: 15, symbol: "validateUser", refCount: 3 },
|
|
283
|
+
{ file: "src/types.ts", line: 42, symbol: "User", refCount: 5 },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const formatted = formatHints(hints);
|
|
287
|
+
|
|
288
|
+
expect(formatted).toContain("Consider these related files:");
|
|
289
|
+
expect(formatted).toContain("src/auth.ts:15 - references validateUser");
|
|
290
|
+
expect(formatted).toContain("src/types.ts:42 - references User");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("Integration: Git diff context", () => {
|
|
295
|
+
it("gets git context from repository", async () => {
|
|
296
|
+
// Use the actual repo for this test
|
|
297
|
+
const context = await getGitDiffContext({
|
|
298
|
+
base: "HEAD~1",
|
|
299
|
+
head: "HEAD",
|
|
300
|
+
cwd: process.cwd(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Should have basic structure
|
|
304
|
+
expect(context).toHaveProperty("diffStat");
|
|
305
|
+
expect(context).toHaveProperty("commits");
|
|
306
|
+
expect(context).toHaveProperty("changedFiles");
|
|
307
|
+
expect(context).toHaveProperty("branch");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("formats git context as XML", () => {
|
|
311
|
+
const context = {
|
|
312
|
+
diffStat: "3 files changed, 100 insertions(+), 20 deletions(-)",
|
|
313
|
+
commits: ["abc1234 feat: add user module", "def5678 fix: email validation"],
|
|
314
|
+
changedFiles: ["src/user.ts", "src/types.ts"],
|
|
315
|
+
branch: "feature/users",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const xml = formatDiffContextXml(context);
|
|
319
|
+
|
|
320
|
+
expect(xml).toContain("<diff_summary>");
|
|
321
|
+
expect(xml).toContain("3 files changed");
|
|
322
|
+
expect(xml).toContain("<commits>");
|
|
323
|
+
expect(xml).toContain("feat: add user module");
|
|
324
|
+
expect(xml).toContain("<changed_files>");
|
|
325
|
+
expect(xml).toContain("src/user.ts");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("Integration: Re-review preamble", () => {
|
|
330
|
+
it("builds re-review preamble with changed files", () => {
|
|
331
|
+
const changedFiles = [
|
|
332
|
+
"src/user.ts",
|
|
333
|
+
"src/auth.ts",
|
|
334
|
+
"src/types.ts",
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const preamble = buildReReviewPreamble(changedFiles, "implementation");
|
|
338
|
+
|
|
339
|
+
expect(preamble).toContain("IMPORTANT: Re-review After Fixes");
|
|
340
|
+
expect(preamble).toContain("This is a RE-REVIEW");
|
|
341
|
+
expect(preamble).toContain("src/user.ts");
|
|
342
|
+
expect(preamble).toContain("src/auth.ts");
|
|
343
|
+
expect(preamble).toContain("implementation review");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("truncates file list at 30 files", () => {
|
|
347
|
+
const manyFiles = Array.from({ length: 50 }, (_, i) => `src/file${i}.ts`);
|
|
348
|
+
|
|
349
|
+
const preamble = buildReReviewPreamble(manyFiles, "review");
|
|
350
|
+
|
|
351
|
+
expect(preamble).toContain("and 20 more files");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("Integration: Flow-Next spec loading", () => {
|
|
356
|
+
beforeAll(() => {
|
|
357
|
+
mkdirSync(join(TEST_FLOW_DIR, "tasks"), { recursive: true });
|
|
358
|
+
Bun.write(join(TEST_FLOW_DIR, "tasks", "fn-99-int.1.md"), SAMPLE_SPEC);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
afterAll(() => {
|
|
362
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("loads task spec from .flow directory", async () => {
|
|
366
|
+
const result = await loadTaskSpec("fn-99-int.1", { flowDir: TEST_FLOW_DIR });
|
|
367
|
+
|
|
368
|
+
expect(result.found).toBe(true);
|
|
369
|
+
expect(result.taskId).toBe("fn-99-int.1");
|
|
370
|
+
expect(result.content).toContain("Integration Test Task");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("formats spec as XML context", async () => {
|
|
374
|
+
const xml = await getTaskSpecContext("fn-99-int.1", { flowDir: TEST_FLOW_DIR });
|
|
375
|
+
|
|
376
|
+
expect(xml).toContain("<task_spec>");
|
|
377
|
+
expect(xml).toContain("# fn-99-int.1");
|
|
378
|
+
expect(xml).toContain("</task_spec>");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("returns empty string for missing spec (graceful)", async () => {
|
|
382
|
+
const xml = await getTaskSpecContext("fn-88.99", { flowDir: TEST_FLOW_DIR });
|
|
383
|
+
|
|
384
|
+
expect(xml).toBe("");
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("Integration: Verdict parsing", () => {
|
|
389
|
+
// Note: Actual verdict parsing happens in chat.ts parseVerdict()
|
|
390
|
+
// These tests verify the expected format
|
|
391
|
+
|
|
392
|
+
it("recognizes SHIP verdict format", () => {
|
|
393
|
+
const response = `
|
|
394
|
+
## Review Summary
|
|
395
|
+
Code looks good!
|
|
396
|
+
|
|
397
|
+
<verdict>SHIP</verdict>
|
|
398
|
+
`;
|
|
399
|
+
const match = response.match(/<verdict>(SHIP|NEEDS_WORK|MAJOR_RETHINK)<\/verdict>/i);
|
|
400
|
+
expect(match).not.toBeNull();
|
|
401
|
+
expect(match![1].toUpperCase()).toBe("SHIP");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("recognizes NEEDS_WORK verdict format", () => {
|
|
405
|
+
const response = `
|
|
406
|
+
## Review Summary
|
|
407
|
+
Some issues found.
|
|
408
|
+
|
|
409
|
+
<verdict>NEEDS_WORK</verdict>
|
|
410
|
+
`;
|
|
411
|
+
const match = response.match(/<verdict>(SHIP|NEEDS_WORK|MAJOR_RETHINK)<\/verdict>/i);
|
|
412
|
+
expect(match).not.toBeNull();
|
|
413
|
+
expect(match![1].toUpperCase()).toBe("NEEDS_WORK");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("recognizes MAJOR_RETHINK verdict format", () => {
|
|
417
|
+
const response = `
|
|
418
|
+
## Review Summary
|
|
419
|
+
Significant problems.
|
|
420
|
+
|
|
421
|
+
<verdict>MAJOR_RETHINK</verdict>
|
|
422
|
+
`;
|
|
423
|
+
const match = response.match(/<verdict>(SHIP|NEEDS_WORK|MAJOR_RETHINK)<\/verdict>/i);
|
|
424
|
+
expect(match).not.toBeNull();
|
|
425
|
+
expect(match![1].toUpperCase()).toBe("MAJOR_RETHINK");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("handles case-insensitive verdict", () => {
|
|
429
|
+
const response = `<verdict>ship</verdict>`;
|
|
430
|
+
const match = response.match(/<verdict>(SHIP|NEEDS_WORK|MAJOR_RETHINK)<\/verdict>/i);
|
|
431
|
+
expect(match).not.toBeNull();
|
|
432
|
+
expect(match![1].toUpperCase()).toBe("SHIP");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("Integration: Full pipeline simulation", () => {
|
|
437
|
+
let originalPath: string | undefined;
|
|
438
|
+
|
|
439
|
+
beforeEach(async () => {
|
|
440
|
+
process.env.XDG_DATA_HOME = TEST_DIR;
|
|
441
|
+
// Remove claude from PATH to ensure tests don't depend on Claude CLI
|
|
442
|
+
originalPath = process.env.PATH;
|
|
443
|
+
process.env.PATH = "/nonexistent";
|
|
444
|
+
|
|
445
|
+
mkdirSync(join(TEST_DIR, "src"), { recursive: true });
|
|
446
|
+
await Bun.write(join(TEST_DIR, "src", "user.ts"), SAMPLE_TS_FILE);
|
|
447
|
+
await ensureState();
|
|
448
|
+
|
|
449
|
+
// Set window root to TEST_DIR for relative path resolution
|
|
450
|
+
await updateWindowPaths(1, [TEST_DIR]);
|
|
451
|
+
|
|
452
|
+
clearReviewState();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
afterEach(async () => {
|
|
456
|
+
if (originalPath) {
|
|
457
|
+
process.env.PATH = originalPath;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
461
|
+
} catch {
|
|
462
|
+
// Ignore
|
|
463
|
+
}
|
|
464
|
+
delete process.env.XDG_DATA_HOME;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("runs complete flowctl-like workflow", async () => {
|
|
468
|
+
// Step 1: builder (setup-review)
|
|
469
|
+
// This is what flowctl calls with: rp-cli -w $W -e 'builder "summary"'
|
|
470
|
+
const builderResult = await builderCommand(1, "integration test");
|
|
471
|
+
expect(builderResult.success).toBe(true);
|
|
472
|
+
expect(builderResult.output).toMatch(/^Tab: [a-f0-9-]{36}$/);
|
|
473
|
+
const tabId = builderResult.data!.tabId;
|
|
474
|
+
|
|
475
|
+
// Step 2: select add (add files to review)
|
|
476
|
+
// This is what flowctl calls with: rp-cli -w $W -t $T -e 'select add "path"'
|
|
477
|
+
const selectResult = await selectAddCommand(1, tabId, "src/user.ts");
|
|
478
|
+
expect(selectResult.success).toBe(true);
|
|
479
|
+
expect(selectResult.data?.added).toBe(1);
|
|
480
|
+
|
|
481
|
+
// Step 3: chat_send (trigger review)
|
|
482
|
+
// This is what flowctl calls with: rp-cli -w $W -t $T -e 'call chat_send {...}'
|
|
483
|
+
const chatResult = await chatSendCommand(1, tabId, JSON.stringify({
|
|
484
|
+
message: "Review this user module for security and correctness",
|
|
485
|
+
mode: "review",
|
|
486
|
+
new_chat: true,
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
expect(chatResult.success).toBe(true);
|
|
490
|
+
expect(chatResult.data?.id).toBeDefined();
|
|
491
|
+
expect(chatResult.output).toContain("Chat:");
|
|
492
|
+
|
|
493
|
+
// Verify the XML context was generated correctly
|
|
494
|
+
const xmlFile = Bun.file(chatResult.data!.path);
|
|
495
|
+
expect(await xmlFile.exists()).toBe(true);
|
|
496
|
+
const xmlContent = await xmlFile.text();
|
|
497
|
+
|
|
498
|
+
// Verify the context contains expected elements
|
|
499
|
+
expect(xmlContent).toContain('<?xml version="1.0" encoding="UTF-8"?>');
|
|
500
|
+
expect(xmlContent).toContain("<context>");
|
|
501
|
+
expect(xmlContent).toContain("<prompt>");
|
|
502
|
+
expect(xmlContent).toContain("Review this user module");
|
|
503
|
+
expect(xmlContent).toContain("<files>");
|
|
504
|
+
expect(xmlContent).toContain("user.ts");
|
|
505
|
+
expect(xmlContent).toContain("</context>");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("supports re-review workflow with preamble", async () => {
|
|
509
|
+
// First review
|
|
510
|
+
const tab = await createTab(1);
|
|
511
|
+
await updateTab(1, tab.id, {
|
|
512
|
+
selectedFiles: [join(TEST_DIR, "src", "user.ts")],
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const firstReview = await chatSendCommand(1, tab.id, JSON.stringify({
|
|
516
|
+
message: "Initial review",
|
|
517
|
+
mode: "review",
|
|
518
|
+
new_chat: true,
|
|
519
|
+
}));
|
|
520
|
+
expect(firstReview.success).toBe(true);
|
|
521
|
+
const firstChatId = firstReview.data!.id;
|
|
522
|
+
|
|
523
|
+
// Second review (re-review) - continuing same chat
|
|
524
|
+
const reReview = await chatSendCommand(1, tab.id, JSON.stringify({
|
|
525
|
+
message: "Please re-review after fixes",
|
|
526
|
+
mode: "review",
|
|
527
|
+
chat_id: firstChatId, // Continue from first chat
|
|
528
|
+
new_chat: false,
|
|
529
|
+
}));
|
|
530
|
+
|
|
531
|
+
expect(reReview.success).toBe(true);
|
|
532
|
+
expect(reReview.data?.isReReview).toBe(true);
|
|
533
|
+
|
|
534
|
+
// Verify re-review preamble is in the context
|
|
535
|
+
const xmlContent = await Bun.file(reReview.data!.path).text();
|
|
536
|
+
expect(xmlContent).toContain("RE-REVIEW");
|
|
537
|
+
});
|
|
538
|
+
});
|
package/src/parseExpression.ts
CHANGED
|
@@ -7,61 +7,35 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
|
+
import { parse as shellParse } from "shell-quote";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Tokenize a shell-like string
|
|
13
|
-
*
|
|
13
|
+
* Tokenize a shell-like string
|
|
14
|
+
*
|
|
15
|
+
* Uses shell-quote for most parsing, but preserves JSON objects literally
|
|
16
|
+
* since shell-quote would strip internal quotes from JSON.
|
|
14
17
|
*/
|
|
15
18
|
export function tokenize(input: string): string[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
let inQuote: string | null = null;
|
|
19
|
-
let escape = false;
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < input.length; i++) {
|
|
22
|
-
const char = input[i];
|
|
23
|
-
|
|
24
|
-
if (escape) {
|
|
25
|
-
current += char;
|
|
26
|
-
escape = false;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (char === "\\") {
|
|
31
|
-
escape = true;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
19
|
+
// Check if input contains a JSON object (starts with {)
|
|
20
|
+
const jsonStart = input.indexOf("{");
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} else if (inQuote === null) {
|
|
40
|
-
// Start of quoted string
|
|
41
|
-
inQuote = char;
|
|
42
|
-
} else {
|
|
43
|
-
// Different quote inside a quoted string
|
|
44
|
-
current += char;
|
|
45
|
-
}
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
22
|
+
if (jsonStart !== -1) {
|
|
23
|
+
// Split into pre-JSON and JSON parts
|
|
24
|
+
const preJson = input.slice(0, jsonStart).trim();
|
|
25
|
+
const jsonPart = input.slice(jsonStart);
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
current += char;
|
|
58
|
-
}
|
|
27
|
+
// Parse the pre-JSON part with shell-quote
|
|
28
|
+
const preTokens = preJson
|
|
29
|
+
? shellParse(preJson).filter((t): t is string => typeof t === "string")
|
|
30
|
+
: [];
|
|
59
31
|
|
|
60
|
-
|
|
61
|
-
|
|
32
|
+
// Keep the JSON part as-is
|
|
33
|
+
return [...preTokens, jsonPart];
|
|
62
34
|
}
|
|
63
35
|
|
|
64
|
-
|
|
36
|
+
// No JSON, use shell-quote for everything
|
|
37
|
+
const parsed = shellParse(input);
|
|
38
|
+
return parsed.filter((t): t is string => typeof t === "string");
|
|
65
39
|
}
|
|
66
40
|
|
|
67
41
|
/**
|