Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/input-queue.spec.ts

typescript205 行查看原文
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { buildSelectionPrompt } from "@zpi-dev/ui/selections";
import { chunk, deferred, done, fakeServer, send } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { clipboardText, select } from "../helpers/composer.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expect, test } from "../helpers/test.ts";
import { seedHistory } from "../history-fixture.ts";

test("editing a queued quoted message preserves its reference cards and resubmitted context", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-queue-quotes-"));
  const server = await fakeServer((_body, response) => {
    send(response, chunk({ content: "waiting" }));
  });
  let app: ElectronApplication | undefined;
  try {
    const cwd = join(dir, "workspace");
    await mkdir(cwd);
    const id = seedHistory(dir, cwd, 1).id;
    app = await launchDesktop({ dir, url: server.url });
    const page = await app.firstWindow();
    await select(page, id);
    const editor = page.getByLabel("消息", { exact: true });
    await editor.fill("hold");
    await editor.press("Enter");
    await expect(page.getByLabel("停止", { exact: true })).toBeVisible();
    const selections = [{ text: "const result = 1;", path: join(cwd, "example.ts") }];
    for (const text of ["解释一下", ""]) {
      const prompt = buildSelectionPrompt(text, selections);
      const queued = await page.evaluate(
        ({ id, prompt }) => window.zpi.submitInput({ sessionId: id, text: prompt }),
        { id, prompt },
      );
      expect(queued.ok).toBe(true);
      const row = page.getByTestId("queue-item");
      await expect(row).toHaveCount(1);
      await expect(row.locator("[data-conversation-selection-reference-count]")).toHaveAttribute(
        "data-conversation-selection-reference-count",
        "1",
      );
      await expect(row).not.toContainText("# userselect:");
      await row.getByLabel("编辑", { exact: true }).click();
      await expect(row).toHaveCount(0);
      await expect(editor).toHaveText(text);
      await expect(page.locator(".composer-container .selection-reference-chip")).toHaveAttribute(
        "data-conversation-selection-reference-count",
        "1",
      );
      await editor.press("Enter");
      await expect(row).toHaveCount(1);
      const snapshot = await page.evaluate((id) => window.zpi.getSessionSnapshot(id), id);
      if (!snapshot.ok) throw new Error(snapshot.error.message);
      expect(snapshot.value.view.queue?.items[0]?.text).toBe(prompt);
      await row.getByLabel("移除待发送消息", { exact: true }).click();
      await expect(row).toHaveCount(0);
      await expect(page.locator(".composer-container .selection-reference-chip")).toHaveCount(0);
    }
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

test("queued messages reorder, edit, delete, send immediately and persist through stop/restart", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-queue-ui-"));
  const releaseFirst = deferred();
  const server = await fakeServer(async (body, res) => {
    const user =
      (body.messages as unknown as { role: string; content: string }[])
        .filter((m) => m.role === "user")
        .at(-1)?.content ?? "";
    send(res, chunk({ content: "reply" }));
    if (user === "阻塞一") {
      await releaseFirst.promise;
      if (!res.destroyed) done(res);
    } else if (!user.startsWith("阻塞")) done(res);
  });
  let app: ElectronApplication | undefined;
  try {
    const cwd = join(dir, "workspace");
    await mkdir(cwd);
    const a = seedHistory(dir, cwd, 1).id,
      b = seedHistory(dir, cwd, 1).id;
    const file = join(cwd, "note.txt");
    await writeFile(file, "queue reference");
    app = await launchDesktop({ dir, url: server.url });
    let page = await app.firstWindow();
    await select(page, a);
    await page.evaluate(() => window.zpi.updatePreferences({ showSendButton: true }));
    let editor = page.getByLabel("消息", { exact: true });
    await expect(page.getByLabel("发送", { exact: true })).toBeDisabled();
    await editor.fill("阻塞一");
    await expect(page.getByLabel("发送", { exact: true })).toBeEnabled();
    await page.getByLabel("发送", { exact: true }).click();
    await expect(page.getByLabel("停止", { exact: true })).toBeVisible();
    const canonical = `待发送一 [note.txt](${file})`;
    await editor.fill(canonical);
    await expect(page.getByLabel("加入队列", { exact: true })).toBeEnabled();
    await expect(page.getByLabel("停止", { exact: true })).toHaveCount(0);
    await page.getByLabel("加入队列", { exact: true }).click();
    await editor.fill("待发送二");
    await editor.press("Enter");
    await expect(editor).toHaveText("");
    await editor.fill("待发送三");
    await editor.press("Enter");
    const rows = page.getByTestId("queue-item");
    await expect(rows).toHaveCount(3);
    await expect(rows.locator(".queue-text")).toHaveText(["待发送一 note.txt", "待发送二", "待发送三"]);
    await expect(rows.getByLabel("拖拽排序")).toHaveCount(3);
    expect(server.requests).toHaveLength(1);
    const handle = await rows.last().getByLabel("拖拽排序").boundingBox(),
      first = await rows.first().boundingBox();
    if (!handle || !first) throw new Error("queue drag bounds");
    await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2);
    await page.mouse.down();
    await page.mouse.move(handle.x + handle.width / 2, first.y + first.height / 2, { steps: 10 });
    await page.mouse.up();
    await expect(rows.locator(".queue-text")).toHaveText(["待发送三", "待发送一 note.txt", "待发送二"]);
    await expect
      .poll(() =>
        rows.evaluateAll((nodes) => nodes.every((node) => getComputedStyle(node).transform === "none")),
      )
      .toBe(true);
    await page.screenshot({ path: "test-results/desktop-queue-light.png" });
    await page.emulateMedia({ colorScheme: "dark" });
    await page.screenshot({ path: "test-results/desktop-queue-dark.png" });
    await page.emulateMedia({ colorScheme: "light" });
    await rows.filter({ hasText: "待发送一" }).getByLabel("编辑", { exact: true }).click();
    await expect(editor).toHaveText("待发送一 note.txt");
    await editor.press("Meta+A");
    expect(await clipboardText(page)).toBe(canonical);
    await editor.fill(`修改后的消息 [note.txt](${file})`);
    await editor.press("Enter");
    await expect(editor).toHaveText("");
    await rows.filter({ hasText: "待发送二" }).getByLabel("移除待发送消息", { exact: true }).click();
    await expect(rows).toHaveCount(2);
    await rows.first().getByRole("button", { name: "立即", exact: true }).click();
    await expect(rows).toHaveCount(0);
    await expect(page.getByTestId("run")).toHaveCount(4);
    await expect(page.getByTestId("run").nth(1)).toHaveAttribute("data-status", "aborted");
    await expect(page.getByTestId("run").nth(1).getByTestId("progress")).toContainText("已停止");
    await expect(page.getByTestId("run").nth(1).locator(".answer")).toHaveText("reply");
    await expect(page.getByTestId("run").nth(1).locator(".run-notice")).toHaveCount(0);
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    expect(server.requests).toHaveLength(3);
    await editor.fill("阻塞二");
    await editor.press("Enter");
    await expect(page.getByLabel("停止", { exact: true })).toBeVisible();
    await editor.fill("重启后继续");
    await editor.press("Enter");
    await expect(rows).toHaveCount(1);
    await select(page, b);
    await editor.fill("另一任务的草稿");
    await expect(rows).toHaveCount(0);
    await select(page, a);
    await expect(rows).toHaveCount(1);
    await page.getByLabel("停止", { exact: true }).click();
    await expect(page.locator(".queue-paused")).toContainText("由于你中断了当前响应,队列已暂停");
    await expect(page.getByTestId("run").last().getByTestId("progress")).toContainText("已停止");
    await expect(page.getByTestId("run").last().locator(".run-notice")).toHaveCount(0);
    await app.close();
    app = await launchDesktop({ dir, url: server.url });
    page = await app.firstWindow();
    editor = page.getByLabel("消息", { exact: true });
    await expect(page.getByTestId("queue-item")).toHaveCount(1);
    await select(page, b);
    await expect(editor).toHaveText("另一任务的草稿");
    await select(page, a);
    await expect(page.getByTestId("run").locator(".run-notice")).toHaveCount(0);
    await page.getByRole("button", { name: "继续按顺序自动发送队列中的内容", exact: true }).click();
    await expect(page.getByTestId("queue-item")).toHaveCount(0);
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    await editor.fill("阻塞三");
    await editor.press("Enter");
    await expect(page.getByLabel("停止", { exact: true })).toBeVisible();
    await editor.fill("应被清除的队列消息");
    await editor.press("Enter");
    await expect(page.getByTestId("queue-item")).toHaveCount(1);
    await page.getByLabel("停止", { exact: true }).click();
    await expect(page.locator(".queue-paused")).toBeVisible();
    await editor.fill("新消息");
    await editor.press("Enter");
    const dialog = page.getByRole("dialog");
    await expect(dialog).toContainText("要清除之前已排队的 1 条消息吗?");
    await dialog.getByRole("button", { name: "清空队列", exact: true }).click();
    await expect(dialog).toHaveCount(0);
    await expect(page.getByTestId("queue-item")).toHaveCount(0);
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    await expect(editor).toHaveText("");
    await expect(page.getByRole("alert")).toHaveCount(0);
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});