Files
inkos/packages/studio/e2e/authoring-confirm.spec.ts
Ma a07993dc3a feat(e2e): authoring confirm flow — propose_action card + real-browser test
- server.ts: add details.toolExecutions to the propose_action response path
  so the frontend can render ProposedActionPreview from the JSON body.
- ToolExecutionSteps.tsx: add data-testid="confirm-action" to the confirm
  button so the E2E selector can click it.
- agent-session.ts: move terminalToolResultTail guard before isLlmStubEnabled
  check to prevent stub re-proposing after a terminal tool result.
- playwright.config.ts: use reuseExistingServer=false and dedicated ports
  4580/4581 (separate from the dev server's 4567/4569) so INKOS_AGENT_LLM_STUB=1
  is always active and no real LLM call is made with the fake API key.
- e2e/authoring-confirm.spec.ts: new E2E spec covering the full confirm flow.
- e2e/fixtures/seed-authoring-confirm.ts: seed fixture that writes story graph,
  fake secrets, and a fresh session before each test run.
2026-06-24 18:14:10 +08:00

72 lines
3.1 KiB
TypeScript

/**
* Real-browser multi-step E2E: user drives the agent authoring confirm flow.
*
* Flow:
* 1. Open the film tree page (#/film/<id>) — the project must already exist.
* 2. Click the "AI 对话创作" button (data-testid="open-authoring").
* 3. Type "帮我搭一个三幕结构" into the chat textarea and press Enter.
* 4. Wait for the agent to stream and render the proposed-action confirm card.
* 5. Click the confirm button (data-testid="confirm-action").
* 6. Wait for execution, then cross-check the graph via the API endpoint.
*
* The dev server runs with INKOS_AGENT_LLM_STUB=1 (set in playwright.config.ts),
* so the agent deterministically proposes draft_structure when the user message
* mentions "结构". No real LLM call is made.
*/
import { test, expect } from "@playwright/test";
import { seedAuthoringConfirm, E2E_AUTHOR_ID } from "./fixtures/seed-authoring-confirm";
test.beforeEach(async () => {
await seedAuthoringConfirm();
});
test("user drives agent to draft a structure via the confirm flow", async ({ page }) => {
// Step 1: navigate to the film tree page
await page.goto(`/#/film/${E2E_AUTHOR_ID}`);
// Wait for the tree page to mount (the film-title testid is set in StoryGraphTree
// only after the graph loads; the project may not have a graph yet, so we wait
// for the page's main content area to appear instead)
await expect(page.getByTestId("open-authoring")).toBeVisible({ timeout: 15_000 });
// Step 2: click the authoring button → navigates to #/film-author/<id>
await page.getByTestId("open-authoring").click();
// The ChatPage loads and immediately creates/loads an authoring session.
// Wait for the chat input to become enabled (session ready).
const chatInput = page.getByPlaceholder("输入指令...");
await expect(chatInput).toBeVisible({ timeout: 20_000 });
await expect(chatInput).toBeEnabled({ timeout: 20_000 });
// Step 3: type the instruction and press Enter to send
await chatInput.fill("帮我搭一个三幕结构");
await chatInput.press("Enter");
// Step 4: wait for the agent to stream and render the proposed-action card.
// The stub emits propose_action immediately, but the SSE pipeline still goes
// through the server → SSE event → store update → React render cycle.
// We give it up to 60 s to account for any startup lag on the CI machine.
await expect(page.getByTestId("confirm-action")).toBeVisible({ timeout: 60_000 });
// Step 5: click the confirm button
await page.getByTestId("confirm-action").click();
// Step 6: cross-check via the graph API endpoint.
// The stub's stubChatCompletion returns STRUCTURE_JSON (4 nodes) when the
// prompt mentions "骨架/nodes/结构". Poll until the graph has ≥4 nodes.
await expect
.poll(
async () => {
const res = await page.request.get(
`/api/v1/projects/${E2E_AUTHOR_ID}/story-graph`,
);
if (!res.ok()) return 0;
const g = (await res.json()) as { nodes: unknown[] };
return g.nodes?.length ?? 0;
},
{ timeout: 60_000, intervals: [1_000, 2_000, 3_000] },
)
.toBeGreaterThanOrEqual(4);
});