Z ZPI 文档 v0.3.8
介绍页

源码

packages/builtin-plugins/test/runtime.test.ts

typescript278 行查看原文
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { assembleSession, createCodingTools, type PluginSnapshot } from "@zpi-dev/coding-agent";
import { afterEach, expect, test, vi } from "vitest";
import { desktopPluginManifests } from "../src/index.ts";
import { defaultMemorySettings } from "../src/memory/types.ts";
import type { BuiltinPluginPorts, PluginConfiguration, PluginSessionRecord } from "../src/ports.ts";
import { BuiltinPluginRuntime } from "../src/runtime.ts";

const cleanup: (() => Promise<void>)[] = [];
afterEach(async () => {
  for (const close of cleanup.splice(0).reverse()) await close();
});
async function fixture() {
  const directory = await mkdtemp(join(tmpdir(), "zpi-builtin-runtime-"));
  cleanup.push(() => rm(directory, { recursive: true, force: true }));
  const settings = new Map<string, unknown>();
  const records = new Map<string, PluginSessionRecord>([
    ["owner", { id: "owner", projectId: null, title: "Task", createdAt: 1, updatedAt: 1 }],
  ]);
  const snapshots: PluginSnapshot[] = structuredClone([...desktopPluginManifests]);
  const config: PluginConfiguration = { plugins: snapshots };
  const ports: BuiltinPluginPorts = {
    directory,
    homeDirectory: directory,
    storage: {
      getSetting<T>(key: string) {
        return settings.get(key) as T | undefined;
      },
      setSetting: (key, value) => {
        settings.set(key, value);
      },
      setRuntime: vi.fn(),
    },
    sessions: {
      get: (id) => records.get(id),
      records: () => records.values(),
      cwd: () => directory,
      configuration: () => config,
      configure: vi.fn(),
      state: () => ({ titleState: "new" }),
      update: vi.fn(),
      createChild: vi.fn(async () => "child"),
      delete: vi.fn(async () => {}),
      select: vi.fn(),
      run: vi.fn(async () => {}),
      active: () => undefined,
      stop: vi.fn(async () => {}),
      closed: (id) => !records.has(id),
      deleting: () => false,
      changing: () => false,
      blocked: () => false,
      history: () => ({
        snapshot: () => ({ generation: "generation", endSeq: 0 }),
        readReverse: () => [],
        readRange: () => [],
      }),
    },
    models: {
      valid: () => true,
      selection: () => null,
      create: vi.fn(() => {
        throw new Error("Unexpected model call");
      }),
    },
    memorySettings: () => structuredClone(defaultMemorySettings),
    files: { preview: async () => ({ kind: "text", text: "" }) },
    credentials: () => "",
    fetch: vi.fn(async () => {
      throw new Error("Unexpected network call");
    }),
    closing: () => false,
    publish: vi.fn(),
    titleChanged: vi.fn(),
    notify: vi.fn(() => true),
    error: vi.fn(),
  };
  const runtime = new BuiltinPluginRuntime(ports);
  cleanup.push(async () => {
    await runtime.close();
    await runtime.drain();
  });
  return { runtime, ports, snapshots, directory, records };
}

test("all seven runtimes assemble with generic ports and preview performs no execution", async () => {
  const { runtime, ports, snapshots, directory } = await fixture();
  const contributions = runtime.contributions(
    snapshots,
    { sessionId: "preview", cwd: directory, preview: true },
    { validate: () => {}, bind: (snapshot) => snapshot, agents: () => [] },
  );
  const assembly = assembleSession({ ...contributions, builtinTools: createCodingTools(directory) });
  expect(contributions.plugins?.slice(0, 7).map((plugin) => plugin.runtime)).toEqual([
    "desktop.questions",
    "desktop.web-search",
    "desktop.collaboration",
    "desktop.background-tasks",
    "desktop.memory",
    "desktop.session-title",
    "desktop.notifications",
  ]);
  const tools = assembly.activeTools.map((tool) => tool.name);
  expect(tools).toEqual(
    expect.arrayContaining([
      "AskUserQuestion",
      "web_search",
      "web_fetch",
      "Agent",
      "SendMessage",
      "TaskOutput",
      "TaskStop",
    ]),
  );
  expect(tools.filter((name) => name === "TaskOutput")).toHaveLength(1);
  expect(Object.values(assembly.getContextSections()).join("\n")).toContain("Available agent types");
  expect(ports.models.create).not.toHaveBeenCalled();
  expect(ports.fetch).not.toHaveBeenCalled();
  expect(ports.sessions.run).not.toHaveBeenCalled();
  expect(ports.storage.setRuntime).not.toHaveBeenCalled();
  expect(await readdir(directory)).toEqual([]);
});

test("imported Agent definitions are loaded only when collaboration requests them", async () => {
  const { runtime, snapshots, directory } = await fixture();
  const agents = vi.fn(() => []);
  const external = { validate: () => {}, bind: (snapshot: PluginSnapshot) => snapshot, agents };
  const context = { sessionId: "preview", cwd: directory, preview: true };
  const disabled = runtime.contributions(
    snapshots.filter((plugin) => plugin.runtime !== "desktop.collaboration"),
    context,
    external,
  );
  assembleSession({ ...disabled, builtinTools: createCodingTools(directory) }).getContextSections();
  expect(agents).not.toHaveBeenCalled();

  const enabled = runtime.contributions(snapshots, context, external);
  expect(agents).not.toHaveBeenCalled();
  assembleSession({ ...enabled, builtinTools: createCodingTools(directory) }).getContextSections();
  expect(agents).toHaveBeenCalledOnce();
});

test("ordinary messages skip Agent parsing while mentions still resolve", async () => {
  const { runtime } = await fixture();
  const imported = vi.fn(() => []);
  expect(runtime.agentRequest("owner", "An ordinary user message", imported)).toBeUndefined();
  expect(imported).not.toHaveBeenCalled();

  const agent = runtime.listSubagents("owner", []).agents[0];
  if (!agent) throw new Error("Missing builtin agent");
  expect(runtime.agentRequest("owner", `@${agent.name} Inspect the task`, imported)).toEqual({
    agent: agent.name,
    prompt: "Inspect the task",
  });
  expect(imported).toHaveBeenCalledOnce();
});

test.each(["disabled", "child"])(
  "slash suggestions skip imported Agent parsing for %s collaboration",
  async (mode) => {
    const { runtime, ports } = await fixture();
    const original = ports.sessions.configuration("owner");
    const profile = runtime.listSubagents("owner", []).agents[0];
    if (!profile) throw new Error("Missing builtin agent");
    const configuration = vi
      .spyOn(ports.sessions, "configuration")
      .mockReturnValue(
        mode === "disabled" ? { ...original, plugins: [] } : { ...original, subagentProfile: profile },
      );
    const imported = vi.fn(() => []);
    expect(runtime.commandSubagents("owner", imported)).toEqual([]);
    expect(imported).not.toHaveBeenCalled();

    configuration.mockReturnValue(original);
    expect(runtime.commandSubagents("owner", imported)).toContainEqual(profile);
    expect(imported).toHaveBeenCalledOnce();
  },
);

test("plugin shutdown attempts every owner and retains original failures", async () => {
  const { runtime } = await fixture();
  const failure = new Error("memory failed to close");
  const first = vi.spyOn(runtime.longMemory, "close").mockRejectedValueOnce(failure);
  const collaboration = vi.spyOn(runtime.collaboration, "close");
  const background = vi.spyOn(runtime.background, "close");
  const titles = vi.spyOn(runtime.titles, "close");
  const notifications = vi.spyOn(runtime.notifications, "close");
  await expect(runtime.close()).rejects.toBe(failure);
  expect(first).toHaveBeenCalledOnce();
  for (const owner of [collaboration, background, titles, notifications])
    expect(owner).toHaveBeenCalledOnce();
});

test("a late child result is durable while the parent's current run is cancelled", async () => {
  const { runtime, ports } = await fixture();
  vi.spyOn(ports.sessions, "active").mockReturnValue({
    runId: "cancelled-run",
    done: Promise.resolve(),
    aborted: true,
  });
  await runtime.inbox.deliver({
    parentSessionId: "owner",
    agentId: "child",
    agentType: "Explore",
    description: "Late findings",
    status: "completed",
    result: "Retain for the next interaction",
  });
  expect(ports.storage.setRuntime).toHaveBeenCalledWith(
    "owner",
    "subagent_inbox",
    "inbox",
    [expect.objectContaining({ text: expect.stringContaining("Retain for the next interaction") })],
    "queued",
  );
  expect(ports.sessions.run).not.toHaveBeenCalled();
});

test("child settlement retains the host's durable-storage diagnostic after publication fails", async () => {
  const { runtime } = await fixture();
  vi.spyOn(runtime.collaboration, "taskForChild").mockReturnValue({
    agentId: "agent",
    childSessionId: "child",
    parentSessionId: "owner",
    parentToolCallId: "tool",
    agentType: "Explore",
    description: "Explore",
    color: "blue",
    background: true,
    status: "running",
    startedAt: 1,
    modelLabel: "model",
    totalToolUseCount: 0,
    totalTokens: 0,
  });
  const failure = new Error("publication failed");
  const update = vi
    .spyOn(runtime.collaboration, "update")
    .mockImplementationOnce(() => {
      throw failure;
    })
    .mockImplementation(() => {});
  const report = vi.fn(() => "storage: publication failed");
  runtime.settle(
    "child",
    { runId: "run", done: Promise.resolve() },
    { status: "completed", endedAt: 2, result: "Findings" },
    report,
  );
  expect(report).toHaveBeenCalledWith(failure, "subagent.persist");
  expect(update).toHaveBeenLastCalledWith(
    "agent",
    expect.objectContaining({ status: "error", error: "storage: publication failed" }),
  );
});

test("builtin implementation graph has no desktop, UI or cross-package relative imports", async () => {
  const source = fileURLToPath(new URL("../src/", import.meta.url));
  const files = (await readdir(source, { recursive: true })).filter((name) => name.endsWith(".ts"));
  for (const name of files) {
    const file = join(source, name);
    const content = await readFile(file, "utf8");
    for (const match of content.matchAll(/(?:from\s*|import\s*\()?["']([^"'\n]+)["']/g)) {
      const specifier = match[1];
      if (!specifier.endsWith(".ts") && !specifier.startsWith("zpi-") && specifier !== "electron") continue;
      expect(specifier, `${name}: ${specifier}`).not.toMatch(
        /^(?:@zpi-dev\/(?:desktop|ui)|electron)(?:\/|$)/,
      );
      if (specifier.startsWith("."))
        expect(relative(source, resolve(dirname(file), specifier)), `${name}: ${specifier}`).not.toMatch(
          /^\.\.(?:\/|$)/,
        );
    }
  }
});