Archived
fix(loop-engine): 「自动收步」留痕去掉 internal —— machinery() 挡下的第五处同病
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { TaskId } from '@contracts';
|
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 { 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 { TurnRunnerDeps } from '../internal/usecase/turn-runner.ts';
|
||||||
import type { PendingAcceptance } from '../port.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;
|
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<string, unknown> | 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 就「判齐」 */
|
/** 空案例模型:必做项全无 + autoComplete → 一进 runTurn 就「判齐」 */
|
||||||
const READY_MODEL: CaseModel = { name: '空案', autoComplete: true, items: [], sentries: [] } as never;
|
const READY_MODEL: CaseModel = { name: '空案', autoComplete: true, items: [], sentries: [] } as never;
|
||||||
const READY_RUNTIME: CaseRuntime = { items: [] } as never;
|
const READY_RUNTIME: CaseRuntime = { items: [] } as never;
|
||||||
@@ -38,7 +64,7 @@ type Harness = {
|
|||||||
/** 每次推帧时「已写下几行」—— 帧推在行动之前的话,它是 0 */
|
/** 每次推帧时「已写下几行」—— 帧推在行动之前的话,它是 0 */
|
||||||
readonly writesAtPush: number[];
|
readonly writesAtPush: number[];
|
||||||
/** 这一拍写下的全部留痕 —— 断言「被拒/无事发生」走这里 */
|
/** 这一拍写下的全部留痕 —— 断言「被拒/无事发生」走这里 */
|
||||||
readonly appended: Array<{ kind: string; title: string }>;
|
readonly appended: Array<{ kind: string; title: string; detail: Record<string, unknown> | undefined }>;
|
||||||
/** 真被调到的工具名 —— 断言「拦没拦住调用」走这里 */
|
/** 真被调到的工具名 —— 断言「拦没拦住调用」走这里 */
|
||||||
readonly toolCalls: string[];
|
readonly toolCalls: string[];
|
||||||
/** 真立起来的监听 —— 断言「拦没拦住立监听」走这里 */
|
/** 真立起来的监听 —— 断言「拦没拦住立监听」走这里 */
|
||||||
@@ -174,8 +200,8 @@ function harness(
|
|||||||
declared: async () => over.declaredListeners ?? [{ eventType: 'data', keyFilter: {} }],
|
declared: async () => over.declaredListeners ?? [{ eventType: 'data', keyFilter: {} }],
|
||||||
} as never,
|
} as never,
|
||||||
memory: {
|
memory: {
|
||||||
append: async (entry: { kind: string; title: string }) => {
|
append: async (entry: { kind: string; title: string; detail?: Record<string, unknown> }) => {
|
||||||
appended.push({ kind: entry.kind, title: entry.title });
|
appended.push({ kind: entry.kind, title: entry.title, detail: entry.detail });
|
||||||
},
|
},
|
||||||
loadActivityMemory: async () => [],
|
loadActivityMemory: async () => [],
|
||||||
window: async () => over.trail ?? [],
|
window: async () => over.trail ?? [],
|
||||||
@@ -2311,6 +2337,17 @@ describe('auto 步:配方在拍首跑', () => {
|
|||||||
const out = await h.runner.runTurn(TASK);
|
const out = await h.runner.runTurn(TASK);
|
||||||
expect(h.issued).toHaveLength(0);
|
expect(h.issued).toHaveLength(0);
|
||||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).toBe('Completed');
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -2037,7 +2037,13 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
|||||||
kind: 'engine-transition',
|
kind: 'engine-transition',
|
||||||
/* 标题别以「完成」开头 —— `task-log.actorOf` 按 /^(完成|开始办)/ 认 AI,而这一步是引擎收的 */
|
/* 标题别以「完成」开头 —— `task-log.actorOf` 按 /^(完成|开始办)/ 认 AI,而这一步是引擎收的 */
|
||||||
title: `自动收步:${current.item} —— 卡已交回,这一步到此为止`,
|
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);
|
await deps.state.save(taskId, model, parsed, settled);
|
||||||
/* `Navigation.kind` 没有 'progressed' 这一档(只有 proceed/breakpoint/converged,
|
/* `Navigation.kind` 没有 'progressed' 这一档(只有 proceed/breakpoint/converged,
|
||||||
|
|||||||
@@ -450,7 +450,15 @@ describe('日志分得清谁干的', () => {
|
|||||||
it('引擎自己做主的仍署「系统」—— 起单不是谁办的,是流程走到了这儿', () => {
|
it('引擎自己做主的仍署「系统」—— 起单不是谁办的,是流程走到了这儿', () => {
|
||||||
const lines = structuredLog([
|
const lines = structuredLog([
|
||||||
entry('engine-transition', { seq: 1, title: '起单:A1 · 询价受理' }),
|
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(['系统', '系统']);
|
expect(lines.map((l) => l.actor)).toEqual(['系统', '系统']);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user