deer-flow/frontend/tests/e2e/run-input-retry.spec.ts

168 lines
5.4 KiB
TypeScript

import { expect, test, type Route } from "@playwright/test";
import { mockLangGraphAPI, MOCK_THREAD_ID } from "./utils/mock-api";
const RUN_A = "00000000-0000-0000-0000-000000000091";
const RUN_B = "00000000-0000-0000-0000-000000000092";
const INPUT_TEXT = "Check the updated requirement";
type SubmittedInput = {
request_id: string;
text: string;
mode: "queue" | "steer";
};
for (const mode of ["queue", "steer"] as const) {
test(`${mode} retries a rejected draft against the new active run`, async ({
page,
}) => {
let activeRunId = RUN_A;
let acceptedInput: Record<string, unknown> | undefined;
const submissions: Array<SubmittedInput & { runId: string }> = [];
const streams = new Map<string, Route>();
const run = (runId: string) => ({
run_id: runId,
thread_id: MOCK_THREAD_ID,
assistant_id: "lead_agent",
status: runId === activeRunId ? "running" : "success",
metadata: {},
kwargs: {},
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
mockLangGraphAPI(page, {
threads: [
{
thread_id: MOCK_THREAD_ID,
title: "Input retry regression",
messages: [
{ type: "human", id: "human-original", content: "Start working" },
],
},
],
});
await page.route("**/api/features", (route) =>
route.fulfill({ json: { run_inputs: true } }),
);
await page.route(`**/api/threads/${MOCK_THREAD_ID}/inputs`, (route) =>
route.fulfill({
json: {
items: acceptedInput ? [acceptedInput] : [],
active_run_id: activeRunId,
},
}),
);
await page.route(
new RegExp(`/api/langgraph/threads/${MOCK_THREAD_ID}/runs(?:\\?|$)`),
(route) => route.fulfill({ json: [run(activeRunId)] }),
);
await page.route(
new RegExp(
`/api/langgraph/threads/${MOCK_THREAD_ID}/runs/(${RUN_A}|${RUN_B})(?:\\?|$)`,
),
(route) => {
const runId = new URL(route.request().url()).pathname
.split("/")
.at(-1)!;
return route.fulfill({ json: run(runId) });
},
);
// Hold each observer stream until the test explicitly finishes that run.
// Receiving B's join request is the synchronization point proving the
// mounted page has adopted the new active run without resetting its draft.
await page.route(
`**/api/langgraph/threads/${MOCK_THREAD_ID}/runs/*/stream*`,
(route) => {
const runId = new URL(route.request().url()).pathname
.split("/")
.at(-2)!;
streams.set(runId, route);
},
);
await page.route(
`**/api/threads/${MOCK_THREAD_ID}/runs/*/inputs`,
(route) => {
const runId = new URL(route.request().url()).pathname
.split("/")
.at(-2)!;
const body = route.request().postDataJSON() as SubmittedInput;
submissions.push({ ...body, runId });
if (runId === RUN_A) {
return route.fulfill({
status: 409,
json: {
detail:
"Run is finishing or stopped; this input was not accepted",
},
});
}
acceptedInput = {
id: body.request_id,
target_run_id: runId,
message_id: "human-update",
text: body.text,
mode: body.mode,
status: "pending",
created_at: "2026-01-01T00:00:01Z",
};
return route.fulfill({ json: acceptedInput });
},
);
async function finishStream(runId: string) {
const route = streams.get(runId);
if (!route) return;
streams.delete(runId);
await route.fulfill({
contentType: "text/event-stream",
body:
`event: metadata\ndata: ${JSON.stringify({ run_id: runId, thread_id: MOCK_THREAD_ID })}\n\n` +
"event: end\ndata: {}\n\n",
});
}
try {
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await expect.poll(() => streams.has(RUN_A)).toBe(true);
const textarea = page.getByPlaceholder(/how can i assist you/i);
const send = page.getByRole("button", { name: "Send update" });
await textarea.fill(INPUT_TEXT);
if (mode === "steer") {
await page.getByRole("button", { name: "Send while working" }).click();
await page.getByRole("menuitem", { name: /^Steer/ }).click();
}
await send.click();
await expect(
page.getByText(
"The message was not confirmed. Check the task status and try again; your draft is kept.",
),
).toBeVisible();
await expect(textarea).toHaveValue(INPUT_TEXT);
expect(submissions).toHaveLength(1);
expect(submissions[0]).toMatchObject({
runId: RUN_A,
text: INPUT_TEXT,
mode,
});
activeRunId = RUN_B;
await finishStream(RUN_A);
await expect.poll(() => streams.has(RUN_B)).toBe(true);
await send.click();
await expect.poll(() => submissions.length).toBe(2);
expect(submissions[1]).toMatchObject({
runId: RUN_B,
text: INPUT_TEXT,
mode,
});
expect(submissions[1]!.request_id).not.toBe(submissions[0]!.request_id);
await expect(textarea).toHaveValue("");
await expect(
page.getByTestId("run-inputs").getByText(INPUT_TEXT),
).toBeVisible();
} finally {
await Promise.all([...streams.keys()].map(finishStream));
}
});
}