ctb 1.0.0 → 1.2.1

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.
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Unit tests for CLI argument parsing and env loading.
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ rmdirSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ // Helper to create a test directory with .env file
17
+ function createTestDir(): string {
18
+ const testDir = `/tmp/ctb-test-${Date.now()}`;
19
+ mkdirSync(testDir, { recursive: true });
20
+ return testDir;
21
+ }
22
+
23
+ function cleanupTestDir(dir: string): void {
24
+ try {
25
+ const envPath = join(dir, ".env");
26
+ if (existsSync(envPath)) unlinkSync(envPath);
27
+ rmdirSync(dir);
28
+ } catch {
29
+ // Ignore cleanup errors
30
+ }
31
+ }
32
+
33
+ describe("CLI .env file parsing", () => {
34
+ let testDir: string;
35
+
36
+ beforeEach(() => {
37
+ testDir = createTestDir();
38
+ });
39
+
40
+ afterEach(() => {
41
+ cleanupTestDir(testDir);
42
+ });
43
+
44
+ test("parses simple key=value pairs", () => {
45
+ const envPath = join(testDir, ".env");
46
+ writeFileSync(
47
+ envPath,
48
+ `TELEGRAM_BOT_TOKEN=test-token
49
+ TELEGRAM_ALLOWED_USERS=123,456`,
50
+ );
51
+
52
+ const content = readFileSync(envPath, "utf-8");
53
+ const lines = content.split("\n");
54
+ const env: Record<string, string> = {};
55
+
56
+ for (const line of lines) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed || trimmed.startsWith("#")) continue;
59
+ const eqIndex = trimmed.indexOf("=");
60
+ if (eqIndex === -1) continue;
61
+ env[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
62
+ }
63
+
64
+ expect(env.TELEGRAM_BOT_TOKEN).toBe("test-token");
65
+ expect(env.TELEGRAM_ALLOWED_USERS).toBe("123,456");
66
+ });
67
+
68
+ test("handles quoted values", () => {
69
+ const envPath = join(testDir, ".env");
70
+ writeFileSync(
71
+ envPath,
72
+ `SINGLE='single quoted'
73
+ DOUBLE="double quoted"`,
74
+ );
75
+
76
+ const content = readFileSync(envPath, "utf-8");
77
+ const lines = content.split("\n");
78
+ const env: Record<string, string> = {};
79
+
80
+ for (const line of lines) {
81
+ const trimmed = line.trim();
82
+ if (!trimmed || trimmed.startsWith("#")) continue;
83
+ const eqIndex = trimmed.indexOf("=");
84
+ if (eqIndex === -1) continue;
85
+ let value = trimmed.slice(eqIndex + 1).trim();
86
+ if (
87
+ (value.startsWith('"') && value.endsWith('"')) ||
88
+ (value.startsWith("'") && value.endsWith("'"))
89
+ ) {
90
+ value = value.slice(1, -1);
91
+ }
92
+ env[trimmed.slice(0, eqIndex).trim()] = value;
93
+ }
94
+
95
+ expect(env.SINGLE).toBe("single quoted");
96
+ expect(env.DOUBLE).toBe("double quoted");
97
+ });
98
+
99
+ test("ignores comments", () => {
100
+ const envPath = join(testDir, ".env");
101
+ writeFileSync(
102
+ envPath,
103
+ `# This is a comment
104
+ TELEGRAM_BOT_TOKEN=token
105
+ # Another comment
106
+ TELEGRAM_ALLOWED_USERS=123`,
107
+ );
108
+
109
+ const content = readFileSync(envPath, "utf-8");
110
+ const lines = content.split("\n");
111
+ const env: Record<string, string> = {};
112
+
113
+ for (const line of lines) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed || trimmed.startsWith("#")) continue;
116
+ const eqIndex = trimmed.indexOf("=");
117
+ if (eqIndex === -1) continue;
118
+ env[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
119
+ }
120
+
121
+ expect(Object.keys(env)).toHaveLength(2);
122
+ expect(env.TELEGRAM_BOT_TOKEN).toBe("token");
123
+ });
124
+
125
+ test("handles empty lines", () => {
126
+ const envPath = join(testDir, ".env");
127
+ writeFileSync(
128
+ envPath,
129
+ `TELEGRAM_BOT_TOKEN=token
130
+
131
+ TELEGRAM_ALLOWED_USERS=123
132
+
133
+ `,
134
+ );
135
+
136
+ const content = readFileSync(envPath, "utf-8");
137
+ const lines = content.split("\n");
138
+ const env: Record<string, string> = {};
139
+
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+ if (!trimmed || trimmed.startsWith("#")) continue;
143
+ const eqIndex = trimmed.indexOf("=");
144
+ if (eqIndex === -1) continue;
145
+ env[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
146
+ }
147
+
148
+ expect(Object.keys(env)).toHaveLength(2);
149
+ });
150
+
151
+ test("handles values with equals signs", () => {
152
+ const envPath = join(testDir, ".env");
153
+ writeFileSync(envPath, `API_KEY=key=with=equals`);
154
+
155
+ const content = readFileSync(envPath, "utf-8");
156
+ const lines = content.split("\n");
157
+ const env: Record<string, string> = {};
158
+
159
+ for (const line of lines) {
160
+ const trimmed = line.trim();
161
+ if (!trimmed || trimmed.startsWith("#")) continue;
162
+ const eqIndex = trimmed.indexOf("=");
163
+ if (eqIndex === -1) continue;
164
+ env[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
165
+ }
166
+
167
+ expect(env.API_KEY).toBe("key=with=equals");
168
+ });
169
+
170
+ test("handles missing .env file gracefully", () => {
171
+ const nonExistentPath = join(testDir, "nonexistent", ".env");
172
+ expect(existsSync(nonExistentPath)).toBe(false);
173
+ });
174
+ });
175
+
176
+ describe("CLI argument parsing", () => {
177
+ function parseArgs(args: string[]): {
178
+ token?: string;
179
+ users?: string;
180
+ dir?: string;
181
+ help?: boolean;
182
+ version?: boolean;
183
+ tut?: boolean;
184
+ } {
185
+ const options: {
186
+ token?: string;
187
+ users?: string;
188
+ dir?: string;
189
+ help?: boolean;
190
+ version?: boolean;
191
+ tut?: boolean;
192
+ } = {};
193
+
194
+ for (const arg of args) {
195
+ if (arg === "--help" || arg === "-h") {
196
+ options.help = true;
197
+ } else if (arg === "--version" || arg === "-v") {
198
+ options.version = true;
199
+ } else if (arg === "tut" || arg === "tutorial") {
200
+ options.tut = true;
201
+ } else if (arg.startsWith("--token=")) {
202
+ options.token = arg.slice(8);
203
+ } else if (arg.startsWith("--users=")) {
204
+ options.users = arg.slice(8);
205
+ } else if (arg.startsWith("--dir=")) {
206
+ options.dir = arg.slice(6);
207
+ }
208
+ }
209
+
210
+ return options;
211
+ }
212
+
213
+ test("parses --help flag", () => {
214
+ expect(parseArgs(["--help"])).toEqual({ help: true });
215
+ });
216
+
217
+ test("parses -h shorthand", () => {
218
+ expect(parseArgs(["-h"])).toEqual({ help: true });
219
+ });
220
+
221
+ test("parses --version flag", () => {
222
+ expect(parseArgs(["--version"])).toEqual({ version: true });
223
+ });
224
+
225
+ test("parses -v shorthand", () => {
226
+ expect(parseArgs(["-v"])).toEqual({ version: true });
227
+ });
228
+
229
+ test("parses tut command", () => {
230
+ expect(parseArgs(["tut"])).toEqual({ tut: true });
231
+ });
232
+
233
+ test("parses tutorial command", () => {
234
+ expect(parseArgs(["tutorial"])).toEqual({ tut: true });
235
+ });
236
+
237
+ test("parses --token option", () => {
238
+ const result = parseArgs(["--token=my-bot-token"]);
239
+ expect(result.token).toBe("my-bot-token");
240
+ });
241
+
242
+ test("parses --users option", () => {
243
+ const result = parseArgs(["--users=123,456,789"]);
244
+ expect(result.users).toBe("123,456,789");
245
+ });
246
+
247
+ test("parses --dir option", () => {
248
+ const result = parseArgs(["--dir=/path/to/project"]);
249
+ expect(result.dir).toBe("/path/to/project");
250
+ });
251
+
252
+ test("parses multiple options", () => {
253
+ const result = parseArgs([
254
+ "--token=token123",
255
+ "--users=111,222",
256
+ "--dir=/home/user/project",
257
+ ]);
258
+ expect(result.token).toBe("token123");
259
+ expect(result.users).toBe("111,222");
260
+ expect(result.dir).toBe("/home/user/project");
261
+ });
262
+
263
+ test("handles empty args array", () => {
264
+ expect(parseArgs([])).toEqual({});
265
+ });
266
+
267
+ test("ignores unknown flags", () => {
268
+ const result = parseArgs(["--unknown", "--another=value"]);
269
+ expect(result).toEqual({});
270
+ });
271
+
272
+ test("handles token with special characters", () => {
273
+ const result = parseArgs(["--token=123:ABC-xyz_789"]);
274
+ expect(result.token).toBe("123:ABC-xyz_789");
275
+ });
276
+
277
+ test("handles path with spaces when quoted", () => {
278
+ // This tests the arg as it would come from shell
279
+ const result = parseArgs(["--dir=/path/with spaces/project"]);
280
+ expect(result.dir).toBe("/path/with spaces/project");
281
+ });
282
+ });
283
+
284
+ describe("Path resolution with base directory", () => {
285
+ function resolvePath(path: string, baseDir?: string): string {
286
+ const homedir = () => "/Users/test";
287
+ const expanded = path.replace(/^~/, homedir());
288
+ if (baseDir && !expanded.startsWith("/")) {
289
+ // Simulate resolve(baseDir, expanded)
290
+ return `${baseDir}/${expanded}`.replace(/\/+/g, "/");
291
+ }
292
+ return expanded.startsWith("/") ? expanded : `/${expanded}`;
293
+ }
294
+
295
+ test("resolves absolute path unchanged", () => {
296
+ const result = resolvePath("/absolute/path", "/base/dir");
297
+ expect(result).toBe("/absolute/path");
298
+ });
299
+
300
+ test("resolves relative path from base directory", () => {
301
+ const result = resolvePath("relative/path", "/base/dir");
302
+ expect(result).toBe("/base/dir/relative/path");
303
+ });
304
+
305
+ test("expands tilde to home directory", () => {
306
+ const result = resolvePath("~/projects", "/base/dir");
307
+ expect(result).toBe("/Users/test/projects");
308
+ });
309
+
310
+ test("tilde path is not affected by base directory", () => {
311
+ const result = resolvePath("~/projects", "/base/dir");
312
+ expect(result).toBe("/Users/test/projects");
313
+ });
314
+
315
+ test("handles dot-relative paths", () => {
316
+ const result = resolvePath("./subdir", "/base/dir");
317
+ expect(result).toBe("/base/dir/./subdir");
318
+ });
319
+
320
+ test("handles parent directory paths", () => {
321
+ const result = resolvePath("../sibling", "/base/dir");
322
+ expect(result).toBe("/base/dir/../sibling");
323
+ });
324
+
325
+ test("works without base directory for absolute paths", () => {
326
+ const result = resolvePath("/absolute/path");
327
+ expect(result).toBe("/absolute/path");
328
+ });
329
+ });
330
+
331
+ describe("Instance directory hashing", () => {
332
+ function hashDir(dir: string): string {
333
+ let hash = 0;
334
+ for (let i = 0; i < dir.length; i++) {
335
+ const char = dir.charCodeAt(i);
336
+ hash = ((hash << 5) - hash + char) | 0;
337
+ }
338
+ return Math.abs(hash).toString(36).slice(0, 8);
339
+ }
340
+
341
+ test("generates consistent hash for same path", () => {
342
+ const path = "/Users/test/project";
343
+ expect(hashDir(path)).toBe(hashDir(path));
344
+ });
345
+
346
+ test("generates different hash for different paths", () => {
347
+ const hash1 = hashDir("/Users/test/project1");
348
+ const hash2 = hashDir("/Users/test/project2");
349
+ expect(hash1).not.toBe(hash2);
350
+ });
351
+
352
+ test("generates short hash (max 8 chars)", () => {
353
+ const hash = hashDir("/very/long/path/to/some/project/directory");
354
+ expect(hash.length).toBeLessThanOrEqual(8);
355
+ });
356
+
357
+ test("generates alphanumeric hash", () => {
358
+ const hash = hashDir("/Users/test/project");
359
+ expect(hash).toMatch(/^[a-z0-9]+$/);
360
+ });
361
+
362
+ test("handles empty string", () => {
363
+ const hash = hashDir("");
364
+ expect(hash).toBe("0");
365
+ });
366
+
367
+ test("handles root path", () => {
368
+ const hash = hashDir("/");
369
+ expect(hash.length).toBeGreaterThan(0);
370
+ });
371
+
372
+ test("handles home directory variations", () => {
373
+ const hash1 = hashDir("/Users/user1/project");
374
+ const hash2 = hashDir("/Users/user2/project");
375
+ expect(hash1).not.toBe(hash2);
376
+ });
377
+ });
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Unit tests for file path detection in formatting module.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { detectFilePaths } from "../formatting";
7
+
8
+ describe("detectFilePaths", () => {
9
+ describe("paths in backticks", () => {
10
+ test("detects single file path in backticks", () => {
11
+ const text = "Here is the file: `/Users/test/file.txt`";
12
+ const result = detectFilePaths(text);
13
+ expect(result).toHaveLength(1);
14
+ expect(result[0]?.path).toBe("/Users/test/file.txt");
15
+ expect(result[0]?.display).toBe("test/file.txt");
16
+ });
17
+
18
+ test("detects multiple file paths in backticks", () => {
19
+ const text = "Files: `/home/user/a.txt` and `/home/user/b.json`";
20
+ const result = detectFilePaths(text);
21
+ expect(result).toHaveLength(2);
22
+ expect(result[0]?.path).toBe("/home/user/a.txt");
23
+ expect(result[1]?.path).toBe("/home/user/b.json");
24
+ });
25
+
26
+ test("handles paths with special characters", () => {
27
+ const text = "`/Users/test/my-file_v2.txt`";
28
+ const result = detectFilePaths(text);
29
+ expect(result).toHaveLength(1);
30
+ expect(result[0]?.path).toBe("/Users/test/my-file_v2.txt");
31
+ });
32
+
33
+ test("handles paths with numbers", () => {
34
+ const text = "`/tmp/output123.csv`";
35
+ const result = detectFilePaths(text);
36
+ expect(result).toHaveLength(1);
37
+ expect(result[0]?.path).toBe("/tmp/output123.csv");
38
+ });
39
+ });
40
+
41
+ describe("paths after common prefixes", () => {
42
+ test("detects path after 'file:'", () => {
43
+ const text = "file: /Users/test/document.pdf";
44
+ const result = detectFilePaths(text);
45
+ expect(result).toHaveLength(1);
46
+ expect(result[0]?.path).toBe("/Users/test/document.pdf");
47
+ });
48
+
49
+ test("detects path after 'saved:'", () => {
50
+ const text = "saved: /home/user/output.json";
51
+ const result = detectFilePaths(text);
52
+ expect(result).toHaveLength(1);
53
+ expect(result[0]?.path).toBe("/home/user/output.json");
54
+ });
55
+
56
+ test("detects path after 'created:'", () => {
57
+ const text = "created: /tmp/new-file.txt";
58
+ const result = detectFilePaths(text);
59
+ expect(result).toHaveLength(1);
60
+ expect(result[0]?.path).toBe("/tmp/new-file.txt");
61
+ });
62
+
63
+ test("detects path after 'wrote:'", () => {
64
+ const text = "wrote: /var/log/app.log";
65
+ const result = detectFilePaths(text);
66
+ expect(result).toHaveLength(1);
67
+ expect(result[0]?.path).toBe("/var/log/app.log");
68
+ });
69
+
70
+ test("detects path after 'output:'", () => {
71
+ const text = "output: /home/user/result.csv";
72
+ const result = detectFilePaths(text);
73
+ expect(result).toHaveLength(1);
74
+ expect(result[0]?.path).toBe("/home/user/result.csv");
75
+ });
76
+
77
+ test("detects path after 'generated:'", () => {
78
+ const text = "generated: /tmp/report.html";
79
+ const result = detectFilePaths(text);
80
+ expect(result).toHaveLength(1);
81
+ expect(result[0]?.path).toBe("/tmp/report.html");
82
+ });
83
+
84
+ test("is case insensitive for prefixes", () => {
85
+ const text = "FILE: /Users/test/doc.txt";
86
+ const result = detectFilePaths(text);
87
+ expect(result).toHaveLength(1);
88
+ });
89
+ });
90
+
91
+ describe("standalone absolute paths", () => {
92
+ test("detects /Users path", () => {
93
+ const text = "The file is at /Users/htlin/projects/code.ts";
94
+ const result = detectFilePaths(text);
95
+ expect(result).toHaveLength(1);
96
+ expect(result[0]?.path).toBe("/Users/htlin/projects/code.ts");
97
+ });
98
+
99
+ test("detects /home path", () => {
100
+ const text = "Located at /home/user/data.json";
101
+ const result = detectFilePaths(text);
102
+ expect(result).toHaveLength(1);
103
+ expect(result[0]?.path).toBe("/home/user/data.json");
104
+ });
105
+
106
+ test("detects /tmp path", () => {
107
+ const text = "Temp file: /tmp/cache.db";
108
+ const result = detectFilePaths(text);
109
+ expect(result).toHaveLength(1);
110
+ expect(result[0]?.path).toBe("/tmp/cache.db");
111
+ });
112
+
113
+ test("detects /var path", () => {
114
+ const text = "Log at /var/log/system.log";
115
+ const result = detectFilePaths(text);
116
+ expect(result).toHaveLength(1);
117
+ expect(result[0]?.path).toBe("/var/log/system.log");
118
+ });
119
+
120
+ test("detects /etc path", () => {
121
+ const text = "Config: /etc/nginx/nginx.conf";
122
+ const result = detectFilePaths(text);
123
+ expect(result).toHaveLength(1);
124
+ expect(result[0]?.path).toBe("/etc/nginx/nginx.conf");
125
+ });
126
+
127
+ test("detects /opt path", () => {
128
+ const text = "Binary at /opt/app/bin/run.sh";
129
+ const result = detectFilePaths(text);
130
+ expect(result).toHaveLength(1);
131
+ expect(result[0]?.path).toBe("/opt/app/bin/run.sh");
132
+ });
133
+ });
134
+
135
+ describe("deduplication", () => {
136
+ test("removes duplicate paths", () => {
137
+ const text =
138
+ "File `/Users/test/file.txt` was created at /Users/test/file.txt";
139
+ const result = detectFilePaths(text);
140
+ expect(result).toHaveLength(1);
141
+ });
142
+
143
+ test("keeps unique paths", () => {
144
+ const text = "`/tmp/a.txt` `/tmp/b.txt` `/tmp/c.txt`";
145
+ const result = detectFilePaths(text);
146
+ expect(result).toHaveLength(3);
147
+ });
148
+ });
149
+
150
+ describe("display name generation", () => {
151
+ test("uses last 2 path components for display", () => {
152
+ const text = "`/very/long/path/to/file.txt`";
153
+ const result = detectFilePaths(text);
154
+ expect(result[0]?.display).toBe("to/file.txt");
155
+ });
156
+
157
+ test("handles single component paths", () => {
158
+ const text = "`/tmp/file.txt`";
159
+ const result = detectFilePaths(text);
160
+ expect(result[0]?.display).toBe("tmp/file.txt");
161
+ });
162
+
163
+ test("handles root level files", () => {
164
+ const text = "file: /tmp/x.log";
165
+ const result = detectFilePaths(text);
166
+ expect(result[0]?.display).toBe("tmp/x.log");
167
+ });
168
+ });
169
+
170
+ describe("edge cases", () => {
171
+ test("returns empty array for text without paths", () => {
172
+ const text = "This is just regular text without any file paths.";
173
+ const result = detectFilePaths(text);
174
+ expect(result).toHaveLength(0);
175
+ });
176
+
177
+ test("ignores relative paths", () => {
178
+ const text = "Use ./local/file.txt or ../parent/file.txt";
179
+ const result = detectFilePaths(text);
180
+ expect(result).toHaveLength(0);
181
+ });
182
+
183
+ test("ignores URLs", () => {
184
+ const text = "Visit https://example.com/path/file.txt";
185
+ const result = detectFilePaths(text);
186
+ expect(result).toHaveLength(0);
187
+ });
188
+
189
+ test("ignores paths without extensions", () => {
190
+ const text = "`/Users/test/directory`";
191
+ const result = detectFilePaths(text);
192
+ expect(result).toHaveLength(0);
193
+ });
194
+
195
+ test("handles empty string", () => {
196
+ const result = detectFilePaths("");
197
+ expect(result).toHaveLength(0);
198
+ });
199
+
200
+ test("handles paths with various extensions", () => {
201
+ const extensions = [
202
+ ".txt",
203
+ ".json",
204
+ ".ts",
205
+ ".js",
206
+ ".py",
207
+ ".md",
208
+ ".yaml",
209
+ ".yml",
210
+ ".csv",
211
+ ".html",
212
+ ".css",
213
+ ".xml",
214
+ ".pdf",
215
+ ".log",
216
+ ".sh",
217
+ ];
218
+ for (const ext of extensions) {
219
+ const text = `/tmp/file${ext}`;
220
+ const result = detectFilePaths(text);
221
+ expect(result.length).toBeGreaterThanOrEqual(0); // May or may not match depending on pattern
222
+ }
223
+ });
224
+
225
+ test("handles multiline text", () => {
226
+ const text = `
227
+ First file: /Users/test/a.txt
228
+ Second file: /Users/test/b.json
229
+ Third: \`/tmp/c.csv\`
230
+ `;
231
+ const result = detectFilePaths(text);
232
+ expect(result.length).toBeGreaterThanOrEqual(1);
233
+ });
234
+ });
235
+
236
+ describe("relative paths with working directory", () => {
237
+ test("resolves relative path with working directory", () => {
238
+ const text = "Created file: `output.txt`";
239
+ const result = detectFilePaths(text, "/Users/test/project");
240
+ expect(result).toHaveLength(1);
241
+ expect(result[0]?.path).toBe("/Users/test/project/output.txt");
242
+ });
243
+
244
+ test("resolves nested relative path", () => {
245
+ const text = "See `src/main.ts`";
246
+ const result = detectFilePaths(text, "/Users/test/project");
247
+ expect(result).toHaveLength(1);
248
+ expect(result[0]?.path).toBe("/Users/test/project/src/main.ts");
249
+ });
250
+
251
+ test("keeps absolute path unchanged with working directory", () => {
252
+ const text = "`/absolute/path/file.txt`";
253
+ const result = detectFilePaths(text, "/Users/test/project");
254
+ expect(result).toHaveLength(1);
255
+ expect(result[0]?.path).toBe("/absolute/path/file.txt");
256
+ });
257
+
258
+ test("handles multiple relative paths", () => {
259
+ const text = "Files: `a.txt` and `b.json`";
260
+ const result = detectFilePaths(text, "/project");
261
+ expect(result).toHaveLength(2);
262
+ expect(result[0]?.path).toBe("/project/a.txt");
263
+ expect(result[1]?.path).toBe("/project/b.json");
264
+ });
265
+
266
+ test("handles relative path after prefix", () => {
267
+ const text = "output: result.csv";
268
+ const result = detectFilePaths(text, "/data");
269
+ expect(result).toHaveLength(1);
270
+ expect(result[0]?.path).toBe("/data/result.csv");
271
+ });
272
+
273
+ test("ignores relative paths without working directory", () => {
274
+ const text = "`relative/path.txt`";
275
+ const result = detectFilePaths(text);
276
+ // Without working directory, relative paths should still be captured but not resolved
277
+ expect(result.length).toBeGreaterThanOrEqual(0);
278
+ });
279
+ });
280
+
281
+ describe("complex scenarios", () => {
282
+ test("handles Claude-style response with file mentions", () => {
283
+ const text = `
284
+ I've created the following files:
285
+
286
+ 1. Main script: \`/Users/htlin/project/src/main.ts\`
287
+ 2. Configuration: saved: /Users/htlin/project/config.json
288
+ 3. Test file at /Users/htlin/project/test/main.test.ts
289
+
290
+ The output was written to \`/tmp/build-output.log\`.
291
+ `;
292
+ const result = detectFilePaths(text);
293
+ expect(result.length).toBeGreaterThanOrEqual(3);
294
+ expect(result.some((r) => r.path.includes("main.ts"))).toBe(true);
295
+ expect(result.some((r) => r.path.includes("config.json"))).toBe(true);
296
+ });
297
+
298
+ test("handles paths in code blocks", () => {
299
+ const text = `
300
+ Here's the code:
301
+ \`\`\`
302
+ const file = "/Users/test/data.json";
303
+ \`\`\`
304
+ Output saved to \`/tmp/result.txt\`
305
+ `;
306
+ const result = detectFilePaths(text);
307
+ // Should find at least the backtick one
308
+ expect(result.some((r) => r.path.includes("result.txt"))).toBe(true);
309
+ });
310
+ });
311
+ });