Files
agent/apps/web/tests/reducer.test.ts
T

94 lines
5.2 KiB
TypeScript

import type { HarnessEvent } from "@agent-studio/shared";
import { describe, expect, it } from "vitest";
import { createInitialHarnessState, reduceHarnessEvent } from "../src/store/reducer.js";
const base = {
sessionId: "ses_test",
timestamp: "2026-09-07T08:00:00.000Z",
};
function event<T extends HarnessEvent>(value: Omit<T, "eventId" | "sessionId" | "timestamp">): T {
return {
...base,
...value,
eventId: `evt_${value.sequence}`,
} as T;
}
describe("reduceHarnessEvent", () => {
it("shows tool-only assistant turns before the first text token without duplicating the final message", () => {
const pending = [
event<HarnessEvent>({ type: "run.started", sequence: 1, runId: "run_1", payload: { modelId: "qwen" } }),
event<HarnessEvent>({ type: "tool.requested", sequence: 2, runId: "run_1", payload: { toolCallId: "tool_1", toolName: "mock_ads_metrics", arguments: { accountId: "demo-account", days: 7 } } }),
].reduce(reduceHarnessEvent, createInitialHarnessState());
expect(pending.messages.msg_run_1).toMatchObject({ role: "assistant", runId: "run_1", status: "running", text: "" });
const final = reduceHarnessEvent(pending, event<HarnessEvent>({ type: "assistant.delta", sequence: 3, runId: "run_1", payload: { messageId: "msg_run_1", delta: "已获取指标" } }));
expect(final.messageOrder).toEqual(["msg_run_1"]);
expect(final.messages.msg_run_1?.text).toBe("已获取指标");
});
it("builds messages, run state, tools, approvals, and timeline from ordered events", () => {
const events: HarnessEvent[] = [
event({ type: "user.message", sequence: 1, payload: { messageId: "msg_user", text: "分析素材", attachmentIds: [] } }),
event({ type: "run.started", sequence: 2, runId: "run_1", payload: { modelId: "gpt-test" } }),
event({ type: "assistant.delta", sequence: 3, runId: "run_1", payload: { messageId: "msg_ai", delta: "正在" } }),
event({ type: "tool.requested", sequence: 4, runId: "run_1", payload: { toolCallId: "tool_1", toolName: "save_report_draft", arguments: { title: "草稿" } } }),
event({ type: "approval.required", sequence: 5, runId: "run_1", payload: { approvalId: "ap_1", toolCallId: "tool_1", toolName: "save_report_draft", arguments: { title: "草稿" }, riskLevel: "medium", expiresAt: "2026-09-07T08:05:00.000Z" } }),
event({ type: "approval.resolved", sequence: 6, runId: "run_1", payload: { approvalId: "ap_1", status: "approved" } }),
event({ type: "tool.completed", sequence: 7, runId: "run_1", payload: { toolCallId: "tool_1", toolName: "save_report_draft", result: { path: "draft.md" } } }),
event({ type: "assistant.delta", sequence: 8, runId: "run_1", payload: { messageId: "msg_ai", delta: "完成" } }),
event({ type: "assistant.completed", sequence: 9, runId: "run_1", payload: { messageId: "msg_ai" } }),
event({ type: "run.completed", sequence: 10, runId: "run_1", payload: { messageId: "msg_ai" } }),
];
const state = events.reduce(reduceHarnessEvent, createInitialHarnessState());
expect(state.messageOrder).toEqual(["msg_user", "msg_ai"]);
expect(state.messages.msg_ai?.text).toBe("正在完成");
expect(state.messages.msg_ai?.status).toBe("complete");
expect(state.runs.run_1?.status).toBe("complete");
expect(state.tools.tool_1).toMatchObject({ status: "completed", approvalId: "ap_1" });
expect(state.approvals.ap_1?.status).toBe("approved");
expect(state.timeline).toHaveLength(10);
expect(state.lastSequence).toBe(10);
});
it("ignores duplicates and flags a sequence gap without applying the event", () => {
const first = event<HarnessEvent>({
type: "user.message",
sequence: 1,
payload: { messageId: "msg_1", text: "one", attachmentIds: [] },
});
const duplicate = event<HarnessEvent>({
type: "user.message",
sequence: 1,
payload: { messageId: "msg_duplicate", text: "duplicate", attachmentIds: [] },
});
const gap = event<HarnessEvent>({
type: "assistant.delta",
sequence: 3,
runId: "run_1",
payload: { messageId: "msg_ai", delta: "missed two" },
});
const afterFirst = reduceHarnessEvent(createInitialHarnessState(), first);
expect(reduceHarnessEvent(afterFirst, duplicate)).toBe(afterFirst);
const afterGap = reduceHarnessEvent(afterFirst, gap);
expect(afterGap.resetRequired).toBe(true);
expect(afterGap.lastSequence).toBe(1);
expect(afterGap.messages.msg_ai).toBeUndefined();
});
it("marks an assistant message incomplete when its run fails", () => {
const state = [
event<HarnessEvent>({ type: "run.started", sequence: 1, runId: "run_2", payload: { modelId: "gpt-test" } }),
event<HarnessEvent>({ type: "assistant.delta", sequence: 2, runId: "run_2", payload: { messageId: "msg_ai", delta: "partial" } }),
event<HarnessEvent>({ type: "run.failed", sequence: 3, runId: "run_2", payload: { code: "MODEL_ERROR", message: "failed safely" } }),
].reduce(reduceHarnessEvent, createInitialHarnessState());
expect(state.runs.run_2).toMatchObject({ status: "failed", error: "failed safely" });
expect(state.messages.msg_ai).toMatchObject({ status: "failed", error: "failed safely" });
});
});