feat(ql3): commit service manager legacy rollback

This commit is contained in:
whyour
2026-08-20 17:40:29 +08:00
parent ad291e514d
commit 2cd6ea1d55
16 changed files with 3382 additions and 62 deletions
@@ -11,7 +11,16 @@ const {
const {
localServiceManagerLegacyRollbackPreparationPath,
prepareLocalServiceManagerLegacyRollback,
} = require('../dist/deployment/service-manager/serviceLegacyRollback.js');
} = require('../dist/deployment/service-manager/legacy-rollback/preparation.js');
const {
authorizeLocalServiceManagerLegacyRollback,
consumeLocalServiceManagerLegacyRollback,
} = require('../dist/deployment/service-manager/legacy-rollback/consumer.js');
const {
localServiceManagerLegacyStartOutcomeDigest,
localServiceManagerLegacyStartOutcomePath,
localServiceManagerRollbackObservationDigest,
} = require('../dist/deployment/service-manager/legacy-rollback/contract.js');
const {
prepareLocalServiceManagerIntent,
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
@@ -384,6 +393,126 @@ function rollbackPrepareCommand(state, stoppedResult, head) {
};
}
function rollbackAuthorizeCommand(state, prepared) {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.legacy-rollback.authorize',
options: {
deploymentRoot: state.root,
allowRootService: process.getuid() === 0,
},
request: {
cutoverId: state.cutoverId,
profile: 'edge',
instanceId: 'edge-router-1',
generation: 1,
expectedActivationDigest: state.activationDigest,
expectedPreparationDigest: prepared.preparationDigest,
expectedInstanceHeadDigest: prepared.instanceHeadDigest,
requestedAtMs: 1786416000500,
},
};
}
function rollbackObservation(serviceName, active, observedAtMs) {
const payload = {
managerKind: 'systemd',
serviceName,
fragmentPath: `/etc/systemd/system/${serviceName}.service`,
loadState: 'loaded',
activeState: active ? 'active' : 'inactive',
subState: active ? 'running' : 'dead',
enabledState: 'enabled',
mainPid: active ? 5723 : 0,
observedAtMs,
};
return {
...payload,
observationDigest: localServiceManagerRollbackObservationDigest(payload),
};
}
function publishLegacyStartOutcome(
state,
prepared,
authorized,
outcomeState = 'legacy_running',
) {
const preparation = JSON.parse(
fs.readFileSync(
localServiceManagerLegacyRollbackPreparationPath(
state.root,
state.cutoverId,
1,
),
'utf8',
),
);
const legacyObservation = rollbackObservation(
'qinglong',
outcomeState === 'legacy_running',
1786416000600,
);
const targetObservation = rollbackObservation(
'qinglong3',
false,
1786416000601,
);
const payload = {
schema: 'qinglong3-local-service-manager-legacy-start-outcome',
schemaVersion: 1,
state: outcomeState,
cutoverId: state.cutoverId,
profile: 'edge',
instanceId: 'edge-router-1',
generation: 1,
activationDigest: state.activationDigest,
managerKind: 'systemd',
preparationDigest: prepared.preparationDigest,
authorizationDigest: authorized.authorizationDigest,
barrierDigest: '9'.repeat(64),
legacyDescriptorDigest: 'a'.repeat(64),
targetDescriptorDigest: preparation.targetDescriptorDigest,
mutationDisposition: 'executed',
manualReason:
outcomeState === 'legacy_running' ? null : 'manager_state_unproved',
legacyObservation,
targetObservation,
completedAtMs: 1786416000610,
};
const outcome = {
...payload,
outcomeDigest: localServiceManagerLegacyStartOutcomeDigest(payload),
};
writePrivate(
localServiceManagerLegacyStartOutcomePath(state.root, state.cutoverId, 1),
outcome,
);
return outcome;
}
function rollbackConsumeCommand(state, prepared, authorized) {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.legacy-rollback.consume',
options: {
deploymentRoot: state.root,
allowRootService: process.getuid() === 0,
},
request: {
cutoverId: state.cutoverId,
profile: 'edge',
instanceId: 'edge-router-1',
generation: 1,
expectedActivationDigest: state.activationDigest,
expectedPreparationDigest: prepared.preparationDigest,
expectedAuthorizationDigest: authorized.authorizationDigest,
expectedAuthorizationHeadDigest: authorized.instanceHeadDigest,
requestedAtMs: 1786416000700,
},
};
}
test('commits adopted service active evidence and replays from the instance head', async (t) => {
const state = fixture(t);
const prepared = prepare(
@@ -574,6 +703,92 @@ test('prepares and exactly replays lossless service-manager legacy rollback evid
assert.equal(replay.instanceHeadDigest, prepared.instanceHeadDigest);
});
test('authorizes, consumes and exactly replays service-manager legacy running evidence', async (t) => {
const state = fixture(t);
const { stoppedResult, head } = await stopAdoptedTarget(state, '039');
const prepared = prepareLocalServiceManagerLegacyRollback(
rollbackPrepareCommand(state, stoppedResult, head),
);
const authorizeCommand = rollbackAuthorizeCommand(state, prepared);
const authorized =
authorizeLocalServiceManagerLegacyRollback(authorizeCommand);
assert.equal(authorized.status, 'prepared');
assert.equal(authorized.state, 'legacy_restart_requested');
assert.equal(
authorizeLocalServiceManagerLegacyRollback(authorizeCommand).status,
'existing',
);
const requestedHead = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(requestedHead.state, 'legacy_restart_requested');
assert.equal(
requestedHead.sourceRecordDigest,
authorized.authorizationDigest,
);
const outcome = publishLegacyStartOutcome(state, prepared, authorized);
const consumeCommand = rollbackConsumeCommand(state, prepared, authorized);
const consumed = consumeLocalServiceManagerLegacyRollback(consumeCommand);
assert.equal(consumed.status, 'prepared');
assert.equal(consumed.state, 'legacy_running');
assert.equal(consumed.outcomeDigest, outcome.outcomeDigest);
assert.match(consumed.completionDigest, /^[0-9a-f]{64}$/);
const replay = consumeLocalServiceManagerLegacyRollback(consumeCommand);
assert.equal(replay.status, 'existing');
assert.equal(replay.completionDigest, consumed.completionDigest);
assert.equal(replay.instanceHeadDigest, consumed.instanceHeadDigest);
});
test('requires current rollback evidence before publishing legacy start authorization', async (t) => {
const state = fixture(t);
const { stoppedResult, head } = await stopAdoptedTarget(state, '041');
const prepared = prepareLocalServiceManagerLegacyRollback(
rollbackPrepareCommand(state, stoppedResult, head),
);
fs.writeFileSync(state.targetPath, 'target-drift-before-authorization\n', {
mode: 0o600,
});
assert.throws(
() =>
authorizeLocalServiceManagerLegacyRollback(
rollbackAuthorizeCommand(state, prepared),
),
/prepared rollback data drifted after authorization/,
);
const unchanged = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(unchanged.state, 'rollback_prepared');
assert.equal(unchanged.headDigest, prepared.instanceHeadDigest);
});
test('terminalizes an unproved root legacy start outcome', async (t) => {
const state = fixture(t);
const { stoppedResult, head } = await stopAdoptedTarget(state, '043');
const prepared = prepareLocalServiceManagerLegacyRollback(
rollbackPrepareCommand(state, stoppedResult, head),
);
const authorized = authorizeLocalServiceManagerLegacyRollback(
rollbackAuthorizeCommand(state, prepared),
);
publishLegacyStartOutcome(state, prepared, authorized, 'manual_required');
const consumed = consumeLocalServiceManagerLegacyRollback(
rollbackConsumeCommand(state, prepared, authorized),
);
assert.equal(consumed.state, 'manual_required');
const terminal = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(terminal.state, 'manual_required');
assert.equal(terminal.sourceRecordDigest, consumed.completionDigest);
});
test('keeps target_stopped when service-manager rollback would discard target writes', async (t) => {
const state = fixture(t);
const { stoppedResult, head } = await stopAdoptedTarget(state, '033');
@@ -0,0 +1,144 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
localServiceManagerLegacyStartOutcomeDigest,
localServiceManagerRollbackObservationDigest,
normalizeLocalServiceManagerLegacyRollbackBridgeCommand,
normalizeLocalServiceManagerLegacyStartOutcome,
} = require('../dist/deployment/service-manager/legacy-rollback/contract.js');
function command(kind = 'systemd') {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.legacy-rollback.execute',
options: {
deploymentRoot: '/var/lib/qinglong3',
controllerRoot: '/var/lib/qinglong3-service-bridge',
allowRootController: true,
manager:
kind === 'systemd'
? { kind, executable: '/usr/bin/systemctl' }
: {
kind,
serviceExecutable: '/sbin/rc-service',
updateExecutable: '/sbin/rc-update',
},
},
request: {
cutoverId: 'edge-router-cutover',
generation: 1,
expectedAuthorizationDigest: '1'.repeat(64),
},
};
}
function observation(serviceName, active, observedAtMs) {
const payload = {
managerKind: 'systemd',
serviceName,
fragmentPath: `/etc/systemd/system/${serviceName}.service`,
loadState: 'loaded',
activeState: active ? 'active' : 'inactive',
subState: active ? 'running' : 'dead',
enabledState: 'enabled',
mainPid: active ? 3101 : 0,
observedAtMs,
};
return {
...payload,
observationDigest: localServiceManagerRollbackObservationDigest(payload),
};
}
function outcome() {
const payload = {
schema: 'qinglong3-local-service-manager-legacy-start-outcome',
schemaVersion: 1,
state: 'legacy_running',
cutoverId: 'edge-router-cutover',
profile: 'edge',
instanceId: 'edge-router-1',
generation: 1,
activationDigest: '2'.repeat(64),
managerKind: 'systemd',
preparationDigest: '3'.repeat(64),
authorizationDigest: '4'.repeat(64),
barrierDigest: '5'.repeat(64),
legacyDescriptorDigest: '6'.repeat(64),
targetDescriptorDigest: '7'.repeat(64),
mutationDisposition: 'response-loss-inspected',
manualReason: null,
legacyObservation: observation('qinglong', true, 100),
targetObservation: observation('qinglong3', false, 101),
completedAtMs: 102,
};
return {
...payload,
outcomeDigest: localServiceManagerLegacyStartOutcomeDigest(payload),
};
}
test('normalizes exact systemd and OpenRC legacy rollback bridge commands', () => {
assert.equal(
normalizeLocalServiceManagerLegacyRollbackBridgeCommand(command()).options
.manager.kind,
'systemd',
);
assert.equal(
normalizeLocalServiceManagerLegacyRollbackBridgeCommand(command('openrc'))
.options.manager.kind,
'openrc',
);
});
test('rejects shell surface, path drift and unbound root acknowledgement', () => {
assert.throws(
() =>
normalizeLocalServiceManagerLegacyRollbackBridgeCommand({
...command(),
shell: 'systemctl start qinglong',
}),
/shape is invalid/,
);
assert.throws(
() =>
normalizeLocalServiceManagerLegacyRollbackBridgeCommand({
...command(),
options: { ...command().options, deploymentRoot: '/var/lib/../tmp' },
}),
/supervisor-safe absolute path/,
);
assert.throws(
() =>
normalizeLocalServiceManagerLegacyRollbackBridgeCommand({
...command(),
options: { ...command().options, allowRootController: false },
}),
/command is invalid/,
);
});
test('binds both service observations and all rollback outcome digests', () => {
const value = outcome();
const normalized = normalizeLocalServiceManagerLegacyStartOutcome(value);
assert.equal(normalized.state, 'legacy_running');
assert.equal(normalized.legacyObservation.serviceName, 'qinglong');
assert.equal(normalized.targetObservation.serviceName, 'qinglong3');
assert.throws(
() =>
normalizeLocalServiceManagerLegacyStartOutcome({
...value,
targetDescriptorDigest: '8'.repeat(64),
}),
/outcome drifted/,
);
assert.throws(
() =>
normalizeLocalServiceManagerLegacyStartOutcome({
...value,
legacyObservation: value.targetObservation,
}),
/outcome drifted/,
);
});