|
| 1 | +/** |
| 2 | + * Multi-session debugging e2e — real VS Code extension host |
| 3 | + * |
| 4 | + * WHAT: Start two concurrent debug sessions (server + client), |
| 5 | + * step through code, inspect variables, detect null token bug, |
| 6 | + * patch code with sed, restart, verify fix, revert. |
| 7 | + * HOW: Use vscode.commands.executeCommand to call extension commands. |
| 8 | + * Real VS Code extension host, real debuggers. |
| 9 | + * WHEN: npm run test:vscode (runs inside @vscode/test-electron) |
| 10 | + */ |
| 11 | + |
| 12 | +import * as assert from 'assert'; |
| 13 | +import * as vscode from 'vscode'; |
| 14 | +import * as fs from 'fs'; |
| 15 | +import * as path from 'path'; |
| 16 | + |
| 17 | +const SERVER_CONFIG = 'Debug Backend (server)'; |
| 18 | +const CLIENT_CONFIG = 'Debug Backend (client)'; |
| 19 | +const PROJECT_ROOT = path.resolve(__dirname, '../../..'); |
| 20 | +const CLIENT_FILE = path.join(PROJECT_ROOT, 'src/e2e/multi-session/client.ts'); |
| 21 | +const CLIENT_BACKUP = CLIENT_FILE + '.bak'; |
| 22 | + |
| 23 | +// ── Helpers ──────────────────────────────────────────────────────────────── |
| 24 | + |
| 25 | +async function cmd(command: string, args?: object): Promise<any> { |
| 26 | + return vscode.commands.executeCommand(command, args); |
| 27 | +} |
| 28 | + |
| 29 | +async function waitForExtension(timeoutMs = 20_000): Promise<void> { |
| 30 | + const deadline = Date.now() + timeoutMs; |
| 31 | + while (Date.now() < deadline) { |
| 32 | + try { await vscode.commands.executeCommand('debuggingAI.status'); return; } |
| 33 | + catch { await new Promise(r => setTimeout(r, 300)); } |
| 34 | + } |
| 35 | + throw new Error('Extension did not become ready within timeout'); |
| 36 | +} |
| 37 | + |
| 38 | +// ── Suite ────────────────────────────────────────────────────────────────── |
| 39 | + |
| 40 | +suite('Multi-session debugging e2e', () => { |
| 41 | + |
| 42 | + suiteSetup(async function (this: Mocha.Context) { |
| 43 | + this.timeout(25_000); |
| 44 | + await waitForExtension(); |
| 45 | + }); |
| 46 | + |
| 47 | + teardown(async () => { |
| 48 | + // Stop both sessions |
| 49 | + await cmd('debuggingAI.quit'); |
| 50 | + // Restore client.ts if patched |
| 51 | + if (fs.existsSync(CLIENT_BACKUP)) { |
| 52 | + fs.copyFileSync(CLIENT_BACKUP, CLIENT_FILE); |
| 53 | + fs.unlinkSync(CLIENT_BACKUP); |
| 54 | + } |
| 55 | + await new Promise(r => setTimeout(r, 200)); |
| 56 | + }); |
| 57 | + |
| 58 | + test('start two concurrent sessions (server + client)', async function (this: Mocha.Context) { |
| 59 | + this.timeout(60_000); |
| 60 | + |
| 61 | + // PART 1: Start server session |
| 62 | + console.log('[e2e] Starting server session...'); |
| 63 | + const server: any = await cmd('debuggingAI.start', { config: SERVER_CONFIG }); |
| 64 | + |
| 65 | + if (!server.ok) { |
| 66 | + // Configs may not exist in test environment — skip gracefully |
| 67 | + this.skip(); |
| 68 | + return; |
| 69 | + } |
| 70 | + |
| 71 | + assert.strictEqual(server.state, 'paused', 'server should be paused at entry'); |
| 72 | + const serverId = server.sessionId; |
| 73 | + console.log(`[e2e] Server started: ${serverId}`); |
| 74 | + |
| 75 | + // PART 2: Start client session |
| 76 | + console.log('[e2e] Starting client session...'); |
| 77 | + const client: any = await cmd('debuggingAI.start', { config: CLIENT_CONFIG }); |
| 78 | + assert.strictEqual(client.state, 'paused', 'client should be paused at entry'); |
| 79 | + const clientId = client.sessionId; |
| 80 | + console.log(`[e2e] Client started: ${clientId}`); |
| 81 | + |
| 82 | + // Verify two distinct sessions |
| 83 | + assert.notStrictEqual(serverId, clientId, 'sessions should have distinct IDs'); |
| 84 | + console.log('[e2e] ✓ Two concurrent sessions active'); |
| 85 | + |
| 86 | + // PART 3: Step through client, inspect for null token |
| 87 | + console.log('[e2e] Stepping client to inspect variables...'); |
| 88 | + const step1: any = await cmd('debuggingAI.next'); |
| 89 | + assert.strictEqual(step1.state, 'paused'); |
| 90 | + console.log(`[e2e] Client at ${step1.file}:${step1.line}`); |
| 91 | + |
| 92 | + // Inspect: obj should exist but have no token |
| 93 | + const inspect: any = await cmd('debuggingAI.inspect', { expression: 'obj' }); |
| 94 | + console.log(`[e2e] obj value: ${inspect.valueRepr}`); |
| 95 | + console.log('[e2e] Expected: no token field (bug present)'); |
| 96 | + |
| 97 | + // PART 4: Patch the code |
| 98 | + console.log('[e2e] Patching client.ts to add token...'); |
| 99 | + fs.copyFileSync(CLIENT_FILE, CLIENT_BACKUP); |
| 100 | + const code = fs.readFileSync(CLIENT_FILE, 'utf8'); |
| 101 | + const patched = code.replace( |
| 102 | + /const enriched = { \.\.\.obj };/, |
| 103 | + "const enriched = { ...obj, token: 'client-token-xyz' };", |
| 104 | + ); |
| 105 | + fs.writeFileSync(CLIENT_FILE, patched); |
| 106 | + console.log('[e2e] ✓ Code patched: token added'); |
| 107 | + |
| 108 | + // PART 5: Restart and verify fix |
| 109 | + console.log('[e2e] Restarting client with patched code...'); |
| 110 | + const restart: any = await cmd('debuggingAI.restart'); |
| 111 | + assert.strictEqual(restart.state, 'paused'); |
| 112 | + console.log('[e2e] ✓ Client restarted with patched code'); |
| 113 | + |
| 114 | + // Step to enriched assignment |
| 115 | + const step2: any = await cmd('debuggingAI.next'); |
| 116 | + assert.strictEqual(step2.state, 'paused'); |
| 117 | + |
| 118 | + // Inspect: enriched should now have token |
| 119 | + const inspectFixed: any = await cmd('debuggingAI.inspect', { expression: 'enriched' }); |
| 120 | + console.log(`[e2e] enriched value: ${inspectFixed.valueRepr}`); |
| 121 | + console.log('[e2e] Expected: has token field (fix verified)'); |
| 122 | + |
| 123 | + // Verify by looking for 'token' in the output |
| 124 | + assert.ok( |
| 125 | + inspectFixed.valueRepr?.includes('token') || inspectFixed.valueRepr?.includes('xyz'), |
| 126 | + 'enriched should contain token field after patch', |
| 127 | + ); |
| 128 | + |
| 129 | + console.log('[e2e] ✓ Fix verified: token flows end-to-end'); |
| 130 | + |
| 131 | + // PART 6: Verify revert |
| 132 | + console.log('[e2e] Reverting code to original (broken) state...'); |
| 133 | + fs.copyFileSync(CLIENT_BACKUP, CLIENT_FILE); |
| 134 | + fs.unlinkSync(CLIENT_BACKUP); |
| 135 | + console.log('[e2e] ✓ Code reverted'); |
| 136 | + |
| 137 | + console.log('[e2e] === MULTI-SESSION E2E COMPLETE ===\n'); |
| 138 | + }); |
| 139 | + |
| 140 | +}); |
0 commit comments