Files
dsh/upstream/container/platform/test/managed-lifecycle-patch.test.mjs
T

373 lines
19 KiB
JavaScript

import assert from 'node:assert/strict'
import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import test from 'node:test'
import {
applyPatch,
MANAGED_LIFECYCLE_MODULE,
PROFILE_PACKAGE_STORAGE_MODULE,
verifyPatch,
} from '../../environment/resources/patches/managed-lifecycle.mjs'
async function fixture({ duplicateImport = false, sigterm = true, wrapper = true } = {}) {
const root = await mkdtemp(join(tmpdir(), 'dsh-managed-lifecycle-patch-'))
await mkdir(join(root, 'lib'))
await writeFile(join(root, 'package.json'), '{"type":"module"}\n')
const dynamicImport = 'const { runProfile } = await import("./profile-boot-wrapper_42.js");'
await writeFile(join(root, 'lib/bin.js'), `#!/usr/bin/env node
const invocation = parseDshArgs(process.argv.slice(2), readVersion());
switch (invocation.mode) {
case "profile": {
${dynamicImport}
${duplicateImport ? dynamicImport : ''}
}
}
`)
await writeFile(join(root, 'lib/profile-boot-wrapper_42.js'), wrapper
? 'import { x as runProfile } from "./profile-boot-anyHash.js";\nexport { runProfile };\n'
: 'export const unrelated = true;\n')
await writeFile(join(root, 'lib/profile-boot-anyHash.js'), `async function runProfile(options) {
${sigterm ? '\tprocess.on("SIGTERM", () => {\n\t\tinterrupt(0);\n\t});' : '\tprocess.on("SIGHUP", () => interrupt(0));'}
\tprocess.on("SIGINT", () => { interrupt(130); });
\tconst ctx = await boot(NAME, rootConfig, patches, (hostCtx) => {
\t\thostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment);
\t});
\treturn {
\t\tctx,
\t\tshutdown
\t};
}
export { runProfile as x };
`)
return root
}
async function listen(socketPath, handler) {
const server = createServer(handler)
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(socketPath, resolve)
})
await chmod(socketPath, 0o600)
return server
}
function json(response, status, value) {
response.writeHead(status, { 'content-type': 'application/json' })
response.end(JSON.stringify(value))
}
test('patches a hash-independent Profile import chain exactly once', async () => {
const root = await fixture()
applyPatch(root)
verifyPatch(root)
assert.match(await readFile(join(root, 'lib/bin.js'), 'utf8'), /prepareManagedInvocation/)
assert.match(await readFile(join(root, 'lib/profile-boot-anyHash.js'), 'utf8'), /managedSigtermHandler/)
assert.match(await readFile(join(root, MANAGED_LIFECYCLE_MODULE), 'utf8'), /dshPlatformLifecycle/)
assert.match(await readFile(join(root, PROFILE_PACKAGE_STORAGE_MODULE), 'utf8'), /prepareProfilePackageStorage/)
assert.throws(() => applyPatch(root), /already applied/)
})
test('fails closed when upstream Profile anchors are missing or ambiguous', async () => {
await assert.rejects(Promise.resolve().then(async () => applyPatch(await fixture({ duplicateImport: true }))), /found 2/)
await assert.rejects(Promise.resolve().then(async () => applyPatch(await fixture({ wrapper: false }))), /found 0/)
await assert.rejects(Promise.resolve().then(async () => applyPatch(await fixture({ sigterm: false }))), /SIGTERM handler.*found 0/)
})
async function fakePnpm(root) {
const bin = join(root, 'bin')
await mkdir(bin, { recursive: true })
const executable = join(bin, 'pnpm')
await writeFile(executable, `#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
if (process.env.DSH_TEST_PNPM_FAIL === '1') {
process.stderr.write('fixture pnpm failed\\n')
process.exit(1)
}
const workspace = await import('node:fs').then(({ readFileSync }) => readFileSync('pnpm-workspace.yaml', 'utf8'))
const match = /^storeDir:\\s*(.+)$/m.exec(workspace)
if (match === null) process.exit(2)
const storeRoot = JSON.parse(match[1])
mkdirSync('node_modules', { recursive: true })
writeFileSync(join('node_modules', '.modules.yaml'), JSON.stringify({ storeDir: join(storeRoot, 'v11') }))
writeFileSync(join('node_modules', 'migration-marker'), 'rebuilt\\n')
`)
await chmod(executable, 0o755)
return bin
}
test('pins compatible Profiles to their custom DSH_HOME store', async () => {
const root = await fixture()
applyPatch(root)
const storage = await import(`${pathToFileURL(join(root, PROFILE_PACKAGE_STORAGE_MODULE)).href}?storage`)
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-profile-storage-home-'))
const profileDir = join(dshHome, 'profiles', 'web')
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
const modulesRoot = join(profileDir, 'node_modules')
await mkdir(modulesRoot, { recursive: true })
await writeFile(join(profileDir, 'package.json'), '{"name":"dsh-profile-web"}\n')
await writeFile(workspacePath, 'packages:\n - .\n\nnodeLinker: hoisted\n')
const previousHome = process.env.DSH_HOME
process.env.DSH_HOME = dshHome
try {
assert.deepEqual(storage.prepareProfilePackageStorage('web'), {
status: 'ready',
currentStore: null,
storeRoot: join(dshHome, '.pnpm-store'),
})
assert.match(await readFile(workspacePath, 'utf8'), new RegExp(
`storeDir: ${JSON.stringify(join(dshHome, '.pnpm-store')).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
))
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({
storeDir: join(dshHome, '.pnpm-store', 'v11'),
}))
assert.equal(storage.prepareProfilePackageStorage('web').status, 'ready')
assert.equal((await readFile(workspacePath, 'utf8')).match(/^storeDir:/gm)?.length, 1)
const ambiguous = `${await readFile(workspacePath, 'utf8')}storeDir: "/another/store"\n`
await writeFile(workspacePath, ambiguous)
assert.throws(() => storage.prepareProfilePackageStorage('web'), /multiple storeDir entries/)
assert.equal(await readFile(workspacePath, 'utf8'), ambiguous)
} finally {
if (previousHome === undefined) delete process.env.DSH_HOME
else process.env.DSH_HOME = previousHome
}
})
test('migrates a legacy Profile store and recovers a failed or interrupted rebuild', async () => {
const root = await fixture()
applyPatch(root)
const storage = await import(`${pathToFileURL(join(root, PROFILE_PACKAGE_STORAGE_MODULE)).href}?migration`)
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-profile-migration-home-'))
const profileDir = join(dshHome, 'profiles', 'web')
const modulesRoot = join(profileDir, 'node_modules')
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
const legacyStore = join(root, 'workspace', '.pnpm-store', 'v11')
const originalWorkspace = `packages:\n - .\n\nstoreDir: ${JSON.stringify(dirname(legacyStore))}\n`
await mkdir(modulesRoot, { recursive: true })
await mkdir(legacyStore, { recursive: true })
await writeFile(join(legacyStore, 'store-marker'), 'cached\n')
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({ storeDir: legacyStore }))
await writeFile(join(modulesRoot, 'original-marker'), 'original\n')
await writeFile(join(profileDir, 'package.json'), '{"name":"dsh-profile-web","private":true}\n')
await writeFile(join(profileDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
await writeFile(workspacePath, originalWorkspace)
const bin = await fakePnpm(root)
const previous = {
DSH_HOME: process.env.DSH_HOME,
PATH: process.env.PATH,
DSH_TEST_PNPM_FAIL: process.env.DSH_TEST_PNPM_FAIL,
}
process.env.DSH_HOME = dshHome
process.env.PATH = `${bin}:${process.env.PATH}`
try {
assert.deepEqual(storage.prepareProfilePackageStorage('web'), {
status: 'migrated',
currentStore: join(dshHome, '.pnpm-store', 'v11'),
storeRoot: join(dshHome, '.pnpm-store'),
})
assert.equal(await readFile(join(dshHome, '.pnpm-store', 'v11', 'store-marker'), 'utf8'), 'cached\n')
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
await assert.rejects(readFile(join(profileDir, '.dsh-platform-node_modules.previous')), { code: 'ENOENT' })
await assert.rejects(readFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous')), { code: 'ENOENT' })
await rm(modulesRoot, { recursive: true })
await mkdir(modulesRoot)
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({ storeDir: legacyStore }))
await writeFile(join(modulesRoot, 'original-marker'), 'restored\n')
await writeFile(workspacePath, originalWorkspace)
process.env.DSH_TEST_PNPM_FAIL = '1'
assert.throws(() => storage.prepareProfilePackageStorage('web'), /fixture pnpm failed/)
assert.equal(await readFile(join(modulesRoot, 'original-marker'), 'utf8'), 'restored\n')
assert.equal(await readFile(workspacePath, 'utf8'), originalWorkspace)
delete process.env.DSH_TEST_PNPM_FAIL
await writeFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous'), originalWorkspace)
await writeFile(workspacePath, `packages:\n - .\n\nstoreDir: ${JSON.stringify(join(dshHome, '.pnpm-store'))}\n`)
assert.equal(storage.prepareProfilePackageStorage('web').status, 'migrated')
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
await rm(modulesRoot, { recursive: true })
await mkdir(modulesRoot)
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({ storeDir: legacyStore }))
await writeFile(join(modulesRoot, 'original-marker'), 'interrupted\n')
await writeFile(workspacePath, originalWorkspace)
await writeFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous'), originalWorkspace)
await rename(modulesRoot, join(profileDir, '.dsh-platform-node_modules.previous'))
await writeFile(workspacePath, `packages:\n - .\n\nstoreDir: ${JSON.stringify(join(dshHome, '.pnpm-store'))}\n`)
assert.equal(storage.prepareProfilePackageStorage('web').status, 'migrated')
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
await rm(modulesRoot, { recursive: true })
await mkdir(modulesRoot)
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({
storeDir: join(dshHome, '.pnpm-store', 'v11'),
}))
await writeFile(join(modulesRoot, 'partial-marker'), 'partial\n')
await writeFile(workspacePath, `packages:\n - .\n\nstoreDir: ${JSON.stringify(join(dshHome, '.pnpm-store'))}\n`)
await mkdir(join(profileDir, '.dsh-platform-node_modules.previous'))
await writeFile(join(profileDir, '.dsh-platform-node_modules.previous', '.modules.yaml'), JSON.stringify({
storeDir: legacyStore,
}))
await writeFile(join(profileDir, '.dsh-platform-node_modules.previous', 'original-marker'), 'original\n')
await writeFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous'), originalWorkspace)
assert.equal(storage.prepareProfilePackageStorage('web').status, 'migrated')
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
await assert.rejects(readFile(join(modulesRoot, 'partial-marker')), { code: 'ENOENT' })
} finally {
for (const [name, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[name]
else process.env[name] = value
}
}
})
test('gates only managed Web launches and routes restart through Management', async t => {
const root = await fixture()
applyPatch(root)
const runRoot = await mkdtemp(join(tmpdir(), 'dsh-managed-lifecycle-run-'))
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-managed-lifecycle-home-'))
const profileDir = join(dshHome, 'profiles', 'web')
await mkdir(profileDir, { recursive: true })
await mkdir(join(root, 'node_modules', 'built-in-bundle'), { recursive: true })
await writeFile(join(root, 'node_modules', 'built-in-bundle', 'package.json'), '{"name":"built-in-bundle"}\n')
const profileManifest = join(profileDir, 'package.json')
await writeFile(join(profileDir, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\n')
await writeFile(profileManifest, JSON.stringify({
dependencies: { 'missing-dependency': '1.0.0' },
dsh: { profile: { bundles: ['built-in-bundle', 'removed-plugin', '../../outside', 'missing-dependency'] } },
}, null, 2))
const calls = []
let claimStatus = 200
let disposition = 'request-restart'
let lifecycleState = 'running'
let signalDelayMs = 0
const broker = await listen(join(runRoot, 'dsh-lifecycle.sock'), async (request, response) => {
const chunks = []
for await (const chunk of request) chunks.push(chunk)
calls.push({ socket: 'broker', path: request.url, body: JSON.parse(Buffer.concat(chunks)) })
if (claimStatus !== 200) json(response, claimStatus, { error: 'launch already claimed' })
else if (request.url === '/v1/runtime/claim') json(response, 200, { sessionId: 'session-1' })
else if (request.url === '/v1/runtime/ready') json(response, 200, { ready: true })
else {
if (signalDelayMs > 0) await new Promise(resolve => setTimeout(resolve, signalDelayMs))
json(response, 200, { disposition })
}
})
const management = await listen(join(runRoot, 'management-cli.sock'), (request, response) => {
calls.push({ socket: 'management', path: request.url })
if (request.url?.endsWith('/status')) json(response, 200, { dshLifecycle: { state: lifecycleState }, recoveryMode: null })
else json(response, 202, { taskId: request.url?.endsWith('/start-dsh') ? 'start-task' : 'restart-task' })
})
t.after(() => { broker.close(); management.close() })
const previous = Object.fromEntries(['DSH_HOME', 'DSH_PLATFORM_MANAGED', 'DSH_PLATFORM_RUN', 'DSH_PLATFORM_LAUNCH_TOKEN']
.map(name => [name, process.env[name]]))
process.env.DSH_HOME = dshHome
process.env.DSH_PLATFORM_MANAGED = '1'
process.env.DSH_PLATFORM_RUN = runRoot
process.env.DSH_PLATFORM_LAUNCH_TOKEN = 'launch-token'
try {
const adapter = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?authorized`)
assert.equal(await adapter.prepareManagedInvocation({ mode: 'plugin', profile: 'web' }), null)
assert.equal(calls.length, 0)
assert.match(await readFile(join(profileDir, 'pnpm-workspace.yaml'), 'utf8'), new RegExp(
`storeDir: ${JSON.stringify(join(dshHome, '.pnpm-store')).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
))
assert.equal(await adapter.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), null)
assert.equal(process.env.DSH_PLATFORM_LAUNCH_TOKEN, undefined)
assert.deepEqual(
JSON.parse(await readFile(profileManifest, 'utf8')).dsh.profile.bundles,
['built-in-bundle', 'missing-dependency'],
)
await adapter.markManagedReady({
get: name => name === 'connection'
? { authenticatedUrl: base => `${base}/?token=fixture-token` }
: name === 'webServer' ? { port: 3079 } : undefined,
})
assert.equal(calls.some(value => value.socket === 'broker' && value.path === '/v1/runtime/ready'), true)
assert.deepEqual(calls.find(value => value.path === '/v1/runtime/ready')?.body, {
sessionId: 'session-1',
readyUrl: 'http://127.0.0.1:3079/?token=fixture-token',
})
let provided
adapter.provideManagedLifecycle({ provide: (name, value) => { provided = { name, value } } })
assert.equal(provided.name, 'dshPlatformLifecycle')
assert.deepEqual(await provided.value.restart(), { taskId: 'restart-task' })
const restartCallsBeforeSigterm = calls.filter(value => value.path?.endsWith('/restart-dsh')).length
let interrupts = 0
const onSigterm = adapter.managedSigtermHandler(() => { interrupts += 1 })
onSigterm()
for (let attempt = 0; attempt < 50 && calls.filter(value => value.path?.endsWith('/restart-dsh')).length === restartCallsBeforeSigterm; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 5))
}
assert.equal(calls.some(value => value.socket === 'broker' && value.path === '/v1/runtime/signal'), true)
assert.equal(calls.filter(value => value.path?.endsWith('/restart-dsh')).length, restartCallsBeforeSigterm + 1)
assert.equal(interrupts, 0)
onSigterm()
assert.equal(interrupts, 1)
const restartCallsBeforeCancellation = calls.filter(value => value.path?.endsWith('/restart-dsh')).length
signalDelayMs = 30
let cancellationInterrupts = 0
const cancelSigterm = adapter.managedSigtermHandler(() => { cancellationInterrupts += 1 })
cancelSigterm()
cancelSigterm()
await new Promise(resolve => setTimeout(resolve, 50))
assert.equal(cancellationInterrupts, 1)
assert.equal(calls.filter(value => value.path?.endsWith('/restart-dsh')).length, restartCallsBeforeCancellation)
signalDelayMs = 0
disposition = 'terminate'
let terminationInterrupts = 0
const managementCalls = calls.filter(value => value.path?.endsWith('/restart-dsh')).length
adapter.managedSigtermHandler(() => { terminationInterrupts += 1 })()
for (let attempt = 0; attempt < 50 && terminationInterrupts === 0; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 5))
}
assert.equal(terminationInterrupts, 1)
assert.equal(calls.filter(value => value.path?.endsWith('/restart-dsh')).length, managementCalls)
claimStatus = 409
disposition = 'request-restart'
const duplicate = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?duplicate`)
assert.equal(await duplicate.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 0)
lifecycleState = 'stopped'
const stopped = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?stopped`)
assert.equal(await stopped.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 0)
assert.equal(calls.some(value => value.path?.endsWith('/start-dsh')), true)
lifecycleState = 'failed'
const failed = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?failed`)
assert.equal(await failed.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 1)
} finally {
for (const [name, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[name]
else process.env[name] = value
}
}
})
test('keeps native behavior without the image marker and fails closed without the Broker', async () => {
const root = await fixture()
applyPatch(root)
const adapter = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?fail-closed`)
const previous = Object.fromEntries(['DSH_PLATFORM_MANAGED', 'DSH_PLATFORM_RUN']
.map(name => [name, process.env[name]]))
try {
delete process.env.DSH_PLATFORM_MANAGED
assert.equal(await adapter.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), null)
process.env.DSH_PLATFORM_MANAGED = '1'
process.env.DSH_PLATFORM_RUN = await mkdtemp(join(tmpdir(), 'dsh-no-broker-'))
assert.equal(await adapter.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 1)
} finally {
for (const [name, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[name]
else process.env[name] = value
}
}
})