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",              // 哪个模型(测试用的假名)
posted @ 2026-09-18 00:26  lukefoto  阅读(9)  评论(0)    收藏  举报