'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@v7'); 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'); assert.equal(manifest.upgradeReadiness.file, 'upgrade-readiness.sh'); assert.equal(manifest.upgradeRehearsal.file, 'upgrade-rehearsal.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', 'upgrade-readiness.sh', 'upgrade-rehearsal.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'); assert.equal(verification.gates.firstAutomationJourney, 'passed'); assert.equal(verification.gates.ownerCredentialPresentation, 'passed'); assert.equal(verification.gates.legacyUpgradeReadiness, 'passed'); assert.equal(verification.gates.legacyUpgradeStage, '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, /alpha-first-automation/); 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', 'upgrade-readiness.sh', 'upgrade-rehearsal.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', 'upgrade-readiness', 'upgrade-rehearsal', '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 === 'upgrade-readiness') { fs.appendFileSync( path.join(paths.outputRoot, 'upgrade-readiness.sh'), '# drift\n', ); } else if (mutation === 'upgrade-rehearsal') { fs.appendFileSync( path.join(paths.outputRoot, 'upgrade-rehearsal.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', 'upgrade-readiness.sh', 'upgrade-rehearsal.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 ;; *'/owner-credential-install.json'*) printf '%s\\n' '{"status":"installed"}'; 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('generated upgrade readiness drives two read-only legacy inspections', (t) => { const paths = fixture(t); createLocalAlphaTrialKit(createOptions(paths), adapters()); const fakeBin = path.join(paths.fixtureRoot, 'readiness-fake-bin'); fs.mkdirSync(fakeBin); const dockerLog = path.join(paths.fixtureRoot, 'readiness-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) printf '%s\\n' 'sha256:${'2'.repeat( 64, )}|arm64|65532:65532|${revision}|short-lived|none-by-default' exit 0 ;; esac case " $* " in *'/sqlite-inspect.json'*) printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'a'.repeat( 64, )}"}}'; exit 0 ;; *'/data-directory-inspect.json'*) printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'b'.repeat( 64, )}"}}'; exit 0 ;; esac exit 1 `, { mode: 0o755 }, ); const legacyRoot = path.join(paths.fixtureRoot, 'legacy-data'); fs.mkdirSync(path.join(legacyRoot, 'db'), { recursive: true, mode: 0o700 }); fs.writeFileSync(path.join(legacyRoot, 'db', 'database.sqlite'), 'legacy', { mode: 0o600, }); const evidenceRoot = path.join(paths.fixtureRoot, 'upgrade-readiness'); const run = spawnSync( 'sh', [ path.join(paths.outputRoot, 'upgrade-readiness.sh'), 'edge', legacyRoot, evidenceRoot, ], { encoding: 'utf8', env: { ...process.env, FAKE_DOCKER_LOG: dockerLog, PATH: `${fakeBin}:${process.env.PATH}`, }, }, ); assert.equal(run.status, 0, `${run.stderr}\n${run.stdout}`); assert.match(run.stdout, /upgrade readiness inspection completed/); assert.equal( JSON.parse( fs.readFileSync( path.join(evidenceRoot, 'results', 'sqlite-inspect.result.json'), 'utf8', ), ).status, 'inspected', ); const calls = fs.readFileSync(dockerLog, 'utf8'); assert.match(calls, /dst=\/var\/lib\/qinglong2,readonly/); assert.match(calls, /--network none/); assert.match(calls, /--memory 128m --memory-swap 128m/); assert.doesNotMatch(calls, /adoption\.stage|activation\.prepare|cutover/); }); test('generated upgrade rehearsal stages and verifies reviewed legacy plans without cutover', (t) => { const paths = fixture(t); createLocalAlphaTrialKit(createOptions(paths), adapters()); const fakeBin = path.join(paths.fixtureRoot, 'rehearsal-fake-bin'); fs.mkdirSync(fakeBin); const dockerLog = path.join(paths.fixtureRoot, 'rehearsal-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) printf '%s\n' 'sha256:${'2'.repeat( 64, )}|arm64|65532:65532|${revision}|short-lived|none-by-default' exit 0 ;; esac case " $* " in *'/sqlite-stage.json'*) printf '%s\n' '{"status":"staged"}'; exit 0 ;; *'/sqlite-verify.json'*) printf '%s\n' '{"status":"verified","evidence":{"manifestDigest":"${'c'.repeat( 64, )}"}}'; exit 0 ;; *'/sqlite-activation.json'*) printf '%s\n' '{"status":"prepared","evidence":{"activationDigest":"${'d'.repeat( 64, )}"}}'; exit 0 ;; *'/data-directory-stage.json'*) printf '%s\n' '{"status":"staged","evidence":{"manifestDigest":"${'e'.repeat( 64, )}"}}'; exit 0 ;; *'/data-directory-verify.json'*) printf '%s\n' '{"status":"verified"}'; exit 0 ;; esac exit 1 `, { mode: 0o755 }, ); const legacyRoot = path.join(paths.fixtureRoot, 'rehearsal-legacy-data'); fs.mkdirSync(path.join(legacyRoot, 'db'), { recursive: true, mode: 0o700 }); const legacyDatabase = path.join(legacyRoot, 'db', 'database.sqlite'); fs.writeFileSync(legacyDatabase, 'legacy-unchanged', { mode: 0o600 }); const rehearsalRoot = path.join(paths.fixtureRoot, 'upgrade-rehearsal'); const run = spawnSync( 'sh', [ path.join(paths.outputRoot, 'upgrade-rehearsal.sh'), 'edge', legacyRoot, rehearsalRoot, 'a'.repeat(64), 'b'.repeat(64), ], { encoding: 'utf8', env: { ...process.env, FAKE_DOCKER_LOG: dockerLog, PATH: `${fakeBin}:${process.env.PATH}`, }, }, ); assert.equal(run.status, 0, `${run.stderr}\n${run.stdout}`); assert.match(run.stdout, /side-by-side upgrade stage completed/); assert.equal(fs.readFileSync(legacyDatabase, 'utf8'), 'legacy-unchanged'); const summary = JSON.parse( fs.readFileSync(path.join(rehearsalRoot, 'stage-summary.json'), 'utf8'), ); assert.equal(summary.status, 'verified'); assert.equal(summary.legacySource, 'read_only'); assert.equal(summary.cutover, 'not_authorized'); assert.equal(summary.sqlite.activationDigest, 'd'.repeat(64)); assert.equal(summary.dataDirectory.manifestDigest, 'e'.repeat(64)); const calls = fs.readFileSync(dockerLog, 'utf8'); assert.match(calls, /dst=\/var\/lib\/qinglong2,readonly/); assert.match(calls, /--network none/); assert.match(calls, /--memory 128m --memory-swap 128m/); assert.doesNotMatch(calls, /cutover|target-start|legacy-rollback/); }); 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'); });