Files
penguin-harness/packages/web/e2e/subagent.spec.mjs
T
Yaowei Zheng 0eaaa54e25 Make English the repository working language (#5)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:45:02 +08:00

100 lines
4.3 KiB
JavaScript

/**
* The subagent (run_subagent) card survives a **reload**: the parent Trace only stores a single
* session_meta pointer for the child session; the server's readMessages expands the child Trace
* from that pointer and fills in the origin chain, and the frontend reattaches the child session
* to the tool card from that. Also verifies the child session's title is generated by the model
* from **the child session's own conversation** (the mock only replies with "Subagent TODO
* summary" when the title request sees the child's own answer), and that discovery-based
* backfill must not collapse it to NULL.
*
* Standalone spec: shares one server with chat.spec.mjs, so it registers its own user here
* (not the first user; registration auto-provisions a `project-<8hex>`), independent of
* chat.spec's execution order.
*/
import { test, expect } from "@playwright/test";
import { provisionAndLogin } from "./auth.mjs";
const BASE = process.env.BASE_URL;
const MOCK = process.env.MOCK_URL;
const U = "subuser";
const P = "password123";
test("subagent card survives a reload; child session title is generated from its own conversation", async ({
page,
}) => {
await provisionAndLogin(page.request, U, P);
// Account creation auto-provisions a default Project; must use it (the frontend selects it
// too) — don't create another one.
const projects = await (await page.request.get(`${BASE}/api/projects`)).json();
expect(projects.projects, "auto-provisioned project").toHaveLength(1);
const projectId = projects.projects[0].projectId;
const put = await page.request.put(`${BASE}/api/projects/${projectId}/models`, {
data: {
defaultModel: { provider: "custom", modelId: "claude-4-8" },
models: [
{
provider: "custom",
modelId: "claude-4-8",
apiKey: "sk-mock",
baseUrl: MOCK,
contextWindow: 200000,
},
],
},
});
expect(put.ok(), "put models").toBeTruthy();
const agentId = "default_agent";
// Approval defaults to allow-all: child sessions inherit the parent's approval mode, no
// manual approval needed.
const sessRes = await page.request.post(
`${BASE}/api/projects/${projectId}/agents/${agentId}/sessions`,
{ data: { provider: "custom", modelId: "claude-4-8" } },
);
expect(sessRes.ok(), `create session: ${await sessRes.text()}`).toBeTruthy();
const sess = await sessRes.json();
const sessionId = sess.session.sessionId;
await page.goto(`${BASE}/chat/${sessionId}`);
const ta = page.getByPlaceholder(/输入消息/);
await ta.waitFor();
await ta.fill("run a subagent");
await page.getByRole("button", { name: "发送" }).click();
// Parent's final answer (turn 2): the whole turn has ended. Nothing changes with streaming
// after this, so assertions are deterministic (turn end collapses the "Reasoning & tools"
// group, and child session end collapses the child session card).
await expect(page.getByText("Command finished; the result looks as expected.")).toBeVisible();
/** Expand the "Reasoning & tools" group → expand the child session card → child session body visible. */
const openSubagent = async () => {
await page
.getByRole("button", { name: /运行完毕/ })
.first()
.click();
await expect(page.getByText("run_subagent").first()).toBeVisible();
const card = page.getByRole("button", { name: /子会话/ }).first();
await expect(card).toBeVisible();
await card.click();
await expect(page.getByText("Subagent report: 3 TODOs").first()).toBeVisible();
};
// Under live streaming, the child session card is nested inside the run_subagent tool card.
await openSubagent();
// --- Must still hold after reload (the fix's goal: parent Trace's child session pointer + server-side expansion) ---
await page.reload();
await openSubagent();
// --- Child session title: generated by the model from the child session's own conversation (async, poll until persisted) ---
const childTitle = async () => {
const list = await (
await page.request.get(`${BASE}/api/projects/${projectId}/agents/${agentId}/sessions`)
).json();
return list.sessions.find((s) => s.sessionId !== sessionId)?.title ?? null;
};
await expect.poll(childTitle, { timeout: 10000 }).toBe("Subagent TODO summary");
});