mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
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: 44,
|
|
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',
|
|
],
|
|
);
|
|
});
|