mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
358 lines
11 KiB
JavaScript
358 lines
11 KiB
JavaScript
'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/,
|
|
);
|
|
});
|