feat(ql3): capture post-cutover reconciliation evidence

This commit is contained in:
whyour
2026-09-01 11:46:40 +08:00
parent 45879ef0a7
commit 367ec49e4a
11 changed files with 371 additions and 16 deletions
+81 -1
View File
@@ -166,7 +166,8 @@ function adapters(overrides = {}, variant = 'headless') {
test('materializes and offline-audits one closed two-image trial kit', (t) => {
const paths = fixture(t);
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v8');
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v9');
assert.equal(manifest.schemaVersion, 10);
assert.equal(manifest.variant, 'headless');
assert.equal(manifest.sourceRevision, revision);
assert.equal(manifest.architecture, 'arm64');
@@ -278,6 +279,70 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
/QingLong Local Alpha target-stop evidence:/,
);
assert.match(cutoverRehearsalContents, /tr -d '\\n'/);
assert.match(cutoverRehearsalContents, /--capture-after-write/);
assert.match(cutoverRehearsalContents, /"operation":"task\.put"/);
assert.match(
cutoverRehearsalContents,
/local\.deployment\.reconciliation\.capture\.prepare/,
);
assert.match(
cutoverRehearsalContents,
/local\.deployment\.reconciliation\.capture\.commit/,
);
assert.match(
cutoverRehearsalContents,
/local\.deployment\.reconciliation\.capture\.verify/,
);
assert.match(
cutoverRehearsalContents,
/qinglong\/local-alpha-upgrade-reconciliation-capture-summary@v1/,
);
const postWriteTemplate = cutoverRehearsalContents.match(
/cat >"\$rehearsal_root\/commands\/post-cutover-task\.json" <<EOF\n([^\n]+)\nEOF/,
);
assert.ok(postWriteTemplate, 'post-cutover Task command template is missing');
const postWriteCommand = JSON.parse(
postWriteTemplate[1]
.replaceAll('$write_ms', '1')
.replace(/\$[A-Za-z_][A-Za-z0-9_]*/g, 'fixture'),
);
assert.equal(postWriteCommand.operation, 'task.put');
assert.equal(postWriteCommand.request.taskId, 'alpha-post-cutover-write');
assert.equal(postWriteCommand.request.expectedRevision, null);
const capturePrepareTemplate = cutoverRehearsalContents.match(
/cat >"\$rehearsal_root\/commands\/reconciliation-capture-prepare\.json" <<EOF\n([^\n]+)\nEOF/,
);
assert.ok(
capturePrepareTemplate,
'reconciliation capture prepare command template is missing',
);
const capturePrepareCommand = JSON.parse(
capturePrepareTemplate[1]
.replaceAll('$allow_root_service', 'false')
.replaceAll('$capture_prepare_ms', '1')
.replace(/\$[A-Za-z_][A-Za-z0-9_]*/g, 'fixture'),
);
assert.equal(
capturePrepareCommand.operation,
'local.deployment.reconciliation.capture.prepare',
);
assert.deepEqual(Object.keys(capturePrepareCommand.request).sort(), [
'activationPath',
'applicationConfigPath',
'captureId',
'cutoverId',
'expectedActivationDigest',
'expectedHeadDigest',
'expectedStoppedRecordDigest',
'generation',
'instanceId',
'legacySourcePath',
'preparedAtMs',
'profile',
'recoveryPath',
'stoppedAuthority',
'targetDatabasePath',
]);
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
assert.equal(report.compatible, true);
assert.equal(report.sourceRevision, revision);
@@ -329,6 +394,7 @@ test('materializes a distinct loopback Console trial kit without widening the he
assert.equal(verification.gates.legacyUpgradeReadiness, 'passed');
assert.equal(verification.gates.legacyUpgradeStage, 'passed');
assert.equal(verification.gates.legacyUpgradeCutover, 'passed');
assert.equal(verification.gates.legacyUpgradeReconciliationCapture, 'passed');
const quickstartContents = fs.readFileSync(
path.join(paths.outputRoot, 'quickstart.sh'),
'utf8',
@@ -784,6 +850,20 @@ test('create rejects verification detached from the reviewed workflow', (t) => {
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('create rejects verification without the exact reconciliation capture gate', (t) => {
const paths = fixture(t);
const evidence = JSON.parse(
fs.readFileSync(paths.verificationEvidence, 'utf8'),
);
delete evidence.gates.legacyUpgradeReconciliationCapture;
fs.writeFileSync(paths.verificationEvidence, `${JSON.stringify(evidence)}\n`);
assert.throws(
() => createLocalAlphaTrialKit(createOptions(paths), adapters()),
/verification evidence is incompatible/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('verification recorder rejects non-milestone workflow provenance', (t) => {
const paths = fixture(t);
const output = path.join(paths.fixtureRoot, 'unreviewed-verification.json');
@@ -165,3 +165,42 @@ test('rejects Alpha evidence recorded before all native gates', () => {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});
test('rejects removal of the exact post-write reconciliation capture gate', () => {
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-reconciliation-ci-audit-'),
);
try {
fs.mkdirSync(path.join(temporaryRoot, 'deploy/containers'), {
recursive: true,
});
fs.cpSync(
path.join(root, 'deploy/containers/ql3-local-operator'),
path.join(temporaryRoot, 'deploy/containers/ql3-local-operator'),
{ recursive: true },
);
fs.mkdirSync(path.join(temporaryRoot, '.github/workflows'), {
recursive: true,
});
const workflow = fs
.readFileSync(path.join(root, '.github/workflows/ql3-ci.yml'), 'utf8')
.replace('--capture-after-write', '--capture-gate-removed');
fs.writeFileSync(
path.join(temporaryRoot, '.github/workflows/ql3-ci.yml'),
workflow,
);
fs.copyFileSync(
path.join(root, 'ql3-release.json'),
path.join(temporaryRoot, 'ql3-release.json'),
);
const report = auditLocalOperatorImageContract(temporaryRoot);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'LOCAL_OPERATOR_CI_CONTRACT_DRIFT',
),
);
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});