feat(ql3): add read-only cluster copilot console

This commit is contained in:
whyour
2026-08-16 04:35:28 +08:00
parent da4e7cf688
commit c4a1238a92
32 changed files with 3407 additions and 20 deletions
@@ -2,6 +2,7 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
@@ -47,3 +48,13 @@ test('fails closed before Docker without explicit opt-in', () => {
assert.match(result.stderr, /QL3_CLUSTER_ADMIN_PRODUCT_LIVE=1 is required/);
assert.equal(result.stderr.includes('spawn'), false);
});
test('binds the live image gate to loopback Console assets and shutdown', () => {
const source = fs.readFileSync(script, 'utf8');
assert.match(source, /function runConsoleContract\(image\)/);
assert.match(source, /\[facade, 'copilot-console'/);
assert.match(source, /body\.includes\('Cluster field console'\)/);
assert.match(source, /runConsoleContract\(image\);/);
assert.match(source, /consoleLoopback: true/);
assert.match(source, /consoleAssets: true/);
});
@@ -0,0 +1,155 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const {
auditClusterCopilotConsole,
} = require('../../scripts/ql3-cluster-copilot-console-audit.cjs');
const root = path.resolve(__dirname, '../..');
function intercept(target, mutate) {
return (relativePath) => {
const source = fs.readFileSync(path.join(root, relativePath), 'utf8');
return relativePath === target ? mutate(source) : source;
};
}
test('keeps the QingLong 3.0 Copilot Console independent and read-only', () => {
const report = auditClusterCopilotConsole({ root });
assert.deepEqual(report, {
schemaVersion: 1,
component: 'cluster-copilot-console',
owner: '@qinglong/cluster-admin',
lifecycle: 'operator-workstation-loopback',
operations: ['inspect', 'output'],
legacyUiCoupled: false,
kubernetesResident: false,
assetCount: 3,
sourceFileCount: 4,
findings: [],
compatible: true,
});
});
test('rejects a remote listener or mutation vocabulary', () => {
const listener = auditClusterCopilotConsole({
root,
readFile: intercept(
'packages/ql3-cluster-admin/src/copilot-console/server.ts',
(source) => source.replaceAll('127.0.0.1', '0.0.0.0'),
),
});
const mutation = auditClusterCopilotConsole({
root,
readFile: intercept(
'packages/ql3-cluster-admin/src/copilot-console/contracts.ts',
(source) =>
source.replace(
"'inspect' | 'output'",
"'inspect' | 'output' | 'cancel'",
),
),
});
assert.equal(listener.compatible, false);
assert.equal(mutation.compatible, false);
assert.ok(
listener.findings.some(
({ code }) => code === 'CLUSTER_COPILOT_CONSOLE_CONTRACT_MISSING',
),
);
assert.ok(
mutation.findings.some(
({ code }) => code === 'CLUSTER_COPILOT_CONSOLE_AUTHORITY_WIDENED',
),
);
});
test('rejects browser persistence, dynamic rendering and product drift', () => {
for (const injected of ['localStorage', 'innerHTML', 'WebSocket']) {
const report = auditClusterCopilotConsole({
root,
readFile: intercept(
'packages/ql3-cluster-admin/assets/copilot-console/app.js',
(source) => source + '\n// ' + injected + '\n',
),
});
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'CLUSTER_COPILOT_CONSOLE_AUTHORITY_WIDENED',
),
);
}
const product = auditClusterCopilotConsole({
root,
readFile: intercept(
'packages/ql3-cluster-admin/src/product-cli/productCommand.ts',
(source) => source.replace("name: 'copilot-console'", "name: 'removed'"),
),
});
assert.equal(product.compatible, false);
assert.ok(
product.findings.some(
({ code }) => code === 'CLUSTER_COPILOT_CONSOLE_PRODUCT_ENTRY_MISSING',
),
);
const image = auditClusterCopilotConsole({
root,
readFile: intercept(
'deploy/containers/ql3-cluster-admin/Dockerfile',
(source) =>
source.replaceAll(
'packages/ql3-cluster-admin/assets/copilot-console',
'packages/ql3-cluster-admin/assets/removed',
),
),
});
assert.equal(image.compatible, false);
assert.ok(
image.findings.some(
({ code, target }) =>
code === 'CLUSTER_COPILOT_CONSOLE_CONTRACT_MISSING' &&
target === 'deploy/containers/ql3-cluster-admin/Dockerfile',
),
);
});
test('rejects coupling into the legacy UI or Kubernetes workloads', () => {
const legacyTarget = 'src/pages/login/index.tsx';
const legacy = auditClusterCopilotConsole({
root,
readFile: intercept(
legacyTarget,
(source) => source + '\n// ql3-copilot-console\n',
),
});
const kubernetesTarget =
'deploy/kubernetes/ql3-cluster/base/deployment.yaml';
const kubernetes = auditClusterCopilotConsole({
root,
readFile: intercept(
kubernetesTarget,
(source) => source + '\n# ql3-copilot-console\n',
),
});
assert.equal(legacy.compatible, false);
assert.equal(kubernetes.compatible, false);
assert.ok(
legacy.findings.some(
({ code, target }) =>
code === 'CLUSTER_COPILOT_CONSOLE_LEGACY_UI_COUPLED' &&
target === legacyTarget,
),
);
assert.ok(
kubernetes.findings.some(
({ code, target }) =>
code === 'CLUSTER_COPILOT_CONSOLE_KUBERNETES_RESIDENT' &&
target === kubernetesTarget,
),
);
});
@@ -14,6 +14,10 @@ const {
test('ships runtime JavaScript and declarations without development maps', () => {
for (const [packagePath, files] of [
['packages/ql3-runtime-core', ['dist/**/*.js', 'dist/**/*.d.ts']],
[
'packages/ql3-cluster-admin',
['dist/**/*.js', 'dist/**/*.d.ts', 'assets/copilot-console/*'],
],
[
'packages/ql3-local-process',
['dist/**/*.js', 'dist/**/*.d.ts', 'assets'],
+1 -1
View File
@@ -135,7 +135,7 @@ function createFixture(t, options = {}) {
? isControlAi
? 'Optional QingLong 3.0 AI-enabled cluster control plane'
: 'QingLong 3.0 PostgreSQL-backed cluster control plane'
: 'QingLong 3.0 cluster operations and bounded stdio MCP',
: 'QingLong 3.0 cluster operations and bounded Copilot surfaces',
'org.opencontainers.image.licenses': 'Apache-2.0',
'org.opencontainers.image.revision': revision,
'org.opencontainers.image.source':
+2 -2
View File
@@ -340,10 +340,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: clusterAdmin.rootSourceFileRoles,
},
{
sourceFiles: 116,
sourceFiles: 120,
rootSourceFiles: 1,
rootSourceLines: 61,
nestedSourceFiles: 115,
nestedSourceFiles: 119,
rootSourceFileRoles: {
'modelInvocationMigrationCli.ts': 'binary_entry',
},