const assert = require('node:assert/strict'); const crypto = require('node:crypto'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { test } = require('node:test'); const { consumeLocalServiceManagerCutoverOutcome, } = require('../dist/deployment/service-manager/serviceCutoverConsumer.js'); const { prepareLocalServiceManagerIntent, } = require('../dist/deployment/service-manager/serviceManagerIntent.js'); const { localServiceManagerObservationDigest, localServiceManagerOutcomeDigest, } = require('../dist/deployment/service-manager/serviceOutcomeContract.js'); const { advanceLocalCutoverInstanceHead, claimLocalCutoverInstance, readLocalCutoverInstanceHead, } = require('../dist/deployment/cutover/instanceLineage.js'); const { cutoverDigest, } = require('../dist/deployment/cutover/targetEvidence.js'); function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); } function writePrivate(filePath, value) { fs.writeFileSync( filePath, typeof value === 'string' ? value : `${JSON.stringify(value)}\n`, { mode: 0o600 }, ); } function fixture(t) { const root = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-service-cutover-')), ); t.after(() => fs.rmSync(root, { recursive: true, force: true })); fs.chmodSync(root, 0o700); const service = path.join(root, 'service'); const cutoverId = 'cutover-edge-router-1'; const journal = path.join(service, 'cutovers', cutoverId); fs.mkdirSync(journal, { recursive: true, mode: 0o700 }); fs.chmodSync(service, 0o700); fs.chmodSync(path.join(service, 'cutovers'), 0o700); fs.chmodSync(journal, 0o700); const sourcePath = path.join(root, 'legacy.sqlite'); const targetPath = path.join(root, 'target.sqlite'); const recoveryPath = path.join(root, 'recovery.sqlite'); const manifestPath = path.join(root, 'manifest.json'); const activationPath = path.join(root, 'activation.json'); for (const [filePath, contents] of [ [sourcePath, 'legacy\n'], [targetPath, 'target\n'], [recoveryPath, 'legacy\n'], ]) { writePrivate(filePath, contents); } const manifestPayload = { schemaVersion: 1, kind: 'qinglong3-local-adoption-manifest-fixture', }; const manifestDigest = cutoverDigest(manifestPayload); writePrivate(manifestPath, { ...manifestPayload, manifestDigest }); const target = fs.statSync(targetPath, { bigint: true }); const activationPayload = { schemaVersion: 1, kind: 'qinglong3-local-sqlite-activation', state: 'prepared', profile: 'edge', createdAtMs: 1786416000000, adoptionManifestDigest: manifestDigest, planDigest: '2'.repeat(64), sourcePathDigest: sha256(sourcePath), recoverySha256: sha256(fs.readFileSync(recoveryPath)), targetSha256: sha256(fs.readFileSync(targetPath)), targetPathDigest: sha256(targetPath), targetDevice: target.dev.toString(), targetInode: target.ino.toString(), }; const activationDigest = cutoverDigest(activationPayload); writePrivate(activationPath, { ...activationPayload, activationDigest }); const commitmentPayload = { schemaVersion: 1, kind: 'qinglong3-local-legacy-silence-commitment', state: 'legacy_stopped', cutoverId, profile: 'edge', instanceId: 'edge-router-1', activationDigest, requestedAtMs: 1786416000010, observedAtMs: 1786416000020, previousRecordDigest: '3'.repeat(64), controller: { kind: 'docker', endpointDigest: '4'.repeat(64), legacyContainerId: '5'.repeat(64), legacyContainerIdentityDigest: '6'.repeat(64), legacySourceBindingDigest: '7'.repeat(64), }, }; const commitmentDigest = cutoverDigest(commitmentPayload); const commitmentPath = path.join(journal, '0002-legacy-stopped.json'); writePrivate(commitmentPath, { ...commitmentPayload, commitmentDigest, }); const applicationPath = path.join(root, 'local-application.json'); writePrivate(applicationPath, { schema: 'qinglong/local-application-process@v3', instanceId: 'edge-router-1', profile: 'edge', storage: { mode: 'adopted', sourcePath, targetPath, recoveryPath, manifestPath, activationPath, expectedActivationDigest: activationDigest, }, runtime: {}, pluginPackages: {}, ai: { deployment: 'excluded' }, cutover: { cutoverId, commitmentPath, expectedCommitmentDigest: commitmentDigest, }, }); writePrivate( path.join(service, 'qinglong3.service'), '[Service]\nExecStart=/usr/bin/node /opt/qinglong3/app.js\n', ); const identity = { options: { deploymentRoot: root }, request: { cutoverId, profile: 'edge', instanceId: 'edge-router-1', expectedActivationDigest: activationDigest, requestedAtMs: 1786416000030, }, }; claimLocalCutoverInstance(identity, process.getuid(), '8'.repeat(64)); advanceLocalCutoverInstanceHead( identity, process.getuid(), 'legacy_stopped', 0, commitmentDigest, ); const procRoot = path.join(root, 'proc'); fs.mkdirSync(procRoot, { mode: 0o700 }); return { root, service, cutoverId, activationDigest, commitmentDigest, applicationPath, sourcePath, identity, procRoot, }; } function prepare(state, generation, action, previousRecordDigest, actionId) { return prepareLocalServiceManagerIntent({ schemaVersion: 1, operation: 'local.deployment.service-manager.intent.prepare', options: { deploymentRoot: state.root, allowRootService: process.getuid() === 0, }, request: { actionId, action, serviceKind: 'systemd', lineage: { mode: 'adopted', cutoverId: state.cutoverId, generation, expectedActivationDigest: state.activationDigest, previousRecordDigest, }, requestedAtMs: 1786416000100 + generation, }, }); } function publishOutcome(prepared, action, state, mainPid, completedAtMs) { const intent = JSON.parse(fs.readFileSync(prepared.intentPath, 'utf8')); const observationPayload = { managerKind: 'systemd', serviceName: 'qinglong3', fragmentPath: '/etc/systemd/system/qinglong3.service', loadState: 'loaded', activeState: state === 'active' ? 'active' : 'inactive', subState: state === 'active' ? 'running' : 'dead', enabledState: 'enabled', mainPid, observedAtMs: completedAtMs - 1, }; const observation = { ...observationPayload, observationDigest: localServiceManagerObservationDigest(observationPayload), }; const payload = { schemaVersion: 1, kind: 'qinglong3-local-service-manager-outcome', actionId: prepared.actionId, action, intentDigest: prepared.intentDigest, descriptorDigest: intent.descriptor.sha256, state, mutationDisposition: 'executed', manualReason: null, observation, completedAtMs, }; writePrivate(prepared.outcomePath, { ...payload, outcomeDigest: localServiceManagerOutcomeDigest(payload), }); } function publishReceipt(state, processId, startTicks) { const executable = fs.realpathSync(process.execPath); const payload = { schemaVersion: 1, schema: 'qinglong/local-application-startup-receipt@v1', instanceId: 'edge-router-1', profile: 'edge', aiStatus: 'deployment_excluded', bootId: '00000000-0000-4000-8000-000000000001', activeBootAgeMs: 1000, processId, processStartTicks: startTicks, nodeExecutable: executable, nodeVersion: 'v24.18.0', }; const digest = crypto .createHash('sha256') .update('qinglong.local-application-startup-receipt.v1\0', 'utf8') .update(JSON.stringify(payload), 'utf8') .digest('hex'); writePrivate(`${state.applicationPath}.active.json`, { ...payload, sha256: digest, }); const processRoot = path.join(state.procRoot, String(processId)); fs.mkdirSync(processRoot, { mode: 0o700 }); const fields = ['S', ...Array(18).fill('0'), startTicks, '0']; fs.writeFileSync( path.join(processRoot, 'stat'), `${processId} (node) ${fields.join(' ')}\n`, ); fs.symlinkSync(executable, path.join(processRoot, 'exe')); return digest; } function publishShutdownReceipt( state, processId, startTicks, startupReceiptDigest, ) { const payload = { schemaVersion: 1, schema: 'qinglong/local-application-shutdown-receipt@v1', instanceId: 'edge-router-1', profile: 'edge', signal: 'SIGTERM', stopResult: 'stopped', startupReceiptDigest, bootId: '00000000-0000-4000-8000-000000000001', stoppedBootAgeMs: 2000, processId, processStartTicks: startTicks, nodeExecutable: fs.realpathSync(process.execPath), nodeVersion: 'v24.18.0', }; const digest = crypto .createHash('sha256') .update('qinglong.local-application-shutdown-receipt.v1\0', 'utf8') .update(JSON.stringify(payload), 'utf8') .digest('hex'); writePrivate(`${state.applicationPath}.stopped.json`, { ...payload, sha256: digest, }); return digest; } function consumeCommand(state, prepared) { return { schemaVersion: 1, operation: 'local.deployment.service-manager.cutover.consume', options: { deploymentRoot: state.root, allowRootService: process.getuid() === 0, startupTimeoutMs: 100, startupPollMs: 10, }, request: { actionId: prepared.actionId, expectedIntentDigest: prepared.intentDigest, }, }; } test('commits adopted service active evidence and replays from the instance head', async (t) => { const state = fixture(t); const prepared = prepare( state, 1, 'install-enable-start', state.commitmentDigest, '123e4567-e89b-42d3-a456-426614174021', ); publishOutcome( prepared, 'install-enable-start', 'active', 4123, 1786416000200, ); const receiptDigest = publishReceipt(state, 4123, '100001'); const command = consumeCommand(state, prepared); const result = await consumeLocalServiceManagerCutoverOutcome(command, { procRoot: state.procRoot, }); assert.equal(result.status, 'prepared'); assert.equal(result.state, 'target_active'); const head = readLocalCutoverInstanceHead( state.root, 'edge-router-1', process.getuid(), ); assert.equal(head.state, 'target_active'); assert.equal(head.sourceRecordDigest, result.recordDigest); const recordPath = path.join( state.root, 'service', 'cutovers', state.cutoverId, 'service-manager-g01-active.json', ); const record = JSON.parse(fs.readFileSync(recordPath, 'utf8')); assert.equal(record.evidence.startupReceiptDigest, receiptDigest); assert.match(record.evidence.processIdentityDigest, /^[0-9a-f]{64}$/); assert.equal( ( await consumeLocalServiceManagerCutoverOutcome(command, { procRoot: state.procRoot, }) ).status, 'existing', ); }); test('restart cannot reuse the previous generation startup receipt', async (t) => { const state = fixture(t); const first = prepare( state, 1, 'install-enable-start', state.commitmentDigest, '123e4567-e89b-42d3-a456-426614174022', ); publishOutcome(first, 'install-enable-start', 'active', 4223, 1786416000200); publishReceipt(state, 4223, '100002'); const firstResult = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, first), { procRoot: state.procRoot }, ); const restart = prepare( state, 2, 'restart', firstResult.recordDigest, '123e4567-e89b-42d3-a456-426614174023', ); publishOutcome(restart, 'restart', 'active', 5223, 1786416000300); let clock = 0; const result = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, restart), { procRoot: state.procRoot, now: () => clock, wait: async (milliseconds) => { clock += milliseconds; }, }, ); assert.equal(result.state, 'manual_required'); const head = readLocalCutoverInstanceHead( state.root, 'edge-router-1', process.getuid(), ); assert.equal(head.state, 'manual_required'); }); test('stop advances only after the exact receipted process identity disappears', async (t) => { const state = fixture(t); const first = prepare( state, 1, 'install-enable-start', state.commitmentDigest, '123e4567-e89b-42d3-a456-426614174024', ); publishOutcome(first, 'install-enable-start', 'active', 4323, 1786416000200); const startupReceiptDigest = publishReceipt(state, 4323, '100003'); const firstResult = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, first), { procRoot: state.procRoot }, ); const stopped = prepare( state, 1, 'stop', firstResult.recordDigest, '123e4567-e89b-42d3-a456-426614174025', ); publishOutcome(stopped, 'stop', 'stopped', 0, 1786416000300); const shutdownReceiptDigest = publishShutdownReceipt( state, 4323, '100003', startupReceiptDigest, ); fs.rmSync(path.join(state.procRoot, '4323'), { recursive: true, force: true, }); const result = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, stopped), { procRoot: state.procRoot }, ); assert.equal(result.state, 'target_stopped'); const head = readLocalCutoverInstanceHead( state.root, 'edge-router-1', process.getuid(), ); assert.equal(head.state, 'target_stopped'); assert.equal(head.sourceRecordDigest, result.recordDigest); const record = JSON.parse( fs.readFileSync( path.join( state.root, 'service', 'cutovers', state.cutoverId, 'service-manager-g01-stopped.json', ), 'utf8', ), ); assert.equal(record.evidence.shutdownReceiptDigest, shutdownReceiptDigest); }); test('stop without an exact shutdown receipt requires manual resolution', async (t) => { const state = fixture(t); const first = prepare( state, 1, 'install-enable-start', state.commitmentDigest, '123e4567-e89b-42d3-a456-426614174028', ); publishOutcome(first, 'install-enable-start', 'active', 4623, 1786416000200); publishReceipt(state, 4623, '100006'); const firstResult = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, first), { procRoot: state.procRoot }, ); const stopped = prepare( state, 1, 'stop', firstResult.recordDigest, '123e4567-e89b-42d3-a456-426614174029', ); publishOutcome(stopped, 'stop', 'stopped', 0, 1786416000300); fs.rmSync(path.join(state.procRoot, '4623'), { recursive: true, force: true, }); const result = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, stopped), { procRoot: state.procRoot }, ); assert.equal(result.state, 'manual_required'); }); test('rejects legacy source content drift before committing service active', async (t) => { const state = fixture(t); const prepared = prepare( state, 1, 'install-enable-start', state.commitmentDigest, '123e4567-e89b-42d3-a456-426614174026', ); publishOutcome( prepared, 'install-enable-start', 'active', 4423, 1786416000200, ); publishReceipt(state, 4423, '100004'); fs.writeFileSync(state.sourcePath, 'legacy-drifted\n', { mode: 0o600 }); await assert.rejects( consumeLocalServiceManagerCutoverOutcome(consumeCommand(state, prepared), { procRoot: state.procRoot, }), /adopted data evidence drifted/, ); const head = readLocalCutoverInstanceHead( state.root, 'edge-router-1', process.getuid(), ); assert.equal(head.state, 'legacy_stopped'); }); test('terminalizes a manager PID replaced before Owner receipt verification', async (t) => { const state = fixture(t); const prepared = prepare( state, 1, 'install-enable-start', state.commitmentDigest, '123e4567-e89b-42d3-a456-426614174027', ); publishOutcome( prepared, 'install-enable-start', 'active', 4523, 1786416000200, ); publishReceipt(state, 4524, '100005'); let clock = 0; const result = await consumeLocalServiceManagerCutoverOutcome( consumeCommand(state, prepared), { procRoot: state.procRoot, now: () => clock, wait: async (milliseconds) => { clock += milliseconds; }, }, ); assert.equal(result.state, 'manual_required'); const head = readLocalCutoverInstanceHead( state.root, 'edge-router-1', process.getuid(), ); assert.equal(head.state, 'manual_required'); });