fix(api): align frontend with REST contracts

This commit is contained in:
2026-07-30 20:38:52 +08:00
parent b57e58ff87
commit 782363cfb6
65 changed files with 99362 additions and 318 deletions
+41 -30
View File
@@ -41,20 +41,34 @@ const makeStream = (chunks: string[]) =>
},
});
const mockNewSessionStream = (response: unknown) => {
apiFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({ session_id: "s1" }),
})
.mockResolvedValueOnce(response);
};
describe("streamAgentChat", () => {
beforeEach(() => {
apiFetch.mockReset();
});
it("parses token and done events from chunked SSE", async () => {
apiFetch.mockResolvedValue({
ok: true,
body: makeStream([
'event: token\ndata: {"session_id":"s1","content":"he"}\n\n',
'event: token\ndata: {"session_id":"s1","content":"llo"}\n\n',
'event: done\ndata: {"session_id":"s1"}\n\n',
]),
});
apiFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({ session_id: "s1" }),
})
.mockResolvedValueOnce({
ok: true,
body: makeStream([
'event: token\ndata: {"session_id":"s1","content":"he"}\n\n',
'event: token\ndata: {"session_id":"s1","content":"llo"}\n\n',
'event: done\ndata: {"session_id":"s1"}\n\n',
]),
});
const events: Array<{ type: string; content?: string; sessionId?: string }> = [];
@@ -65,14 +79,13 @@ describe("streamAgentChat", () => {
});
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/stream"),
expect.stringContaining("/api/v1/agent/sessions/s1/runs"),
expect.objectContaining({
method: "POST",
projectHeaderMode: "include",
skipAuthRedirect: true,
body: JSON.stringify({
message: "hi",
session_id: undefined,
model: "provider/model",
approval_mode: undefined,
}),
@@ -111,7 +124,7 @@ describe("streamAgentChat", () => {
});
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/session/s1/stream"),
expect.stringContaining("/api/v1/agent/sessions/s1/runs/current/events"),
expect.objectContaining({
method: "GET",
projectHeaderMode: "include",
@@ -132,7 +145,7 @@ describe("streamAgentChat", () => {
});
it("parses progress events", async () => {
apiFetch.mockResolvedValue({
mockNewSessionStream({
ok: true,
body: makeStream([
'event: progress\ndata: {"session_id":"s1","id":"p1","phase":"tool","status":"running","title":"正在调用后端数据查询","detail":"GET /api/v1/demo"}\n\n',
@@ -159,7 +172,7 @@ describe("streamAgentChat", () => {
});
it("parses tool_call arguments when params is empty", async () => {
apiFetch.mockResolvedValue({
mockNewSessionStream({
ok: true,
body: makeStream([
'event: tool_call\ndata: {"session_id":"agent-1e75dd01-29e","tool":"locate_features","params":{},"arguments":"{\\"ids\\":[\\"142902\\"],\\"feature_type\\":\\"junction\\"}"}\n\n',
@@ -183,7 +196,7 @@ describe("streamAgentChat", () => {
});
it("parses permission request and response events", async () => {
apiFetch.mockResolvedValue({
mockNewSessionStream({
ok: true,
body: makeStream([
'event: permission_request\ndata: {"session_id":"s1","request_id":"perm-1","permission":"bash","patterns":["rm *"],"target":"rm tmp.txt","always":["rm *"],"created_at":123}\n\n',
@@ -220,7 +233,7 @@ describe("streamAgentChat", () => {
});
it("parses question request, response, and todo update events", async () => {
apiFetch.mockResolvedValue({
mockNewSessionStream({
ok: true,
body: makeStream([
'event: question_request\ndata: {"session_id":"s1","request_id":"q-1","questions":[{"header":"范围","question":"选择范围","options":[{"label":"城区","description":"中心城区"}],"multiple":false,"custom":true}],"tool":{"message_id":"m1","call_id":"c1"},"created_at":123}\n\n',
@@ -283,7 +296,7 @@ describe("streamAgentChat", () => {
});
it("emits error when response is not ok", async () => {
apiFetch.mockResolvedValue({
mockNewSessionStream({
ok: false,
body: null,
text: async () => "bad request",
@@ -301,7 +314,7 @@ describe("streamAgentChat", () => {
});
it("emits re-login message on unauthorized response", async () => {
apiFetch.mockResolvedValue({
mockNewSessionStream({
ok: false,
status: 401,
body: null,
@@ -343,14 +356,11 @@ describe("streamAgentChat", () => {
await abortAgentChat("s1");
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/abort"),
expect.stringContaining("/api/v1/agent/sessions/s1/runs/current"),
expect.objectContaining({
method: "POST",
method: "DELETE",
projectHeaderMode: "include",
skipAuthRedirect: true,
body: JSON.stringify({
session_id: "s1",
}),
}),
);
});
@@ -365,13 +375,13 @@ describe("streamAgentChat", () => {
await replyAgentPermission("s1", "perm-1", "once");
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/permission/perm-1/reply"),
expect.stringContaining("/api/v1/agent/sessions/s1/permission-responses"),
expect.objectContaining({
method: "POST",
projectHeaderMode: "include",
skipAuthRedirect: true,
body: JSON.stringify({
session_id: "s1",
request_id: "perm-1",
reply: "once",
}),
}),
@@ -389,25 +399,27 @@ describe("streamAgentChat", () => {
await rejectAgentQuestion("s1", "q-2");
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/question/q-1/reply"),
expect.stringContaining("/api/v1/agent/sessions/s1/question-responses"),
expect.objectContaining({
method: "POST",
projectHeaderMode: "include",
skipAuthRedirect: true,
body: JSON.stringify({
session_id: "s1",
request_id: "q-1",
action: "reply",
answers: [["城区"]],
}),
}),
);
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/question/q-2/reject"),
expect.stringContaining("/api/v1/agent/sessions/s1/question-responses"),
expect.objectContaining({
method: "POST",
projectHeaderMode: "include",
skipAuthRedirect: true,
body: JSON.stringify({
session_id: "s1",
request_id: "q-2",
action: "reject",
}),
}),
);
@@ -425,11 +437,10 @@ describe("streamAgentChat", () => {
expect(sessionId).toBe("forked-s1");
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/agent/chat/fork"),
expect.stringContaining("/api/v1/agent/sessions/s1/forks"),
expect.objectContaining({
method: "POST",
body: JSON.stringify({
session_id: "s1",
keep_message_count: 3,
}),
}),