Ctrl K
ComunidadGuías…

tests/e2e/control-coordinator-followup.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { expect, test } from "@playwright/test";
import { controlContinuationDatabase } from "../support/control-continuation-database";
import { saveControlTranscript } from "../../lib/control/transcript-store";
import { controlContinuationSummary } from "../../lib/control/continuation-presentation";
import {
  claimControlContinuation,
  listControlContinuations,
  updateClaimedControlContinuation,
} from "../../lib/control/continuation-store";
import { actOnControlContinuation } from "../../lib/control/continuation-actions";
import { superviseControlContinuation } from "../../lib/control/continuation-supervisor";
import { enableScopedE2EAuth, scopedPath } from "./helpers/auth";
import {
  fulfillJson,
  mockBaseChrome,
  mockControlSessionBootstrap,
} from "./helpers/automation-control-plane-fixtures";

for (const ending of ["cancelled", "timed out"] as const) {
  test(`worker handoff updates the conversation without another prompt, and ${ending} survives reload`, async ({
    page,
  }, testInfo) => {
    const f = await controlContinuationDatabase("neon");
    const client = f.client as unknown as Parameters<
      typeof saveControlTranscript
    >[1];
    let unlisten: (() => Promise<void>) | undefined;
    try {
      const { continuation } = await f.rpc<{ continuation: { id: string } }>(
        "control_register_continuation",
        f.registerArgs
      );
      await f.checkpointParent();
      await f.rpc("control_refresh_continuation", {
        p_user_id: f.owner,
        p_continuation_id: continuation.id,
        p_parent_ai_call_id: f.parentCallId,
        p_parent_message: f.parentMessage,
      });
      await page.addInitScript(() => {
        class FixtureEventSource extends EventTarget {
          private listener: (event: Event) => void;
          constructor(url: string) {
            super();
            this.listener = (event: Event) => {
              const detail = (event as CustomEvent).detail;
              if (decodeURIComponent(url).includes(detail.table))
                this.dispatchEvent(
                  new MessageEvent("message", { data: JSON.stringify(detail) })
                );
            };
            window.addEventListener("fixture-table-event", this.listener);
            queueMicrotask(() => this.dispatchEvent(new Event("open")));
          }
          close() {
            window.removeEventListener("fixture-table-event", this.listener);
          }
        }
        Object.defineProperty(window, "EventSource", {
          value: FixtureEventSource,
        });
      });
      await enableScopedE2EAuth(page);
      await mockBaseChrome(page);
      await mockControlSessionBootstrap(page);
      await page.route("**/api/control/sessions**", async (route) => {
        const saved = await saveControlTranscript(
          { userId: f.owner, sessionId: f.sessionId, messages: [] },
          client
        );
        return fulfillJson(
          route,
          new URL(route.request().url()).searchParams.has("id")
            ? saved
            : [saved]
        );
      });
      let userPrompts = 0;
      await page.route("**/api/control/chat", (route) => {
        userPrompts++;
        return fulfillJson(route, { error: "No user prompt expected" }, 500);
      });
      await page.route("**/api/control/continuations**", async (route) => {
        if (route.request().method() === "POST") {
          const body = route.request().postDataJSON();
          const result = await actOnControlContinuation(
            f.owner,
            body.id,
            body.action,
            { client }
          );
          return fulfillJson(route, result, result.status);
        }
        return fulfillJson(route, {
          continuations: (
            await listControlContinuations(f.owner, f.sessionId, client)
          ).map(controlContinuationSummary),
        });
      });
      unlisten = await f.db.listen("mogplex_table_events", (payload) => {
        void page
          .evaluate(
            (detail) =>
              window.dispatchEvent(
                new CustomEvent("fixture-table-event", { detail })
              ),
            JSON.parse(payload)
          )
          .catch(() => undefined);
      });
      await page.goto(scopedPath(`control?mission=${f.sessionId}`));
      const status = page.getByRole("region", {
        name: "Coordinator follow-up",
      });
      await expect(status).toContainText("Waiting for workers");
      await expect(status).toContainText("resume here automatically");
      const composer = page.getByRole("textbox", {
        name: "Ask for follow-up changes",
      });
      await composer.fill("Keep my draft while results arrive");
      await f.db.query(
        "update external_agent_runs set status='success' where id=any($1)",
        [f.workerIds]
      );
      await claimControlContinuation(
        f.owner,
        continuation.id,
        "runtime",
        client
      );
      await expect(status).toContainText(
        "Coordinator is reviewing the results"
      );
      await saveControlTranscript(
        {
          userId: f.owner,
          sessionId: f.sessionId,
          messages: [
            {
              id: "automatic-reply",
              role: "assistant",
              parts: [
                {
                  type: "text",
                  text: "I reviewed the worker changes. The focused tests pass; integration remains to be checked.",
                },
              ],
            },
          ],
        },
        client
      );
      await expect(
        page.getByText(
          "I reviewed the worker changes. The focused tests pass; integration remains to be checked.",
          { exact: true }
        )
      ).toBeVisible();
      await expect(composer).toHaveValue("Keep my draft while results arrive");
      expect(userPrompts).toBe(0);
      const finalLabel =
        ending === "cancelled"
          ? "Coordinator follow-up cancelled"
          : "Coordinator follow-up stopped";
      if (ending === "cancelled") {
        await status.getByRole("button", { name: "Stop follow-up" }).click();
        await expect(status).toContainText(
          "Workers and sandbox were left unchanged"
        );
      } else {
        await superviseControlContinuation(
          { userId: f.owner, continuationId: continuation.id },
          "runtime",
          {
            client,
            waitForWorker: async () => ({
              ok: false,
              error: { code: "MAX_DURATION_EXCEEDED" },
            }),
          }
        );
        await expect(status).toContainText("reached its time limit");
        await expect(
          status.getByRole("button", { name: "Stop follow-up" })
        ).toHaveCount(0);
      }
      await expect(status).toContainText(finalLabel);
      expect(
        await updateClaimedControlContinuation(
          {
            userId: f.owner,
            id: continuation.id,
            runtimeRunId: "runtime",
            status: "finished",
          },
          client
        )
      ).toBeNull();
      await page.reload();
      await expect(status).toContainText(finalLabel);
      await expect(
        page.getByText(
          "I reviewed the worker changes. The focused tests pass; integration remains to be checked.",
          { exact: true }
        )
      ).toBeVisible();
      await page.screenshot({
        path: testInfo.outputPath("coordinator-followup-desktop.png"),
      });
      await page.setViewportSize({ width: 375, height: 812 });
      await expect(status).toBeVisible();
      await expect(status).toContainText(finalLabel);
      await page.screenshot({
        path: testInfo.outputPath("coordinator-followup-mobile.png"),
      });
    } finally {
      await unlisten?.();
      await f.db.close();
    }
  });
}