/** * 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"); });