feat: ship deterministic parser and lifecycle release

This commit is contained in:
inman committed 2026-08-28 17:13:58 +08:00
1 parent 208c434b31
commit 7b5d855b09
176 files changed
+20248 -1362

No files matched your search

+275 -12
View File
@@ -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'/);
});