mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add bounded copilot product client
This commit is contained in:
@@ -0,0 +1,639 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = 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 { createServer } = require('node:https');
|
||||
|
||||
const packageRoot = path.resolve(__dirname, '..');
|
||||
const cliPath = path.join(packageRoot, 'dist', 'copilot-client', 'cli.js');
|
||||
const fixtureRoot = path.resolve(
|
||||
packageRoot,
|
||||
'../ql3-cluster-control/test/fixtures/mtls',
|
||||
);
|
||||
const caFixture = path.join(fixtureRoot, 'ca-cert.pem');
|
||||
const certificateFixture = path.join(fixtureRoot, 'server-cert.pem');
|
||||
const privateKeyFixture = path.join(fixtureRoot, 'server-key.pem');
|
||||
|
||||
const {
|
||||
CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
|
||||
ClusterCopilotClientRemoteError,
|
||||
executeClusterCopilotClient,
|
||||
probeClusterCopilotClientReadiness,
|
||||
validateClusterCopilotClientConfiguration,
|
||||
} = require('../dist/copilot-client/client.js');
|
||||
const {
|
||||
CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_CANCELLATION_RESPONSE_SCHEMA,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_INSPECTION_RESPONSE_SCHEMA,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_READ_RESPONSE_SCHEMA,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_REQUEST_SCHEMA,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_RESPONSE_SCHEMA,
|
||||
normalizeClusterCopilotClientCommand,
|
||||
prepareClusterCopilotClientRequest,
|
||||
validateClusterCopilotClientResponse,
|
||||
} = require('../dist/copilot-client/contracts.js');
|
||||
|
||||
const credential = `ql3c_credential-1_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
const baseCommand = {
|
||||
schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
|
||||
projectId: 'project-1',
|
||||
sourceRunId: 'source-run-1',
|
||||
requestId: 'diagnosis-request-1',
|
||||
};
|
||||
|
||||
function privateFile(directory, name, contents) {
|
||||
const filePath = path.join(directory, name);
|
||||
fs.writeFileSync(filePath, contents, { mode: 0o600 });
|
||||
return fs.realpathSync(filePath);
|
||||
}
|
||||
|
||||
function temporaryDirectory(t) {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-copilot-client-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return directory;
|
||||
}
|
||||
|
||||
function configuration(directory, port) {
|
||||
const caFile = privateFile(
|
||||
directory,
|
||||
'ca.pem',
|
||||
fs.readFileSync(caFixture),
|
||||
);
|
||||
return privateFile(
|
||||
directory,
|
||||
'client.json',
|
||||
JSON.stringify({
|
||||
schema: CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
|
||||
endpoint: `https://localhost:${port}/`,
|
||||
servername: 'localhost',
|
||||
caFile,
|
||||
requestTimeoutMs: 2_000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function commandFile(directory, name, operation, extra = {}) {
|
||||
return privateFile(
|
||||
directory,
|
||||
`${name}.json`,
|
||||
JSON.stringify({ ...baseCommand, operation, ...extra }),
|
||||
);
|
||||
}
|
||||
|
||||
function jsonResponse(response, statusCode, requestId, body, headers = {}) {
|
||||
const bytes = Buffer.from(JSON.stringify(body));
|
||||
response.writeHead(statusCode, {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'content-length': String(bytes.length),
|
||||
...(requestId === null ? {} : { 'x-request-id': requestId }),
|
||||
...headers,
|
||||
});
|
||||
response.end(bytes);
|
||||
}
|
||||
|
||||
async function startServer(handler) {
|
||||
const server = createServer(
|
||||
{
|
||||
key: fs.readFileSync(privateKeyFixture),
|
||||
cert: fs.readFileSync(certificateFixture),
|
||||
minVersion: 'TLSv1.3',
|
||||
maxVersion: 'TLSv1.3',
|
||||
},
|
||||
handler,
|
||||
);
|
||||
await new Promise((resolvePromise, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolvePromise);
|
||||
});
|
||||
return {
|
||||
port: server.address().port,
|
||||
close: () =>
|
||||
new Promise((resolvePromise, reject) => {
|
||||
server.close((error) =>
|
||||
error ? reject(error) : resolvePromise(),
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function runCli(args) {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const child = spawn(process.execPath, [cliPath, ...args], {
|
||||
cwd: packageRoot,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const stdout = [];
|
||||
const stderr = [];
|
||||
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
||||
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
||||
child.once('error', reject);
|
||||
child.once('close', (status, signal) => {
|
||||
resolvePromise({
|
||||
status,
|
||||
signal,
|
||||
stdout: Buffer.concat(stdout).toString('utf8'),
|
||||
stderr: Buffer.concat(stderr).toString('utf8'),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function diagnoseResponse(overrides = {}) {
|
||||
return {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_RESPONSE_SCHEMA,
|
||||
requestId: baseCommand.requestId,
|
||||
status: 'created',
|
||||
replayed: false,
|
||||
sourceRunId: baseCommand.sourceRunId,
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
outcome: 'succeeded',
|
||||
stage: 'model',
|
||||
reason: null,
|
||||
outputArtifact: {
|
||||
artifactId: 'cdo:artifact-1',
|
||||
artifactDigest: 'a'.repeat(64),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function inspectionResponse(overrides = {}) {
|
||||
return {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_INSPECTION_RESPONSE_SCHEMA,
|
||||
status: 'running',
|
||||
projectId: baseCommand.projectId,
|
||||
sourceRunId: baseCommand.sourceRunId,
|
||||
requestId: baseCommand.requestId,
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
outcome: null,
|
||||
stage: null,
|
||||
reason: null,
|
||||
outputAvailable: false,
|
||||
admittedAtMs: 100,
|
||||
finalizedAtMs: null,
|
||||
usage: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function outputResponse() {
|
||||
return {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_READ_RESPONSE_SCHEMA,
|
||||
status: 'available',
|
||||
projectId: baseCommand.projectId,
|
||||
sourceRunId: baseCommand.sourceRunId,
|
||||
requestId: baseCommand.requestId,
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
reference: {
|
||||
artifactId: 'cdo:artifact-1',
|
||||
artifactDigest: 'a'.repeat(64),
|
||||
contentDigest: 'b'.repeat(64),
|
||||
outputBytes: Buffer.byteLength('diagnosis'),
|
||||
sealedAtMs: 200,
|
||||
},
|
||||
result: {
|
||||
text: 'diagnosis',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cancellationResponse() {
|
||||
return {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_CANCELLATION_RESPONSE_SCHEMA,
|
||||
status: 'accepted',
|
||||
convergence: 'terminal',
|
||||
projectId: baseCommand.projectId,
|
||||
sourceRunId: baseCommand.sourceRunId,
|
||||
requestId: baseCommand.requestId,
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
runStatus: 'cancelled',
|
||||
outcome: 'cancelled',
|
||||
runVersion: 7,
|
||||
eventSequence: 7,
|
||||
cancelRequestedAtMs: 500,
|
||||
cancelReason: 'user',
|
||||
};
|
||||
}
|
||||
|
||||
test('normalizes only the four bounded commands and derives exact requests', () => {
|
||||
const commands = [
|
||||
{ ...baseCommand, operation: 'diagnose', traceId: 'trace-1' },
|
||||
{ ...baseCommand, operation: 'inspect' },
|
||||
{ ...baseCommand, operation: 'output' },
|
||||
{
|
||||
...baseCommand,
|
||||
operation: 'cancel',
|
||||
mutationId: '11111111-1111-4111-8111-111111111111',
|
||||
},
|
||||
];
|
||||
for (const command of commands) {
|
||||
const normalized = normalizeClusterCopilotClientCommand(command);
|
||||
assert.deepEqual(normalized, command);
|
||||
assert.equal(Object.isFrozen(normalized), true);
|
||||
}
|
||||
assert.deepEqual(prepareClusterCopilotClientRequest(commands[0]), {
|
||||
method: 'POST',
|
||||
path: '/api/v3/projects/project-1/runs/source-run-1/copilot/failure-diagnoses',
|
||||
requestId: baseCommand.requestId,
|
||||
body: {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_REQUEST_SCHEMA,
|
||||
traceId: 'trace-1',
|
||||
},
|
||||
acceptedStatusCodes: [200, 201],
|
||||
});
|
||||
assert.deepEqual(
|
||||
prepareClusterCopilotClientRequest(commands[2], 'transport-read-1'),
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/v3/projects/project-1/runs/source-run-1/copilot/failure-diagnoses/diagnosis-request-1/output',
|
||||
requestId: 'transport-read-1',
|
||||
body: null,
|
||||
acceptedStatusCodes: [200],
|
||||
},
|
||||
);
|
||||
assert.throws(() =>
|
||||
normalizeClusterCopilotClientCommand({ ...commands[0], model: 'private' }),
|
||||
);
|
||||
assert.throws(() =>
|
||||
prepareClusterCopilotClientRequest(commands[1], '../invalid'),
|
||||
);
|
||||
});
|
||||
|
||||
test('validates exact target-bound response state for every operation', () => {
|
||||
const diagnose = { ...baseCommand, operation: 'diagnose', traceId: 'trace-1' };
|
||||
const inspect = { ...baseCommand, operation: 'inspect' };
|
||||
const output = { ...baseCommand, operation: 'output' };
|
||||
const cancel = {
|
||||
...baseCommand,
|
||||
operation: 'cancel',
|
||||
mutationId: '11111111-1111-4111-8111-111111111111',
|
||||
};
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(diagnoseResponse(), diagnose).status,
|
||||
'created',
|
||||
);
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(inspectionResponse(), inspect).status,
|
||||
'running',
|
||||
);
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(
|
||||
inspectionResponse({
|
||||
status: 'terminal',
|
||||
outcome: 'succeeded',
|
||||
stage: 'model',
|
||||
outputAvailable: true,
|
||||
finalizedAtMs: 200,
|
||||
usage: {
|
||||
inputTokens: 3,
|
||||
outputTokens: 2,
|
||||
totalTokens: 5,
|
||||
currency: 'USD',
|
||||
costMicros: 7,
|
||||
},
|
||||
}),
|
||||
inspect,
|
||||
).status,
|
||||
'terminal',
|
||||
);
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(outputResponse(), output).result.text,
|
||||
'diagnosis',
|
||||
);
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(cancellationResponse(), cancel)
|
||||
.status,
|
||||
'accepted',
|
||||
);
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(
|
||||
diagnoseResponse({ status: 'existing', replayed: true }),
|
||||
diagnose,
|
||||
).replayed,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
validateClusterCopilotClientResponse(
|
||||
{
|
||||
...cancellationResponse(),
|
||||
status: 'already_requested',
|
||||
convergence: 'model_in_flight',
|
||||
runStatus: 'running',
|
||||
outcome: null,
|
||||
runVersion: 6,
|
||||
eventSequence: 6,
|
||||
},
|
||||
cancel,
|
||||
).convergence,
|
||||
'model_in_flight',
|
||||
);
|
||||
assert.throws(() =>
|
||||
validateClusterCopilotClientResponse(
|
||||
diagnoseResponse({ schema: 'qinglong/drift@v1' }),
|
||||
diagnose,
|
||||
),
|
||||
);
|
||||
assert.throws(() =>
|
||||
validateClusterCopilotClientResponse(
|
||||
diagnoseResponse({ projectId: 'widened' }),
|
||||
diagnose,
|
||||
),
|
||||
);
|
||||
assert.throws(() =>
|
||||
validateClusterCopilotClientResponse(
|
||||
inspectionResponse({ projectId: 'other-project' }),
|
||||
inspect,
|
||||
),
|
||||
);
|
||||
const invalidOutput = outputResponse();
|
||||
invalidOutput.reference.outputBytes += 1;
|
||||
assert.throws(() =>
|
||||
validateClusterCopilotClientResponse(invalidOutput, output),
|
||||
);
|
||||
assert.throws(() =>
|
||||
validateClusterCopilotClientResponse(
|
||||
cancellationResponse(),
|
||||
{ ...cancel, requestId: 'other-request' },
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('uses TLS 1.3, Bearer credential and exact request identities end to end', async (t) => {
|
||||
const seen = [];
|
||||
const server = await startServer((request, response) => {
|
||||
const chunks = [];
|
||||
request.on('data', (chunk) => chunks.push(chunk));
|
||||
request.on('end', () => {
|
||||
const body = Buffer.concat(chunks).toString('utf8');
|
||||
seen.push({
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
authorization: request.headers.authorization,
|
||||
requestId: request.headers['x-request-id'],
|
||||
body: body === '' ? null : JSON.parse(body),
|
||||
peerCertificate: request.socket.getPeerCertificate(),
|
||||
});
|
||||
const requestId = request.headers['x-request-id'];
|
||||
if (request.url === '/readyz') {
|
||||
assert.equal(request.headers.authorization, undefined);
|
||||
jsonResponse(response, 200, null, { status: 'ready' });
|
||||
} else if (request.method === 'POST' && request.url.endsWith('/cancellation')) {
|
||||
jsonResponse(response, 202, requestId, cancellationResponse());
|
||||
} else if (request.method === 'POST') {
|
||||
jsonResponse(response, 201, requestId, diagnoseResponse());
|
||||
} else if (request.url.endsWith('/output')) {
|
||||
jsonResponse(response, 200, requestId, outputResponse());
|
||||
} else {
|
||||
jsonResponse(response, 200, requestId, inspectionResponse());
|
||||
}
|
||||
});
|
||||
});
|
||||
t.after(() => server.close());
|
||||
const directory = temporaryDirectory(t);
|
||||
const configFile = configuration(directory, server.port);
|
||||
const credentialFile = privateFile(directory, 'credential', credential);
|
||||
assert.deepEqual(validateClusterCopilotClientConfiguration(configFile), {
|
||||
schemaVersion: 1,
|
||||
transport: 'https',
|
||||
clientCertificate: 'forbidden',
|
||||
});
|
||||
assert.deepEqual(await probeClusterCopilotClientReadiness(configFile), {
|
||||
schemaVersion: 1,
|
||||
transport: 'https',
|
||||
ready: true,
|
||||
});
|
||||
|
||||
const operations = [
|
||||
['diagnose', { traceId: 'trace-1' }, baseCommand.requestId],
|
||||
['inspect', {}, 'transport-read-1'],
|
||||
['output', {}, 'transport-read-2'],
|
||||
[
|
||||
'cancel',
|
||||
{ mutationId: '11111111-1111-4111-8111-111111111111' },
|
||||
'11111111-1111-4111-8111-111111111111',
|
||||
],
|
||||
];
|
||||
for (const [operation, extra, expectedRequestId] of operations) {
|
||||
const result = await executeClusterCopilotClient(
|
||||
{
|
||||
configFile,
|
||||
commandFile: commandFile(directory, operation, operation, extra),
|
||||
credentialFile,
|
||||
},
|
||||
{ createRequestId: () => expectedRequestId },
|
||||
);
|
||||
assert.equal(result.operation, operation);
|
||||
assert.equal(result.requestId, expectedRequestId);
|
||||
}
|
||||
assert.equal(seen.length, 5);
|
||||
for (const request of seen.slice(1)) {
|
||||
assert.equal(request.authorization, `Bearer ${credential}`);
|
||||
assert.equal(request.peerCertificate.subject, undefined);
|
||||
}
|
||||
assert.deepEqual(seen[1].body, {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_REQUEST_SCHEMA,
|
||||
traceId: 'trace-1',
|
||||
});
|
||||
assert.equal(seen[2].body, null);
|
||||
assert.equal(seen[3].body, null);
|
||||
assert.deepEqual(seen[4].body, {
|
||||
schema: 'qinglong/run-cancellation@v1',
|
||||
mutationId: '11111111-1111-4111-8111-111111111111',
|
||||
});
|
||||
});
|
||||
|
||||
test('fails closed on weak files, request-id drift and low-sensitive remote errors', async (t) => {
|
||||
let mode = 'readiness-drift';
|
||||
const server = await startServer((request, response) => {
|
||||
request.resume();
|
||||
request.on('end', () => {
|
||||
if (mode === 'readiness-drift') {
|
||||
jsonResponse(response, 200, null, { status: 'ready', widened: true });
|
||||
return;
|
||||
}
|
||||
if (mode === 'drift') {
|
||||
jsonResponse(response, 201, 'wrong-request', diagnoseResponse());
|
||||
return;
|
||||
}
|
||||
jsonResponse(
|
||||
response,
|
||||
429,
|
||||
request.headers['x-request-id'],
|
||||
{ code: 'copilot_rate_limited', reason: 'private detail' },
|
||||
{ 'retry-after': '30' },
|
||||
);
|
||||
});
|
||||
});
|
||||
t.after(() => server.close());
|
||||
const directory = temporaryDirectory(t);
|
||||
const configFile = configuration(directory, server.port);
|
||||
const command = commandFile(directory, 'diagnose', 'diagnose', {
|
||||
traceId: 'trace-1',
|
||||
});
|
||||
const credentialFile = privateFile(directory, 'credential', credential);
|
||||
const paths = { configFile, commandFile: command, credentialFile };
|
||||
await assert.rejects(probeClusterCopilotClientReadiness(configFile), {
|
||||
code: 'QL3_CLUSTER_COPILOT_CLIENT_REQUEST_FAILED',
|
||||
});
|
||||
|
||||
mode = 'drift';
|
||||
await assert.rejects(executeClusterCopilotClient(paths), {
|
||||
code: 'QL3_CLUSTER_COPILOT_CLIENT_REQUEST_FAILED',
|
||||
});
|
||||
|
||||
mode = 'remote';
|
||||
await assert.rejects(
|
||||
executeClusterCopilotClient(paths),
|
||||
(error) => {
|
||||
assert.equal(error instanceof ClusterCopilotClientRemoteError, true);
|
||||
assert.equal(error.statusCode, 429);
|
||||
assert.equal(error.responseCode, 'copilot_rate_limited');
|
||||
assert.equal(error.requestId, baseCommand.requestId);
|
||||
assert.equal(error.retryAfterSeconds, 30);
|
||||
assert.equal(JSON.stringify(error).includes('private detail'), false);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
const cli = await runCli([
|
||||
`--config=${configFile}`,
|
||||
`--command=${command}`,
|
||||
`--credential=${credentialFile}`,
|
||||
]);
|
||||
assert.equal(cli.status, 1);
|
||||
assert.equal(cli.stdout, '');
|
||||
assert.deepEqual(JSON.parse(cli.stderr), {
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-cluster-copilot-client',
|
||||
event: 'command_failed',
|
||||
code: 'QL3_CLUSTER_COPILOT_CLIENT_REMOTE_REJECTED',
|
||||
statusCode: 429,
|
||||
responseCode: 'copilot_rate_limited',
|
||||
requestId: baseCommand.requestId,
|
||||
retryAfterSeconds: 30,
|
||||
});
|
||||
assert.equal(cli.stderr.includes(credential), false);
|
||||
assert.equal(cli.stderr.includes('private detail'), false);
|
||||
assert.equal(cli.stderr.includes(directory), false);
|
||||
|
||||
fs.chmodSync(credentialFile, 0o644);
|
||||
await assert.rejects(executeClusterCopilotClient(paths), {
|
||||
code: 'QL3_CLUSTER_COPILOT_CLIENT_CONFIG_INVALID',
|
||||
});
|
||||
});
|
||||
|
||||
test('CLI help, usage and explicit output success remain deterministic', async (t) => {
|
||||
const help = await runCli(['--help']);
|
||||
assert.equal(help.status, 0);
|
||||
assert.match(help.stdout, /^Usage: ql3-copilot-client /);
|
||||
assert.equal(help.stderr, '');
|
||||
const usage = await runCli([]);
|
||||
assert.equal(usage.status, 64);
|
||||
assert.equal(usage.stdout, '');
|
||||
assert.equal(
|
||||
JSON.parse(usage.stderr).code,
|
||||
'QL3_CLUSTER_COPILOT_CLIENT_USAGE_INVALID',
|
||||
);
|
||||
|
||||
const server = await startServer((request, response) => {
|
||||
request.resume();
|
||||
request.on('end', () => {
|
||||
jsonResponse(
|
||||
response,
|
||||
200,
|
||||
request.headers['x-request-id'],
|
||||
outputResponse(),
|
||||
);
|
||||
});
|
||||
});
|
||||
t.after(() => server.close());
|
||||
const directory = temporaryDirectory(t);
|
||||
const result = await runCli([
|
||||
`--config=${configuration(directory, server.port)}`,
|
||||
`--command=${commandFile(directory, 'output', 'output')}`,
|
||||
`--credential=${privateFile(directory, 'credential', credential)}`,
|
||||
]);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stderr, '');
|
||||
const fact = JSON.parse(result.stdout);
|
||||
assert.equal(fact.operation, 'output');
|
||||
assert.equal(fact.result.result.text, 'diagnosis');
|
||||
assert.equal(fact.component, 'qinglong3-cluster-copilot-client');
|
||||
});
|
||||
|
||||
test('rejects response framing drift, oversized bodies, aborts and timeouts', async (t) => {
|
||||
let mode = 'content-type';
|
||||
const server = await startServer((request, response) => {
|
||||
response.on('error', () => {});
|
||||
request.resume();
|
||||
request.on('end', () => {
|
||||
if (mode === 'timeout') return;
|
||||
if (mode === 'abort') {
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'x-request-id': request.headers['x-request-id'],
|
||||
});
|
||||
response.write('{"schema":');
|
||||
response.destroy();
|
||||
return;
|
||||
}
|
||||
if (mode === 'oversized') {
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'x-request-id': request.headers['x-request-id'],
|
||||
});
|
||||
response.end(Buffer.alloc(2 * 1024 * 1024 + 1, 0x20));
|
||||
return;
|
||||
}
|
||||
const body = Buffer.from(JSON.stringify(diagnoseResponse()));
|
||||
response.writeHead(201, {
|
||||
'content-type': 'text/plain',
|
||||
'content-length': String(body.length),
|
||||
'x-request-id': request.headers['x-request-id'],
|
||||
});
|
||||
response.end(body);
|
||||
});
|
||||
});
|
||||
t.after(() => server.close());
|
||||
const directory = temporaryDirectory(t);
|
||||
const caFile = privateFile(
|
||||
directory,
|
||||
'ca.pem',
|
||||
fs.readFileSync(caFixture),
|
||||
);
|
||||
const configFile = privateFile(
|
||||
directory,
|
||||
'client.json',
|
||||
JSON.stringify({
|
||||
schema: CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
|
||||
endpoint: `https://localhost:${server.port}/`,
|
||||
servername: 'localhost',
|
||||
caFile,
|
||||
requestTimeoutMs: 1_000,
|
||||
}),
|
||||
);
|
||||
const paths = {
|
||||
configFile,
|
||||
commandFile: commandFile(directory, 'diagnose', 'diagnose', {
|
||||
traceId: 'trace-1',
|
||||
}),
|
||||
credentialFile: privateFile(directory, 'credential', credential),
|
||||
};
|
||||
for (const failureMode of [
|
||||
'content-type',
|
||||
'oversized',
|
||||
'abort',
|
||||
'timeout',
|
||||
]) {
|
||||
mode = failureMode;
|
||||
await assert.rejects(executeClusterCopilotClient(paths), {
|
||||
code: 'QL3_CLUSTER_COPILOT_CLIENT_REQUEST_FAILED',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -54,6 +54,9 @@ const {
|
||||
const {
|
||||
validateClusterAuthenticatedManagementClientConfiguration,
|
||||
} = require('../dist/management-support/pluginPackageManagementClient.js');
|
||||
const {
|
||||
CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
|
||||
} = require('../dist/copilot-client/client.js');
|
||||
|
||||
function runCli(args) {
|
||||
return spawnSync(process.execPath, [cliPath, ...args], {
|
||||
@@ -121,6 +124,41 @@ async function startReadinessServer(status) {
|
||||
};
|
||||
}
|
||||
|
||||
async function startCopilotReadinessServer(status) {
|
||||
const server = createServer(
|
||||
{
|
||||
key: fs.readFileSync(localhostPrivateKeyFixture),
|
||||
cert: fs.readFileSync(localhostCertificateFixture),
|
||||
minVersion: 'TLSv1.3',
|
||||
maxVersion: 'TLSv1.3',
|
||||
},
|
||||
(request, response) => {
|
||||
assert.equal(request.method, 'GET');
|
||||
assert.equal(request.url, '/readyz');
|
||||
assert.equal(request.headers.authorization, undefined);
|
||||
const body = Buffer.from(JSON.stringify({ status: status.value }));
|
||||
response.writeHead(status.value === 'ready' ? 200 : 503, {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'content-length': String(body.length),
|
||||
});
|
||||
response.end(body);
|
||||
},
|
||||
);
|
||||
await new Promise((resolvePromise, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolvePromise);
|
||||
});
|
||||
return {
|
||||
port: server.address().port,
|
||||
close: () =>
|
||||
new Promise((resolvePromise, reject) => {
|
||||
server.close((error) =>
|
||||
error ? reject(error) : resolvePromise(),
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function privateFile(directory, name, contents) {
|
||||
const filePath = path.join(directory, name);
|
||||
fs.writeFileSync(filePath, contents, { mode: 0o600 });
|
||||
@@ -193,6 +231,17 @@ function validContextFixture(t) {
|
||||
'/api/v3/plugin-packages/management',
|
||||
'forbidden',
|
||||
);
|
||||
const copilotConfig = privateFile(
|
||||
directory,
|
||||
'copilot-client.json',
|
||||
JSON.stringify({
|
||||
schema: CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
|
||||
endpoint: 'https://copilot.example.test:8443/',
|
||||
servername: 'copilot.example.test',
|
||||
caFile,
|
||||
requestTimeoutMs: 1_000,
|
||||
}),
|
||||
);
|
||||
const kubeconfigFile = privateFile(
|
||||
directory,
|
||||
'kubeconfig.json',
|
||||
@@ -236,6 +285,7 @@ function validContextFixture(t) {
|
||||
}),
|
||||
);
|
||||
const commands = {
|
||||
copilot: { configFile: copilotConfig },
|
||||
package: { configFile: packageConfig },
|
||||
'package-kubernetes': { configFile: packageConfig, kubernetesFile },
|
||||
'worker-credential': {
|
||||
@@ -283,7 +333,7 @@ function validContextFixture(t) {
|
||||
|
||||
test('catalog exposes only reviewed remote clients from the same package', () => {
|
||||
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
|
||||
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 7);
|
||||
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 8);
|
||||
assert.equal(
|
||||
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
|
||||
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
|
||||
@@ -324,6 +374,7 @@ test('help and version are bounded installation-derived product facts', () => {
|
||||
const help = qingLong3ClusterProductHelp();
|
||||
assert.match(help, /^Usage: ql3-cluster-admin <command> \[arguments\]/);
|
||||
assert.match(help, /\n run\s+retry or stop Runs/);
|
||||
assert.match(help, /\n copilot\s+diagnose, inspect, read or cancel Runs/);
|
||||
assert.match(help, /Server, migration, recovery, executor and key-custody/);
|
||||
assert.equal(help.includes('plugin-package-manage'), false);
|
||||
assert.equal(
|
||||
@@ -558,8 +609,9 @@ test('validates the complete operator context offline without operational author
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-cluster-product-cli',
|
||||
event: 'context_valid',
|
||||
commandCount: 7,
|
||||
commandCount: 8,
|
||||
commands: [
|
||||
{ name: 'copilot', transport: 'https', clientCertificate: 'forbidden' },
|
||||
{ name: 'package', transport: 'https', clientCertificate: 'forbidden' },
|
||||
{
|
||||
name: 'package-kubernetes',
|
||||
@@ -671,6 +723,68 @@ test('probes a context with fixed read-only readiness semantics and exit status'
|
||||
assert.equal(fact.commands[0].status, 'not_ready');
|
||||
});
|
||||
|
||||
test('probes Copilot context through its separate unauthenticated readiness contract', async (t) => {
|
||||
const status = { value: 'ready' };
|
||||
const server = await startCopilotReadinessServer(status);
|
||||
t.after(() => server.close());
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-copilot-probe-context-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const caFile = privateFile(
|
||||
directory,
|
||||
'ca.pem',
|
||||
fs.readFileSync(localhostCaFixture),
|
||||
);
|
||||
const configFile = privateFile(
|
||||
directory,
|
||||
'copilot.json',
|
||||
JSON.stringify({
|
||||
schema: CLUSTER_COPILOT_CLIENT_CONFIG_SCHEMA,
|
||||
endpoint: `https://localhost:${server.port}/`,
|
||||
servername: 'localhost',
|
||||
caFile,
|
||||
requestTimeoutMs: 1_000,
|
||||
}),
|
||||
);
|
||||
const contextFile = privateFile(
|
||||
directory,
|
||||
'operator-context.json',
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
commands: { copilot: { configFile } },
|
||||
}),
|
||||
);
|
||||
|
||||
const ready = await runCliAsync([
|
||||
'context',
|
||||
'probe',
|
||||
`--context=${contextFile}`,
|
||||
]);
|
||||
assert.equal(ready.status, 0, ready.stderr);
|
||||
assert.equal(ready.stderr, '');
|
||||
assert.deepEqual(JSON.parse(ready.stdout), {
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-cluster-product-cli',
|
||||
event: 'context_probed',
|
||||
commandCount: 1,
|
||||
commands: [{ name: 'copilot', transport: 'https', status: 'ready' }],
|
||||
allReady: true,
|
||||
requestMethod: 'GET',
|
||||
requestPath: '/readyz',
|
||||
mutation: false,
|
||||
});
|
||||
|
||||
status.value = 'not_ready';
|
||||
const notReady = await runCliAsync([
|
||||
'context',
|
||||
'probe',
|
||||
`--context=${contextFile}`,
|
||||
]);
|
||||
assert.equal(notReady.status, 69);
|
||||
assert.equal(JSON.parse(notReady.stdout).allReady, false);
|
||||
});
|
||||
|
||||
test('context validation fails closed for invalid client configuration and syntax', (t) => {
|
||||
const fixture = validContextFixture(t);
|
||||
fs.writeFileSync(fixture.commands.run.configFile, '{}', { mode: 0o600 });
|
||||
|
||||
Reference in New Issue
Block a user