Files
qinglong/test/back/ql3LocalAlphaTrialKitBundle.test.cjs
T

525 lines
17 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
auditLocalAlphaTrialKit,
createLocalAlphaTrialKit,
createLocalAlphaTrialKitVerificationEvidence,
parseArguments,
sha256File,
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
const {
createClusterImageSbom,
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
const {
readReleaseIdentity,
} = require('../../scripts/lib/ql3-release-identity.cjs');
const root = path.resolve(__dirname, '../..');
const version = readReleaseIdentity(root).version;
const revision = 'a'.repeat(40);
function imageInspection(
role,
idCharacter = role === 'application' ? '1' : '2',
variant = 'headless',
) {
return {
Id: `sha256:${idCharacter.repeat(64)}`,
Os: 'linux',
Architecture: 'arm64',
Config: {
User: '65532:65532',
Labels: {
'org.opencontainers.image.title':
role === 'application'
? variant === 'console'
? 'QingLong 3.0 Local Console Application'
: 'QingLong 3.0 Local Application'
: 'QingLong 3.0 Local Operator',
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
'org.opencontainers.image.revision': revision,
'org.opencontainers.image.version': version,
...(role === 'application'
? {
'io.qinglong.profile':
variant === 'console'
? 'edge-application-api,standalone-application-api'
: 'edge,standalone',
'io.qinglong.ai': 'excluded',
...(variant === 'console'
? { 'io.qinglong.local.console': 'offline-loopback' }
: {}),
}
: {
'io.qinglong.lifecycle': 'short-lived',
'io.qinglong.authority': 'local-owner-management',
'io.qinglong.network': 'none-by-default',
}),
},
},
};
}
function fixture(t, variant = 'headless') {
const fixtureRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-bundle-')),
);
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
const applicationSbom = path.join(fixtureRoot, 'application.json');
const operatorSbom = path.join(fixtureRoot, 'operator.json');
const verificationEvidence = path.join(
fixtureRoot,
'verification-evidence-source.json',
);
const readme = path.join(fixtureRoot, 'README-source.md');
fs.writeFileSync(
applicationSbom,
`${JSON.stringify(
createClusterImageSbom({
root,
image: variant === 'console' ? 'local-console' : 'local',
}),
)}\n`,
);
fs.writeFileSync(
operatorSbom,
`${JSON.stringify(
createClusterImageSbom({ root, image: 'local-operator' }),
)}\n`,
);
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
const paths = {
fixtureRoot,
applicationSbom,
operatorSbom,
verificationEvidence,
readme,
outputRoot: path.join(fixtureRoot, 'bundle'),
};
createLocalAlphaTrialKitVerificationEvidence(
verificationOptions(paths, { variant }),
adapters({}, variant),
);
return paths;
}
function verificationOptions(paths, overrides = {}) {
return {
root,
output: paths.verificationEvidence,
architecture: 'arm64',
variant: 'headless',
sourceRevision: revision,
applicationImage: 'qinglong3-local-application:test-arm64',
operatorImage: 'qinglong3-local-operator:test-arm64',
repository: 'whyour/qinglong',
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
workflowSha: revision,
eventName: 'workflow_dispatch',
job: 'local-image',
runId: '32990652047',
runAttempt: '1',
...overrides,
};
}
function createOptions(paths, variant = 'headless') {
return {
root,
outputRoot: paths.outputRoot,
architecture: 'arm64',
variant,
sourceRevision: revision,
applicationImage: 'qinglong3-local-application:test-arm64',
operatorImage: 'qinglong3-local-operator:test-arm64',
applicationSbom: paths.applicationSbom,
operatorSbom: paths.operatorSbom,
verificationEvidence: paths.verificationEvidence,
readme: paths.readme,
};
}
function adapters(overrides = {}, variant = 'headless') {
return {
inspectImage(image) {
return image.includes('operator')
? imageInspection('operator', '2', variant)
: imageInspection('application', '1', variant);
},
saveImages(images, archivePath) {
assert.deepEqual(images, [
'qinglong3-local-application:test-arm64',
'qinglong3-local-operator:test-arm64',
]);
fs.writeFileSync(archivePath, Buffer.alloc(2048, 7), { flag: 'wx' });
},
...overrides,
};
}
test('materializes and offline-audits one closed two-image trial kit', (t) => {
const paths = fixture(t);
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v4');
assert.equal(manifest.variant, 'headless');
assert.equal(manifest.sourceRevision, revision);
assert.equal(manifest.architecture, 'arm64');
assert.equal(manifest.images.application.architecture, 'arm64');
assert.equal(manifest.images.operator.architecture, 'arm64');
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
assert.equal(manifest.verification.file, 'verification-evidence.json');
assert.equal(manifest.quickstart.file, 'quickstart.sh');
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
assert.equal(syntax.status, 0, syntax.stderr);
const quickstartContents = fs.readFileSync(quickstart, 'utf8');
assert.match(
quickstartContents,
/QingLong 3\.0 Local Alpha is active \(\$VARIANT, \$profile, \$ARCHITECTURE\)/,
);
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
assert.equal(report.compatible, true);
assert.equal(report.sourceRevision, revision);
assert.equal(report.workflowRunId, '32990652047');
assert.equal(report.variant, 'headless');
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
'README.md',
'SHA256SUMS',
'manifest.json',
'qinglong3-local-application.cdx.json',
'qinglong3-local-operator.cdx.json',
'qinglong3-local-trial-kit-arm64.docker.tar',
'quickstart.sh',
'verification-evidence.json',
]);
});
test('materializes a distinct loopback Console trial kit without widening the headless archive', (t) => {
const paths = fixture(t, 'console');
const manifest = createLocalAlphaTrialKit(
createOptions(paths, 'console'),
adapters({}, 'console'),
);
assert.equal(manifest.variant, 'console');
const applicationSbom = JSON.parse(
fs.readFileSync(
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
'utf8',
),
);
assert.equal(
applicationSbom.metadata.properties.find(
(property) => property.name === 'qinglong:image-profile',
).value,
'local-console',
);
const verification = JSON.parse(
fs.readFileSync(
path.join(paths.outputRoot, 'verification-evidence.json'),
'utf8',
),
);
assert.equal(verification.gates.consoleLiveJourney, 'passed');
const quickstartContents = fs.readFileSync(
path.join(paths.outputRoot, 'quickstart.sh'),
'utf8',
);
assert.match(quickstartContents, /VARIANT='console'/);
assert.match(quickstartContents, /network_mode=host/);
assert.match(quickstartContents, /application_config=local-api\.json/);
assert.match(quickstartContents, /"host":"127\.0\.0\.1","port":5700/);
assert.match(quickstartContents, /Console: http:\/\/127\.0\.0\.1:5700\//);
assert.match(quickstartContents, /do not expose the port on LAN/);
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
assert.equal(report.compatible, true);
assert.equal(report.variant, 'console');
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
'README.md',
'SHA256SUMS',
'manifest.json',
'qinglong3-local-application.cdx.json',
'qinglong3-local-console-trial-kit-arm64.docker.tar',
'qinglong3-local-operator.cdx.json',
'quickstart.sh',
'verification-evidence.json',
]);
});
test('fails closed and removes a partial output on incompatible image identity', (t) => {
const paths = fixture(t);
const options = createOptions(paths);
assert.throws(
() =>
createLocalAlphaTrialKit(
options,
adapters({
inspectImage(image) {
const inspection = image.includes('operator')
? imageInspection('operator')
: imageInspection('application');
inspection.Config.Labels['org.opencontainers.image.revision'] =
'b'.repeat(40);
return inspection;
},
}),
),
/image identity is incompatible/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
for (const mutation of [
'archive',
'extra',
'quickstart',
'sbom',
'verification',
]) {
const paths = fixture(t);
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
createLocalAlphaTrialKit(createOptions(paths), adapters());
if (mutation === 'archive') {
fs.appendFileSync(
path.join(
paths.outputRoot,
'qinglong3-local-trial-kit-arm64.docker.tar',
),
'tamper',
);
} else if (mutation === 'extra') {
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
} else if (mutation === 'quickstart') {
fs.appendFileSync(
path.join(paths.outputRoot, 'quickstart.sh'),
'# drift\n',
);
} else if (mutation === 'sbom') {
fs.copyFileSync(
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
path.join(paths.outputRoot, 'qinglong3-local-operator.cdx.json'),
);
} else {
fs.appendFileSync(
path.join(paths.outputRoot, 'verification-evidence.json'),
'tamper',
);
}
assert.throws(
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
/differs|not closed|incompatible/,
mutation,
);
}
});
test('offline audit rejects a rehashed non-canonical quickstart', (t) => {
const paths = fixture(t);
createLocalAlphaTrialKit(createOptions(paths), adapters());
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
fs.appendFileSync(quickstart, '# locally rewritten\n');
const manifestPath = path.join(paths.outputRoot, 'manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
manifest.quickstart = {
file: 'quickstart.sh',
sha256: sha256File(quickstart),
bytes: fs.statSync(quickstart).size,
};
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const checkedFiles = [
'qinglong3-local-trial-kit-arm64.docker.tar',
'qinglong3-local-application.cdx.json',
'qinglong3-local-operator.cdx.json',
'verification-evidence.json',
'quickstart.sh',
'README.md',
'manifest.json',
];
fs.writeFileSync(
path.join(paths.outputRoot, 'SHA256SUMS'),
`${checkedFiles
.map(
(name) =>
`${sha256File(path.join(paths.outputRoot, name)).slice(7)} ${name}`,
)
.join('\n')}\n`,
);
assert.throws(
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
/quickstart differs from the canonical deployment journey/,
);
});
test('generated quickstart drives the closed fresh Edge journey', (t) => {
const paths = fixture(t);
createLocalAlphaTrialKit(createOptions(paths), adapters());
const fakeBin = path.join(paths.fixtureRoot, 'fake-bin');
fs.mkdirSync(fakeBin);
const dockerLog = path.join(paths.fixtureRoot, 'docker.log');
const fakeDocker = path.join(fakeBin, 'docker');
fs.writeFileSync(
fakeDocker,
`#!/bin/sh
printf '%s\\n' "$*" >>"$FAKE_DOCKER_LOG"
case "$1:$2" in
info:|load:*) exit 0 ;;
image:inspect)
case "$*" in
*local-application*) printf '%s\\n' 'sha256:${'1'.repeat(
64,
)}|arm64|65532:65532|${revision}' ;;
*local-operator*) printf '%s\\n' 'sha256:${'2'.repeat(
64,
)}|arm64|65532:65532|${revision}|short-lived|none-by-default' ;;
*) exit 1 ;;
esac
exit 0
;;
logs:*) printf '%s\\n' '{"event":"active"}'; exit 0 ;;
inspect:*) printf '%s\\n' 'true'; exit 0 ;;
esac
case " $* " in
*' --detach '*) printf '%s\\n' 'fake-container-id'; exit 0 ;;
*'/setup.json'*) printf '%s\\n' '{"status":"prepared"}'; exit 0 ;;
*'/owner-provision.json'*) printf '%s\\n' '{"status":"inserted"}'; exit 0 ;;
*'/owner-challenge.json'*) printf '%s\\n' '{"status":"inserted"}'; exit 0 ;;
*'/owner-claim.json'*) printf '%s\\n' '{"status":"inserted","role":"owner"}'; exit 0 ;;
esac
exit 1
`,
{ mode: 0o755 },
);
const dataRoot = path.join(paths.fixtureRoot, 'quickstart-data');
const run = spawnSync(
'sh',
[
path.join(paths.outputRoot, 'quickstart.sh'),
'edge',
dataRoot,
'ql3-alpha-test',
],
{
encoding: 'utf8',
env: {
...process.env,
FAKE_DOCKER_LOG: dockerLog,
PATH: `${fakeBin}:${process.env.PATH}`,
},
},
);
assert.equal(
run.status,
0,
`${run.stderr}\n${run.stdout}\n${fs.readFileSync(dockerLog, 'utf8')}`,
);
assert.match(
run.stdout,
/QingLong 3\.0 Local Alpha is active \(headless, edge, arm64\)/,
);
assert.equal(
fs.readFileSync(path.join(dataRoot, 'container.id'), 'utf8'),
'fake-container-id\n',
);
assert.equal(
JSON.parse(
fs.readFileSync(
path.join(dataRoot, 'results', 'owner-claim.json.result.json'),
'utf8',
),
).role,
'owner',
);
const calls = fs.readFileSync(dockerLog, 'utf8');
assert.match(
calls,
/load --input .*qinglong3-local-trial-kit-arm64\.docker\.tar/,
);
assert.match(calls, /--memory 128m --memory-swap 128m/);
assert.match(calls, /--pids-limit 64/);
assert.doesNotMatch(calls, /--network (?!none)/);
});
test('create rejects an image reference that could alter the shell journey', (t) => {
const paths = fixture(t);
const options = createOptions(paths);
options.applicationImage = 'qinglong3-local-application:test;unexpected';
assert.throws(
() => createLocalAlphaTrialKit(options, adapters()),
/application image reference is invalid/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('create rejects verification detached from the reviewed workflow', (t) => {
const paths = fixture(t);
const evidence = JSON.parse(
fs.readFileSync(paths.verificationEvidence, 'utf8'),
);
evidence.workflow.job = 'unreviewed-job';
fs.writeFileSync(paths.verificationEvidence, `${JSON.stringify(evidence)}\n`);
assert.throws(
() => createLocalAlphaTrialKit(createOptions(paths), adapters()),
/verification evidence is incompatible/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('verification recorder rejects non-milestone workflow provenance', (t) => {
const paths = fixture(t);
const output = path.join(paths.fixtureRoot, 'unreviewed-verification.json');
assert.throws(
() =>
createLocalAlphaTrialKitVerificationEvidence(
verificationOptions(paths, { output, eventName: 'push' }),
adapters(),
),
/verification evidence identity or output is invalid/,
);
assert.equal(fs.existsSync(output), false);
});
test('CLI grammar is exact and separates create from offline audit', () => {
assert.deepEqual(
parseArguments(['--mode=audit', '--bundle=/tmp/ql3-bundle']),
{ mode: 'audit', bundleRoot: '/tmp/ql3-bundle' },
);
assert.throws(
() =>
parseArguments([
'--mode=audit',
'--bundle=/tmp/ql3-bundle',
'--allow-extra=true',
]),
/audit arguments are invalid/,
);
assert.throws(
() => parseArguments(['--mode=create', '--output=/tmp/output']),
/create arguments are invalid/,
);
const recorded = parseArguments([
'--mode=record-verification',
'--application-image=qinglong3-local-application:test-arm64',
'--operator-image=qinglong3-local-operator:test-arm64',
'--architecture=arm64',
'--variant=headless',
`--source-revision=${revision}`,
'--repository=whyour/qinglong',
'--workflow-ref=whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
`--workflow-sha=${revision}`,
'--event=workflow_dispatch',
'--job=local-image',
'--run-id=32990652047',
'--run-attempt=1',
'--output=/tmp/verification-evidence.json',
]);
assert.equal(recorded.mode, 'record-verification');
assert.equal(recorded.runId, '32990652047');
});