mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): rehearse cross-domain reconciliation completion
This commit is contained in:
@@ -80,8 +80,8 @@ function writeMilestone(directory, manifest) {
|
||||
|
||||
function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
return {
|
||||
schemaVersion: 6,
|
||||
schema: 'qinglong/alpha-local-milestone@v6',
|
||||
schemaVersion: 7,
|
||||
schema: 'qinglong/alpha-local-milestone@v7',
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
variant,
|
||||
|
||||
@@ -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@v6');
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v7');
|
||||
assert.match(
|
||||
manifest.artifacts.amd64.upgradeReadinessSha256,
|
||||
/^sha256:[0-9a-f]{64}$/,
|
||||
@@ -251,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@v6');
|
||||
assert.equal(report.schema, 'qinglong/alpha-local-milestone-audit@v7');
|
||||
});
|
||||
|
||||
test('finalizes Console trial kits as a separately named milestone', (t) => {
|
||||
|
||||
@@ -8,8 +8,10 @@ const test = require('node:test');
|
||||
|
||||
const {
|
||||
automationFixture,
|
||||
completionReviewFixture,
|
||||
parseArguments,
|
||||
reviewFixture,
|
||||
secretConfigFixture,
|
||||
} = require('../../scripts/ql3-local-alpha-reconciliation-decision-fixture.cjs');
|
||||
|
||||
const DOMAINS = [
|
||||
@@ -29,6 +31,10 @@ 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';
|
||||
const SECRET_CONFIG_ID = '019f8680-143d-4000-8000-000000000491';
|
||||
const SECRET_CONFIG_DECISION_ID = '019f8680-143d-7000-8000-0000000004a1';
|
||||
const SECRET_CONFIG_PLAN_DIGEST = '6'.repeat(64);
|
||||
const SECRET_CONFIG_PREPARATION_DIGEST = '7'.repeat(64);
|
||||
|
||||
function fixture(t) {
|
||||
const root = fs.realpathSync(
|
||||
@@ -248,6 +254,119 @@ test('review fixture fails closed without the exact Legacy Crontabs fact', (t) =
|
||||
assert.equal(fs.existsSync(output), false);
|
||||
});
|
||||
|
||||
function completionReviewState(t) {
|
||||
const state = fixture(t);
|
||||
const unknownPath = path.join(
|
||||
state.root,
|
||||
'diagnostics/legacy-unknown-table-0.json',
|
||||
);
|
||||
const unknown = JSON.parse(fs.readFileSync(unknownPath, 'utf8'));
|
||||
unknown.records = [];
|
||||
unknown.recordCount = 0;
|
||||
fs.writeFileSync(unknownPath, `${JSON.stringify(unknown)}\n`);
|
||||
const additions = [
|
||||
['legacy', 'secret_and_config', '6'],
|
||||
['target', 'secret_and_config', '7'],
|
||||
['target', 'run_history', '8'],
|
||||
];
|
||||
for (const [database, domain, digest] of additions) {
|
||||
const pagePath = path.join(
|
||||
state.root,
|
||||
'diagnostics',
|
||||
`${database}-${domain}-table-0.json`,
|
||||
);
|
||||
const page = JSON.parse(fs.readFileSync(pagePath, 'utf8'));
|
||||
page.records = [
|
||||
{
|
||||
schema: 'qinglong3-local-reconciliation-diagnostic-fact',
|
||||
schemaVersion: 1,
|
||||
ordinal: 1,
|
||||
database,
|
||||
domain,
|
||||
factKind: 'table',
|
||||
objectType: 'table',
|
||||
name:
|
||||
domain === 'secret_and_config'
|
||||
? database === 'legacy'
|
||||
? 'Envs'
|
||||
: 'QingLong3Secrets'
|
||||
: 'QingLong3Runs',
|
||||
tableName:
|
||||
domain === 'secret_and_config'
|
||||
? database === 'legacy'
|
||||
? 'Envs'
|
||||
: 'QingLong3Secrets'
|
||||
: 'QingLong3Runs',
|
||||
rowCount: '1',
|
||||
decisionRequirement: 'required',
|
||||
reason:
|
||||
domain === 'run_history'
|
||||
? 'historical_preservation_required'
|
||||
: 'secret_custody_required',
|
||||
factDigest: digest.repeat(64),
|
||||
},
|
||||
];
|
||||
page.recordCount = 1;
|
||||
fs.writeFileSync(pagePath, `${JSON.stringify(page)}\n`);
|
||||
}
|
||||
const summaryPath = path.join(state.root, 'summary.json');
|
||||
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
||||
summary.review.diagnosticRecords = state.recordCount - 1 + additions.length;
|
||||
fs.writeFileSync(summaryPath, `${JSON.stringify(summary)}\n`);
|
||||
return state;
|
||||
}
|
||||
|
||||
test('builds completion review authority for all adapted domains', (t) => {
|
||||
const state = completionReviewState(t);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.completion-review.ndjson`,
|
||||
);
|
||||
t.after(() => fs.rmSync(output, { force: true }));
|
||||
const report = completionReviewFixture(state.root, output);
|
||||
assert.equal(report.adoptedAutomationTables, 1);
|
||||
assert.equal(report.legacyRunHistoryFacts, 1);
|
||||
assert.equal(report.targetRunHistoryFacts, 1);
|
||||
assert.equal(report.secretConfigFacts, 2);
|
||||
const decisions = fs
|
||||
.readFileSync(output, 'utf8')
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line))
|
||||
.slice(1);
|
||||
assert.ok(
|
||||
decisions.some(
|
||||
(decision) =>
|
||||
decision.database === 'legacy' &&
|
||||
decision.domain === 'run_history' &&
|
||||
decision.disposition === 'retain_both' &&
|
||||
decision.reason === 'preserve_both',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
decisions
|
||||
.filter((decision) => decision.domain === 'secret_and_config')
|
||||
.every(
|
||||
(decision) =>
|
||||
decision.disposition === 'manual_external' &&
|
||||
decision.reason === 'external_recovery_required',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('completion review fixture refuses any blocked domain', (t) => {
|
||||
const state = fixture(t);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.blocked-completion.ndjson`,
|
||||
);
|
||||
assert.throws(
|
||||
() => completionReviewFixture(state.root, output),
|
||||
/refuses blocked diagnostic facts/,
|
||||
);
|
||||
assert.equal(fs.existsSync(output), false);
|
||||
});
|
||||
|
||||
function automationState(t) {
|
||||
const state = fixture(t);
|
||||
fs.writeFileSync(
|
||||
@@ -340,6 +459,99 @@ test('Automation fixture rejects a conflict before writing decisions', (t) => {
|
||||
assert.equal(fs.existsSync(output), false);
|
||||
});
|
||||
|
||||
function secretConfigState(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: 'secret_config_decision_required',
|
||||
profile: 'edge',
|
||||
secretConfig: {
|
||||
secretConfigId: SECRET_CONFIG_ID,
|
||||
secretConfigPlanDigest: SECRET_CONFIG_PLAN_DIGEST,
|
||||
eligibleBindingCount: 1,
|
||||
eligiblePreservationCount: 1,
|
||||
targetConflictCount: 0,
|
||||
unadaptedLegacyConfigCount: 0,
|
||||
},
|
||||
secretConfigDecision: {
|
||||
decisionId: SECRET_CONFIG_DECISION_ID,
|
||||
preparationDigest: SECRET_CONFIG_PREPARATION_DIGEST,
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
const directory = path.join(state.root, 'secret-config', SECRET_CONFIG_ID);
|
||||
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(
|
||||
path.join(directory, 'receipt.json'),
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong3-local-reconciliation-secret-config-plan-receipt',
|
||||
schemaVersion: 1,
|
||||
state: 'reconciliation_secret_config_planned',
|
||||
secretConfigId: SECRET_CONFIG_ID,
|
||||
secretConfigPlanDigest: SECRET_CONFIG_PLAN_DIGEST,
|
||||
outcome: 'ready',
|
||||
eligibleBindingCount: 1,
|
||||
eligiblePreservationCount: 1,
|
||||
targetConflictCount: 0,
|
||||
unadaptedLegacyConfigCount: 0,
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const candidates = [
|
||||
['review_apply_binding', '9'],
|
||||
['review_preserve_disabled', 'a'],
|
||||
].map(([requirement, digest], index) => ({
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-reconciliation-secret-config-plan-candidate',
|
||||
candidateOrdinal: index + 1,
|
||||
candidateDigest: digest.repeat(64),
|
||||
requirement,
|
||||
target: { state: 'absent' },
|
||||
}));
|
||||
fs.writeFileSync(
|
||||
path.join(directory, 'plan.ndjson'),
|
||||
`${candidates.map((candidate) => JSON.stringify(candidate)).join('\n')}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
test('builds reviewed Secret/Config candidate decisions', (t) => {
|
||||
const state = secretConfigState(t);
|
||||
const output = path.join(
|
||||
path.dirname(state.root),
|
||||
`${path.basename(state.root)}.secret-config.ndjson`,
|
||||
);
|
||||
t.after(() => fs.rmSync(output, { force: true }));
|
||||
const report = secretConfigFixture(state.root, output);
|
||||
assert.equal(report.decisionCount, 2);
|
||||
const records = fs
|
||||
.readFileSync(output, 'utf8')
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line));
|
||||
assert.equal(records[0].decisionId, SECRET_CONFIG_DECISION_ID);
|
||||
assert.deepEqual(
|
||||
records.slice(1).map(({ disposition, reason }) => ({
|
||||
disposition,
|
||||
reason,
|
||||
})),
|
||||
[
|
||||
{
|
||||
disposition: 'apply_active_binding',
|
||||
reason: 'reviewed_active_binding',
|
||||
},
|
||||
{
|
||||
disposition: 'preserve_disabled',
|
||||
reason: 'reviewed_disabled_preservation',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI grammar is exact', () => {
|
||||
assert.throws(
|
||||
() => parseArguments(['--mode=review', '--output=/tmp/review.ndjson']),
|
||||
|
||||
@@ -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@v10');
|
||||
assert.equal(manifest.schemaVersion, 11);
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v11');
|
||||
assert.equal(manifest.schemaVersion, 12);
|
||||
assert.equal(manifest.variant, 'headless');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.equal(manifest.architecture, 'arm64');
|
||||
@@ -372,6 +372,10 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
'reconciliation.automation.decision.commit',
|
||||
'reconciliation.automation.apply',
|
||||
'reconciliation.automation.apply.rollback',
|
||||
'reconciliation.run-history.preserve',
|
||||
'reconciliation.secret-config.decision.commit',
|
||||
'reconciliation.secret-config.apply',
|
||||
'reconciliation.complete',
|
||||
]) {
|
||||
assert.match(reconciliationContents, new RegExp(operation));
|
||||
}
|
||||
@@ -393,6 +397,14 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
reconciliationContents,
|
||||
/apply-rollback edge\|standalone .* \/absolute\/legacy-root/,
|
||||
);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/apply-plan edge\|standalone .* \/absolute\/review-decisions\.ndjson \/absolute\/legacy-root/,
|
||||
);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/complete edge\|standalone .* \/absolute\/review-decisions\.ndjson \/absolute\/legacy-root/,
|
||||
);
|
||||
assert.match(reconciliationContents, /result_stage=.*\.\$result_file\.\$\$/);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
@@ -409,7 +421,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
assert.equal(
|
||||
(reconciliationContents.match(/"authorizationLifetimeMs":60000/g) || [])
|
||||
.length,
|
||||
2,
|
||||
3,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
reconciliationContents,
|
||||
@@ -418,6 +430,12 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
assert.match(reconciliationContents, /! -path "\$decision_file"/);
|
||||
assert.match(reconciliationContents, /automaticDecision":"not_authorized/);
|
||||
assert.match(reconciliationContents, /automaticRowDecision":"not_authorized/);
|
||||
assert.match(
|
||||
reconciliationContents,
|
||||
/automaticCandidateDecision":"not_authorized/,
|
||||
);
|
||||
assert.match(reconciliationContents, /"status":"reconciliation_completed"/);
|
||||
assert.match(reconciliationContents, /"adapterCount":\$adapter_count/);
|
||||
assert.match(reconciliationContents, /"completion":"not_attempted"/);
|
||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||
assert.equal(report.compatible, true);
|
||||
@@ -476,6 +494,10 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
||||
verification.gates.legacyUpgradeReconciliationAutomationRollback,
|
||||
'passed',
|
||||
);
|
||||
assert.equal(
|
||||
verification.gates.legacyUpgradeReconciliationCompletion,
|
||||
'passed',
|
||||
);
|
||||
const quickstartContents = fs.readFileSync(
|
||||
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||
'utf8',
|
||||
@@ -991,6 +1013,20 @@ test('create rejects verification without the reviewed Automation rollback gate'
|
||||
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||
});
|
||||
|
||||
test('create rejects verification without cross-domain completion evidence', (t) => {
|
||||
const paths = fixture(t);
|
||||
const evidence = JSON.parse(
|
||||
fs.readFileSync(paths.verificationEvidence, 'utf8'),
|
||||
);
|
||||
delete evidence.gates.legacyUpgradeReconciliationCompletion;
|
||||
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');
|
||||
|
||||
@@ -50,11 +50,61 @@ test('creates one private production-shaped QingLong 2.x readiness fixture', (t)
|
||||
});
|
||||
|
||||
test('requires one normalized absolute output path', () => {
|
||||
assert.equal(parseArguments(['--output=/tmp/ql3-legacy']), '/tmp/ql3-legacy');
|
||||
assert.deepEqual(parseArguments(['--output=/tmp/ql3-legacy']), {
|
||||
output: '/tmp/ql3-legacy',
|
||||
shape: 'production',
|
||||
});
|
||||
assert.deepEqual(
|
||||
parseArguments([
|
||||
'--shape=completion-ready',
|
||||
'--output=/tmp/ql3-completion',
|
||||
]),
|
||||
{
|
||||
output: '/tmp/ql3-completion',
|
||||
shape: 'completion-ready',
|
||||
},
|
||||
);
|
||||
assert.throws(() => parseArguments([]), /usage/);
|
||||
assert.throws(() => parseArguments(['--output=relative']), /absolute path/);
|
||||
assert.throws(
|
||||
() => parseArguments(['--output=/tmp/ql3-completion', '--shape=unsafe']),
|
||||
/usage/,
|
||||
);
|
||||
assert.throws(
|
||||
() => parseArguments(['--output=/tmp/../tmp/legacy']),
|
||||
/normalized absolute non-root path/,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates a completion-ready fixture without unadapted domains', (t) => {
|
||||
const root = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-completion-ready-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const output = path.join(root, 'legacy-data');
|
||||
const result = createFixture(output, 'completion-ready');
|
||||
assert.equal(result.shape, 'completion-ready');
|
||||
const database = new DatabaseSync(result.database, { readOnly: true });
|
||||
try {
|
||||
const tables = database
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name",
|
||||
)
|
||||
.all()
|
||||
.map(({ name }) => name);
|
||||
assert.deepEqual(tables, [
|
||||
'Apps',
|
||||
'Auths',
|
||||
'CrontabStats',
|
||||
'CrontabViews',
|
||||
'Crontabs',
|
||||
'Dependences',
|
||||
'Envs',
|
||||
'RunningInstances',
|
||||
'Subscriptions',
|
||||
'sqlite_sequence',
|
||||
]);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user