158 lines
3.9 KiB
TypeScript
158 lines
3.9 KiB
TypeScript
import { beforeEach, expect, test, rs } from "@rstest/core";
|
|
|
|
const fetchWithAuth = rs.fn();
|
|
|
|
rs.mock("@/core/api/fetcher", () => ({
|
|
fetch: fetchWithAuth,
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
fetchWithAuth.mockReset();
|
|
});
|
|
|
|
test("fetchThreadTokenUsage uses shared auth fetch without JSON GET headers", async () => {
|
|
fetchWithAuth.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
thread_id: "thread-1",
|
|
total_input_tokens: 3,
|
|
total_output_tokens: 4,
|
|
total_tokens: 7,
|
|
total_runs: 1,
|
|
by_model: { unknown: { tokens: 7, runs: 1 } },
|
|
by_caller: {
|
|
lead_agent: 0,
|
|
subagent: 0,
|
|
middleware: 0,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const { fetchThreadTokenUsage } = await import("@/core/threads/api");
|
|
|
|
await expect(fetchThreadTokenUsage("thread-1")).resolves.toMatchObject({
|
|
thread_id: "thread-1",
|
|
total_tokens: 7,
|
|
});
|
|
|
|
expect(fetchWithAuth).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/threads/thread-1/token-usage"),
|
|
{
|
|
method: "GET",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("fetchThreadTokenUsage returns null for unavailable token usage", async () => {
|
|
fetchWithAuth.mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
});
|
|
|
|
const { fetchThreadTokenUsage } = await import("@/core/threads/api");
|
|
|
|
await expect(fetchThreadTokenUsage("thread-1")).resolves.toBeNull();
|
|
});
|
|
|
|
test("branchThreadFromTurn posts the selected turn ids to the gateway", async () => {
|
|
fetchWithAuth.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
thread_id: "branch-thread",
|
|
parent_thread_id: "thread/1",
|
|
parent_checkpoint_id: "checkpoint-2",
|
|
branched_from_message_id: "ai-2",
|
|
workspace_clone_mode: "current_thread_best_effort",
|
|
}),
|
|
});
|
|
|
|
const { branchThreadFromTurn } = await import("@/core/threads/api");
|
|
|
|
await expect(
|
|
branchThreadFromTurn("thread/1", {
|
|
messageId: "ai-2",
|
|
messageIds: ["ai-1", "ai-2"],
|
|
title: "Branch: original",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
thread_id: "branch-thread",
|
|
parent_checkpoint_id: "checkpoint-2",
|
|
});
|
|
|
|
expect(fetchWithAuth).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/threads/thread%2F1/branches"),
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
message_id: "ai-2",
|
|
message_ids: ["ai-1", "ai-2"],
|
|
title: "Branch: original",
|
|
}),
|
|
},
|
|
);
|
|
});
|
|
|
|
test("branchThreadFromTurn surfaces gateway detail on failure", async () => {
|
|
fetchWithAuth.mockResolvedValue({
|
|
ok: false,
|
|
json: async () => ({
|
|
detail: "This turn can no longer be branched from.",
|
|
}),
|
|
});
|
|
|
|
const { branchThreadFromTurn } = await import("@/core/threads/api");
|
|
|
|
await expect(
|
|
branchThreadFromTurn("thread-1", {
|
|
messageId: "ai-2",
|
|
messageIds: ["ai-2"],
|
|
}),
|
|
).rejects.toThrow("This turn can no longer be branched from.");
|
|
});
|
|
|
|
test("compactThreadContext posts agent attribution and abort signal", async () => {
|
|
const controller = new AbortController();
|
|
fetchWithAuth.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
thread_id: "thread-1",
|
|
compacted: true,
|
|
removed_message_count: 4,
|
|
preserved_message_count: 2,
|
|
summary_updated: true,
|
|
checkpoint_id: "checkpoint-3",
|
|
total_tokens: 123,
|
|
}),
|
|
});
|
|
|
|
const { compactThreadContext } = await import("@/core/threads/api");
|
|
|
|
await expect(
|
|
compactThreadContext("thread-1", {
|
|
agentName: "research-agent",
|
|
signal: controller.signal,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
compacted: true,
|
|
checkpoint_id: "checkpoint-3",
|
|
});
|
|
|
|
expect(fetchWithAuth).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/threads/thread-1/compact"),
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
force: true,
|
|
agent_name: "research-agent",
|
|
}),
|
|
signal: controller.signal,
|
|
},
|
|
);
|
|
});
|