判断修改

This commit is contained in:
andy committed 2026-09-14 12:47:15 +08:00
1 parent e06afc8c33
commit 4194a200c1
65 files changed
+9617 -109

No files matched your search

+185 -4
View File
@@ -1,7 +1,9 @@
import assert from 'node:assert/strict';
import { Writable } from 'node:stream';
import test from 'node:test';
import type { ActiveSession } from '../src/auth.js';
import { loadConfig } from '../src/config.js';
import { getPool } from '../src/db.js';
import {
diagnosticDurationMs,
diagnosticError,
@@ -9,7 +11,7 @@ import {
normalizeRequestId
} from '../src/diagnostics.js';
import { buildServer, createControlPlaneLogger } from '../src/server.js';
import { TaskService, type TaskEvent } from '../src/task-service.js';
import { TaskError, TaskService, type TaskEvent } from '../src/task-service.js';
function testConfig(overrides: NodeJS.ProcessEnv = {}) {
return loadConfig({
@@ -39,6 +41,18 @@ function logCollector(): { destination: Writable; records: Array<Record<string,
return { destination, records };
}
const heartbeatSession: ActiveSession = {
id: '33333333-3333-4333-8333-333333333333',
csrfTokenHash: Buffer.alloc(32),
user: {
id: '22222222-2222-4222-8222-222222222222',
organizationId: '11111111-1111-4111-8111-111111111111',
username: 'heartbeat-test',
role: 'user',
erpAccount: 'test-erp'
}
};
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 & {
@@ -102,9 +116,10 @@ test('HTTP diagnostics reuse one request ID and never log query values', async (
}
}
});
app.get('/api/diagnostic-test', async () => ({ ok: true }));
const response = await app.inject({
method: 'GET',
url: '/health/live?token=query-secret',
url: '/api/diagnostic-test?token=query-secret',
headers: { 'x-request-id': 'request:test:12345678' }
});
assert.equal(response.statusCode, 200);
@@ -116,12 +131,178 @@ test('HTTP diagnostics reuse one request ID and never log query values', async (
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(started?.path, '/api/diagnostic-test');
assert.equal(completed?.path, '/api/diagnostic-test');
assert.equal(completed?.status_code, 200);
assert.doesNotMatch(JSON.stringify(records), /query-secret/u);
});
test('successful heartbeats, health probes and workbench polling stay silent while their handlers still run', async (t) => {
const { destination, records } = logCollector();
const config = testConfig({ LOG_LEVEL: 'debug' });
const { app, auth, tasks } = await buildServer({
config,
startParserLoop: false,
loggerDestination: destination,
parser: { async parse() { return { blockers: ['test parser'] }; } }
});
t.after(() => app.close());
const query = t.mock.method(getPool(config), 'query', async () => ({ rows: [{ applied: true }] }));
t.mock.method(auth, 'getActiveSession', async () => heartbeatSession);
t.mock.method(auth, 'verifyCsrf', async () => true);
const listTasks = t.mock.method(tasks, 'listTasksPage', async () => ({
tasks: [], total: 0, offset: 0, limit: 200, has_more: false
}));
t.mock.method(tasks, 'parserConnectionEvidence', async () => ({
authenticated: null, source: null, lastSuccessAt: null, lastAuthFailureAt: null, lastAuthFailureCode: null
}));
const worker = { execution_ready: true, erp_account_matched: true, worker_connection_id: 'test-browser' };
const heartbeat = t.mock.method(tasks, 'heartbeat', async (..._args: Parameters<TaskService['heartbeat']>) => worker);
records.length = 0;
for (const method of ['GET', 'HEAD'] as const) {
for (const path of ['/health/live', '/health/ready', '/api/status', '/api/tasks']) {
const response = await app.inject({
method,
url: `${path}?token=probe-secret`,
headers: { 'x-request-id': 'request:probe:12345678' }
});
assert.equal(response.statusCode, 200);
assert.equal(response.headers['x-request-id'], 'request:probe:12345678');
if (method === 'GET') assert.equal(response.json().ok, true);
}
}
for (let index = 0; index < 3; index += 1) {
const response = await app.inject({
method: 'POST',
url: '/api/connections/heartbeat?token=probe-secret',
headers: { 'x-request-id': `request:heartbeat:${index}` },
payload: { connection_id: 'test-browser', extension_version: 'test-version' }
});
assert.equal(response.statusCode, 200);
assert.equal(response.headers['x-request-id'], `request:heartbeat:${index}`);
assert.deepEqual(response.json(), { ok: true, connected: true, ...worker });
}
assert.equal(query.mock.callCount(), 8);
assert.equal(listTasks.mock.callCount(), 2);
assert.equal(heartbeat.mock.callCount(), 3);
assert.equal(heartbeat.mock.calls[0].arguments[1], 'test-browser');
assert.equal(heartbeat.mock.calls[0].arguments[2], 'test-version');
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(records, []);
});
test('failed heartbeats and readiness probes retain severity, error codes and request correlation', async (t) => {
const { destination, records } = logCollector();
const config = testConfig();
const { app, auth, tasks } = await buildServer({
config,
startParserLoop: false,
loggerDestination: destination,
parser: { async parse() { return { blockers: ['test parser'] }; } }
});
t.after(() => app.close());
let session: ActiveSession | null = null;
let heartbeatError: Error = new TaskError('browser_account_inactive', 'inactive', 403);
t.mock.method(auth, 'getActiveSession', async () => session);
t.mock.method(auth, 'verifyCsrf', async () => true);
t.mock.method(tasks, 'heartbeat', async () => { throw heartbeatError; });
t.mock.method(getPool(config), 'query', async () => {
throw Object.assign(new Error('database-secret'), { code: 'ECONNRESET' });
});
records.length = 0;
const cases = [
{ status: 401, error: 'authentication_required', payload: { connection_id: 'test-browser' } },
{ status: 400, error: 'invalid_request', payload: {} },
{ status: 403, error: 'browser_account_inactive', payload: { connection_id: 'test-browser' } },
{ status: 500, error: 'ECONNRESET', payload: { connection_id: 'test-browser' } },
{ status: 503, error: 'ECONNRESET', payload: undefined }
];
for (const scenario of cases) {
if (scenario.status !== 401) session = heartbeatSession;
if (scenario.status === 500) {
heartbeatError = Object.assign(new Error('heartbeat-secret'), { code: 'ECONNRESET' });
}
const requestId = `request:failure:${scenario.status}`;
const path = scenario.status === 503 ? '/health/ready' : '/api/connections/heartbeat';
const response = await app.inject({
method: scenario.status === 503 ? 'GET' : 'POST',
url: `${path}?token=probe-secret`,
headers: { 'x-request-id': requestId },
...(scenario.payload ? { payload: scenario.payload } : {})
});
assert.equal(response.statusCode, scenario.status);
assert.equal(response.headers['x-request-id'], requestId);
const completed = records.find((record) => record.diagnostic_event === 'http.request.completed'
&& record.request_id === requestId);
assert.equal(completed?.status_code, scenario.status);
assert.equal(completed?.path, path);
assert.equal(completed?.level, scenario.status >= 500 ? 50 : 40);
assert.equal(typeof completed?.duration_ms, 'number');
assert.ok(records.some((record) => record.error_code === scenario.error
&& record.request_id === requestId), `${scenario.status}: ${scenario.error}`);
}
assert.equal(records.some((record) => record.diagnostic_event === 'http.request.started'), false);
assert.doesNotMatch(JSON.stringify(records), /probe-secret|heartbeat-secret|database-secret/u);
});
test('polling failures stay visible and task writes and detail reads keep access logs', async (t) => {
const { destination, records } = logCollector();
const config = testConfig();
const { app, auth, tasks } = await buildServer({
config,
startParserLoop: false,
loggerDestination: destination,
parser: { async parse() { return { blockers: ['test parser'] }; } }
});
t.after(() => app.close());
let session: ActiveSession | null = null;
t.mock.method(auth, 'getActiveSession', async () => session);
t.mock.method(auth, 'verifyCsrf', async () => true);
t.mock.method(getPool(config), 'query', async () => ({ rows: [{ applied: true }] }));
const failQuery = async () => { throw Object.assign(new Error('query-secret'), { code: 'ECONNRESET' }); };
t.mock.method(tasks, 'listTasksPage', failQuery);
t.mock.method(tasks, 'parserConnectionEvidence', failQuery);
const task = { task_id: 'TASK-TEST', status: 'awaiting_attachment' } as Awaited<ReturnType<TaskService['createTask']>>;
t.mock.method(tasks, 'createTask', async () => task);
t.mock.method(tasks, 'getTask', async () => task);
records.length = 0;
const cases = [
{ url: '/api/tasks?status=active', status: 401, error: 'authentication_required' },
{ url: '/api/tasks?limit=0', status: 400, error: 'invalid_request' },
{ url: '/api/tasks?status=confirmed', status: 500, error: 'ECONNRESET' },
{ url: '/api/status', status: 500, error: 'ECONNRESET' }
];
for (const [index, scenario] of cases.entries()) {
if (index > 0) session = heartbeatSession;
const requestId = `request:polling:${index}`;
const response = await app.inject({ method: 'GET', url: scenario.url, headers: { 'x-request-id': requestId } });
assert.equal(response.statusCode, scenario.status);
const logs = records.filter((record) => record.request_id === requestId);
assert.equal(logs.some((record) => record.diagnostic_event === 'http.request.started'), false);
assert.ok(logs.some((record) => record.error_code === scenario.error));
assert.ok(logs.some((record) => record.diagnostic_event === 'http.request.completed'
&& record.status_code === scenario.status && record.level === (scenario.status >= 500 ? 50 : 40)));
}
for (const method of ['POST', 'GET'] as const) {
const requestId = `request:business:${method}`;
const response = await app.inject({
method,
url: method === 'POST' ? '/api/tasks' : '/api/tasks/TASK-TEST',
headers: { 'x-request-id': requestId },
...(method === 'POST' ? { payload: { raw_text: 'business-secret' } } : {})
});
assert.equal(response.statusCode, 200);
assert.equal(response.json().task.task_id, task.task_id);
const logs = records.filter((record) => record.request_id === requestId);
assert.ok(logs.some((record) => record.diagnostic_event === 'http.request.started' && record.level === 30));
assert.ok(logs.some((record) => record.diagnostic_event === 'http.request.completed' && record.level === 30));
}
assert.doesNotMatch(JSON.stringify(records), /query-secret|business-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(