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

831 lines
27 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@v8');
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');
assert.equal(
manifest.upgradeCutoverRehearsal.file,
'upgrade-cutover-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 cutoverRehearsal = path.join(
paths.outputRoot,
'upgrade-cutover-rehearsal.sh',
);
const cutoverSyntax = spawnSync('sh', ['-n', cutoverRehearsal], {
encoding: 'utf8',
});
assert.equal(cutoverSyntax.status, 0, cutoverSyntax.stderr);
const cutoverRehearsalContents = fs.readFileSync(cutoverRehearsal, 'utf8');
assert.match(cutoverRehearsalContents, /VARIANT='headless'/);
assert.match(
cutoverRehearsalContents,
/"transformationRoot":"\$rehearsal_root\/data-directory\/transformation"/,
);
assert.doesNotMatch(
cutoverRehearsalContents,
/mkdir[^\n]*data-directory\/transformation|for directory in[^\n]*data-directory\/transformation/,
);
assert.match(
cutoverRehearsalContents,
/docker_socket=\$\(realpath \/var\/run\/docker\.sock\)/,
);
assert.match(
cutoverRehearsalContents,
/operator_docker_socket=\/run\/docker\.sock/,
);
assert.match(
cutoverRehearsalContents,
/--mount "type=bind,src=\$docker_socket,dst=\$operator_docker_socket"/,
);
assert.match(
cutoverRehearsalContents,
/"dockerSocketPath":"\$operator_docker_socket"/,
);
assert.doesNotMatch(
cutoverRehearsalContents,
/"dockerSocketPath":"\/var\/run\/docker\.sock"|dst=\/var\/run\/docker\.sock/,
);
const legacyStopTemplate = cutoverRehearsalContents.match(
/cat >"\$rehearsal_root\/commands\/legacy-stop\.json" <<EOF\n([^\n]+)\nEOF/,
);
assert.ok(legacyStopTemplate, 'legacy-stop command template is missing');
const legacyStopCommand = JSON.parse(
legacyStopTemplate[1]
.replaceAll('$allow_root_service', 'false')
.replaceAll('$now_ms', '1')
.replace(/\$[a-z_]+/g, 'fixture'),
);
assert.deepEqual(Object.keys(legacyStopCommand.request).sort(), [
'activationPath',
'cutoverId',
'expectedActivationDigest',
'expectedLegacyContainerId',
'expectedLegacyDatabasePath',
'instanceId',
'legacySourcePath',
'profile',
'requestedAtMs',
]);
for (const phase of [
'bootstrap Owner authority',
'transform reviewed Legacy data',
'stop exact Legacy container',
'apply transformed Legacy data',
'prepare adopted deployment bundle',
'create exact target container',
'start exact target container',
'stop target and prove rollback candidate',
]) {
assert.match(
cutoverRehearsalContents,
new RegExp(`phase '${phase}'`),
);
}
assert.match(
cutoverRehearsalContents,
/QingLong Local Alpha target-start result:/,
);
assert.match(
cutoverRehearsalContents,
/QingLong Local Alpha target-stop result:/,
);
assert.match(
cutoverRehearsalContents,
/QingLong Local Alpha target-stop evidence:/,
);
assert.match(cutoverRehearsalContents, /tr -d '\\n'/);
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-cutover-rehearsal.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');
assert.equal(verification.gates.legacyUpgradeCutover, 'not_applicable');
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 cutoverContents = fs.readFileSync(
path.join(paths.outputRoot, 'upgrade-cutover-rehearsal.sh'),
'utf8',
);
assert.match(cutoverContents, /VARIANT='console'/);
assert.match(cutoverContents, /available only in the headless Trial Kit/);
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-cutover-rehearsal.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',
'upgrade-cutover-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 === 'upgrade-cutover-rehearsal') {
fs.appendFileSync(
path.join(paths.outputRoot, 'upgrade-cutover-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',
'upgrade-cutover-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,
new RegExp(`src=${legacyRoot},dst=${legacyRoot},readonly`),
);
assert.equal(
JSON.parse(
fs.readFileSync(path.join(evidenceRoot, 'sqlite-inspect.json'), 'utf8'),
).options.sourcePath,
path.join(legacyRoot, 'db', 'database.sqlite'),
);
assert.equal(
JSON.parse(
fs.readFileSync(
path.join(evidenceRoot, 'data-directory-inspect.json'),
'utf8',
),
).options.dataRoot,
legacyRoot,
);
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,
new RegExp(`dst=${legacyRoot.replaceAll('/', '\\/')},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');
});