dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,303 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdtemp, symlink } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { verifyDshWebReady } from '../../control-plane/hooks/dsh-web-ready.mjs'
|
||||
|
||||
async function fixture(routes) {
|
||||
const requests = new Map()
|
||||
const server = createServer((request, response) => {
|
||||
requests.set(request.url, (requests.get(request.url) ?? 0) + 1)
|
||||
const configured = routes.get(`${request.method} ${request.url}`) ?? routes.get(request.url)
|
||||
const route = typeof configured === 'function' ? configured(requests.get(request.url)) : configured
|
||||
response.writeHead(route?.status ?? 404, {
|
||||
'content-type': route?.type ?? 'text/plain',
|
||||
...route?.headers,
|
||||
})
|
||||
response.end(route?.body ?? '')
|
||||
})
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
return { server, port: server.address().port, requests }
|
||||
}
|
||||
|
||||
test('DSH web readiness requires every boot manifest Plugin bundle', async () => {
|
||||
const manifest = JSON.stringify({ rev: 'one', entries: [
|
||||
{ id: 'one', url: '/plugins/one/client.js?rev=one', rev: 'one' },
|
||||
{ id: 'two', url: '/plugins/two/client.js?rev=two', rev: 'two' },
|
||||
] })
|
||||
const routes = new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>globalThis["__DSH_BOOT__"] = ${manifest}</script>` }],
|
||||
['/plugins/one/client.js?rev=one', { status: 200, type: 'text/javascript', body: 'one' }],
|
||||
['/plugins/two/client.js?rev=two', { status: 404 }],
|
||||
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [{ moduleName: 'ready', enabled: true, fiberPhase: 'active' }] } },
|
||||
}) }],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
try {
|
||||
await assert.rejects(verifyDshWebReady({ port: context.port, managedReady: async () => {} }), /returned HTTP 404/)
|
||||
routes.set('/plugins/two/client.js?rev=two', { status: 200, type: 'text/javascript', body: 'two' })
|
||||
await verifyDshWebReady({ port: context.port, managedReady: async () => {} })
|
||||
assert.equal(context.requests.get('/'), 3)
|
||||
assert.equal(context.requests.get('/plugins/one/client.js?rev=one'), 3)
|
||||
assert.equal(context.requests.get('/plugins/two/client.js?rev=two'), 3)
|
||||
assert.equal(context.requests.get('/api/pluginInventory/list'), 2)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness verifies 0.1.5 initial combo batches', async () => {
|
||||
const entryUrl = '/plugins/??one/client.js&rev=entry-one'
|
||||
const batchUrl = '/plugins/??one/client.js,two/client.js&rev=batch-one'
|
||||
const manifest = JSON.stringify({
|
||||
rev: 'graph-one',
|
||||
entries: [{ id: 'one', url: entryUrl, rev: 'entry-one' }],
|
||||
batches: [{ phase: 'application', url: batchUrl, rev: 'batch-one', entries: ['one', 'two'] }],
|
||||
})
|
||||
const routes = new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
||||
[entryUrl, { status: 200, type: 'text/javascript', body: 'one' }],
|
||||
[batchUrl, { status: 404 }],
|
||||
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [{ moduleName: 'ready', enabled: true, fiberPhase: 'active' }] } },
|
||||
}) }],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
try {
|
||||
await assert.rejects(
|
||||
verifyDshWebReady({ port: context.port, managedReady: async () => ({}) }),
|
||||
/returned HTTP 404/,
|
||||
)
|
||||
routes.set(batchUrl, { status: 200, type: 'text/javascript', body: 'one and two' })
|
||||
await verifyDshWebReady({ port: context.port, managedReady: async () => ({}) })
|
||||
assert.equal(context.requests.get(entryUrl), 3)
|
||||
assert.equal(context.requests.get(batchUrl), 3)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness rejects a malformed combo batch contract', async () => {
|
||||
const manifest = JSON.stringify({
|
||||
rev: 'graph-one',
|
||||
entries: [{ id: 'one', url: '/plugins/one/client.js?rev=one', rev: 'one' }],
|
||||
batches: { phase: 'application', url: '/plugins/??one/client.js&rev=one' },
|
||||
})
|
||||
const context = await fixture(new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
||||
]))
|
||||
try {
|
||||
await assert.rejects(
|
||||
verifyDshWebReady({ port: context.port, managedReady: async () => ({}) }),
|
||||
/boot manifest batches are invalid/,
|
||||
)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness exchanges the private launch token for an authenticated cookie', async () => {
|
||||
const manifest = JSON.stringify({ rev: 'one', entries: [
|
||||
{ id: 'one', url: '/plugins/one/client.js?rev=one', rev: 'one' },
|
||||
] })
|
||||
let expectedCookie = ''
|
||||
const routes = new Map([
|
||||
['/?token=fixture-token', {
|
||||
status: 303,
|
||||
headers: { location: '/', 'set-cookie': 'dsh-auth-fixture=session; Path=/; HttpOnly; SameSite=Strict' },
|
||||
}],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
context.server.removeAllListeners('request')
|
||||
context.server.on('request', (request, response) => {
|
||||
context.requests.set(request.url, (context.requests.get(request.url) ?? 0) + 1)
|
||||
if (request.url === '/?token=fixture-token') {
|
||||
response.writeHead(303, {
|
||||
location: '/',
|
||||
'set-cookie': 'dsh-auth-fixture=session; Path=/; HttpOnly; SameSite=Strict',
|
||||
})
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
expectedCookie = request.headers.cookie ?? ''
|
||||
if (expectedCookie !== 'dsh-auth-fixture=session') {
|
||||
response.writeHead(401)
|
||||
response.end('unauthorized')
|
||||
return
|
||||
}
|
||||
if (request.url === '/') {
|
||||
response.writeHead(200, { 'content-type': 'text/html' })
|
||||
response.end(`<script>window.__DSH_BOOT__ = ${manifest}</script>`)
|
||||
} else if (request.url === '/plugins/one/client.js?rev=one') {
|
||||
response.writeHead(200, { 'content-type': 'text/javascript' })
|
||||
response.end('one')
|
||||
} else if (request.url === '/api/pluginInventory/list') {
|
||||
response.writeHead(200, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [{ moduleName: 'ready', enabled: true, fiberPhase: 'active' }] } },
|
||||
}))
|
||||
} else {
|
||||
response.writeHead(404)
|
||||
response.end()
|
||||
}
|
||||
})
|
||||
try {
|
||||
await verifyDshWebReady({
|
||||
port: context.port,
|
||||
managedReady: async () => ({ ready: true, readyUrl: `http://127.0.0.1:${String(context.port)}/?token=fixture-token` }),
|
||||
})
|
||||
assert.equal(expectedCookie, 'dsh-auth-fixture=session')
|
||||
assert.equal(context.requests.get('/?token=fixture-token'), 1)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness rejects enabled Plugins that are not active', async () => {
|
||||
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
||||
const routes = new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
||||
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [
|
||||
{ moduleName: 'active', enabled: true, fiberPhase: 'active' },
|
||||
{ moduleName: 'disabled', enabled: false, fiberPhase: null },
|
||||
{ moduleName: 'broken', enabled: true, fiberPhase: 'failed' },
|
||||
] } },
|
||||
}) }],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
try {
|
||||
await assert.rejects(
|
||||
verifyDshWebReady({ port: context.port, managedReady: async () => {} }),
|
||||
/DSH Plugins are not active: broken \(failed\)/,
|
||||
)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness rejects Plugin inventory RPC failures', async () => {
|
||||
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
||||
const routes = new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
||||
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: false, error: { code: 'internal', message: 'loader unavailable', details: {} } },
|
||||
}) }],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
try {
|
||||
await assert.rejects(
|
||||
verifyDshWebReady({ port: context.port, managedReady: async () => {} }),
|
||||
/DSH Plugin inventory is unavailable: loader unavailable/,
|
||||
)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness rejects a lifecycle generation change during verification', async () => {
|
||||
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
||||
const routes = new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
||||
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [] } },
|
||||
}) }],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
let observation = 0
|
||||
try {
|
||||
await assert.rejects(verifyDshWebReady({
|
||||
port: context.port,
|
||||
managedReady: async () => ({ generation: ++observation === 1 ? 'one' : 'two', ready: true, readyUrl: null }),
|
||||
}), /lifecycle changed during Web readiness verification/)
|
||||
assert.equal(context.requests.get('/'), 1)
|
||||
assert.equal(context.requests.get('/api/pluginInventory/list'), 1)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness rejects a boot manifest mutation between observations', async () => {
|
||||
const manifest = rev => JSON.stringify({ rev, entries: [] })
|
||||
const routes = new Map([
|
||||
['/', observation => ({
|
||||
status: 200,
|
||||
type: 'text/html',
|
||||
body: `<script>window.__DSH_BOOT__ = ${manifest(observation === 1 ? 'one' : 'two')}</script>`,
|
||||
})],
|
||||
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [] } },
|
||||
}) }],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
try {
|
||||
await assert.rejects(
|
||||
verifyDshWebReady({ port: context.port, managedReady: async () => ({ generation: 'one' }) }),
|
||||
/boot manifest changed during Web readiness verification/,
|
||||
)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('DSH web readiness rejects a Plugin inventory mutation between observations', async () => {
|
||||
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
||||
const inventory = moduleName => JSON.stringify({
|
||||
type: 'server-response',
|
||||
rpcId: 'dsh-platform-readiness',
|
||||
result: { ok: true, value: { entries: [{ moduleName, enabled: true, fiberPhase: 'active' }] } },
|
||||
})
|
||||
const routes = new Map([
|
||||
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
||||
['POST /api/pluginInventory/list', observation => ({
|
||||
status: 200,
|
||||
type: 'application/json',
|
||||
body: inventory(observation === 1 ? 'one' : 'two'),
|
||||
})],
|
||||
])
|
||||
const context = await fixture(routes)
|
||||
try {
|
||||
await assert.rejects(
|
||||
verifyDshWebReady({ port: context.port, managedReady: async () => ({ generation: 'one' }) }),
|
||||
/Plugin inventory changed during Web readiness verification/,
|
||||
)
|
||||
} finally {
|
||||
await new Promise(resolve => context.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('executes readiness checks when launched through the Bootstrap view symlink', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-web-ready-view-'))
|
||||
const executable = join(root, 'dsh-web-ready.mjs')
|
||||
await symlink(new URL('../../control-plane/hooks/dsh-web-ready.mjs', import.meta.url), executable)
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [executable], {
|
||||
env: { ...process.env, DSH_PLATFORM_RUN: join(root, 'missing-run') },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
const stderr = []
|
||||
child.stderr.on('data', chunk => stderr.push(chunk))
|
||||
child.once('error', reject)
|
||||
child.once('exit', code => resolve({ code, stderr: Buffer.concat(stderr).toString('utf8') }))
|
||||
})
|
||||
assert.notEqual(result.code, 0)
|
||||
assert.match(result.stderr, /dsh-lifecycle\.sock/)
|
||||
})
|
||||
Reference in new issue
Block a user