Files
dsh/upstream/container/platform/test/bootstrap.test.mjs
T

1319 lines
53 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
import { canonicalJson } from '../lib/canonical-json.mjs'
import { BootstrapRuntime } from '../bootstrap/lib/runtime.mjs'
import { EnvironmentRunner, loadControlPlane } from '../bootstrap/lib/lifecycle.mjs'
import { createBootstrapControl, listenBootstrapControl } from '../bootstrap/lib/control.mjs'
import { LocalApiClient } from '../../control-plane/modules/updater/lib/client.mjs'
function command(script, args = []) {
return { executable: process.execPath, args: [script, ...args], timeoutSeconds: 5 }
}
const emptyLifecycle = {
prepare: null,
preStart: null,
postStart: null,
preStop: null,
stop: null,
postStop: null,
}
async function environment(components) {
const root = await mkdtemp(join(tmpdir(), 'dsh-environment-runner-'))
await mkdir(join(root, 'artifacts'))
const references = []
const artifacts = []
for (const component of components) {
const artifactId = `component-${component.id}`
const bytes = canonicalJson(component)
const sha256 = createHash('sha256').update(bytes).digest('hex')
await writeFile(join(root, 'artifacts', artifactId), bytes)
references.push({ id: component.id, sha256 })
artifacts.push({
id: artifactId,
mediaType: 'application/vnd.dsh-platform.component.v1+json',
sha256,
size: bytes.byteLength,
url: `https://release.example/${artifactId}`,
})
}
await writeFile(join(root, 'environment.manifest.json'), canonicalJson({
schema: 1,
manifestType: 'environment',
version: 'test',
keyringGeneration: 1,
targetSequence: 1,
issuedAt: '2026-08-19T00:00:00.000Z',
artifacts,
bootstrapApi: 1,
components: references,
patches: [],
systemPlugins: [],
}))
return root
}
function component(id, script, type = 'oneshot', lifecycle = emptyLifecycle) {
return {
schema: 1,
id,
type,
command: command(script),
environment: {},
lifecycle,
health: null,
logging: { stdout: true, stderr: true },
}
}
test('runs components in manifest order and stop phases in reverse order', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-order-'))
const log = join(temp, 'order.log')
const append = join(temp, 'append.mjs')
const service = join(temp, 'service.mjs')
const check = join(temp, 'check.mjs')
await writeFile(append, 'import { appendFileSync } from "node:fs"; appendFileSync(process.argv[2], process.argv[3] + "\\n")')
await writeFile(service, 'import { appendFileSync } from "node:fs"; appendFileSync(process.argv[2], process.argv[3] + "\\n"); setInterval(() => {}, 1000)')
await writeFile(check, 'import { readFileSync } from "node:fs"; process.exit(readFileSync(process.argv[2], "utf8").includes(process.argv[3]) ? 0 : 1)')
const lifecycle = label => ({
...emptyLifecycle,
prepare: command(append, [log, `${label}:prepare`]),
preStart: command(append, [log, `${label}:preStart`]),
postStart: command(append, [log, `${label}:postStart`]),
preStop: command(append, [log, `${label}:preStop`]),
postStop: command(append, [log, `${label}:postStop`]),
})
const first = component('first', service, 'service', lifecycle('first'))
first.command = command(service, [log, 'first:start'])
first.health = {
type: 'exec',
command: command(check, [log, 'first:start']),
intervalSeconds: 1,
timeoutSeconds: 5,
}
const second = component('second', append, 'oneshot', lifecycle('second'))
second.command = command(append, [log, 'second:start'])
const reports = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([first, second]),
capture: () => {},
report: (message, fields) => { reports.push({ message, fields }) },
})
await runner.start()
await runner.stop()
const lines = (await readFile(log, 'utf8')).trim().split('\n')
assert.deepEqual(lines.slice(0, 8), [
'first:prepare', 'second:prepare', 'first:preStart', 'first:start',
'first:postStart', 'second:preStart', 'second:start', 'second:postStart',
])
assert.deepEqual(lines.slice(8), ['second:preStop', 'second:postStop', 'first:preStop', 'first:postStop'])
assert.deepEqual(reports.map(report => [report.message, report.fields.componentId]), [
['component.starting', 'first'],
['component.spawned', 'first'],
['component.ready', 'first'],
['component.starting', 'second'],
['component.ready', 'second'],
['component.stopping', 'second'],
['component.stopped', 'second'],
['component.stopping', 'first'],
['component.stopped', 'first'],
])
assert.equal(reports.find(report => report.message === 'component.ready').fields.elapsedMs >= 0, true)
})
test('starts explicit component groups concurrently and waits before the next group', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-groups-'))
const barrier = join(temp, 'barrier.mjs')
const after = join(temp, 'after.mjs')
const firstStarted = join(temp, 'first.started')
const secondStarted = join(temp, 'second.started')
const firstDone = join(temp, 'first.done')
const secondDone = join(temp, 'second.done')
await writeFile(barrier, `
import { existsSync, writeFileSync } from 'node:fs'
writeFileSync(process.argv[2], '')
while (!existsSync(process.argv[3])) await new Promise(resolve => setTimeout(resolve, 10))
writeFileSync(process.argv[4], '')
`)
await writeFile(after, `
import { existsSync } from 'node:fs'
process.exit(existsSync(process.argv[2]) && existsSync(process.argv[3]) ? 0 : 9)
`)
const first = component('first', barrier)
first.command = command(barrier, [firstStarted, secondStarted, firstDone])
const second = component('second', barrier)
second.command = command(barrier, [secondStarted, firstStarted, secondDone])
const final = component('final', after)
final.command = command(after, [firstDone, secondDone])
const runner = new EnvironmentRunner({
environmentRoot: await environment([first, second, final]),
capture: () => {},
startupGroups: [['first', 'second'], ['final']],
})
await runner.start()
assert.deepEqual(runner.status().components.map(value => value.id), ['first', 'second', 'final'])
await runner.stop()
})
test('waits for a failed startup group before rolling back every started service', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-group-failure-'))
const service = join(temp, 'service.mjs')
const failure = join(temp, 'failure.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
await writeFile(failure, 'process.exit(9)')
const reports = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([
component('service', service, 'service'),
component('failure', failure),
]),
capture: () => {},
report: (message, fields) => { reports.push({ message, fields }) },
startupGroups: [['service', 'failure']],
})
await assert.rejects(runner.start(), /failure command failed/)
assert.deepEqual(runner.status().components, [])
assert.equal(reports.some(report => report.message === 'component.stopped'
&& report.fields.componentId === 'service'), true)
})
test('does not recover an isolated startup failure after its group fatally fails', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-group-recovery-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
const reports = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([
component('gateway', service, 'service'),
component('outbound-proxy', service, 'service'),
]),
capture: () => {},
recoverableComponents: ['outbound-proxy'],
recoveryDelaysMs: [0],
report: (message, fields) => { reports.push({ message, fields }) },
startupGroups: [['gateway', 'outbound-proxy']],
})
runner.startComponentUnlocked = candidate => Promise.reject(new Error(`${candidate.id} unavailable`))
await assert.rejects(runner.start(), /gateway unavailable/)
await new Promise(resolve => setImmediate(resolve))
assert.equal(reports.some(report => report.message === 'component.recovery.started'), false)
})
test('rejects incomplete or duplicated startup groups before component preparation', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-invalid-groups-'))
const marker = join(temp, 'prepared')
const append = join(temp, 'append.mjs')
await writeFile(append, `import { writeFileSync } from 'node:fs'; writeFileSync(process.argv[2], '')`)
const first = component('first', append)
first.lifecycle = { ...emptyLifecycle, prepare: command(append, [marker]) }
const root = await environment([first, component('second', append)])
const omitted = new EnvironmentRunner({
environmentRoot: root,
capture: () => {},
startupGroups: [['first']],
})
await assert.rejects(omitted.start(), /omit components: second/)
await assert.rejects(readFile(marker), error => error?.code === 'ENOENT')
const duplicated = new EnvironmentRunner({
environmentRoot: root,
capture: () => {},
startupGroups: [['first'], ['first', 'second']],
})
await assert.rejects(duplicated.start(), /component first is duplicated/)
})
test('exec health probes use only their exit status and do not emit component logs', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-health-output-'))
const service = join(temp, 'service.mjs')
const health = join(temp, 'health.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
await writeFile(health, 'console.log(JSON.stringify({ noisy: true }, null, 2))')
const candidate = component('service', service, 'service')
candidate.health = {
type: 'exec',
command: command(health),
intervalSeconds: 1,
timeoutSeconds: 5,
}
let captures = 0
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate]),
capture: () => { captures += 1 },
})
await runner.start()
assert.equal((await runner.health()).healthy, true)
assert.equal(captures, 1)
await runner.stop()
})
test('startup health retries do not inherit the steady one-second interval', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-health-retry-'))
const service = join(temp, 'service.mjs')
const health = join(temp, 'health.mjs')
const attempts = join(temp, 'attempts')
await writeFile(service, 'setInterval(() => {}, 1000)')
await writeFile(health, `import { existsSync, writeFileSync } from 'node:fs'; if (!existsSync(process.argv[2])) { writeFileSync(process.argv[2], 'first'); process.exit(1) }`)
const candidate = component('service', service, 'service')
candidate.health = {
type: 'exec',
command: command(health, [attempts]),
intervalSeconds: 2,
timeoutSeconds: 5,
}
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate]),
capture: () => {},
})
const started = Date.now()
await runner.start()
const elapsedMs = Date.now() - started
assert.ok(elapsedMs < 1_000, `startup health retry took ${String(elapsedMs)} ms`)
await runner.stop()
})
test('component environment replaces an inherited root user environment', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-component-environment-'))
const output = join(temp, 'environment.json')
const inspect = join(temp, 'inspect.mjs')
await writeFile(inspect, `import { writeFileSync } from 'node:fs'; writeFileSync(process.argv[2], JSON.stringify({ HOME: process.env.HOME, USER: process.env.USER, LOGNAME: process.env.LOGNAME, XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, XDG_CACHE_HOME: process.env.XDG_CACHE_HOME, XDG_DATA_HOME: process.env.XDG_DATA_HOME }))`)
const candidate = component('dsh-runtime', inspect)
candidate.command = command(inspect, [output])
candidate.environment = {
HOME: '/home/node',
USER: 'node',
LOGNAME: 'node',
XDG_CONFIG_HOME: '/home/node/.config',
XDG_CACHE_HOME: '/home/node/.cache',
XDG_DATA_HOME: '/home/node/.local/share',
}
const original = {
HOME: process.env.HOME,
USER: process.env.USER,
LOGNAME: process.env.LOGNAME,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
}
process.env.HOME = '/root'
process.env.USER = 'root'
process.env.LOGNAME = 'root'
process.env.XDG_CONFIG_HOME = '/root/.config'
process.env.XDG_CACHE_HOME = '/root/.cache'
process.env.XDG_DATA_HOME = '/root/.local/share'
try {
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate]),
capture: () => {},
})
await runner.start()
assert.deepEqual(JSON.parse(await readFile(output, 'utf8')), candidate.environment)
} finally {
for (const [name, value] of Object.entries(original)) {
if (value === undefined) delete process.env[name]
else process.env[name] = value
}
}
})
test('binds prepared service environment and cleanup to one process launch', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-prepared-service-'))
const output = join(temp, 'launch-token')
const service = join(temp, 'service.mjs')
const health = join(temp, 'health.mjs')
await writeFile(service, `import { writeFileSync } from 'node:fs'; writeFileSync(process.argv[2], process.env.DSH_PLATFORM_LAUNCH_TOKEN); setInterval(() => {}, 1000)`)
await writeFile(health, `import { existsSync } from 'node:fs'; process.exit(existsSync(process.argv[2]) ? 0 : 1)`)
let releases = 0
const candidate = component('dsh-runtime', service, 'service')
candidate.command = command(service, [output])
candidate.health = {
type: 'exec', command: command(health, [output]), intervalSeconds: 1, timeoutSeconds: 5,
}
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate]),
capture: () => {},
prepareService: async value => ({
environment: { DSH_PLATFORM_LAUNCH_TOKEN: `token-for-${value.id}` },
release: () => { releases += 1 },
}),
})
try {
await runner.start()
assert.equal(await readFile(output, 'utf8'), 'token-for-dsh-runtime')
assert.equal(releases, 0)
} finally {
await runner.stop()
}
assert.equal(releases, 1)
})
test('releases prepared service ownership when process spawn fails', async () => {
let releases = 0
const candidate = component('dsh-runtime', '/path/that/does/not/exist', 'service')
candidate.command.executable = '/path/that/does/not/exist'
candidate.command.args = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate]),
capture: () => {},
prepareService: async () => ({
environment: { DSH_PLATFORM_LAUNCH_TOKEN: 'unused-token' },
release: () => { releases += 1 },
}),
})
await assert.rejects(runner.start(), /ENOENT/)
assert.equal(releases, 1)
})
test('stops already-started services when a later component fails', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-failure-'))
const service = join(temp, 'service.mjs')
const failure = join(temp, 'failure.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
await writeFile(failure, 'process.exit(9)')
const reports = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([
component('service', service, 'service'),
component('failure', failure),
]),
capture: () => {},
report: (message, fields) => { reports.push({ message, fields }) },
})
await assert.rejects(runner.start(), /failure command failed/)
assert.deepEqual(runner.status().components, [])
const failureReport = reports.find(report => report.message === 'component.start.failed').fields
assert.equal(failureReport.componentId, 'failure')
assert.equal(failureReport.elapsedMs >= 0, true)
assert.match(failureReport.error, /failure command failed.*code=9/)
assert.equal(failureReport.level, 'error')
})
test('reports a service exit after readiness as a fatal Bootstrap condition', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-runtime-exit-'))
const exits = join(temp, 'exits.mjs')
await writeFile(exits, 'setTimeout(() => process.exit(7), 20)')
const reports = []
const observed = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([component('service', exits, 'service')]),
capture: () => {},
report: (message, fields) => { reports.push({ message, fields }) },
})
runner.onFatal(error => observed.push(error))
await runner.start()
const error = await runner.fatal
assert.match(error.message, /service exited unexpectedly.*code=7/)
assert.equal(error.componentId, 'service')
assert.equal(error.exitCode, 7)
assert.equal(observed[0], error)
assert.equal(reports.find(report => report.message === 'component.exited').fields.level, 'error')
await runner.stop()
})
test('recovers an opted-in service exit without stopping sibling services', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-isolated-recovery-'))
const counter = join(temp, 'counter')
const recoverable = join(temp, 'recoverable.mjs')
const sibling = join(temp, 'sibling.mjs')
await writeFile(recoverable, `import { existsSync, writeFileSync } from 'node:fs'; if (!existsSync(process.argv[2])) { writeFileSync(process.argv[2], '1'); setTimeout(() => process.exit(7), 20) } else setInterval(() => {}, 1000)`)
await writeFile(sibling, 'setInterval(() => {}, 1000)')
const candidate = component('outbound-proxy', recoverable, 'service')
candidate.command = command(recoverable, [counter])
const reports = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate, component('management', sibling, 'service')]),
capture: () => {},
recoverableComponents: ['outbound-proxy'],
recoveryDelaysMs: [0, 0, 0],
report: (message, fields) => { reports.push({ message, fields }) },
})
await runner.start()
const deadline = Date.now() + 2_000
while (!reports.some(report => report.message === 'component.recovery.completed')) {
if (Date.now() >= deadline) throw new Error('recoverable component did not restart')
await new Promise(resolve => setTimeout(resolve, 10))
}
assert.deepEqual(runner.status().components.map(value => value.id), ['outbound-proxy', 'management'])
assert.equal(reports.some(report => report.message === 'component.exited'), true)
assert.equal(await Promise.race([
runner.fatal.then(() => 'fatal'),
new Promise(resolve => setTimeout(() => resolve('isolated'), 20)),
]), 'isolated')
await runner.stop()
})
test('isolates a persistently failing opted-in component and starts later services', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-isolated-startup-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
const reports = []
const runner = new EnvironmentRunner({
environmentRoot: await environment([
component('outbound-proxy', service, 'service'),
component('management', service, 'service'),
]),
capture: () => {},
recoverableComponents: ['outbound-proxy'],
recoveryDelaysMs: [0, 0, 0],
report: (message, fields) => { reports.push({ message, fields }) },
})
const start = runner.startComponentUnlocked.bind(runner)
runner.startComponentUnlocked = (candidate, prepare) => candidate.id === 'outbound-proxy'
? Promise.reject(new Error('proxy port conflict'))
: start(candidate, prepare)
await runner.start()
const deadline = Date.now() + 2_000
while (reports.filter(report => report.message === 'component.isolated').length < 2) {
if (Date.now() >= deadline) throw new Error('proxy failure was not isolated')
await new Promise(resolve => setTimeout(resolve, 10))
}
assert.deepEqual(runner.status().components.map(value => value.id), ['management'])
assert.equal(reports.filter(report => report.message === 'component.recovery.failed').length, 3)
await runner.stop()
})
test('contains a service exit during readiness inside the startup operation', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-startup-exit-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'process.exit(1)')
const candidate = component('candidate', service, 'service')
candidate.health = {
type: 'http', host: '127.0.0.1', port: 1, path: '/', intervalSeconds: 1, timeoutSeconds: 1,
}
const runner = new EnvironmentRunner({
environmentRoot: await environment([candidate]),
capture: () => {},
})
await assert.rejects(runner.start(), /exited before health check passed/)
const outcome = await Promise.race([
runner.fatal.then(() => 'fatal'),
new Promise(resolve => setTimeout(() => resolve('contained'), 20)),
])
assert.equal(outcome, 'contained')
})
test('suspends, resumes, and restarts one service while keeping other Environment components running', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-suspend-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
const runner = new EnvironmentRunner({
environmentRoot: await environment([
component('dsh-runtime', service, 'service'),
component('platform-management', service, 'service'),
]),
capture: () => {},
})
await runner.start()
await runner.suspend('dsh-runtime')
await runner.pause('dsh-runtime')
await runner.pause('dsh-runtime')
await assert.rejects(runner.pause('unknown-service'), /does not exist/)
assert.deepEqual(runner.status().components.map(value => value.id), ['platform-management'])
assert.equal((await runner.health()).healthy, false)
await runner.resume('dsh-runtime')
assert.deepEqual(runner.status().components.map(value => value.id), ['dsh-runtime', 'platform-management'])
const managementPid = runner.status().components.find(value => value.id === 'platform-management').pid
const firstDshPid = runner.status().components.find(value => value.id === 'dsh-runtime').pid
await runner.restart('dsh-runtime')
assert.notEqual(runner.status().components.find(value => value.id === 'dsh-runtime').pid, firstDshPid)
assert.equal(runner.status().components.find(value => value.id === 'platform-management').pid, managementPid)
assert.equal((await runner.health()).healthy, true)
assert.equal(await Promise.race([
runner.fatal.then(() => 'fatal'),
new Promise(resolve => setTimeout(() => resolve('intentional'), 20)),
]), 'intentional')
await runner.stop()
})
test('retries a service restart after its first start attempt fails', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-restart-retry-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
const runner = new EnvironmentRunner({
environmentRoot: await environment([component('dsh-runtime', service, 'service')]),
capture: () => {},
})
await runner.start()
const startComponent = runner.startComponentUnlocked.bind(runner)
let failStart = true
runner.startComponentUnlocked = async (...args) => {
if (failStart) {
failStart = false
throw new Error('restart failed')
}
return startComponent(...args)
}
await assert.rejects(runner.restart('dsh-runtime'), /restart failed/)
assert.deepEqual(runner.status().components, [])
await runner.restart('dsh-runtime')
assert.deepEqual(runner.status().components.map(value => value.id), ['dsh-runtime'])
await runner.stop()
})
test('restores a service after a transactional restart candidate fails', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-lifecycle-restart-recovery-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
const runner = new EnvironmentRunner({
environmentRoot: await environment([component('dsh-runtime', service, 'service')]),
capture: () => {},
})
await runner.start()
const startComponent = runner.startComponentUnlocked.bind(runner)
let candidate = true
runner.startComponentUnlocked = async (...args) => {
if (candidate) {
candidate = false
throw new Error('candidate failed')
}
return startComponent(...args)
}
const changes = []
await assert.rejects(runner.restart('dsh-runtime', {
recoverStart: true,
beforeStart: async () => { changes.push('activate') },
onStartFailure: async () => { changes.push('rollback') },
}), /candidate failed/)
assert.deepEqual(changes, ['activate', 'rollback'])
assert.deepEqual(runner.status().components.map(value => value.id), ['dsh-runtime'])
await runner.stop()
})
test('Bootstrap control socket exposes component suspension, resumption, restart, and health', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-bootstrap-control-'))
const calls = []
const runner = {
status: () => ({ components: [] }),
reload: async () => ({}),
health: async () => ({ healthy: true, components: [] }),
suspend: async id => { calls.push(['suspend', id]); return {} },
pause: async id => { calls.push(['pause', id]); return {} },
resume: async id => { calls.push(['resume', id]); return {} },
restart: async (id, options = {}) => {
calls.push(['restart', id])
try {
await options.beforeStart?.()
return {}
} catch (error) {
await options.onStartFailure?.(error)
if (options.recoverStart) calls.push(['restart-recovered', id])
throw error
}
},
record: async (message, fields) => { calls.push(['report', message, fields.pathname ?? null]) },
}
const deployments = {
exclusive: operation => operation(),
setOperation: async operation => { calls.push(['operation', operation]) },
publishStatus: async options => { calls.push(['status', 'published', options ?? null]) },
resetCurrentRuntime: async ({ pauseDsh, restartDsh }) => {
calls.push(['runtime-reset'])
await pauseDsh()
await restartDsh()
return { recordId: 'repaired' }
},
}
const systemPlugins = {
prepare: async () => {
calls.push(['prepare-system-plugins'])
return {
activate: async () => { calls.push(['activate-system-plugins']) },
commit: async () => { calls.push(['commit-system-plugins']) },
rollback: async () => { calls.push(['rollback-system-plugins']) },
}
},
list: async () => [{ id: 'platform-management', installed: true }],
configure: async (id, action) => {
calls.push(['configure', id, action])
return [{ id, installed: true }]
},
recover: async (id, action) => {
calls.push(['recover', id, action])
return [{ id, installed: false }]
},
discard: async () => {
calls.push(['discard-system-plugins'])
return [{ id: 'platform-management', installed: true }]
},
}
const server = createBootstrapControl(runner, { deployments, systemPlugins })
const socket = join(root, 'run', 'bootstrap.sock')
await listenBootstrapControl(server, socket)
const client = new LocalApiClient(socket)
try {
assert.equal((await client.request('GET', '/v1/health')).healthy, true)
assert.deepEqual((await client.request('GET', '/v1/system-plugins')).plugins, [{
id: 'platform-management', installed: true,
}])
assert.deepEqual((await client.request('POST', '/v1/system-plugins/action', {
id: 'diagnostics', action: 'disable',
})).plugins, [{ id: 'diagnostics', installed: true }])
assert.deepEqual((await client.request('POST', '/v1/system-plugins/recovery-action', {
id: 'platform-management', action: 'uninstall',
})).plugins, [{ id: 'platform-management', installed: false }])
assert.deepEqual((await client.request('POST', '/v1/system-plugins/discard')).plugins, [{
id: 'platform-management', installed: true,
}])
await client.request('POST', '/v1/components/dsh-runtime/suspend')
await client.request('POST', '/v1/components/dsh-runtime/pause')
await client.request('POST', '/v1/components/dsh-runtime/resume')
await client.request('POST', '/v1/components/dsh-runtime/restart')
assert.deepEqual(calls, [
['configure', 'diagnostics', 'disable'],
['recover', 'platform-management', 'uninstall'],
['discard-system-plugins'],
['suspend', 'dsh-runtime'],
['pause', 'dsh-runtime'],
['operation', 'starting'],
['resume', 'dsh-runtime'],
['status', 'published', null],
['operation', 'restarting'],
['prepare-system-plugins'],
['restart', 'dsh-runtime'],
['activate-system-plugins'],
['commit-system-plugins'],
['status', 'published', null],
])
assert.equal((await client.request('POST', '/v1/deployments/runtime/reset')).recordId, 'repaired')
assert.deepEqual(calls.slice(-3), [
['runtime-reset'],
['pause', 'dsh-runtime'],
['restart', 'dsh-runtime'],
])
systemPlugins.prepare = async () => {
calls.push(['prepare-system-plugins-failed'])
return {
activate: async () => {
calls.push(['activate-system-plugins-failed'])
throw new Error('overlay failed')
},
rollback: async () => { calls.push(['rollback-system-plugins']) },
}
}
await assert.rejects(client.request('POST', '/v1/components/dsh-runtime/restart'), /overlay failed/)
assert.deepEqual(calls.slice(-8), [
['operation', 'restarting'],
['prepare-system-plugins-failed'],
['restart', 'dsh-runtime'],
['activate-system-plugins-failed'],
['rollback-system-plugins'],
['restart-recovered', 'dsh-runtime'],
['status', 'published', { operation: 'restart-failed', recoveryMode: 'overlay failed' }],
['report', 'bootstrap.request.failed', '/v1/components/dsh-runtime/restart'],
])
runner.resume = async id => {
calls.push(['resume-failed', id])
throw new Error('resume remained unavailable')
}
runner.status = () => ({ components: [], recoveryMode: 'resume remained unavailable' })
await assert.rejects(
client.request('POST', '/v1/components/dsh-runtime/resume'),
/resume remained unavailable/,
)
assert.deepEqual(calls.slice(-4), [
['operation', 'starting'],
['resume-failed', 'dsh-runtime'],
['status', 'published', { recoveryMode: 'resume remained unavailable' }],
['report', 'bootstrap.request.failed', '/v1/components/dsh-runtime/resume'],
])
} finally {
await new Promise(resolve => server.close(resolve))
}
})
test('loads the checked-in Control Plane independently from an Environment manifest', async () => {
const controlPlane = await loadControlPlane(join(
dirname(dirname(fileURLToPath(import.meta.url))),
'..',
'control-plane',
))
assert.equal(controlPlane.manifest.version, null)
assert.deepEqual(controlPlane.components.map(component => component.id), [
'gateway', 'access-manager', 'outbound-proxy', 'platform-recovery', 'platform-management',
])
})
test('keeps the Control Plane running while Environment operations replace DSH', async () => {
const calls = []
const runner = (name, status) => ({
fatal: new Promise(() => {}),
start: async () => { calls.push(`${name}:start`) },
stop: async () => { calls.push(`${name}:stop`) },
reload: async () => { calls.push(`${name}:reload`); return status },
suspend: async id => { calls.push(`${name}:suspend:${id}`); return status },
pause: async id => { calls.push(`${name}:pause:${id}`); return status },
resume: async id => { calls.push(`${name}:resume:${id}`); return status },
restart: async id => { calls.push(`${name}:restart:${id}`); return status },
health: async () => ({ healthy: true, components: status.components }),
status: () => status,
})
const controlPlane = runner('control', {
environmentVersion: null,
components: [{ id: 'gateway' }],
})
const environmentRunner = runner('environment', {
environmentVersion: '1.0.0',
components: [{ id: 'dsh-runtime' }],
})
const runtime = new BootstrapRuntime({
controlPlane,
environment: environmentRunner,
onEnvironmentVerified: async () => { calls.push('environment:verified') },
onDshAvailable: async () => { calls.push('dsh:available') },
})
await runtime.start()
await runtime.suspend('dsh-runtime')
assert.equal(runtime.status().dshLifecycle.state, 'stopped')
await runtime.reload()
assert.equal(runtime.status().dshLifecycle.state, 'running')
await runtime.pause('dsh-runtime')
await runtime.resume('dsh-runtime')
assert.equal(runtime.status().dshLifecycle.state, 'running')
await runtime.restart('dsh-runtime')
await runtime.reload()
assert.equal((await runtime.health()).healthy, true)
assert.deepEqual(runtime.status().controlPlane, [{ id: 'gateway' }])
await runtime.stop()
assert.deepEqual(calls, [
'control:start',
'environment:start',
'environment:verified',
'environment:suspend:dsh-runtime',
'environment:reload',
'environment:verified',
'dsh:available',
'environment:pause:dsh-runtime',
'environment:resume:dsh-runtime',
'environment:verified',
'dsh:available',
'environment:restart:dsh-runtime',
'environment:verified',
'dsh:available',
'environment:reload',
'environment:verified',
'dsh:available',
'environment:stop',
'control:stop',
])
})
test('starts DSH alongside the Control Plane but gates readiness on access classification', async () => {
const calls = []
let resolveEnvironmentStarted
const environmentStarted = new Promise(resolve => { resolveEnvironmentStarted = resolve })
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}),
start: async () => { calls.push('control:start'); await environmentStarted },
stop: async () => { calls.push('control:stop') },
status: () => ({ components: [{ id: 'access-manager' }] }),
},
environment: {
fatal: new Promise(() => {}),
start: async () => { calls.push('environment:start'); resolveEnvironmentStarted() },
stop: async () => { calls.push('environment:stop') },
status: () => ({ environmentVersion: '1.0.0', components: [{ id: 'dsh-runtime' }] }),
},
beforeReady: async () => { calls.push('access:classify') },
})
await runtime.start()
assert.deepEqual(calls, ['control:start', 'environment:start', 'access:classify'])
await runtime.stop()
const blocked = []
let resolveBlockedEnvironmentStarted
const blockedEnvironmentStarted = new Promise(resolve => { resolveBlockedEnvironmentStarted = resolve })
const failure = new Error('access classification failed')
const blockedRuntime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}),
start: async () => { blocked.push('control:start'); await blockedEnvironmentStarted },
stop: async () => { blocked.push('control:stop') },
status: () => ({ components: [{ id: 'access-manager' }] }),
},
environment: {
fatal: new Promise(() => {}),
start: async () => { blocked.push('environment:start'); resolveBlockedEnvironmentStarted() },
stop: async () => { blocked.push('environment:stop') },
status: () => ({ environmentVersion: '1.0.0', components: [] }),
},
beforeReady: async () => { blocked.push('access:classify'); throw failure },
})
await assert.rejects(blockedRuntime.start(), error => error === failure)
assert.deepEqual(blocked, [
'control:start', 'environment:start', 'access:classify', 'environment:stop', 'control:stop',
])
})
test('isolates a non-DSH Environment exit and keeps the Control Plane available', async () => {
const calls = []
let emitEnvironmentFatal
const controlPlane = {
fatal: new Promise(() => {}),
start: async () => { calls.push('control:start') },
stop: async () => { calls.push('control:stop') },
status: () => ({ components: [{ id: 'gateway' }, { id: 'platform-management' }] }),
}
const environmentRunner = {
fatal: new Promise(() => {}),
onFatal: listener => {
emitEnvironmentFatal = listener
return () => { emitEnvironmentFatal = undefined }
},
start: async () => { calls.push('environment:start') },
stop: async () => { calls.push('environment:stop') },
restart: async id => { calls.push(`environment:restart:${id}`); return { components: [{ id }] } },
status: () => ({ environmentVersion: '1.0.0', components: [] }),
}
const runtime = new BootstrapRuntime({
controlPlane,
environment: environmentRunner,
onEnvironmentFatal: async error => { calls.push(`recovery:${error.message}`) },
})
await runtime.start()
const failure = Object.assign(new Error('worker exited unexpectedly'), { componentId: 'worker' })
emitEnvironmentFatal(failure)
await runtime.recovery
assert.equal(runtime.status().recoveryMode, 'worker exited unexpectedly')
assert.deepEqual(runtime.status().controlPlane.map(component => component.id), ['gateway', 'platform-management'])
assert.deepEqual(calls, [
'control:start',
'environment:start',
'recovery:worker exited unexpectedly',
'environment:stop',
])
assert.equal(await Promise.race([
runtime.fatal.then(() => 'fatal'),
new Promise(resolve => setTimeout(() => resolve('available'), 20)),
]), 'available')
await runtime.restart('dsh-runtime')
assert.equal(runtime.status().recoveryMode, null)
await runtime.stop()
})
test('recovers an unexpected DSH exit without stopping the Control Plane', async () => {
const calls = []
let emitEnvironmentFatal
const environmentRunner = {
fatal: new Promise(() => {}),
onFatal: listener => { emitEnvironmentFatal = listener; return () => {} },
start: async () => { calls.push('environment:start') },
stop: async () => { calls.push('environment:stop') },
restart: async id => { calls.push(`environment:restart:${id}`); return { components: [{ id }] } },
status: () => ({ environmentVersion: '1.0.3', components: [{ id: 'dsh-runtime' }] }),
}
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}),
start: async () => { calls.push('control:start') },
stop: async () => { calls.push('control:stop') },
status: () => ({ components: [{ id: 'gateway' }] }),
},
environment: environmentRunner,
validateDeployment: async () => { calls.push('validate') },
prepareDeployment: async () => { calls.push('prepare') },
onEnvironmentVerified: async () => { calls.push('environment:verified') },
onDshRecovered: async () => { calls.push('recovery:published') },
onDshAvailable: async () => { calls.push('dsh:available') },
recoveryDelaysMs: [0, 2_000, 5_000],
sleep: async milliseconds => { calls.push(`delay:${String(milliseconds)}`) },
})
await runtime.start()
calls.length = 0
emitEnvironmentFatal(Object.assign(new Error('dsh-runtime exited unexpectedly'), { componentId: 'dsh-runtime' }))
await runtime.recovery
assert.deepEqual(calls, [
'delay:0', 'validate', 'prepare', 'environment:restart:dsh-runtime',
'environment:verified', 'recovery:published', 'dsh:available',
])
assert.deepEqual(runtime.status().dshLifecycle, {
state: 'running', action: null, taskId: null, attempt: 0, maxAttempts: 3,
error: null, updatedAt: runtime.status().dshLifecycle.updatedAt,
})
assert.equal(runtime.status().recoveryMode, null)
await runtime.stop()
})
test('requires exactly three bounded DSH recovery delays', () => {
const runner = {
fatal: new Promise(() => {}), onFatal: () => () => {},
start: async () => {}, stop: async () => {}, status: () => ({ components: [] }),
}
assert.throws(() => new BootstrapRuntime({
controlPlane: runner, environment: runner, recoveryDelaysMs: [0, 2_000],
}), /three non-negative/)
assert.throws(() => new BootstrapRuntime({
controlPlane: runner, environment: runner, recoveryDelaysMs: [0, -1, 5_000],
}), /three non-negative/)
})
test('replaces a SIGKILLed DSH process while preserving the Control Plane', async () => {
const temp = await mkdtemp(join(tmpdir(), 'dsh-runtime-sigkill-recovery-'))
const service = join(temp, 'service.mjs')
await writeFile(service, 'setInterval(() => {}, 1000)')
const environmentRunner = new EnvironmentRunner({
environmentRoot: await environment([component('dsh-runtime', service, 'service')]),
capture: () => {},
})
let controlStarts = 0
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}),
start: async () => { controlStarts += 1 },
stop: async () => {},
status: () => ({ components: [{ id: 'gateway', pid: 100 }] }),
},
environment: environmentRunner,
recoveryDelaysMs: [0, 0, 0],
})
await runtime.start()
runtime.markStartupComplete()
const firstPid = runtime.status().components.find(value => value.id === 'dsh-runtime').pid
process.kill(firstPid, 'SIGKILL')
let secondPid = firstPid
for (let attempt = 0; attempt < 100; attempt += 1) {
secondPid = runtime.status().components.find(value => value.id === 'dsh-runtime')?.pid
if (runtime.status().dshLifecycle.state === 'running' && secondPid !== undefined && secondPid !== firstPid) break
await new Promise(resolve => setTimeout(resolve, 10))
}
assert.notEqual(secondPid, firstPid)
assert.equal(runtime.status().dshLifecycle.state, 'running')
assert.equal(runtime.status().recoveryMode, null)
assert.equal(controlStarts, 1)
await runtime.stop()
})
test('does not restart DSH again when recovered-status publication fails', async () => {
let emitEnvironmentFatal
let restarts = 0
let publications = 0
const reports = []
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}), start: async () => {}, stop: async () => {},
status: () => ({ components: [{ id: 'gateway' }] }),
},
environment: {
fatal: new Promise(() => {}),
onFatal: listener => { emitEnvironmentFatal = listener; return () => {} },
start: async () => {}, stop: async () => {},
restart: async () => { restarts += 1 },
status: () => ({ environmentVersion: '1.0.3', components: [{ id: 'dsh-runtime' }] }),
},
onDshRecovered: async () => { throw new Error('status unavailable') },
onDshAvailable: async () => { publications += 1 },
recoveryDelaysMs: [0, 0, 0],
report: (message, fields) => { reports.push({ message, fields }) },
})
await runtime.start()
emitEnvironmentFatal(Object.assign(new Error('dsh-runtime exited'), { componentId: 'dsh-runtime' }))
await runtime.recovery
assert.equal(restarts, 1)
assert.equal(publications, 0)
assert.equal(runtime.status().dshLifecycle.state, 'running')
assert.equal(reports.some(entry => entry.message === 'dsh.recovery-status.failed'), true)
await runtime.stop()
})
test('limits unexpected DSH recovery to three attempts before recovery mode', async () => {
const delays = []
const reports = []
let emitEnvironmentFatal
let restarts = 0
let stopped = 0
let isolated = 0
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}), start: async () => {}, stop: async () => {},
status: () => ({ components: [{ id: 'gateway' }] }),
},
environment: {
fatal: new Promise(() => {}),
onFatal: listener => { emitEnvironmentFatal = listener; return () => {} },
start: async () => {},
stop: async () => { stopped += 1 },
restart: async () => { restarts += 1; throw new Error(`restart ${String(restarts)} failed`) },
status: () => ({ environmentVersion: '1.0.3', components: [] }),
},
recoveryDelaysMs: [0, 2_000, 5_000],
sleep: async milliseconds => { delays.push(milliseconds) },
onEnvironmentFatal: async () => { isolated += 1 },
report: (message, fields) => { reports.push({ message, fields }) },
})
await runtime.start()
emitEnvironmentFatal(Object.assign(new Error('DSH repeatedly crashed'), { componentId: 'dsh-runtime' }))
await runtime.recovery
assert.deepEqual(delays, [0, 2_000, 5_000])
assert.equal(restarts, 3)
assert.equal(isolated, 1)
assert.equal(stopped, 1)
assert.equal(runtime.status().dshLifecycle.state, 'failed')
assert.equal(runtime.status().dshLifecycle.attempt, 3)
assert.equal(runtime.status().recoveryMode, 'DSH repeatedly crashed')
assert.deepEqual(reports.filter(entry => entry.message === 'dsh.recovery.attempt.failed')
.map(entry => entry.fields.attempt), [1, 2, 3])
await runtime.stop()
})
test('defers unexpected DSH recovery while a Deployment transaction owns lifecycle', async () => {
let emitEnvironmentFatal
let restarts = 0
let stops = 0
const reports = []
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}), start: async () => {}, stop: async () => {},
status: () => ({ components: [{ id: 'gateway' }] }),
},
environment: {
fatal: new Promise(() => {}),
onFatal: listener => { emitEnvironmentFatal = listener; return () => {} },
start: async () => {},
stop: async () => { stops += 1 },
restart: async () => { restarts += 1 },
status: () => ({ environmentVersion: '1.0.3', components: [] }),
},
ownsDshLifecycle: async () => true,
report: (message, fields) => { reports.push({ message, fields }) },
})
await runtime.start()
emitEnvironmentFatal(Object.assign(new Error('candidate exited'), { componentId: 'dsh-runtime' }))
await runtime.recovery
assert.equal(restarts, 0)
assert.equal(stops, 0)
assert.equal(runtime.status().dshLifecycle.state, 'recovering')
assert.equal(runtime.status().dshLifecycle.attempt, 0)
assert.equal(reports.some(entry => entry.message === 'dsh.recovery.deferred'), true)
await runtime.stop()
})
test('enters recovery mode when an explicit DSH resume or restart fails', async () => {
const environment = {
fatal: new Promise(() => {}),
onFatal: () => () => {},
start: async () => {},
stop: async () => {},
resume: async () => { throw new Error('DSH resume failed') },
restart: async () => { throw new Error('DSH restart failed') },
status: () => ({ environmentVersion: '1.0.0', components: [] }),
}
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}),
start: async () => {},
stop: async () => {},
status: () => ({ components: [{ id: 'platform-management' }] }),
},
environment,
})
await runtime.start()
await assert.rejects(runtime.resume('dsh-runtime'), /DSH resume failed/)
assert.equal(runtime.status().recoveryMode, 'DSH resume failed')
await assert.rejects(runtime.restart('dsh-runtime'), /DSH restart failed/)
assert.equal(runtime.status().recoveryMode, 'DSH restart failed')
await runtime.stop()
})
test('reports secondary failures while isolating an unexpected Environment exit', async () => {
let emitEnvironmentFatal
const reports = []
const runtime = new BootstrapRuntime({
controlPlane: {
fatal: new Promise(() => {}),
start: async () => {},
stop: async () => {},
status: () => ({ components: [] }),
},
environment: {
fatal: new Promise(() => {}),
onFatal: listener => { emitEnvironmentFatal = listener; return () => {} },
start: async () => {},
stop: async () => { throw new Error('environment cleanup failed') },
status: () => ({ environmentVersion: 'env-1', components: [] }),
},
onEnvironmentFatal: async () => { throw new Error('status publication failed') },
report: (message, fields) => { reports.push({ message, fields }) },
})
await runtime.start()
emitEnvironmentFatal(new Error('dsh-runtime crashed'))
await runtime.recovery
assert.equal(reports.some(entry => entry.message === 'environment.recovery-report.failed'
&& entry.fields.error.message === 'status publication failed'), true)
assert.equal(reports.some(entry => entry.message === 'environment.recovery-stop.failed'
&& entry.fields.error.message === 'environment cleanup failed'), true)
})
test('validates mandatory Patches before every operation that starts DSH', async () => {
const calls = []
const service = name => ({
fatal: new Promise(() => {}),
start: async () => { calls.push(`${name}:start`) },
stop: async () => { calls.push(`${name}:stop`) },
reload: async () => { calls.push(`${name}:reload`) },
suspend: async id => { calls.push(`${name}:suspend:${id}`) },
resume: async id => { calls.push(`${name}:resume:${id}`) },
restart: async id => { calls.push(`${name}:restart:${id}`) },
health: async () => ({ healthy: true, components: [] }),
status: () => ({ components: [] }),
})
let validations = 0
const runtime = new BootstrapRuntime({
controlPlane: service('control'),
environment: service('environment'),
validateDeployment: async () => {
validations += 1
calls.push('validate')
if (validations === 1) throw new Error('Patch verification failed')
},
})
await runtime.start({ onEnvironmentFailure: async () => true })
await runtime.reload()
await runtime.resume('dsh-runtime')
await runtime.restart('dsh-runtime')
await runtime.resume('gateway')
assert.deepEqual(calls, [
'control:start',
'validate',
'validate',
'environment:start',
'validate',
'environment:reload',
'validate',
'environment:resume:dsh-runtime',
'validate',
'environment:restart:dsh-runtime',
'environment:resume:gateway',
])
})
test('retries a previous Deployment without restarting the Control Plane', async () => {
const calls = []
let environmentStarts = 0
const controlPlane = {
fatal: new Promise(() => {}),
start: async () => { calls.push('control:start') },
stop: async () => { calls.push('control:stop') },
status: () => ({ components: [] }),
}
const environmentRunner = {
fatal: new Promise(() => {}),
start: async () => {
environmentStarts += 1
calls.push(`environment:start:${String(environmentStarts)}`)
if (environmentStarts === 1) throw new Error('candidate failed')
},
stop: async () => { calls.push('environment:stop') },
status: () => ({ environmentVersion: 'test', components: [] }),
}
const runtime = new BootstrapRuntime({ controlPlane, environment: environmentRunner })
await runtime.start({
onEnvironmentFailure: async error => {
assert.match(error.message, /candidate failed/)
calls.push('deployment:restore')
return true
},
})
assert.deepEqual(calls, [
'control:start',
'environment:start:1',
'deployment:restore',
'environment:start:2',
])
})
test('keeps DSH stopped when mandatory Patch verification fails for current and previous', async () => {
let validations = 0
let environmentStarts = 0
const controlPlane = {
fatal: new Promise(() => {}),
start: async () => {},
stop: async () => {},
status: () => ({ components: [{ id: 'gateway' }] }),
}
const environmentRunner = {
fatal: new Promise(() => {}),
start: async () => { environmentStarts += 1 },
stop: async () => {},
status: () => ({ environmentVersion: null, components: [] }),
}
const runtime = new BootstrapRuntime({
controlPlane,
environment: environmentRunner,
validateDeployment: async () => {
validations += 1
throw new Error(`Patch verification failed for ${validations === 1 ? 'current' : 'previous'}`)
},
})
await runtime.start({
allowRecovery: true,
onEnvironmentFailure: async error => {
assert.match(error.message, /current/)
return true
},
})
assert.equal(validations, 2)
assert.equal(environmentStarts, 0)
assert.equal(runtime.status().recoveryMode, 'Deployment candidate and fallback both failed')
assert.deepEqual(runtime.status().controlPlane, [{ id: 'gateway' }])
})
test('keeps the Control Plane alive in recovery mode when no Deployment starts', async () => {
const calls = []
const controlPlane = {
fatal: new Promise(() => {}),
start: async () => { calls.push('control:start') },
stop: async () => { calls.push('control:stop') },
status: () => ({ components: [{ id: 'gateway' }] }),
}
const environmentRunner = {
fatal: new Promise(() => {}),
start: async () => { throw new Error('Runtime missing') },
stop: async () => {},
status: () => ({ environmentVersion: null, components: [] }),
}
const runtime = new BootstrapRuntime({ controlPlane, environment: environmentRunner })
await runtime.start({ allowRecovery: true, onEnvironmentFailure: async () => false })
assert.equal(runtime.status().recoveryMode, 'Runtime missing')
assert.deepEqual(runtime.status().controlPlane, [{ id: 'gateway' }])
assert.deepEqual(calls, ['control:start'])
})
test('enters recovery mode when a failed candidate fallback cannot resolve', async () => {
const calls = []
const controlPlane = {
fatal: new Promise(() => {}),
start: async () => { calls.push('control:start') },
stop: async () => { calls.push('control:stop') },
status: () => ({ components: [{ id: 'gateway' }] }),
}
const environmentRunner = {
fatal: new Promise(() => {}),
start: async () => { throw new Error('candidate unhealthy') },
stop: async () => {},
status: () => ({ environmentVersion: null, components: [] }),
}
const runtime = new BootstrapRuntime({ controlPlane, environment: environmentRunner })
await runtime.start({
allowRecovery: true,
onEnvironmentFailure: async () => { throw new Error('previous image is unavailable') },
})
assert.equal(runtime.status().recoveryMode, 'Deployment failed and its fallback could not be resolved')
assert.deepEqual(calls, ['control:start'])
})