~cytrogen/gstack

ref: a0328be04c37ce4a52f6fb169b355400ecbb0ef2 gstack/browse/test/sidebar-agent.test.ts -rw-r--r-- 22.1 KiB
a0328be0 — Garry Tan feat: always-on adversarial review + scope drift + plan mode design tools (v0.14.3.0) (#694) 10 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
/**
 * Tests for sidebar agent queue parsing and inbox writing.
 *
 * sidebar-agent.ts functions are not exported (it's an entry-point script),
 * so we test the same logic inline: JSONL parsing, writeToInbox filesystem
 * behavior, and edge cases.
 */

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

// ─── Helpers: replicate sidebar-agent logic for unit testing ──────

/** Parse a single JSONL line — same logic as sidebar-agent poll() */
function parseQueueLine(line: string): any | null {
  if (!line.trim()) return null;
  try {
    const entry = JSON.parse(line);
    if (!entry.message && !entry.prompt) return null;
    return entry;
  } catch {
    return null;
  }
}

/** Read all valid entries from a JSONL string — same as countLines + readLine loop */
function parseQueueFile(content: string): any[] {
  const entries: any[] = [];
  const lines = content.split('\n').filter(Boolean);
  for (const line of lines) {
    const entry = parseQueueLine(line);
    if (entry) entries.push(entry);
  }
  return entries;
}

/** Write to inbox — extracted logic from sidebar-agent.ts writeToInbox() */
function writeToInbox(
  gitRoot: string,
  message: string,
  pageUrl?: string,
  sessionId?: string,
): string | null {
  if (!gitRoot) return null;

  const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox');
  fs.mkdirSync(inboxDir, { recursive: true });

  const now = new Date();
  const timestamp = now.toISOString().replace(/:/g, '-');
  const filename = `${timestamp}-observation.json`;
  const tmpFile = path.join(inboxDir, `.${filename}.tmp`);
  const finalFile = path.join(inboxDir, filename);

  const inboxMessage = {
    type: 'observation',
    timestamp: now.toISOString(),
    page: { url: pageUrl || 'unknown', title: '' },
    userMessage: message,
    sidebarSessionId: sessionId || 'unknown',
  };

  fs.writeFileSync(tmpFile, JSON.stringify(inboxMessage, null, 2));
  fs.renameSync(tmpFile, finalFile);
  return finalFile;
}

/** Shorten paths — same logic as sidebar-agent.ts shorten() */
function shorten(str: string): string {
  return str
    .replace(/\/Users\/[^/]+/g, '~')
    .replace(/\/conductor\/workspaces\/[^/]+\/[^/]+/g, '')
    .replace(/\.claude\/skills\/gstack\//g, '')
    .replace(/browse\/dist\/browse/g, '$B');
}

/** describeToolCall — replicated from sidebar-agent.ts for unit testing */
function describeToolCall(tool: string, input: any): string {
  if (!input) return '';

  if (tool === 'Bash' && input.command) {
    const cmd = input.command;
    const browseMatch = cmd.match(/\$B\s+(\w+)|browse[^\s]*\s+(\w+)/);
    if (browseMatch) {
      const browseCmd = browseMatch[1] || browseMatch[2];
      const args = cmd.split(/\s+/).slice(2).join(' ');
      switch (browseCmd) {
        case 'goto': return `Opening ${args.replace(/['"]/g, '')}`;
        case 'snapshot': return args.includes('-i') ? 'Scanning for interactive elements' : args.includes('-D') ? 'Checking what changed' : 'Taking a snapshot of the page';
        case 'screenshot': return `Saving screenshot${args ? ` to ${shorten(args)}` : ''}`;
        case 'click': return `Clicking ${args}`;
        case 'fill': { const parts = args.split(/\s+/); return `Typing "${parts.slice(1).join(' ')}" into ${parts[0]}`; }
        case 'text': return 'Reading page text';
        case 'html': return args ? `Reading HTML of ${args}` : 'Reading full page HTML';
        case 'links': return 'Finding all links on the page';
        case 'forms': return 'Looking for forms';
        case 'console': return 'Checking browser console for errors';
        case 'network': return 'Checking network requests';
        case 'url': return 'Checking current URL';
        case 'back': return 'Going back';
        case 'forward': return 'Going forward';
        case 'reload': return 'Reloading the page';
        case 'scroll': return args ? `Scrolling to ${args}` : 'Scrolling down';
        case 'wait': return `Waiting for ${args}`;
        case 'inspect': return args ? `Inspecting CSS of ${args}` : 'Getting CSS for last picked element';
        case 'style': return `Changing CSS: ${args}`;
        case 'cleanup': return 'Removing page clutter (ads, popups, banners)';
        case 'prettyscreenshot': return 'Taking a clean screenshot';
        case 'css': return `Checking CSS property: ${args}`;
        case 'is': return `Checking if element is ${args}`;
        case 'diff': return `Comparing ${args}`;
        case 'responsive': return 'Taking screenshots at mobile, tablet, and desktop sizes';
        case 'status': return 'Checking browser status';
        case 'tabs': return 'Listing open tabs';
        case 'focus': return 'Bringing browser to front';
        case 'select': return `Selecting option in ${args}`;
        case 'hover': return `Hovering over ${args}`;
        case 'viewport': return `Setting viewport to ${args}`;
        case 'upload': return `Uploading file to ${args.split(/\s+/)[0]}`;
        default: return `Running browse ${browseCmd} ${args}`.trim();
      }
    }
    if (cmd.includes('git ')) return `Running: ${shorten(cmd)}`;
    let short = shorten(cmd);
    return short.length > 100 ? short.slice(0, 100) + '…' : short;
  }

  if (tool === 'Read' && input.file_path) return `Reading ${shorten(input.file_path)}`;
  if (tool === 'Edit' && input.file_path) return `Editing ${shorten(input.file_path)}`;
  if (tool === 'Write' && input.file_path) return `Writing ${shorten(input.file_path)}`;
  if (tool === 'Grep' && input.pattern) return `Searching for "${input.pattern}"`;
  if (tool === 'Glob' && input.pattern) return `Finding files matching ${input.pattern}`;
  try { return shorten(JSON.stringify(input)).slice(0, 80); } catch { return ''; }
}

// ─── Test setup ──────────────────────────────────────────────────

let tmpDir: string;

beforeEach(() => {
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sidebar-agent-test-'));
});

afterEach(() => {
  fs.rmSync(tmpDir, { recursive: true, force: true });
});

// ─── Queue File Parsing ─────────────────────────────────────────

describe('queue file parsing', () => {
  test('valid JSONL line parsed correctly', () => {
    const line = JSON.stringify({ message: 'hello', prompt: 'check this', pageUrl: 'https://example.com' });
    const entry = parseQueueLine(line);
    expect(entry).not.toBeNull();
    expect(entry.message).toBe('hello');
    expect(entry.prompt).toBe('check this');
    expect(entry.pageUrl).toBe('https://example.com');
  });

  test('malformed JSON line skipped without crash', () => {
    const entry = parseQueueLine('this is not json {{{');
    expect(entry).toBeNull();
  });

  test('valid JSON without message or prompt is skipped', () => {
    const line = JSON.stringify({ foo: 'bar' });
    const entry = parseQueueLine(line);
    expect(entry).toBeNull();
  });

  test('empty file returns no entries', () => {
    const entries = parseQueueFile('');
    expect(entries).toEqual([]);
  });

  test('file with blank lines returns no entries', () => {
    const entries = parseQueueFile('\n\n\n');
    expect(entries).toEqual([]);
  });

  test('mixed valid and invalid lines', () => {
    const content = [
      JSON.stringify({ message: 'first' }),
      'not json',
      JSON.stringify({ unrelated: true }),
      JSON.stringify({ message: 'second', prompt: 'do stuff' }),
    ].join('\n');

    const entries = parseQueueFile(content);
    expect(entries.length).toBe(2);
    expect(entries[0].message).toBe('first');
    expect(entries[1].message).toBe('second');
  });
});

// ─── writeToInbox ────────────────────────────────────────────────

describe('writeToInbox', () => {
  test('creates .context/sidebar-inbox/ directory', () => {
    writeToInbox(tmpDir, 'test message');
    const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
    expect(fs.existsSync(inboxDir)).toBe(true);
    expect(fs.statSync(inboxDir).isDirectory()).toBe(true);
  });

  test('writes valid JSON file', () => {
    const filePath = writeToInbox(tmpDir, 'test message', 'https://example.com', 'session-123');
    expect(filePath).not.toBeNull();
    expect(fs.existsSync(filePath!)).toBe(true);

    const data = JSON.parse(fs.readFileSync(filePath!, 'utf-8'));
    expect(data.type).toBe('observation');
    expect(data.userMessage).toBe('test message');
    expect(data.page.url).toBe('https://example.com');
    expect(data.sidebarSessionId).toBe('session-123');
    expect(data.timestamp).toBeTruthy();
  });

  test('atomic write — final file exists, no .tmp left', () => {
    const filePath = writeToInbox(tmpDir, 'atomic test');
    expect(filePath).not.toBeNull();
    expect(fs.existsSync(filePath!)).toBe(true);

    // Check no .tmp files remain in the inbox directory
    const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
    const files = fs.readdirSync(inboxDir);
    const tmpFiles = files.filter(f => f.endsWith('.tmp'));
    expect(tmpFiles.length).toBe(0);

    // Final file should end with -observation.json
    const jsonFiles = files.filter(f => f.endsWith('-observation.json') && !f.startsWith('.'));
    expect(jsonFiles.length).toBe(1);
  });

  test('handles missing git root gracefully', () => {
    const result = writeToInbox('', 'test');
    expect(result).toBeNull();
  });

  test('defaults pageUrl to unknown when not provided', () => {
    const filePath = writeToInbox(tmpDir, 'no url provided');
    expect(filePath).not.toBeNull();
    const data = JSON.parse(fs.readFileSync(filePath!, 'utf-8'));
    expect(data.page.url).toBe('unknown');
  });

  test('defaults sessionId to unknown when not provided', () => {
    const filePath = writeToInbox(tmpDir, 'no session');
    expect(filePath).not.toBeNull();
    const data = JSON.parse(fs.readFileSync(filePath!, 'utf-8'));
    expect(data.sidebarSessionId).toBe('unknown');
  });

  test('multiple writes create separate files', () => {
    writeToInbox(tmpDir, 'message 1');
    // Tiny delay to ensure different timestamps
    const t = Date.now();
    while (Date.now() === t) {} // spin until next ms
    writeToInbox(tmpDir, 'message 2');

    const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
    const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
    expect(files.length).toBe(2);
  });
});

// ─── describeToolCall (verbose narration) ────────────────────────

describe('describeToolCall', () => {
  // Browse navigation commands
  test('goto → plain English with URL', () => {
    const result = describeToolCall('Bash', { command: '$B goto https://example.com' });
    expect(result).toBe('Opening https://example.com');
  });

  test('goto strips quotes from URL', () => {
    const result = describeToolCall('Bash', { command: '$B goto "https://example.com"' });
    expect(result).toBe('Opening https://example.com');
  });

  test('url → checking current URL', () => {
    expect(describeToolCall('Bash', { command: '$B url' })).toBe('Checking current URL');
  });

  test('back/forward/reload → plain English', () => {
    expect(describeToolCall('Bash', { command: '$B back' })).toBe('Going back');
    expect(describeToolCall('Bash', { command: '$B forward' })).toBe('Going forward');
    expect(describeToolCall('Bash', { command: '$B reload' })).toBe('Reloading the page');
  });

  // Snapshot variants
  test('snapshot -i → scanning for interactive elements', () => {
    expect(describeToolCall('Bash', { command: '$B snapshot -i' })).toBe('Scanning for interactive elements');
  });

  test('snapshot -D → checking what changed', () => {
    expect(describeToolCall('Bash', { command: '$B snapshot -D' })).toBe('Checking what changed');
  });

  test('snapshot (plain) → taking a snapshot', () => {
    expect(describeToolCall('Bash', { command: '$B snapshot' })).toBe('Taking a snapshot of the page');
  });

  // Interaction commands
  test('click → clicking element', () => {
    expect(describeToolCall('Bash', { command: '$B click @e3' })).toBe('Clicking @e3');
  });

  test('fill → typing into element', () => {
    expect(describeToolCall('Bash', { command: '$B fill @e4 "hello world"' })).toBe('Typing ""hello world"" into @e4');
  });

  test('scroll with selector → scrolling to element', () => {
    expect(describeToolCall('Bash', { command: '$B scroll .footer' })).toBe('Scrolling to .footer');
  });

  test('scroll without args → scrolling down', () => {
    expect(describeToolCall('Bash', { command: '$B scroll' })).toBe('Scrolling down');
  });

  // Reading commands
  test('text → reading page text', () => {
    expect(describeToolCall('Bash', { command: '$B text' })).toBe('Reading page text');
  });

  test('html with selector → reading HTML of element', () => {
    expect(describeToolCall('Bash', { command: '$B html .header' })).toBe('Reading HTML of .header');
  });

  test('html without selector → reading full page HTML', () => {
    expect(describeToolCall('Bash', { command: '$B html' })).toBe('Reading full page HTML');
  });

  test('links → finding all links', () => {
    expect(describeToolCall('Bash', { command: '$B links' })).toBe('Finding all links on the page');
  });

  test('console → checking console', () => {
    expect(describeToolCall('Bash', { command: '$B console' })).toBe('Checking browser console for errors');
  });

  // Inspector commands
  test('inspect with selector → inspecting CSS', () => {
    expect(describeToolCall('Bash', { command: '$B inspect .header' })).toBe('Inspecting CSS of .header');
  });

  test('inspect without args → getting last picked element', () => {
    expect(describeToolCall('Bash', { command: '$B inspect' })).toBe('Getting CSS for last picked element');
  });

  test('style → changing CSS', () => {
    expect(describeToolCall('Bash', { command: '$B style .header color red' })).toBe('Changing CSS: .header color red');
  });

  test('cleanup → removing page clutter', () => {
    expect(describeToolCall('Bash', { command: '$B cleanup --all' })).toBe('Removing page clutter (ads, popups, banners)');
  });

  // Visual commands
  test('screenshot → saving screenshot', () => {
    expect(describeToolCall('Bash', { command: '$B screenshot /tmp/shot.png' })).toBe('Saving screenshot to /tmp/shot.png');
  });

  test('screenshot without path', () => {
    expect(describeToolCall('Bash', { command: '$B screenshot' })).toBe('Saving screenshot');
  });

  test('responsive → multi-size screenshots', () => {
    expect(describeToolCall('Bash', { command: '$B responsive' })).toBe('Taking screenshots at mobile, tablet, and desktop sizes');
  });

  // Non-browse tools
  test('Read tool → reading file', () => {
    expect(describeToolCall('Read', { file_path: '/Users/foo/project/src/app.ts' })).toBe('Reading ~/project/src/app.ts');
  });

  test('Grep tool → searching for pattern', () => {
    expect(describeToolCall('Grep', { pattern: 'handleClick' })).toBe('Searching for "handleClick"');
  });

  test('Glob tool → finding files', () => {
    expect(describeToolCall('Glob', { pattern: '**/*.tsx' })).toBe('Finding files matching **/*.tsx');
  });

  test('Edit tool → editing file', () => {
    expect(describeToolCall('Edit', { file_path: '/Users/foo/src/main.ts' })).toBe('Editing ~/src/main.ts');
  });

  // Edge cases
  test('null input → empty string', () => {
    expect(describeToolCall('Bash', null)).toBe('');
  });

  test('unknown browse command → generic description', () => {
    expect(describeToolCall('Bash', { command: '$B newtab https://foo.com' })).toContain('newtab');
  });

  test('non-browse bash → shortened command', () => {
    expect(describeToolCall('Bash', { command: 'echo hello' })).toBe('echo hello');
  });

  test('full browse binary path recognized', () => {
    const result = describeToolCall('Bash', { command: '/Users/garrytan/.claude/skills/gstack/browse/dist/browse goto https://example.com' });
    expect(result).toBe('Opening https://example.com');
  });

  test('tab command → switching tab', () => {
    expect(describeToolCall('Bash', { command: '$B tab 2' })).toContain('tab');
  });
});

// ─── Per-tab agent concurrency (source code validation) ──────────

describe('per-tab agent concurrency', () => {
  const serverSrc = fs.readFileSync(path.join(__dirname, '..', 'src', 'server.ts'), 'utf-8');
  const agentSrc = fs.readFileSync(path.join(__dirname, '..', 'src', 'sidebar-agent.ts'), 'utf-8');

  test('server has per-tab agent state map', () => {
    expect(serverSrc).toContain('tabAgents');
    expect(serverSrc).toContain('TabAgentState');
    expect(serverSrc).toContain('getTabAgent');
  });

  test('server returns per-tab agent status in /sidebar-chat', () => {
    expect(serverSrc).toContain('getTabAgentStatus');
    expect(serverSrc).toContain('tabAgentStatus');
  });

  test('spawnClaude accepts forTabId parameter', () => {
    const spawnFn = serverSrc.slice(
      serverSrc.indexOf('function spawnClaude('),
      serverSrc.indexOf('\nfunction ', serverSrc.indexOf('function spawnClaude(') + 1),
    );
    expect(spawnFn).toContain('forTabId');
    expect(spawnFn).toContain('tabState.status');
  });

  test('sidebar-command endpoint uses per-tab agent state', () => {
    expect(serverSrc).toContain('msgTabId');
    expect(serverSrc).toContain('tabState.status');
    expect(serverSrc).toContain('tabState.queue');
  });

  test('agent event handler resets per-tab state', () => {
    expect(serverSrc).toContain('eventTabId');
    expect(serverSrc).toContain('tabState.status = \'idle\'');
  });

  test('agent event handler processes per-tab queue', () => {
    // After agent_done, should process next message from THIS tab's queue
    expect(serverSrc).toContain('tabState.queue.length > 0');
    expect(serverSrc).toContain('tabState.queue.shift');
  });

  test('sidebar-agent uses per-tab processing set', () => {
    expect(agentSrc).toContain('processingTabs');
    expect(agentSrc).not.toContain('isProcessing');
  });

  test('sidebar-agent sends tabId with all events', () => {
    // sendEvent should accept tabId parameter
    expect(agentSrc).toContain('async function sendEvent(event: Record<string, any>, tabId?: number)');
    // askClaude should extract tabId from queue entry
    expect(agentSrc).toContain('const { prompt, args, stateFile, cwd, tabId }');
  });

  test('sidebar-agent allows concurrent agents across tabs', () => {
    // poll() should not block globally — it should check per-tab
    expect(agentSrc).toContain('processingTabs.has(tid)');
    // askClaude should be fire-and-forget (no await blocking the loop)
    expect(agentSrc).toContain('askClaude(entry).catch');
  });

  test('queue entries include tabId', () => {
    const spawnFn = serverSrc.slice(
      serverSrc.indexOf('function spawnClaude('),
      serverSrc.indexOf('\nfunction ', serverSrc.indexOf('function spawnClaude(') + 1),
    );
    expect(spawnFn).toContain('tabId: agentTabId');
  });

  test('health check monitors all per-tab agents', () => {
    expect(serverSrc).toContain('for (const [tid, state] of tabAgents)');
  });
});

describe('BROWSE_TAB tab pinning (cross-tab isolation)', () => {
  const serverSrc = fs.readFileSync(path.join(__dirname, '..', 'src', 'server.ts'), 'utf-8');
  const agentSrc = fs.readFileSync(path.join(__dirname, '..', 'src', 'sidebar-agent.ts'), 'utf-8');
  const cliSrc = fs.readFileSync(path.join(__dirname, '..', 'src', 'cli.ts'), 'utf-8');

  test('sidebar-agent passes BROWSE_TAB env var to claude process', () => {
    // The env block should include BROWSE_TAB set to the tab ID
    expect(agentSrc).toContain('BROWSE_TAB');
    expect(agentSrc).toContain('String(tid)');
  });

  test('CLI reads BROWSE_TAB and sends tabId in command body', () => {
    expect(cliSrc).toContain('process.env.BROWSE_TAB');
    expect(cliSrc).toContain('tabId: parseInt(browseTab');
  });

  test('handleCommand accepts tabId from request body', () => {
    const handleFn = serverSrc.slice(
      serverSrc.indexOf('async function handleCommand('),
      serverSrc.indexOf('\nasync function ', serverSrc.indexOf('async function handleCommand(') + 1) > 0
        ? serverSrc.indexOf('\nasync function ', serverSrc.indexOf('async function handleCommand(') + 1)
        : serverSrc.indexOf('\n// ', serverSrc.indexOf('async function handleCommand(') + 200),
    );
    // Should destructure tabId from body
    expect(handleFn).toContain('tabId');
    // Should save and restore the active tab
    expect(handleFn).toContain('savedTabId');
    expect(handleFn).toContain('switchTab(tabId');
  });

  test('handleCommand restores active tab after command (success path)', () => {
    // On success, should restore savedTabId without stealing focus
    const handleFn = serverSrc.slice(
      serverSrc.indexOf('async function handleCommand('),
      serverSrc.length,
    );
    // Count restore calls — should appear in both success and error paths
    const restoreCount = (handleFn.match(/switchTab\(savedTabId/g) || []).length;
    expect(restoreCount).toBeGreaterThanOrEqual(2); // success + error paths
  });

  test('handleCommand restores active tab on error path', () => {
    // The catch block should also restore
    const catchBlock = serverSrc.slice(
      serverSrc.indexOf('} catch (err: any) {', serverSrc.indexOf('async function handleCommand(')),
    );
    expect(catchBlock).toContain('switchTab(savedTabId');
  });

  test('tab pinning only activates when tabId is provided', () => {
    const handleFn = serverSrc.slice(
      serverSrc.indexOf('async function handleCommand('),
      serverSrc.indexOf('try {', serverSrc.indexOf('async function handleCommand(') + 1),
    );
    // Should check tabId is not undefined/null before switching
    expect(handleFn).toContain('tabId !== undefined');
    expect(handleFn).toContain('tabId !== null');
  });

  test('CLI only sends tabId when BROWSE_TAB is set', () => {
    // Should conditionally include tabId in the body
    expect(cliSrc).toContain('browseTab ? { tabId:');
  });
});