feat(ql3): ship selectable local console trial kit

This commit is contained in:
whyour
2026-08-28 06:50:37 +08:00
parent 84d1a61372
commit c60ab7e48a
31 changed files with 1339 additions and 192 deletions
+40 -8
View File
@@ -78,18 +78,19 @@ function writeMilestone(directory, manifest) {
fs.writeFileSync(path.join(directory, 'SHA256SUMS'), checksums(directory));
}
function localManifest(attempt = runAttempt) {
function localManifest(attempt = runAttempt, variant = 'headless') {
return {
schemaVersion: 1,
schema: 'qinglong/alpha-local-milestone@v1',
schemaVersion: 2,
schema: 'qinglong/alpha-local-milestone@v2',
maturity: 'alpha_candidate_not_public_release',
product: 'local',
variant,
version,
sourceRevision: revision,
workflow: workflow('local-alpha-milestone', attempt),
artifacts: {
amd64: {
artifactName: `ql3-alpha-${revision}-local-amd64`,
artifactName: `ql3-alpha-${revision}-local-${variant}-amd64`,
architecture: 'amd64',
bundleManifest: {
file: 'manifest.json',
@@ -102,7 +103,7 @@ function localManifest(attempt = runAttempt) {
verificationSha256: digest('5'),
},
arm64: {
artifactName: `ql3-alpha-${revision}-local-arm64`,
artifactName: `ql3-alpha-${revision}-local-${variant}-arm64`,
architecture: 'arm64',
bundleManifest: {
file: 'manifest.json',
@@ -163,7 +164,10 @@ function fixture(t, options = {}) {
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
const localMilestoneRoot = path.join(fixtureRoot, 'local');
const clusterMilestoneRoot = path.join(fixtureRoot, 'cluster');
writeMilestone(localMilestoneRoot, localManifest(options.localAttempt));
writeMilestone(
localMilestoneRoot,
localManifest(options.localAttempt, options.variant || 'headless'),
);
writeMilestone(clusterMilestoneRoot, clusterManifest(options.clusterAttempt));
const readme = path.join(fixtureRoot, 'README-source.md');
fs.writeFileSync(readme, '# QingLong 3.0 Alpha stage index\n');
@@ -196,11 +200,11 @@ function finalizeOptions(paths) {
test('closes Local and Cluster milestones into one deployment-facing stage index', (t) => {
const paths = fixture(t);
const manifest = finalizeAlphaStageIndex(finalizeOptions(paths));
assert.equal(manifest.schema, 'qinglong/alpha-stage-index@v1');
assert.equal(manifest.schema, 'qinglong/alpha-stage-index@v2');
assert.deepEqual(Object.keys(manifest.milestones), ['local', 'cluster']);
assert.deepEqual(
manifest.deploymentSelections.local.architectures.amd64.requiredArtifacts,
[`ql3-alpha-${revision}-local-amd64`],
[`ql3-alpha-${revision}-local-headless-amd64`],
);
assert.deepEqual(
manifest.deploymentSelections.cluster.architectures.arm64.requiredArtifacts,
@@ -224,6 +228,34 @@ test('closes Local and Cluster milestones into one deployment-facing stage index
assert.deepEqual(report.profiles, ['edge', 'standalone', 'cluster']);
});
test('indexes the Console milestone as a distinct loopback deployment selection', (t) => {
const paths = fixture(t, { variant: 'console' });
const manifest = finalizeAlphaStageIndex(finalizeOptions(paths));
assert.equal(
manifest.milestones.local.artifactName,
`ql3-alpha-${revision}-local-console-milestone`,
);
assert.equal(manifest.deploymentSelections.local.variant, 'console');
assert.deepEqual(manifest.deploymentSelections.local.profiles, [
'edge-application-api',
'standalone-application-api',
]);
assert.equal(
manifest.deploymentSelections.local.intent,
'fresh_loopback_console_non_production_trial',
);
const report = auditAlphaStageIndex({
stageRoot: paths.outputRoot,
localMilestoneRoot: paths.localMilestoneRoot,
clusterMilestoneRoot: paths.clusterMilestoneRoot,
});
assert.deepEqual(report.profiles, [
'edge-application-api',
'standalone-application-api',
'cluster',
]);
});
test('rejects Local and Cluster milestones from different workflow attempts', (t) => {
const paths = fixture(t, { clusterAttempt: '2' });
assert.throws(
+34
View File
@@ -128,6 +128,40 @@ test('generates the AI-excluded local application image closure', () => {
);
});
test('generates the bounded AI-excluded local Console image closure', () => {
const document = createClusterImageSbom({ root, image: 'local-console' });
const report = auditClusterImageSbom(document, {
root,
image: 'local-console',
});
assert.deepEqual(report, {
image: 'local-console',
root: `pkg:npm/%40qinglong/local-application-image@${version}`,
components: 12,
externalComponents: 2,
internalComponents: 10,
dependencyNodes: 13,
inventoryVerified: false,
});
assert.equal(
document.components.some(
(component) => component.name === '@qinglong/local-api',
),
true,
);
assert.equal(
document.components.some(
(component) => component.name === '@qinglong/local-owner-console',
),
true,
);
assert.equal(
document.components.some((component) => component.name === '@qinglong/ai'),
false,
);
});
test('generates the headless Worker image runtime closure', () => {
const document = createClusterImageSbom({ root, image: 'worker' });
const report = auditClusterImageSbom(document, {
@@ -56,6 +56,7 @@ test('accepts the empty fail-closed production exception policy', () => {
control: 0,
'control-ai': 0,
local: 0,
'local-console': 0,
'local-operator': 0,
worker: 0,
},
+54 -10
View File
@@ -29,7 +29,7 @@ const revision = 'a'.repeat(40);
const runId = '33047425710';
const runAttempt = '2';
function imageInspection(role, architecture, idCharacter) {
function imageInspection(role, architecture, idCharacter, variant = 'headless') {
return {
Id: `sha256:${idCharacter.repeat(64)}`,
Os: 'linux',
@@ -39,15 +39,23 @@ function imageInspection(role, architecture, idCharacter) {
Labels: {
'org.opencontainers.image.title':
role === 'application'
? 'QingLong 3.0 Local 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': 'edge,standalone',
'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',
@@ -60,6 +68,7 @@ function imageInspection(role, architecture, idCharacter) {
}
function createBundle(fixtureRoot, architecture, options = {}) {
const variant = options.variant || 'headless';
const bundleFixture = path.join(fixtureRoot, architecture);
fs.mkdirSync(bundleFixture);
const applicationSbom = path.join(bundleFixture, 'application.json');
@@ -69,7 +78,12 @@ function createBundle(fixtureRoot, architecture, options = {}) {
const outputRoot = path.join(bundleFixture, 'bundle');
fs.writeFileSync(
applicationSbom,
`${JSON.stringify(createClusterImageSbom({ root, image: 'local' }))}\n`,
`${JSON.stringify(
createClusterImageSbom({
root,
image: variant === 'console' ? 'local-console' : 'local',
}),
)}\n`,
);
fs.writeFileSync(
operatorSbom,
@@ -88,7 +102,7 @@ function createBundle(fixtureRoot, architecture, options = {}) {
const adapters = {
inspectImage(image) {
const role = image.includes('operator') ? 'operator' : 'application';
return imageInspection(role, architecture, characters[role]);
return imageInspection(role, architecture, characters[role], variant);
},
saveImages(images, archivePath) {
assert.deepEqual(images, [applicationImage, operatorImage]);
@@ -104,6 +118,7 @@ function createBundle(fixtureRoot, architecture, options = {}) {
root,
output: verificationEvidence,
architecture,
variant,
sourceRevision: revision,
applicationImage,
operatorImage,
@@ -123,6 +138,7 @@ function createBundle(fixtureRoot, architecture, options = {}) {
root,
outputRoot,
architecture,
variant,
sourceRevision: revision,
applicationImage,
operatorImage,
@@ -152,9 +168,16 @@ function fixture(t, options = {}) {
readme,
outputRoot: path.join(fixtureRoot, 'milestone'),
bundles: {
amd64: createBundle(fixtureRoot, 'amd64', options.amd64),
arm64: createBundle(fixtureRoot, 'arm64', options.arm64),
amd64: createBundle(fixtureRoot, 'amd64', {
...options.amd64,
variant: options.variant || 'headless',
}),
arm64: createBundle(fixtureRoot, 'arm64', {
...options.arm64,
variant: options.variant || 'headless',
}),
},
variant: options.variant || 'headless',
};
}
@@ -165,6 +188,7 @@ function finalizeOptions(paths) {
bundles: paths.bundles,
readme: paths.readme,
sourceRevision: revision,
variant: paths.variant,
repository: 'whyour/qinglong',
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
workflowSha: revision,
@@ -177,16 +201,17 @@ function finalizeOptions(paths) {
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
const paths = fixture(t);
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v1');
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v2');
assert.equal(manifest.variant, 'headless');
assert.equal(manifest.sourceRevision, revision);
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
assert.equal(
manifest.artifacts.amd64.artifactName,
`ql3-alpha-${revision}-local-amd64`,
`ql3-alpha-${revision}-local-headless-amd64`,
);
assert.equal(
manifest.artifacts.arm64.artifactName,
`ql3-alpha-${revision}-local-arm64`,
`ql3-alpha-${revision}-local-headless-arm64`,
);
assert.notEqual(
manifest.artifacts.amd64.archiveSha256,
@@ -204,6 +229,25 @@ test('finalizes two exact native trial kits into one closed milestone index', (t
assert.deepEqual(report.architectures, ['amd64', 'arm64']);
assert.equal(report.workflowRunId, runId);
assert.equal(report.workflowRunAttempt, runAttempt);
assert.equal(report.variant, 'headless');
});
test('finalizes Console trial kits as a separately named milestone', (t) => {
const paths = fixture(t, { variant: 'console' });
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
assert.equal(manifest.variant, 'console');
assert.equal(
manifest.artifacts.amd64.artifactName,
`ql3-alpha-${revision}-local-console-amd64`,
);
assert.equal(
manifest.artifacts.arm64.artifactName,
`ql3-alpha-${revision}-local-console-arm64`,
);
assert.equal(
auditLocalAlphaMilestone({ milestoneRoot: paths.outputRoot }).variant,
'console',
);
});
test('rejects a trial kit from another run attempt before publishing', (t) => {
+83 -13
View File
@@ -27,6 +27,7 @@ const revision = 'a'.repeat(40);
function imageInspection(
role,
idCharacter = role === 'application' ? '1' : '2',
variant = 'headless',
) {
return {
Id: `sha256:${idCharacter.repeat(64)}`,
@@ -37,15 +38,23 @@ function imageInspection(
Labels: {
'org.opencontainers.image.title':
role === 'application'
? 'QingLong 3.0 Local 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': 'edge,standalone',
'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',
@@ -57,7 +66,7 @@ function imageInspection(
};
}
function fixture(t) {
function fixture(t, variant = 'headless') {
const fixtureRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-bundle-')),
);
@@ -71,7 +80,12 @@ function fixture(t) {
const readme = path.join(fixtureRoot, 'README-source.md');
fs.writeFileSync(
applicationSbom,
`${JSON.stringify(createClusterImageSbom({ root, image: 'local' }))}\n`,
`${JSON.stringify(
createClusterImageSbom({
root,
image: variant === 'console' ? 'local-console' : 'local',
}),
)}\n`,
);
fs.writeFileSync(
operatorSbom,
@@ -89,8 +103,8 @@ function fixture(t) {
outputRoot: path.join(fixtureRoot, 'bundle'),
};
createLocalAlphaTrialKitVerificationEvidence(
verificationOptions(paths),
adapters(),
verificationOptions(paths, { variant }),
adapters({}, variant),
);
return paths;
}
@@ -100,6 +114,7 @@ function verificationOptions(paths, overrides = {}) {
root,
output: paths.verificationEvidence,
architecture: 'arm64',
variant: 'headless',
sourceRevision: revision,
applicationImage: 'qinglong3-local-application:test-arm64',
operatorImage: 'qinglong3-local-operator:test-arm64',
@@ -114,11 +129,12 @@ function verificationOptions(paths, overrides = {}) {
};
}
function createOptions(paths) {
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',
@@ -129,12 +145,12 @@ function createOptions(paths) {
};
}
function adapters(overrides = {}) {
function adapters(overrides = {}, variant = 'headless') {
return {
inspectImage(image) {
return image.includes('operator')
? imageInspection('operator')
: imageInspection('application');
? imageInspection('operator', '2', variant)
: imageInspection('application', '1', variant);
},
saveImages(images, archivePath) {
assert.deepEqual(images, [
@@ -150,7 +166,8 @@ function adapters(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.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');
@@ -164,13 +181,14 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
const quickstartContents = fs.readFileSync(quickstart, 'utf8');
assert.match(
quickstartContents,
/QingLong 3\.0 Local Alpha is active \(\$profile, \$ARCHITECTURE\)/,
/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',
@@ -183,6 +201,57 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
]);
});
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);
@@ -352,7 +421,7 @@ exit 1
);
assert.match(
run.stdout,
/QingLong 3\.0 Local Alpha is active \(edge, arm64\)/,
/QingLong 3\.0 Local Alpha is active \(headless, edge, arm64\)/,
);
assert.equal(
fs.readFileSync(path.join(dataRoot, 'container.id'), 'utf8'),
@@ -439,6 +508,7 @@ test('CLI grammar is exact and separates create from offline audit', () => {
'--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',
@@ -0,0 +1,77 @@
'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 root = path.resolve(__dirname, '../..');
const script = path.join(root, 'scripts/ql3-local-console-image-inventory.cjs');
const packages = [
'@qinglong/local-admin',
'@qinglong/local-api',
'@qinglong/local-application',
'@qinglong/local-command-file',
'@qinglong/local-execution',
'@qinglong/local-owner-console',
'@qinglong/local-process',
'@qinglong/local-secret',
'@qinglong/local-sqlite',
'@qinglong/runtime-core',
'croner',
'semver',
];
function inventoryFixture(t) {
const inventoryRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-console-inventory-')),
);
t.after(() => fs.rmSync(inventoryRoot, { recursive: true, force: true }));
for (const packageName of packages) {
const packageRoot = path.join(inventoryRoot, packageName);
fs.mkdirSync(packageRoot, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, 'package.json'),
`${JSON.stringify({
name: packageName,
version: '3.0.0-alpha.1',
})}\n`,
);
}
return inventoryRoot;
}
function runInventory(inventoryRoot) {
return spawnSync(
process.execPath,
[script, `--inventory-root=${inventoryRoot}`],
{ encoding: 'utf8' },
);
}
test('accepts the exact bounded Local Console package closure', (t) => {
const inventoryRoot = inventoryFixture(t);
const run = runInventory(inventoryRoot);
assert.equal(run.status, 0, run.stderr);
const report = JSON.parse(run.stdout);
assert.equal(report.image, 'local-console');
assert.equal(report.packageCount, 12);
assert.equal(report.ai, 'excluded');
assert.equal(report.listener, 'loopback-only');
assert.equal(report.compatible, true);
});
test('rejects one unreviewed runtime package', (t) => {
const inventoryRoot = inventoryFixture(t);
const extra = path.join(inventoryRoot, 'unreviewed');
fs.mkdirSync(extra);
fs.writeFileSync(
path.join(extra, 'package.json'),
'{"name":"unreviewed","version":"1.0.0"}\n',
);
const run = runInventory(inventoryRoot);
assert.notEqual(run.status, 0);
assert.match(run.stderr, /package closure drifted/);
});
+44
View File
@@ -63,6 +63,50 @@ test('accepts the exact AI-excluded local application image contract', () => {
'croner',
'semver',
]);
assert.deepEqual(report.consoleRuntimePackages, [
'@qinglong/local-admin',
'@qinglong/local-api',
'@qinglong/local-application',
'@qinglong/local-command-file',
'@qinglong/local-execution',
'@qinglong/local-owner-console',
'@qinglong/local-process',
'@qinglong/local-secret',
'@qinglong/local-sqlite',
'@qinglong/runtime-core',
'croner',
'semver',
]);
});
test('rejects Console assets, loopback identity or retained JavaScript drift', () => {
const current = fixture();
try {
const dockerfilePath = path.join(current.target, 'Dockerfile');
const dockerfile = fs
.readFileSync(dockerfilePath, 'utf8')
.replace(
'--retain-js=local-api/assets/console/console.js',
'--retain-js=local-api/assets/console/unreviewed.js',
)
.replace('io.qinglong.local.console="offline-loopback"', 'io.qinglong.local.console="public"');
fs.writeFileSync(dockerfilePath, dockerfile);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) =>
code === 'CONSOLE_RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED',
),
);
assert.ok(
report.findings.some(
({ code }) => code === 'CONSOLE_RUNTIME_IDENTITY_OR_LABEL_DRIFT',
),
);
} finally {
current.close();
}
});
test('rejects a mutable runtime base image', () => {
@@ -1,4 +1,5 @@
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');
@@ -575,6 +576,37 @@ test('rejects invalid or missing explicit JavaScript assets before mutation', ()
}
});
test('CLI retains one reviewed JavaScript asset outside dist', () => {
const current = fixture();
try {
const application = writePackage(
current.root,
'local-api',
'@qinglong/local-api',
);
fs.writeFileSync(path.join(application.dist, 'index.js'), "'use strict';\n");
const assets = path.join(application.packageRoot, 'assets/console');
fs.mkdirSync(assets, { recursive: true });
const retained = path.join(assets, 'console.js');
fs.writeFileSync(retained, "'use strict';\n");
const run = spawnSync(
process.execPath,
[
path.resolve(__dirname, '../../scripts/ql3-prune-runtime-artifact.cjs'),
current.root,
'@qinglong/local-api',
'--retain-js=local-api/assets/console/console.js',
],
{ encoding: 'utf8' },
);
assert.equal(run.status, 0, run.stderr);
assert.equal(fs.existsSync(retained), true);
assert.equal(JSON.parse(run.stdout).schemaVersion, 1);
} finally {
current.close();
}
});
test('rejects symbolic links before making partial changes', () => {
const current = fixture();
try {
+20 -1
View File
@@ -109,6 +109,25 @@ test('audits one source-derived QingLong 3 release identity', () => {
});
});
test('requires version identity on both headless and Console runtime targets', (t) => {
const fixture = createFixture(t);
const dockerfilePath = path.join(
fixture,
'deploy/containers/ql3-local-application/Dockerfile',
);
const dockerfile = fs.readFileSync(dockerfilePath, 'utf8');
const label = `org.opencontainers.image.version="${SOURCE_VERSION}"`;
assert.equal(dockerfile.split(label).length - 1, 2);
fs.writeFileSync(
dockerfilePath,
dockerfile.replace(label, 'org.opencontainers.image.version="drifted"'),
);
assert.throws(
() => auditReleaseVersionContract(fixture),
/container Dockerfile release identity drifted/,
);
});
test('plans the exact governed version surface without touching legacy 2.x', () => {
const plan = createVersionTransitionPlan({
root,
@@ -116,7 +135,7 @@ test('plans the exact governed version surface without touching legacy 2.x', ()
targetVersion: TARGET_VERSION,
});
assert.equal(plan.fileCount, 66);
assert.equal(plan.replacementCount, 86);
assert.equal(plan.replacementCount, 87);
assert.equal(plan.legacyRootPackageVersion, LEGACY_VERSION);
assert.equal(plan.legacyRootExcluded, true);
assert.equal(