'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', ) { return { Id: `sha256:${idCharacter.repeat(64)}`, Os: 'linux', Architecture: 'arm64', Config: { User: '65532:65532', Labels: { 'org.opencontainers.image.title': role === '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': 'edge,standalone', 'io.qinglong.ai': 'excluded', } : { 'io.qinglong.lifecycle': 'short-lived', 'io.qinglong.authority': 'local-owner-management', 'io.qinglong.network': 'none-by-default', }), }, }, }; } function fixture(t) { 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: '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), adapters(), ); return paths; } function verificationOptions(paths, overrides = {}) { return { root, output: paths.verificationEvidence, architecture: 'arm64', 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) { return { root, outputRoot: paths.outputRoot, architecture: 'arm64', 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 = {}) { return { inspectImage(image) { return image.includes('operator') ? imageInspection('operator') : imageInspection('application'); }, 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@v3'); 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 \(\$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.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('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 \(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', `--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'); });