feat: ship deterministic parser and lifecycle release
This commit is contained in:
1 parent
208c434b31
commit
7b5d855b09
176 files changed
+20248
-1362
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import test from 'node:test';
|
||||
@@ -10,12 +10,14 @@ import { REQUIRED_SCHEMA_VERSION } from '../src/db.js';
|
||||
import {
|
||||
TaskService,
|
||||
authorizeLifecycleOperationForManualConfirmation,
|
||||
buildImmediateParserPromotionGates,
|
||||
classifyExpiredExecutionResult,
|
||||
classifyExecutionResult,
|
||||
executionLifecycleFacts,
|
||||
failureSummary,
|
||||
executionStatusImmutable,
|
||||
executionUpdateAllowed,
|
||||
hasPrewriteNoErpEvidence,
|
||||
hasLifecycleTestContext,
|
||||
isLifecycleOperation,
|
||||
parseLifecycleFacts,
|
||||
@@ -33,6 +35,8 @@ test('message routing starts a new session for a business directive, not for a s
|
||||
assert.equal(detectBusinessDirective('散拼团拼单\n预订客户:辽宁康辉'), '散拼团拼单');
|
||||
assert.equal(detectBusinessDirective('补充车型/项目说明:旅游大巴'), null);
|
||||
assert.equal(detectBusinessDirective('请继续处理,安排用车'), null);
|
||||
assert.equal(detectBusinessDirective('AgentBus 转发\n安排用车\n团号:LW-260903A-A'), '安排用车');
|
||||
assert.equal(detectBusinessDirective(`发团日期:09-03\n预订客户:老挝联泰人名币\n产品搜索:遇见老挝\n预估人数:15+1\n用房数量:8标1单`), null);
|
||||
assert.equal(isNewDirectiveMessage('导入散拼子单名单\n预订客户:辽宁康辉'), true);
|
||||
assert.equal(isNewDirectiveMessage('数量改为 2'), false);
|
||||
});
|
||||
@@ -124,7 +128,7 @@ test('production OSS configuration requires provider credentials and derives the
|
||||
assert.equal(config.DATA_RETENTION_ENABLED, false);
|
||||
});
|
||||
|
||||
test('OSS artifact store writes metadata with an idempotent UUID execution path', async () => {
|
||||
test('OSS artifact store writes metadata with an idempotent UUID execution path and cleans captured keys', async () => {
|
||||
const config = loadConfig({
|
||||
NODE_ENV: 'test',
|
||||
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 19).toString('base64'),
|
||||
@@ -181,6 +185,11 @@ test('OSS artifact store writes metadata with an idempotent UUID execution path'
|
||||
assert.equal(artifact.storage_backend, 'oss');
|
||||
assert.equal(artifact.public_url, 'https://test-bucket.oss-cn-guangzhou.aliyuncs.com/liansyn-platform/attachments/44444444-4444-4444-8444-444444444444/0');
|
||||
assert.deepEqual(calls, [{ operation: 'put', key: 'liansyn-platform/attachments/44444444-4444-4444-8444-444444444444/0' }]);
|
||||
await store.cleanup([artifact]);
|
||||
assert.deepEqual(calls, [
|
||||
{ operation: 'put', key: 'liansyn-platform/attachments/44444444-4444-4444-8444-444444444444/0' },
|
||||
{ operation: 'delete', key: 'liansyn-platform/attachments/44444444-4444-4444-8444-444444444444/0' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('token hashes compare safely and do not expose the original token', () => {
|
||||
@@ -191,17 +200,17 @@ test('token hashes compare safely and do not expose the original token', () => {
|
||||
assert.equal(sameTokenHash(hash, hashToken(`${token}-other`)), false);
|
||||
});
|
||||
|
||||
test('organization automation applies uniformly to every valid AgentBus business action', () => {
|
||||
test('organization automation applies uniformly to every valid task source', () => {
|
||||
const eligible = {
|
||||
needsInput: false,
|
||||
blocked: false,
|
||||
source: 'agentbus',
|
||||
automationEnabled: true
|
||||
};
|
||||
assert.equal(shouldAutomaticallyConfirm(eligible), true);
|
||||
assert.equal(shouldAutomaticallyConfirm({ ...eligible, source: 'agentbus' }), true);
|
||||
assert.equal(shouldAutomaticallyConfirm({ ...eligible, source: 'manual' }), true);
|
||||
assert.equal(shouldAutomaticallyConfirm({ ...eligible, needsInput: true }), false);
|
||||
assert.equal(shouldAutomaticallyConfirm({ ...eligible, blocked: true }), false);
|
||||
assert.equal(shouldAutomaticallyConfirm({ ...eligible, source: 'manual' }), false);
|
||||
assert.equal(shouldAutomaticallyConfirm({ ...eligible, automationEnabled: false }), false);
|
||||
});
|
||||
|
||||
@@ -253,7 +262,7 @@ test('control plane requires the latest durable task-outcome migration before re
|
||||
const { readFile } = await import('node:fs/promises');
|
||||
const db = await readFile(new URL('../src/db.ts', import.meta.url), 'utf8');
|
||||
const server = await readFile(new URL('../src/server.ts', import.meta.url), 'utf8');
|
||||
assert.equal(REQUIRED_SCHEMA_VERSION, '012_agentbus_user_channels');
|
||||
assert.equal(REQUIRED_SCHEMA_VERSION, '014_task_input_attachments');
|
||||
assert.match(db, /schema_migrations/);
|
||||
assert.match(db, /databaseReadiness/);
|
||||
assert.match(db, /assertDatabaseSchema/);
|
||||
@@ -322,6 +331,41 @@ test('successful ERP executor results expose consistent lifecycle boundary facts
|
||||
});
|
||||
});
|
||||
|
||||
test('reconciliation can only be downgraded to no-write with explicit page evidence', () => {
|
||||
const storedMisclassification = {
|
||||
status: 'execution_uncertain',
|
||||
write_attempted: true,
|
||||
execution_phase: 'write_started',
|
||||
report: {
|
||||
status: 'live_submit_blocked',
|
||||
submit_safety: { live_submit_attempted: false },
|
||||
native_request: null,
|
||||
server_response: null,
|
||||
requery: null,
|
||||
side_effects: []
|
||||
}
|
||||
};
|
||||
assert.equal(hasPrewriteNoErpEvidence(storedMisclassification), true);
|
||||
assert.equal(hasPrewriteNoErpEvidence({
|
||||
...storedMisclassification,
|
||||
report: {
|
||||
...storedMisclassification.report,
|
||||
submit_safety: { live_submit_attempted: true }
|
||||
}
|
||||
}), false);
|
||||
assert.equal(hasPrewriteNoErpEvidence({
|
||||
...storedMisclassification,
|
||||
report: {
|
||||
...storedMisclassification.report,
|
||||
ajax_records: [{ prevented_from_network: false, network_request_attempted: true }]
|
||||
}
|
||||
}), false);
|
||||
assert.equal(hasPrewriteNoErpEvidence({
|
||||
...storedMisclassification,
|
||||
report: { ...storedMisclassification.report, side_effects: { unknown: true } }
|
||||
}), false);
|
||||
});
|
||||
|
||||
test('success receipts require verifiable ERP evidence', () => {
|
||||
assert.deepEqual(successReceiptFromResult({
|
||||
status: 'completed',
|
||||
@@ -430,6 +474,51 @@ test('unified lifecycle response plus fresh requery is accepted as completion ev
|
||||
assert.equal(successReceiptFromResult(reviewRequired), null);
|
||||
});
|
||||
|
||||
test('passenger list explicit server success does not require a post-save row requery', () => {
|
||||
const result = {
|
||||
status: 'completed',
|
||||
write_attempted: true,
|
||||
no_erp_write: false,
|
||||
summary: { action: 'passenger_list_import' },
|
||||
report: {
|
||||
status: 'lifecycle_completed',
|
||||
resolved_refs: {
|
||||
kind: 'shared_child_order', identifier: 'D12345',
|
||||
tid: '17100', ddid: '12345'
|
||||
},
|
||||
server_response: { completed: true, http_status: 200 },
|
||||
requery: {
|
||||
matched: true,
|
||||
required: false,
|
||||
skipped: true,
|
||||
completion_policy: 'passenger_list_explicit_server_success'
|
||||
},
|
||||
blockers: [],
|
||||
manual_review_required: false
|
||||
}
|
||||
};
|
||||
assert.deepEqual(successReceiptFromResult(result), {
|
||||
status: 'verified',
|
||||
success: true,
|
||||
identifier: 'D12345',
|
||||
target_kind: 'shared_child_order',
|
||||
action: 'passenger_list_import',
|
||||
server_response_completed: true,
|
||||
requery_matched: false,
|
||||
requery_required: false,
|
||||
completion_policy: 'passenger_list_explicit_server_success',
|
||||
source_only: false,
|
||||
order_number: 'D12345',
|
||||
verification_status: 'lifecycle_completed'
|
||||
});
|
||||
assert.equal(classifyExecutionResult(result).status, 'completed');
|
||||
|
||||
const wrongAction = structuredClone(result) as any;
|
||||
wrongAction.summary.action = 'arrangement_hotel';
|
||||
assert.equal(successReceiptFromResult(wrongAction), null);
|
||||
assert.equal(classifyExecutionResult(wrongAction).status, 'reconciliation_pending');
|
||||
});
|
||||
|
||||
test('backend-attached export requires frozen hashes and explicit no-delivery evidence before it becomes a success receipt', () => {
|
||||
const artifacts = Array.from({ length: 10 }, (_, index) => ({
|
||||
type: `source-${index + 1}`,
|
||||
@@ -785,6 +874,20 @@ test('uncertain ERP writes have a reconciliation-specific failure code', () => {
|
||||
assert.match(failure?.failure_message || '', /ERP 写入结果不确定/);
|
||||
});
|
||||
|
||||
test('successful parse lifecycle facts are not promoted to a task failure', () => {
|
||||
const failure = failureSummary({
|
||||
status: 'agent_parse_passed',
|
||||
operation: { action: 'team_order_create' },
|
||||
blockers: [],
|
||||
agent_returned: true,
|
||||
plugin_dispatch_started: false,
|
||||
erp_write_started: false,
|
||||
no_plugin_dispatch: true,
|
||||
no_erp_write: true
|
||||
}, '', 'completed', 'verification');
|
||||
assert.equal(failure, null);
|
||||
});
|
||||
|
||||
test('expired plugin results fail safely and preserve reconciliation when the write boundary is unknown', () => {
|
||||
assert.deepEqual(classifyExpiredExecutionResult({
|
||||
status: 'running',
|
||||
@@ -831,6 +934,31 @@ test('ERP business-rule rejection preserves the native message for the user', ()
|
||||
assert.equal(failure?.failure_message, message);
|
||||
});
|
||||
|
||||
test('passenger capacity rejection preserves the business message and retains technical evidence', () => {
|
||||
const message = '本次名单共 25 人,超过该独立团在 ERP 中最多可录入的 16 人。请调整名单人数后重新提交。';
|
||||
const blockers = [
|
||||
'passenger_native_target_row_count_not_reached:16:25',
|
||||
'passenger_native_target_row_missing:17',
|
||||
'passenger_dom_target_row_count_too_small:16:25'
|
||||
];
|
||||
const failure = failureSummary({
|
||||
status: 'blocked',
|
||||
error_code: 'erp_passenger_count_limit_exceeded',
|
||||
failure_stage: 'lifecycle_live',
|
||||
failure_source: 'erp',
|
||||
failure_message: message,
|
||||
message,
|
||||
blockers,
|
||||
write_attempted: false,
|
||||
no_erp_write: true
|
||||
});
|
||||
assert.equal(failure?.error_code, 'erp_passenger_count_limit_exceeded');
|
||||
assert.equal(failure?.failure_stage, 'lifecycle_live');
|
||||
assert.equal(failure?.failure_source, 'erp');
|
||||
assert.equal(failure?.failure_message, message);
|
||||
assert.deepEqual(failure?.validation_errors, blockers);
|
||||
});
|
||||
|
||||
test('nested ERP business feedback is promoted when the executor omitted failure metadata', () => {
|
||||
const message = '此团还有【应收团款】账,不能取消!';
|
||||
const failure = failureSummary({
|
||||
@@ -932,9 +1060,15 @@ test('AgentBus channel migration scopes encrypted keys and durable replies', asy
|
||||
const server = await readFile(new URL('../src/server.ts', import.meta.url), 'utf8');
|
||||
assert.ok(server.includes("app.get('/api/channels'"));
|
||||
assert.ok(server.includes("app.post('/api/channels'"));
|
||||
assert.ok(server.includes("app.delete('/api/channels/:channelId'"));
|
||||
assert.ok(server.includes('rotate-key'));
|
||||
assert.ok(server.includes("app.get('/channels'"));
|
||||
const channels = await readFile(new URL('../src/agentbus-channels.ts', import.meta.url), 'utf8');
|
||||
assert.match(channels, /async delete\(/);
|
||||
assert.match(channels, /DELETE FROM user_channels/);
|
||||
assert.match(channels, /agentbus_channel\.deleted/);
|
||||
assert.match(channels, /channel_managed_by_environment/);
|
||||
assert.match(channels, /deletable:/);
|
||||
assert.match(channels, /currentExternalUserRef === LEGACY_CHANNEL_REF/);
|
||||
assert.match(channels, /LEGACY_CHANNEL_REF/);
|
||||
assert.match(channels, /WHERE NOT EXISTS/);
|
||||
@@ -945,10 +1079,76 @@ test('AgentBus channel migration scopes encrypted keys and durable replies', asy
|
||||
const agentBus = await readFile(new URL('../src/agentbus.ts', import.meta.url), 'utf8');
|
||||
assert.match(agentBus, /stop\(persistStatus = true\)/);
|
||||
assert.match(channels, /listener\.stop\(false\)/);
|
||||
assert.match(channels, /reloadRequested = true/);
|
||||
assert.match(channels, /while \(this\.started && this\.reloadRequested\)/);
|
||||
assert.match(channels, /runtimeStatusWrites/);
|
||||
assert.match(channels, /previous[\s\S]+\.catch\(\(\) => undefined\)/);
|
||||
});
|
||||
|
||||
test('business parser migration snapshots modes and encrypts dual candidates', async () => {
|
||||
const { readFile } = await import('node:fs/promises');
|
||||
const sql = await readFile(new URL('../migrations/013_business_parser_modes.sql', import.meta.url), 'utf8');
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS business_parser_settings/);
|
||||
assert.match(sql, /mode IN \('ai', 'shadow', 'auto', 'program'\)/);
|
||||
assert.match(sql, /ADD COLUMN IF NOT EXISTS parser_config_revision/);
|
||||
assert.match(sql, /ADD COLUMN IF NOT EXISTS parser_engine_affinity/);
|
||||
assert.match(sql, /parser_engine_affinity = 'ai'/);
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS parse_decisions/);
|
||||
assert.match(sql, /program_result_ciphertext text/);
|
||||
assert.match(sql, /ai_result_ciphertext text/);
|
||||
assert.match(sql, /UNIQUE \(task_id, attempt_no\)/);
|
||||
const taskService = await readFile(new URL('../src/task-service.ts', import.meta.url), 'utf8');
|
||||
assert.match(taskService, /pg_advisory_xact_lock/);
|
||||
assert.match(taskService, /parserFieldDifferences/);
|
||||
assert.match(taskService, /parser_program_result_ciphertext/);
|
||||
assert.match(taskService, /parser_unreviewed_difference_count/);
|
||||
assert.match(taskService, /t\.parser_mode, t\.parser_config_revision/);
|
||||
assert.match(taskService, /display_order: displayIndex \+ 1/);
|
||||
assert.match(taskService, /buildImmediateParserPromotionGates/);
|
||||
assert.equal([...taskService.matchAll(/resolveParserModeSnapshot\(client, context\.organizationId,/g)].length, 2);
|
||||
assert.doesNotMatch(taskService, /shadow_continuous_days|shadow_real_tasks|auto_continuous_days|program_authoritative_tasks|ai_fallback_rate_percent/);
|
||||
const server = await readFile(new URL('../src/server.ts', import.meta.url), 'utf8');
|
||||
for (const route of [
|
||||
'/api/settings/parser-routing',
|
||||
'/api/settings/parser-routing/:routeId',
|
||||
'/api/settings/parser-routing/emergency-ai',
|
||||
'/api/tasks/:taskId/reparse',
|
||||
'/api/parser-decisions/:decisionId/review'
|
||||
]) assert.ok(server.includes(route), route);
|
||||
});
|
||||
|
||||
test('parser promotion uses immediate correctness checks without time, volume, or fallback-rate waits', () => {
|
||||
const ready = buildImmediateParserPromotionGates({
|
||||
programVersion: 'ltjt-program-parser-test',
|
||||
otherRoutesInAuto: 0,
|
||||
unreviewedDifferences: 0,
|
||||
programWrong: 0,
|
||||
programContractErrors: 0,
|
||||
programRuntimeErrors: 0,
|
||||
downstreamFailures: 0
|
||||
});
|
||||
assert.equal(ready.shadow.eligible, true);
|
||||
assert.equal(ready.auto.eligible, true);
|
||||
assert.equal(ready.program.eligible, true);
|
||||
const codes = Object.values(ready).flatMap((gate) => gate.requirements.map((item) => item.code));
|
||||
assert.deepEqual(codes.filter((code) => /days|tasks|rate/.test(code)), []);
|
||||
|
||||
const blocked = buildImmediateParserPromotionGates({
|
||||
programVersion: 'ltjt-program-parser-test',
|
||||
otherRoutesInAuto: 1,
|
||||
unreviewedDifferences: 1,
|
||||
programWrong: 1,
|
||||
programContractErrors: 1,
|
||||
programRuntimeErrors: 1,
|
||||
downstreamFailures: 1
|
||||
});
|
||||
assert.equal(blocked.shadow.eligible, true);
|
||||
assert.equal(blocked.auto.eligible, false);
|
||||
assert.equal(blocked.program.eligible, false);
|
||||
assert.deepEqual(blocked.shadow.requirements.map((item) => item.code), ['program_implemented']);
|
||||
assert.equal(Object.values(blocked).flatMap((gate) => gate.requirements).some((item) => item.code === 'release_sequence_ready'), false);
|
||||
});
|
||||
|
||||
test('persistent-session migration removes automatic expiry for active sessions', async () => {
|
||||
const { readFile } = await import('node:fs/promises');
|
||||
const sql = await readFile(new URL('../migrations/008_persistent_sessions.sql', import.meta.url), 'utf8');
|
||||
@@ -974,8 +1174,8 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
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(index, /styles\.css\?v=20260817-channels-root-4/);
|
||||
assert.match(index, /app\.js\?v=20260817-channels-root-4/);
|
||||
assert.match(index, /styles\.css\?v=20260827-roster-attachment-1/);
|
||||
assert.match(index, /app\.js\?v=20260828-shared-plan-visitor-export-1/);
|
||||
assert.match(index, /id="statusDetailsPopover"/);
|
||||
assert.match(index, /id="statusDetailsRefresh"/);
|
||||
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);
|
||||
@@ -983,6 +1183,9 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.match(app, /HISTORY_TASK_PAGE_SIZE/);
|
||||
assert.match(app, /historyFilters/);
|
||||
assert.match(app, /historyPagination/);
|
||||
assert.match(index, /id="historyBatchActions"/);
|
||||
assert.match(index, /id="historySelectAll"/);
|
||||
assert.match(index, /id="historyDeleteSelectedButton"/);
|
||||
assert.match(app, /IS_HISTORY_PAGE = CURRENT_PAGE === '\/history'/);
|
||||
assert.match(index, /href="\/history"/);
|
||||
assert.match(server, /app\.get\('\/history'/);
|
||||
@@ -990,8 +1193,24 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.match(index, /id="channelsPage"[^>]*hidden/);
|
||||
assert.match(index, /href="\/channels"/);
|
||||
assert.match(server, /app\.get\('\/channels'/);
|
||||
assert.match(app, /IS_PARSER_ROUTING_PAGE = CURRENT_PAGE === '\/parser-routing'/);
|
||||
assert.match(app, /route\.display_order/);
|
||||
assert.match(index, /id="parserRoutingPage"[^>]*hidden/);
|
||||
assert.match(index, /href="\/parser-routing"/);
|
||||
assert.match(server, /app\.get\('\/parser-routing'/);
|
||||
assert.match(styles, /html\.parser-routing-page-active/);
|
||||
assert.match(styles, /grid-template-rows: auto auto auto auto/);
|
||||
assert.match(styles, /\.detail-panel\s*\{[^}]*grid-template-rows: auto auto auto;[^}]*overflow-y: auto;/s);
|
||||
assert.match(styles, /\.task-detail\s*\{[^}]*height: auto;[^}]*overflow: visible;/s);
|
||||
assert.match(styles, /\.task-detail > \.task-lifecycle-panel\s*\{[^}]*flex: 0 0 auto;/s);
|
||||
assert.match(index, /全部切回 AI/);
|
||||
assert.match(app, /changeParserRoutingMode/);
|
||||
assert.match(app, /renameChannel\(channelId\)/);
|
||||
assert.match(app, /channelAction = 'rename'/);
|
||||
assert.match(app, /deleteChannel\(channelId\)/);
|
||||
assert.match(app, /channelAction = 'delete'/);
|
||||
assert.match(app, /apiRequest\(`\/api\/channels\/\$\{encodeURIComponent\(channelId\)\}`,[\s\S]+method: 'DELETE'/);
|
||||
assert.match(app, /持久化 AgentBus 回执记录(包括未发送回执)会被移除/);
|
||||
assert.match(server, /search: z\.string\(\)\.max\(200\)\.optional\(\)/);
|
||||
assert.match(server, /offset: z\.coerce\.number\(\)\.int\(\)\.min\(0\)/);
|
||||
assert.match(server, /include_total/);
|
||||
@@ -1064,7 +1283,13 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.doesNotMatch(app, /lifecycle\.scrollTop = lifecycle\.scrollHeight/);
|
||||
assert.match(app, /taskState\.textContent = task \? taskStatusLabel\(task\)/);
|
||||
assert.match(app, /source: 'task_error_summary'/);
|
||||
assert.doesNotMatch(app, /task-card-delete|data-task-action/);
|
||||
assert.match(app, /data-history-task-select/);
|
||||
assert.match(app, /data-history-task-delete/);
|
||||
assert.match(app, /const historySelectedTaskIds = new Set\(\)/);
|
||||
assert.match(app, /async function deleteHistorySelectedTasks\(\)/);
|
||||
assert.match(app, /apiRequest\('\/api\/tasks\/bulk-delete'/);
|
||||
assert.match(app, /function renderHistoryBatchActions\(visibleTasks/);
|
||||
assert.match(app, /historySelectedTaskIds\.clear\(\)/);
|
||||
assert.match(app, /recorded_at/);
|
||||
assert.match(app, /技术详情/);
|
||||
assert.match(styles, /\.task-important-message-card/);
|
||||
@@ -1073,7 +1298,13 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.match(app, /AgentBus/);
|
||||
assert.match(app, /人工输入/);
|
||||
assert.match(index, /id="automationToggleButton"/);
|
||||
assert.match(index, /所有来源的新任务解析通过后/);
|
||||
assert.match(app, /apiRequest\('\/api\/settings\/automation'/);
|
||||
assert.match(app, /所有来源的新任务解析通过后将自动进入 ERP/);
|
||||
assert.match(app, /automationSettingsSyncInFlight/);
|
||||
assert.match(app, /eventStream\.addEventListener\('open',[\s\S]+syncAutomationSettings\(\{ background: true \}\)/);
|
||||
assert.match(app, /async function refreshBackgroundState\(\)[\s\S]+syncAutomationSettings\(\{ background: true \}\)/);
|
||||
assert.match(app, /window\.addEventListener\('focus',[\s\S]+refreshBackgroundState\(\)/);
|
||||
assert.match(app, /async function autoDispatchReadyTasks\(\{ force = false \} = \{\}\)/);
|
||||
assert.match(app, /if \(!force && retryAt > Date\.now\(\)\) continue/);
|
||||
assert.match(app, /const bridgeReadyForDispatch = !wasBridgeConnected/);
|
||||
@@ -1090,13 +1321,14 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.doesNotMatch(app, /formatArtifactBytes/);
|
||||
assert.match(app, /生命周期测试写入审核/);
|
||||
assert.match(app, /审核并授权本次测试写入/);
|
||||
assert.match(app, /AgentBus 任务不因生命周期类型转为人工确认/);
|
||||
assert.match(app, /任何来源的任务都不因生命周期类型转为人工确认/);
|
||||
assert.match(server, /api\/settings\/automation/);
|
||||
assert.match(taskService, /automation_enabled/);
|
||||
assert.match(taskService, /task\.auto_confirmed/);
|
||||
assert.match(taskService, /prepareParsedOperationForConfirmation/);
|
||||
assert.match(taskService, /authorizeLifecycleOperationForManualConfirmation/);
|
||||
assert.match(taskService, /shouldAutomaticallyConfirm\(\{/);
|
||||
assert.doesNotMatch(taskService, /text\(source\) === 'agentbus'/);
|
||||
assert.doesNotMatch(taskService, /!isLifecycleOperation\(operation\)/);
|
||||
assert.match(styles, /\.task-operation-review-card/);
|
||||
assert.match(styles, /\.task-important-message-card\.state-ok/);
|
||||
@@ -1109,14 +1341,28 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.match(app, /void sendToExtension\('DELETE_TASK'/);
|
||||
assert.doesNotMatch(app, /await sendToExtension\('DELETE_TASK'/);
|
||||
assert.match(taskService, /async hardDeleteTask\(/);
|
||||
assert.match(taskService, /async hardDeleteTasks\(/);
|
||||
assert.match(taskService, /task_id = ANY\(\$2::text\[\]\)/);
|
||||
assert.match(taskService, /const missingTaskIds = normalizedTaskIds\.filter/);
|
||||
assert.match(taskService, /DELETE FROM audit_events[\s\S]+entity_id = ANY\(\$2::text\[\]\)/);
|
||||
assert.match(taskService, /DELETE FROM audit_events/);
|
||||
const hardDeleteTasksSource = taskService.slice(
|
||||
taskService.indexOf('async hardDeleteTasks('),
|
||||
taskService.indexOf('async cancelTask(')
|
||||
);
|
||||
assert.match(hardDeleteTasksSource, /FROM task_artifacts a/);
|
||||
assert.match(hardDeleteTasksSource, /FOR UPDATE OF a/);
|
||||
assert.match(hardDeleteTasksSource, /this\.artifactStore\.cleanup\(outcome\.artifacts\)/);
|
||||
assert.match(server, /taskBulkDeleteSchema/);
|
||||
assert.match(server, /\.max\(100\)/);
|
||||
assert.match(server, /app\.post\('\/api\/tasks\/bulk-delete'/);
|
||||
assert.match(server, /app\.delete\('\/api\/tasks\/:taskId'/);
|
||||
assert.match(bridge, /status: 'deleted'/);
|
||||
assert.match(background, /LTJT_HARD_DELETE_TASK/);
|
||||
assert.doesNotMatch(app, /task-json-output|taskResponseJson/);
|
||||
assert.doesNotMatch(styles, /\.task-json-output/);
|
||||
assert.doesNotMatch(app, /appendTaskLogEntry|task\.logs/);
|
||||
assert.match(app, /title: 'AI处理阶段'/);
|
||||
assert.match(app, /title: '系统解析阶段'/);
|
||||
assert.match(app, /title: '系统写入处理'/);
|
||||
assert.match(app, /task-stage-card is-\$\{state\.mode\}/);
|
||||
assert.match(styles, /\.task-stage-card\.is-pending/);
|
||||
@@ -1125,7 +1371,8 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
assert.match(styles, /\.task-stage-card \{[\s\S]*display: flex;[\s\S]*justify-content: space-between;/);
|
||||
assert.match(styles, /\.workbench-grid \{[\s\S]*min-width: 0;[\s\S]*overflow: hidden;/);
|
||||
assert.match(styles, /overflow-x: hidden/);
|
||||
assert.doesNotMatch(styles, /\.task-card-delete/);
|
||||
assert.match(styles, /\.task-card-delete/);
|
||||
assert.match(styles, /\.history-batch-actions/);
|
||||
assert.match(app, /currentOption = stage\.options\.find/);
|
||||
assert.match(app, /task-stage-status is-current/);
|
||||
assert.doesNotMatch(app, /for \(const option of stage\.options\)/);
|
||||
@@ -1317,4 +1564,20 @@ test('public task exposes persisted outcome timestamps and keeps the latest erro
|
||||
assert.match(importantMessage.text, /LW-260907A-B/);
|
||||
assert.deepEqual(importantMessage.success_receipt, task.success_receipt);
|
||||
assert.equal(task.source, 'manual');
|
||||
assert.equal(task.failure, null);
|
||||
});
|
||||
|
||||
test('platform only synthesizes an error timeline entry for a currently failed task', async () => {
|
||||
const app = await readFile(new URL('../../LianSyn-platform/app.js', import.meta.url), 'utf8');
|
||||
const failureInfo = app.slice(
|
||||
app.indexOf('function taskFailureInfo'),
|
||||
app.indexOf('function failureStageLabel')
|
||||
);
|
||||
const lifecycleEntries = app.slice(
|
||||
app.indexOf('function taskLifecycleEntries'),
|
||||
app.indexOf('function lifecycleLevel')
|
||||
);
|
||||
assert.match(failureInfo, /isFailureStatus\(status\)/);
|
||||
assert.match(lifecycleEntries, /isFailureStatus\(status\)/);
|
||||
assert.match(lifecycleEntries, /source: 'task_error_summary'/);
|
||||
});
|
||||
Reference in new issue
Block a user