mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
* feat(plugins): unify source installation and simplify plugin settings * feat(plugins): install npm sources and review CLI updates Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm. * fix(plugins): preserve CLI diagnostics and exercise reviewed updates Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes. * test(plugins): remove unused pending helper * test(plugins): pack registry fixtures with the Windows command runner * test(plugins): assert failed client contribution setup * docs(plugins): consolidate guides and document npm publishing * docs(plugins): introduce publishing choices before npm instructions * test(plugins): own browser journey cleanup in a fixture * feat(plugins): prepare scaffolded packages for publishing
331 lines
9.6 KiB
TypeScript
331 lines
9.6 KiB
TypeScript
import { z } from "zod";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
MutableDaemonConfigSchema,
|
|
SessionInboundMessageSchema,
|
|
SessionOutboundMessageSchema,
|
|
StatusMessageSchema,
|
|
} from "./messages.js";
|
|
|
|
describe("plugin protocol compatibility", () => {
|
|
it.each([undefined, { paseo: ">=0.8.0" }])(
|
|
"parses plugin catalogs with requirements %j",
|
|
(requirements) => {
|
|
const message = {
|
|
type: "plugin.catalog.get.response",
|
|
payload: {
|
|
requestId: "catalog",
|
|
plugins: [
|
|
{ id: "example", clientBundle: "bundle", ...(requirements ? { requirements } : {}) },
|
|
],
|
|
},
|
|
};
|
|
expect(SessionOutboundMessageSchema.parse(message)).toEqual(message);
|
|
},
|
|
);
|
|
it("parses plugin timeline append messages and advertises the capability", () => {
|
|
expect(
|
|
SessionInboundMessageSchema.parse({
|
|
type: "agent.timeline.append.request",
|
|
requestId: "request-append",
|
|
agentId: "agent-1",
|
|
item: {
|
|
type: "plugin",
|
|
id: "review-1",
|
|
kind: "review",
|
|
version: 1,
|
|
data: { status: "running" },
|
|
},
|
|
}),
|
|
).toMatchObject({ type: "agent.timeline.append.request" });
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "agent.timeline.append.response",
|
|
payload: { requestId: "request-append", seq: 7, epoch: "epoch-1" },
|
|
}),
|
|
).toMatchObject({ type: "agent.timeline.append.response" });
|
|
expect(
|
|
StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "server-1",
|
|
features: { pluginTimelineItems: true },
|
|
},
|
|
}).payload,
|
|
).toMatchObject({ features: { pluginTimelineItems: true } });
|
|
});
|
|
|
|
it.each([0, -1, 1.5])("rejects plugin timeline version %s", (version) => {
|
|
expect(() =>
|
|
SessionInboundMessageSchema.parse({
|
|
type: "agent.timeline.append.request",
|
|
requestId: "append-1",
|
|
agentId: "agent-1",
|
|
item: { type: "plugin", id: "row-1", kind: "review", version, data: {} },
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("keeps old directory plugin config valid when enabled is absent", () => {
|
|
const config = MutableDaemonConfigSchema.parse({
|
|
mcp: { injectIntoAgents: true },
|
|
plugins: { example: { source: "directory", path: "/plugins/example" } },
|
|
});
|
|
|
|
expect(config.plugins?.example?.enabled).toBeUndefined();
|
|
});
|
|
|
|
it("uses namespaced management request and response pairs", () => {
|
|
expect(
|
|
SessionInboundMessageSchema.parse({
|
|
type: "plugin.directory.install.request",
|
|
requestId: "request-1",
|
|
path: "/plugins/example",
|
|
id: "example-work",
|
|
}).type,
|
|
).toBe("plugin.directory.install.request");
|
|
expect(
|
|
SessionInboundMessageSchema.parse({
|
|
type: "plugin.directory.inspect.request",
|
|
requestId: "request-0",
|
|
path: "/plugins/example",
|
|
}).type,
|
|
).toBe("plugin.directory.inspect.request");
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.directory.install.response",
|
|
payload: {
|
|
requestId: "request-1",
|
|
plugin: {
|
|
id: "example-work",
|
|
description: "Reviews changes before merge",
|
|
path: "/plugins/example",
|
|
enabled: true,
|
|
status: "running",
|
|
},
|
|
},
|
|
}).type,
|
|
).toBe("plugin.directory.install.response");
|
|
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.directory.install.response",
|
|
payload: {
|
|
requestId: "request-old-shape",
|
|
plugin: {
|
|
id: "example-work",
|
|
path: "/plugins/example",
|
|
enabled: true,
|
|
status: "running",
|
|
},
|
|
},
|
|
}).type,
|
|
).toBe("plugin.directory.install.response");
|
|
});
|
|
|
|
it("uses capability-gated source management RPCs", () => {
|
|
expect(
|
|
SessionInboundMessageSchema.parse({
|
|
type: "plugin.source.install.request",
|
|
requestId: "request-install",
|
|
source: "owner/repository:plugins/review",
|
|
ref: "main",
|
|
}).type,
|
|
).toBe("plugin.source.install.request");
|
|
expect(
|
|
SessionInboundMessageSchema.parse({
|
|
type: "plugin.source.install.request",
|
|
requestId: "request-install-old-client",
|
|
source: "owner/repository",
|
|
pluginPath: "plugins/review",
|
|
}).type,
|
|
).toBe("plugin.source.install.request");
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.source.status.response",
|
|
payload: {
|
|
requestId: "request-status",
|
|
plugins: [
|
|
{
|
|
id: "review",
|
|
source: "git",
|
|
path: "/plugins/review",
|
|
currentCommit: "a".repeat(40),
|
|
latestCommit: "b".repeat(40),
|
|
commitsBehind: 2,
|
|
updateAvailable: true,
|
|
},
|
|
],
|
|
},
|
|
}).type,
|
|
).toBe("plugin.source.status.response");
|
|
|
|
const older = StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "older-host",
|
|
features: { pluginManagement: true },
|
|
},
|
|
});
|
|
expect(older.payload.features?.pluginGitManagement).toBeUndefined();
|
|
});
|
|
|
|
it("uses a namespaced snapshot RPC for structured plugin logs", () => {
|
|
expect(
|
|
SessionInboundMessageSchema.parse({
|
|
type: "plugin.logs.get.request",
|
|
requestId: "request-logs",
|
|
pluginId: "example",
|
|
}),
|
|
).toEqual({
|
|
type: "plugin.logs.get.request",
|
|
requestId: "request-logs",
|
|
pluginId: "example",
|
|
});
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.logs.get.response",
|
|
payload: {
|
|
requestId: "request-logs",
|
|
pluginId: "example",
|
|
entries: [
|
|
{
|
|
sequence: 7,
|
|
timestamp: "2026-08-16T12:00:00.000Z",
|
|
stream: "stderr",
|
|
message: "failed to connect",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).toEqual({
|
|
type: "plugin.logs.get.response",
|
|
payload: {
|
|
requestId: "request-logs",
|
|
pluginId: "example",
|
|
entries: [
|
|
{
|
|
sequence: 7,
|
|
timestamp: "2026-08-16T12:00:00.000Z",
|
|
stream: "stderr",
|
|
message: "failed to connect",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps the plugin logs capability optional for older server info", () => {
|
|
const older = StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "older-host",
|
|
features: { pluginManagement: true },
|
|
},
|
|
});
|
|
const current = StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "current-host",
|
|
features: { pluginManagement: true, pluginLogs: true },
|
|
},
|
|
});
|
|
|
|
expect(older.payload.features?.pluginLogs).toBeUndefined();
|
|
expect(current.payload.features?.pluginLogs).toBe(true);
|
|
});
|
|
|
|
it("keeps the plugin themes capability optional for older server info", () => {
|
|
const older = StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "older-host",
|
|
features: { plugins: true },
|
|
},
|
|
});
|
|
const current = StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "current-host",
|
|
features: { plugins: true, pluginThemes: true },
|
|
},
|
|
});
|
|
|
|
expect(older.payload.features?.pluginThemes).toBeUndefined();
|
|
expect(current.payload.features?.pluginThemes).toBe(true);
|
|
});
|
|
|
|
it("keeps the catalog change notification safe for older clients", () => {
|
|
expect(
|
|
StatusMessageSchema.parse({
|
|
type: "status",
|
|
payload: { status: "plugin_catalog_changed", pluginId: "example" },
|
|
}),
|
|
).toEqual({
|
|
type: "status",
|
|
payload: { status: "plugin_catalog_changed", pluginId: "example" },
|
|
});
|
|
});
|
|
|
|
it("requires plugin action payloads and keeps remove empty", () => {
|
|
expect(() =>
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.reload.response",
|
|
payload: { requestId: "request-1" },
|
|
}),
|
|
).toThrow();
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.remove.response",
|
|
payload: { requestId: "request-2" },
|
|
}),
|
|
).toEqual({ type: "plugin.remove.response", payload: { requestId: "request-2" } });
|
|
expect(() =>
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "plugin.remove.response",
|
|
payload: { requestId: "request-2", plugin: { id: "extra" } },
|
|
}),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
it("npm metadata leaves legacy plugin rows parseable without extending their closed source enum", () => {
|
|
const legacyPlugin = z.object({
|
|
id: z.string(),
|
|
path: z.string(),
|
|
enabled: z.boolean(),
|
|
status: z.enum(["running", "disabled", "failed"]),
|
|
source: z.enum(["directory", "git"]).optional(),
|
|
});
|
|
const plugin = {
|
|
id: "review",
|
|
path: "/plugins/review",
|
|
enabled: true,
|
|
status: "running",
|
|
installation: {
|
|
identity: { kind: "npm", packageName: "@acme/review", pluginPath: "." },
|
|
currentRevision: "1.2.0",
|
|
},
|
|
};
|
|
const message = SessionOutboundMessageSchema.parse({
|
|
type: "plugin.list.response",
|
|
payload: { requestId: "npm", plugins: [plugin] },
|
|
});
|
|
expect(legacyPlugin.parse(plugin)).toEqual({
|
|
id: "review",
|
|
path: "/plugins/review",
|
|
enabled: true,
|
|
status: "running",
|
|
});
|
|
expect(message).toEqual({
|
|
type: "plugin.list.response",
|
|
payload: { requestId: "npm", plugins: [plugin] },
|
|
});
|
|
});
|