Files

148 lines
6.7 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import vm from 'node:vm'
import { injectRandomUuidPolyfill, LIFECYCLE_TRANSITION_GUARD, PLUGIN_RECOVERY_GUARD, RANDOM_UUID_POLYFILL, SESSION_VALIDITY_GUARD } from '../lib/polyfill.mjs'
async function simulatePluginError(bundlePath, { eventType = 'error', storage = new Map() } = {}) {
let replacement
const events = []
const listeners = new Map()
const context = {
URL,
crypto: globalThis.crypto,
location: {
href: 'http://gateway.local/sessions/current',
pathname: '/sessions/current',
search: '',
hash: '',
replace: value => { replacement = value },
},
sessionStorage: {
getItem: key => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, value),
},
addEventListener: (name, callback) => listeners.set(name, callback),
fetch: async (input, init) => {
if (input === '/_dsh_gateway/readiness') return { json: async () => ({ state: 'ready', pluginRecoveryEligible: true }) }
if (input === '/_dsh_platform/plugin-api/v1/status') return {
json: async () => ({
dshLifecycle: {
state: 'running',
taskId: 'lifecycle-task',
updatedAt: new Date().toISOString(),
},
}),
}
if (input === '/_dsh_gateway/client-event') {
events.push(JSON.parse(init.body))
return { json: async () => ({}) }
}
return { ok: true, json: async () => ({}) }
},
}
vm.runInNewContext(PLUGIN_RECOVERY_GUARD.slice('<script>'.length, -'</script>'.length), context)
if (eventType === 'error') listeners.get('error')?.({ filename: `http://gateway.local${bundlePath}` })
else if (eventType === 'unhandledrejection') listeners.get('unhandledrejection')?.({
reason: { stack: `TypeError: failed to import http://gateway.local${bundlePath}` },
})
await new Promise(resolve => setImmediate(resolve))
return { events, replacement, storage }
}
test('polyfill is inserted immediately after the head tag', () => {
assert.equal(
injectRandomUuidPolyfill('<html><head data-x="1"><title>x</title></head></html>'),
`<html><head data-x="1">${RANDOM_UUID_POLYFILL}${LIFECYCLE_TRANSITION_GUARD}${SESSION_VALIDITY_GUARD}${PLUGIN_RECOVERY_GUARD}<title>x</title></head></html>`,
)
})
test('polyfill falls back to the start when no head exists', () => {
assert.equal(injectRandomUuidPolyfill('<main>x</main>'), `${RANDOM_UUID_POLYFILL}${LIFECYCLE_TRANSITION_GUARD}${SESSION_VALIDITY_GUARD}${PLUGIN_RECOVERY_GUARD}<main>x</main>`)
})
test('gateway lifecycle guard moves an already-open DSH page into the holding flow', async () => {
let inspect
let replacement
const context = {
EventSource: class {
addEventListener(name, callback) { if (name === 'state') inspect = callback }
},
URL,
fetch: async () => ({ json: async () => ({ dshLifecycle: { state: 'restarting' } }) }),
location: {
pathname: '/sessions/current', search: '?view=chat', hash: '#latest',
replace: value => { replacement = value },
},
}
vm.runInNewContext(LIFECYCLE_TRANSITION_GUARD.slice('<script>'.length, -'</script>'.length), context)
await new Promise(resolve => setImmediate(resolve))
assert.equal(replacement, '/_dsh_gateway/wait?return=%2Fsessions%2Fcurrent%3Fview%3Dchat%23latest')
replacement = undefined
await inspect()
assert.equal(replacement, undefined)
})
test('plugin recovery guard runs before DSH modules and bounds lifecycle recovery', () => {
assert.doesNotThrow(() => new Function(PLUGIN_RECOVERY_GUARD.slice('<script>'.length, -'</script>'.length)))
assert.match(PLUGIN_RECOVERY_GUARD, /globalThis\.fetch=function/)
assert.match(PLUGIN_RECOVERY_GUARD, /\/_dsh_gateway\/client-event/)
assert.match(PLUGIN_RECOVERY_GUARD, /\/_dsh_gateway\/wait/)
assert.match(PLUGIN_RECOVERY_GUARD, /attempt>=3/)
assert.match(PLUGIN_RECOVERY_GUARD, /\/_dsh_gateway\/plugin-failure/)
assert.match(PLUGIN_RECOVERY_GUARD, /browser\.plugin-load\.recovery\.completed/)
assert.match(PLUGIN_RECOVERY_GUARD, /addEventListener\("error"/)
assert.match(PLUGIN_RECOVERY_GUARD, /unhandledrejection/)
assert.doesNotMatch(PLUGIN_RECOVERY_GUARD, /Failed to load plugins/)
assert.doesNotMatch(PLUGIN_RECOVERY_GUARD, /localStorage/)
})
test('Gateway session guard reloads DSH pages only after Access Manager rejects the session', () => {
assert.doesNotThrow(() => new Function(SESSION_VALIDITY_GUARD.slice('<script>'.length, -'</script>'.length)))
assert.match(SESSION_VALIDITY_GUARD, /auth\/session-context/)
assert.match(SESSION_VALIDITY_GUARD, /response\.status===401/)
assert.doesNotMatch(SESSION_VALIDITY_GUARD, /innerText|textContent/)
})
test('plugin recovery guard catches official and third-party dynamic import failures', async () => {
for (const [path, pluginId] of [
['/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js?rev=abc123', '@deepseek-ai/dsh-client-ui-input-trigger'],
['/plugins/community-plugin/client.js?rev=def456', 'community-plugin'],
]) {
const result = await simulatePluginError(path)
assert.match(result.replacement, /^\/_dsh_gateway\/wait\?return=/)
assert.deepEqual(result.events.map(value => [value.event, value.pluginId]), [
['browser.plugin-load.failed', pluginId],
['browser.plugin-load.recovery.started', pluginId],
])
}
})
test('plugin recovery guard retries twice and opens the Gateway failure page after the third failure', async () => {
const path = '/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js?rev=abc123'
const storage = new Map()
const first = await simulatePluginError(path, { storage })
const second = await simulatePluginError(path, { storage })
const third = await simulatePluginError(path, { storage })
assert.match(first.replacement, /^\/_dsh_gateway\/wait\?return=/)
assert.match(second.replacement, /^\/_dsh_gateway\/wait\?return=/)
assert.equal(third.replacement, '/_dsh_gateway/plugin-failure')
assert.equal(first.events.at(-1).recoveryAttempt, 1)
assert.equal(second.events.at(-1).recoveryAttempt, 2)
assert.deepEqual(third.events.map(value => [value.event, value.recoveryAttempt]), [
['browser.plugin-load.recovery.failed', 3],
])
})
test('plugin recovery guard does not inspect page text', async () => {
const result = await simulatePluginError('/plugins/example/client.js?rev=abc123', { eventType: 'none' })
assert.equal(result.replacement, undefined)
assert.deepEqual(result.events, [])
})
test('polyfill is feature guarded and uses no weak random source', () => {
assert.match(RANDOM_UUID_POLYFILL, /typeof c\.randomUUID!=="function"/)
assert.match(RANDOM_UUID_POLYFILL, /c\.getRandomValues/)
assert.doesNotMatch(RANDOM_UUID_POLYFILL, /Math\.random/)
})