回执增加内容

This commit is contained in:
andy committed 2026-09-20 14:59:19 +08:00
1 parent ea9c4b34c6
commit 40230ce33d
17 files changed
+729 -9

No files matched your search

+55 -2
View File
@@ -9,6 +9,7 @@ import { getPool, closePool, quoteIdentifier } from '../control-plane/src/db.ts'
import { encryptText } from '../control-plane/src/crypto.ts';
import { ArrangementLedger, currentArrangements } from '../control-plane/src/arrangement-ledger.ts';
import { TaskService } from '../control-plane/src/task-service.ts';
import { TeamProgressLedger } from '../control-plane/src/team-progress.ts';
import { taskResultText } from '../control-plane/src/agentbus.ts';
const rawUrl = process.env.LTJT_ARRANGEMENT_TEST_DATABASE_URL;
@@ -20,6 +21,7 @@ const config = loadConfig({ NODE_ENV: 'test', DATABASE_URL: rawUrl, DATABASE_SCH
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 31).toString('base64') });
const pool = getPool(config);
const ledger = new ArrangementLedger(config);
const progress = new TeamProgressLedger(config);
let created = false;
try {
await pool.query(`CREATE SCHEMA ${quoteIdentifier(schema)}`);
@@ -75,6 +77,7 @@ try {
assert.equal(retained.find(e => e.source_task_id === first.taskId).details.resource_name, '实际资源');
assert.equal((await pool.query('SELECT 1 FROM task_attempts WHERE id = $1', [first.executionId])).rowCount, 0);
assert.equal(currentArrangements(retained).length, 2, 'task cascade leaves ledger intact');
assert.ok((await progress.history(pool, ownerA, 'LW-TEST-A')).find(e => e.source_task_id === first.taskId).source_task_deleted_at);
const secondHotel = await task(ownerA, 'arrangement_hotel', 'create', '200');
await complete(ownerA, secondHotel);
@@ -89,15 +92,17 @@ try {
service.emitEvent = emit;
assert.equal((await pool.query('SELECT status FROM tasks WHERE task_id = $1', [rollback.taskId])).rows[0].status, 'running');
assert.ok(!(await ledger.history(pool, ownerA, 'LW-TEST-A')).some(e => e.execution_id === rollback.executionId));
assert.ok(!(await progress.history(pool, ownerA, 'LW-TEST-A')).some(e => e.execution_id === rollback.executionId));
// Deleting an archived, pre-feature success as the first interaction backfills it.
const historic = await task(ownerB, 'arrangement_transport');
await pool.query(`UPDATE tasks SET status = 'completed', archived_at = now(), success_receipt_at = now(),
execution_result_ciphertext = $2 WHERE task_id = $1`, [historic.taskId, encryptText(config, JSON.stringify(historic.result))]);
execution_result_ciphertext = $2, execution_result = $3 WHERE task_id = $1`, [historic.taskId, encryptText(config, JSON.stringify(historic.result)), {execution_id:historic.executionId}]);
await service.hardDeleteTask(ownerB, historic.taskId);
const backfill = await ledger.history(pool, ownerB, 'LW-TEST-A');
assert.equal(backfill.length, 1);
assert.equal(backfill[0].action, 'arrangement_transport');
assert.equal((await progress.history(pool, ownerB, 'LW-TEST-A')).length, 1);
assert.ok(backfill[0].source_task_deleted_at);
assert.ok(!(await ledger.history(pool, ownerA, 'LW-TEST-A')).some(e => e.execution_id === historic.executionId));
@@ -105,10 +110,58 @@ try {
[org, context('ledger-admin').userId]), e => e.code === '23514');
await assert.rejects(pool.query(`INSERT INTO arrangement_ledger_owners (organization_id, owner_user_id) VALUES ($1,$2)`,
[otherOrg, ownerA.userId]), e => e.code === '23503');
// Exercise the same transactional API for non-arrangement operations.
async function business(owner, action, data, refs, extra = {}) {
const item = await task(owner, action);
item.operation = {action, data};
item.result = {...item.result, summary:{action}, report:{...item.result.report, resolved_refs:refs}, ...extra};
await pool.query('UPDATE tasks SET operation = $2, operation_ciphertext = $3 WHERE task_id = $1',
[item.taskId,{action},encryptText(config,JSON.stringify(item.operation))]);
return item;
}
const parentRefs = {kind:'shared_plan',identifier:'LW-PROGRESS'};
const childRefs = {kind:'shared_child_order',identifier:'D300',parent_group_no:'LW-PROGRESS'};
const plan = await business(ownerA,'shared_plan_create',{},parentRefs);
await complete(ownerA,plan);
const child = await business(ownerA,'shared_child_order_create',{},childRefs,{erp_receipt:{order_number:'D300',parent_group_no:'LW-PROGRESS'}});
await complete(ownerA,child);
for (const count of [20,18]) {
const item = await business(ownerA,'passenger_list_import',{passenger_list:{row_count:count,tsv:'PRIVATE ROSTER'}},childRefs);
const outcome = await complete(ownerA,item);
assert.match(outcome.important_message.text,new RegExp('D300 名单:已导入,最近一次'+count+'人'));
assert.doesNotMatch(outcome.important_message.text,/PRIVATE ROSTER/);
if(count===18) await service.hardDeleteTask(ownerA,item.taskId);
}
const cancel = await business(ownerA,'order_cancel',{transition:{to_status:'已取消'}},parentRefs);
cancel.result.report.requery={matched:true,child_status_propagation_matched:true,child_refs:[{ddid:'300'}]};
await complete(ownerA,cancel);
const restore = await business(ownerA,'order_restore',{transition:{to_status:'已恢复'}},parentRefs);
const restored = await complete(ownerA,restore);
assert.match(restored.important_message.text,/订单状态:已恢复/);
assert.match(restored.important_message.text,/子单 D300.*已取消/);
assert.match(restored.important_message.text,/最近一次18人/,'latest roster survives deletion');
assert.match((await service.getTask(org,plan.taskId,ownerA)).important_message.text,/本系统记录子单:0个/,'old snapshot stays frozen');
assert.equal((await progress.history(pool,ownerB,'LW-PROGRESS')).length,0);
assert.equal((await progress.history(pool,{...ownerA,organizationId:otherOrg},'LW-PROGRESS')).length,0);
const partial = await business(ownerA,'shared_child_order_batch_create',{},parentRefs,{
status:'reconciliation_pending',uncertain:true,report:{status:'split_child_batch_stopped_after_write',batch_results:[
{status:'completed',adapter_status:'split_child_completed',parent_group_no:'LW-PARTIAL',child_order_no:'D401',blockers:[]},
{status:'uncertain',parent_group_no:'LW-PARTIAL',child_order_no:'D402'}]}
});
const partialOutcome = await complete(ownerA,partial);
assert.equal(partialOutcome.status,'reconciliation_pending');
assert.deepEqual((await progress.history(pool,ownerA,'LW-PARTIAL')).map(e=>e.subject_number),['D401']);
assert.ok(!partialOutcome.success_receipt,'partial progress does not produce a success receipt');
await assert.rejects(pool.query('INSERT INTO team_progress_owners (organization_id, owner_user_id) VALUES ($1,$2)',
[org,context('ledger-admin').userId]),e=>e.code==='23514');
await assert.rejects(pool.query('INSERT INTO team_progress_owners (organization_id, owner_user_id) VALUES ($1,$2)',
[otherOrg,ownerA.userId]),e=>e.code==='23503');
console.log(JSON.stringify({ ok: true, migrations: migrations.length, verified: [
'real migrations', 'result ingestion and public/AgentBus receipts', 'deduplication', 'account/org isolation',
'frozen receipts', 'force-delete retention', 'row-specific clear', 'transaction rollback',
'archived historical backfill before delete', 'admin and cross-org ownership constraints'
'archived historical backfill before delete', 'admin and cross-org ownership constraints',
'non-arrangement receipts', 'per-child latest roster without private data', 'retained roster after delete', 'parent restore preserves child cancellation',
'partial batch proven rows without false task success'
] }));
} finally {
if (created) await pool.query(`DROP SCHEMA ${quoteIdentifier(schema)} CASCADE`);