mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 21:23:04 +00:00
1738 lines
50 KiB
TypeScript
1738 lines
50 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { afterEach, expect, test, vi } from "vitest";
|
|
import { createPaseoApi, createPaseoClient } from "./index.js";
|
|
import { DaemonClient } from "./daemon-client.js";
|
|
import type { PaseoAgent, PaseoClient, PaseoWorkspace } from "./index.js";
|
|
|
|
type FakeWebSocketHandler = (...args: unknown[]) => void;
|
|
|
|
class FakeWebSocket {
|
|
static instances: FakeWebSocket[] = [];
|
|
|
|
readyState = 1;
|
|
sent: Array<string | ArrayBuffer | Uint8Array> = [];
|
|
onopen: FakeWebSocketHandler | null = null;
|
|
onmessage: FakeWebSocketHandler | null = null;
|
|
onclose: FakeWebSocketHandler | null = null;
|
|
onerror: FakeWebSocketHandler | null = null;
|
|
|
|
constructor(
|
|
readonly url: string,
|
|
readonly protocols?: string | string[],
|
|
) {
|
|
FakeWebSocket.instances.push(this);
|
|
}
|
|
|
|
send(data: string | ArrayBuffer | Uint8Array): void {
|
|
this.sent.push(data);
|
|
const frame = JSON.parse(String(data));
|
|
if (frame.message?.type === "subscription.release.request")
|
|
this.message(
|
|
sessionMessage({
|
|
type: "subscription.release.response",
|
|
payload: {
|
|
requestId: frame.message.requestId,
|
|
subscriptionId: frame.message.subscriptionId,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = 3;
|
|
}
|
|
|
|
open(): void {
|
|
this.onopen?.();
|
|
}
|
|
|
|
message(data: string): void {
|
|
this.onmessage?.({ data });
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
FakeWebSocket.instances.length = 0;
|
|
});
|
|
|
|
function sessionMessage(message: object): string {
|
|
return JSON.stringify({
|
|
type: "session",
|
|
message,
|
|
});
|
|
}
|
|
|
|
function parseSentSessionMessage(data: string | ArrayBuffer | Uint8Array | undefined): {
|
|
type: string;
|
|
requestId?: string;
|
|
agentId?: string;
|
|
workspaceId?: string;
|
|
provider?: string;
|
|
providers?: string[];
|
|
cwd?: string;
|
|
config?: unknown;
|
|
draftConfig?: unknown;
|
|
filter?: unknown;
|
|
page?: unknown;
|
|
sync?: unknown;
|
|
text?: string;
|
|
} {
|
|
if (typeof data !== "string") {
|
|
throw new Error("Expected string WebSocket frame");
|
|
}
|
|
const parsed = JSON.parse(data);
|
|
return parsed.message;
|
|
}
|
|
|
|
function parseSentFrame(
|
|
data: string | ArrayBuffer | Uint8Array | undefined,
|
|
): Record<string, unknown> {
|
|
if (typeof data !== "string") {
|
|
throw new Error("Expected string WebSocket frame");
|
|
}
|
|
return JSON.parse(data);
|
|
}
|
|
|
|
async function connectClient(
|
|
features: Record<string, boolean> = {
|
|
providerUsageList: true,
|
|
providersSnapshotCwd: true,
|
|
ownedSubscriptions: true,
|
|
},
|
|
): Promise<{ client: PaseoClient; ws: FakeWebSocket }> {
|
|
const client = createPaseoClient({
|
|
url: "ws://daemon.test",
|
|
webSocketFactory: (url) => new FakeWebSocket(url),
|
|
reconnect: { enabled: false },
|
|
});
|
|
|
|
const connectPromise = client.connect();
|
|
const ws = FakeWebSocket.instances[0];
|
|
ws.open();
|
|
const hello = parseSentFrame(ws.sent.at(-1));
|
|
expect(hello).toMatchObject({
|
|
type: "hello",
|
|
clientType: "cli",
|
|
protocolVersion: 1,
|
|
});
|
|
expect(hello.clientId).toEqual(expect.stringMatching(/^paseo-sdk-/));
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "status",
|
|
payload: {
|
|
status: "server_info",
|
|
serverId: "srv_sdk_test",
|
|
hostname: null,
|
|
version: null,
|
|
features,
|
|
},
|
|
}),
|
|
);
|
|
await connectPromise;
|
|
|
|
return { client, ws };
|
|
}
|
|
|
|
function acknowledgeObservation(ws: FakeWebSocket, subscriptionId: string): void {
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
ws.message(
|
|
sessionMessage({
|
|
type: request.type.replace(/([._])request$/, "$1response"),
|
|
payload: {
|
|
requestId: request.requestId,
|
|
subscriptionId,
|
|
entries: [],
|
|
pageInfo: { nextCursor: null, prevCursor: null, hasMore: false },
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function observeAgents(client: PaseoClient, ws: FakeWebSocket): Promise<string> {
|
|
const ready = client.agents.list({ subscribe: {} });
|
|
acknowledgeObservation(ws, "agents-sdk");
|
|
return (await ready).subscriptionId!;
|
|
}
|
|
|
|
function createWorkspace(input: Partial<PaseoWorkspace> = {}): PaseoWorkspace {
|
|
return {
|
|
id: "workspace_sdk",
|
|
projectId: "project_sdk",
|
|
projectDisplayName: "SDK",
|
|
projectRootPath: "/repo/sdk",
|
|
workspaceDirectory: "/repo/sdk",
|
|
projectKind: "git",
|
|
workspaceKind: "directory",
|
|
name: "sdk",
|
|
archivingAt: null,
|
|
status: "done",
|
|
statusEnteredAt: null,
|
|
activityAt: "2026-05-16T00:00:00.000Z",
|
|
scripts: [],
|
|
gitRuntime: null,
|
|
githubRuntime: null,
|
|
...input,
|
|
};
|
|
}
|
|
|
|
function createAgent(input: Partial<PaseoAgent> = {}): PaseoAgent {
|
|
return {
|
|
id: "agent_sdk",
|
|
provider: "codex",
|
|
cwd: "/repo/sdk",
|
|
model: null,
|
|
createdAt: "2026-05-16T00:00:00.000Z",
|
|
updatedAt: "2026-05-16T00:00:00.000Z",
|
|
lastUserMessageAt: null,
|
|
status: "idle",
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: false,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsRewindBoth: false,
|
|
supportsRewindConversation: false,
|
|
supportsRewindFiles: false,
|
|
supportsToolInvocations: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
title: null,
|
|
labels: {},
|
|
archivedAt: null,
|
|
...input,
|
|
};
|
|
}
|
|
|
|
test("createPaseoClient exposes workspace list through the daemon client", async () => {
|
|
const { client, ws } = await connectClient();
|
|
|
|
const listPromise = client.workspaces.list({
|
|
filter: { query: "sdk" },
|
|
page: { limit: 10 },
|
|
});
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
|
|
expect(request).toMatchObject({
|
|
type: "fetch_workspaces_request",
|
|
filter: { query: "sdk" },
|
|
page: { limit: 10 },
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "fetch_workspaces_response",
|
|
payload: {
|
|
requestId: request.requestId,
|
|
entries: [],
|
|
pageInfo: {
|
|
nextCursor: null,
|
|
prevCursor: null,
|
|
hasMore: false,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(listPromise).resolves.toEqual({
|
|
requestId: request.requestId,
|
|
entries: [],
|
|
emptyProjects: [],
|
|
pageInfo: {
|
|
nextCursor: null,
|
|
prevCursor: null,
|
|
hasMore: false,
|
|
},
|
|
});
|
|
expect(client.getConnectionState()).toEqual({ status: "connected" });
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("createPaseoApi borrows daemon capabilities without exposing connection ownership", () => {
|
|
const daemonClient = new DaemonClient({
|
|
url: "ws://daemon.test",
|
|
clientId: "borrowed-api",
|
|
reconnect: { enabled: false },
|
|
});
|
|
|
|
const paseo = createPaseoApi(daemonClient);
|
|
|
|
expect(Object.keys(paseo).sort()).toEqual([
|
|
"agents",
|
|
"config",
|
|
"dispose",
|
|
"hosts",
|
|
"observeEvents",
|
|
"projects",
|
|
"providers",
|
|
"terminals",
|
|
"workspaces",
|
|
]);
|
|
expect("connect" in paseo).toBe(false);
|
|
expect("close" in paseo).toBe(false);
|
|
expect("skills" in paseo.agents).toBe(false);
|
|
});
|
|
|
|
test("agent handles send permission responses for their agent", async () => {
|
|
const { client, ws } = await connectClient();
|
|
|
|
await client.agents.ref("agent_sdk").respondToPermission({
|
|
requestId: "permission-request",
|
|
response: {
|
|
behavior: "deny",
|
|
selectedActionId: "deny-once",
|
|
message: "Not approved",
|
|
interrupt: true,
|
|
},
|
|
});
|
|
|
|
expect(parseSentFrame(ws.sent.at(-1))).toEqual({
|
|
type: "session",
|
|
message: {
|
|
type: "agent_permission_response",
|
|
agentId: "agent_sdk",
|
|
requestId: "permission-request",
|
|
response: {
|
|
behavior: "deny",
|
|
selectedActionId: "deny-once",
|
|
message: "Not approved",
|
|
interrupt: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("project actions list registered projects through the existing RPC", async () => {
|
|
const { client, ws } = await connectClient();
|
|
|
|
const listPromise = client.projects.list({
|
|
requestId: "projects-list-request",
|
|
sync: { generation: "daemon-generation", afterSeq: 7 },
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "project.list.request",
|
|
requestId: "projects-list-request",
|
|
sync: { generation: "daemon-generation", afterSeq: 7 },
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "project.list.response",
|
|
payload: {
|
|
requestId: "projects-list-request",
|
|
projects: [
|
|
{
|
|
projectId: "project_sdk",
|
|
projectKey: "sdk",
|
|
projectDisplayName: "SDK",
|
|
projectCustomName: null,
|
|
projectCustomIconRevision: null,
|
|
projectIconRevision: "icon-revision",
|
|
projectRootPath: "/repo/sdk",
|
|
projectKind: "git",
|
|
syncSeq: 8,
|
|
},
|
|
],
|
|
sync: {
|
|
generation: "daemon-generation",
|
|
headSeq: 8,
|
|
mode: "changes",
|
|
removals: [],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(listPromise).resolves.toEqual({
|
|
requestId: "projects-list-request",
|
|
projects: [
|
|
{
|
|
projectId: "project_sdk",
|
|
projectKey: "sdk",
|
|
projectDisplayName: "SDK",
|
|
projectCustomName: null,
|
|
projectCustomIconRevision: null,
|
|
projectIconRevision: "icon-revision",
|
|
projectRootPath: "/repo/sdk",
|
|
projectKind: "git",
|
|
syncSeq: 8,
|
|
},
|
|
],
|
|
sync: {
|
|
generation: "daemon-generation",
|
|
headSeq: 8,
|
|
mode: "changes",
|
|
removals: [],
|
|
},
|
|
});
|
|
await client.close();
|
|
});
|
|
|
|
test("project actions subscribe to existing project updates", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const updates: string[] = [];
|
|
const unsubscribe = client.projects.subscribe((update) => {
|
|
updates.push(update.kind === "upsert" ? update.project.projectDisplayName : update.projectId);
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "session.events.set_subscription.request",
|
|
events: ["project.update"],
|
|
});
|
|
acknowledgeObservation(ws, "projects-sdk");
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "project.update",
|
|
payload: {
|
|
subscriptionId: "projects-sdk",
|
|
kind: "upsert",
|
|
project: {
|
|
projectId: "project_sdk",
|
|
projectKey: "sdk",
|
|
projectDisplayName: "Renamed SDK",
|
|
projectCustomName: "Renamed SDK",
|
|
projectCustomIconRevision: null,
|
|
projectIconRevision: "icon-revision",
|
|
projectRootPath: "/repo/sdk",
|
|
projectKind: "git",
|
|
syncSeq: 9,
|
|
},
|
|
generation: "daemon-generation",
|
|
seq: 9,
|
|
},
|
|
}),
|
|
);
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "project.update",
|
|
payload: {
|
|
subscriptionId: "projects-sdk",
|
|
kind: "remove",
|
|
projectId: "project_removed",
|
|
generation: "daemon-generation",
|
|
seq: 10,
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(updates).toEqual(["Renamed SDK", "project_removed"]);
|
|
|
|
unsubscribe();
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "project.update",
|
|
payload: {
|
|
subscriptionId: "projects-sdk",
|
|
kind: "remove",
|
|
projectId: "project_after_unsubscribe",
|
|
},
|
|
}),
|
|
);
|
|
expect(updates).toEqual(["Renamed SDK", "project_removed"]);
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("agent actions list the daemon directory without exposing the low-level client", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const listedAgent = createAgent({ title: "Planner" });
|
|
|
|
const listPromise = client.agents.list({
|
|
filter: { includeArchived: false },
|
|
page: { limit: 10 },
|
|
});
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(request).toMatchObject({
|
|
type: "fetch_agents_request",
|
|
filter: { includeArchived: false },
|
|
page: { limit: 10 },
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "fetch_agents_response",
|
|
payload: {
|
|
requestId: request.requestId,
|
|
entries: [
|
|
{
|
|
agent: listedAgent,
|
|
project: {
|
|
projectKey: "project_sdk",
|
|
projectName: "SDK",
|
|
workspaceName: "main",
|
|
checkout: {
|
|
cwd: "/repo/sdk",
|
|
isGit: false,
|
|
currentBranch: null,
|
|
remoteUrl: null,
|
|
isPaseoOwnedWorktree: false,
|
|
mainRepoRoot: null,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
nextCursor: null,
|
|
prevCursor: null,
|
|
hasMore: false,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(listPromise).resolves.toMatchObject({
|
|
entries: [{ agent: { id: "agent_sdk", title: "Planner" } }],
|
|
});
|
|
await client.close();
|
|
});
|
|
|
|
test("workspace handles keep identity and refresh snapshots through existing driver calls", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const openedWorkspace = createWorkspace();
|
|
|
|
const openPromise = client.workspaces.open("/repo/sdk", "open-workspace-request");
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "open_project_request",
|
|
cwd: "/repo/sdk",
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "open_project_response",
|
|
payload: {
|
|
requestId: "open-workspace-request",
|
|
workspace: openedWorkspace,
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const workspace = await openPromise;
|
|
expect(workspace.id).toBe("workspace_sdk");
|
|
expect(workspace.projectId).toBe("project_sdk");
|
|
expect(workspace.directory).toBe("/repo/sdk");
|
|
expect(workspace.current()).toEqual(openedWorkspace);
|
|
|
|
const refreshedWorkspace = createWorkspace({ name: "sdk refreshed" });
|
|
const refetchPromise = workspace.refresh({ requestId: "workspace-refetch-request" });
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "fetch_workspaces_request",
|
|
requestId: "workspace-refetch-request",
|
|
page: { limit: 200 },
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "fetch_workspaces_response",
|
|
payload: {
|
|
requestId: "workspace-refetch-request",
|
|
entries: [],
|
|
pageInfo: {
|
|
nextCursor: "workspace-page-2",
|
|
prevCursor: null,
|
|
hasMore: true,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
const secondPageRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(secondPageRequest).toMatchObject({
|
|
type: "fetch_workspaces_request",
|
|
page: { limit: 200, cursor: "workspace-page-2" },
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "fetch_workspaces_response",
|
|
payload: {
|
|
requestId: secondPageRequest.requestId,
|
|
entries: [refreshedWorkspace],
|
|
pageInfo: {
|
|
nextCursor: null,
|
|
prevCursor: null,
|
|
hasMore: false,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(refetchPromise).resolves.toEqual(refreshedWorkspace);
|
|
expect(workspace.current()).toEqual(refreshedWorkspace);
|
|
|
|
const directoryReady = client.workspaces.list({ subscribe: {} });
|
|
acknowledgeObservation(ws, "workspaces-sdk");
|
|
await directoryReady;
|
|
const updates: string[] = [];
|
|
const unsubscribe = workspace.subscribe((update) => {
|
|
if (update.kind === "upsert") {
|
|
updates.push(update.workspace.name);
|
|
}
|
|
});
|
|
const pushedWorkspace = createWorkspace({ name: "sdk pushed" });
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "workspace_update",
|
|
payload: { subscriptionId: "workspaces-sdk", kind: "upsert", workspace: pushedWorkspace },
|
|
}),
|
|
);
|
|
expect(updates).toEqual(["sdk pushed"]);
|
|
expect(workspace.current()).toEqual(pushedWorkspace);
|
|
|
|
const titlePromise = workspace.setTitle("SDK review", "workspace-title-request");
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "workspace.title.set.request",
|
|
requestId: "workspace-title-request",
|
|
workspaceId: "workspace_sdk",
|
|
title: "SDK review",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "workspace.title.set.response",
|
|
payload: {
|
|
requestId: "workspace-title-request",
|
|
workspaceId: "workspace_sdk",
|
|
accepted: true,
|
|
title: "SDK review",
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
await expect(titlePromise).resolves.toEqual({ title: "SDK review" });
|
|
|
|
unsubscribe();
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "workspace_update",
|
|
payload: {
|
|
subscriptionId: "workspaces-sdk",
|
|
kind: "upsert",
|
|
workspace: createWorkspace({ name: "sdk after unsubscribe" }),
|
|
},
|
|
}),
|
|
);
|
|
expect(updates).toEqual(["sdk pushed"]);
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("plugin-shaped PR workspace create and agent create use the existing daemon RPCs", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const createdWorkspace = createWorkspace({ id: "workspace_fresh", name: "Issue 42" });
|
|
|
|
const workspacePromise = client.workspaces.create({
|
|
source: {
|
|
kind: "worktree",
|
|
cwd: "/repo/sdk",
|
|
action: "checkout",
|
|
checkoutSource: { kind: "change_request", forge: "github", number: 42 },
|
|
},
|
|
title: "Issue 42",
|
|
});
|
|
const workspaceRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(workspaceRequest).toMatchObject({
|
|
type: "workspace.create.request",
|
|
source: {
|
|
kind: "worktree",
|
|
cwd: "/repo/sdk",
|
|
action: "checkout",
|
|
checkoutSource: { kind: "change_request", forge: "github", number: 42 },
|
|
},
|
|
title: "Issue 42",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "workspace.create.response",
|
|
payload: {
|
|
requestId: workspaceRequest.requestId,
|
|
workspace: createdWorkspace,
|
|
setupTerminalId: null,
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const workspace = await workspacePromise;
|
|
const agentPromise = workspace.agents.create({
|
|
config: { provider: "codex/gpt-5.4" },
|
|
parent: client.agents.ref("parent_sdk"),
|
|
prompt: "Implement issue 42.",
|
|
});
|
|
const agentRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(agentRequest).toMatchObject({
|
|
type: "create_agent_request",
|
|
config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk" },
|
|
workspaceId: "workspace_fresh",
|
|
callerAgentId: "parent_sdk",
|
|
initialPrompt: "Implement issue 42.",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "status",
|
|
payload: {
|
|
status: "agent_created",
|
|
requestId: agentRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
agent: createAgent({ workspaceId: "workspace_fresh" }),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const agent = await agentPromise;
|
|
expect(agent.workspaceId).toBe("workspace_fresh");
|
|
expect(agent.cwd).toBe("/repo/sdk");
|
|
await client.close();
|
|
});
|
|
|
|
test("agents.create forwards internal on the request, outside the agent config", async () => {
|
|
const { client, ws } = await connectClient({
|
|
providerUsageList: true,
|
|
providersSnapshotCwd: true,
|
|
ownedSubscriptions: true,
|
|
backgroundWorkspaces: true,
|
|
});
|
|
|
|
const createPromise = client.agents.create({
|
|
config: { provider: "codex/gpt-5.4" },
|
|
cwd: "/repo/sdk",
|
|
title: "Summary helper",
|
|
background: true,
|
|
autoArchive: true,
|
|
});
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(request).toMatchObject({
|
|
type: "create_agent_request",
|
|
config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk", title: "Summary helper" },
|
|
background: true,
|
|
autoArchive: true,
|
|
});
|
|
expect(request.config).not.toHaveProperty("internal");
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "status",
|
|
payload: {
|
|
status: "agent_created",
|
|
requestId: request.requestId,
|
|
agentId: "agent_helper",
|
|
agent: createAgent({ id: "agent_helper" }),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const agent = await createPromise;
|
|
expect(agent.id).toBe("agent_helper");
|
|
await client.close();
|
|
});
|
|
|
|
test("agents.create with internal fails before sending on a host without internal agents", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const sentBefore = ws.sent.length;
|
|
|
|
await expect(
|
|
client.agents.create({
|
|
config: { provider: "codex/gpt-5.4" },
|
|
cwd: "/repo/sdk",
|
|
background: true,
|
|
}),
|
|
).rejects.toThrow("Update the host to use background workspaces.");
|
|
expect(ws.sent).toHaveLength(sentBefore);
|
|
await client.close();
|
|
});
|
|
|
|
test("agent handles delegate create, send, timeline refetch, archive, and local updates", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const createdAgent = createAgent();
|
|
|
|
const createPromise = client.agents.create({
|
|
config: { provider: "codex/gpt-5.4" },
|
|
cwd: "/repo/sdk",
|
|
prompt: "ship it",
|
|
});
|
|
const createRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(createRequest).toMatchObject({
|
|
type: "create_agent_request",
|
|
config: {
|
|
provider: "codex",
|
|
model: "gpt-5.4",
|
|
cwd: "/repo/sdk",
|
|
},
|
|
initialPrompt: "ship it",
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "status",
|
|
payload: {
|
|
status: "agent_created",
|
|
requestId: createRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
agent: createdAgent,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const agent = await createPromise;
|
|
expect(agent.id).toBe("agent_sdk");
|
|
expect(agent.current()).toEqual(createdAgent);
|
|
|
|
await observeAgents(client, ws);
|
|
const updatedAgents: string[] = [];
|
|
const unsubscribe = agent.subscribe((update) => {
|
|
if (update.kind === "upsert") {
|
|
updatedAgents.push(update.agent.title ?? "");
|
|
}
|
|
});
|
|
const updatedAgent = createAgent({ title: "Updated" });
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "agent_update",
|
|
payload: { subscriptionId: "agents-sdk", kind: "upsert", agent: updatedAgent, project: null },
|
|
}),
|
|
);
|
|
expect(updatedAgents).toEqual(["Updated"]);
|
|
expect(agent.current()).toEqual(updatedAgent);
|
|
|
|
const sendPromise = agent.send("hello", {
|
|
messageId: "message-sdk",
|
|
activeTurnBehavior: "steer",
|
|
});
|
|
const sendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(sendRequest).toMatchObject({
|
|
type: "send_agent_message_request",
|
|
agentId: "agent_sdk",
|
|
text: "hello",
|
|
messageId: "message-sdk",
|
|
activeTurnBehavior: "steer",
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "send_agent_message_response",
|
|
payload: {
|
|
requestId: sendRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
accepted: true,
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
await sendPromise;
|
|
|
|
const runPromise = agent.run("finish the task", {
|
|
messageId: "run-message-sdk",
|
|
activeTurnBehavior: "interrupt",
|
|
timeoutMs: 30_000,
|
|
});
|
|
const runSendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(runSendRequest).toMatchObject({
|
|
type: "send_agent_message_request",
|
|
agentId: "agent_sdk",
|
|
text: "finish the task",
|
|
messageId: "run-message-sdk",
|
|
activeTurnBehavior: "interrupt",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "send_agent_message_response",
|
|
payload: {
|
|
requestId: runSendRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
accepted: true,
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
const waitRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(waitRequest).toMatchObject({
|
|
type: "wait_for_finish_request",
|
|
agentId: "agent_sdk",
|
|
timeoutMs: 30_000,
|
|
});
|
|
const finishedAgent = createAgent({ title: "Finished" });
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "wait_for_finish_response",
|
|
payload: {
|
|
requestId: waitRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
status: "idle",
|
|
final: finishedAgent,
|
|
error: null,
|
|
lastMessage: "DONE",
|
|
},
|
|
}),
|
|
);
|
|
await expect(runPromise).resolves.toMatchObject({ status: "idle", lastMessage: "DONE" });
|
|
expect(agent.current()).toEqual(finishedAgent);
|
|
|
|
const defaultWaitPromise = agent.waitForFinish();
|
|
const defaultWaitRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(defaultWaitRequest).toMatchObject({
|
|
type: "wait_for_finish_request",
|
|
agentId: "agent_sdk",
|
|
timeoutMs: 10 * 60_000,
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "wait_for_finish_response",
|
|
payload: {
|
|
requestId: defaultWaitRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
status: "idle",
|
|
final: finishedAgent,
|
|
error: null,
|
|
lastMessage: "DONE",
|
|
},
|
|
}),
|
|
);
|
|
await expect(defaultWaitPromise).resolves.toMatchObject({ status: "idle" });
|
|
|
|
const timelineAgent = createAgent({ title: "Timeline" });
|
|
const timelinePromise = agent.timeline.refetch({ limit: 5 });
|
|
const timelineRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(timelineRequest).toMatchObject({
|
|
type: "fetch_agent_timeline_request",
|
|
agentId: "agent_sdk",
|
|
limit: 5,
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "fetch_agent_timeline_response",
|
|
payload: {
|
|
requestId: timelineRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
agent: timelineAgent,
|
|
direction: "tail",
|
|
projection: "projected",
|
|
epoch: "epoch-sdk",
|
|
reset: false,
|
|
staleCursor: false,
|
|
gap: false,
|
|
window: {
|
|
minSeq: 0,
|
|
maxSeq: 0,
|
|
nextSeq: 0,
|
|
},
|
|
startCursor: null,
|
|
endCursor: null,
|
|
hasOlder: false,
|
|
hasNewer: false,
|
|
entries: [],
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
await timelinePromise;
|
|
expect(agent.current()).toEqual(timelineAgent);
|
|
|
|
const appendPromise = agent.timeline.append({
|
|
type: "plugin",
|
|
id: "review-1",
|
|
kind: "review",
|
|
version: 1,
|
|
data: { status: "running" },
|
|
});
|
|
const appendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(appendRequest).toMatchObject({
|
|
type: "agent.timeline.append.request",
|
|
agentId: "agent_sdk",
|
|
item: { id: "review-1", kind: "review" },
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "agent.timeline.append.response",
|
|
payload: { requestId: appendRequest.requestId, seq: 8, epoch: "epoch-sdk" },
|
|
}),
|
|
);
|
|
await expect(appendPromise).resolves.toEqual({ seq: 8, epoch: "epoch-sdk" });
|
|
|
|
const archivePromise = agent.archive();
|
|
const archiveRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(archiveRequest).toMatchObject({
|
|
type: "archive_agent_request",
|
|
agentId: "agent_sdk",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "agent_archived",
|
|
payload: {
|
|
requestId: archiveRequest.requestId,
|
|
agentId: "agent_sdk",
|
|
archivedAt: "2026-05-16T01:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(archivePromise).resolves.toEqual({
|
|
archivedAt: "2026-05-16T01:00:00.000Z",
|
|
});
|
|
expect(agent.current()?.archivedAt).toBe("2026-05-16T01:00:00.000Z");
|
|
expect(agent.archivedAt).toBe("2026-05-16T01:00:00.000Z");
|
|
|
|
unsubscribe();
|
|
await client.close();
|
|
});
|
|
|
|
test("agent handles list the session's own commands through the existing daemon RPC", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const agent = client.agents.ref("agent_sdk");
|
|
|
|
const commandsPromise = agent.commands({ requestId: "agent-commands-request" });
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(request).toMatchObject({
|
|
type: "list_commands_request",
|
|
agentId: "agent_sdk",
|
|
requestId: "agent-commands-request",
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "list_commands_response",
|
|
payload: {
|
|
requestId: "agent-commands-request",
|
|
agentId: "agent_sdk",
|
|
commands: [
|
|
{
|
|
name: "brainstorming",
|
|
description: "Turn an idea into a design",
|
|
argumentHint: "[topic]",
|
|
kind: "skill",
|
|
},
|
|
{
|
|
name: "usage",
|
|
description: "Show usage",
|
|
argumentHint: "",
|
|
kind: "command",
|
|
},
|
|
],
|
|
error: null,
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(commandsPromise).resolves.toMatchObject({
|
|
agentId: "agent_sdk",
|
|
error: null,
|
|
commands: [
|
|
{ name: "brainstorming", kind: "skill" },
|
|
{ name: "usage", kind: "command" },
|
|
],
|
|
});
|
|
await client.close();
|
|
});
|
|
|
|
test("agent handles expose the observed snapshot through readonly properties", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const agent = client.agents.ref("agent_sdk");
|
|
|
|
expect(agent.capabilities).toBeNull();
|
|
expect(agent.availableModes).toBeNull();
|
|
expect(agent.pendingPermissions).toBeNull();
|
|
expect(agent.activeTurn).toBeNull();
|
|
expect(agent.lastUsage).toBeNull();
|
|
expect(agent.lastError).toBeNull();
|
|
expect(agent.features).toBeNull();
|
|
expect(agent.runtimeInfo).toBeNull();
|
|
expect(agent.archivedAt).toBeNull();
|
|
|
|
const observedAgent = createAgent({
|
|
capabilities: {
|
|
supportsStreaming: false,
|
|
supportsSessionPersistence: false,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: false,
|
|
supportsReasoningStream: false,
|
|
supportsRewindBoth: true,
|
|
supportsRewindConversation: true,
|
|
supportsRewindFiles: true,
|
|
supportsToolInvocations: false,
|
|
},
|
|
availableModes: [{ id: "plan", label: "Plan" }],
|
|
pendingPermissions: [{ id: "permission-sdk", provider: "codex", name: "shell", kind: "tool" }],
|
|
activeTurn: { turnId: "turn-sdk", startedAt: "2026-05-16T00:05:00.000Z" },
|
|
lastUsage: { inputTokens: 120, outputTokens: 45, totalCostUsd: 0.02 },
|
|
lastError: "provider exited",
|
|
features: [{ type: "toggle", id: "web-search", label: "Web search", value: true }],
|
|
runtimeInfo: { provider: "codex", sessionId: "session-sdk", model: "gpt-5.4" },
|
|
archivedAt: "2026-05-16T02:00:00.000Z",
|
|
});
|
|
await observeAgents(client, ws);
|
|
const unsubscribe = agent.subscribe(() => {});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "agent_update",
|
|
payload: {
|
|
subscriptionId: "agents-sdk",
|
|
kind: "upsert",
|
|
agent: observedAgent,
|
|
project: null,
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(agent.capabilities).toEqual(observedAgent.capabilities);
|
|
expect(agent.availableModes).toEqual([{ id: "plan", label: "Plan" }]);
|
|
expect(agent.pendingPermissions).toEqual([
|
|
{ id: "permission-sdk", provider: "codex", name: "shell", kind: "tool" },
|
|
]);
|
|
expect(agent.activeTurn).toEqual({
|
|
turnId: "turn-sdk",
|
|
startedAt: "2026-05-16T00:05:00.000Z",
|
|
});
|
|
expect(agent.lastUsage).toEqual({ inputTokens: 120, outputTokens: 45, totalCostUsd: 0.02 });
|
|
expect(agent.lastError).toBe("provider exited");
|
|
expect(agent.features).toEqual([
|
|
{ type: "toggle", id: "web-search", label: "Web search", value: true },
|
|
]);
|
|
expect(agent.runtimeInfo).toEqual({
|
|
provider: "codex",
|
|
sessionId: "session-sdk",
|
|
model: "gpt-5.4",
|
|
});
|
|
expect(agent.archivedAt).toBe("2026-05-16T02:00:00.000Z");
|
|
|
|
unsubscribe();
|
|
await client.close();
|
|
});
|
|
|
|
test("provider actions delegate to existing provider RPCs and local snapshot updates", async () => {
|
|
const { client, ws } = await connectClient();
|
|
|
|
const modelsPromise = client.providers.listModels("codex", {
|
|
cwd: "/repo/sdk",
|
|
requestId: "provider-models-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "list_provider_models_request",
|
|
requestId: "provider-models-request",
|
|
provider: "codex",
|
|
cwd: "/repo/sdk",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "list_provider_models_response",
|
|
payload: {
|
|
requestId: "provider-models-request",
|
|
provider: "codex",
|
|
models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4" }],
|
|
error: null,
|
|
fetchedAt: "2026-05-16T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(modelsPromise).resolves.toMatchObject({
|
|
provider: "codex",
|
|
models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4" }],
|
|
});
|
|
|
|
const modesPromise = client.providers.listModes("codex", {
|
|
cwd: "/repo/sdk",
|
|
requestId: "provider-modes-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "list_provider_modes_request",
|
|
requestId: "provider-modes-request",
|
|
provider: "codex",
|
|
cwd: "/repo/sdk",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "list_provider_modes_response",
|
|
payload: {
|
|
requestId: "provider-modes-request",
|
|
provider: "codex",
|
|
modes: [{ id: "full-access", label: "Full Access" }],
|
|
error: null,
|
|
fetchedAt: "2026-05-16T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(modesPromise).resolves.toMatchObject({
|
|
provider: "codex",
|
|
modes: [{ id: "full-access", label: "Full Access" }],
|
|
});
|
|
|
|
const featuresPromise = client.providers.listFeatures(
|
|
{
|
|
provider: "codex/gpt-5.4",
|
|
cwd: "/repo/sdk",
|
|
modeId: "full-access",
|
|
thinkingOptionId: "high",
|
|
featureValues: { webSearch: true },
|
|
},
|
|
{ requestId: "provider-features-request" },
|
|
);
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "list_provider_features_request",
|
|
requestId: "provider-features-request",
|
|
draftConfig: {
|
|
provider: "codex",
|
|
cwd: "/repo/sdk",
|
|
modeId: "full-access",
|
|
model: "gpt-5.4",
|
|
thinkingOptionId: "high",
|
|
featureValues: { webSearch: true },
|
|
},
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "list_provider_features_response",
|
|
payload: {
|
|
requestId: "provider-features-request",
|
|
provider: "codex",
|
|
features: [{ type: "toggle", id: "webSearch", label: "Web Search", value: true }],
|
|
error: null,
|
|
fetchedAt: "2026-05-16T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(featuresPromise).resolves.toMatchObject({
|
|
provider: "codex",
|
|
features: [{ type: "toggle", id: "webSearch", label: "Web Search", value: true }],
|
|
});
|
|
|
|
const availablePromise = client.providers.listAvailable({
|
|
requestId: "providers-available-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "list_available_providers_request",
|
|
requestId: "providers-available-request",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "list_available_providers_response",
|
|
payload: {
|
|
requestId: "providers-available-request",
|
|
providers: [{ provider: "codex", available: true, error: null }],
|
|
error: null,
|
|
fetchedAt: "2026-05-16T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(availablePromise).resolves.toMatchObject({
|
|
providers: [{ provider: "codex", available: true, error: null }],
|
|
});
|
|
|
|
const snapshotPromise = client.providers.snapshot({
|
|
cwd: "/repo/sdk",
|
|
requestId: "providers-snapshot-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "get_providers_snapshot_request",
|
|
requestId: "providers-snapshot-request",
|
|
cwd: "/repo/sdk",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "get_providers_snapshot_response",
|
|
payload: {
|
|
requestId: "providers-snapshot-request",
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(snapshotPromise).resolves.toMatchObject({
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
});
|
|
|
|
const readyPromise = client.providers.waitForReady({ cwd: "/repo/sdk", timeoutMs: 5_000 });
|
|
acknowledgeObservation(ws, "provider-wait");
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
const readyRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(readyRequest).toMatchObject({
|
|
type: "get_providers_snapshot_request",
|
|
cwd: "/repo/sdk",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "get_providers_snapshot_response",
|
|
payload: {
|
|
requestId: readyRequest.requestId,
|
|
cwd: "/repo/sdk",
|
|
entries: [{ provider: "codex", status: "loading", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "providers_snapshot_update",
|
|
payload: {
|
|
subscriptionId: "provider-wait",
|
|
cwd: "/repo/other",
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:30.000Z",
|
|
},
|
|
}),
|
|
);
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "providers_snapshot_update",
|
|
payload: {
|
|
subscriptionId: "provider-wait",
|
|
cwd: "/repo/sdk",
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:01.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(readyPromise).resolves.toMatchObject({
|
|
requestId: readyRequest.requestId,
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:01.000Z",
|
|
});
|
|
|
|
const canonicalReadyPromise = client.providers.waitForReady({
|
|
cwd: "/repo/./sdk",
|
|
timeoutMs: 5_000,
|
|
});
|
|
acknowledgeObservation(ws, "provider-canonical-wait");
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
const canonicalReadyRequest = parseSentSessionMessage(ws.sent.at(-1));
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "providers_snapshot_update",
|
|
payload: {
|
|
subscriptionId: "provider-canonical-wait",
|
|
cwd: "/repo/sdk",
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:02.000Z",
|
|
},
|
|
}),
|
|
);
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "get_providers_snapshot_response",
|
|
payload: {
|
|
requestId: canonicalReadyRequest.requestId,
|
|
cwd: "/repo/sdk",
|
|
entries: [{ provider: "codex", status: "loading", enabled: true }],
|
|
generatedAt: "2026-05-16T00:00:01.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(canonicalReadyPromise).resolves.toMatchObject({
|
|
requestId: canonicalReadyRequest.requestId,
|
|
cwd: "/repo/sdk",
|
|
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
|
});
|
|
|
|
const refreshPromise = client.providers.refresh({
|
|
cwd: "/repo/sdk",
|
|
providers: ["codex"],
|
|
requestId: "providers-refresh-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "refresh_providers_snapshot_request",
|
|
requestId: "providers-refresh-request",
|
|
cwd: "/repo/sdk",
|
|
providers: ["codex"],
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "refresh_providers_snapshot_response",
|
|
payload: {
|
|
requestId: "providers-refresh-request",
|
|
acknowledged: true,
|
|
},
|
|
}),
|
|
);
|
|
await expect(refreshPromise).resolves.toEqual({
|
|
requestId: "providers-refresh-request",
|
|
acknowledged: true,
|
|
});
|
|
|
|
const diagnosticPromise = client.providers.diagnostic("codex", {
|
|
requestId: "provider-diagnostic-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "provider_diagnostic_request",
|
|
requestId: "provider-diagnostic-request",
|
|
provider: "codex",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "provider_diagnostic_response",
|
|
payload: {
|
|
requestId: "provider-diagnostic-request",
|
|
provider: "codex",
|
|
diagnostic: "Codex is ready.",
|
|
},
|
|
}),
|
|
);
|
|
await expect(diagnosticPromise).resolves.toEqual({
|
|
requestId: "provider-diagnostic-request",
|
|
provider: "codex",
|
|
diagnostic: "Codex is ready.",
|
|
});
|
|
const usagePromise = client.providers.listUsage({
|
|
requestId: "provider-usage-request",
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "provider.usage.list.request",
|
|
requestId: "provider-usage-request",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "provider.usage.list.response",
|
|
payload: {
|
|
requestId: "provider-usage-request",
|
|
fetchedAt: "2026-05-16T00:10:00.000Z",
|
|
providers: [
|
|
{
|
|
providerId: "codex",
|
|
displayName: "Codex",
|
|
status: "available",
|
|
planLabel: "pro",
|
|
windows: [
|
|
{
|
|
id: "weekly",
|
|
label: "Weekly limit",
|
|
usedPct: 25,
|
|
remainingPct: 75,
|
|
resetsAt: "2026-05-20T00:00:00.000Z",
|
|
tone: "ok",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
await expect(usagePromise).resolves.toEqual({
|
|
requestId: "provider-usage-request",
|
|
fetchedAt: "2026-05-16T00:10:00.000Z",
|
|
providers: [
|
|
{
|
|
providerId: "codex",
|
|
displayName: "Codex",
|
|
status: "available",
|
|
planLabel: "pro",
|
|
windows: [
|
|
{
|
|
id: "weekly",
|
|
label: "Weekly limit",
|
|
usedPct: 25,
|
|
remainingPct: 75,
|
|
resetsAt: "2026-05-20T00:00:00.000Z",
|
|
tone: "ok",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const snapshotUpdates: string[] = [];
|
|
const snapshotModelDefaults: Array<string | undefined> = [];
|
|
const unsubscribe = client.providers.subscribe((update) => {
|
|
snapshotUpdates.push(update.generatedAt);
|
|
snapshotModelDefaults.push(update.entries[0]?.models?.[0]?.defaultThinkingOptionId);
|
|
});
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "session.events.set_subscription.request",
|
|
events: ["providers_snapshot_update"],
|
|
});
|
|
acknowledgeObservation(ws, "provider-updates");
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "providers_snapshot_update",
|
|
payload: {
|
|
subscriptionId: "provider-updates",
|
|
cwd: "/repo/sdk",
|
|
entries: [
|
|
{
|
|
provider: "codex",
|
|
status: "ready",
|
|
enabled: true,
|
|
models: [
|
|
{
|
|
provider: "codex",
|
|
id: "gpt-5.5",
|
|
label: "GPT-5.5",
|
|
thinkingOptions: [{ id: "high", label: "High", isDefault: true }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
generatedAt: "2026-05-16T01:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
expect(snapshotUpdates).toEqual(["2026-05-16T01:00:00.000Z"]);
|
|
expect(snapshotModelDefaults).toEqual(["high"]);
|
|
|
|
unsubscribe();
|
|
await client.close();
|
|
});
|
|
|
|
test("waitForReady reads an older host on the existing connection", async () => {
|
|
const { client, ws } = await connectClient({});
|
|
const waiting = client.providers.waitForReady({ cwd: "/repo/./sdk" });
|
|
await expect
|
|
.poll(() => parseSentSessionMessage(ws.sent.at(-1)).type)
|
|
.toBe("get_providers_snapshot_request");
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "get_providers_snapshot_response",
|
|
payload: {
|
|
requestId: request.requestId,
|
|
entries: [],
|
|
generatedAt: "2026-09-11T00:00:00.000Z",
|
|
},
|
|
}),
|
|
);
|
|
await expect(waiting).resolves.toMatchObject({ entries: [] });
|
|
expect(FakeWebSocket.instances).toHaveLength(1);
|
|
await client.close();
|
|
});
|
|
|
|
test("provider usage requires the advertised host capability", async () => {
|
|
const { client, ws } = await connectClient({});
|
|
const sentBeforeUsage = ws.sent.length;
|
|
|
|
await expect(client.providers.listUsage()).rejects.toThrow(
|
|
"Update the host to list provider usage.",
|
|
);
|
|
expect(ws.sent).toHaveLength(sentBeforeUsage);
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("config actions delegate to existing daemon config RPCs", async () => {
|
|
const { client, ws } = await connectClient();
|
|
|
|
const getPromise = client.config.get("config-get-request");
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "get_daemon_config_request",
|
|
requestId: "config-get-request",
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "get_daemon_config_response",
|
|
payload: {
|
|
requestId: "config-get-request",
|
|
config: {
|
|
mcp: { injectIntoAgents: true },
|
|
providers: {},
|
|
autoArchiveAfterMerge: false,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
await expect(getPromise).resolves.toEqual({
|
|
requestId: "config-get-request",
|
|
config: {
|
|
mcp: { injectIntoAgents: true },
|
|
providers: {},
|
|
browserTools: { enabled: false },
|
|
metadataGeneration: { providers: [] },
|
|
autoArchiveAfterMerge: false,
|
|
enableTerminalAgentHooks: false,
|
|
appendSystemPrompt: "",
|
|
},
|
|
});
|
|
|
|
const patchPromise = client.config.patch(
|
|
{
|
|
providers: {
|
|
codex: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
},
|
|
"config-patch-request",
|
|
);
|
|
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
|
type: "set_daemon_config_request",
|
|
requestId: "config-patch-request",
|
|
config: {
|
|
providers: {
|
|
codex: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "set_daemon_config_response",
|
|
payload: {
|
|
requestId: "config-patch-request",
|
|
config: {
|
|
mcp: { injectIntoAgents: true },
|
|
providers: {
|
|
codex: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
autoArchiveAfterMerge: false,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
await expect(patchPromise).resolves.toEqual({
|
|
requestId: "config-patch-request",
|
|
config: {
|
|
mcp: { injectIntoAgents: true },
|
|
providers: {
|
|
codex: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
browserTools: { enabled: false },
|
|
metadataGeneration: { providers: [] },
|
|
autoArchiveAfterMerge: false,
|
|
enableTerminalAgentHooks: false,
|
|
appendSystemPrompt: "",
|
|
},
|
|
});
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("agent config maps provider/model and provider-native options to the daemon", async () => {
|
|
const { client, ws } = await connectClient();
|
|
const createdAgent = createAgent({
|
|
model: "gpt-5.4",
|
|
currentModeId: "full-access",
|
|
});
|
|
const createPromise = client.agents.create({
|
|
config: {
|
|
provider: "codex/gpt-5.4",
|
|
modeId: "full-access",
|
|
thinkingOptionId: "high",
|
|
featureValues: { webSearch: true },
|
|
options: {
|
|
approval_policy: "never",
|
|
sandbox_mode: "workspace-write",
|
|
sandbox_workspace_write: {
|
|
writable_roots: ["/repo/sdk"],
|
|
network_access: false,
|
|
},
|
|
},
|
|
},
|
|
cwd: "/repo/sdk",
|
|
prompt: "use configured provider",
|
|
});
|
|
const request = parseSentSessionMessage(ws.sent.at(-1));
|
|
expect(request).toMatchObject({
|
|
type: "create_agent_request",
|
|
config: {
|
|
provider: "codex",
|
|
cwd: "/repo/sdk",
|
|
model: "gpt-5.4",
|
|
modeId: "full-access",
|
|
thinkingOptionId: "high",
|
|
featureValues: { webSearch: true },
|
|
providerOptions: {
|
|
approval_policy: "never",
|
|
sandbox_mode: "workspace-write",
|
|
sandbox_workspace_write: {
|
|
writable_roots: ["/repo/sdk"],
|
|
network_access: false,
|
|
},
|
|
},
|
|
},
|
|
initialPrompt: "use configured provider",
|
|
});
|
|
|
|
ws.message(
|
|
sessionMessage({
|
|
type: "status",
|
|
payload: {
|
|
status: "agent_created",
|
|
requestId: request.requestId,
|
|
agentId: "agent_sdk",
|
|
agent: createdAgent,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const agent = await createPromise;
|
|
expect(agent.current()).toEqual(createdAgent);
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("agent config requires provider/model syntax", async () => {
|
|
const { client } = await connectClient();
|
|
|
|
await expect(
|
|
client.agents.create({
|
|
config: { provider: "codex" },
|
|
cwd: "/repo/sdk",
|
|
}),
|
|
).rejects.toThrow('Expected config.provider in "provider/model" format');
|
|
|
|
await client.close();
|
|
});
|
|
|
|
test("canceled timeline handles and captured state are collectible while their API stays alive", async () => {
|
|
const source = `
|
|
import assert from "node:assert/strict";
|
|
import { createPaseoApi } from ${JSON.stringify(new URL("./index.ts", import.meta.url).href)};
|
|
import { DaemonClient } from ${JSON.stringify(new URL("./daemon-client.ts", import.meta.url).href)};
|
|
const drivers = [], apis = [];
|
|
let errors = 0;
|
|
async function batch(mode) {
|
|
const driver = new DaemonClient({ url: "ws://127.0.0.1:1/ws", clientId: mode, reconnect: { enabled: false } });
|
|
const scope = new AbortController();
|
|
const api = createPaseoApi(driver, { signal: scope.signal });
|
|
drivers.push(driver); apis.push(api);
|
|
const refs = [], ready = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const state = { bytes: new Uint8Array(64 * 1024) };
|
|
const handle = api.agents.ref("unconnected-agent").timeline.subscribe(update => {
|
|
if (update.event.type === "error") errors++;
|
|
return state.bytes[0];
|
|
});
|
|
refs.push({ handle: new WeakRef(handle), state: new WeakRef(state) });
|
|
ready.push(handle.ready.catch(() => {}));
|
|
if (mode === "callable") handle();
|
|
if (mode === "release") await handle.release();
|
|
}
|
|
if (mode === "scope-abort") scope.abort();
|
|
if (mode === "client-close") await driver.close();
|
|
if (mode !== "active") await Promise.all(ready);
|
|
return refs;
|
|
}
|
|
const groups = {};
|
|
for (const mode of ["callable", "release", "scope-abort", "client-close", "active"]) groups[mode] = await batch(mode);
|
|
async function collect() {
|
|
for (let i = 0; i < 12; i++) { await new Promise(r => setImmediate(r)); global.gc(); }
|
|
}
|
|
const count = refs => ({ handles: refs.filter(r => r.handle.deref()).length, states: refs.filter(r => r.state.deref()).length });
|
|
await collect();
|
|
const live = Object.fromEntries(Object.entries(groups).map(([mode, refs]) => [mode, count(refs)]));
|
|
console.log(JSON.stringify({ phase: "API alive", live, errors }));
|
|
for (const api of apis) await api.dispose();
|
|
await collect();
|
|
const disposed = Object.fromEntries(Object.entries(groups).map(([mode, refs]) => [mode, count(refs)]));
|
|
console.log(JSON.stringify({ phase: "API disposed", disposed }));
|
|
for (const driver of drivers) await driver.close();
|
|
for (const mode of ["callable", "release", "scope-abort", "client-close"]) assert.deepEqual(live[mode], { handles: 0, states: 0 }, mode);
|
|
assert.deepEqual(live.active, { handles: 100, states: 100 }, "active ownership is retained");
|
|
assert.equal(errors, 100, "client shutdown reports failure; explicit cancellation does not");
|
|
for (const result of Object.values(disposed)) assert.deepEqual(result, { handles: 0, states: 0 });
|
|
`;
|
|
const env = Object.fromEntries(
|
|
Object.entries(process.env).filter(([key]) => !/^(PASEO_|EXPO_|E2E_|AGENT_BROWSER_)/.test(key)),
|
|
);
|
|
const result = await promisify(execFile)(
|
|
process.execPath,
|
|
["--expose-gc", "--import", "tsx", "--input-type=module", "-e", source],
|
|
{ env, timeout: 15000 },
|
|
);
|
|
expect(result.stdout).toContain('"phase":"API alive"');
|
|
}, 20000);
|