agent-yes 1.73.2 → 1.75.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.
@@ -0,0 +1,581 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import path from "path";
5
+
6
+ // Reroute homedir() so global index writes/reads land in a temp dir.
7
+ let testHome: string;
8
+
9
+ vi.mock("os", async () => {
10
+ const actual = await vi.importActual<typeof import("os")>("os");
11
+ return {
12
+ ...actual,
13
+ homedir: () => testHome,
14
+ };
15
+ });
16
+
17
+ beforeEach(async () => {
18
+ testHome = await mkdtemp(path.join(tmpdir(), "ay-sub-test-"));
19
+ vi.resetModules();
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await rm(testHome, { recursive: true, force: true }).catch(() => null);
24
+ });
25
+
26
+ async function loadModule() {
27
+ return await import("./subcommands.ts");
28
+ }
29
+
30
+ describe("subcommands.parseArgs", () => {
31
+ it("collects positional args and bare flags", async () => {
32
+ const { parseArgs } = await loadModule();
33
+ const out = parseArgs(["foo", "bar", "--all"]);
34
+ expect(out.positional).toEqual(["foo", "bar"]);
35
+ expect(out.flags.all).toBe(true);
36
+ });
37
+
38
+ it("parses --key=value form", async () => {
39
+ const { parseArgs } = await loadModule();
40
+ const out = parseArgs(["--code=enter"]);
41
+ expect(out.flags.code).toBe("enter");
42
+ });
43
+
44
+ it("parses --key value form for non-boolean keys", async () => {
45
+ const { parseArgs } = await loadModule();
46
+ const out = parseArgs(["--cwd", "/tmp/foo"]);
47
+ expect(out.flags.cwd).toBe("/tmp/foo");
48
+ });
49
+
50
+ it("treats well-known boolean flags as boolean even with a following positional", async () => {
51
+ const { parseArgs } = await loadModule();
52
+ const out = parseArgs(["--all", "claude"]);
53
+ expect(out.flags.all).toBe(true);
54
+ expect(out.positional).toEqual(["claude"]);
55
+ });
56
+
57
+ it("supports -n N short form", async () => {
58
+ const { parseArgs } = await loadModule();
59
+ const out = parseArgs(["-n", "50", "keyword"]);
60
+ expect(out.flags.n).toBe("50");
61
+ expect(out.positional).toEqual(["keyword"]);
62
+ });
63
+ });
64
+
65
+ describe("subcommands.controlCodeFromName", () => {
66
+ it("maps named codes to the right control bytes", async () => {
67
+ const { controlCodeFromName } = await loadModule();
68
+ expect(controlCodeFromName("enter")).toBe("\r");
69
+ expect(controlCodeFromName("cr")).toBe("\r");
70
+ expect(controlCodeFromName("esc")).toBe("\x1b");
71
+ expect(controlCodeFromName("ctrl-c")).toBe("\x03");
72
+ expect(controlCodeFromName("ctrl-y")).toBe("\x19");
73
+ expect(controlCodeFromName("ctrl-d")).toBe("\x04");
74
+ expect(controlCodeFromName("tab")).toBe("\t");
75
+ expect(controlCodeFromName("none")).toBe("");
76
+ expect(controlCodeFromName("")).toBe("");
77
+ });
78
+
79
+ it("supports raw:0xNN escape", async () => {
80
+ const { controlCodeFromName } = await loadModule();
81
+ expect(controlCodeFromName("raw:0x03")).toBe("\x03");
82
+ expect(controlCodeFromName("raw:0x1b")).toBe("\x1b");
83
+ });
84
+
85
+ it("throws on unknown code names", async () => {
86
+ const { controlCodeFromName } = await loadModule();
87
+ expect(() => controlCodeFromName("nope")).toThrow(/unknown --code/);
88
+ });
89
+ });
90
+
91
+ describe("subcommands.matchKeyword", () => {
92
+ const baseRecord = {
93
+ pid: 1234,
94
+ cli: "claude",
95
+ prompt: "fix the parser bug",
96
+ cwd: "/v1/code/snomiao/agent-yes",
97
+ log_file: null,
98
+ status: "active" as const,
99
+ exit_code: null,
100
+ exit_reason: null,
101
+ started_at: 0,
102
+ };
103
+
104
+ it("matches by exact pid", async () => {
105
+ const { matchKeyword } = await loadModule();
106
+ expect(matchKeyword(baseRecord, "1234")).toBe(true);
107
+ expect(matchKeyword(baseRecord, "9999")).toBe(false);
108
+ });
109
+
110
+ it("matches by cwd substring (case-insensitive)", async () => {
111
+ const { matchKeyword } = await loadModule();
112
+ expect(matchKeyword(baseRecord, "agent-yes")).toBe(true);
113
+ expect(matchKeyword(baseRecord, "AGENT-YES")).toBe(true);
114
+ expect(matchKeyword(baseRecord, "different-project")).toBe(false);
115
+ });
116
+
117
+ it("matches by exact cli name", async () => {
118
+ const { matchKeyword } = await loadModule();
119
+ expect(matchKeyword(baseRecord, "claude")).toBe(true);
120
+ expect(matchKeyword(baseRecord, "codex")).toBe(false);
121
+ });
122
+
123
+ it("matches by prompt substring", async () => {
124
+ const { matchKeyword } = await loadModule();
125
+ expect(matchKeyword(baseRecord, "parser")).toBe(true);
126
+ expect(matchKeyword(baseRecord, "rocketship")).toBe(false);
127
+ });
128
+
129
+ it("returns true for empty keyword (no filter)", async () => {
130
+ const { matchKeyword } = await loadModule();
131
+ expect(matchKeyword(baseRecord, "")).toBe(true);
132
+ });
133
+
134
+ it("ignores prompt match if prompt is null", async () => {
135
+ const { matchKeyword } = await loadModule();
136
+ const r = { ...baseRecord, prompt: null };
137
+ expect(matchKeyword(r, "parser")).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe("subcommands.runSubcommand routing", () => {
142
+ it("returns null for unknown subcommands so cli.ts falls through", async () => {
143
+ const { runSubcommand } = await loadModule();
144
+ const code = await runSubcommand(["bun", "cli.js", "definitely-not-a-cmd"]);
145
+ expect(code).toBeNull();
146
+ });
147
+
148
+ it("ls on an empty index prints 'no running agents'", async () => {
149
+ const { runSubcommand } = await loadModule();
150
+ const stderr: string[] = [];
151
+ const orig = process.stderr.write.bind(process.stderr);
152
+ (process.stderr as any).write = (s: any) => {
153
+ stderr.push(String(s));
154
+ return true;
155
+ };
156
+ try {
157
+ const code = await runSubcommand(["bun", "cli.js", "ls"]);
158
+ expect(code).toBe(0);
159
+ expect(stderr.join("")).toMatch(/no running agents/);
160
+ } finally {
161
+ process.stderr.write = orig;
162
+ }
163
+ });
164
+
165
+ it("ls --json emits a parseable JSON array", async () => {
166
+ const mod = await loadModule();
167
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
168
+ await appendGlobalPid({
169
+ pid: process.pid,
170
+ cli: "claude",
171
+ prompt: "live test",
172
+ cwd: process.cwd(),
173
+ log_file: null,
174
+ status: "active",
175
+ exit_code: null,
176
+ exit_reason: null,
177
+ started_at: Date.now(),
178
+ });
179
+
180
+ const stdout: string[] = [];
181
+ const orig = process.stdout.write.bind(process.stdout);
182
+ (process.stdout as any).write = (s: any) => {
183
+ stdout.push(String(s));
184
+ return true;
185
+ };
186
+ try {
187
+ const code = await mod.runSubcommand(["bun", "cli.js", "ls", "--json"]);
188
+ expect(code).toBe(0);
189
+ } finally {
190
+ process.stdout.write = orig;
191
+ }
192
+
193
+ const parsed = JSON.parse(stdout.join(""));
194
+ expect(Array.isArray(parsed)).toBe(true);
195
+ expect(parsed[0]).toMatchObject({ pid: process.pid, cli: "claude" });
196
+ });
197
+
198
+ it("read errors cleanly when keyword resolves to no agent", async () => {
199
+ const { runSubcommand } = await loadModule();
200
+ const stderr: string[] = [];
201
+ const orig = process.stderr.write.bind(process.stderr);
202
+ (process.stderr as any).write = (s: any) => {
203
+ stderr.push(String(s));
204
+ return true;
205
+ };
206
+ try {
207
+ const code = await runSubcommand(["bun", "cli.js", "read", "no-such-agent-keyword"]);
208
+ expect(code).toBe(1);
209
+ expect(stderr.join("")).toMatch(/no running agent matched/);
210
+ } finally {
211
+ process.stderr.write = orig;
212
+ }
213
+ });
214
+
215
+ it("send refuses when missing arguments", async () => {
216
+ const { runSubcommand } = await loadModule();
217
+ const stderr: string[] = [];
218
+ const orig = process.stderr.write.bind(process.stderr);
219
+ (process.stderr as any).write = (s: any) => {
220
+ stderr.push(String(s));
221
+ return true;
222
+ };
223
+ try {
224
+ const code = await runSubcommand(["bun", "cli.js", "send"]);
225
+ expect(code).toBe(1);
226
+ expect(stderr.join("")).toMatch(/usage:/);
227
+ } finally {
228
+ process.stderr.write = orig;
229
+ }
230
+ });
231
+
232
+ it("send errors when matched record has no fifo_file", async () => {
233
+ const mod = await loadModule();
234
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
235
+ await appendGlobalPid({
236
+ pid: process.pid,
237
+ cli: "claude",
238
+ prompt: "no-fifo-test",
239
+ cwd: process.cwd(),
240
+ log_file: null,
241
+ fifo_file: null, // explicitly missing — old Rust agent
242
+ status: "active",
243
+ exit_code: null,
244
+ exit_reason: null,
245
+ started_at: Date.now(),
246
+ });
247
+
248
+ const stderr: string[] = [];
249
+ const orig = process.stderr.write.bind(process.stderr);
250
+ (process.stderr as any).write = (s: any) => {
251
+ stderr.push(String(s));
252
+ return true;
253
+ };
254
+ try {
255
+ const code = await mod.runSubcommand([
256
+ "bun",
257
+ "cli.js",
258
+ "send",
259
+ String(process.pid),
260
+ "anything",
261
+ ]);
262
+ expect(code).toBe(1);
263
+ expect(stderr.join("")).toMatch(/no fifo_file recorded/);
264
+ } finally {
265
+ process.stderr.write = orig;
266
+ }
267
+ });
268
+ });
269
+
270
+ describe("subcommands.cmdLs human table", () => {
271
+ function captureStdout() {
272
+ const chunks: string[] = [];
273
+ const orig = process.stdout.write.bind(process.stdout);
274
+ (process.stdout as any).write = (s: any) => {
275
+ chunks.push(String(s));
276
+ return true;
277
+ };
278
+ return {
279
+ get text() {
280
+ return chunks.join("");
281
+ },
282
+ restore() {
283
+ process.stdout.write = orig;
284
+ },
285
+ };
286
+ }
287
+
288
+ it("prints a header and row for each record", async () => {
289
+ const { runSubcommand } = await loadModule();
290
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
291
+ await appendGlobalPid({
292
+ pid: process.pid,
293
+ cli: "claude",
294
+ prompt: "table format test",
295
+ cwd: process.cwd(),
296
+ log_file: null,
297
+ status: "active",
298
+ exit_code: null,
299
+ exit_reason: null,
300
+ started_at: Date.now() - 5000,
301
+ });
302
+
303
+ const cap = captureStdout();
304
+ try {
305
+ const code = await runSubcommand(["bun", "cli.js", "ls"]);
306
+ expect(code).toBe(0);
307
+ } finally {
308
+ cap.restore();
309
+ }
310
+ expect(cap.text).toMatch(/PID\s+CLI\s+STATUS\s+AGE\s+CWD\s+PROMPT/);
311
+ expect(cap.text).toMatch(new RegExp(`${process.pid}\\s`));
312
+ expect(cap.text).toMatch(/claude/);
313
+ expect(cap.text).toMatch(/table format test/);
314
+ });
315
+
316
+ it("renders ages across seconds/minutes/hours/days correctly", async () => {
317
+ const { runSubcommand } = await loadModule();
318
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
319
+ const now = Date.now();
320
+ // four records with ages spanning the four units; use distinct fake pids
321
+ // that won't pass liveOnly, so use process.pid for one and --all for full.
322
+ await appendGlobalPid({
323
+ pid: process.pid,
324
+ cli: "claude",
325
+ prompt: "x",
326
+ cwd: process.cwd(),
327
+ log_file: null,
328
+ status: "active",
329
+ exit_code: null,
330
+ exit_reason: null,
331
+ started_at: now - 2_000, // 2s
332
+ });
333
+
334
+ const cap = captureStdout();
335
+ try {
336
+ await runSubcommand(["bun", "cli.js", "ls"]);
337
+ } finally {
338
+ cap.restore();
339
+ }
340
+ // age column should show "2s"
341
+ expect(cap.text).toMatch(/\b2s\b/);
342
+ });
343
+
344
+ it("scopes to --cwd <dir>", async () => {
345
+ const { runSubcommand } = await loadModule();
346
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
347
+ await appendGlobalPid({
348
+ pid: process.pid,
349
+ cli: "claude",
350
+ prompt: "should appear",
351
+ cwd: process.cwd(),
352
+ log_file: null,
353
+ status: "active",
354
+ exit_code: null,
355
+ exit_reason: null,
356
+ started_at: Date.now(),
357
+ });
358
+ const otherCwd = await mkdtemp(path.join(tmpdir(), "ay-other-"));
359
+ try {
360
+ // No record under otherCwd → scoped ls finds nothing
361
+ const stderr: string[] = [];
362
+ const orig = process.stderr.write.bind(process.stderr);
363
+ (process.stderr as any).write = (s: any) => {
364
+ stderr.push(String(s));
365
+ return true;
366
+ };
367
+ try {
368
+ const code = await runSubcommand(["bun", "cli.js", "ls", "--cwd", otherCwd]);
369
+ expect(code).toBe(0);
370
+ expect(stderr.join("")).toMatch(/no running agents/);
371
+ } finally {
372
+ process.stderr.write = orig;
373
+ }
374
+ } finally {
375
+ await rm(otherCwd, { recursive: true, force: true }).catch(() => null);
376
+ }
377
+ });
378
+ });
379
+
380
+ describe("subcommands.cmdRead renders raw log via xterm-headless", () => {
381
+ it("tail -n N emits last N lines of rendered output", async () => {
382
+ const { runSubcommand } = await loadModule();
383
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
384
+ // Build a tiny synthetic raw log: 100 newline-separated lines.
385
+ const tmp = await mkdtemp(path.join(tmpdir(), "ay-raw-log-"));
386
+ try {
387
+ const logPath = path.join(tmp, "x.raw.log");
388
+ const lines: string[] = [];
389
+ for (let i = 0; i < 100; i++) lines.push(`line-${i}`);
390
+ await writeFile(logPath, lines.join("\r\n") + "\r\n");
391
+
392
+ await appendGlobalPid({
393
+ pid: process.pid,
394
+ cli: "claude",
395
+ prompt: null,
396
+ cwd: process.cwd(),
397
+ log_file: logPath,
398
+ status: "active",
399
+ exit_code: null,
400
+ exit_reason: null,
401
+ started_at: Date.now(),
402
+ });
403
+
404
+ const stdout: string[] = [];
405
+ const orig = process.stdout.write.bind(process.stdout);
406
+ (process.stdout as any).write = (s: any) => {
407
+ stdout.push(String(s));
408
+ return true;
409
+ };
410
+ try {
411
+ const code = await runSubcommand(["bun", "cli.js", "tail", String(process.pid), "-n", "5"]);
412
+ expect(code).toBe(0);
413
+ } finally {
414
+ process.stdout.write = orig;
415
+ }
416
+ const text = stdout.join("");
417
+ // last 5 lines should be 95..99
418
+ expect(text).toMatch(/line-99/);
419
+ expect(text).toMatch(/line-95/);
420
+ // earlier lines should NOT be in output
421
+ expect(text).not.toMatch(/line-50\b/);
422
+ } finally {
423
+ await rm(tmp, { recursive: true, force: true }).catch(() => null);
424
+ }
425
+ });
426
+
427
+ it("read errors when log_file path is missing on disk", async () => {
428
+ const { runSubcommand } = await loadModule();
429
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
430
+ await appendGlobalPid({
431
+ pid: process.pid,
432
+ cli: "claude",
433
+ prompt: null,
434
+ cwd: process.cwd(),
435
+ log_file: "/nonexistent/path/to/log",
436
+ status: "active",
437
+ exit_code: null,
438
+ exit_reason: null,
439
+ started_at: Date.now(),
440
+ });
441
+ const stderr: string[] = [];
442
+ const orig = process.stderr.write.bind(process.stderr);
443
+ (process.stderr as any).write = (s: any) => {
444
+ stderr.push(String(s));
445
+ return true;
446
+ };
447
+ try {
448
+ const code = await runSubcommand(["bun", "cli.js", "read", String(process.pid)]);
449
+ expect(code).toBe(1);
450
+ expect(stderr.join("")).toMatch(/log file not found/);
451
+ } finally {
452
+ process.stderr.write = orig;
453
+ }
454
+ });
455
+ });
456
+
457
+ describe("subcommands.cmdSend writes bytes to FIFO", () => {
458
+ // Skip on non-unix because FIFO creation requires mkfifo
459
+ const itUnix = process.platform === "linux" || process.platform === "darwin";
460
+
461
+ it.skipIf(!itUnix)("delivers a message to a real FIFO", async () => {
462
+ const { runSubcommand } = await loadModule();
463
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
464
+ const { spawnSync } = await import("child_process");
465
+ const tmp = await mkdtemp(path.join(tmpdir(), "ay-fifo-"));
466
+ try {
467
+ const fifo = path.join(tmp, "test.fifo");
468
+ const r = spawnSync("mkfifo", [fifo]);
469
+ if (r.status !== 0) {
470
+ // mkfifo unavailable — skip
471
+ return;
472
+ }
473
+
474
+ // Open RDWR side first (matches Rust behaviour) so writes don't block.
475
+ const fs = await import("fs");
476
+ const rdwrFd = fs.openSync(fifo, fs.constants.O_RDWR);
477
+
478
+ await appendGlobalPid({
479
+ pid: process.pid,
480
+ cli: "claude",
481
+ prompt: null,
482
+ cwd: process.cwd(),
483
+ log_file: null,
484
+ fifo_file: fifo,
485
+ status: "active",
486
+ exit_code: null,
487
+ exit_reason: null,
488
+ started_at: Date.now(),
489
+ });
490
+
491
+ const stdout: string[] = [];
492
+ const orig = process.stdout.write.bind(process.stdout);
493
+ (process.stdout as any).write = (s: any) => {
494
+ stdout.push(String(s));
495
+ return true;
496
+ };
497
+ try {
498
+ const code = await runSubcommand([
499
+ "bun",
500
+ "cli.js",
501
+ "send",
502
+ String(process.pid),
503
+ "hello-fifo",
504
+ ]);
505
+ expect(code).toBe(0);
506
+ expect(stdout.join("")).toMatch(/sent to pid/);
507
+ } finally {
508
+ process.stdout.write = orig;
509
+ }
510
+
511
+ // Now read the bytes back from our RDWR fd.
512
+ const buf = Buffer.alloc(4096);
513
+ const n = fs.readSync(rdwrFd, buf, 0, buf.length, null);
514
+ const received = buf.subarray(0, n).toString();
515
+ expect(received).toBe("hello-fifo\r");
516
+ fs.closeSync(rdwrFd);
517
+ } finally {
518
+ await rm(tmp, { recursive: true, force: true }).catch(() => null);
519
+ }
520
+ });
521
+
522
+ it("--code=none skips the trailing CR", async () => {
523
+ const { controlCodeFromName } = await loadModule();
524
+ expect(controlCodeFromName("none")).toBe("");
525
+ });
526
+ });
527
+
528
+ describe("subcommands.listRecords merges per-cwd TS file with global", () => {
529
+ it("includes records from <cwd>/.agent-yes/pid-records.jsonl", async () => {
530
+ // Write a fake per-cwd file that uses the live process pid so liveOnly
531
+ // doesn't drop it.
532
+ const cwd = await mkdtemp(path.join(tmpdir(), "ay-pcwd-"));
533
+ try {
534
+ const dir = path.join(cwd, ".agent-yes");
535
+ await mkdir(dir, { recursive: true });
536
+ const file = path.join(dir, "pid-records.jsonl");
537
+ const record = {
538
+ _id: "abc123",
539
+ pid: process.pid,
540
+ cli: "claude",
541
+ prompt: "merged test",
542
+ cwd,
543
+ logFile: "/dev/null",
544
+ fifoFile: "/dev/null",
545
+ status: "active",
546
+ exitReason: "",
547
+ startedAt: Date.now(),
548
+ };
549
+ await writeFile(file, JSON.stringify(record) + "\n");
550
+
551
+ const origCwd = process.cwd();
552
+ process.chdir(cwd);
553
+ try {
554
+ const mod = await loadModule();
555
+ const stdout: string[] = [];
556
+ const orig = process.stdout.write.bind(process.stdout);
557
+ (process.stdout as any).write = (s: any) => {
558
+ stdout.push(String(s));
559
+ return true;
560
+ };
561
+ try {
562
+ const code = await mod.runSubcommand(["bun", "cli.js", "ls", "--json"]);
563
+ expect(code).toBe(0);
564
+ } finally {
565
+ process.stdout.write = orig;
566
+ }
567
+ const parsed = JSON.parse(stdout.join(""));
568
+ expect(parsed).toHaveLength(1);
569
+ expect(parsed[0]).toMatchObject({
570
+ pid: process.pid,
571
+ cli: "claude",
572
+ prompt: "merged test",
573
+ });
574
+ } finally {
575
+ process.chdir(origCwd);
576
+ }
577
+ } finally {
578
+ await rm(cwd, { recursive: true, force: true }).catch(() => null);
579
+ }
580
+ });
581
+ });