feat(web): add harness event runtime bridge
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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("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" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import type { AppendMessage } from "@assistant-ui/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { extractMessageInput, useHarnessRuntime } from "../src/assistant/use-harness-runtime.js";
|
||||
import { createInitialHarnessState, type HarnessClientState } from "../src/store/reducer.js";
|
||||
|
||||
function appendMessage(text: string): AppendMessage {
|
||||
return {
|
||||
role: "user",
|
||||
content: [{ type: "text", text }],
|
||||
attachments: [],
|
||||
createdAt: new Date("2026-09-07T08:00:00.000Z"),
|
||||
metadata: { custom: {} },
|
||||
parentId: null,
|
||||
sourceId: null,
|
||||
runConfig: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
describe("useHarnessRuntime", () => {
|
||||
it("turns a composer message into one harness API call", async () => {
|
||||
const sendMessage = vi.fn().mockResolvedValue({ runId: "run_1" });
|
||||
const cancel = vi.fn().mockResolvedValue({ cancelled: true });
|
||||
let snapshot: HarnessClientState = createInitialHarnessState();
|
||||
const subscribers = new Set<() => void>();
|
||||
const store = {
|
||||
getSnapshot: () => snapshot,
|
||||
subscribe: (subscriber: () => void) => {
|
||||
subscribers.add(subscriber);
|
||||
return () => subscribers.delete(subscriber);
|
||||
},
|
||||
sendMessage,
|
||||
cancel,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useHarnessRuntime(store));
|
||||
await act(async () => {
|
||||
result.current.thread.append(appendMessage(" hello agent "));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith({ text: "hello agent", attachmentIds: [] });
|
||||
|
||||
snapshot = { ...snapshot, runs: { run_1: { runId: "run_1", modelId: "test", status: "running" } } };
|
||||
act(() => subscribers.forEach((subscriber) => subscriber()));
|
||||
await act(async () => {
|
||||
result.current.thread.cancelRun();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(cancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("extracts uploaded attachment ids without retaining file data", () => {
|
||||
const message = appendMessage("caption");
|
||||
const input = extractMessageInput({
|
||||
...message,
|
||||
attachments: [{
|
||||
id: "att_one",
|
||||
type: "file",
|
||||
name: "one.png",
|
||||
contentType: "image/png",
|
||||
status: { type: "complete" },
|
||||
content: [],
|
||||
}],
|
||||
});
|
||||
|
||||
expect(input).toEqual({ text: "caption", attachmentIds: ["att_one"] });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user