mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): ship selectable local console trial kit
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user