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 { EMPTY_ROLLBACK_PREPARATION_DIGEST, EMPTY_RESOLUTION_DIGEST, runLocalDeploymentCutoverManualCommand, runLocalDeploymentLegacyRollback, runLocalDeploymentDockerTarget, stopLocalDeploymentDockerTarget, stopLegacyDockerForLocalDeployment, } = require('../dist/deployment/localDeployment.js'); function rootAcknowledgement() { return typeof process.getuid === 'function' && process.getuid() === 0; } function digest(value) { return crypto .createHash('sha256') .update(JSON.stringify(value), 'utf8') .digest('hex'); } function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); } function targetPath(state, hostPath) { return path.join('/host', path.relative(state.managementRoot, hostPath)); } function startupReceipt(state, processId) { 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: 10_000 + processId, processId, processStartTicks: String(100_000 + processId), nodeExecutable: '/usr/local/bin/node', nodeVersion: 'v24.18.0', }; const sha256 = crypto .createHash('sha256') .update('qinglong.local-application-startup-receipt.v1\0', 'utf8') .update(JSON.stringify(payload), 'utf8') .digest('hex'); fs.writeFileSync( `${state.applicationConfigPath}.active.json`, `${JSON.stringify({ ...payload, sha256 })}\n`, { mode: 0o600 }, ); return sha256; } function stoppedLegacyInspection(state, running = false) { return JSON.stringify([ { Id: state.legacyContainerId, Created: '2026-08-09T00:00:00.000000000Z', Name: '/qinglong-legacy', State: { Running: running, Restarting: false, Paused: false, Pid: running ? 42 : 0, Status: running ? 'running' : 'exited', }, Config: { Image: 'whyour/qinglong:2.17.17' }, HostConfig: { RestartPolicy: { Name: 'no' } }, Mounts: [ { Type: 'bind', Source: state.managementRoot, Destination: '/ql/data', RW: true, }, ], }, ]); } function targetInspection(state, options = {}) { return JSON.stringify([ { Id: state.targetContainerId, Image: state.targetImageId, Created: '2026-08-09T01:00:00.000000000Z', Name: '/qinglong3-target', State: { Running: state.targetRunning, Restarting: false, Paused: false, Pid: state.targetRunning ? 100 : 0, Status: state.targetRunning ? 'running' : 'exited', }, Config: { Image: state.targetImage, Cmd: [ ...(options.normalApplicationCommand === true ? [] : ['--cutover-probe']), '--config', state.targetApiConfigPath ?? state.targetApplicationConfigPath, ], }, HostConfig: { RestartPolicy: { Name: 'no' }, ReadonlyRootfs: true, Privileged: false, SecurityOpt: ['no-new-privileges'], }, Mounts: [ { Type: 'bind', Source: state.deploymentRoot, Destination: targetPath(state, state.deploymentRoot), RW: true, }, { Type: 'bind', Source: state.legacySourcePath, Destination: targetPath(state, state.legacySourcePath), RW: options.writableLegacySource === true, }, ...[ state.targetDatabasePath, state.recoveryPath, state.manifestPath, state.activationPath, ].map((filePath) => ({ Type: 'bind', Source: filePath, Destination: targetPath(state, filePath), RW: true, })), ], }, ]); } function fixture(t) { const managementRoot = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-target-cutover-')), ); fs.chmodSync(managementRoot, 0o700); t.after(() => fs.rmSync(managementRoot, { recursive: true, force: true })); const deploymentRoot = path.join(managementRoot, 'runtime'); const serviceRoot = path.join(deploymentRoot, 'service'); const cutoverId = 'cutover-edge-1'; const journal = path.join(serviceRoot, 'cutovers', cutoverId); for (const directory of [ deploymentRoot, serviceRoot, path.dirname(journal), journal, ]) { fs.mkdirSync(directory, { mode: 0o700 }); } const legacySourcePath = path.join(managementRoot, 'database.sqlite'); const targetDatabasePath = path.join(managementRoot, 'database.ql3.sqlite'); const recoveryPath = path.join(managementRoot, 'database.recovery.sqlite'); const manifestPath = path.join(managementRoot, 'adoption-manifest.json'); const activationPath = path.join(managementRoot, 'activation.json'); fs.writeFileSync(legacySourcePath, 'legacy-source\n', { mode: 0o600 }); fs.writeFileSync(targetDatabasePath, 'target-initial\n', { mode: 0o600 }); fs.writeFileSync(recoveryPath, 'legacy-source\n', { mode: 0o600 }); fs.writeFileSync(manifestPath, '{}\n', { mode: 0o600 }); const targetStat = fs.statSync(targetDatabasePath, { bigint: true }); const activationPayload = { schemaVersion: 1, kind: 'qinglong3-local-sqlite-activation', state: 'prepared', profile: 'edge', createdAtMs: 1_000, adoptionManifestDigest: '1'.repeat(64), planDigest: '2'.repeat(64), sourcePathDigest: crypto .createHash('sha256') .update(legacySourcePath, 'utf8') .digest('hex'), sourceSha256: crypto .createHash('sha256') .update(fs.readFileSync(legacySourcePath)) .digest('hex'), recoverySha256: crypto .createHash('sha256') .update(fs.readFileSync(recoveryPath)) .digest('hex'), targetSha256: crypto .createHash('sha256') .update(fs.readFileSync(targetDatabasePath)) .digest('hex'), targetPathDigest: crypto .createHash('sha256') .update(targetDatabasePath, 'utf8') .digest('hex'), targetDevice: targetStat.dev.toString(), targetInode: targetStat.ino.toString(), }; const activationDigest = digest(activationPayload); fs.writeFileSync( activationPath, `${JSON.stringify({ ...activationPayload, activationDigest })}\n`, { mode: 0o600 }, ); const state = { managementRoot, deploymentRoot, journal, cutoverId, legacySourcePath, targetDatabasePath, recoveryPath, manifestPath, activationPath, activationDigest, legacyContainerId: '7'.repeat(64), targetContainerId: '8'.repeat(64), targetImage: `registry.example/qinglong3@sha256:${'9'.repeat(64)}`, targetImageAuthority: 'registry-digest', targetImageId: `sha256:${'a'.repeat(64)}`, targetRunning: false, legacyRunning: false, nextProcessId: 10, }; const commonOptions = { deploymentRoot, dockerExecutable: fs.realpathSync(process.execPath), dockerSocketPath: path.join(managementRoot, 'docker.sock'), allowRootService: rootAcknowledgement(), }; const legacy = stopLegacyDockerForLocalDeployment( { schemaVersion: 1, operation: 'local.deployment.cutover.legacy-stop', options: commonOptions, request: { cutoverId, profile: 'edge', instanceId: 'edge-router-1', activationPath, legacySourcePath, expectedLegacyDatabasePath: '/ql/data/database.sqlite', expectedActivationDigest: activationDigest, expectedLegacyContainerId: state.legacyContainerId, requestedAtMs: 2_000, }, }, { validateSocket() {}, runDocker({ args }) { return args[1] === 'inspect' ? stoppedLegacyInspection(state) : `${state.legacyContainerId}\n`; }, }, ); state.legacyCommitmentDigest = legacy.commitmentDigest; state.legacyCommitmentPath = path.join(journal, '0002-legacy-stopped.json'); state.applicationConfigPath = path.join( deploymentRoot, 'local-application.json', ); state.targetApplicationConfigPath = targetPath( state, state.applicationConfigPath, ); state.targetCommitmentPath = targetPath(state, state.legacyCommitmentPath); fs.writeFileSync( state.applicationConfigPath, `${JSON.stringify({ schema: 'qinglong/local-application-process@v3', profile: 'edge', instanceId: 'edge-router-1', storage: { mode: 'adopted', sourcePath: targetPath(state, legacySourcePath), targetPath: targetPath(state, targetDatabasePath), recoveryPath: targetPath(state, recoveryPath), manifestPath: targetPath(state, manifestPath), activationPath: targetPath(state, activationPath), expectedActivationDigest: activationDigest, }, cutover: { cutoverId, commitmentPath: state.targetCommitmentPath, expectedCommitmentDigest: legacy.commitmentDigest, }, })}\n`, { mode: 0o600 }, ); return state; } function prepareAdoptedV4Baseline(state) { const commitDigest = 'b'.repeat(64); const receiptDigest = 'c'.repeat(64); const config = { schema: 'qinglong/local-application-process@v4', profile: 'edge', instanceId: 'edge-router-1', storage: { mode: 'adopted', sourcePath: targetPath(state, state.legacySourcePath), targetPath: targetPath(state, state.targetDatabasePath), recoveryPath: targetPath(state, state.recoveryPath), manifestPath: targetPath(state, state.manifestPath), activationPath: targetPath(state, state.activationPath), expectedActivationDigest: state.activationDigest, }, cutover: { cutoverId: state.cutoverId, commitmentPath: state.targetCommitmentPath, expectedCommitmentDigest: state.legacyCommitmentDigest, }, legacyDataApplication: { commitPath: path.join( state.deploymentRoot, 'transformation', 'commit.json', ), expectedCommitDigest: commitDigest, expectedReceiptDigest: receiptDigest, }, }; fs.writeFileSync(state.applicationConfigPath, `${JSON.stringify(config)}\n`, { mode: 0o600, }); const target = fs.statSync(state.targetDatabasePath, { bigint: true }); const baselinePayload = { schemaVersion: 1, kind: 'qinglong3-local-adopted-target-baseline', state: 'prepared', preparedAtMs: 2_000, profile: 'edge', instanceId: 'edge-router-1', cutoverId: state.cutoverId, activationDigest: state.activationDigest, commitmentDigest: state.legacyCommitmentDigest, applicationConfigDigest: digest(config), legacyDataApplicationCommitDigest: commitDigest, legacyDataApplicationReceiptDigest: receiptDigest, targetPathDigest: sha256(state.targetDatabasePath), targetDevice: target.dev.toString(), targetInode: target.ino.toString(), targetSha256: sha256(fs.readFileSync(state.targetDatabasePath)), targetSidecarsClear: true, }; const baseline = { ...baselinePayload, baselineDigest: digest(baselinePayload), }; fs.writeFileSync( path.join(state.deploymentRoot, 'service/adopted-target-baseline.json'), `${JSON.stringify(baseline)}\n`, { mode: 0o600 }, ); return baseline; } function prepareLocalApiTarget(state) { state.apiConfigPath = path.join(state.deploymentRoot, 'local-api.json'); state.targetApiConfigPath = targetPath(state, state.apiConfigPath); state.targetApi = { configPath: state.apiConfigPath, expectedTargetConfigPath: state.targetApiConfigPath, }; fs.writeFileSync( state.apiConfigPath, `${JSON.stringify({ schema: 'qinglong/local-api-process@v1', deploymentRoot: targetPath(state, state.deploymentRoot), applicationConfigFilePath: state.targetApplicationConfigPath, ownerPepperKeyringDirectory: targetPath( state, path.join(state.deploymentRoot, 'owner-peppers'), ), listener: { host: '127.0.0.1', port: 5700 }, })}\n`, { mode: 0o600 }, ); } function command(state, generation = 1) { return { schemaVersion: 1, operation: generation === 1 ? 'local.deployment.cutover.target-start' : 'local.deployment.cutover.target-restart', options: { deploymentRoot: state.deploymentRoot, dockerExecutable: fs.realpathSync(process.execPath), dockerSocketPath: path.join(state.managementRoot, 'docker.sock'), allowRootService: rootAcknowledgement(), }, request: { cutoverId: state.cutoverId, profile: 'edge', instanceId: 'edge-router-1', activationPath: state.activationPath, legacySourcePath: state.legacySourcePath, targetDatabasePath: state.targetDatabasePath, recoveryPath: state.recoveryPath, manifestPath: state.manifestPath, expectedLegacyDatabasePath: '/ql/data/database.sqlite', expectedActivationDigest: state.activationDigest, expectedLegacyCommitmentDigest: state.legacyCommitmentDigest, expectedLegacyContainerId: state.legacyContainerId, expectedTargetContainerId: state.targetContainerId, targetImage: { authority: state.targetImageAuthority, reference: state.targetImage, imageId: state.targetImageId, }, applicationConfigPath: state.applicationConfigPath, expectedTargetApplicationConfigPath: state.targetApplicationConfigPath, ...(state.targetApi === undefined ? {} : { targetApi: state.targetApi }), expectedTargetCommitmentPath: state.targetCommitmentPath, generation, requestedAtMs: 2_000 + generation, }, }; } function stopCommand(state, generation = 1) { const value = command(state, generation); return { ...value, operation: 'local.deployment.cutover.target-stop', request: { ...value.request, requestedAtMs: 5_000 + generation, }, }; } function rollbackCommand( state, stopped, operation, expectedPreparationDigest = EMPTY_ROLLBACK_PREPARATION_DIGEST, ) { const value = stopCommand(state, stopped.generation); return { ...value, operation, request: { ...value.request, expectedInstanceHeadDigest: stopped.instanceHeadDigest, expectedStoppedRecordDigest: stopped.recordDigest, expectedPreparationDigest, rollbackRequestedAtMs: 7_000 + stopped.generation, }, }; } function manualCommand( state, operation, expectedPreparationDigest = EMPTY_RESOLUTION_DIGEST, ) { const head = JSON.parse( fs.readFileSync( path.join( state.deploymentRoot, 'service', 'cutover-instances', 'edge-router-1', 'head.json', ), 'utf8', ), ); return { schemaVersion: 1, operation, options: { deploymentRoot: state.deploymentRoot, dockerExecutable: fs.realpathSync(process.execPath), dockerSocketPath: path.join(state.managementRoot, 'docker.sock'), allowRootService: rootAcknowledgement(), }, request: { profile: 'edge', instanceId: 'edge-router-1', currentCutoverId: state.cutoverId, nextCutoverId: 'cutover-edge-2', currentActivationDigest: state.activationDigest, nextActivationDigest: state.activationDigest, expectedInstanceHeadDigest: head.headDigest, expectedManualRecordDigest: head.sourceRecordDigest, expectedLegacyContainerId: state.legacyContainerId, expectedTargetContainerId: state.targetContainerId, expectedPreparationDigest, requestedAtMs: 3_000, }, }; } function harness(state, options = {}) { const calls = []; let time = 10_000; return { calls, validateSocket() {}, runDocker({ args }) { calls.push(args); if ( args[0] === 'container' && args[1] === 'inspect' && args[2] === state.legacyContainerId ) { return stoppedLegacyInspection( state, options.legacyRunning === true || state.legacyRunning === true, ); } if (args[0] === 'container' && args[1] === 'inspect') { return targetInspection(state, options); } if (args[0] === 'container' && args[1] === 'start') { if (args[2] === state.legacyContainerId) { if (options.leaveLegacyStopped !== true) state.legacyRunning = true; if (options.startTargetWithLegacy === true) state.targetRunning = true; if (options.loseLegacyStartResponse === true) { throw new Error('simulated lost legacy start response'); } return `${state.legacyContainerId}\n`; } if (options.leaveStopped !== true) { state.targetRunning = true; startupReceipt(state, ++state.nextProcessId); } return `${state.targetContainerId}\n`; } if (args[0] === 'container' && args[1] === 'update') { return `${state.targetContainerId}\n`; } if (args[0] === 'container' && args[1] === 'stop') { if (options.leaveRunningOnStop !== true) state.targetRunning = false; if (options.loseStopResponse === true) { throw new Error('simulated lost stop response'); } return `${state.targetContainerId}\n`; } throw new Error('unexpected Docker call'); }, now() { time += options.expireImmediately === true ? 31_000 : 1; return time; }, async wait() { if (options.crashDuringObservation === true) { throw new Error('simulated supervisor crash'); } }, afterBarrier: options.crashAfterStopBarrier === true || options.crashAfterRollbackBarrier === true || options.startTargetAfterRollbackBarrier === true ? () => { if (options.startTargetAfterRollbackBarrier === true) { state.targetRunning = true; return; } throw new Error( options.crashAfterRollbackBarrier === true ? 'simulated rollback supervisor crash' : 'simulated stop supervisor crash', ); } : undefined, afterStart: options.crashAfterLegacyStart === true ? () => { throw new Error('simulated crash after legacy start'); } : undefined, }; } test('starts an exact target once and replays the active commitment without Docker', async (t) => { const state = fixture(t); const controller = harness(state); const active = await runLocalDeploymentDockerTarget( command(state), controller, ); assert.equal(active.status, 'prepared'); assert.equal(active.state, 'target_active'); assert.equal(active.generation, 1); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 1, ); const request = JSON.parse( fs.readFileSync( path.join(state.journal, '0003-target-start-decision.json'), 'utf8', ), ); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0004-target-start-outcome.json'), 'utf8', ), ); assert.equal(request.state, 'target_start_requested'); assert.equal(outcome.state, 'target_active'); assert.equal(outcome.previousRecordDigest, request.recordDigest); const replay = await runLocalDeploymentDockerTarget(command(state), { validateSocket() { throw new Error('terminal replay must not open Docker authority'); }, runDocker() { throw new Error('terminal replay must not inspect or start'); }, }); assert.equal(replay.status, 'existing'); assert.equal(replay.recordDigest, active.recordDigest); }); test('requires the target container to keep the Legacy source read-only', async (t) => { const state = fixture(t); const controller = harness(state, { writableLegacySource: true }); const result = await runLocalDeploymentDockerTarget( command(state), controller, ); assert.equal(result.state, 'manual_required'); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 0, ); const request = JSON.parse( fs.readFileSync( path.join(state.journal, '0003-target-start-decision.json'), 'utf8', ), ); assert.equal(request.evidence.reason, 'target_preflight_unproved'); }); test('requires cutover target-start to use frozen-admission probe mode', async (t) => { const state = fixture(t); const controller = harness(state, { normalApplicationCommand: true }); const result = await runLocalDeploymentDockerTarget( command(state), controller, ); assert.equal(result.state, 'manual_required'); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 0, ); const request = JSON.parse( fs.readFileSync( path.join(state.journal, '0003-target-start-decision.json'), 'utf8', ), ); assert.equal(request.evidence.reason, 'target_preflight_unproved'); }); test('starts an offline Trial Kit image only when its local reference and content ID both match', async (t) => { const state = fixture(t); state.targetImageAuthority = 'local-image-id'; state.targetImage = 'qinglong3-local-application:ci-amd64'; const active = await runLocalDeploymentDockerTarget( command(state), harness(state), ); assert.equal(active.state, 'target_active'); }); test('starts an exact Local API cutover probe with both entry and Application configs bound', async (t) => { const state = fixture(t); prepareLocalApiTarget(state); state.targetImageAuthority = 'local-image-id'; state.targetImage = 'qinglong3-local-console:ci-amd64'; const active = await runLocalDeploymentDockerTarget( command(state), harness(state), ); assert.equal(active.state, 'target_active'); const request = JSON.parse( fs.readFileSync( path.join(state.journal, '0003-target-start-decision.json'), 'utf8', ), ); assert.match( request.evidence.targetApplicationBindingDigest, /^[0-9a-f]{64}$/, ); }); test('fails closed before starting when a Local API entry points at another Application config', async (t) => { const state = fixture(t); prepareLocalApiTarget(state); const config = JSON.parse(fs.readFileSync(state.apiConfigPath, 'utf8')); config.applicationConfigFilePath = '/host/runtime/other-application.json'; fs.writeFileSync(state.apiConfigPath, `${JSON.stringify(config)}\n`, { mode: 0o600, }); await assert.rejects( runLocalDeploymentDockerTarget(command(state), harness(state)), /Local API configuration binding is invalid/, ); }); test('rejects a Local API entry that aliases the Application target config path', async (t) => { const state = fixture(t); prepareLocalApiTarget(state); state.targetApi.expectedTargetConfigPath = state.targetApplicationConfigPath; await assert.rejects( runLocalDeploymentDockerTarget(command(state), harness(state)), /API and Application configuration paths must be distinct/, ); }); test('rejects a Local API owner pepper directory equal to its deployment root', async (t) => { const state = fixture(t); prepareLocalApiTarget(state); const config = JSON.parse(fs.readFileSync(state.apiConfigPath, 'utf8')); config.ownerPepperKeyringDirectory = config.deploymentRoot; fs.writeFileSync(state.apiConfigPath, `${JSON.stringify(config)}\n`, { mode: 0o600, }); await assert.rejects( runLocalDeploymentDockerTarget(command(state), harness(state)), /Local API configuration binding is invalid/, ); }); test('makes an offline Trial Kit target manual-required when its inspected content ID drifted', async (t) => { const state = fixture(t); state.targetImageAuthority = 'local-image-id'; state.targetImage = 'qinglong3-local-application:ci-amd64'; const request = command(state); state.targetImageId = `sha256:${'b'.repeat(64)}`; const controller = harness(state); const result = await runLocalDeploymentDockerTarget(request, controller); assert.equal(result.state, 'manual_required'); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 0, ); }); test('rejects a mutable local tag under registry digest authority before Docker access', async (t) => { const state = fixture(t); const request = command(state); request.request.targetImage.reference = 'qinglong3-local-application:ci-amd64'; await assert.rejects( runLocalDeploymentDockerTarget(request, { validateSocket() { throw new Error('invalid image authority must not reach Docker'); }, }), /targetImage identity is invalid/, ); }); test('recovers a crash after the start barrier by inspection without repeating start', async (t) => { const state = fixture(t); const crashing = harness(state, { leaveStopped: true, crashDuringObservation: true, }); await assert.rejects( runLocalDeploymentDockerTarget(command(state), crashing), /simulated supervisor crash/, ); assert.equal( fs.existsSync(path.join(state.journal, '0003-target-start-decision.json')), true, ); assert.equal( fs.existsSync(path.join(state.journal, '0004-target-start-outcome.json')), false, ); assert.equal(crashing.calls.filter((args) => args[1] === 'start').length, 1); state.targetRunning = true; startupReceipt(state, ++state.nextProcessId); const recovering = harness(state); const recovered = await runLocalDeploymentDockerTarget( command(state), recovering, ); assert.equal(recovered.state, 'target_active'); assert.equal( recovering.calls.filter((args) => args[1] === 'start').length, 0, ); }); test('makes an unproved target start terminal manual_required', async (t) => { const state = fixture(t); const controller = harness(state, { leaveStopped: true, expireImmediately: true, }); const unresolved = await runLocalDeploymentDockerTarget( command(state), controller, ); assert.equal(unresolved.status, 'prepared'); assert.equal(unresolved.state, 'manual_required'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0004-target-start-outcome.json'), 'utf8', ), ); assert.equal(outcome.state, 'manual_required'); assert.equal(outcome.evidence.reason, 'target_start_result_unproved'); assert.equal(JSON.stringify(outcome).includes('Docker command'), false); const replay = await runLocalDeploymentDockerTarget(command(state), { validateSocket() { throw new Error('manual terminal must not reopen Docker authority'); }, }); assert.equal(replay.status, 'existing'); assert.equal(replay.state, 'manual_required'); }); test('reproves legacy silence before each target restart generation', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const previousReceipt = JSON.parse( fs.readFileSync(`${state.applicationConfigPath}.active.json`, 'utf8'), ).sha256; state.targetRunning = false; const controller = harness(state); const restarted = await runLocalDeploymentDockerTarget( command(state, 2), controller, ); assert.equal(restarted.state, 'target_active'); const states = [5, 6, 7, 8].map((number) => { const file = fs .readdirSync(state.journal) .find((name) => name.startsWith(String(number).padStart(4, '0'))); return JSON.parse(fs.readFileSync(path.join(state.journal, file), 'utf8')) .state; }); assert.deepEqual(states, [ 'legacy_recheck_requested', 'legacy_reverified', 'target_restart_requested', 'target_active', ]); const legacyInspectIndex = controller.calls.findIndex( (args) => args[1] === 'inspect' && args[2] === state.legacyContainerId, ); const startIndex = controller.calls.findIndex((args) => args[1] === 'start'); assert.ok(legacyInspectIndex >= 0 && legacyInspectIndex < startIndex); const currentReceipt = JSON.parse( fs.readFileSync(`${state.applicationConfigPath}.active.json`, 'utf8'), ).sha256; assert.notEqual(currentReceipt, previousReceipt); }); test('never restarts target when legacy silence cannot be reverified', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); state.targetRunning = false; const controller = harness(state, { legacyRunning: true }); const unresolved = await runLocalDeploymentDockerTarget( command(state, 2), controller, ); assert.equal(unresolved.state, 'manual_required'); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 0, ); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-legacy-recheck-outcome.json'), 'utf8', ), ); assert.equal(outcome.evidence.reason, 'legacy_silence_unproved'); }); test('rejects a self-consistent restart request detached from legacy reverified evidence', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); state.targetRunning = false; await runLocalDeploymentDockerTarget(command(state, 2), harness(state)); const requestPath = path.join( state.journal, '0007-target-restart-decision.json', ); const request = JSON.parse(fs.readFileSync(requestPath, 'utf8')); request.previousRecordDigest = 'f'.repeat(64); const { recordDigest: ignored, ...payload } = request; request.recordDigest = digest(payload); fs.writeFileSync(requestPath, `${JSON.stringify(request, null, 2)}\n`, { mode: 0o600, }); await assert.rejects( runLocalDeploymentDockerTarget(command(state, 2), { validateSocket() { throw new Error('drift must fail before Docker authority'); }, }), /journal record drifted/, ); }); test('refuses target start when the writable target database mount is not bound', async (t) => { const state = fixture(t); const detached = command(state); detached.request.targetDatabasePath = '/var/db/detached-qinglong3.sqlite'; const controller = harness(state); const unresolved = await runLocalDeploymentDockerTarget(detached, controller); assert.equal(unresolved.state, 'manual_required'); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 0, ); const decision = JSON.parse( fs.readFileSync( path.join(state.journal, '0003-target-start-decision.json'), 'utf8', ), ); assert.equal(decision.evidence.reason, 'target_preflight_unproved'); }); test('prevents a new cutover id from bypassing a manual-required instance head', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget( command(state), harness(state, { leaveStopped: true, expireImmediately: true }), ); let dockerCalls = 0; await assert.rejects( Promise.resolve().then(() => stopLegacyDockerForLocalDeployment( { schemaVersion: 1, operation: 'local.deployment.cutover.legacy-stop', options: command(state).options, request: { cutoverId: 'cutover-edge-2', profile: 'edge', instanceId: 'edge-router-1', activationPath: state.activationPath, legacySourcePath: state.legacySourcePath, expectedLegacyDatabasePath: '/ql/data/database.sqlite', expectedActivationDigest: state.activationDigest, expectedLegacyContainerId: state.legacyContainerId, requestedAtMs: 3_000, }, }, { validateSocket() { dockerCalls += 1; }, runDocker() { dockerCalls += 1; throw new Error('must not reach Docker'); }, }, ), ), /explicit manual resolution is required/, ); assert.equal(dockerCalls, 0); assert.equal( fs.existsSync( path.join(state.deploymentRoot, 'service', 'cutovers', 'cutover-edge-2'), ), false, ); }); test('diagnoses and resolves manual-required through inspect-only prepare and CAS commit', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget( command(state), harness(state, { leaveStopped: true, expireImmediately: true }), ); const controller = harness(state, { leaveStopped: true }); const diagnosed = runLocalDeploymentCutoverManualCommand( manualCommand(state, 'local.deployment.cutover.manual-diagnose'), controller, ); assert.equal(diagnosed.state, 'manual_diagnosed'); assert.equal(diagnosed.legacyState, 'stopped'); assert.equal(diagnosed.targetState, 'stopped'); assert.ok(controller.calls.every((args) => args[1] === 'inspect')); const prepared = runLocalDeploymentCutoverManualCommand( manualCommand(state, 'local.deployment.cutover.manual-resolution-prepare'), harness(state, { leaveStopped: true }), ); assert.equal(prepared.state, 'resolution_prepared'); assert.match(prepared.preparationDigest, /^[0-9a-f]{64}$/); const commitCommand = manualCommand( state, 'local.deployment.cutover.manual-resolution-commit', prepared.preparationDigest, ); const committed = runLocalDeploymentCutoverManualCommand( commitCommand, harness(state, { leaveStopped: true }), ); assert.equal(committed.state, 'resolution_authorized'); assert.equal(committed.status, 'prepared'); const head = JSON.parse( fs.readFileSync( path.join( state.deploymentRoot, 'service', 'cutover-instances', 'edge-router-1', 'head.json', ), 'utf8', ), ); assert.equal(head.cutoverId, 'cutover-edge-2'); assert.equal(head.state, 'resolution_authorized'); assert.equal(head.previousHeadDigest, diagnosed.instanceHeadDigest); const replay = runLocalDeploymentCutoverManualCommand(commitCommand, { validateSocket() { throw new Error('commit replay must not reopen Docker authority'); }, runDocker() { throw new Error('commit replay must not inspect or mutate'); }, }); assert.equal(replay.status, 'existing'); assert.equal(replay.instanceHeadDigest, committed.instanceHeadDigest); await assert.rejects( runLocalDeploymentDockerTarget(command(state), { validateSocket() { throw new Error('stale cutover must fail before Docker authority'); }, }), /not bound to the instance lineage head/, ); const nextLegacy = stopLegacyDockerForLocalDeployment( { schemaVersion: 1, operation: 'local.deployment.cutover.legacy-stop', options: command(state).options, request: { cutoverId: 'cutover-edge-2', profile: 'edge', instanceId: 'edge-router-1', activationPath: state.activationPath, legacySourcePath: state.legacySourcePath, expectedLegacyDatabasePath: '/ql/data/database.sqlite', expectedActivationDigest: state.activationDigest, expectedLegacyContainerId: state.legacyContainerId, requestedAtMs: 4_000, }, }, { validateSocket() {}, runDocker({ args }) { return args[1] === 'inspect' ? stoppedLegacyInspection(state) : `${state.legacyContainerId}\n`; }, }, ); assert.equal(nextLegacy.state, 'legacy_stopped'); const nextHead = JSON.parse( fs.readFileSync( path.join( state.deploymentRoot, 'service', 'cutover-instances', 'edge-router-1', 'head.json', ), 'utf8', ), ); assert.equal(nextHead.cutoverId, 'cutover-edge-2'); assert.equal(nextHead.state, 'legacy_stopped'); }); test('rejects manual resolution commit when stopped evidence drifts', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget( command(state), harness(state, { leaveStopped: true, expireImmediately: true }), ); const prepared = runLocalDeploymentCutoverManualCommand( manualCommand(state, 'local.deployment.cutover.manual-resolution-prepare'), harness(state, { leaveStopped: true }), ); state.targetRunning = true; assert.throws( () => runLocalDeploymentCutoverManualCommand( manualCommand( state, 'local.deployment.cutover.manual-resolution-commit', prepared.preparationDigest, ), harness(state), ), /requires both legacy and target to be proved stopped/, ); const head = JSON.parse( fs.readFileSync( path.join( state.deploymentRoot, 'service', 'cutover-instances', 'edge-router-1', 'head.json', ), 'utf8', ), ); assert.equal(head.state, 'manual_required'); }); test('stops an active target and proves an unchanged rollback candidate', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const controller = harness(state); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), controller, ); assert.equal(stopped.state, 'target_stopped'); assert.equal(stopped.reconciliation, 'rollback_candidate'); assert.equal(controller.calls.filter((args) => args[1] === 'stop').length, 1); const request = JSON.parse( fs.readFileSync( path.join(state.journal, '0005-target-stop-decision.json'), 'utf8', ), ); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-target-stop-outcome.json'), 'utf8', ), ); assert.equal(request.state, 'target_stop_requested'); assert.equal(outcome.state, 'target_stopped'); assert.equal(outcome.evidence.reconciliation.targetMatchesActivation, true); assert.equal(outcome.evidence.reconciliation.sourceMatchesActivation, true); const replay = stopLocalDeploymentDockerTarget(stopCommand(state), { validateSocket() { throw new Error('stopped replay must not open Docker authority'); }, }); assert.equal(replay.status, 'existing'); assert.equal(replay.recordDigest, stopped.recordDigest); await assert.rejects( runLocalDeploymentDockerTarget(command(state, 2), { validateSocket() { throw new Error('stopped target must fail before Docker authority'); }, }), /target command is not bound to the instance lineage head/, ); }); test('uses the authenticated v4 post-apply baseline for clean rollback', async (t) => { const state = fixture(t); fs.writeFileSync(state.targetDatabasePath, 'authenticated applied target\n'); const baseline = prepareAdoptedV4Baseline(state); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(stopped.state, 'target_stopped'); assert.equal(stopped.reconciliation, 'rollback_candidate'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-target-stop-outcome.json'), 'utf8', ), ); const reconciliation = outcome.evidence.reconciliation; assert.equal(reconciliation.baselineKind, 'adopted_target'); assert.equal(reconciliation.baselineDigest, baseline.baselineDigest); assert.equal(reconciliation.targetMatchesActivation, false); assert.equal(reconciliation.targetMatchesBaseline, true); assert.equal(reconciliation.sourceMatchesActivation, true); }); test('requires reconciliation when the v4 target writes after its baseline', async (t) => { const state = fixture(t); fs.writeFileSync(state.targetDatabasePath, 'authenticated applied target\n'); const baseline = prepareAdoptedV4Baseline(state); await runLocalDeploymentDockerTarget(command(state), harness(state)); fs.writeFileSync(state.targetDatabasePath, 'post-cutover write\n'); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(stopped.reconciliation, 'reconciliation_required'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-target-stop-outcome.json'), 'utf8', ), ); assert.equal( outcome.evidence.reconciliation.baselineDigest, baseline.baselineDigest, ); assert.equal(outcome.evidence.reconciliation.targetMatchesBaseline, false); }); test('fails a drifted v4 target baseline closed to manual review', async (t) => { const state = fixture(t); fs.writeFileSync(state.targetDatabasePath, 'authenticated applied target\n'); prepareAdoptedV4Baseline(state); await runLocalDeploymentDockerTarget(command(state), harness(state)); const baselinePath = path.join( state.deploymentRoot, 'service/adopted-target-baseline.json', ); const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); fs.writeFileSync( baselinePath, `${JSON.stringify({ ...baseline, targetSha256: 'd'.repeat(64) })}\n`, { mode: 0o600 }, ); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(stopped.state, 'target_stopped'); assert.equal(stopped.reconciliation, 'manual_review'); }); test('prepares and commits an exact legacy rollback without mutating target data', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const preparing = harness(state); const prepared = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), preparing, ); assert.equal(prepared.state, 'rollback_prepared'); assert.match(prepared.preparationDigest, /^[0-9a-f]{64}$/); assert.equal(preparing.calls.filter((args) => args[1] === 'start').length, 0); const targetBefore = fs.readFileSync(state.targetDatabasePath); const commitCommand = rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-commit', prepared.preparationDigest, ); const committing = harness(state); const committed = runLocalDeploymentLegacyRollback(commitCommand, committing); assert.equal(committed.state, 'legacy_running'); assert.equal( committing.calls.filter( (args) => args[1] === 'start' && args[2] === state.legacyContainerId, ).length, 1, ); assert.deepEqual(fs.readFileSync(state.targetDatabasePath), targetBefore); assert.equal( JSON.parse( fs.readFileSync( path.join(state.journal, '0007-legacy-rollback-start-decision.json'), 'utf8', ), ).state, 'legacy_restart_requested', ); assert.equal( JSON.parse( fs.readFileSync( path.join(state.journal, '0008-legacy-rollback-start-outcome.json'), 'utf8', ), ).state, 'legacy_running', ); const replay = runLocalDeploymentLegacyRollback(commitCommand, { validateSocket() { throw new Error('terminal rollback replay must not open Docker'); }, runDocker() { throw new Error('terminal rollback replay must not inspect or start'); }, }); assert.equal(replay.status, 'existing'); assert.equal(replay.recordDigest, committed.recordDigest); }); test('refuses rollback preparation after the target has produced new facts', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); fs.appendFileSync(state.targetDatabasePath, 'new-qinglong3-fact\n'); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(stopped.reconciliation, 'reconciliation_required'); let dockerCalls = 0; assert.throws( () => runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), { validateSocket() { dockerCalls += 1; }, }, ), /requires the exact rollback candidate/, ); assert.equal(dockerCalls, 0); }); test('terminalizes rollback when data drifts between prepare and commit', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const prepared = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), harness(state), ); fs.appendFileSync(state.targetDatabasePath, 'post-prepare-drift\n'); const controller = harness(state); const result = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-commit', prepared.preparationDigest, ), controller, ); assert.equal(result.state, 'manual_required'); assert.equal( controller.calls.filter((args) => args[1] === 'start').length, 0, ); const decision = JSON.parse( fs.readFileSync( path.join(state.journal, '0007-legacy-rollback-start-decision.json'), 'utf8', ), ); assert.equal(decision.evidence.reason, 'legacy_restart_preflight_unproved'); }); test('does not blindly start legacy after a crash at the rollback barrier', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const prepared = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), harness(state), ); const commitCommand = rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-commit', prepared.preparationDigest, ); assert.throws( () => runLocalDeploymentLegacyRollback( commitCommand, harness(state, { crashAfterRollbackBarrier: true }), ), /simulated rollback supervisor crash/, ); const recovering = harness(state); const result = runLocalDeploymentLegacyRollback(commitCommand, recovering); assert.equal(result.state, 'manual_required'); assert.equal( recovering.calls.filter((args) => args[1] === 'start').length, 0, ); }); test('rechecks target stopped after the rollback barrier before starting legacy', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const prepared = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), harness(state), ); const controller = harness(state, { startTargetAfterRollbackBarrier: true, }); const result = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-commit', prepared.preparationDigest, ), controller, ); assert.equal(result.state, 'manual_required'); assert.equal( controller.calls.some( (args) => args[1] === 'start' && args[2] === state.legacyContainerId, ), false, ); }); test('recovers a crash after legacy start by inspection without starting twice', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const prepared = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), harness(state), ); const commitCommand = rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-commit', prepared.preparationDigest, ); assert.throws( () => runLocalDeploymentLegacyRollback( commitCommand, harness(state, { crashAfterLegacyStart: true }), ), /simulated crash after legacy start/, ); const recovering = harness(state); const result = runLocalDeploymentLegacyRollback(commitCommand, recovering); assert.equal(result.state, 'legacy_running'); assert.equal( recovering.calls.filter((args) => args[1] === 'start').length, 0, ); }); test('does not claim rollback success when target is also running', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const prepared = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), harness(state), ); const result = runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-commit', prepared.preparationDigest, ), harness(state, { startTargetWithLegacy: true }), ); assert.equal(result.state, 'manual_required'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0008-legacy-rollback-start-outcome.json'), 'utf8', ), ); assert.equal(outcome.evidence.reason, 'legacy_restart_result_unproved'); }); test('rejects unsafe rollback preparation directory entries', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); const instanceDirectory = path.join( state.deploymentRoot, 'service', 'cutover-instances', 'edge-router-1', ); fs.symlinkSync( state.activationPath, path.join(instanceDirectory, 'rollback-untrusted.json'), ); assert.throws( () => runLocalDeploymentLegacyRollback( rollbackCommand( state, stopped, 'local.deployment.cutover.legacy-rollback-prepare', ), harness(state), ), /preparation directory contains an unsafe entry/, ); }); test('classifies a written target as reconciliation-required without restarting legacy', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); fs.appendFileSync(state.targetDatabasePath, 'new-qinglong3-fact\n'); const controller = harness(state); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), controller, ); assert.equal(stopped.state, 'target_stopped'); assert.equal(stopped.reconciliation, 'reconciliation_required'); assert.equal( controller.calls.some( (args) => args[1] === 'start' && args[2] === state.legacyContainerId, ), false, ); }); test('treats target SQLite sidecars as reconciliation-required', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); fs.writeFileSync(`${state.targetDatabasePath}-wal`, 'pending-wal\n', { mode: 0o600, }); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(stopped.reconciliation, 'reconciliation_required'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-target-stop-outcome.json'), 'utf8', ), ); assert.equal(outcome.evidence.reconciliation.targetMatchesActivation, true); assert.equal(outcome.evidence.reconciliation.targetSidecarsClear, false); }); test('requires manual review when legacy source no longer matches activation', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); fs.appendFileSync(state.legacySourcePath, 'offline-legacy-drift\n'); const stopped = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(stopped.reconciliation, 'manual_review'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-target-stop-outcome.json'), 'utf8', ), ); assert.equal(outcome.evidence.reconciliation.targetMatchesActivation, true); assert.equal(outcome.evidence.reconciliation.sourceMatchesActivation, false); }); test('recovers a crash after the stop barrier by converging stop-and-inspect', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); assert.throws( () => stopLocalDeploymentDockerTarget( stopCommand(state), harness(state, { crashAfterStopBarrier: true }), ), /simulated stop supervisor crash/, ); assert.equal( fs.existsSync(path.join(state.journal, '0005-target-stop-decision.json')), true, ); assert.equal( fs.existsSync(path.join(state.journal, '0006-target-stop-outcome.json')), false, ); const recovered = stopLocalDeploymentDockerTarget( stopCommand(state), harness(state), ); assert.equal(recovered.state, 'target_stopped'); }); test('makes an unproved target stop terminal manual-required', async (t) => { const state = fixture(t); await runLocalDeploymentDockerTarget(command(state), harness(state)); const controller = harness(state, { leaveRunningOnStop: true }); const unresolved = stopLocalDeploymentDockerTarget( stopCommand(state), controller, ); assert.equal(unresolved.state, 'manual_required'); assert.equal(unresolved.reconciliation, 'manual_review'); const outcome = JSON.parse( fs.readFileSync( path.join(state.journal, '0006-target-stop-outcome.json'), 'utf8', ), ); assert.equal(outcome.evidence.reason, 'target_stop_result_unproved'); });