mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
amplificationPermille,
|
||||
buildComposeStorageReport,
|
||||
normalizeComposeStorageManifest,
|
||||
normalizeSession,
|
||||
parseArguments,
|
||||
parseSectorsWritten,
|
||||
validateComposeStorageReport,
|
||||
validateStorageIdentity,
|
||||
} = require('../../scripts/ql3-physical-edge-compose-storage.cjs');
|
||||
const {
|
||||
canonicalDigest,
|
||||
} = require('../../scripts/ql3-physical-edge-evidence.cjs');
|
||||
|
||||
function manifest(overrides = {}) {
|
||||
return normalizeComposeStorageManifest({
|
||||
schemaVersion: 1,
|
||||
evidenceClass: 'physical_edge_compose_storage_candidate',
|
||||
profile: 'edge',
|
||||
deviceId: 'router-a1',
|
||||
expectedArchitecture: 'arm64',
|
||||
expectedFilesystem: 'ext4',
|
||||
snapshotCount: 3,
|
||||
databasePayloadBytes: 4 * 1024 * 1024,
|
||||
maximumPrepareWriteAmplificationPermille: 50_000,
|
||||
maximumResumeWriteAmplificationPermille: 50_000,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function identity(bootId, sectorsWritten, overrides = {}) {
|
||||
return {
|
||||
platform: 'linux',
|
||||
architecture: 'arm64',
|
||||
bootId,
|
||||
mountPath: '/mnt/ql3-evidence',
|
||||
filesystem: 'ext4',
|
||||
mountOptions: ['rw', 'noatime'],
|
||||
blockDevice: '/dev/mmcblk0p3',
|
||||
sectorsWritten,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionFixture(overrides = {}) {
|
||||
const before = identity('019f0000-0000-4000-8000-000000000001', 10_000);
|
||||
const deploymentRoot =
|
||||
'/mnt/ql3-evidence/.ql3-compose-storage-019f0000-0000-4000-8000-000000000010';
|
||||
const rolloutId = '019f0000-0000-4000-8000-000000000011';
|
||||
const collectionId = '019f0000-0000-4000-8000-000000000012';
|
||||
const options = {
|
||||
deploymentRoot,
|
||||
allowRootService: false,
|
||||
};
|
||||
const body = {
|
||||
schemaVersion: 1,
|
||||
evidenceClass: 'physical_edge_compose_storage_session',
|
||||
sessionId: '019f0000-0000-4000-8000-000000000010',
|
||||
manifestDigest: canonicalDigest(manifest()),
|
||||
uid: 1000,
|
||||
preparedAt: '2026-07-29T00:00:00.000Z',
|
||||
dataPath: '/mnt/ql3-evidence',
|
||||
deploymentRoot,
|
||||
barrier: {
|
||||
identity: before,
|
||||
sectorsWritten: before.sectorsWritten,
|
||||
tree: {
|
||||
entries: 40,
|
||||
regularFiles: 20,
|
||||
directories: 20,
|
||||
logicalBytes: 20_000_000,
|
||||
allocatedBytes: 20_500_000,
|
||||
},
|
||||
},
|
||||
prepareMeasurement: {
|
||||
logicalSnapshotBytes: 12_000_000,
|
||||
deviceBytesWritten: 24_000_000,
|
||||
writeAmplificationPermille: 2000,
|
||||
},
|
||||
collection: {
|
||||
snapshotCount: 3,
|
||||
prepareCommand: {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.compose.evidence-collection.prepare',
|
||||
options,
|
||||
request: {
|
||||
expectedGeneration: 4,
|
||||
collectionId,
|
||||
rolloutIds: [rolloutId],
|
||||
restoreIds: [],
|
||||
preparedAtMs: 1785283200100,
|
||||
},
|
||||
},
|
||||
commitCommand: {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.compose.evidence-collection.commit',
|
||||
options,
|
||||
request: {
|
||||
expectedGeneration: 4,
|
||||
collectionId,
|
||||
committedAtMs: 1785283200101,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
rolloutId,
|
||||
snapshot: {
|
||||
contractVersion: 43,
|
||||
sha256: 'a'.repeat(64),
|
||||
bytes: 4_000_000,
|
||||
pageCount: 1000,
|
||||
pageSize: 4096,
|
||||
},
|
||||
},
|
||||
targetPath:
|
||||
'/mnt/ql3-evidence/.ql3-compose-storage-019f0000-0000-4000-8000-000000000010/service/rollout-backups/019f0000-0000-4000-8000-000000000011.sqlite',
|
||||
stagePath:
|
||||
'/mnt/ql3-evidence/.ql3-compose-storage-019f0000-0000-4000-8000-000000000010/service/rollout-backups/.019f0000-0000-4000-8000-000000000011.sqlite.ql3-collection-stage',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
return { ...body, sha256: canonicalDigest(body) };
|
||||
}
|
||||
|
||||
function reportFixture(overrides = {}) {
|
||||
const session = normalizeSession(sessionFixture());
|
||||
const before = session.barrier.identity;
|
||||
const afterStart = identity('019f0000-0000-4000-8000-000000000002', 200);
|
||||
const after = identity(afterStart.bootId, 220);
|
||||
return buildComposeStorageReport({
|
||||
manifest: manifest(),
|
||||
session,
|
||||
observed: { before, afterStart, after },
|
||||
measurements: {
|
||||
logicalSnapshotBytes: 12_000_000,
|
||||
collectedSnapshotBytes: 4_000_000,
|
||||
prepareDeviceBytesWritten: 24_000_000,
|
||||
resumeDeviceBytesWritten: 10_240,
|
||||
prepareWriteAmplificationPermille: 2000,
|
||||
resumeWriteAmplificationPermille: 3,
|
||||
barrierAllocatedBytes: 20_500_000,
|
||||
finalAllocatedBytes: 16_400_000,
|
||||
reclaimedAllocatedBytes: 4_100_000,
|
||||
},
|
||||
outcomes: {
|
||||
commitStatus: 'collected',
|
||||
replayStatus: 'existing',
|
||||
sqliteIntegrity: 'ok',
|
||||
stageRemoved: true,
|
||||
tombstonePresent: true,
|
||||
retainedSnapshots: 2,
|
||||
},
|
||||
generatedAt: '2026-07-29T00:01:00.000Z',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('normalizes a bounded Edge-only Compose storage manifest', () => {
|
||||
assert.equal(manifest().snapshotCount, 3);
|
||||
assert.throws(
|
||||
() => manifest({ snapshotCount: 4 }),
|
||||
/snapshotCount must be 3/,
|
||||
);
|
||||
assert.throws(
|
||||
() => manifest({ databasePayloadBytes: 512 * 1024 }),
|
||||
/between 1 and 64 MiB/,
|
||||
);
|
||||
assert.throws(
|
||||
() => manifest({ maximumResumeWriteAmplificationPermille: 999 }),
|
||||
/between 1000 and 1000000/,
|
||||
);
|
||||
});
|
||||
|
||||
test('requires explicit prepare/resume files and absolute paths', () => {
|
||||
assert.deepEqual(
|
||||
parseArguments([
|
||||
'prepare',
|
||||
'--manifest=/etc/qinglong/compose-storage.json',
|
||||
'--data-path=/mnt/ql3-evidence',
|
||||
'--session=/mnt/ql3-evidence/evidence/session.json',
|
||||
'--json',
|
||||
]),
|
||||
{
|
||||
phase: 'prepare',
|
||||
manifestPath: '/etc/qinglong/compose-storage.json',
|
||||
dataPath: '/mnt/ql3-evidence',
|
||||
sessionPath: '/mnt/ql3-evidence/evidence/session.json',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
parseArguments([
|
||||
'resume',
|
||||
'--manifest=/etc/qinglong/compose-storage.json',
|
||||
'--session=/mnt/ql3-evidence/evidence/session.json',
|
||||
'--output=/mnt/ql3-evidence/evidence/report.json',
|
||||
]),
|
||||
{
|
||||
phase: 'resume',
|
||||
manifestPath: '/etc/qinglong/compose-storage.json',
|
||||
sessionPath: '/mnt/ql3-evidence/evidence/session.json',
|
||||
outputPath: '/mnt/ql3-evidence/evidence/report.json',
|
||||
json: false,
|
||||
},
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseArguments([
|
||||
'resume',
|
||||
'--manifest=manifest.json',
|
||||
'--session=/data/session.json',
|
||||
'--output=/data/report.json',
|
||||
]),
|
||||
/manifestPath must be absolute/,
|
||||
);
|
||||
});
|
||||
|
||||
test('parses Linux partition writes and computes integer upper bounds', () => {
|
||||
assert.equal(parseSectorsWritten('1 2 3 4 5 6 700 8 9 10 11\n'), 700);
|
||||
assert.equal(amplificationPermille(8192, 4096), 2000);
|
||||
assert.equal(amplificationPermille(-1, 4096), null);
|
||||
assert.throws(() => parseSectorsWritten('1 2 3'), /statistics are invalid/);
|
||||
});
|
||||
|
||||
test('binds the durable pre-reboot session digest and exact stage facts', () => {
|
||||
const session = normalizeSession(sessionFixture());
|
||||
assert.equal(session.collection.snapshotCount, 3);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeSession({
|
||||
...session,
|
||||
collection: { ...session.collection, snapshotCount: 2 },
|
||||
}),
|
||||
/session is invalid or drifted/,
|
||||
);
|
||||
const escapedRoot = '/opt/qinglong3';
|
||||
const escaped = sessionFixture({
|
||||
deploymentRoot: escapedRoot,
|
||||
});
|
||||
assert.throws(
|
||||
() => normalizeSession(escaped),
|
||||
/session is invalid or drifted/,
|
||||
);
|
||||
const widenedCommand = sessionFixture();
|
||||
widenedCommand.collection.commitCommand.options.deploymentRoot = escapedRoot;
|
||||
const { sha256: ignored, ...widenedBody } = widenedCommand;
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeSession({
|
||||
...widenedBody,
|
||||
sha256: canonicalDigest(widenedBody),
|
||||
}),
|
||||
/session is invalid or drifted/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeSession({ ...session, unexpected: true }),
|
||||
/session keys must be exactly/,
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts only a different-boot exact recovery with bounded writes', () => {
|
||||
const report = reportFixture();
|
||||
assert.equal(report.supported, false);
|
||||
assert.equal(report.qualification.passed, true);
|
||||
assert.ok(
|
||||
report.qualification.doesNotProve.includes(
|
||||
'abrupt_power_interruption_provenance',
|
||||
),
|
||||
);
|
||||
const currentObserved = {
|
||||
bootId: report.observed.after.bootId,
|
||||
architecture: 'arm64',
|
||||
dataFilesystem: 'ext4',
|
||||
dataPath: '/mnt/ql3-evidence',
|
||||
};
|
||||
assert.deepEqual(
|
||||
validateComposeStorageReport(report, manifest(), currentObserved),
|
||||
[],
|
||||
);
|
||||
const tampered = {
|
||||
...report,
|
||||
measurements: {
|
||||
...report.measurements,
|
||||
reclaimedAllocatedBytes: 0,
|
||||
},
|
||||
};
|
||||
assert.match(
|
||||
validateComposeStorageReport(tampered, manifest(), currentObserved).join(
|
||||
'; ',
|
||||
),
|
||||
/SHA-256 is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails same-boot, storage drift, outcome drift and write budget overflow', () => {
|
||||
const session = normalizeSession(sessionFixture());
|
||||
const sameBoot = session.barrier.identity;
|
||||
const report = buildComposeStorageReport({
|
||||
manifest: manifest(),
|
||||
session,
|
||||
observed: {
|
||||
before: sameBoot,
|
||||
afterStart: sameBoot,
|
||||
after: sameBoot,
|
||||
},
|
||||
measurements: {
|
||||
collectedSnapshotBytes: 4_000_000,
|
||||
prepareWriteAmplificationPermille: 50_001,
|
||||
resumeWriteAmplificationPermille: 50_001,
|
||||
reclaimedAllocatedBytes: 0,
|
||||
},
|
||||
outcomes: {
|
||||
commitStatus: 'existing',
|
||||
replayStatus: 'existing',
|
||||
sqliteIntegrity: 'invalid',
|
||||
stageRemoved: false,
|
||||
tombstonePresent: false,
|
||||
retainedSnapshots: 3,
|
||||
},
|
||||
generatedAt: '2026-07-29T00:01:00.000Z',
|
||||
});
|
||||
assert.equal(report.qualification.passed, false);
|
||||
assert.match(
|
||||
report.qualification.violations.join('; '),
|
||||
/boot identity.*recovery outcome.*write amplification.*not reclaimed/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
validateStorageIdentity(
|
||||
manifest(),
|
||||
identity('boot', 1, {
|
||||
architecture: 'x64',
|
||||
mountPath: '/data',
|
||||
filesystem: 'xfs',
|
||||
mountOptions: ['ro'],
|
||||
blockDevice: 'overlay',
|
||||
}),
|
||||
'/mnt/ql3-evidence',
|
||||
),
|
||||
[
|
||||
'architecture did not match manifest',
|
||||
'dataPath was not the dedicated mount point',
|
||||
'filesystem did not match manifest',
|
||||
'dataPath was not mounted read-write',
|
||||
'block device identity was invalid',
|
||||
],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user