import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' import test from 'node:test' import { EditorWorkspaces, waitForEditorReady } from '../lib/editor-runtime.js' function deferred() { let resolve let reject const promise = new Promise((accept, fail) => { resolve = accept; reject = fail }) return { promise, resolve, reject } } function fakeRuntime() { const closure = deferred() return { url: 'http://127.0.0.1:40000', token: 'test-token', closed: closure.promise, stop: async () => { closure.resolve() }, finish: closure.resolve, } } test('constructing and disposing an unused launcher never starts a runtime', async () => { let starts = 0 const editor = new EditorWorkspaces(async () => { starts++; return fakeRuntime() }) assert.equal(starts, 0) await editor.dispose() assert.equal(starts, 0) await assert.rejects(editor.open('/repo'), /stopped/) }) test('concurrent workspace opens share one startup and preserve exact folder paths', async () => { let starts = 0 const startup = deferred() const editor = new EditorWorkspaces(() => { starts++; return startup.promise }) const first = editor.open('/repo with spaces/#feature?&') const second = editor.open('/another/repo') assert.equal(starts, 1) startup.resolve(fakeRuntime()) const results = await Promise.all([first, second]) assert.equal(new URL(results[0].url).searchParams.get('folder'), '/repo with spaces/#feature?&') assert.equal(new URL(results[1].url).searchParams.get('folder'), '/another/repo') assert.equal(new URL(results[0].url).searchParams.get('tkn'), 'test-token') await editor.open('/third') assert.equal(starts, 1) await editor.dispose() }) test('failed startup does not retry until another explicit open', async () => { let starts = 0 const editor = new EditorWorkspaces(async () => { starts++ if (starts === 1) throw new Error('startup failed') return fakeRuntime() }) await assert.rejects(editor.open('/repo'), /startup failed/) assert.equal(starts, 1) await editor.open('/repo') assert.equal(starts, 2) await editor.dispose() }) test('server exit clears its cached handle but does not restart automatically', async () => { let starts = 0 const runtimes = [] const editor = new EditorWorkspaces(async () => { starts++ const runtime = fakeRuntime() runtimes.push(runtime) return runtime }) await editor.open('/repo') runtimes[0].finish() await runtimes[0].closed assert.equal(starts, 1) await editor.open('/repo') assert.equal(starts, 2) await editor.dispose() }) test('disposal during startup shuts down without returning an editor URL', async () => { const startup = deferred() const runtime = fakeRuntime() const editor = new EditorWorkspaces(() => startup.promise) const opening = editor.open('/repo') const disposing = editor.dispose() startup.resolve(runtime) await assert.rejects(opening, /stopped/) await disposing await runtime.closed }) test('first file open carries a correctly encoded remote URI only in the new-tab URL', async () => { const editor = new EditorWorkspaces(async () => fakeRuntime(), async () => ({ opened: false })) const filePath = '/repo/file #?%雪.txt' const result = await editor.openFile('/repo', filePath) const url = new URL(result.url) const payload = JSON.parse(url.searchParams.get('payload')) assert.equal(result.delivery, 'new') assert.equal(url.searchParams.get('folder'), '/repo') assert.equal(payload[0][0], 'openFile') const remoteFile = new URL(payload[0][1]) assert.equal(remoteFile.protocol, 'vscode-remote:') assert.equal(remoteFile.host, url.host) assert.equal(decodeURIComponent(remoteFile.pathname), filePath) await editor.dispose() }) test('file delivery to an existing workspace never returns a navigation URL', async () => { let starts = 0 const received = [] const editor = new EditorWorkspaces(async () => { starts++; return fakeRuntime() }, async request => { received.push(request); return { opened: true } }) await editor.open('/repo') const result = await editor.openFile('/repo', '/repo/file.txt', { requireExisting: true }) assert.deepEqual(result, { delivery: 'existing', directory: '/repo' }) assert.equal(starts, 1) assert.equal(received[0].filePath, '/repo/file.txt') await editor.dispose() }) test('an unavailable remembered workspace never falls back to a replacement tab', async () => { let starts = 0 const editor = new EditorWorkspaces(async () => { starts++; return fakeRuntime() }, async () => ({ opened: false })) await assert.rejects(editor.openFile('/repo', '/repo/file.txt', { requireExisting: true }), /not connected/) assert.equal(starts, 0) await editor.open('/repo') await assert.rejects(editor.openFile('/repo', '/repo/file.txt', { requireExisting: true }), /not ready/) assert.equal(starts, 1) await editor.dispose() }) test('live bridge rejection is surfaced without new-tab fallback', async () => { const editor = new EditorWorkspaces(async () => fakeRuntime(), async () => { throw new Error('document unavailable') }) await assert.rejects(editor.openFile('/repo', '/repo/file.txt'), /document unavailable/) await editor.dispose() }) function fakeChild() { const child = new EventEmitter() child.stdout = new EventEmitter() child.stderr = new EventEmitter() return child } test('readiness requires complete loopback Web UI announcement and strips token', async () => { const child = fakeChild() const ready = waitForEditorReady(child) child.stdout.emit('data', 'Extension host agent listening on 40000\nWeb UI avail') child.stdout.emit('data', 'able at http://127.0.0.1:40') child.stdout.emit('data', '000/?tkn=secret\n') assert.equal(await ready, 'http://127.0.0.1:40000') assert.equal(child.listenerCount('exit'), 0) assert.equal(child.stdout.listenerCount('data'), 0) }) test('installed runtime localhost readiness format is normalized to IPv4 loopback', async () => { const child = fakeChild() const ready = waitForEditorReady(child) child.stdout.emit('data', 'Web UI available at http://localhost:40000?tkn=secret\n') assert.equal(await ready, 'http://127.0.0.1:40000') }) test('readiness rejects an early exit and removes listeners', async () => { const child = fakeChild() const ready = waitForEditorReady(child) child.emit('exit', 1) await assert.rejects(ready, /exit 1/) assert.equal(child.stdout.listenerCount('data'), 0) }) test('readiness rejects a spawn error without including process output', async () => { const child = fakeChild() const ready = waitForEditorReady(child) child.stderr.emit('data', 'sensitive startup output') child.emit('error', new Error('internal detail')) await assert.rejects(ready, { message: 'Unable to execute the installed VS Code runtime.' }) })