Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/test/resources.test.ts

typescript217 行查看原文
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { builtinPlugins } from "@zpi-dev/builtin-plugins";
import { buildMentionMarkdown } from "@zpi-dev/coding-agent";
import { expect, test } from "vitest";
import { chunk, done, send } from "../../../tests/fake-server.ts";
import { SessionHost } from "../src/main/session-host.ts";
import { SettingsStore } from "../src/main/storage.ts";
import { cleanup, codec, fixture, run } from "./helpers/context-fixture.ts";

const defaultInstructions = builtinPlugins.flatMap((plugin) => plugin.instructions ?? []).join("\n\n");

test("session resources remain bound to the project root after bash changes directory", async () => {
  const f = await fixture((_, response, index) => {
    if (index === 0) {
      send(
        response,
        chunk({
          tool_calls: [
            {
              index: 0,
              id: "cd",
              type: "function",
              function: {
                name: "bash",
                arguments: JSON.stringify({ command: "cd nested && pwd" }),
              },
            },
          ],
        }),
      );
      done(response, "tool_calls");
    } else {
      send(response, chunk({ content: "ok" }));
      done(response);
    }
  });
  const project = await f.host.addProject(f.workspace);
  f.session = await f.host.createSession(project.id);
  const nested = join(f.workspace, "nested");
  await mkdir(join(nested, ".zpi", "skills", "nested-only"), { recursive: true });
  await mkdir(join(f.workspace, ".zpi", "skills", "root-only"), { recursive: true });
  await writeFile(join(f.workspace, "AGENTS.md"), "ROOT POLICY");
  await writeFile(join(nested, "AGENTS.md"), "NESTED POLICY");
  for (const [root, name] of [
    [f.workspace, "root-only"],
    [nested, "nested-only"],
  ]) {
    await writeFile(join(root, ".zpi", "skills", name, "SKILL.md"), "---\ndescription: guidance\n---\nBODY");
  }
  await run(f, "change directory");
  await run(f, "check resources again");
  expect(f.server.requests).toHaveLength(3);
  expect(f.server.requests[1].messages).toContainEqual(
    expect.objectContaining({ role: "tool", content: expect.stringContaining(join(project.path, "nested")) }),
  );
  for (const request of f.server.requests) {
    const messages = request.messages as unknown as { role: string; content: string }[];
    const prompt = messages
      .filter((message) => message.role === "system")
      .map((message) => message.content)
      .join("\n\n");
    expect(prompt).toContain("ROOT POLICY");
    expect(prompt).toContain("<name>root-only</name>");
    expect(prompt).not.toContain("NESTED POLICY");
    expect(prompt).not.toContain("<name>nested-only</name>");
  }
  expect((await f.host.listSessionSkills(f.session.id)).skills.map((s) => s.name)).toEqual(["root-only"]);
  expect(f.host.workspaceInfo(f.session.id).cwd).toBe(project.path);
});

test.each(["/中文读杠", "$100", "$missing task", "@someone@example.com"])(
  "ordinary input %j reaches the provider and remains in desktop history",
  async (text) => {
    const f = await fixture();
    await run(f, text);
    expect(f.server.requests).toHaveLength(1);
    expect(f.server.requests[0].messages).toContainEqual(
      expect.objectContaining({ role: "user", content: text }),
    );
    expect(f.host.getSessionSnapshot(f.session.id).view.runs[0]).toMatchObject({
      userMessage: text,
      status: "completed",
    });
  },
);

test("desktop link instructions reach prompt previews, new runs and restored custom sessions", async () => {
  for (const template of [
    undefined,
    { id: "custom", name: "Custom", preamble: "Custom assistant", rules: "CUSTOM RULE" },
  ]) {
    const f = await fixture(undefined, template);
    f.settings.setPluginEnabled("zpi.collaboration", false);
    f.settings.setPluginEnabled("zpi.memory", false);
    const assertLinks = (prompt: string) => {
      expect(prompt).toContain("Return web URLs as Markdown links");
      expect(prompt).toContain("return local file references as Markdown links");
      expect(prompt).toContain("use absolute paths for link destinations");
      expect(prompt).toContain("[My Report.md](</absolute/path/My Report.md>)");
      expect(prompt).toContain("outside inline code and code blocks");
      expect(prompt).toContain("Prefer Markdown links for ordinary file references");
      expect(prompt).not.toContain("To cite a local file in your answer, use ::zcode-file-citation");
      for (const part of defaultInstructions.split("\n\n")) expect(prompt).toContain(part);
      if (template) expect(prompt).toContain("CUSTOM RULE");
    };
    const preview = await f.host.previewPrompt();
    assertLinks(preview.prompt);
    expect(preview.customInstructions).toBe(defaultInstructions);
    expect(preview.basePrompt).not.toContain("<system_rules>");
    expect(preview.basePrompt).not.toContain("return local file references as Markdown links");
    expect(preview.prompt.replace(`\n\n${preview.customInstructions}`, "")).toBe(preview.basePrompt);
    await run(f, "list files and the preview URL");
    const requestPrompt = (index: number) => {
      const messages = f.server.requests[index].messages as unknown as { role: string; content: string }[];
      return messages
        .filter((message) => message.role === "system")
        .map((message) => message.content)
        .join("\n\n");
    };
    const systemPrompt = requestPrompt(0);
    assertLinks(systemPrompt);
    for (const part of preview.prompt.split("\n\n")) expect(systemPrompt).toContain(part);
    expect(systemPrompt.split("return local file references as Markdown links")).toHaveLength(2);
    await f.host.close();
    const restarted = new SessionHost(
      f.dir,
      new SettingsStore(f.dir, codec),
      f.resources,
      f.workspace,
      undefined,
    );
    await restarted.init();
    cleanup.push(() => restarted.close());
    await restarted.startRun({ sessionId: f.session.id, text: "show clickable paths" });
    await restarted.activeRuns.get(f.session.id)?.done;
    assertLinks(requestPrompt(1));
  }
});

test("prompt snapshots and skill defaults survive restart; the read-only preview omits project context", async () => {
  const f = await fixture(undefined, {
    id: "mine",
    name: "Mine",
    preamble: "In {{cwd}} for {{project_name}} on {{platform}}",
    rules: "MY RULE",
  });
  const root = join(f.resources, ".zpi", "skills", "review");
  await mkdir(root, { recursive: true });
  await writeFile(join(root, "SKILL.md"), "---\nname: review\ndescription: review code\n---\nBODY");
  await writeFile(join(f.workspace, "AGENTS.md"), "PROJECT POLICY");
  const original = f.session;
  await run(f, "original first message");
  f.server.requests.splice(0);
  f.session = await f.host.createSession(null, { cwd: f.workspace });
  expect(f.session.draft).toBe(true);

  const preview = await f.host.previewPrompt();
  expect(preview.prompt).toContain("MY RULE");
  expect(preview.prompt).not.toContain("PROJECT POLICY");
  expect(preview.prompt).toContain("available_skills");
  const configured = f.session;
  await run(f, "one");
  expect(JSON.stringify(f.server.requests[0])).toContain("MY RULE");
  expect(JSON.stringify(f.server.requests[0])).toContain("PROJECT POLICY");
  const path = (await f.host.getSkillSettings()).skills[0].path;
  await f.host.setSkillEnabled(path, false);
  expect((await f.host.listSessionSkills(configured.id)).skills).toHaveLength(1);
  await run(f, "$review task");
  expect(JSON.stringify(f.server.requests[1])).toContain("MY RULE");
  expect(JSON.stringify(f.server.requests[1])).not.toContain("BODY");
  expect(f.server.requests[1].messages).toContainEqual(
    expect.objectContaining({ role: "user", content: "$review task" }),
  );
  f.session = await f.host.createSession(null, { cwd: f.workspace });
  expect((await f.host.listSessionSkills(f.session.id)).skills).toHaveLength(0);
  const linked = `${buildMentionMarkdown("$review", path)} task`;
  await run(f, linked);
  expect(f.host.getSessionSnapshot(f.session.id).session.draft).toBe(false);
  expect(f.server.requests[2].messages).toContainEqual(
    expect.objectContaining({ role: "user", content: linked }),
  );
  expect(JSON.stringify(f.server.requests[2])).toContain("MY RULE");
  expect(JSON.stringify(f.server.requests[2])).not.toContain("available_skills");
  expect(JSON.stringify(f.server.requests[2])).not.toContain("BODY");
  f.session = original;
  await run(f, "original");
  expect(JSON.stringify(f.server.requests[3])).toContain("MY RULE");
  const restarted = new SessionHost(
    f.dir,
    new SettingsStore(f.dir, codec),
    f.resources,
    f.workspace,
    undefined,
  );
  await restarted.init();
  cleanup.push(() => restarted.close());
  await restarted.startRun({ sessionId: configured.id, text: "$review after restart" });
  await restarted.activeRuns.get(configured.id)?.done;
  expect(JSON.stringify(f.server.requests[4])).toContain("MY RULE");
  expect(JSON.stringify(f.server.requests[4])).not.toContain("BODY");
  expect(f.server.requests[4].messages).toContainEqual(
    expect.objectContaining({ role: "user", content: "$review after restart" }),
  );
  expect(f.host.listTools().map((t) => t.name)).toEqual([
    "read",
    "bash",
    "edit",
    "write",
    "AskUserQuestion",
    "Agent",
    "SendMessage",
    "TaskOutput",
    "TaskStop",
  ]);
});