mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:36:13 +08:00
501 lines
16 KiB
JavaScript
501 lines
16 KiB
JavaScript
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 { spawnSync } = require('node:child_process');
|
|
const { afterEach, test } = require('node:test');
|
|
const { DataTypes, Sequelize } = require('sequelize');
|
|
const {
|
|
defineSchemaMigrationModel,
|
|
} = require('../../back/data/schemaMigration');
|
|
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
|
|
const {
|
|
runningInstanceRunReferenceMigration,
|
|
} = require('../../back/migrations/0003-running-instance-run-reference');
|
|
const {
|
|
runCancellationRequestMigration,
|
|
} = require('../../back/migrations/0004-run-cancellation-request');
|
|
const {
|
|
runAttemptDeadlineMigration,
|
|
} = require('../../back/migrations/0006-run-attempt-deadline');
|
|
const { runMigrations } = require('../../back/migrations/runner');
|
|
const {
|
|
LegacySequelizeRunRepository,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
|
|
const {
|
|
LegacySequelizeShadowTerminalDifferenceSource,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/legacyShadowTerminalDifferenceSource');
|
|
const {
|
|
LegacyShadowRunWriter,
|
|
} = require('../../back/runtime/application/legacyShadowRunWriter');
|
|
const {
|
|
LegacyShadowTerminalDifferenceAuditor,
|
|
} = require('../../back/runtime/application/legacyShadowTerminalDifferenceAuditor');
|
|
const {
|
|
createLegacyLogArtifactId,
|
|
} = require('../../back/runtime/compatibility/legacyTaskRevision');
|
|
const {
|
|
parseArguments,
|
|
} = require('../../scripts/ql3-legacy-shadow-terminal-audit.cjs');
|
|
|
|
const REPOSITORY_ROOT = path.resolve(__dirname, '../..');
|
|
const CLI_PATH = path.join(
|
|
REPOSITORY_ROOT,
|
|
'scripts',
|
|
'ql3-legacy-shadow-terminal-audit.cjs',
|
|
);
|
|
const BASE_TIME = 1_750_100_000_000;
|
|
const databases = [];
|
|
const temporaryDirectories = [];
|
|
let idSequence = 4_000;
|
|
let timeSequence = BASE_TIME;
|
|
|
|
function nextId() {
|
|
idSequence += 1;
|
|
return `019f7300-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
|
|
}
|
|
|
|
function nextTime() {
|
|
timeSequence += 10_000;
|
|
return timeSequence;
|
|
}
|
|
|
|
async function createDatabase(storage = ':memory:') {
|
|
const database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage,
|
|
logging: false,
|
|
});
|
|
await database.getQueryInterface().createTable('RunningInstances', {
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true,
|
|
},
|
|
cron_id: { type: DataTypes.INTEGER, allowNull: false },
|
|
pid: { type: DataTypes.INTEGER, allowNull: true },
|
|
log_path: { type: DataTypes.STRING, allowNull: true },
|
|
started_at: { type: DataTypes.INTEGER, allowNull: false },
|
|
finished_at: { type: DataTypes.INTEGER, allowNull: true },
|
|
status: { type: DataTypes.INTEGER, allowNull: false },
|
|
exit_code: { type: DataTypes.INTEGER, allowNull: true },
|
|
});
|
|
await runMigrations({
|
|
database,
|
|
migrationModel: defineSchemaMigrationModel(database),
|
|
migrations: [
|
|
runSchemaMigration,
|
|
runningInstanceRunReferenceMigration,
|
|
runCancellationRequestMigration,
|
|
runAttemptDeadlineMigration,
|
|
],
|
|
logger: { info() {} },
|
|
});
|
|
databases.push(database);
|
|
return database;
|
|
}
|
|
|
|
async function createStack(storage) {
|
|
const database = await createDatabase(storage);
|
|
const repository = new LegacySequelizeRunRepository(database);
|
|
const writer = new LegacyShadowRunWriter(repository, nextId);
|
|
const source = new LegacySequelizeShadowTerminalDifferenceSource(
|
|
database,
|
|
createLegacyLogArtifactId,
|
|
);
|
|
const auditor = new LegacyShadowTerminalDifferenceAuditor(source);
|
|
return { database, repository, writer, source, auditor };
|
|
}
|
|
|
|
async function createShadow(writer, overrides = {}) {
|
|
const acceptedAtMs = overrides.acceptedAtMs ?? nextTime();
|
|
const legacyCronId = overrides.legacyCronId ?? 31;
|
|
const pid = overrides.pid ?? 5_101;
|
|
const logPath = overrides.logPath ?? `cron/${acceptedAtMs}.log`;
|
|
const origin = overrides.origin ?? 'manual';
|
|
const reference = await writer.accept({
|
|
origin,
|
|
projectId: 'default',
|
|
taskId: `legacy-cron:${legacyCronId}`,
|
|
taskRevision: `sha256:terminal-audit-${legacyCronId}`,
|
|
legacyCronId,
|
|
triggerType: origin,
|
|
acceptedAtMs,
|
|
});
|
|
if (overrides.acceptedOnly) {
|
|
return { acceptedAtMs, legacyCronId, pid, logPath, origin, reference };
|
|
}
|
|
await writer.spawned(reference, {
|
|
atMs: acceptedAtMs + 100,
|
|
pid,
|
|
logArtifactId: createLegacyLogArtifactId(logPath),
|
|
});
|
|
await writer.running(reference, acceptedAtMs + 200);
|
|
if (!overrides.runningOnly) {
|
|
await writer.exited(reference, {
|
|
atMs: acceptedAtMs + 500,
|
|
exitCode: overrides.shadowExitCode ?? 0,
|
|
});
|
|
}
|
|
return { acceptedAtMs, legacyCronId, pid, logPath, origin, reference };
|
|
}
|
|
|
|
async function insertInstance(database, shadow, overrides = {}) {
|
|
await database.getQueryInterface().bulkInsert('RunningInstances', [
|
|
{
|
|
cron_id: shadow.legacyCronId,
|
|
run_id:
|
|
overrides.direct === false
|
|
? null
|
|
: overrides.runId ?? shadow.reference.runId,
|
|
attempt_id:
|
|
overrides.direct === false
|
|
? null
|
|
: overrides.attemptId ?? shadow.reference.attemptId,
|
|
pid: overrides.pid ?? shadow.pid,
|
|
log_path: overrides.logPath ?? shadow.logPath,
|
|
started_at: Math.floor((shadow.acceptedAtMs + 100) / 1_000),
|
|
finished_at: Math.floor(
|
|
(shadow.acceptedAtMs + (overrides.finishedOffsetMs ?? 500)) / 1_000,
|
|
),
|
|
status: overrides.status ?? 1,
|
|
exit_code: overrides.exitCode ?? 0,
|
|
},
|
|
]);
|
|
}
|
|
|
|
function auditOptions(shadows, overrides = {}) {
|
|
const times = shadows.map((shadow) => shadow.acceptedAtMs);
|
|
const start = Math.min(...times) - 1;
|
|
const end = Math.max(...times) + 1;
|
|
return {
|
|
profile: overrides.profile ?? 'edge',
|
|
origins: overrides.origins ?? ['manual'],
|
|
windowStartMs: start,
|
|
windowEndMs: end,
|
|
observedAtMs: overrides.observedAtMs ?? end + 5 * 60_000,
|
|
minimumSettlingAgeMs: 5 * 60_000,
|
|
};
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(databases.splice(0).map((database) => database.close()));
|
|
for (const directory of temporaryDirectories.splice(0)) {
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('reports a closed, fully comparable Shadow-to-Legacy terminal match', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const shadow = await createShadow(writer);
|
|
await insertInstance(database, shadow);
|
|
|
|
const report = await auditor.run(auditOptions([shadow]));
|
|
|
|
assert.equal(report.assessment, 'matched');
|
|
assert.equal(report.counts.matched, 1);
|
|
assert.equal(report.terminalAgreementPermille, 1_000);
|
|
assert.equal(report.fullyComparablePermille, 1_000);
|
|
assert.equal(report.coverage.direction, 'shadow_to_legacy');
|
|
assert.equal(report.coverage.legacyWithoutShadow, 'not_measured');
|
|
assert.equal(JSON.stringify(report).includes(shadow.reference.runId), false);
|
|
assert.equal(JSON.stringify(report).includes(shadow.logPath), false);
|
|
});
|
|
|
|
test('separates terminal status and field differences', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const statusShadow = await createShadow(writer, { legacyCronId: 41 });
|
|
const fieldShadow = await createShadow(writer, { legacyCronId: 42 });
|
|
await insertInstance(database, statusShadow, { status: 3, exitCode: 1 });
|
|
await insertInstance(database, fieldShadow, { exitCode: 9 });
|
|
|
|
const report = await auditor.run(auditOptions([statusShadow, fieldShadow]));
|
|
|
|
assert.equal(report.assessment, 'differences_found');
|
|
assert.equal(report.counts.status_mismatch, 1);
|
|
assert.equal(report.counts.field_mismatch, 1);
|
|
assert.equal(report.dimensions.status.mismatched, 1);
|
|
assert.equal(report.dimensions.exitCode.mismatched, 1);
|
|
assert.equal(report.terminalAgreementPermille, 0);
|
|
});
|
|
|
|
test('does not guess when Legacy evidence is missing or ambiguous', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const missing = await createShadow(writer, { legacyCronId: 51 });
|
|
const ambiguous = await createShadow(writer, { legacyCronId: 52 });
|
|
await insertInstance(database, ambiguous, { direct: false });
|
|
await insertInstance(database, ambiguous, { direct: false });
|
|
|
|
const report = await auditor.run(auditOptions([missing, ambiguous]));
|
|
|
|
assert.equal(report.counts.legacy_evidence_missing, 1);
|
|
assert.equal(report.counts.legacy_evidence_ambiguous, 1);
|
|
assert.equal(report.assessment, 'differences_found');
|
|
});
|
|
|
|
test('rejects conflicting direct Run and Attempt references as ambiguous', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const shadow = await createShadow(writer, { legacyCronId: 53 });
|
|
await insertInstance(database, shadow, {
|
|
attemptId: '019f7300-0000-7000-8000-999999999999',
|
|
});
|
|
|
|
const report = await auditor.run(auditOptions([shadow]));
|
|
|
|
assert.equal(report.counts.legacy_evidence_ambiguous, 1);
|
|
assert.equal(report.assessment, 'differences_found');
|
|
});
|
|
|
|
test('classifies a settled cohort member with an active Shadow Run', async () => {
|
|
const { writer, auditor } = await createStack();
|
|
const shadow = await createShadow(writer, { runningOnly: true });
|
|
|
|
const report = await auditor.run(auditOptions([shadow]));
|
|
|
|
assert.equal(report.counts.shadow_not_terminal, 1);
|
|
assert.equal(report.assessment, 'differences_found');
|
|
});
|
|
|
|
test('withholds ratios while the measurement window is still open', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const shadow = await createShadow(writer);
|
|
await insertInstance(database, shadow);
|
|
const options = auditOptions([shadow], {
|
|
observedAtMs: shadow.acceptedAtMs + 1_000,
|
|
});
|
|
|
|
const report = await auditor.run(options);
|
|
|
|
assert.equal(report.assessment, 'window_open');
|
|
assert.equal(report.window.closed, false);
|
|
assert.equal(report.terminalAgreementPermille, undefined);
|
|
assert.equal(report.fullyComparablePermille, undefined);
|
|
});
|
|
|
|
test('withholds ratios when the edge candidate budget is exhausted', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const shadows = [];
|
|
for (let index = 0; index < 9; index += 1) {
|
|
const shadow = await createShadow(writer, {
|
|
legacyCronId: 60 + index,
|
|
pid: 6_000 + index,
|
|
});
|
|
shadows.push(shadow);
|
|
await insertInstance(database, shadow);
|
|
}
|
|
|
|
const report = await auditor.run(auditOptions(shadows));
|
|
|
|
assert.equal(report.scanned, 8);
|
|
assert.equal(report.remaining, true);
|
|
assert.equal(report.stopReason, 'page_limit');
|
|
assert.equal(report.assessment, 'incomplete');
|
|
assert.equal(report.terminalAgreementPermille, undefined);
|
|
});
|
|
|
|
test('marks evidence overflow incomplete instead of accepting a partial match set', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const shadow = await createShadow(writer, { legacyCronId: 71 });
|
|
for (let index = 0; index < 9; index += 1) {
|
|
await insertInstance(database, shadow, {
|
|
direct: false,
|
|
pid: 7_100 + index,
|
|
logPath: `cron/overflow-${index}.log`,
|
|
});
|
|
}
|
|
|
|
const report = await auditor.run(auditOptions([shadow]));
|
|
|
|
assert.equal(report.evidenceComplete, false);
|
|
assert.equal(report.evidenceOverflowPages, 1);
|
|
assert.equal(report.counts.legacy_evidence_ambiguous, 1);
|
|
assert.equal(report.assessment, 'incomplete');
|
|
});
|
|
|
|
test('keeps an exact, conservation-safe matrix for configured origins', async () => {
|
|
const { database, writer, auditor } = await createStack();
|
|
const manual = await createShadow(writer, { legacyCronId: 81 });
|
|
const system = await createShadow(writer, {
|
|
legacyCronId: 82,
|
|
origin: 'scheduled_system',
|
|
});
|
|
await insertInstance(database, manual);
|
|
await insertInstance(database, system);
|
|
|
|
const report = await auditor.run(
|
|
auditOptions([manual, system], {
|
|
origins: ['manual', 'scheduled_system'],
|
|
}),
|
|
);
|
|
|
|
assert.deepEqual(
|
|
report.byOrigin.map(({ origin, scanned, matched }) => ({
|
|
origin,
|
|
scanned,
|
|
matched,
|
|
})),
|
|
[
|
|
{ origin: 'manual', scanned: 1, matched: 1 },
|
|
{ origin: 'scheduled_system', scanned: 1, matched: 1 },
|
|
],
|
|
);
|
|
assert.equal(
|
|
Object.values(report.counts).reduce((sum, count) => sum + count, 0),
|
|
report.scanned,
|
|
);
|
|
});
|
|
|
|
test('reports an empty closed cohort without claiming agreement', async () => {
|
|
const { auditor } = await createStack();
|
|
const report = await auditor.run({
|
|
profile: 'edge',
|
|
origins: ['manual'],
|
|
windowStartMs: BASE_TIME - 10_000,
|
|
windowEndMs: BASE_TIME - 5_000,
|
|
observedAtMs: BASE_TIME + 300_000,
|
|
});
|
|
|
|
assert.equal(report.assessment, 'empty');
|
|
assert.equal(report.scanned, 0);
|
|
assert.equal(report.terminalAgreementPermille, undefined);
|
|
});
|
|
|
|
test('rejects malformed windows, excessive origins and tolerance', async () => {
|
|
const { auditor } = await createStack();
|
|
await assert.rejects(
|
|
auditor.run({
|
|
profile: 'edge',
|
|
origins: ['manual'],
|
|
windowStartMs: 10,
|
|
windowEndMs: 10,
|
|
}),
|
|
/non-empty/,
|
|
);
|
|
await assert.rejects(
|
|
auditor.run({
|
|
profile: 'edge',
|
|
origins: [
|
|
'manual',
|
|
'boot',
|
|
'scheduled_node',
|
|
'scheduled_system',
|
|
'script',
|
|
'subscription',
|
|
'system',
|
|
'grpc',
|
|
],
|
|
windowStartMs: 1,
|
|
windowEndMs: 2,
|
|
}),
|
|
/origin count/,
|
|
);
|
|
await assert.rejects(
|
|
auditor.run({
|
|
profile: 'edge',
|
|
origins: ['manual'],
|
|
windowStartMs: 1,
|
|
windowEndMs: 2,
|
|
correlationToleranceMs: 60_001,
|
|
}),
|
|
/hard limit/,
|
|
);
|
|
});
|
|
|
|
test('rejects an adapter page that exceeds the evidence hard limit', async () => {
|
|
const auditor = new LegacyShadowTerminalDifferenceAuditor({
|
|
async listCandidates() {
|
|
return {
|
|
candidates: [],
|
|
evidence: [
|
|
{
|
|
instanceId: 1,
|
|
legacyCronId: 1,
|
|
startedAtMs: 1,
|
|
finishedAtMs: 2,
|
|
outcome: 'succeeded',
|
|
},
|
|
],
|
|
evidenceTruncated: false,
|
|
truncated: false,
|
|
};
|
|
},
|
|
});
|
|
|
|
await assert.rejects(
|
|
auditor.run({
|
|
profile: 'edge',
|
|
origins: ['manual'],
|
|
windowStartMs: 1,
|
|
windowEndMs: 2,
|
|
observedAtMs: 300_002,
|
|
}),
|
|
/exceeded evidence limit/,
|
|
);
|
|
});
|
|
|
|
test('CLI arguments require an explicit cohort and reject unsupported origins', () => {
|
|
assert.throws(
|
|
() => parseArguments(['--origin=manual', '--window-start-ms=1']),
|
|
/window-end-ms is required/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArguments([
|
|
'--origin=grpc',
|
|
'--window-start-ms=1',
|
|
'--window-end-ms=2',
|
|
]),
|
|
/unsupported Shadow origin/,
|
|
);
|
|
const options = parseArguments([
|
|
'--profile=standalone',
|
|
'--origin=manual,scheduled_system',
|
|
'--window-start-ms=1',
|
|
'--window-end-ms=2',
|
|
'--json',
|
|
]);
|
|
assert.equal(options.profile, 'standalone');
|
|
assert.deepEqual(options.origins, ['manual', 'scheduled_system']);
|
|
assert.equal(options.json, true);
|
|
});
|
|
|
|
test('read-only CLI emits the versioned redacted report', async () => {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-shadow-audit-'));
|
|
temporaryDirectories.push(directory);
|
|
const storage = path.join(directory, 'database.sqlite');
|
|
const { database, writer } = await createStack(storage);
|
|
const shadow = await createShadow(writer, { legacyCronId: 91 });
|
|
await insertInstance(database, shadow);
|
|
await database.close();
|
|
databases.splice(databases.indexOf(database), 1);
|
|
const options = auditOptions([shadow]);
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
CLI_PATH,
|
|
`--database=${storage}`,
|
|
'--profile=edge',
|
|
'--origin=manual',
|
|
`--window-start-ms=${options.windowStartMs}`,
|
|
`--window-end-ms=${options.windowEndMs}`,
|
|
`--observed-at-ms=${options.observedAtMs}`,
|
|
'--json',
|
|
'--fail-on-difference',
|
|
],
|
|
{ cwd: REPOSITORY_ROOT, encoding: 'utf8' },
|
|
);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
const report = JSON.parse(result.stdout);
|
|
assert.equal(
|
|
report.schema,
|
|
'qinglong/legacy-shadow-terminal-difference-report@v1',
|
|
);
|
|
assert.equal(report.assessment, 'matched');
|
|
assert.equal(result.stdout.includes(shadow.reference.runId), false);
|
|
assert.equal(result.stdout.includes(shadow.logPath), false);
|
|
});
|