mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:58:17 +08:00
328 lines
9.2 KiB
JavaScript
328 lines
9.2 KiB
JavaScript
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 { test } = require('node:test');
|
|
|
|
const {
|
|
RESOURCE_TIERS,
|
|
createWorkloadPlans,
|
|
} = require('../../scripts/ql3-linux-resource-gate.cjs');
|
|
const {
|
|
TIER_NAMES,
|
|
bundleArchitectureEvidence,
|
|
evidenceDigest,
|
|
mergeCrossArchitectureEvidence,
|
|
normalizeSource,
|
|
readJsonFile,
|
|
validateArchitectureEvidence,
|
|
} = require('../../scripts/ql3-linux-resource-release-evidence.cjs');
|
|
|
|
const scriptPath = path.resolve(
|
|
__dirname,
|
|
'../../scripts/ql3-linux-resource-release-evidence.cjs',
|
|
);
|
|
|
|
function fixtureSource(overrides = {}) {
|
|
return {
|
|
repository: 'whyour/qinglong',
|
|
revision: 'a'.repeat(40),
|
|
workflow: 'QingLong 3.0 CI',
|
|
runId: '123456',
|
|
runAttempt: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function fixtureTierReport(tierName, architecture) {
|
|
const tier = RESOURCE_TIERS[tierName];
|
|
return {
|
|
schemaVersion: 1,
|
|
tier: tierName,
|
|
evidenceClass: tier.evidenceClass,
|
|
supportedMinimum: tier.supportedMinimum,
|
|
identity: {
|
|
platform: 'linux',
|
|
architecture,
|
|
node: 'v24.18.0',
|
|
uid: 65532,
|
|
gid: 65532,
|
|
},
|
|
envelope: {
|
|
memoryMaxBytes: tier.memoryMaxBytes,
|
|
memoryPeakBytes: Math.min(64 * 1024 * 1024, tier.memoryMaxBytes),
|
|
swapMaxBytes: tier.swapMaxBytes,
|
|
cpuQuotaCores: tier.cpuQuotaCores,
|
|
pidsMax: tier.pidsMax,
|
|
noNewPrivileges: 1,
|
|
seccompMode: 2,
|
|
rootReadOnly: true,
|
|
workspaceReadOnly: true,
|
|
tmpWritable: true,
|
|
memoryEventsBefore: {
|
|
low: 0,
|
|
high: 0,
|
|
max: 0,
|
|
oom: 0,
|
|
oom_kill: 0,
|
|
oom_group_kill: 0,
|
|
},
|
|
memoryEventsAfter: {
|
|
low: 0,
|
|
high: 0,
|
|
max: 0,
|
|
oom: 0,
|
|
oom_kill: 0,
|
|
oom_group_kill: 0,
|
|
},
|
|
},
|
|
workloads: createWorkloadPlans('/workspace', tierName).map(({ name }) => ({
|
|
name,
|
|
report: { passed: true },
|
|
})),
|
|
gates: { passed: true, violations: [] },
|
|
};
|
|
}
|
|
|
|
function fixtureReports(architecture) {
|
|
return Object.fromEntries(
|
|
TIER_NAMES.map((tierName) => [
|
|
tierName,
|
|
fixtureTierReport(tierName, architecture),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function fixtureBundle(architecture, source = fixtureSource()) {
|
|
return bundleArchitectureEvidence({
|
|
source,
|
|
architecture,
|
|
reports: fixtureReports(architecture),
|
|
});
|
|
}
|
|
|
|
function writeJson(filePath, value) {
|
|
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, 'utf8');
|
|
}
|
|
|
|
function runCli(arguments_) {
|
|
return spawnSync(process.execPath, [scriptPath, ...arguments_], {
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
function sourceArguments(source = fixtureSource()) {
|
|
return [
|
|
`--repository=${source.repository}`,
|
|
`--revision=${source.revision}`,
|
|
`--workflow=${source.workflow}`,
|
|
`--run-id=${source.runId}`,
|
|
`--run-attempt=${source.runAttempt}`,
|
|
];
|
|
}
|
|
|
|
test('bundles native x64 and arm64 reports into source-bound release evidence', () => {
|
|
const source = fixtureSource();
|
|
const x64 = fixtureBundle('x64', source);
|
|
const arm64 = fixtureBundle('arm64', source);
|
|
const release = mergeCrossArchitectureEvidence({ source, x64, arm64 });
|
|
|
|
assert.equal(
|
|
release.fixture,
|
|
'qinglong/linux-resource-cross-architecture-evidence@v1',
|
|
);
|
|
assert.deepEqual(
|
|
release.architectures.map(({ architecture }) => architecture),
|
|
['x64', 'arm64'],
|
|
);
|
|
assert.equal(release.architectures[0].tiers.length, 3);
|
|
assert.equal(release.architectures[1].tiers.length, 3);
|
|
assert.equal(release.gates.passed, true);
|
|
assert.equal(release.releaseDigest.length, 64);
|
|
assert.notEqual(x64.bundleDigest, arm64.bundleDigest);
|
|
assert.deepEqual(release.limitations, [
|
|
'CI cgroup evidence is not a supported minimum hardware claim',
|
|
'CI evidence does not replace fixed-device power-loss, flash, thermal, or soak evidence',
|
|
'GitHub workflow identity binding is not a cryptographic hardware attestation',
|
|
]);
|
|
});
|
|
|
|
test('rejects architecture, gate, memory event and schema drift', () => {
|
|
const source = fixtureSource();
|
|
const wrongArchitecture = fixtureReports('x64');
|
|
wrongArchitecture['router-stress-ci'].identity.architecture = 'arm64';
|
|
assert.throws(
|
|
() =>
|
|
bundleArchitectureEvidence({
|
|
source,
|
|
architecture: 'x64',
|
|
reports: wrongArchitecture,
|
|
}),
|
|
/reviewed native identity/,
|
|
);
|
|
|
|
const failedGate = fixtureReports('x64');
|
|
failedGate['edge-release-ci'].gates = {
|
|
passed: false,
|
|
violations: ['benchmark failed'],
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
bundleArchitectureEvidence({
|
|
source,
|
|
architecture: 'x64',
|
|
reports: failedGate,
|
|
}),
|
|
/gate did not pass/,
|
|
);
|
|
|
|
const memoryEvent = fixtureReports('x64');
|
|
memoryEvent['cluster-control-ci'].envelope.memoryEventsAfter.oom_kill = 1;
|
|
assert.throws(
|
|
() =>
|
|
bundleArchitectureEvidence({
|
|
source,
|
|
architecture: 'x64',
|
|
reports: memoryEvent,
|
|
}),
|
|
/memory event oom_kill changed/,
|
|
);
|
|
|
|
const widened = fixtureReports('x64');
|
|
widened['router-stress-ci'].unexpected = true;
|
|
assert.throws(
|
|
() =>
|
|
bundleArchitectureEvidence({
|
|
source,
|
|
architecture: 'x64',
|
|
reports: widened,
|
|
}),
|
|
/report fields are invalid/,
|
|
);
|
|
});
|
|
|
|
test('rejects source identifiers that only coerce to the reviewed text shape', () => {
|
|
for (const source of [
|
|
fixtureSource({ repository: 123 }),
|
|
fixtureSource({ revision: 123 }),
|
|
fixtureSource({ runId: 123456 }),
|
|
]) {
|
|
assert.throws(() => normalizeSource(source), /source (repository|revision|runId)/);
|
|
}
|
|
});
|
|
|
|
test('rejects tampered bundles, cross-source mixing and duplicate architecture', () => {
|
|
const source = fixtureSource();
|
|
const x64 = fixtureBundle('x64', source);
|
|
const arm64 = fixtureBundle('arm64', source);
|
|
const tamperedX64 = { ...x64, bundleDigest: '0'.repeat(64) };
|
|
assert.throws(
|
|
() => validateArchitectureEvidence(tamperedX64, source, 'x64'),
|
|
/digest or gates drifted/,
|
|
);
|
|
|
|
const otherSource = fixtureSource({ revision: 'b'.repeat(40) });
|
|
assert.throws(
|
|
() =>
|
|
mergeCrossArchitectureEvidence({
|
|
source,
|
|
x64,
|
|
arm64: fixtureBundle('arm64', otherSource),
|
|
}),
|
|
/belongs to another source/,
|
|
);
|
|
assert.throws(
|
|
() => mergeCrossArchitectureEvidence({ source, x64, arm64: x64 }),
|
|
/arm64 architecture evidence digest or gates drifted/,
|
|
);
|
|
assert.throws(
|
|
() => mergeCrossArchitectureEvidence({ source, x64, arm64: undefined }),
|
|
/plain object/,
|
|
);
|
|
});
|
|
|
|
test('enforces a shared canonical node budget across sibling branches', () => {
|
|
assert.throws(
|
|
() => evidenceDigest(Array.from({ length: 100_000 }, () => null)),
|
|
/node budget exceeded/,
|
|
);
|
|
});
|
|
|
|
test('CLI creates non-overwriting native bundles and merged evidence', (t) => {
|
|
const temporaryDirectory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-linux-resource-release-evidence-'),
|
|
);
|
|
t.after(() => fs.rmSync(temporaryDirectory, { recursive: true, force: true }));
|
|
const source = fixtureSource();
|
|
|
|
const bundlePaths = {};
|
|
for (const architecture of ['x64', 'arm64']) {
|
|
const reports = fixtureReports(architecture);
|
|
const reportArguments = [];
|
|
for (const tierName of TIER_NAMES) {
|
|
const reportPath = path.join(
|
|
temporaryDirectory,
|
|
`${architecture}-${tierName}.json`,
|
|
);
|
|
writeJson(reportPath, reports[tierName]);
|
|
reportArguments.push(`--${tierName}=${reportPath}`);
|
|
}
|
|
bundlePaths[architecture] = path.join(
|
|
temporaryDirectory,
|
|
`${architecture}.json`,
|
|
);
|
|
const result = runCli([
|
|
'--mode=bundle',
|
|
...sourceArguments(source),
|
|
`--architecture=${architecture}`,
|
|
...reportArguments,
|
|
`--output=${bundlePaths[architecture]}`,
|
|
]);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(JSON.parse(result.stdout).passed, true);
|
|
validateArchitectureEvidence(
|
|
JSON.parse(fs.readFileSync(bundlePaths[architecture], 'utf8')),
|
|
source,
|
|
architecture,
|
|
);
|
|
}
|
|
|
|
const releasePath = path.join(temporaryDirectory, 'release.json');
|
|
const merge = runCli([
|
|
'--mode=merge',
|
|
...sourceArguments(source),
|
|
`--x64=${bundlePaths.x64}`,
|
|
`--arm64=${bundlePaths.arm64}`,
|
|
`--output=${releasePath}`,
|
|
]);
|
|
assert.equal(merge.status, 0, merge.stderr);
|
|
assert.equal(JSON.parse(merge.stdout).passed, true);
|
|
assert.equal(
|
|
JSON.parse(fs.readFileSync(releasePath, 'utf8')).releaseDigest.length,
|
|
64,
|
|
);
|
|
|
|
const overwrite = runCli([
|
|
'--mode=merge',
|
|
...sourceArguments(source),
|
|
`--x64=${bundlePaths.x64}`,
|
|
`--arm64=${bundlePaths.arm64}`,
|
|
`--output=${releasePath}`,
|
|
]);
|
|
assert.notEqual(overwrite.status, 0);
|
|
assert.match(overwrite.stderr, /EEXIST/);
|
|
});
|
|
|
|
test('rejects symlink evidence inputs', (t) => {
|
|
const temporaryDirectory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-linux-resource-symlink-'),
|
|
);
|
|
t.after(() => fs.rmSync(temporaryDirectory, { recursive: true, force: true }));
|
|
const target = path.join(temporaryDirectory, 'target.json');
|
|
const link = path.join(temporaryDirectory, 'link.json');
|
|
writeJson(target, { passed: true });
|
|
fs.symlinkSync(target, link);
|
|
assert.throws(() => readJsonFile(link, 'evidence'), /non-symlink file/);
|
|
});
|