pi-agent - Test Suite Usage - agent.test.ts
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
super(
(event) => event.type === "done" || event.type === "error",
(event) => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
},
);
}
}
这里有两个 TypeScript 概念:泛型继承 和 箭头函数作为参数。
逐层拆解
1. extends EventStream<AssistantMessageEvent, AssistantMessage>
这是泛型类继承。EventStream 是父类,接收两个类型参数:
| 类型参数 | 含义 | 对应到这里的值 |
|---|---|---|
| 第一个 | 流中每个事件的类型 | AssistantMessageEvent(如 { type: "done", ... } 或 { type: "error", ... }) |
| 第二个 | 流最终产出的类型 | AssistantMessage(最终的助手消息对象) |
翻译成白话:MockAssistantStream 是一种特殊的 EventStream,它处理的是 AssistantMessageEvent 类型的事件,最终产出 AssistantMessage。
2. super(...) 传入的两个箭头函数
super(
// 参数1: 终止判断函数(predicate)
(event) => event.type === "done" || event.type === "error",
// 参数2: 结果提取函数(mapper)
(event) => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
},
);
- 第一个函数告诉父类:当 event 的 type 是
"done"或"error"时,这个流就结束了(相当于告诉EventStream什么事件是终止事件) - 第二个函数告诉父类:当流结束时,如何从终止事件中提取最终值
- 如果是
done事件 → 返回event.message - 如果是
error事件 → 返回event.error - 其他情况 → 抛错(防御性编程)
- 如果是
整体模式
这是一个典型的策略模式(Strategy Pattern)写法:EventStream 是通用的流处理基类,通过在 super() 中注入两个函数来配置流的行为,而不是在子类中重写方法。子类 MockAssistantStream 本身没有任何方法,所有逻辑都在这两个回调里。
一个简单的类比
// 等价的伪代码逻辑
class EventStream<TEvent, TResult> {
constructor(
isTerminal: (event: TEvent) => boolean, // 什么时候结束?
extractResult: (event: TEvent) => TResult, // 结束时怎么拿结果?
) { ... }
}
这样 EventStream 本身不需要知道具体是哪种事件,通过泛型 + 回调实现了完全的通用性。
从类名和用法来看,它是一个 Mock(模拟)对象,专门用于测试。不会从大模型获取实际的回复。
为什么说它是假的
1. 名字就暴露了:MockAssistantStream —— Mock 前缀在测试术语里就是"模拟"的意思。
2. 测试里是这样用的(L88-L95):
const stream = new MockAssistantStream();
queueMicrotask(() => {
const message = createAssistantMessage("fallback"); // 硬编码文本
stream.push({ type: "done", reason: "stop", message });
});
return stream;
注意:测试代码自己 new 了一个流,然后手动 push 了一条硬编码的 "fallback" 消息进去。整个过程没有任何网络请求、API 调用。
3. 真正的流来自 streamFn——这是构造 Agent 时传入的依赖(L107):
const agent = new Agent({ streamFn: unusedStreamFunction });
在生产环境里,streamFn 会是一个真正调用大模型 API 并返回流的函数。但在测试中,每个测试都提供自己的假 streamFn,返回的正是这个 MockAssistantStream。
为什么要这么做
这是依赖注入 + Mock的测试模式:
Agent类不直接依赖某个具体的 LLM 提供商,而是依赖一个streamFn函数- 测试时用
MockAssistantStream替换掉真实的 LLM 调用 - 这样测试快(无网络)、稳定(不依赖外部服务)、可控(可以模拟各种场景,比如错误、中止等)
所以这个类看起来"像真的",正是因为它精确地模拟了真实 EventStream 的行为——接口一样,但内部是假数据。
agent.test.ts 27-27
function createAssistantMessage(text: string): AssistantMessage {
return {
role: "assistant",
content: [{ type: "text", text }],
api: "openai-responses",
provider: "openai",
model: "mock",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
}
它的参数 text: string 不是 user(用户)消息。
这个函数创建的是 assistant(助手)消息,不是 user(用户)消息。
看 L27-L45:
function createAssistantMessage(text: string): AssistantMessage {
return {
role: "assistant", // ← 角色是 assistant
content: [{ type: "text", text }],
...
};
}
这个 text 参数是助手回复的内容文本,用来构造一条假的助手消息。role: "assistant" 决定了这是谁说的话。
作为对比,这个测试文件里也有 user 消息的写法(L465):
{ role: "user" as const, content: "Hello", timestamp: Date.now() }
简单区分
| 角色 | 谁说的 | 这个测试文件里怎么构造 |
|---|---|---|
user |
用户输入 | 手动写对象字面量 { role: "user", content: "..." } |
assistant |
大模型回复 | 用 createAssistantMessage("...") |
text 只是表示"这段消息里要填的文本内容",跟谁说的无关。
function createAssistantMessage(text: string): AssistantMessage {
return {
... ...
api: "openai-responses",
... ...
};
}
api: "openai-responses" 的含义
准确地说是这条消息是用哪种 API 协议格式产生的,不是"来源 API",而是“来源 API 的类型”。
AssistantMessage 这个类型有一个 api 字段,用来标识生成这条消息时使用的API 协议/格式。OpenAI 有两套主要的 API:
| API 名称 | 端点 | 说明 |
|---|---|---|
| Chat Completions | /v1/chat/completions |
老的、更常见的格式 |
| Responses API | /v1/responses |
OpenAI 较新的 API 格式 |
"openai-responses" 指的是 OpenAI 的 Responses API 格式。
为什么需要这个字段
同一个 AssistantMessage 类型可能来自不同的 LLM 提供商(OpenAI、Google、Anthropic 等),每家的响应格式不同。这个 api 字段让后续代码知道该用哪种反序列化/解析逻辑来处理这条消息的内容。
在这个测试文件里,它只是一个占位值——因为是 Mock 消息,不需要真的去解析,但类型要求必须填一个合法的值。同文件里还有 provider: "openai" 和 model: "mock",三者组合起来描述了这条假消息的"出身":
api: "openai-responses", // 用了哪种协议格式
provider: "openai", // 哪家供应商
model: "mock", // 哪个模型(测试用的假名)

浙公网安备 33010602011771号