Files
qinglong/packages/ql3-local-owner-cli/test/serviceManagerIntent.test.cjs
T

275 lines
8.4 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
consumeLocalServiceManagerOutcome,
prepareLocalServiceManagerIntent,
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
const {
localServiceManagerObservationDigest,
localServiceManagerOutcomeDigest,
} = require('../dist/deployment/service-manager/serviceOutcomeContract.js');
const {
LocalDeploymentConfigurationError,
} = require('../dist/deployment/foundation/contract.js');
const {
advanceLocalCutoverInstanceHead,
claimLocalCutoverInstance,
} = require('../dist/deployment/cutover/instanceLineage.js');
const roots = [];
afterEach(() => {
for (const root of roots.splice(0)) {
fs.rmSync(root, { force: true, recursive: true });
}
});
function fixture() {
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-service-intent-')),
);
roots.push(root);
fs.chmodSync(root, 0o700);
const service = path.join(root, 'service');
fs.mkdirSync(service, { mode: 0o700 });
const application = `${JSON.stringify(
{
schema: 'qinglong/local-application-process@v2',
instanceId: 'edge-router-1',
profile: 'edge',
storage: { mode: 'fresh' },
},
null,
2,
)}\n`;
fs.writeFileSync(path.join(root, 'local-application.json'), application, {
mode: 0o600,
});
fs.writeFileSync(
path.join(service, 'qinglong3.service'),
'[Service]\nExecStart=/usr/bin/node /opt/qinglong3/app.js\n',
{ mode: 0o600 },
);
return { root, service };
}
function prepareCommand(root) {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.intent.prepare',
options: {
deploymentRoot: root,
allowRootService: process.getuid() === 0,
},
request: {
actionId: '123e4567-e89b-42d3-a456-426614174011',
action: 'install-enable-start',
serviceKind: 'systemd',
lineage: { mode: 'fresh' },
requestedAtMs: 1786416000100,
},
};
}
function adoptedHead(root) {
const previousRecordDigest = 'b'.repeat(64);
const identity = {
options: { deploymentRoot: root },
request: {
cutoverId: 'cutover-edge-router-1',
profile: 'edge',
instanceId: 'edge-router-1',
expectedActivationDigest: 'a'.repeat(64),
requestedAtMs: 1786416000000,
},
};
claimLocalCutoverInstance(identity, process.getuid(), 'c'.repeat(64));
advanceLocalCutoverInstanceHead(
identity,
process.getuid(),
'legacy_stopped',
0,
previousRecordDigest,
);
return { identity, previousRecordDigest };
}
function adoptedApplication(root, identity, previousRecordDigest) {
const material = {
schema: 'qinglong/local-application-process@v3',
instanceId: identity.request.instanceId,
profile: identity.request.profile,
storage: {
mode: 'adopted',
sourcePath: path.join(root, 'legacy.sqlite'),
targetPath: path.join(root, 'target.sqlite'),
recoveryPath: path.join(root, 'recovery.sqlite'),
manifestPath: path.join(root, 'manifest.json'),
activationPath: path.join(root, 'activation.json'),
expectedActivationDigest: identity.request.expectedActivationDigest,
},
runtime: {},
pluginPackages: {},
ai: { deployment: 'excluded' },
cutover: {
cutoverId: identity.request.cutoverId,
commitmentPath: path.join(root, 'legacy-stopped.json'),
expectedCommitmentDigest: previousRecordDigest,
},
};
fs.writeFileSync(
path.join(root, 'local-application.json'),
`${JSON.stringify(material)}\n`,
{ mode: 0o600 },
);
}
test('publishes an exact Owner intent and verifies a bound bridge outcome', () => {
const { root } = fixture();
const command = prepareCommand(root);
const prepared = prepareLocalServiceManagerIntent(command);
assert.equal(prepared.status, 'prepared');
assert.equal(fs.statSync(prepared.intentPath).mode & 0o777, 0o600);
assert.equal(prepareLocalServiceManagerIntent(command).status, 'existing');
const intent = JSON.parse(fs.readFileSync(prepared.intentPath, 'utf8'));
assert.equal(intent.profile, 'edge');
assert.equal(intent.instanceId, 'edge-router-1');
assert.equal(intent.service.uid, process.getuid());
assert.equal(intent.service.gid, process.getgid());
assert.match(intent.deployment.applicationConfigSha256, /^[0-9a-f]{64}$/);
assert.match(intent.descriptor.sha256, /^[0-9a-f]{64}$/);
const observationPayload = {
managerKind: 'systemd',
serviceName: 'qinglong3',
fragmentPath: '/etc/systemd/system/qinglong3.service',
loadState: 'loaded',
activeState: 'active',
subState: 'running',
enabledState: 'enabled',
mainPid: 4123,
observedAtMs: 1786416000200,
};
const observation = {
...observationPayload,
observationDigest: localServiceManagerObservationDigest(observationPayload),
};
const outcomePayload = {
schemaVersion: 1,
kind: 'qinglong3-local-service-manager-outcome',
actionId: intent.actionId,
action: intent.action,
intentDigest: intent.intentDigest,
descriptorDigest: intent.descriptor.sha256,
state: 'active',
mutationDisposition: 'executed',
manualReason: null,
observation,
completedAtMs: 1786416000300,
};
const outcome = {
...outcomePayload,
outcomeDigest: localServiceManagerOutcomeDigest(outcomePayload),
};
fs.writeFileSync(prepared.outcomePath, `${JSON.stringify(outcome)}\n`, {
mode: 0o600,
});
assert.deepEqual(
consumeLocalServiceManagerOutcome({
schemaVersion: 1,
operation: 'local.deployment.service-manager.outcome.consume',
options: command.options,
request: {
actionId: prepared.actionId,
expectedIntentDigest: prepared.intentDigest,
},
}),
{
schemaVersion: 1,
operation: 'local.deployment.service-manager.outcome.consume',
status: 'verified',
actionId: prepared.actionId,
state: 'active',
outcomeDigest: outcome.outcomeDigest,
observationDigest: observation.observationDigest,
},
);
});
test('fails closed when current descriptor material drifts after intent publication', () => {
const { root, service } = fixture();
const command = prepareCommand(root);
const prepared = prepareLocalServiceManagerIntent(command);
fs.writeFileSync(
path.join(service, 'qinglong3.service'),
'[Service]\nExecStart=/usr/bin/false\n',
{ mode: 0o600 },
);
assert.throws(
() => prepareLocalServiceManagerIntent(command),
LocalDeploymentConfigurationError,
);
assert.equal(fs.existsSync(prepared.intentPath), true);
});
test('binds an adopted first start to the current legacy-stopped instance head', () => {
const { root } = fixture();
const { identity, previousRecordDigest } = adoptedHead(root);
adoptedApplication(root, identity, previousRecordDigest);
const command = prepareCommand(root);
command.request.lineage = {
mode: 'adopted',
cutoverId: identity.request.cutoverId,
generation: 1,
expectedActivationDigest: identity.request.expectedActivationDigest,
previousRecordDigest,
};
assert.equal(prepareLocalServiceManagerIntent(command).status, 'prepared');
advanceLocalCutoverInstanceHead(
identity,
process.getuid(),
'target_active',
1,
'e'.repeat(64),
);
const stale = structuredClone(command);
stale.request.actionId = '123e4567-e89b-42d3-a456-426614174012';
assert.throws(
() => prepareLocalServiceManagerIntent(stale),
/lost the instance lineage compare-and-swap/,
);
});
test('does not allow fresh service intent to bypass an existing cutover head', () => {
const { root } = fixture();
adoptedHead(root);
assert.throws(
() => prepareLocalServiceManagerIntent(prepareCommand(root)),
/fresh service intent cannot bypass an instance lineage head/,
);
});
test('rejects adopted lineage when the application remains a fresh v2 deployment', () => {
const { root } = fixture();
const { identity, previousRecordDigest } = adoptedHead(root);
const command = prepareCommand(root);
command.request.lineage = {
mode: 'adopted',
cutoverId: identity.request.cutoverId,
generation: 1,
expectedActivationDigest: identity.request.expectedActivationDigest,
previousRecordDigest,
};
assert.throws(
() => prepareLocalServiceManagerIntent(command),
/service intent does not match adopted application binding/,
);
});