harness 与工作台同日修:数据工单确认即入库 · 叫了人不再自动推进(两道结构闸)· 审核卡 = 终局验收卡结构 · 验收清单人点达标 · 迟到守卫不丢放行裁决 · 画面跟着这一拍发出去的卡走 · 成果详情只翻页 · 占位居中 · 进度条

human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、
data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task
(ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest;
迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。
逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
xcs
2026-08-27 23:20:16 +08:00
co-authored by Claude Fable 5
parent 909b1fe4b7
commit 7b85a79323
79 changed files with 3903 additions and 1297 deletions
@@ -0,0 +1,119 @@
/**
* 迟到的人事件不再推一拍(2026-08-27,用户:「你叫了人我都没填你就直接推进了」)。
*
* 回传落库那一刻回传口已 wake、推进器随即跑了那一拍(读留痕里的 card-reply,不等事件);
* 人事件从队列里投过来晚十几秒,那时上一拍早已发出下一张卡 —— 再 tick 一拍,模型会把没人答的
* 那一步收掉。判法只有一个数:这一单在这条回传之后推进过没有(lastProgressAt > occurredAt)。
*/
import { describe, expect, it } from 'vitest';
import type { StaffId, TaskId } from '@contracts';
import type { LandedFacts, Task } from '../contract.ts';
import { createTaskService } from '../internal/usecase/task-service.ts';
import type { TaskServiceDeps } from '../internal/usecase/task-service.ts';
function harness(lastProgressAt: Date | undefined) {
const task: Task = {
id: 't-1' as TaskId,
activityId: 'w05',
ownerId: 's-1' as StaffId,
title: 'x',
runtime: 'awaiting-human',
envelope: {},
data: {},
caps: { steps: 40, tokens: 200000, humanCalls: 4, activeMinutes: 60 },
used: { steps: 0, tokens: 0, humanCalls: 0, activeMs: 0 },
startedAt: '2026-08-27T09:40:00.000Z',
};
const tasks = new Map<string, Task>([[task.id, task]]);
const appended: Array<{ kind: string; title: string; detail?: Record<string, unknown> }> = [];
let advanced = 0;
let onEvents = 0;
const deps: TaskServiceDeps = {
store: {
get: async (id) => tasks.get(id),
insert: async (t) => void tasks.set(t.id, t),
update: async (id, patch) => {
const next = { ...tasks.get(id)!, ...patch };
tasks.set(id, next);
return next;
},
addConsumption: async (id) => tasks.get(id)!.used,
lastProgressAt: async () => lastProgressAt,
markProgress: async () => undefined,
listByOwner: async () => [],
},
activities: { load: async () => ({ caps: task.caps, envelope: {}, inputSchema: {}, businessModel: {}, stallAfter: 'PT30M' }) },
schemas: { validate: () => ({ ok: true, errors: [] }) },
engine: {
load: async () => undefined,
advance: async () => {
advanced += 1;
return { status: 'awaiting' };
},
onEvent: async () => {
onEvents += 1;
return { status: 'running' };
},
},
memory: {
append: async (entry) => void appended.push({ kind: entry.kind, title: entry.title, ...(entry.detail === undefined ? {} : { detail: entry.detail }) }),
hasHistory: async () => true,
seal: async () => undefined,
distill: async () => undefined,
},
landing: { land: async () => ({ converged: true, emittedPorts: [] as string[], ledgerRowsMerged: 0, facts: {} as LandedFacts }) },
listeners: { hasArmedListener: async () => true },
persistence: { hasDrafts: async () => false, hasRuntimeState: async () => true },
events: { publishTerminal: async () => undefined, publishControl: async () => undefined },
notifier: { notify: async () => undefined },
interaction: { setPhase: async () => undefined },
intake: { askCompletion: async () => undefined },
comms: { record: async () => undefined },
clock: { now: () => new Date('2026-08-27T09:48:30.000Z') },
ids: { next: () => 't-x' },
};
return { service: createTaskService(deps), appended, advanced: () => advanced, onEvents: () => onEvents };
}
const REPLY_AT = '2026-08-27T09:47:48.000Z';
describe('迟到的人事件', () => {
it('回传之后这一单已推进过 → 只记一条到达痕(标 stale),不 onEvent、不推一拍', async () => {
const h = harness(new Date('2026-08-27T09:48:00.200Z'));
const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT });
expect(out.progressed).toBe(false);
expect(h.advanced()).toBe(0);
expect(h.onEvents()).toBe(0);
const arrival = h.appended.find((e) => e.title === '事件到达(human):card-p10');
expect(arrival?.detail?.['stale']).toBe(true);
});
it('回传之后没推进过 → 照常投递并推一拍(这就是叫醒它的那一下)', async () => {
const h = harness(new Date('2026-08-27T09:47:40.000Z'));
await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-s1', payload: {}, occurredAt: REPLY_AT });
expect(h.onEvents()).toBe(1);
expect(h.advanced()).toBe(1);
});
it('放行裁决即使迟到也要投:它只走事件这一条路,拍里读不到它', async () => {
const h = harness(new Date('2026-08-27T09:48:00.000Z'));
const out = await h.service.deliver('t-1' as TaskId, {
kind: 'human',
ref: 'card-acc',
payload: { mode: 'acceptance', verdict: 'pass', values: {} },
occurredAt: REPLY_AT,
});
expect(h.onEvents()).toBe(1);
expect(h.advanced()).toBe(1);
expect(out.progressed).toBe(true);
const arrival = h.appended.find((e) => e.kind === 'observation' && e.title.startsWith('事件到达'));
expect(arrival?.detail?.['stale']).toBeUndefined();
});
it('没带 occurredAt(老调用方)/ 不是人事件 → 行为照旧', async () => {
const h = harness(new Date('2026-08-27T09:48:00.200Z'));
await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-x', payload: {} });
await h.service.deliver('t-1' as TaskId, { kind: 'data', ref: 'row-1', payload: {}, occurredAt: REPLY_AT });
expect(h.advanced()).toBe(2);
});
});