|
| 1 | +// Integration tests for the managed-session engine (apps/desktop/launcher/ |
| 2 | +// tron-session), driven through its real CLI against a Node CDP mock. This is |
| 3 | +// the regression coverage for the *running* implementation; the pure schema / |
| 4 | +// tab-mapping contract it mirrors is unit-tested in @tronbrowser/browser-core. |
| 5 | +import { execFileSync, spawnSync } from 'node:child_process'; |
| 6 | +import { mkdtempSync, rmSync, existsSync } from 'node:fs'; |
| 7 | +import { tmpdir } from 'node:os'; |
| 8 | +import { join } from 'node:path'; |
| 9 | +import { fileURLToPath } from 'node:url'; |
| 10 | +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; |
| 11 | + |
| 12 | +const SESSION = fileURLToPath(new URL('../launcher/tron-session', import.meta.url)); |
| 13 | +const SHIM = fileURLToPath(new URL('../test/fixtures/fake-shim.sh', import.meta.url)); |
| 14 | + |
| 15 | +// The engine shells out to curl + python3 (already launcher dependencies). Skip |
| 16 | +// the suite gracefully where they are unavailable rather than fail spuriously. |
| 17 | +function has(bin: string): boolean { |
| 18 | + return spawnSync('sh', ['-c', `command -v ${bin}`], { encoding: 'utf8' }).status === 0; |
| 19 | +} |
| 20 | +const ready = has('curl') && (has('python3') || has('python')); |
| 21 | + |
| 22 | +let dataDir: string; |
| 23 | + |
| 24 | +function baseEnv(): NodeJS.ProcessEnv { |
| 25 | + return { ...process.env, TRONBROWSER_DATA: dataDir, TRONBROWSER_SHIM: SHIM }; |
| 26 | +} |
| 27 | + |
| 28 | +/** Run a tron-session command, returning stdout (throws on nonzero exit). */ |
| 29 | +function tron(...args: string[]): string { |
| 30 | + return execFileSync(SESSION, args, { env: baseEnv(), encoding: 'utf8', timeout: 30_000 }); |
| 31 | +} |
| 32 | + |
| 33 | +/** Run and capture status + stdout without throwing (for exit-code assertions). */ |
| 34 | +function tronStatus(...args: string[]): { status: number | null; stdout: string } { |
| 35 | + const r = spawnSync(SESSION, args, { env: baseEnv(), encoding: 'utf8', timeout: 30_000 }); |
| 36 | + return { status: r.status, stdout: r.stdout ?? '' }; |
| 37 | +} |
| 38 | + |
| 39 | +describe.skipIf(!ready)('tron-session managed sessions', () => { |
| 40 | + beforeEach(() => { |
| 41 | + dataDir = mkdtempSync(join(tmpdir(), 'tron-session-test-')); |
| 42 | + }); |
| 43 | + |
| 44 | + afterEach(() => { |
| 45 | + try { |
| 46 | + tron('browser', 'close'); |
| 47 | + } catch { |
| 48 | + // ignore — individual tests close their own session |
| 49 | + } |
| 50 | + rmSync(dataDir, { recursive: true, force: true }); |
| 51 | + }); |
| 52 | + |
| 53 | + it('launches a session and writes a live descriptor', () => { |
| 54 | + const out = tron('browser', 'launch'); |
| 55 | + expect(out).toMatch(/managed session ready on 127\.0\.0\.1:\d+/); |
| 56 | + |
| 57 | + const desc = JSON.parse(tron('browser', 'status', '--json')); |
| 58 | + expect(desc.state).toBe('running'); |
| 59 | + expect(desc.version).toBe(1); |
| 60 | + expect(desc.host).toBe('127.0.0.1'); |
| 61 | + expect(desc.port).toBeGreaterThan(0); |
| 62 | + expect(desc.profileName).toBe('agent'); |
| 63 | + expect(desc.headless).toBe(false); |
| 64 | + // The M3.2 attach point must be captured. |
| 65 | + expect(desc.webSocketDebuggerUrl).toMatch(/^ws:\/\/127\.0\.0\.1:\d+\/devtools\/browser\//); |
| 66 | + |
| 67 | + expect(tron('browser', 'status')).toMatch(/running/); |
| 68 | + }); |
| 69 | + |
| 70 | + it('lists the initial tab and marks it current', () => { |
| 71 | + tron('browser', 'launch'); |
| 72 | + const tabs = JSON.parse(tron('browser', 'tabs', '--json')); |
| 73 | + expect(tabs).toHaveLength(1); |
| 74 | + expect(tabs[0].current).toBe(true); |
| 75 | + expect(tabs[0].url).toBe('chrome://newtab/'); |
| 76 | + }); |
| 77 | + |
| 78 | + it('opens a URL as a new current tab', () => { |
| 79 | + tron('browser', 'launch'); |
| 80 | + const out = tron('open', 'http://example.com/contact'); |
| 81 | + expect(out).toMatch(/opened http:\/\/example\.com\/contact/); |
| 82 | + |
| 83 | + const tabs = JSON.parse(tron('browser', 'tabs', '--json')); |
| 84 | + expect(tabs).toHaveLength(2); |
| 85 | + const current = tabs.find((t: { current: boolean }) => t.current); |
| 86 | + expect(current.url).toBe('http://example.com/contact'); |
| 87 | + }); |
| 88 | + |
| 89 | + it('switches the current tab with use, reflected by current', () => { |
| 90 | + tron('browser', 'launch'); |
| 91 | + tron('open', 'http://example.org'); |
| 92 | + const tabs = JSON.parse(tron('browser', 'tabs', '--json')); |
| 93 | + const first = tabs[0].id as string; |
| 94 | + |
| 95 | + tron('browser', 'use', first); |
| 96 | + const after = JSON.parse(tron('browser', 'tabs', '--json')); |
| 97 | + expect(after.find((t: { current: boolean }) => t.current).id).toBe(first); |
| 98 | + expect(tron('browser', 'current')).toContain(first); |
| 99 | + }); |
| 100 | + |
| 101 | + it('rejects a launch while one is already running', () => { |
| 102 | + tron('browser', 'launch'); |
| 103 | + expect(tron('browser', 'launch')).toMatch(/already running/); |
| 104 | + }); |
| 105 | + |
| 106 | + it('uses an ephemeral temp profile for headless and removes it on close', () => { |
| 107 | + tron('browser', 'launch', '--headless'); |
| 108 | + const desc = JSON.parse(tron('browser', 'status', '--json')); |
| 109 | + expect(desc.headless).toBe(true); |
| 110 | + expect(desc.ephemeral).toBe(true); |
| 111 | + expect(desc.profileName).toBe('ephemeral'); |
| 112 | + expect(desc.profileDir.startsWith(tmpdir())).toBe(true); |
| 113 | + expect(existsSync(desc.profileDir)).toBe(true); |
| 114 | + |
| 115 | + tron('browser', 'close'); |
| 116 | + expect(existsSync(desc.profileDir)).toBe(false); |
| 117 | + expect(tron('browser', 'status')).toMatch(/no managed session/); |
| 118 | + }); |
| 119 | + |
| 120 | + it('closes cleanly and reports no session afterwards', () => { |
| 121 | + tron('browser', 'launch'); |
| 122 | + expect(tron('browser', 'close')).toMatch(/closed managed session/); |
| 123 | + expect(tron('browser', 'status')).toMatch(/no managed session/); |
| 124 | + }); |
| 125 | + |
| 126 | + it('exits 3 from `open` when no session is running (legacy-launch signal)', () => { |
| 127 | + const r = tronStatus('open', 'http://fallback.test'); |
| 128 | + expect(r.status).toBe(3); |
| 129 | + }); |
| 130 | +}); |
0 commit comments