Files
LWLT-AI/dist/control-plane/test/control-plane.test.js
2026-07-13 19:57:46 +08:00

139 lines
7.1 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { loadConfig } from '../src/config.js';
import { decryptText, encryptText, hashToken, sameTokenHash } from '../src/crypto.js';
import { buildServer } from '../src/server.js';
import { TaskService, classifyExecutionResult, executionStatusImmutable, executionUpdateAllowed } from '../src/task-service.js';
test('field encryption round-trips without storing plaintext', () => {
const config = loadConfig({
NODE_ENV: 'test',
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 7).toString('base64')
});
const plaintext = '含有客户信息的测试指令';
const encrypted = encryptText(config, plaintext);
assert.notEqual(encrypted, plaintext);
assert.equal(decryptText(config, encrypted), plaintext);
});
test('token hashes compare safely and do not expose the original token', () => {
const token = 'temporary-session-token';
const hash = hashToken(token);
assert.equal(hash.toString('utf8').includes(token), false);
assert.equal(sameTokenHash(hash, hashToken(token)), true);
assert.equal(sameTokenHash(hash, hashToken(`${token}-other`)), false);
});
test('control plane exposes a live health endpoint without a database connection', async () => {
const config = loadConfig({
NODE_ENV: 'test',
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 8).toString('base64'),
DATABASE_URL: 'postgresql://invalid:invalid@127.0.0.1:1/invalid'
});
const { app } = await buildServer({
config,
startParserLoop: false,
parser: {
async parse() {
return { blockers: ['test parser'] };
},
async checkConnection() {
return { ok: false, configured: false };
}
}
});
const response = await app.inject({ method: 'GET', url: '/health/live' });
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), { ok: true, service: 'ltjt-control-plane' });
await app.close();
});
test('migration contains the durable state tables and safety fields', async () => {
const { readFile } = await import('node:fs/promises');
const sql = await readFile(new URL('../migrations/001_initial.sql', import.meta.url), 'utf8');
for (const marker of ['organizations', 'users', 'sessions', 'tasks', 'task_events', 'task_attempts', 'idempotency_keys', 'audit_events', 'browser_connections', 'outbox_events', 'execution_result']) {
assert.match(sql, new RegExp(`\\b${marker}\\b`));
}
});
test('ERP execution results fail closed after a possible write', () => {
assert.deepEqual(classifyExecutionResult({ status: 'running' }), {
rawStatus: 'running',
status: 'running',
terminal: false,
uncertain: false
});
assert.equal(classifyExecutionResult({ status: 'completed', write_attempted: true }).status, 'completed');
assert.equal(classifyExecutionResult({ status: 'blocked', no_erp_write: true }).status, 'blocked');
assert.equal(classifyExecutionResult({ status: 'blocked', write_attempted: true }).status, 'reconciliation_pending');
assert.equal(classifyExecutionResult({ status: 'saved_unverified' }).status, 'reconciliation_pending');
assert.equal(classifyExecutionResult({ status: 'unexpected_plugin_state' }).status, 'reconciliation_pending');
assert.equal(executionUpdateAllowed('queued'), true);
assert.equal(executionUpdateAllowed('confirmed'), false);
assert.equal(executionStatusImmutable('completed'), true);
assert.equal(executionStatusImmutable('reconciliation_pending'), true);
});
test('ERP migration enforces one execution attempt per task', async () => {
const { readFile } = await import('node:fs/promises');
const sql = await readFile(new URL('../migrations/004_erp_execution_idempotency.sql', import.meta.url), 'utf8');
assert.match(sql, /UNIQUE INDEX[\s\S]+ON task_attempts \(task_id\)[\s\S]+phase = 'erp'/i);
assert.match(sql, /tasks_erp_lease_idx/);
});
test('operator page has a login gate and uses the durable task API', async () => {
const { readFile } = await import('node:fs/promises');
const index = await readFile(new URL('../../mock-business-system/index.html', import.meta.url), 'utf8');
const app = await readFile(new URL('../../mock-business-system/app.js', import.meta.url), 'utf8');
const bridge = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8');
const background = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
const inpage = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
assert.match(index, /id="loginPanel"/);
assert.match(index, /id="workbench"[^>]*hidden/);
assert.match(app, /apiRequest\('\/api\/tasks'/);
assert.match(app, /fetch\('\/api\/auth\/login'/);
assert.match(app, /The backend is authoritative/);
assert.match(app, /syncRequested/);
assert.match(app, /api\/events\?since=/);
assert.ok(app.indexOf('/claim') < app.indexOf("sendToExtension('CREATE_TASK'"), 'server claim must precede extension dispatch');
assert.doesNotMatch(app, /syncPendingTasks|>重交</);
assert.match(app, /taskStore\.filter\(isTaskPollable\)/);
assert.match(app, /execution_id: executionId/);
assert.doesNotMatch(bridge, /async function upsertBusinessTask/);
assert.match(bridge, /task: currentBusinessTask/);
assert.match(background, /businessTaskExecutions:[\s\S]+businessTasks: tasks/);
assert.match(inpage, /deferred_dialog_callbacks_suppressed/);
assert.doesNotMatch(inpage, /callback\.call\(window\)/);
});
test('system task events persist a null actor instead of an invalid empty UUID', async () => {
const config = loadConfig({
NODE_ENV: 'test',
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 9).toString('base64')
});
const calls = [];
const client = {
async query(sql, params = []) {
calls.push({ sql, params });
if (sql.includes('INSERT INTO task_events')) {
return {
rows: [{
id: '1',
organization_id: '00000000-0000-0000-0000-000000000001',
task_id: '00000000-0000-0000-0000-000000000002',
status: 'parse_failed',
stage: 'parse',
message: 'blocked',
payload: {},
created_at: new Date('2026-07-13T00:00:00.000Z')
}]
};
}
return { rows: [] };
}
};
const service = new TaskService(config);
await service.emitEvent(client, {
id: '00000000-0000-0000-0000-000000000002',
organization_id: '00000000-0000-0000-0000-000000000001',
task_id: 'TASK-SYSTEM-EVENT'
}, {
status: 'parse_failed',
stage: 'parse',
message: 'blocked'
}, '');
assert.equal(calls[0].params[6], null);
});
//# sourceMappingURL=control-plane.test.js.map