mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
feat(ql3): rehearse reviewed automation rollback
This commit is contained in:
@@ -80,8 +80,8 @@ function writeMilestone(directory, manifest) {
|
||||
|
||||
function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
return {
|
||||
schemaVersion: 5,
|
||||
schema: 'qinglong/alpha-local-milestone@v5',
|
||||
schemaVersion: 6,
|
||||
schema: 'qinglong/alpha-local-milestone@v6',
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
variant,
|
||||
@@ -104,6 +104,7 @@ function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
upgradeReadinessSha256: digest('b'),
|
||||
upgradeRehearsalSha256: digest('d'),
|
||||
upgradeCutoverRehearsalSha256: digest('f'),
|
||||
upgradeReconciliationRehearsalSha256: digest('7'),
|
||||
},
|
||||
arm64: {
|
||||
artifactName: `ql3-alpha-${revision}-local-${variant}-arm64`,
|
||||
@@ -120,6 +121,7 @@ function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
upgradeReadinessSha256: digest('c'),
|
||||
upgradeRehearsalSha256: digest('e'),
|
||||
upgradeCutoverRehearsalSha256: digest('0'),
|
||||
upgradeReconciliationRehearsalSha256: digest('8'),
|
||||
},
|
||||
},
|
||||
readme: null,
|
||||
|
||||
@@ -206,7 +206,7 @@ function finalizeOptions(paths) {
|
||||
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
|
||||
const paths = fixture(t);
|
||||
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v5');
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v6');
|
||||
assert.match(
|
||||
manifest.artifacts.amd64.upgradeReadinessSha256,
|
||||
/^sha256:[0-9a-f]{64}$/,
|
||||
@@ -219,6 +219,10 @@ test('finalizes two exact native trial kits into one closed milestone index', (t
|
||||
manifest.artifacts.amd64.upgradeCutoverRehearsalSha256,
|
||||
/^sha256:[0-9a-f]{64}$/,
|
||||
);
|
||||
assert.match(
|
||||
manifest.artifacts.amd64.upgradeReconciliationRehearsalSha256,
|
||||
/^sha256:[0-9a-f]{64}$/,
|
||||
);
|
||||
assert.equal(manifest.variant, 'headless');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
||||
@@ -247,7 +251,7 @@ test('finalizes two exact native trial kits into one closed milestone index', (t
|
||||
assert.equal(report.workflowRunId, runId);
|
||||
assert.equal(report.workflowRunAttempt, runAttempt);
|
||||
assert.equal(report.variant, 'headless');
|
||||
assert.equal(report.schema, 'qinglong/alpha-local-milestone-audit@v5');
|
||||
assert.equal(report.schema, 'qinglong/alpha-local-milestone-audit@v6');
|
||||
});
|
||||
|
||||
test('finalizes Console trial kits as a separately named milestone', (t) => {
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
automationFixture,
|
||||
parseArguments,
|
||||
reviewFixture,
|
||||
} = require('../../scripts/ql3-local-alpha-reconciliation-decision-fixture.cjs');
|
||||
|
||||
const DOMAINS = [
|
||||
'schema_lineage',
|
||||
'automation',
|
||||
'secret_and_config',
|
||||
'run_history',
|
||||
'plugin_package',
|
||||
'ai_and_tool',
|
||||
'identity_policy_audit',
|
||||
'unknown',
|
||||
];
|
||||
const PLAN_DIGEST = '1'.repeat(64);
|
||||
const PREPARATION_DIGEST = '2'.repeat(64);
|
||||
const AUTOMATION_PLAN_DIGEST = '3'.repeat(64);
|
||||
const INVENTORY_DIGEST = '4'.repeat(64);
|
||||
const REVIEW_ID = '019f8680-143d-4000-8000-000000000301';
|
||||
const AUTOMATION_ID = '019f8680-143d-4000-8000-000000000461';
|
||||
const DECISION_ID = '019f8680-143d-7000-8000-000000000471';
|
||||
|
||||
function fixture(t) {
|
||||
const root = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-reconciliation-decisions-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(root, 'diagnostics'), { mode: 0o700 });
|
||||
let recordCount = 0;
|
||||
for (const database of ['legacy', 'target']) {
|
||||
for (const domain of DOMAINS) {
|
||||
for (const factKind of ['schema_object', 'table']) {
|
||||
const records = [];
|
||||
if (
|
||||
database === 'legacy' &&
|
||||
domain === 'automation' &&
|
||||
factKind === 'table'
|
||||
) {
|
||||
records.push({
|
||||
schema: 'qinglong3-local-reconciliation-diagnostic-fact',
|
||||
schemaVersion: 1,
|
||||
ordinal: 1,
|
||||
database,
|
||||
domain,
|
||||
factKind,
|
||||
objectType: 'table',
|
||||
name: 'Crontabs',
|
||||
tableName: 'Crontabs',
|
||||
rowCount: '1',
|
||||
decisionRequirement: 'required',
|
||||
reason: 'reviewable_fact',
|
||||
factDigest: 'a'.repeat(64),
|
||||
});
|
||||
}
|
||||
if (
|
||||
database === 'legacy' &&
|
||||
domain === 'unknown' &&
|
||||
factKind === 'table'
|
||||
) {
|
||||
records.push({
|
||||
schema: 'qinglong3-local-reconciliation-diagnostic-fact',
|
||||
schemaVersion: 1,
|
||||
ordinal: 1,
|
||||
database,
|
||||
domain,
|
||||
factKind,
|
||||
objectType: 'table',
|
||||
name: 'PluginOwnedState',
|
||||
tableName: 'PluginOwnedState',
|
||||
rowCount: null,
|
||||
decisionRequirement: 'blocked',
|
||||
reason: 'unknown_schema',
|
||||
factDigest: 'b'.repeat(64),
|
||||
});
|
||||
}
|
||||
if (
|
||||
database === 'legacy' &&
|
||||
domain === 'run_history' &&
|
||||
factKind === 'table'
|
||||
) {
|
||||
records.push({
|
||||
schema: 'qinglong3-local-reconciliation-diagnostic-fact',
|
||||
schemaVersion: 1,
|
||||
ordinal: 1,
|
||||
database,
|
||||
domain,
|
||||
factKind,
|
||||
objectType: 'table',
|
||||
name: 'ExecutionHistory',
|
||||
tableName: 'ExecutionHistory',
|
||||
rowCount: '1',
|
||||
decisionRequirement: 'required',
|
||||
reason: 'historical_preservation_required',
|
||||
factDigest: 'd'.repeat(64),
|
||||
});
|
||||
}
|
||||
if (
|
||||
database === 'target' &&
|
||||
domain === 'automation' &&
|
||||
factKind === 'table'
|
||||
) {
|
||||
records.push({
|
||||
schema: 'qinglong3-local-reconciliation-diagnostic-fact',
|
||||
schemaVersion: 1,
|
||||
ordinal: 1,
|
||||
database,
|
||||
domain,
|
||||
factKind,
|
||||
objectType: 'table',
|
||||
name: 'QingLong3TaskDefinitions',
|
||||
tableName: 'QingLong3TaskDefinitions',
|
||||
rowCount: '1',
|
||||
decisionRequirement: 'required',
|
||||
reason: 'reviewable_fact',
|
||||
factDigest: 'c'.repeat(64),
|
||||
});
|
||||
}
|
||||
recordCount += records.length;
|
||||
fs.writeFileSync(
|
||||
path.join(
|
||||
root,
|
||||
'diagnostics',
|
||||
`${database}-${domain}-${factKind}-0.json`,
|
||||
),
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong3-local-reconciliation-diagnostic-page',
|
||||
schemaVersion: 1,
|
||||
state: 'reconciliation_review_prepared',
|
||||
reviewId: REVIEW_ID,
|
||||
planId: '019f8680-143d-4000-8000-000000000201',
|
||||
planDigest: PLAN_DIGEST,
|
||||
preparationDigest: PREPARATION_DIGEST,
|
||||
bundleDigest: 'd'.repeat(64),
|
||||
bundleFingerprintDigest: 'e'.repeat(64),
|
||||
database,
|
||||
domain,
|
||||
factKind,
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
recordCount: records.length,
|
||||
complete: true,
|
||||
nextOffset: null,
|
||||
records,
|
||||
pageDigest: 'f'.repeat(64),
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'summary.json'),
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/local-alpha-reconciliation-rehearsal-summary@v1',
|
||||
status: 'operator_decision_required',
|
||||
profile: 'edge',
|
||||
plan: {
|
||||
planId: '019f8680-143d-4000-8000-000000000201',
|
||||
planDigest: PLAN_DIGEST,
|
||||
},
|
||||
review: {
|
||||
reviewId: REVIEW_ID,
|
||||
preparationDigest: PREPARATION_DIGEST,
|
||||
diagnosticPages: 32,
|
||||
diagnosticRecords: recordCount,
|
||||
},
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
return { root, recordCount };
|
||||
}
|
||||
|
||||
test('builds explicit synthetic review decisions without approving blocked facts', (t) => {
|
||||
const state = fixture(t);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.review.ndjson`,
|
||||
);
|
||||
t.after(() => fs.rmSync(output, { force: true }));
|
||||
const report = reviewFixture(state.root, output);
|
||||
assert.equal(report.adoptedAutomationTables, 1);
|
||||
const records = fs
|
||||
.readFileSync(output, 'utf8')
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line));
|
||||
assert.equal(
|
||||
records[0].kind,
|
||||
'qinglong3-local-reconciliation-review-decision-header',
|
||||
);
|
||||
const crontabs = records.find(
|
||||
(record) => record.factDigest === 'a'.repeat(64),
|
||||
);
|
||||
const unknown = records.find(
|
||||
(record) => record.factDigest === 'b'.repeat(64),
|
||||
);
|
||||
const target = records.find((record) => record.factDigest === 'c'.repeat(64));
|
||||
const legacyRunHistory = records.find(
|
||||
(record) => record.factDigest === 'd'.repeat(64),
|
||||
);
|
||||
assert.deepEqual(
|
||||
[crontabs.disposition, crontabs.reason],
|
||||
['adopt_legacy', 'prefer_legacy'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
[unknown.disposition, unknown.reason],
|
||||
['manual_external', 'external_recovery_required'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
[target.disposition, target.reason],
|
||||
['retain_target', 'preserve_target'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
[legacyRunHistory.disposition, legacyRunHistory.reason],
|
||||
['manual_external', 'external_recovery_required'],
|
||||
);
|
||||
assert.equal(fs.statSync(output).mode & 0o777, 0o600);
|
||||
});
|
||||
|
||||
test('review fixture fails closed without the exact Legacy Crontabs fact', (t) => {
|
||||
const state = fixture(t);
|
||||
const pagePath = path.join(
|
||||
state.root,
|
||||
'diagnostics/legacy-automation-table-0.json',
|
||||
);
|
||||
const page = JSON.parse(fs.readFileSync(pagePath, 'utf8'));
|
||||
page.records[0].tableName = 'CronLikeState';
|
||||
fs.writeFileSync(pagePath, `${JSON.stringify(page)}\n`);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.invalid.ndjson`,
|
||||
);
|
||||
assert.throws(
|
||||
() => reviewFixture(state.root, output),
|
||||
/exactly one reviewable Legacy Crontabs table/,
|
||||
);
|
||||
assert.equal(fs.existsSync(output), false);
|
||||
});
|
||||
|
||||
function automationState(t) {
|
||||
const state = fixture(t);
|
||||
fs.writeFileSync(
|
||||
path.join(state.root, 'summary.json'),
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/local-alpha-reconciliation-rehearsal-summary@v1',
|
||||
status: 'automation_decision_required',
|
||||
profile: 'edge',
|
||||
automation: {
|
||||
automationId: AUTOMATION_ID,
|
||||
automationPlanDigest: AUTOMATION_PLAN_DIGEST,
|
||||
rowCount: 1,
|
||||
eligibleCount: 1,
|
||||
conflictCount: 0,
|
||||
},
|
||||
decision: { decisionId: DECISION_ID },
|
||||
})}\n`,
|
||||
);
|
||||
const directory = path.join(state.root, 'automation', AUTOMATION_ID);
|
||||
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(
|
||||
path.join(directory, 'receipt.json'),
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong3-local-reconciliation-automation-plan-receipt',
|
||||
schemaVersion: 1,
|
||||
automationId: AUTOMATION_ID,
|
||||
automationPlanDigest: AUTOMATION_PLAN_DIGEST,
|
||||
rowCount: 1,
|
||||
eligibleCount: 1,
|
||||
manualCount: 0,
|
||||
conflictCount: 0,
|
||||
legacyInventoryDigest: INVENTORY_DIGEST,
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(directory, 'plan.ndjson'),
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-reconciliation-automation-plan-row',
|
||||
rowOrdinal: 1,
|
||||
sourceDigest: '5'.repeat(64),
|
||||
requirement: 'review_adopt',
|
||||
target: { state: 'absent' },
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
test('builds one reviewed-lossless Automation row decision', (t) => {
|
||||
const state = automationState(t);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.automation.ndjson`,
|
||||
);
|
||||
t.after(() => fs.rmSync(output, { force: true }));
|
||||
const report = automationFixture(state.root, output);
|
||||
assert.equal(report.decisionCount, 1);
|
||||
const records = fs
|
||||
.readFileSync(output, 'utf8')
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line));
|
||||
assert.equal(records[0].decisionId, DECISION_ID);
|
||||
assert.deepEqual(records[1].decision, {
|
||||
rowOrdinal: 1,
|
||||
sourceDigest: '5'.repeat(64),
|
||||
disposition: 'adopt',
|
||||
reason: 'reviewed_lossless',
|
||||
});
|
||||
});
|
||||
|
||||
test('Automation fixture rejects a conflict before writing decisions', (t) => {
|
||||
const state = automationState(t);
|
||||
const summaryPath = path.join(state.root, 'summary.json');
|
||||
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
||||
summary.automation.eligibleCount = 0;
|
||||
summary.automation.conflictCount = 1;
|
||||
fs.writeFileSync(summaryPath, `${JSON.stringify(summary)}\n`);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.conflict.ndjson`,
|
||||
);
|
||||
assert.throws(
|
||||
() => automationFixture(state.root, output),
|
||||
/one conflict-free eligible Automation row/,
|
||||
);
|
||||
assert.equal(fs.existsSync(output), false);
|
||||
});
|
||||
|
||||
test('CLI grammar is exact', () => {
|
||||
assert.throws(
|
||||
() => parseArguments(['--mode=review', '--output=/tmp/review.ndjson']),
|
||||
/fixture arguments are invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseArguments([
|
||||
'--mode=completion',
|
||||
'--reconciliation-root=/tmp/root',
|
||||
'--output=/tmp/review.ndjson',
|
||||
]),
|
||||
/fixture arguments are invalid/,
|
||||
);
|
||||
});
|
||||
@@ -166,8 +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@v9');
|
||||
assert.equal(manifest.schemaVersion, 10);
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v10');
|
||||
assert.equal(manifest.schemaVersion, 11);
|
||||
assert.equal(manifest.variant, 'headless');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.equal(manifest.architecture, 'arm64');
|
||||
@@ -182,6 +182,10 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
manifest.upgradeCutoverRehearsal.file,
|
||||
'upgrade-cutover-rehearsal.sh',
|
||||
);
|
||||
assert.equal(
|
||||
manifest.upgradeReconciliationRehearsal.file,
|
||||
'reconciliation-rehearsal.sh',
|
||||
);
|
||||
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
|
||||
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
|
||||
assert.equal(syntax.status, 0, syntax.stderr);
|
||||
@@ -343,6 +347,78 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
'stoppedAuthority',
|
||||
'targetDatabasePath',
|
||||
]);
|
||||
const reconciliationRehearsal = path.join(
|
||||
paths.outputRoot,
|
||||
'reconciliation-rehearsal.sh',
|
||||
);
|
||||
const reconciliationSyntax = spawnSync(
|
||||
'sh',
|
||||
['-n', reconciliationRehearsal],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
assert.equal(reconciliationSyntax.status, 0, reconciliationSyntax.stderr);
|
||||
const reconciliationContents = fs.readFileSync(
|
||||
reconciliationRehearsal,
|
||||
'utf8',
|
||||
);
|
||||
assert.match(reconciliationContents, /VARIANT='headless'/);
|
||||
for (const operation of [
|
||||
'reconciliation.plan.prepare',
|
||||
'reconciliation.review.diagnostics',
|
||||
'reconciliation.review.commit',
|
||||
'reconciliation.application.commit',
|
||||
'reconciliation.automation.decision.commit',
|
||||
'reconciliation.automation.apply',
|
||||
'reconciliation.automation.apply.rollback',
|
||||
]) {
|
||||
assert.match(reconciliationContents, new RegExp(operation));
|
||||
}
|
||||
assert.match(reconciliationContents, /--memory 128m --memory-swap 128m/);
|
||||
assert.match(reconciliationContents, /--network none/);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/\[ "\$\(stat -c %a "\$decision_parent"\)" = 700 \]/,
|
||||
);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/type=bind,src=\$decision_parent,dst=\$decision_parent,readonly/,
|
||||
);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/type=bind,src=\$legacy_root,dst=\$legacy_root,readonly/,
|
||||
);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/apply-rollback edge\|standalone .* \/absolute\/legacy-root/,
|
||||
);
|
||||
assert.match(reconciliationContents, /result_stage=.*\.\$result_file\.\$\$/);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/\[ -e "\$command_root\/automation-apply\.json" \]/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
reconciliationContents,
|
||||
/type=bind,src=\$input_file,dst=\$input_file,readonly/,
|
||||
);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/\$decision_label parent must contain only the selected decision file/,
|
||||
);
|
||||
assert.equal(
|
||||
(reconciliationContents.match(/"authorizationLifetimeMs":60000/g) || [])
|
||||
.length,
|
||||
2,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
reconciliationContents,
|
||||
/"authorizationLifetimeMs":1800000/,
|
||||
);
|
||||
assert.match(reconciliationContents, /! -path "\$decision_file"/);
|
||||
assert.match(reconciliationContents, /automaticDecision":"not_authorized/);
|
||||
assert.match(reconciliationContents, /automaticRowDecision":"not_authorized/);
|
||||
assert.match(reconciliationContents, /"completion":"not_attempted"/);
|
||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||
assert.equal(report.compatible, true);
|
||||
assert.equal(report.sourceRevision, revision);
|
||||
@@ -356,6 +432,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
'qinglong3-local-operator.cdx.json',
|
||||
'qinglong3-local-trial-kit-arm64.docker.tar',
|
||||
'quickstart.sh',
|
||||
'reconciliation-rehearsal.sh',
|
||||
'upgrade-cutover-rehearsal.sh',
|
||||
'upgrade-readiness.sh',
|
||||
'upgrade-rehearsal.sh',
|
||||
@@ -395,6 +472,10 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
||||
assert.equal(verification.gates.legacyUpgradeStage, 'passed');
|
||||
assert.equal(verification.gates.legacyUpgradeCutover, 'passed');
|
||||
assert.equal(verification.gates.legacyUpgradeReconciliationCapture, 'passed');
|
||||
assert.equal(
|
||||
verification.gates.legacyUpgradeReconciliationAutomationRollback,
|
||||
'passed',
|
||||
);
|
||||
const quickstartContents = fs.readFileSync(
|
||||
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||
'utf8',
|
||||
@@ -429,6 +510,7 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
||||
'qinglong3-local-console-trial-kit-arm64.docker.tar',
|
||||
'qinglong3-local-operator.cdx.json',
|
||||
'quickstart.sh',
|
||||
'reconciliation-rehearsal.sh',
|
||||
'upgrade-cutover-rehearsal.sh',
|
||||
'upgrade-readiness.sh',
|
||||
'upgrade-rehearsal.sh',
|
||||
@@ -467,6 +549,7 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
||||
'upgrade-readiness',
|
||||
'upgrade-rehearsal',
|
||||
'upgrade-cutover-rehearsal',
|
||||
'upgrade-reconciliation-rehearsal',
|
||||
'sbom',
|
||||
'verification',
|
||||
]) {
|
||||
@@ -503,6 +586,11 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
||||
path.join(paths.outputRoot, 'upgrade-cutover-rehearsal.sh'),
|
||||
'# drift\n',
|
||||
);
|
||||
} else if (mutation === 'upgrade-reconciliation-rehearsal') {
|
||||
fs.appendFileSync(
|
||||
path.join(paths.outputRoot, 'reconciliation-rehearsal.sh'),
|
||||
'# drift\n',
|
||||
);
|
||||
} else if (mutation === 'sbom') {
|
||||
fs.copyFileSync(
|
||||
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
||||
@@ -544,6 +632,7 @@ test('offline audit rejects a rehashed non-canonical quickstart', (t) => {
|
||||
'upgrade-readiness.sh',
|
||||
'upgrade-rehearsal.sh',
|
||||
'upgrade-cutover-rehearsal.sh',
|
||||
'reconciliation-rehearsal.sh',
|
||||
'README.md',
|
||||
'manifest.json',
|
||||
];
|
||||
@@ -652,7 +741,7 @@ exit 1
|
||||
assert.doesNotMatch(calls, /--network (?!none)/);
|
||||
});
|
||||
|
||||
test('generated upgrade readiness drives two read-only legacy inspections', (t) => {
|
||||
test('generated upgrade readiness retries one transient read-only inspection', (t) => {
|
||||
const paths = fixture(t);
|
||||
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||
const fakeBin = path.join(paths.fixtureRoot, 'readiness-fake-bin');
|
||||
@@ -676,9 +765,16 @@ case " $* " in
|
||||
*'/sqlite-inspect.json'*) printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'a'.repeat(
|
||||
64,
|
||||
)}"}}'; exit 0 ;;
|
||||
*'/data-directory-inspect.json'*) printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'b'.repeat(
|
||||
64,
|
||||
)}"}}'; exit 0 ;;
|
||||
*'/data-directory-inspect.json'*)
|
||||
if [ ! -e "$FAKE_DOCKER_DATA_ATTEMPT" ]; then
|
||||
: >"$FAKE_DOCKER_DATA_ATTEMPT"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'b'.repeat(
|
||||
64,
|
||||
)}"}}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
exit 1
|
||||
`,
|
||||
@@ -702,6 +798,10 @@ exit 1
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
FAKE_DOCKER_DATA_ATTEMPT: path.join(
|
||||
paths.fixtureRoot,
|
||||
'readiness-data-attempt',
|
||||
),
|
||||
FAKE_DOCKER_LOG: dockerLog,
|
||||
PATH: `${fakeBin}:${process.env.PATH}`,
|
||||
},
|
||||
@@ -740,6 +840,19 @@ exit 1
|
||||
);
|
||||
assert.match(calls, /--network none/);
|
||||
assert.match(calls, /--memory 128m --memory-swap 128m/);
|
||||
assert.equal((calls.match(/\/sqlite-inspect\.json/g) ?? []).length, 1);
|
||||
assert.equal(
|
||||
(calls.match(/\/data-directory-inspect\.json/g) ?? []).length,
|
||||
2,
|
||||
);
|
||||
assert.equal(
|
||||
fs
|
||||
.readdirSync(path.join(evidenceRoot, 'results'))
|
||||
.some((entry) =>
|
||||
entry.startsWith('.data-directory-inspect.result.json.'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.doesNotMatch(calls, /adoption\.stage|activation\.prepare|cutover/);
|
||||
});
|
||||
|
||||
@@ -864,6 +977,20 @@ test('create rejects verification without the exact reconciliation capture gate'
|
||||
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||
});
|
||||
|
||||
test('create rejects verification without the reviewed Automation rollback gate', (t) => {
|
||||
const paths = fixture(t);
|
||||
const evidence = JSON.parse(
|
||||
fs.readFileSync(paths.verificationEvidence, 'utf8'),
|
||||
);
|
||||
delete evidence.gates.legacyUpgradeReconciliationAutomationRollback;
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user