Files
LWLT-AIBOT/control-plane/test/diagnostics.test.ts
inman 81a0cdac8e Revert "merge: integrate extension auto-update"
This reverts commit 322475860a, reversing
changes made to f52d9d7413.
2026-09-03 16:45:14 +08:00

181 lines
6.9 KiB
TypeScript

import assert from 'node:assert/strict';
import { Writable } from 'node:stream';
import test from 'node:test';
import { loadConfig } from '../src/config.js';
import {
diagnosticDurationMs,
diagnosticError,
diagnosticRequestPath,
normalizeRequestId
} from '../src/diagnostics.js';
import { buildServer, createControlPlaneLogger } from '../src/server.js';
import { TaskService, type TaskEvent } from '../src/task-service.js';
function testConfig(overrides: NodeJS.ProcessEnv = {}) {
return loadConfig({
NODE_ENV: 'test',
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 41).toString('base64'),
DATABASE_URL: 'postgresql://invalid:invalid@127.0.0.1:1/invalid',
...overrides
});
}
function logCollector(): { destination: Writable; records: Array<Record<string, unknown>> } {
const chunks: string[] = [];
const records: Array<Record<string, unknown>> = [];
const destination = new Writable({
write(chunk, _encoding, callback) {
chunks.push(String(chunk));
const lines = chunks.join('').split('\n');
chunks.length = 0;
const remainder = lines.pop() || '';
if (remainder) chunks.push(remainder);
for (const line of lines) {
if (line.trim()) records.push(JSON.parse(line) as Record<string, unknown>);
}
callback();
}
});
return { destination, records };
}
test('diagnostic errors preserve code and stack location without leaking the error message', () => {
const secret = 'customer-secret-value';
const error = new Error(`download https://user:password@example.test/list.xlsx?token=${secret}`) as Error & {
code: string;
errno: number;
syscall: string;
};
error.code = 'ECONNRESET';
error.errno = -54;
error.syscall = 'read';
const metadata = diagnosticError(error, 'download_failed');
const serialized = JSON.stringify(metadata);
assert.equal(metadata.error_code, 'ECONNRESET');
assert.equal(metadata.error_name, 'Error');
assert.match(String(metadata.error_fingerprint), /^[a-f0-9]{24}$/u);
assert.equal(metadata.error_errno, -54);
assert.equal(metadata.error_syscall, 'read');
assert.doesNotMatch(serialized, /customer-secret-value|password|example\.test|list\.xlsx/u);
});
test('diagnostic request identifiers and paths are stable and query-safe', () => {
assert.equal(normalizeRequestId('request:wechat:12345678'), 'request:wechat:12345678');
const generated = normalizeRequestId('token=must-not-be-used');
assert.match(generated, /^[a-f0-9-]{36}$/u);
assert.doesNotMatch(generated, /token/u);
assert.equal(diagnosticRequestPath('/api/tasks/TASK-1?token=secret#fragment'), '/api/tasks/TASK-1');
const startedAt = process.hrtime.bigint() - 2_000_000n;
assert.ok(diagnosticDurationMs(startedAt) >= 1);
});
test('control-plane logger redacts credentials and includes deployment identity', async () => {
const { destination, records } = logCollector();
const logger = createControlPlaneLogger(testConfig({ DEPLOYMENT_REVISION: 'commit-2360506' }), destination);
logger.info({
diagnostic_event: 'test.redaction',
password: 'root-secret',
nested: { token: 'nested-secret' },
safe_value: 'visible'
}, 'diagnostic test');
await new Promise((resolve) => setImmediate(resolve));
assert.equal(records.length, 1);
assert.equal(records[0].service, 'ltjt-control-plane');
assert.equal(records[0].deployment_revision, 'commit-2360506');
assert.equal(records[0].password, '[REDACTED]');
assert.deepEqual(records[0].nested, { token: '[REDACTED]' });
assert.equal(records[0].safe_value, 'visible');
});
test('HTTP diagnostics reuse one request ID and never log query values', async () => {
const { destination, records } = logCollector();
const { app } = await buildServer({
config: testConfig(),
startParserLoop: false,
loggerDestination: destination,
parser: {
async parse() {
return { blockers: ['test parser'] };
},
async checkConnection() {
return { ok: false, configured: false };
}
}
});
const response = await app.inject({
method: 'GET',
url: '/health/live?token=query-secret',
headers: { 'x-request-id': 'request:test:12345678' }
});
assert.equal(response.statusCode, 200);
assert.equal(response.headers['x-request-id'], 'request:test:12345678');
await app.close();
await new Promise((resolve) => setImmediate(resolve));
const started = records.find((record) => record.diagnostic_event === 'http.request.started');
const completed = records.find((record) => record.diagnostic_event === 'http.request.completed');
assert.equal(started?.request_id, 'request:test:12345678');
assert.equal(completed?.request_id, 'request:test:12345678');
assert.equal(started?.path, '/health/live');
assert.equal(completed?.path, '/health/live');
assert.equal(completed?.status_code, 200);
assert.doesNotMatch(JSON.stringify(records), /query-secret/u);
});
test('task state and audit diagnostics record keys but never business values', async () => {
const logs: Array<Record<string, unknown>> = [];
const service = new TaskService(
testConfig(),
{} as never,
{
info(metadata) { logs.push(metadata); },
warn(metadata) { logs.push(metadata); },
error(metadata) { logs.push(metadata); }
}
) as unknown as {
notify(event: TaskEvent): void;
audit(
client: { query: (...args: unknown[]) => Promise<{ rowCount: number }> },
context: { organizationId: string; userId: string; requestId: string },
eventType: string,
entityId: string,
metadata: Record<string, unknown>
): Promise<void>;
};
service.notify({
id: 8,
organization_id: 'org-secret',
task_id: 'TASK-DIAGNOSTIC-1',
status: 'awaiting_attachment',
stage: 'input',
message: 'customer-secret-message',
payload: { customer_name: 'customer-secret-value', row_count: 12 },
created_at: new Date().toISOString()
});
await service.audit(
{ async query() { return { rowCount: 1 }; } },
{ organizationId: 'org-secret', userId: 'user-secret', requestId: 'request:audit:12345678' },
'task.passenger_roster_attachment_rejected',
'TASK-DIAGNOSTIC-1',
{ customer_name: 'customer-secret-value', error_code: 'roster_file_invalid' }
);
assert.ok(logs.some((log) => log.diagnostic_event === 'task.state.emitted'
&& Array.isArray(log.payload_keys)
&& (log.payload_keys as string[]).includes('customer_name')));
assert.ok(logs.some((log) => log.diagnostic_event === 'audit.event.staged'
&& log.request_id === 'request:audit:12345678'));
assert.doesNotMatch(JSON.stringify(logs), /customer-secret-value|customer-secret-message|org-secret|user-secret/u);
});
test('production configuration rejects raw AgentBus payload logging', () => {
assert.throws(
() => testConfig({ NODE_ENV: 'production', AGENTBUS_LOG_PAYLOADS: 'true' }),
/AGENTBUS_LOG_PAYLOADS must remain false in production/u
);
assert.throws(
() => testConfig({ LOG_LEVEL: 'verbose' }),
/Invalid enum value/u
);
});