From fc48920c32fe78f752ccbb2cabe4aa118392bc19 Mon Sep 17 00:00:00 2001 From: xcs Date: Sat, 29 Aug 2026 07:02:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(loop-engine):=20=E3=80=8C=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=94=B6=E6=AD=A5=E3=80=8D=E7=95=99=E7=97=95=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=20internal=20=E2=80=94=E2=80=94=20machinery()=20=E6=8C=A1?= =?UTF-8?q?=E4=B8=8B=E7=9A=84=E7=AC=AC=E4=BA=94=E5=A4=84=E5=90=8C=E7=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R10:评审挖出第五个同病的 —— turn-runner.ts 那条「自动收步:…卡已交回」用的是 SKELETON 里就有的 kind: 'engine-transition',但 detail.internal: true 让它照样 被 machinery() 的第一刀滤掉,永远进不了工作日志。保留 item / auto 两个键,只删 internal。 新增回归测试:跑真实的 turn-runner.ts,捕获这一笔留痕喂给 structuredLog 全程, 断言可见且归系统。 顺带修实一条名不副实的既有用例:task-memory.test.ts:453 那条看着像在覆盖 「自动收步」这件事,但 entry() 工厂不设默认 detail,原入参是 {},根本没走到 machinery() 的 internal 过滤 —— 补上生产环境真实的 detail 形状(item / auto 两键)。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- .../__tests__/turn-runner-acceptance.test.ts | 43 +++++++++++++++++-- .../internal/usecase/turn-runner.ts | 8 +++- .../memory/__tests__/task-memory.test.ts | 10 ++++- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts b/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts index 073e83e..c22a919 100644 --- a/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts +++ b/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts @@ -6,6 +6,8 @@ */ import { describe, expect, it } from 'vitest'; import type { TaskId } from '@contracts'; +import { deriveSide, structuredLog } from '@modules/memory'; +import type { EntryKind, GovernanceMark, MemoryEntry, VersionStamp } from '@modules/memory'; import { createTurnRunner, echoTitleOf, handsOff, resendVerdict, sameGist, stalledTurns, toolInputHash, unmetWhy, answeredCardFact, earlyFinishTitleOf } from '../internal/usecase/turn-runner.ts'; import type { TurnRunnerDeps } from '../internal/usecase/turn-runner.ts'; import type { PendingAcceptance } from '../port.ts'; @@ -14,6 +16,30 @@ import type { CaseRuntime } from '../internal/domain/lifecycle.ts'; const TASK = 't-1' as TaskId; +/** 合成的信封字段 —— 与 `task-memory.test.ts` 的 `entry()` 工厂同一套写法 */ +const STAMP: VersionStamp = { contractVersion: '1', businessModelVersion: '1', skillVersions: [] }; +const GOVERNANCE: GovernanceMark = { masked: false, learnable: true }; + +/** + * **把捕获到的一笔真实留痕包成完整 `MemoryEntry`,喂给 `structuredLog` 验收**(2026-08-29,R10)。 + * 与 `auto-runner.test.ts` 的同名助手同一套写法 —— 见那边的注释。 + */ +function asLogEntry(captured: { kind: string; title: string; detail: Record | undefined }): MemoryEntry { + const kind = captured.kind as EntryKind; + return { + id: 'e-1', + taskId: TASK, + seq: 1, + at: '2026-08-29T00:00:00.000Z', + kind, + side: deriveSide(kind), + title: captured.title, + stamp: STAMP, + governance: GOVERNANCE, + ...(captured.detail === undefined ? {} : { detail: captured.detail }), + }; +} + /** 空案例模型:必做项全无 + autoComplete → 一进 runTurn 就「判齐」 */ const READY_MODEL: CaseModel = { name: '空案', autoComplete: true, items: [], sentries: [] } as never; const READY_RUNTIME: CaseRuntime = { items: [] } as never; @@ -38,7 +64,7 @@ type Harness = { /** 每次推帧时「已写下几行」—— 帧推在行动之前的话,它是 0 */ readonly writesAtPush: number[]; /** 这一拍写下的全部留痕 —— 断言「被拒/无事发生」走这里 */ - readonly appended: Array<{ kind: string; title: string }>; + readonly appended: Array<{ kind: string; title: string; detail: Record | undefined }>; /** 真被调到的工具名 —— 断言「拦没拦住调用」走这里 */ readonly toolCalls: string[]; /** 真立起来的监听 —— 断言「拦没拦住立监听」走这里 */ @@ -174,8 +200,8 @@ function harness( declared: async () => over.declaredListeners ?? [{ eventType: 'data', keyFilter: {} }], } as never, memory: { - append: async (entry: { kind: string; title: string }) => { - appended.push({ kind: entry.kind, title: entry.title }); + append: async (entry: { kind: string; title: string; detail?: Record }) => { + appended.push({ kind: entry.kind, title: entry.title, detail: entry.detail }); }, loadActivityMemory: async () => [], window: async () => over.trail ?? [], @@ -2311,6 +2337,17 @@ describe('auto 步:配方在拍首跑', () => { const out = await h.runner.runTurn(TASK); expect(h.issued).toHaveLength(0); expect(out.runtime.items.find((i) => i.id === 'S1')?.state).toBe('Completed'); + /* + * **配方留痕真的到得了工作日志**(2026-08-29,R10,spec §5.9 验收)。 + * 上一轮(Task 12)只测过 actorOf 这个纯函数;这里把「自动收步」这一笔捕获到的 + * 真实 {kind, title, detail} 喂给 structuredLog 全程 —— 从前它带着 `internal: true`, + * 会被 `machinery()` 挡在骨架之外,`actorOf` 判得再对也没用。 + */ + const settled = h.appended.find((e) => e.title.startsWith('自动收步:')); + expect(settled).toBeDefined(); + const lines = structuredLog([asLogEntry(settled!)]); + expect(lines).toHaveLength(1); + expect(lines[0]?.who).toBe('system'); }); /* diff --git a/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts b/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts index bc52d9a..ff2ec10 100644 --- a/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts +++ b/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts @@ -2037,7 +2037,13 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService { kind: 'engine-transition', /* 标题别以「完成」开头 —— `task-log.actorOf` 按 /^(完成|开始办)/ 认 AI,而这一步是引擎收的 */ title: `自动收步:${current.item} —— 卡已交回,这一步到此为止`, - detail: { internal: true, item: current.item, auto: current.item }, + /* + * **不再标 internal**(2026-08-29,R10)—— 从前的 `internal: true` 会被 + * `task-log.ts` 的 `machinery()` 挡下,这一笔永远进不了工作日志。 + * `engine-transition` 本就在骨架里,标题也不匹配「完成/开始办」的正则, + * 只要不被 `machinery()` 滤掉,它就会正确地归系统 —— 不必藏成内部痕。 + */ + detail: { item: current.item, auto: current.item }, }); await deps.state.save(taskId, model, parsed, settled); /* `Navigation.kind` 没有 'progressed' 这一档(只有 proceed/breakpoint/converged, diff --git a/apps/api/src/modules/memory/__tests__/task-memory.test.ts b/apps/api/src/modules/memory/__tests__/task-memory.test.ts index 067dd6a..9a0a856 100644 --- a/apps/api/src/modules/memory/__tests__/task-memory.test.ts +++ b/apps/api/src/modules/memory/__tests__/task-memory.test.ts @@ -450,7 +450,15 @@ describe('日志分得清谁干的', () => { it('引擎自己做主的仍署「系统」—— 起单不是谁办的,是流程走到了这儿', () => { const lines = structuredLog([ entry('engine-transition', { seq: 1, title: '起单:A1 · 询价受理' }), - entry('engine-transition', { seq: 2, title: '自动收步:查历史价' }), + /* + * **补上生产环境真实的 detail 形状**(2026-08-29,R10,评审 Important)—— + * `entry()` 工厂不设默认 `detail`,从前这一条造出来的入参是 `{}`, + * 根本没走到 `machinery()` 那道 `internal` 过滤,是因为无关的原因才绿的。 + * `turn-runner.ts` 真实留下的这一笔带 `item` / `auto` 两个键(见 R10 之后的 + * `turn-runner.ts:2039` 一带),这里照实给上,让这条测试真的守住「不带 internal + * 也能显示」这件事,而不是凑巧躲过了过滤。 + */ + entry('engine-transition', { seq: 2, title: '自动收步:查历史价', detail: { item: '查历史价', auto: '查历史价' } }), ]); expect(lines.map((l) => l.actor)).toEqual(['系统', '系统']); });