测试你的 Action
Ageniti 在 @ageniti/core/test-utils 提供了一套零依赖的测试工具包。它兼容任何带普通断言的运行器 —— node:test、Vitest、Jest —— 因为这些 helper 抛的是普通 Error,不绑定任何特定框架。
核心思路:你只需对着共享 runtime 测一次 action,这份行为就在所有 surface(CLI、HTTP、MCP、tool call、React)上成立 —— 因为每个 surface 都跑同一个 runtime。
快速开始
import test from "node:test";
import { createTestRuntime, expectOk, expectError } from "@ageniti/core/test-utils";
import { createTask } from "./actions/create-task.js";
test("能创建任务", async () => {
const t = createTestRuntime([createTask], {
services: { tasks: fakeTaskService() },
});
const data = expectOk(await t.invoke("create_task", { title: "Ship it" }));
assert.equal(data.title, "Ship it");
});
test("拒绝空标题", async () => {
const t = createTestRuntime([createTask]);
expectError(await t.invoke("create_task", { title: "" }), "VALIDATION_ERROR");
});createTestRuntime(actions, options?)
起一个为测试预配置好的 runtime:
- 所有
actions自动注册 - 默认 surface 为
json—— 无确认门、无 UI 假设 - 默认绕过确认(所以 destructive action 在测试里不用传
confirm也能跑)
可选项:
| 选项 | 作用 |
| --- | --- |
| services | 注入依赖桩,在 run 里通过 ctx.services 取用。 |
| allow | 模拟权限结果。{ allow: false } 全部拒绝;传函数或字符串可控制 permissionChecker。 |
| middleware | 提供中间件,验证横切逻辑。 |
| hooks | 提供生命周期 hook。 |
| redact | 自定义脱敏字段。 |
| idempotencyCache | 提供缓存,测试幂等重放。 |
返回对象包含:
runtime—— 底层 runtime,需要直接操作时用invoke(name, input?, options?)—— 调用 action,返回结果 envelopestream(name, input?, options?)—— 调用并拿到实时事件流
断言 helper
import { expectOk, expectError, expectLog, collectStream } from "@ageniti/core/test-utils";expectOk(envelope)—— 断言成功并返回envelope.data。expectError(envelope, code?)—— 断言失败;若给了code,同时断言错误码(如"VALIDATION_ERROR"、"PERMISSION_DENIED")。expectLog(envelope, matcher)—— 断言存在某条日志。matcher可以是子串、RegExp或判定函数。collectStream(stream)—— 把异步事件流收集成数组,方便断言log/progress/artifact/result的完整序列。
测试流式行为
import { createTestRuntime, collectStream } from "@ageniti/core/test-utils";
test("先 progress 后 result", async () => {
const t = createTestRuntime([longRunningAction]);
const events = await collectStream(t.stream("reindex", { full: true }));
const types = events.map((e) => e.type);
assert.ok(types.includes("progress"));
assert.equal(types.at(-1), "result");
assert.equal(events.at(-1).envelope.ok, true);
});测试权限
test("缺少权限时拒绝", async () => {
const t = createTestRuntime([deleteTask], { allow: false });
expectError(await t.invoke("delete_task", { taskId: "t_1" }), "PERMISSION_DENIED");
});桩掉依赖
用 stubAction(name, options) 造一个可控的假 action —— 测中间件或组合逻辑、又不想接真实现时很方便:
import { stubAction, createTestRuntime, expectOk } from "@ageniti/core/test-utils";
const stub = stubAction("charge_card", { returns: { receiptId: "r_1" } });
const t = createTestRuntime([stub]);
expectOk(await t.invoke("charge_card", {}));为什么这样就够了
因为每个 surface(CLI、HTTP、MCP、OpenAI / AI SDK tool call、React)都只是同一个 runtime 之上的薄适配层,所以一个 action 测试通过,就意味着这个能力在它暴露的每一处都正确。你不需要为每个 surface 各写一遍测试。