Files
Genarrative/apps/ai-game-creator-shell/tests/directHistoryAnchorGate.test.ts
T
k88936 99be1d76fe 抽离 DirectProject 独立聊天容器并删除工作台壳 Direct 状态
- 新增 view/project-development/chat 模块:DirectProjectChatView 自己持有项目清单订阅、首屏锚点、历史分页、发送队列、附件、终止、草稿与本地消息
- DirectProjectChatView 入参收窄为项目路径、入口首轮需求(canonical content[])和两条权限门,Direct 状态不再穿过 App.tsx
- 线程订阅与聊天 reducer 提成 useDirectThreadChatSubscription:subscribe/consume/notify 单飞循环、订阅欠账、SUBSCRIPTION_EXPIRED 重订与 DirectThreadChatState
- chat/components 按「一个组件一个目录、逻辑与表现同目录」重组(ChatHeader、Composer、Conversation、SettingsDialog、ToolCallGroup)
- DirectProjectChatView 拆成聊天头、会话区、回合、输入盒四个组件
- App.tsx 删除 Direct 专属 state/ref/effect/handler 与 directCodex 分支,首轮需求按 canonical content 交给聊天
- 工作台级动作(运行本地预览等)经 DirectProjectChatHandle.announce 交给聊天自己的本地消息流
- Direct 首轮认领记录提成 app/initialSupervisorMessageClaims,供各入口共用
- Rust 侧 Direct 契约 ts-rs 导出目录改到 chat/generated
- 补齐 direct_codex_user_item/wire.rs 既有的 rustfmt 空格漂移
- 同步 ADR、实施计划、decision-log、pitfalls 与 Direct 技术文档
2026-09-19 15:08:31 +08:00

109 lines
3.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
directHistoryAnchorGateToWaitFor,
openDirectHistoryAnchorGate,
reuseOrOpenDirectHistoryAnchorGate,
} from '../src/view/project-development/chat/history/directHistoryAnchorGate';
const projectA = '/tmp/项目A';
const projectB = '/tmp/项目B';
/** 订阅侧的回执:一次订阅只 settle 一次。 */
function subscribeReceipt(gate: { settle: (anchor: string | null) => void }) {
gate.settle('turn-2-user');
}
describe('openDirectHistoryAnchorGate', () => {
it('默认不解析:回执到达前首屏必须等它', async () => {
const gate = openDirectHistoryAnchorGate(projectA);
const settled = { value: 'pending' as string | null | 'pending' };
void gate.anchor.then((anchor) => {
settled.value = anchor;
});
// 让微任务跑一轮:没有回执就不该有结果。
await Promise.resolve();
await Promise.resolve();
expect(settled.value).toBe('pending');
expect(gate.projectPath).toBe(projectA);
expect(gate.consumed).toBe(false);
});
it('settle 之后 anchor 解析成回执里的 lastCompletedItemId', async () => {
const gate = openDirectHistoryAnchorGate(projectA);
subscribeReceipt(gate);
await expect(gate.anchor).resolves.toBe('turn-2-user');
});
it('订阅不可用时 settle(null):首屏退化成取文件尾', async () => {
const gate = openDirectHistoryAnchorGate(projectA);
gate.settle(null);
await expect(gate.anchor).resolves.toBeNull();
});
});
describe('reuseOrOpenDirectHistoryAnchorGate', () => {
it('同一个项目复用同一道闸门:订阅侧要 settle 首屏已经开好的那道', () => {
const opened = openDirectHistoryAnchorGate(projectA);
expect(reuseOrOpenDirectHistoryAnchorGate(opened, projectA)).toBe(opened);
});
it('换项目开一道新闸门,不复用旧项目的回执', () => {
const opened = openDirectHistoryAnchorGate(projectA);
const next = reuseOrOpenDirectHistoryAnchorGate(opened, projectB);
expect(next).not.toBe(opened);
expect(next.projectPath).toBe(projectB);
expect(next.consumed).toBe(false);
});
it('还没有闸门时新开一道', () => {
expect(reuseOrOpenDirectHistoryAnchorGate(null, projectA).projectPath).toBe(
projectA,
);
});
});
describe('directHistoryAnchorGateToWaitFor', () => {
it('没有闸门(首屏比订阅 effect 先跑)时开一道给调用方登记', async () => {
const gate = directHistoryAnchorGateToWaitFor(null, projectA);
expect(gate).not.toBeNull();
expect(gate?.projectPath).toBe(projectA);
subscribeReceipt(gate!);
await expect(gate!.anchor).resolves.toBe('turn-2-user');
});
it('同项目未消费的闸门直接复用:首屏等到订阅回执', async () => {
const opened = openDirectHistoryAnchorGate(projectA);
const toWaitFor = directHistoryAnchorGateToWaitFor(opened, projectA);
expect(toWaitFor).toBe(opened);
subscribeReceipt(opened);
await expect(toWaitFor!.anchor).resolves.toBe('turn-2-user');
});
it('同项目已消费返回 null:重开项目没有新回执可等,退回取文件尾', () => {
const opened = openDirectHistoryAnchorGate(projectA);
opened.consumed = true;
expect(directHistoryAnchorGateToWaitFor(opened, projectA)).toBeNull();
});
it('换项目不复用旧闸门:新项目的首屏要等新订阅的回执', () => {
const opened = openDirectHistoryAnchorGate(projectA);
subscribeReceipt(opened);
opened.consumed = true;
const toWaitFor = directHistoryAnchorGateToWaitFor(opened, projectB);
expect(toWaitFor).not.toBeNull();
expect(toWaitFor).not.toBe(opened);
expect(toWaitFor?.projectPath).toBe(projectB);
});
});