Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
178 lines
6.7 KiB
JavaScript
178 lines
6.7 KiB
JavaScript
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.' })
|
|
})
|