测试你的 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,返回结果 envelope
  • stream(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 各写一遍测试。