tmux-team 1.0.0 → 2.0.0-alpha.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,652 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Talk Command Tests - --delay, --wait, preambles, nonce detection
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import type { Context, Tmux, UI, Paths, ResolvedConfig, Flags } from '../types.js';
10
+ import { ExitCodes } from '../exits.js';
11
+ import { cmdTalk } from './talk.js';
12
+
13
+ // ─────────────────────────────────────────────────────────────
14
+ // Test utilities
15
+ // ─────────────────────────────────────────────────────────────
16
+
17
+ function createMockTmux(): Tmux & {
18
+ sends: Array<{ pane: string; message: string }>;
19
+ captureReturn: string;
20
+ } {
21
+ const mock = {
22
+ sends: [] as Array<{ pane: string; message: string }>,
23
+ captureReturn: '',
24
+ send(pane: string, message: string) {
25
+ mock.sends.push({ pane, message });
26
+ },
27
+ capture(_pane: string, _lines: number) {
28
+ return mock.captureReturn;
29
+ },
30
+ };
31
+ return mock;
32
+ }
33
+
34
+ function createMockUI(): UI & { errors: string[]; warnings: string[]; jsonOutput: unknown[] } {
35
+ const mock = {
36
+ errors: [] as string[],
37
+ warnings: [] as string[],
38
+ jsonOutput: [] as unknown[],
39
+ info: vi.fn(),
40
+ success: vi.fn(),
41
+ warn: (msg: string) => mock.warnings.push(msg),
42
+ error: (msg: string) => mock.errors.push(msg),
43
+ table: vi.fn(),
44
+ json: (data: unknown) => mock.jsonOutput.push(data),
45
+ };
46
+ return mock;
47
+ }
48
+
49
+ function createTestPaths(testDir: string): Paths {
50
+ return {
51
+ globalDir: testDir,
52
+ globalConfig: path.join(testDir, 'config.json'),
53
+ localConfig: path.join(testDir, 'tmux-team.json'),
54
+ stateFile: path.join(testDir, 'state.json'),
55
+ };
56
+ }
57
+
58
+ function createDefaultConfig(): ResolvedConfig {
59
+ return {
60
+ mode: 'polling',
61
+ preambleMode: 'always',
62
+ defaults: {
63
+ timeout: 60,
64
+ pollInterval: 0.1, // Fast polling for tests
65
+ captureLines: 100,
66
+ },
67
+ agents: {},
68
+ paneRegistry: {
69
+ claude: { pane: '1.0', remark: 'Test agent' },
70
+ codex: { pane: '1.1' },
71
+ gemini: { pane: '1.2' },
72
+ },
73
+ };
74
+ }
75
+
76
+ function createContext(
77
+ overrides: Partial<{
78
+ tmux: Tmux;
79
+ ui: UI;
80
+ config: Partial<ResolvedConfig>;
81
+ flags: Partial<Flags>;
82
+ paths: Paths;
83
+ }>
84
+ ): Context {
85
+ const exitError = new Error('exit called');
86
+ (exitError as Error & { exitCode?: number }).exitCode = 0;
87
+
88
+ const config = { ...createDefaultConfig(), ...overrides.config };
89
+ const flags: Flags = { json: false, verbose: false, ...overrides.flags };
90
+
91
+ return {
92
+ argv: [],
93
+ flags,
94
+ ui: overrides.ui || createMockUI(),
95
+ config,
96
+ tmux: overrides.tmux || createMockTmux(),
97
+ paths: overrides.paths || createTestPaths('/tmp/test'),
98
+ exit: ((code: number) => {
99
+ const err = new Error(`exit(${code})`);
100
+ (err as Error & { exitCode: number }).exitCode = code;
101
+ throw err;
102
+ }) as (code: number) => never,
103
+ };
104
+ }
105
+
106
+ // ─────────────────────────────────────────────────────────────
107
+ // Tests
108
+ // ─────────────────────────────────────────────────────────────
109
+
110
+ describe('buildMessage (via cmdTalk)', () => {
111
+ let testDir: string;
112
+
113
+ beforeEach(() => {
114
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
115
+ });
116
+
117
+ afterEach(() => {
118
+ if (fs.existsSync(testDir)) {
119
+ fs.rmSync(testDir, { recursive: true, force: true });
120
+ }
121
+ });
122
+
123
+ it('returns original message when preambleMode is disabled', async () => {
124
+ const tmux = createMockTmux();
125
+ const ctx = createContext({
126
+ tmux,
127
+ paths: createTestPaths(testDir),
128
+ config: { preambleMode: 'disabled' },
129
+ });
130
+
131
+ await cmdTalk(ctx, 'claude', 'Hello');
132
+
133
+ expect(tmux.sends).toHaveLength(1);
134
+ expect(tmux.sends[0].message).toBe('Hello');
135
+ });
136
+
137
+ it('returns original message when --no-preamble flag is set', async () => {
138
+ const tmux = createMockTmux();
139
+ const ctx = createContext({
140
+ tmux,
141
+ paths: createTestPaths(testDir),
142
+ flags: { noPreamble: true },
143
+ config: {
144
+ preambleMode: 'always',
145
+ agents: { claude: { preamble: 'Be brief' } },
146
+ },
147
+ });
148
+
149
+ await cmdTalk(ctx, 'claude', 'Hello');
150
+
151
+ expect(tmux.sends).toHaveLength(1);
152
+ expect(tmux.sends[0].message).toBe('Hello');
153
+ });
154
+
155
+ it('returns original message when agent has no preamble', async () => {
156
+ const tmux = createMockTmux();
157
+ const ctx = createContext({
158
+ tmux,
159
+ paths: createTestPaths(testDir),
160
+ config: { preambleMode: 'always', agents: {} },
161
+ });
162
+
163
+ await cmdTalk(ctx, 'claude', 'Hello');
164
+
165
+ expect(tmux.sends).toHaveLength(1);
166
+ expect(tmux.sends[0].message).toBe('Hello');
167
+ });
168
+
169
+ it('prepends [SYSTEM: preamble] when preambleMode is always', async () => {
170
+ const tmux = createMockTmux();
171
+ const ctx = createContext({
172
+ tmux,
173
+ paths: createTestPaths(testDir),
174
+ config: {
175
+ preambleMode: 'always',
176
+ agents: { claude: { preamble: 'Be helpful and concise' } },
177
+ },
178
+ });
179
+
180
+ await cmdTalk(ctx, 'claude', 'Hello');
181
+
182
+ expect(tmux.sends).toHaveLength(1);
183
+ expect(tmux.sends[0].message).toContain('[SYSTEM: Be helpful and concise]');
184
+ expect(tmux.sends[0].message).toContain('Hello');
185
+ });
186
+
187
+ it('formats preamble as [SYSTEM: <preamble>]\\n\\n<message>', async () => {
188
+ const tmux = createMockTmux();
189
+ const ctx = createContext({
190
+ tmux,
191
+ paths: createTestPaths(testDir),
192
+ config: {
193
+ preambleMode: 'always',
194
+ agents: { claude: { preamble: 'Test preamble' } },
195
+ },
196
+ });
197
+
198
+ await cmdTalk(ctx, 'claude', 'Test message');
199
+
200
+ expect(tmux.sends[0].message).toBe('[SYSTEM: Test preamble]\n\nTest message');
201
+ });
202
+ });
203
+
204
+ describe('cmdTalk - basic send', () => {
205
+ let testDir: string;
206
+
207
+ beforeEach(() => {
208
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
209
+ });
210
+
211
+ afterEach(() => {
212
+ if (fs.existsSync(testDir)) {
213
+ fs.rmSync(testDir, { recursive: true, force: true });
214
+ }
215
+ });
216
+
217
+ it('sends message to specified agent pane', async () => {
218
+ const tmux = createMockTmux();
219
+ const ctx = createContext({ tmux, paths: createTestPaths(testDir) });
220
+
221
+ await cmdTalk(ctx, 'claude', 'Hello Claude');
222
+
223
+ expect(tmux.sends).toHaveLength(1);
224
+ expect(tmux.sends[0].pane).toBe('1.0');
225
+ expect(tmux.sends[0].message).toBe('Hello Claude');
226
+ });
227
+
228
+ it('sends message to all configured agents', async () => {
229
+ const tmux = createMockTmux();
230
+ const ctx = createContext({ tmux, paths: createTestPaths(testDir) });
231
+
232
+ await cmdTalk(ctx, 'all', 'Hello everyone');
233
+
234
+ expect(tmux.sends).toHaveLength(3);
235
+ expect(tmux.sends.map((s) => s.pane).sort()).toEqual(['1.0', '1.1', '1.2']);
236
+ });
237
+
238
+ it('removes exclamation marks for gemini agent', async () => {
239
+ const tmux = createMockTmux();
240
+ const ctx = createContext({ tmux, paths: createTestPaths(testDir) });
241
+
242
+ await cmdTalk(ctx, 'gemini', 'Hello! This is exciting!');
243
+
244
+ expect(tmux.sends).toHaveLength(1);
245
+ expect(tmux.sends[0].message).toBe('Hello This is exciting');
246
+ });
247
+
248
+ it('exits with error for unknown agent', async () => {
249
+ const ui = createMockUI();
250
+ const ctx = createContext({ ui, paths: createTestPaths(testDir) });
251
+
252
+ await expect(cmdTalk(ctx, 'unknown', 'Hello')).rejects.toThrow('exit(3)');
253
+
254
+ expect(ui.errors).toHaveLength(1);
255
+ expect(ui.errors[0]).toContain("Agent 'unknown' not found");
256
+ });
257
+
258
+ it('outputs JSON when --json flag is set', async () => {
259
+ const tmux = createMockTmux();
260
+ const ui = createMockUI();
261
+ const ctx = createContext({
262
+ tmux,
263
+ ui,
264
+ paths: createTestPaths(testDir),
265
+ flags: { json: true },
266
+ });
267
+
268
+ await cmdTalk(ctx, 'claude', 'Hello');
269
+
270
+ expect(ui.jsonOutput).toHaveLength(1);
271
+ expect(ui.jsonOutput[0]).toMatchObject({
272
+ target: 'claude',
273
+ pane: '1.0',
274
+ status: 'sent',
275
+ });
276
+ });
277
+ });
278
+
279
+ describe('cmdTalk - --delay flag', () => {
280
+ let testDir: string;
281
+
282
+ beforeEach(() => {
283
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
284
+ vi.useFakeTimers();
285
+ });
286
+
287
+ afterEach(() => {
288
+ vi.useRealTimers();
289
+ if (fs.existsSync(testDir)) {
290
+ fs.rmSync(testDir, { recursive: true, force: true });
291
+ }
292
+ });
293
+
294
+ it('waits specified seconds before sending', async () => {
295
+ const tmux = createMockTmux();
296
+ const ctx = createContext({
297
+ tmux,
298
+ paths: createTestPaths(testDir),
299
+ flags: { delay: 2 },
300
+ });
301
+
302
+ const promise = cmdTalk(ctx, 'claude', 'Hello');
303
+
304
+ // Before delay, no message sent
305
+ expect(tmux.sends).toHaveLength(0);
306
+
307
+ // Advance time
308
+ await vi.advanceTimersByTimeAsync(2000);
309
+ await promise;
310
+
311
+ expect(tmux.sends).toHaveLength(1);
312
+ });
313
+ });
314
+
315
+ describe('cmdTalk - --wait mode', () => {
316
+ let testDir: string;
317
+
318
+ beforeEach(() => {
319
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
320
+ });
321
+
322
+ afterEach(() => {
323
+ if (fs.existsSync(testDir)) {
324
+ fs.rmSync(testDir, { recursive: true, force: true });
325
+ }
326
+ });
327
+
328
+ it('appends nonce instruction to message', async () => {
329
+ const tmux = createMockTmux();
330
+ // Set up capture to return the nonce marker immediately
331
+ let captureCount = 0;
332
+ tmux.capture = () => {
333
+ captureCount++;
334
+ if (captureCount === 1) return ''; // Baseline
335
+ // Return marker on second capture
336
+ const sent = tmux.sends[0]?.message || '';
337
+ const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
338
+ return match ? `Response here {tmux-team-end:${match[1]}}` : '';
339
+ };
340
+
341
+ const ctx = createContext({
342
+ tmux,
343
+ paths: createTestPaths(testDir),
344
+ flags: { wait: true, timeout: 5 },
345
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
346
+ });
347
+
348
+ await cmdTalk(ctx, 'claude', 'Hello');
349
+
350
+ expect(tmux.sends).toHaveLength(1);
351
+ expect(tmux.sends[0].message).toContain(
352
+ '[IMPORTANT: When your response is complete, print exactly:'
353
+ );
354
+ expect(tmux.sends[0].message).toMatch(/\{tmux-team-end:[a-f0-9]+\}/);
355
+ });
356
+
357
+ it('detects nonce marker and extracts response', async () => {
358
+ const tmux = createMockTmux();
359
+ const ui = createMockUI();
360
+
361
+ let captureCount = 0;
362
+
363
+ tmux.capture = () => {
364
+ captureCount++;
365
+ if (captureCount === 1) return 'baseline content';
366
+ // Extract nonce from sent message and return matching marker
367
+ const sent = tmux.sends[0]?.message || '';
368
+ const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
369
+ if (match) {
370
+ return `baseline content\n\nAgent response here\n\n{tmux-team-end:${match[1]}}`;
371
+ }
372
+ return 'baseline content';
373
+ };
374
+
375
+ const ctx = createContext({
376
+ tmux,
377
+ ui,
378
+ paths: createTestPaths(testDir),
379
+ flags: { wait: true, json: true, timeout: 5 },
380
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
381
+ });
382
+
383
+ await cmdTalk(ctx, 'claude', 'Hello');
384
+
385
+ expect(ui.jsonOutput).toHaveLength(1);
386
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
387
+ expect(output.status).toBe('completed');
388
+ expect(output.response).toBeDefined();
389
+ });
390
+
391
+ it('returns timeout error with correct exit code', async () => {
392
+ const tmux = createMockTmux();
393
+ const ui = createMockUI();
394
+
395
+ // Capture never returns the marker
396
+ tmux.capture = () => 'no marker here';
397
+
398
+ const ctx = createContext({
399
+ tmux,
400
+ ui,
401
+ paths: createTestPaths(testDir),
402
+ flags: { wait: true, json: true, timeout: 0.1 },
403
+ config: { defaults: { timeout: 0.1, pollInterval: 0.02, captureLines: 100 } },
404
+ });
405
+
406
+ try {
407
+ await cmdTalk(ctx, 'claude', 'Hello');
408
+ expect.fail('Should have thrown');
409
+ } catch (err) {
410
+ const error = err as Error & { exitCode: number };
411
+ expect(error.exitCode).toBe(ExitCodes.TIMEOUT);
412
+ }
413
+
414
+ expect(ui.jsonOutput).toHaveLength(1);
415
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
416
+ expect(output.status).toBe('timeout');
417
+ expect(output.error).toContain('Timed out');
418
+ });
419
+
420
+ it('isolates response from baseline using scrollback', async () => {
421
+ const tmux = createMockTmux();
422
+ const ui = createMockUI();
423
+
424
+ let captureCount = 0;
425
+ const baseline = 'Previous conversation\nOld content here';
426
+
427
+ tmux.capture = () => {
428
+ captureCount++;
429
+ if (captureCount === 1) return baseline;
430
+ // Second capture includes baseline + new content + marker
431
+ const sent = tmux.sends[0]?.message || '';
432
+ const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
433
+ if (match) {
434
+ return `${baseline}\n\nNew response content\n\n{tmux-team-end:${match[1]}}`;
435
+ }
436
+ return baseline;
437
+ };
438
+
439
+ const ctx = createContext({
440
+ tmux,
441
+ ui,
442
+ paths: createTestPaths(testDir),
443
+ flags: { wait: true, json: true, timeout: 5 },
444
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
445
+ });
446
+
447
+ await cmdTalk(ctx, 'claude', 'Hello');
448
+
449
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
450
+ expect(output.status).toBe('completed');
451
+ // Response should NOT include baseline content
452
+ expect(output.response).toBe('New response content');
453
+ });
454
+
455
+ it('clears active request on completion', async () => {
456
+ const tmux = createMockTmux();
457
+ let captureCount = 0;
458
+
459
+ tmux.capture = () => {
460
+ captureCount++;
461
+ if (captureCount === 1) return '';
462
+ const sent = tmux.sends[0]?.message || '';
463
+ const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
464
+ return match ? `Done {tmux-team-end:${match[1]}}` : '';
465
+ };
466
+
467
+ const paths = createTestPaths(testDir);
468
+ const ctx = createContext({
469
+ tmux,
470
+ paths,
471
+ flags: { wait: true, timeout: 5 },
472
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
473
+ });
474
+
475
+ await cmdTalk(ctx, 'claude', 'Hello');
476
+
477
+ // Check state file is cleaned up
478
+ if (fs.existsSync(paths.stateFile)) {
479
+ const state = JSON.parse(fs.readFileSync(paths.stateFile, 'utf-8'));
480
+ expect(state.requests.claude).toBeUndefined();
481
+ }
482
+ });
483
+
484
+ it('clears active request on timeout', async () => {
485
+ const tmux = createMockTmux();
486
+ tmux.capture = () => 'no marker';
487
+
488
+ const paths = createTestPaths(testDir);
489
+ const ctx = createContext({
490
+ tmux,
491
+ paths,
492
+ flags: { wait: true, timeout: 0.05 },
493
+ config: { defaults: { timeout: 0.05, pollInterval: 0.01, captureLines: 100 } },
494
+ });
495
+
496
+ try {
497
+ await cmdTalk(ctx, 'claude', 'Hello');
498
+ } catch {
499
+ // Expected timeout
500
+ }
501
+
502
+ // Check state file is cleaned up
503
+ if (fs.existsSync(paths.stateFile)) {
504
+ const state = JSON.parse(fs.readFileSync(paths.stateFile, 'utf-8'));
505
+ expect(state.requests.claude).toBeUndefined();
506
+ }
507
+ });
508
+
509
+ it('errors when using --wait with all target', async () => {
510
+ const ui = createMockUI();
511
+ const ctx = createContext({
512
+ ui,
513
+ paths: createTestPaths(testDir),
514
+ flags: { wait: true },
515
+ });
516
+
517
+ await expect(cmdTalk(ctx, 'all', 'Hello')).rejects.toThrow('exit(1)');
518
+ expect(ui.errors[0]).toContain('Wait mode is not supported');
519
+ });
520
+ });
521
+
522
+ describe('cmdTalk - nonce collision handling', () => {
523
+ let testDir: string;
524
+
525
+ beforeEach(() => {
526
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
527
+ });
528
+
529
+ afterEach(() => {
530
+ if (fs.existsSync(testDir)) {
531
+ fs.rmSync(testDir, { recursive: true, force: true });
532
+ }
533
+ });
534
+
535
+ it('ignores old markers in scrollback that do not match current nonce', async () => {
536
+ const tmux = createMockTmux();
537
+ const ui = createMockUI();
538
+
539
+ let captureCount = 0;
540
+ const oldMarker = '{tmux-team-end:0000}'; // Old marker from previous request
541
+
542
+ tmux.capture = () => {
543
+ captureCount++;
544
+ if (captureCount === 1) {
545
+ // Baseline includes an OLD marker
546
+ return `Old response ${oldMarker}`;
547
+ }
548
+ // New capture still has old marker but not new one yet
549
+ if (captureCount === 2) {
550
+ return `Old response ${oldMarker}\nNew question asked`;
551
+ }
552
+ // Finally, new marker appears
553
+ const sent = tmux.sends[0]?.message || '';
554
+ const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
555
+ if (match) {
556
+ return `Old response ${oldMarker}\nNew question asked\nNew response {tmux-team-end:${match[1]}}`;
557
+ }
558
+ return `Old response ${oldMarker}`;
559
+ };
560
+
561
+ const ctx = createContext({
562
+ tmux,
563
+ ui,
564
+ paths: createTestPaths(testDir),
565
+ flags: { wait: true, json: true, timeout: 5 },
566
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
567
+ });
568
+
569
+ await cmdTalk(ctx, 'claude', 'Hello');
570
+
571
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
572
+ expect(output.status).toBe('completed');
573
+ // Response should be from after the new question, not triggered by old marker
574
+ expect(output.response as string).not.toContain('Old response');
575
+ });
576
+ });
577
+
578
+ describe('cmdTalk - JSON output contract', () => {
579
+ let testDir: string;
580
+
581
+ beforeEach(() => {
582
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
583
+ });
584
+
585
+ afterEach(() => {
586
+ if (fs.existsSync(testDir)) {
587
+ fs.rmSync(testDir, { recursive: true, force: true });
588
+ }
589
+ });
590
+
591
+ it('includes required fields in success response', async () => {
592
+ const tmux = createMockTmux();
593
+ const ui = createMockUI();
594
+
595
+ let captureCount = 0;
596
+ tmux.capture = () => {
597
+ captureCount++;
598
+ if (captureCount === 1) return '';
599
+ const sent = tmux.sends[0]?.message || '';
600
+ const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
601
+ return match ? `Response {tmux-team-end:${match[1]}}` : '';
602
+ };
603
+
604
+ const ctx = createContext({
605
+ tmux,
606
+ ui,
607
+ paths: createTestPaths(testDir),
608
+ flags: { wait: true, json: true, timeout: 5 },
609
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
610
+ });
611
+
612
+ await cmdTalk(ctx, 'claude', 'Hello');
613
+
614
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
615
+ expect(output).toHaveProperty('target', 'claude');
616
+ expect(output).toHaveProperty('pane', '1.0');
617
+ expect(output).toHaveProperty('status', 'completed');
618
+ expect(output).toHaveProperty('requestId');
619
+ expect(output).toHaveProperty('nonce');
620
+ expect(output).toHaveProperty('marker');
621
+ expect(output).toHaveProperty('response');
622
+ });
623
+
624
+ it('includes required fields in timeout response', async () => {
625
+ const tmux = createMockTmux();
626
+ const ui = createMockUI();
627
+ tmux.capture = () => 'no marker';
628
+
629
+ const ctx = createContext({
630
+ tmux,
631
+ ui,
632
+ paths: createTestPaths(testDir),
633
+ flags: { wait: true, json: true, timeout: 0.05 },
634
+ config: { defaults: { timeout: 0.05, pollInterval: 0.01, captureLines: 100 } },
635
+ });
636
+
637
+ try {
638
+ await cmdTalk(ctx, 'claude', 'Hello');
639
+ } catch {
640
+ // Expected
641
+ }
642
+
643
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
644
+ expect(output).toHaveProperty('target', 'claude');
645
+ expect(output).toHaveProperty('pane', '1.0');
646
+ expect(output).toHaveProperty('status', 'timeout');
647
+ expect(output).toHaveProperty('error');
648
+ expect(output).toHaveProperty('requestId');
649
+ expect(output).toHaveProperty('nonce');
650
+ expect(output).toHaveProperty('marker');
651
+ });
652
+ });