mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
395 lines
12 KiB
JavaScript
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',
|
|
});
|
|
});
|