0eaaa54e25
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
100 lines
4.3 KiB
JavaScript
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");
|
|
});
|