Files
qinglong/packages/ql3-cluster-admin/test/copilotMcp.test.cjs
T

395 lines
12 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { InMemoryTransport } = require('@modelcontextprotocol/server');
const {
CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
ClusterCopilotClientRemoteError,
} = require('../dist/copilot-client/client.js');
const {
CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
} = require('../dist/copilot-client/contracts.js');
const {
CLUSTER_COPILOT_MCP_RESULT_SCHEMA,
CLUSTER_COPILOT_MCP_SERVER_CONFIG_SCHEMA,
createQingLongClusterCopilotMcpServer,
normalizeClusterCopilotMcpServerConfig,
readClusterCopilotMcpServerConfig,
} = require('../dist/copilot-mcp/server.js');
const packageRoot = path.resolve(__dirname, '..');
const caFixture = path.resolve(
packageRoot,
'../ql3-cluster-control/test/fixtures/mtls/ca-cert.pem',
);
const credential = `ql3c_credential-1_${Buffer.alloc(32, 7).toString('base64url')}`;
function temporaryDirectory(t) {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-copilot-mcp-')),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return directory;
}
function privateFile(directory, name, contents) {
const filePath = path.join(directory, name);
fs.writeFileSync(filePath, contents, { mode: 0o600 });
return fs.realpathSync(filePath);
}
function config(maxConcurrentRequests = 2) {
return Object.freeze({
schema: CLUSTER_COPILOT_MCP_SERVER_CONFIG_SCHEMA,
clientConfigFile: '/private/client.json',
credentialFile: '/private/credential',
maxConcurrentRequests,
});
}
async function client(server, t) {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const pending = new Map();
clientTransport.onmessage = (message) => {
const waiter = pending.get(message.id);
if (waiter) {
pending.delete(message.id);
waiter(message);
}
};
await server.connect(serverTransport);
await clientTransport.start();
t.after(async () => {
await clientTransport.close();
await server.close();
});
let nextId = 1;
const request = (method, params = undefined) => {
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, resolve);
clientTransport
.send({
jsonrpc: '2.0',
id,
method,
...(params === undefined ? {} : { params }),
})
.catch(reject);
});
};
const initialized = await request('initialize', {
protocolVersion: '2025-11-25',
capabilities: {},
clientInfo: { name: 'ql3-test', version: '1.0.0' },
});
assert.equal(initialized.result.protocolVersion, '2025-11-25');
await clientTransport.send({
jsonrpc: '2.0',
method: 'notifications/initialized',
});
return { request };
}
test('discovers four exact bounded Tools and maps every call to a direct command', async (t) => {
const executions = [];
const server = createQingLongClusterCopilotMcpServer({
config: config(),
execute: async (execution) => {
executions.push(execution);
return Object.freeze({
schemaVersion: 1,
operation: execution.command.operation,
requestId: execution.command.requestId,
result: Object.freeze({ accepted: true }),
});
},
});
const connected = await client(server, t);
const listed = await connected.request('tools/list', {});
assert.deepEqual(
listed.result.tools.map((tool) => tool.name),
[
'qinglong.cluster.copilot.failure_diagnose',
'qinglong.cluster.copilot.failure_diagnosis.get',
'qinglong.cluster.copilot.failure_diagnosis.output.get',
'qinglong.cluster.copilot.failure_diagnosis.cancel',
],
);
assert.deepEqual(listed.result.tools.map((tool) => tool.annotations), [
{
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
readOnlyHint: false,
},
{
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
readOnlyHint: true,
},
{
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
readOnlyHint: true,
},
{
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
readOnlyHint: false,
},
]);
for (const tool of listed.result.tools) {
assert.equal(tool.inputSchema.additionalProperties, false);
assert.equal(tool.outputSchema.additionalProperties, false);
}
const base = {
projectId: 'project-1',
sourceRunId: 'source-run-1',
requestId: 'request-1',
};
const calls = [
['qinglong.cluster.copilot.failure_diagnose', { ...base, traceId: 'trace-1' }],
['qinglong.cluster.copilot.failure_diagnosis.get', base],
['qinglong.cluster.copilot.failure_diagnosis.output.get', base],
['qinglong.cluster.copilot.failure_diagnosis.cancel', { ...base, mutationId: 'mutation-1' }],
];
const responses = [];
for (const [name, argumentsValue] of calls) {
responses.push(
await connected.request('tools/call', { name, arguments: argumentsValue }),
);
}
assert.deepEqual(
executions.map((execution) => execution.command),
[
{
schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
operation: 'diagnose',
...base,
traceId: 'trace-1',
},
{ schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA, operation: 'inspect', ...base },
{ schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA, operation: 'output', ...base },
{
schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
operation: 'cancel',
...base,
mutationId: 'mutation-1',
},
],
);
assert.ok(executions.every((execution) => execution.configFile === '/private/client.json'));
assert.ok(executions.every((execution) => execution.credentialFile === '/private/credential'));
assert.deepEqual(
responses.map((response) => response.result.structuredContent.sensitivity),
['low', 'low', 'potentially_sensitive', 'low'],
);
assert.deepEqual(responses[2].result.structuredContent, {
schema: CLUSTER_COPILOT_MCP_RESULT_SCHEMA,
operation: 'output',
requestId: 'request-1',
sensitivity: 'potentially_sensitive',
trust: {
classification: 'untrusted_model_output',
instructionPolicy: 'data_only_never_execute',
actionAuthority: 'none',
},
result: { accepted: true },
});
});
test('fails closed on unknown input and returns only bounded remote error detail', async (t) => {
let calls = 0;
const server = createQingLongClusterCopilotMcpServer({
config: config(),
execute: async (execution) => {
calls += 1;
throw new ClusterCopilotClientRemoteError(
429,
'quota_exhausted',
execution.command.requestId,
12,
);
},
});
const connected = await client(server, t);
const invalid = await connected.request('tools/call', {
name: 'qinglong.cluster.copilot.failure_diagnosis.get',
arguments: {
projectId: 'project-1',
sourceRunId: 'source-run-1',
requestId: 'request-1',
endpoint: 'https://forbidden.example/',
},
});
assert.equal(calls, 0);
assert.ok(invalid.error || invalid.result?.isError);
assert.doesNotMatch(JSON.stringify(invalid), /forbidden\.example/);
const rejected = await connected.request('tools/call', {
name: 'qinglong.cluster.copilot.failure_diagnosis.get',
arguments: {
projectId: 'project-1',
sourceRunId: 'source-run-1',
requestId: 'request-1',
},
});
assert.equal(calls, 1);
assert.equal(rejected.result.isError, true);
assert.deepEqual(JSON.parse(rejected.result.content[0].text), {
code: 'copilot_remote_rejected',
statusCode: 429,
responseCode: 'quota_exhausted',
requestId: 'request-1',
retryAfterSeconds: 12,
});
});
test('rejects shared-client result drift and unbounded remote error fields', async (t) => {
const argumentsValue = {
projectId: 'project-1',
sourceRunId: 'source-run-1',
requestId: 'request-1',
};
const drifted = await client(
createQingLongClusterCopilotMcpServer({
config: config(),
execute: async () => ({
schemaVersion: 1,
operation: 'output',
requestId: 'request-1',
result: {},
}),
}),
t,
);
const driftedResponse = await drifted.request('tools/call', {
name: 'qinglong.cluster.copilot.failure_diagnosis.get',
arguments: argumentsValue,
});
assert.deepEqual(JSON.parse(driftedResponse.result.content[0].text), {
code: 'copilot_request_failed',
});
const unbounded = await client(
createQingLongClusterCopilotMcpServer({
config: config(),
execute: async () => {
throw new ClusterCopilotClientRemoteError(
999,
'x'.repeat(1_000),
'request-1',
9_999,
);
},
}),
t,
);
const unboundedResponse = await unbounded.request('tools/call', {
name: 'qinglong.cluster.copilot.failure_diagnosis.get',
arguments: argumentsValue,
});
assert.deepEqual(JSON.parse(unboundedResponse.result.content[0].text), {
code: 'copilot_request_failed',
});
});
test('rejects concurrent work immediately without a hidden queue', async (t) => {
let release;
const held = new Promise((resolve) => {
release = resolve;
});
let calls = 0;
const server = createQingLongClusterCopilotMcpServer({
config: config(1),
execute: async (execution) => {
calls += 1;
await held;
return {
schemaVersion: 1,
operation: execution.command.operation,
requestId: execution.command.requestId,
result: {},
};
},
});
const connected = await client(server, t);
const first = connected.request('tools/call', {
name: 'qinglong.cluster.copilot.failure_diagnosis.get',
arguments: {
projectId: 'project-1',
sourceRunId: 'source-run-1',
requestId: 'request-1',
},
});
await new Promise((resolve) => setImmediate(resolve));
const second = await connected.request('tools/call', {
name: 'qinglong.cluster.copilot.failure_diagnosis.get',
arguments: {
projectId: 'project-1',
sourceRunId: 'source-run-2',
requestId: 'request-2',
},
});
assert.equal(calls, 1);
assert.equal(second.result.isError, true);
assert.deepEqual(JSON.parse(second.result.content[0].text), {
code: 'copilot_mcp_busy',
});
release();
await first;
});
test('requires exact private startup configuration and validates client authority', (t) => {
assert.throws(
() =>
normalizeClusterCopilotMcpServerConfig({
...config(),
maxConcurrentRequests: 17,
}),
{ code: 'QL3_CLUSTER_COPILOT_MCP_CONFIG_INVALID' },
);
const directory = temporaryDirectory(t);
const caFile = privateFile(directory, 'ca.pem', fs.readFileSync(caFixture));
const clientConfigFile = privateFile(
directory,
'client.json',
JSON.stringify({
schema: CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
endpoint: 'https://localhost:9443/',
servername: 'localhost',
caFile,
requestTimeoutMs: 2_000,
}),
);
const credentialFile = privateFile(directory, 'credential', credential);
const serverConfigFile = privateFile(
directory,
'mcp.json',
JSON.stringify({
schema: CLUSTER_COPILOT_MCP_SERVER_CONFIG_SCHEMA,
clientConfigFile,
credentialFile,
maxConcurrentRequests: 2,
}),
);
assert.deepEqual(readClusterCopilotMcpServerConfig(serverConfigFile), {
schema: CLUSTER_COPILOT_MCP_SERVER_CONFIG_SCHEMA,
clientConfigFile,
credentialFile,
maxConcurrentRequests: 2,
});
fs.chmodSync(serverConfigFile, 0o644);
assert.throws(() => readClusterCopilotMcpServerConfig(serverConfigFile), {
code: 'QL3_CLUSTER_COPILOT_MCP_CONFIG_INVALID',
});
});