Files
2026-07-13 11:59:58 +08:00

99 lines
2.9 KiB
TypeScript

import { beforeEach, describe, expect, rs, test } from "@rstest/core";
rs.mock("@/core/api/fetcher", () => ({
fetch: rs.fn(),
}));
rs.mock("@/core/config", () => ({
getBackendBaseURL: () => "/backend",
}));
import { fetch as fetcher } from "@/core/api/fetcher";
import { fetchSubtaskSteps } from "@/core/tasks/api";
const mockedFetch = rs.mocked(fetcher);
function jsonResponse(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
statusText: status >= 400 ? "Error" : "OK",
headers: { "Content-Type": "application/json" },
});
}
function stepEvent(seq: number, messageIndex: number, toolName: string) {
return {
event_type: "subagent.step",
seq,
content: {
task_id: "A",
message_index: messageIndex,
kind: "tool",
text: "",
tool_name: toolName,
},
metadata: { task_id: "A", message_index: messageIndex },
};
}
beforeEach(() => {
mockedFetch.mockReset();
});
describe("fetchSubtaskSteps", () => {
test("scopes the request to the task and only fetches subagent.step", async () => {
mockedFetch.mockResolvedValueOnce(jsonResponse(200, []));
await fetchSubtaskSteps("thread 1", "run/1", "task-A");
const url = mockedFetch.mock.calls[0]![0] as string;
expect(url).toContain(
"/backend/api/threads/thread%201/runs/run%2F1/events",
);
expect(url).toContain("task_id=task-A");
expect(url).toContain("event_types=subagent.step");
expect(url).toContain("limit=");
expect(url).not.toContain("after_seq");
});
test("pages forward with after_seq until a short page, accumulating in order", async () => {
mockedFetch
.mockResolvedValueOnce(
jsonResponse(200, [
stepEvent(10, 0, "web_search"),
stepEvent(11, 1, "read_file"),
]),
)
.mockResolvedValueOnce(jsonResponse(200, [stepEvent(12, 2, "bash")]));
const steps = await fetchSubtaskSteps("t", "r", "A", 2);
expect(steps.map((s) => s.message_index)).toEqual([0, 1, 2]);
expect(steps.map((s) => s.tool_name)).toEqual([
"web_search",
"read_file",
"bash",
]);
expect(mockedFetch).toHaveBeenCalledTimes(2);
expect(mockedFetch.mock.calls[0]![0] as string).not.toContain("after_seq");
expect(mockedFetch.mock.calls[1]![0] as string).toContain("after_seq=11");
});
test("stops after a single page when it is shorter than the page size", async () => {
mockedFetch.mockResolvedValueOnce(
jsonResponse(200, [stepEvent(10, 0, "web_search")]),
);
const steps = await fetchSubtaskSteps("t", "r", "A", 500);
expect(steps).toHaveLength(1);
expect(mockedFetch).toHaveBeenCalledTimes(1);
});
test("throws when a page request fails", async () => {
mockedFetch.mockResolvedValueOnce(jsonResponse(500, { detail: "boom" }));
await expect(fetchSubtaskSteps("t", "r", "A")).rejects.toThrow();
});
});