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|>重交 { const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 9).toString('base64') }); const calls: Array<{ sql: string; params: unknown[] }> = []; const client = { async query(sql: string, params: unknown[] = []) { 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) as unknown as { emitEvent: ( client: unknown, row: Record, data: Record, actorUserId: string ) => Promise; }; 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); });