Archived
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user