mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): gate primary on shadow capture evidence
This commit is contained in:
@@ -25,6 +25,34 @@ function loadResult(status, mode = 'off') {
|
||||
sourcePath: '/data/config/qinglong3-rollout.json',
|
||||
status,
|
||||
},
|
||||
...(status === 'accepted'
|
||||
? {
|
||||
primaryGateReceipt: {
|
||||
schema: 'qinglong/legacy-shadow-primary-gate@v1',
|
||||
schemaVersion: 1,
|
||||
profile: 'standalone',
|
||||
origin: 'manual',
|
||||
generatedAtMs: NOW - 2_000,
|
||||
assessment: 'eligible',
|
||||
window: {
|
||||
startInclusiveMs: NOW - 10_000,
|
||||
endExclusiveMs: NOW - 5_000,
|
||||
},
|
||||
counts: {
|
||||
admitted: 32,
|
||||
captured: 32,
|
||||
terminalScanned: 32,
|
||||
terminalMatched: 32,
|
||||
},
|
||||
evidence: {
|
||||
captureSha256: 'a'.repeat(64),
|
||||
terminalSha256: 'b'.repeat(64),
|
||||
resourceSha256: 'c'.repeat(64),
|
||||
},
|
||||
violations: [],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,6 +199,27 @@ test('accepted bootstrap audits a lazy stack import failure without installing',
|
||||
assert.deepEqual(calls, ['load-stack', 'audit:failed']);
|
||||
});
|
||||
|
||||
test('accepted bootstrap rejects a Primary receipt for another Profile before loading', async () => {
|
||||
const calls = [];
|
||||
const load = loadResult('accepted', 'primary');
|
||||
load.primaryGateReceipt.profile = 'edge';
|
||||
await assert.rejects(
|
||||
bootstrapDefaultManualPrimaryRuntime({
|
||||
load: async () => load,
|
||||
deploymentProfile: 'standalone',
|
||||
async loadStack() {
|
||||
calls.push('load-stack');
|
||||
throw new Error('must remain lazy');
|
||||
},
|
||||
audit(record) {
|
||||
calls.push(`audit:${record.activation}`);
|
||||
},
|
||||
}),
|
||||
/does not authorize this deployment Profile/,
|
||||
);
|
||||
assert.deepEqual(calls, ['audit:failed']);
|
||||
});
|
||||
|
||||
test('disabled bootstrap stays inert even with an invalid deployment profile', async () => {
|
||||
const previous = process.env.QL_DEPLOYMENT_PROFILE;
|
||||
process.env.QL_DEPLOYMENT_PROFILE = 'invalid-profile';
|
||||
@@ -219,7 +268,7 @@ test('accepted bootstrap rejects and audits an invalid deployment profile', asyn
|
||||
}),
|
||||
/QL_DEPLOYMENT_PROFILE is invalid/,
|
||||
);
|
||||
assert.deepEqual(calls, ['load-stack', 'audit:selected', 'audit:failed']);
|
||||
assert.deepEqual(calls, ['audit:failed']);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.QL_DEPLOYMENT_PROFILE;
|
||||
else process.env.QL_DEPLOYMENT_PROFILE = previous;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { afterEach, test } = require('node:test');
|
||||
const {
|
||||
LegacyShadowCaptureAuthority,
|
||||
} = require('../../back/runtime/application/legacyShadowCaptureAuthority');
|
||||
const {
|
||||
bootstrapLegacyShadowCaptureEvidence,
|
||||
} = require('../../back/runtime/adapters/legacy/bootstrapLegacyShadowCaptureEvidence');
|
||||
|
||||
const directories = [];
|
||||
|
||||
function startup(state = 'reconciled') {
|
||||
if (state !== 'reconciled') return { state };
|
||||
return {
|
||||
state: 'reconciled',
|
||||
profile: 'edge',
|
||||
origins: 1,
|
||||
summary: {},
|
||||
metrics: {},
|
||||
report: {
|
||||
schema: 'qinglong/legacy-shadow-startup-difference-report@v1',
|
||||
schemaVersion: 1,
|
||||
profile: 'edge',
|
||||
assessment: 'converged',
|
||||
configuredOriginCount: 1,
|
||||
budget: { pageSize: 8, maxPages: 1, maxCandidates: 8 },
|
||||
coverage: {
|
||||
pages: 1,
|
||||
scanned: 0,
|
||||
stopReason: 'complete',
|
||||
remaining: false,
|
||||
resumeAvailable: false,
|
||||
},
|
||||
outcomes: {
|
||||
completed: 0,
|
||||
cancelled: 0,
|
||||
abandoned: 0,
|
||||
markedLost: 0,
|
||||
repaired: 0,
|
||||
pending: 0,
|
||||
ambiguous: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
byOrigin: [
|
||||
{
|
||||
origin: 'manual',
|
||||
scanned: 0,
|
||||
completed: 0,
|
||||
cancelled: 0,
|
||||
abandoned: 0,
|
||||
markedLost: 0,
|
||||
repaired: 0,
|
||||
pending: 0,
|
||||
ambiguous: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
test('exports one qualified no-identity capture report on clean shutdown', async () => {
|
||||
let now = 1_750_300_000_000;
|
||||
const authority = new LegacyShadowCaptureAuthority(
|
||||
{ now: () => now },
|
||||
'019f75d2-3333-7333-8333-333333333333',
|
||||
);
|
||||
let evidence;
|
||||
const audits = [];
|
||||
const handle = await bootstrapLegacyShadowCaptureEvidence({
|
||||
startup: startup(),
|
||||
origins: ['manual'],
|
||||
profile: 'edge',
|
||||
outputPath: '/private/evidence.json',
|
||||
snapshot: (origins) => authority.snapshot(origins),
|
||||
async write(_outputPath, value) {
|
||||
evidence = value;
|
||||
},
|
||||
audit: (record) => audits.push(record),
|
||||
});
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
authority.admit('manual').captured();
|
||||
}
|
||||
now += 1_000;
|
||||
|
||||
assert.equal(handle.active, true);
|
||||
assert.equal((await handle.close()).state, 'exported');
|
||||
assert.deepEqual(await handle.close(), audits.at(-1));
|
||||
assert.equal(evidence.qualification.passed, true);
|
||||
assert.equal(evidence.capture.totals.admitted, 8);
|
||||
assert.equal(evidence.capture.capturePermille, 1_000);
|
||||
assert.doesNotMatch(JSON.stringify(evidence), /taskId|runId|attemptId|pid/);
|
||||
assert.deepEqual(
|
||||
audits.map((record) => record.state),
|
||||
['armed', 'exported'],
|
||||
);
|
||||
});
|
||||
|
||||
test('writes owner-private evidence once and refuses overwrite', async () => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-capture-'));
|
||||
directories.push(directory);
|
||||
const outputPath = path.join(directory, 'capture.json');
|
||||
let now = 1_750_300_000_000;
|
||||
const authority = new LegacyShadowCaptureAuthority(
|
||||
{ now: () => now },
|
||||
'019f75d2-4444-7444-8444-444444444444',
|
||||
);
|
||||
const options = {
|
||||
startup: startup(),
|
||||
origins: ['manual'],
|
||||
profile: 'edge',
|
||||
outputPath,
|
||||
snapshot: (origins) => authority.snapshot(origins),
|
||||
audit() {},
|
||||
};
|
||||
const first = await bootstrapLegacyShadowCaptureEvidence(options);
|
||||
authority.admit('manual').captured();
|
||||
now += 1_000;
|
||||
assert.equal((await first.close()).state, 'exported');
|
||||
assert.equal((await fs.stat(outputPath)).mode & 0o777, 0o600);
|
||||
|
||||
const second = await bootstrapLegacyShadowCaptureEvidence(options);
|
||||
authority.admit('manual').captured();
|
||||
now += 1_000;
|
||||
assert.equal((await second.close()).state, 'failed');
|
||||
});
|
||||
|
||||
test('stays inert without an explicit path and rejects missing startup authority', async () => {
|
||||
const disabled = await bootstrapLegacyShadowCaptureEvidence({
|
||||
startup: { state: 'disabled' },
|
||||
origins: [],
|
||||
profile: 'edge',
|
||||
audit() {},
|
||||
});
|
||||
assert.equal(disabled.active, false);
|
||||
assert.equal((await disabled.close()).state, 'disabled');
|
||||
|
||||
const failed = await bootstrapLegacyShadowCaptureEvidence({
|
||||
startup: { state: 'incomplete' },
|
||||
origins: ['manual'],
|
||||
profile: 'edge',
|
||||
outputPath: '/private/evidence.json',
|
||||
audit() {},
|
||||
});
|
||||
assert.equal(failed.active, false);
|
||||
assert.equal((await failed.close()).state, 'failed');
|
||||
});
|
||||
@@ -319,6 +319,9 @@ test('HTTP startup orders Shadow recovery after Legacy normalization and before
|
||||
const shadowRecovery = source.indexOf(
|
||||
'await bootstrapLegacyShadowStartupReconciliation()',
|
||||
);
|
||||
const captureEvidence = source.indexOf(
|
||||
'await bootstrapLegacyShadowCaptureEvidence({',
|
||||
);
|
||||
const primaryActivation = source.indexOf(
|
||||
'await bootstrapDefaultManualPrimaryRuntime()',
|
||||
);
|
||||
@@ -328,6 +331,7 @@ test('HTTP startup orders Shadow recovery after Legacy normalization and before
|
||||
|
||||
assert.equal(legacyNormalization >= 0, true);
|
||||
assert.equal(legacyNormalization < shadowRecovery, true);
|
||||
assert.equal(shadowRecovery < primaryActivation, true);
|
||||
assert.equal(shadowRecovery < captureEvidence, true);
|
||||
assert.equal(captureEvidence < primaryActivation, true);
|
||||
assert.equal(primaryActivation < listen, true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
LegacyShadowCaptureAuthority,
|
||||
createLegacyShadowCaptureReport,
|
||||
} = require('../../back/runtime/application/legacyShadowCaptureAuthority');
|
||||
|
||||
const EPOCH = '019f75d2-1111-7111-8111-111111111111';
|
||||
|
||||
function fixture() {
|
||||
let now = 1_750_200_000_000;
|
||||
const authority = new LegacyShadowCaptureAuthority({ now: () => now }, EPOCH);
|
||||
return {
|
||||
authority,
|
||||
advance(milliseconds = 1_000) {
|
||||
now += milliseconds;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('creates a conserved origin-scoped capture window', () => {
|
||||
const value = fixture();
|
||||
const before = value.authority.snapshot(['manual']);
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
value.authority.admit('manual').captured();
|
||||
}
|
||||
value.advance();
|
||||
const after = value.authority.snapshot(['manual']);
|
||||
|
||||
const report = createLegacyShadowCaptureReport(
|
||||
'edge',
|
||||
['manual'],
|
||||
before,
|
||||
after,
|
||||
);
|
||||
|
||||
assert.equal(report.assessment, 'captured');
|
||||
assert.equal(report.totals.admitted, 8);
|
||||
assert.equal(report.totals.captured, 8);
|
||||
assert.equal(report.totals.failed, 0);
|
||||
assert.equal(report.totals.pending, 0);
|
||||
assert.equal(report.capturePermille, 1_000);
|
||||
assert.equal(JSON.stringify(report).includes('task'), false);
|
||||
});
|
||||
|
||||
test('separates fixed failure stages and incomplete admissions', () => {
|
||||
const value = fixture();
|
||||
const before = value.authority.snapshot(['manual', 'scheduled_node']);
|
||||
value.authority.admit('manual').failed('fact');
|
||||
value.authority.admit('manual').failed('accept');
|
||||
value.authority.admit('scheduled_node');
|
||||
value.advance();
|
||||
|
||||
const report = createLegacyShadowCaptureReport(
|
||||
'standalone',
|
||||
['manual', 'scheduled_node'],
|
||||
before,
|
||||
value.authority.snapshot(['manual', 'scheduled_node']),
|
||||
);
|
||||
|
||||
assert.equal(report.assessment, 'incomplete');
|
||||
assert.deepEqual(report.totals, {
|
||||
admitted: 3,
|
||||
captured: 0,
|
||||
failed: 2,
|
||||
pending: 1,
|
||||
failures: { fact: 1, observer: 0, initialization: 0, accept: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects cross-epoch, pending-baseline and incomplete-origin evidence', () => {
|
||||
const left = fixture();
|
||||
const right = new LegacyShadowCaptureAuthority(
|
||||
{ now: () => 1_750_200_001_000 },
|
||||
'019f75d2-2222-7222-8222-222222222222',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createLegacyShadowCaptureReport(
|
||||
'edge',
|
||||
['manual'],
|
||||
left.authority.snapshot(['manual']),
|
||||
right.snapshot(['manual']),
|
||||
),
|
||||
/cross process epochs/,
|
||||
);
|
||||
|
||||
left.authority.admit('manual');
|
||||
const pending = left.authority.snapshot(['manual']);
|
||||
left.advance();
|
||||
assert.throws(
|
||||
() =>
|
||||
createLegacyShadowCaptureReport(
|
||||
'edge',
|
||||
['manual'],
|
||||
pending,
|
||||
left.authority.snapshot(['manual']),
|
||||
),
|
||||
/starts with pending/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createLegacyShadowCaptureReport(
|
||||
'edge',
|
||||
['manual', 'boot'],
|
||||
left.authority.snapshot(['manual']),
|
||||
left.authority.snapshot(['manual']),
|
||||
),
|
||||
/window must be non-empty|coverage is incomplete/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
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 {
|
||||
createLegacyShadowPrimaryGateReceipt,
|
||||
parseLegacyShadowPrimaryGateReceipt,
|
||||
} = require('../../back/runtime/domain/legacyShadowPrimaryGate');
|
||||
const {
|
||||
parseArguments,
|
||||
readEvidence,
|
||||
run,
|
||||
} = require('../../scripts/ql3-legacy-shadow-primary-gate.cjs');
|
||||
|
||||
const START = 1_750_400_000_000;
|
||||
const END = START + 60_000;
|
||||
const GENERATED = END + 6 * 60_000;
|
||||
const directories = [];
|
||||
|
||||
function captureEvidence(admitted = 8) {
|
||||
const outcomes = {
|
||||
completed: 0,
|
||||
cancelled: 0,
|
||||
abandoned: 0,
|
||||
markedLost: 0,
|
||||
repaired: 0,
|
||||
pending: 0,
|
||||
ambiguous: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
};
|
||||
return {
|
||||
schema: 'qinglong/legacy-shadow-capture-evidence@v1',
|
||||
profile: 'edge',
|
||||
startup: {
|
||||
schema: 'qinglong/legacy-shadow-startup-difference-report@v1',
|
||||
profile: 'edge',
|
||||
assessment: 'converged',
|
||||
configuredOriginCount: 1,
|
||||
coverage: { remaining: false },
|
||||
outcomes,
|
||||
byOrigin: [{ origin: 'manual', scanned: 0, ...outcomes }],
|
||||
},
|
||||
capture: {
|
||||
schema: 'qinglong/legacy-shadow-capture-report@v1',
|
||||
profile: 'edge',
|
||||
assessment: 'captured',
|
||||
epoch: '019f75d2-5555-7555-8555-555555555555',
|
||||
window: {
|
||||
basis: 'process_local_legacy_admission',
|
||||
startInclusiveMs: START,
|
||||
endExclusiveMs: END,
|
||||
},
|
||||
configuredOriginCount: 1,
|
||||
totals: {
|
||||
admitted,
|
||||
captured: admitted,
|
||||
failed: 0,
|
||||
pending: 0,
|
||||
failures: { fact: 0, observer: 0, initialization: 0, accept: 0 },
|
||||
},
|
||||
byOrigin: [
|
||||
{
|
||||
origin: 'manual',
|
||||
admitted,
|
||||
captured: admitted,
|
||||
failed: 0,
|
||||
pending: 0,
|
||||
failures: {
|
||||
fact: 0,
|
||||
observer: 0,
|
||||
initialization: 0,
|
||||
accept: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
capturePermille: 1_000,
|
||||
},
|
||||
qualification: {
|
||||
passed: true,
|
||||
startupConverged: true,
|
||||
originCoverageExact: true,
|
||||
captureComplete: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function terminal(scanned = 8) {
|
||||
return {
|
||||
schema: 'qinglong/legacy-shadow-terminal-difference-report@v1',
|
||||
profile: 'edge',
|
||||
observedAtMs: GENERATED - 1,
|
||||
window: {
|
||||
basis: 'shadow_run_created_at',
|
||||
startInclusiveMs: START,
|
||||
endExclusiveMs: END,
|
||||
minimumSettlingAgeMs: 300_000,
|
||||
closed: true,
|
||||
},
|
||||
coverage: {
|
||||
direction: 'shadow_to_legacy',
|
||||
cohort: 'legacy_owned_shadow_runs',
|
||||
legacyWithoutShadow: 'not_measured',
|
||||
},
|
||||
scanned,
|
||||
remaining: false,
|
||||
evidenceComplete: true,
|
||||
assessment: 'matched',
|
||||
counts: { matched: scanned },
|
||||
byOrigin: [{ origin: 'manual', scanned, matched: scanned }],
|
||||
terminalAgreementPermille: 1_000,
|
||||
fullyComparablePermille: 1_000,
|
||||
};
|
||||
}
|
||||
|
||||
function resource() {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
fixture: 'qinglong/legacy-shadow-resource-rollback-evidence@v1',
|
||||
profile: 'edge',
|
||||
workload: { mode: 'full', runtime: 'compiled_backend' },
|
||||
rollback: {
|
||||
performed: true,
|
||||
legacyContinued: true,
|
||||
shadowWritesStopped: true,
|
||||
databaseIntegrity: 'ok',
|
||||
},
|
||||
qualification: { passed: true, violations: [] },
|
||||
};
|
||||
}
|
||||
|
||||
function input(overrides = {}) {
|
||||
return {
|
||||
profile: 'edge',
|
||||
generatedAtMs: GENERATED,
|
||||
capture: captureEvidence(),
|
||||
terminal: terminal(),
|
||||
resource: resource(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('combines capture, startup, terminal and rollback evidence into eligibility', () => {
|
||||
const receipt = createLegacyShadowPrimaryGateReceipt(input());
|
||||
|
||||
assert.equal(receipt.assessment, 'eligible');
|
||||
assert.deepEqual(receipt.violations, []);
|
||||
assert.deepEqual(receipt.counts, {
|
||||
admitted: 8,
|
||||
captured: 8,
|
||||
terminalScanned: 8,
|
||||
terminalMatched: 8,
|
||||
});
|
||||
assert.deepEqual(parseLegacyShadowPrimaryGateReceipt(receipt), receipt);
|
||||
});
|
||||
|
||||
test('fails closed for an undersized cohort, terminal drift and audit-only rollback', () => {
|
||||
const capture = captureEvidence(7);
|
||||
const terminalReport = terminal(6);
|
||||
const rollback = resource();
|
||||
rollback.workload.mode = 'audit-only';
|
||||
const receipt = createLegacyShadowPrimaryGateReceipt(
|
||||
input({ capture, terminal: terminalReport, resource: rollback }),
|
||||
);
|
||||
|
||||
assert.equal(receipt.assessment, 'ineligible');
|
||||
assert.deepEqual(receipt.violations, [
|
||||
'capture_sample_budget_invalid',
|
||||
'terminal_not_matched',
|
||||
'resource_not_compiled_full_rollback',
|
||||
]);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseLegacyShadowPrimaryGateReceipt({
|
||||
...receipt,
|
||||
assessment: 'eligible',
|
||||
}),
|
||||
/invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI reads no-follow bounded inputs and publishes a no-replace receipt', () => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-primary-gate-'));
|
||||
directories.push(directory);
|
||||
const paths = Object.fromEntries(
|
||||
['capture', 'terminal', 'resource', 'output'].map((name) => [
|
||||
name,
|
||||
path.join(directory, `${name}.json`),
|
||||
]),
|
||||
);
|
||||
fs.writeFileSync(paths.capture, `${JSON.stringify(captureEvidence())}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.writeFileSync(paths.terminal, `${JSON.stringify(terminal())}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.writeFileSync(paths.resource, `${JSON.stringify(resource())}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
const options = parseArguments([
|
||||
'--profile=edge',
|
||||
`--capture=${paths.capture}`,
|
||||
`--terminal=${paths.terminal}`,
|
||||
`--resource=${paths.resource}`,
|
||||
`--output=${paths.output}`,
|
||||
`--generated-at-ms=${GENERATED}`,
|
||||
]);
|
||||
|
||||
const receipt = run(options);
|
||||
assert.equal(receipt.assessment, 'eligible');
|
||||
assert.equal(
|
||||
parseLegacyShadowPrimaryGateReceipt(readEvidence(paths.output).value)
|
||||
.assessment,
|
||||
'eligible',
|
||||
);
|
||||
assert.equal(fs.statSync(paths.output).mode & 0o777, 0o600);
|
||||
assert.throws(() => run(options), /EEXIST/);
|
||||
|
||||
const symlink = path.join(directory, 'capture-link.json');
|
||||
fs.symlinkSync(paths.capture, symlink);
|
||||
assert.throws(() => readEvidence(symlink), /ELOOP|symbolic/i);
|
||||
});
|
||||
@@ -125,6 +125,7 @@ test('serializes a successful observed process lifecycle without starting it', a
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
const observation = observer.begin(acceptedFact());
|
||||
assert.equal(await observation.captureSettled(), 'captured');
|
||||
observation.spawned({
|
||||
atMs: ACCEPTED_AT_MS + 1,
|
||||
pid: 4242,
|
||||
@@ -231,6 +232,7 @@ test('is default-off, fail-open, and rejects primary ownership', async () => {
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
const failed = shadow.begin(acceptedFact());
|
||||
assert.equal(await failed.captureSettled(), 'failed');
|
||||
failed.spawned({ atMs: ACCEPTED_AT_MS + 1 });
|
||||
failed.exited({ atMs: ACCEPTED_AT_MS + 2, exitCode: 0 });
|
||||
await failed.settled();
|
||||
|
||||
@@ -146,6 +146,12 @@ test(
|
||||
assert.equal(report.rollback.enabled.runDelta, 1);
|
||||
assert.equal(report.rollback.enabled.defaultObserverLoaded, true);
|
||||
assert.equal(report.rollback.enabled.repositoryLoaded, true);
|
||||
assert.equal(report.rollback.enabled.capture.assessment, 'captured');
|
||||
assert.equal(report.rollback.enabled.capture.totals.admitted, 1);
|
||||
assert.equal(report.rollback.enabled.capture.totals.captured, 1);
|
||||
assert.equal(report.rollback.enabled.capture.totals.failed, 0);
|
||||
assert.equal(report.rollback.enabled.capture.totals.pending, 0);
|
||||
assert.equal(report.rollback.enabled.capture.byOrigin[0].origin, 'system');
|
||||
assert.deepEqual(report.rollback.off.configuredOrigins, []);
|
||||
assert.equal(report.rollback.off.legacyExitCode, 0);
|
||||
assert.equal(report.rollback.off.runDelta, 0);
|
||||
|
||||
@@ -15,13 +15,19 @@ const NOW = 1_750_000_000_000;
|
||||
|
||||
function enabledManifest(overrides = {}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
revision: 'manual-primary-canary-1',
|
||||
enabled: true,
|
||||
approvedBy: 'operator:admin',
|
||||
approvedAtMs: NOW - 1_000,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
rollbackPlanRef: 'docs/runbooks/disable-primary.md',
|
||||
primaryGate: {
|
||||
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
|
||||
origin: 'manual',
|
||||
receiptFile: 'manual-primary-gate.json',
|
||||
receiptSha256: 'a'.repeat(64),
|
||||
},
|
||||
rollout: {
|
||||
defaultMode: 'off',
|
||||
origins: { manual: 'primary' },
|
||||
@@ -120,7 +126,7 @@ test('parses a time-bounded, manual-only rollout manifest', () => {
|
||||
);
|
||||
|
||||
const disabled = parseRuntimeRolloutManifest(
|
||||
{ schemaVersion: 1, revision: 'disabled-1', enabled: false },
|
||||
{ schemaVersion: 2, revision: 'disabled-1', enabled: false },
|
||||
NOW,
|
||||
);
|
||||
assert.equal(disabled.policy.modeFor('manual'), 'off');
|
||||
@@ -128,6 +134,7 @@ test('parses a time-bounded, manual-only rollout manifest', () => {
|
||||
|
||||
test('rejects broad, stale, incomplete, and extensible rollout manifests', () => {
|
||||
const cases = [
|
||||
enabledManifest({ schemaVersion: 1 }),
|
||||
enabledManifest({
|
||||
rollout: {
|
||||
defaultMode: 'primary',
|
||||
@@ -135,6 +142,14 @@ test('rejects broad, stale, incomplete, and extensible rollout manifests', () =>
|
||||
allowLegacyFallbackBeforeStart: false,
|
||||
},
|
||||
}),
|
||||
enabledManifest({
|
||||
primaryGate: {
|
||||
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
|
||||
origin: 'manual',
|
||||
receiptFile: '../escaped.json',
|
||||
receiptSha256: 'a'.repeat(64),
|
||||
},
|
||||
}),
|
||||
enabledManifest({
|
||||
rollout: {
|
||||
defaultMode: 'off',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
@@ -8,6 +9,9 @@ const { afterEach, test } = require('node:test');
|
||||
const {
|
||||
loadRuntimeRolloutManifest,
|
||||
} = require('../../back/runtime/adapters/fs/runtimeRolloutManifestLoader');
|
||||
const {
|
||||
createLegacyShadowPrimaryGateReceipt,
|
||||
} = require('../../back/runtime/domain/legacyShadowPrimaryGate');
|
||||
|
||||
const NOW = 1_750_000_000_000;
|
||||
const directories = [];
|
||||
@@ -18,15 +22,103 @@ async function fixturePath(name = 'qinglong3-rollout.json') {
|
||||
return path.join(directory, name);
|
||||
}
|
||||
|
||||
function enabledManifest() {
|
||||
return {
|
||||
function gateReceipt() {
|
||||
const start = NOW - 20_000;
|
||||
const end = NOW - 10_000;
|
||||
const startup = {
|
||||
schema: 'qinglong/legacy-shadow-startup-difference-report@v1',
|
||||
profile: 'edge',
|
||||
assessment: 'converged',
|
||||
configuredOriginCount: 1,
|
||||
coverage: { remaining: false },
|
||||
byOrigin: [{ origin: 'manual' }],
|
||||
};
|
||||
const capture = {
|
||||
schema: 'qinglong/legacy-shadow-capture-evidence@v1',
|
||||
profile: 'edge',
|
||||
startup,
|
||||
capture: {
|
||||
schema: 'qinglong/legacy-shadow-capture-report@v1',
|
||||
profile: 'edge',
|
||||
assessment: 'captured',
|
||||
configuredOriginCount: 1,
|
||||
window: {
|
||||
basis: 'process_local_legacy_admission',
|
||||
startInclusiveMs: start,
|
||||
endExclusiveMs: end,
|
||||
},
|
||||
totals: { admitted: 8, captured: 8, failed: 0, pending: 0 },
|
||||
byOrigin: [{ origin: 'manual' }],
|
||||
capturePermille: 1_000,
|
||||
},
|
||||
qualification: { passed: true },
|
||||
};
|
||||
const terminal = {
|
||||
schema: 'qinglong/legacy-shadow-terminal-difference-report@v1',
|
||||
profile: 'edge',
|
||||
observedAtMs: NOW - 3_000,
|
||||
window: { startInclusiveMs: start, endExclusiveMs: end, closed: true },
|
||||
coverage: {
|
||||
direction: 'shadow_to_legacy',
|
||||
cohort: 'legacy_owned_shadow_runs',
|
||||
legacyWithoutShadow: 'not_measured',
|
||||
},
|
||||
assessment: 'matched',
|
||||
scanned: 8,
|
||||
remaining: false,
|
||||
evidenceComplete: true,
|
||||
counts: { matched: 8 },
|
||||
byOrigin: [{ origin: 'manual', scanned: 8 }],
|
||||
terminalAgreementPermille: 1_000,
|
||||
fullyComparablePermille: 1_000,
|
||||
};
|
||||
const resource = {
|
||||
schemaVersion: 1,
|
||||
fixture: 'qinglong/legacy-shadow-resource-rollback-evidence@v1',
|
||||
profile: 'edge',
|
||||
workload: { mode: 'full', runtime: 'compiled_backend' },
|
||||
rollback: {
|
||||
performed: true,
|
||||
legacyContinued: true,
|
||||
shadowWritesStopped: true,
|
||||
databaseIntegrity: 'ok',
|
||||
},
|
||||
qualification: { passed: true, violations: [] },
|
||||
};
|
||||
return createLegacyShadowPrimaryGateReceipt({
|
||||
profile: 'edge',
|
||||
generatedAtMs: NOW - 2_000,
|
||||
capture,
|
||||
terminal,
|
||||
resource,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeGateReceipt(sourcePath, value = gateReceipt()) {
|
||||
const bytes = Buffer.from(`${JSON.stringify(value)}\n`);
|
||||
await fs.writeFile(
|
||||
path.join(path.dirname(sourcePath), 'primary-gate.json'),
|
||||
bytes,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
return crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
}
|
||||
|
||||
function enabledManifest(receiptSha256) {
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
revision: 'manual-primary-canary-1',
|
||||
enabled: true,
|
||||
approvedBy: 'operator:admin',
|
||||
approvedAtMs: NOW - 1_000,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
rollbackPlanRef: 'docs/runbooks/disable-primary.md',
|
||||
primaryGate: {
|
||||
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
|
||||
origin: 'manual',
|
||||
receiptFile: 'primary-gate.json',
|
||||
receiptSha256,
|
||||
},
|
||||
rollout: {
|
||||
defaultMode: 'off',
|
||||
origins: { manual: 'primary' },
|
||||
@@ -69,7 +161,8 @@ test('fails closed when the rollout file is absent', async () => {
|
||||
|
||||
test('loads an approved manifest and audits only bounded metadata', async () => {
|
||||
const sourcePath = await fixturePath();
|
||||
const raw = JSON.stringify(enabledManifest());
|
||||
const receiptSha256 = await writeGateReceipt(sourcePath);
|
||||
const raw = JSON.stringify(enabledManifest(receiptSha256));
|
||||
await fs.writeFile(sourcePath, raw);
|
||||
|
||||
const result = await loadRuntimeRolloutManifest(sourcePath, {
|
||||
@@ -79,10 +172,52 @@ test('loads an approved manifest and audits only bounded metadata', async () =>
|
||||
assert.equal(result.status, 'accepted');
|
||||
assert.equal(result.policy.modeFor('manual'), 'primary');
|
||||
assert.equal(result.audit.revision, 'manual-primary-canary-1');
|
||||
assert.equal(result.primaryGateReceipt.assessment, 'eligible');
|
||||
assert.match(result.audit.sourceSha256, /^[a-f0-9]{64}$/);
|
||||
assert.doesNotMatch(JSON.stringify(result.audit), /operator:admin|rollback/);
|
||||
});
|
||||
|
||||
test('rejects a missing, tampered or ineligible Primary gate receipt', async () => {
|
||||
const missingPath = await fixturePath('missing-gate-manifest.json');
|
||||
await fs.writeFile(
|
||||
missingPath,
|
||||
JSON.stringify(enabledManifest('a'.repeat(64))),
|
||||
);
|
||||
const missing = await loadRuntimeRolloutManifest(missingPath, {
|
||||
clock: { now: () => NOW },
|
||||
});
|
||||
assert.equal(missing.status, 'rejected');
|
||||
assert.equal(missing.audit.reasonCode, 'PRIMARY_GATE_READ_FAILED');
|
||||
|
||||
const tamperedPath = await fixturePath('tampered-gate-manifest.json');
|
||||
const tampered = gateReceipt();
|
||||
tampered.sources.terminal.counts.matched = 7;
|
||||
const tamperedDigest = await writeGateReceipt(tamperedPath, tampered);
|
||||
await fs.writeFile(
|
||||
tamperedPath,
|
||||
JSON.stringify(enabledManifest(tamperedDigest)),
|
||||
);
|
||||
const tamperedResult = await loadRuntimeRolloutManifest(tamperedPath, {
|
||||
clock: { now: () => NOW },
|
||||
});
|
||||
assert.equal(tamperedResult.status, 'rejected');
|
||||
assert.equal(tamperedResult.audit.reasonCode, 'PRIMARY_GATE_INVALID');
|
||||
|
||||
const invalidPath = await fixturePath('invalid-gate-manifest.json');
|
||||
const ineligible = {
|
||||
...gateReceipt(),
|
||||
assessment: 'ineligible',
|
||||
violations: ['terminal_not_matched'],
|
||||
};
|
||||
const digest = await writeGateReceipt(invalidPath, ineligible);
|
||||
await fs.writeFile(invalidPath, JSON.stringify(enabledManifest(digest)));
|
||||
const invalid = await loadRuntimeRolloutManifest(invalidPath, {
|
||||
clock: { now: () => NOW },
|
||||
});
|
||||
assert.equal(invalid.status, 'rejected');
|
||||
assert.equal(invalid.audit.reasonCode, 'PRIMARY_GATE_INVALID');
|
||||
});
|
||||
|
||||
test('rejects malformed and oversized files without exposing their contents', async () => {
|
||||
const malformedPath = await fixturePath('malformed.json');
|
||||
await fs.writeFile(malformedPath, '{"approvedBy":"secret"');
|
||||
|
||||
Reference in New Issue
Block a user