Files
paseo/packages/client/src/daemon-client.test.ts
T
HelloThereEveryone 74c8172f8b Wait for a plugin reload to finish instead of failing at the request deadline (#6306)
* Wait for plugin reload completion instead of the request deadline

The daemon answers plugin.reload.request only once the plugin is running
again or has failed, and a reload queues behind other plugin lifecycle
work such as an install with a preparation command. The shared client
applied its sixty-second session RPC deadline to that request, so a slow
reload failed with "Timeout waiting for message" while the daemon kept
loading the plugin, inviting a reload the daemon was still running.

Reload now waits for the daemon's own answer. Enable and disable keep the
session deadline, and a real startup failure still reaches the caller.

* Cover a slow plugin reload end to end

The CLI fixture now holds its own git build on a gate file, starts a
reload of an installed plugin behind that queued install, observes the
reload request at the proxy, keeps the gate closed past the client's
former one-minute deadline, and then asserts the reload's real result:
CLI exit 0, the plugin running, and exactly one reactivation. Listing the
plugin during the wait stays readable and does not restart it. A plugin
that cannot start still fails its reload with the daemon's error.

The request proxy that recorded install sources becomes a small observer
that records any session request, so the wait is measured from the
request boundary rather than from process start.

Verified: npx tsx packages/cli/tests/e2e/plugin-lifecycle.test.ts exits 0
in 135 s; the same fixture without the new scenario takes 66 s.

* Document how a reload reports its result

The CLI docs now say `paseo plugin reload` waits for the daemon to stop
and start the plugin, that cancelling the command or losing the
connection does not stop the daemon's work, and that `plugin ls` shows
the previous state until the reload finishes.

The plugin reference records the lifecycle fact behind it: a reload
queues behind the daemon's other plugin work, the daemon answers when
the plugin is running again or has failed, and status reads, Settings and
the logs are not the reload's outcome.

* Cover the Settings reload wait end to end

Adds a browser scenario for the ticket's user-facing half: with a gated
git install holding the daemon's plugin lifecycle queue, the row's Reload
goes out, the test keeps the gate closed past sixty seconds, asserts the
row stays pending with no error feedback, then releases the build and
asserts the daemon's plugin.reload.response and the app's "Reloaded
e2e-plugin" result.

The scenario turns plugins on through the daemon instead of the global
switch, whose transient "Plugins enabled" confirmation is not its
subject. The wait is measured from the reload request frame, which is an
outbound frame, not from the response stream.

Verified: npx playwright test --project=browser
e2e/browser/plugin-management.spec.ts -g "keeps Reload pending past a
minute" -> 1 passed (1.5m); plugin-reload-pending.png,
plugin-reload-past-deadline.png and plugin-reload-completed.png show the
pending row, the surface past the former deadline with no error, and the
finished reload.

* Correct the status guidance for a cancelled reload

Review finding: the note said a listing keeps the plugin's previous
state until the reload finishes, but that only holds while the reload is
still queued. Once it executes, the daemon removes the plugin from the
runtime catalog before stopping it (plugins/runtime.ts:377-382) and
resolvePluginStatus maps an enabled plugin that is not running to
"failed" (plugins/index.ts:71-78), so a listing read in that window
shows failed for a healthy reload.

Both passages now separate the two windows and say that neither a
listing nor the logs report the outcome of the outstanding reload.
The interim "failed" itself is TUC-1401.

* Harden the reload coverage against failures and lost connections

Review findings folded into the tests:

The Settings scenario releases its gated build in a finally block. Before
this, a failing assertion left the build polling forever, the daemon's
plugin lifecycle queue stayed blocked, and the fixture teardown's own
removePlugin timed out behind it. It also asserts the row's actions are
enabled again once the reload settles, next to the result feedback.

The connection-close regression now reconnects and asserts nothing
resends the lost reload; the no-replay invariant already covered for
legacy requests now holds for a pending reload too.

Verified: npx vitest run packages/client/src/daemon-client.test.ts -> 161
passed. npx playwright test --project=browser
e2e/browser/plugin-management.spec.ts -g "keeps Reload pending past a
minute" -> 1 passed (1.4m).

* Let the gated build finish before its files go

Review finding on packages/cli/tests/e2e/plugin-lifecycle.test.ts:450:
the finally block released the gate and deleted the gate directory
without waiting for the install and the reload it had started. The build
polls for the gate file, so cleanup could delete the release before the
child saw it and leave the build waiting until daemon shutdown.

The fixture now keeps both promises, releases the gate, and awaits them
before deleting their files.

Verified: npx tsx packages/cli/tests/e2e/plugin-lifecycle.test.ts -> exit
0, 161 s.

* Read the reload scenario as intent, not transport code

Review finding: the new browser scenario kept frame parsing, selectors,
polling, deadline arithmetic, screenshots and gate cleanup in one body.
docs/testing.md asks for complex setup in reusable helpers.

The wire observation, the gated queue hold, the row-state assertions, the
deadline wait and the surface captures are helpers now, and the body
reads as the flow: open Settings, hold the queue, reload, still pending
past the deadline, release, read the daemon's result.

Verified: npx playwright test --project=browser
e2e/browser/plugin-management.spec.ts -g "keeps Reload pending past a
minute" -> 1 passed (1.4m).

* Keep the reload lifecycle fact in one document

Review finding: the cli.md paragraph repeated the queue and interim-state
explanation that the plugin reference already owns. CLAUDE.md: "One fact,
one doc. Every other mention is a link."

The reference keeps the full explanation; the CLI page states what the
command does and links there.

* Release the gated build when its setup fails

Review finding: holdPluginLifecycleQueue started the install, then
waited up to 30 seconds for the build's started marker. A preparation
that took longer made the helper throw before it returned its cleanup
handle and before the test entered its try block, so nothing wrote the
release file: the build polled forever, the daemon's plugin lifecycle
queue stayed blocked, and the fixture's own removePlugin timed out
behind it.

The helper now owns that failure path: it releases the gate and drains
the install before rethrowing, so a queue it hands out is always
settleable.

Verified on Linux (Chromium, Playwright):
- With the started marker suppressed in a temporary local patch, the run
  fails at the 30-second marker wait inside the helper and the teardown
  still completes (both attempts ~33 seconds, no removePlugin request
  timeout).
- With the marker in place, the scenario passes: 1 passed (1.4m).

* Keep an early install failure out of the unhandled path

Review finding: the marker wait in holdPluginLifecycleQueue ran before
the install promise had a rejection handler, so a preparation that failed
or a connection that closed before the build wrote its marker surfaced as
an unhandled rejection, which fails the run before the helper's own catch
can drain the queue.

The helper attaches a handler the moment the install starts and races the
marker wait against the install, so an early failure reaches the caller
as the daemon's own error, a build that never writes its marker still
fails as a 30-second marker timeout, and every path releases the gate and
drains the install.

Verified on Linux (Chromium, Playwright), patching the build command
locally for each failure branch:
- Early failure (build exits 1): fails in 4.2 s with the daemon's
  DaemonRpcError for plugin.source.install.request, no unhandled
  rejection, teardown completes.
- No marker (build waits for the gate): fails at the 30-second marker
  wait, no unhandled rejection, teardown completes with no removePlugin
  request timeout.
- Unpatched: 1 passed (1.5m).

* Keep a failed launch from ending the run before its cleanup
2026-10-17 10:57:10 +02:00

7846 lines
212 KiB
TypeScript

import { readFileSync } from "node:fs";
import { afterEach, expect, expectTypeOf, test, vi } from "vitest";
import { z } from "zod";
import {
DaemonClient,
supportsUsageReports,
type DaemonClientTrace,
type CreateAgentRequestOptions,
type DaemonTransport,
type Logger,
} from "./daemon-client";
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
import { BROWSER_AUTOMATION_COMMAND_NAMES } from "@getpaseo/protocol/browser-automation/rpc-schemas";
import {
decodeFileTransferFrame,
encodeFileTransferFrame,
FileTransferOpcode,
} from "@getpaseo/protocol/binary-frames/index";
import {
encodeTerminalSnapshotPayload,
encodeTerminalStreamFrame,
TerminalStreamOpcode,
} from "@getpaseo/protocol/terminal-stream-protocol";
expectTypeOf<"getGitDiff" extends keyof DaemonClient ? true : false>().toEqualTypeOf<false>();
expectTypeOf<
"getHighlightedDiff" extends keyof DaemonClient ? true : false
>().toEqualTypeOf<false>();
expectTypeOf<
"exploreFileSystem" extends keyof DaemonClient ? true : false
>().toEqualTypeOf<false>();
function createMockLogger() {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
}
interface TraceRecord {
phase: "begin" | "end";
name?: string;
args?: Record<string, string>;
}
function createTraceRecorder(): { trace: DaemonClientTrace; records: TraceRecord[] } {
const records: TraceRecord[] = [];
return {
trace: {
isEnabled: () => true,
beginSection: (name, args) => records.push({ phase: "begin", name, args }),
endSection: () => records.push({ phase: "end" }),
},
records,
};
}
function createMockTransport() {
const sent: Array<string | Uint8Array | ArrayBuffer> = [];
let onMessage: (data: unknown) => void = () => {};
let onOpen: () => void = () => {};
let onClose: (_event?: unknown) => void = () => {};
let onError: (_event?: unknown) => void = () => {};
let serverInfoOrdinal = 1;
const transport: DaemonTransport = {
send: (data) => {
sent.push(data);
if (typeof data !== "string") {
return;
}
const frame = JSON.parse(data) as { type?: string };
if (frame.type === "ping") {
onMessage(JSON.stringify({ type: "pong" }));
}
},
close: () => {},
onMessage: (handler) => {
onMessage = (data) => handler(data, typeof data !== "string");
return () => {};
},
onOpen: (handler) => {
onOpen = handler;
return () => {};
},
onClose: (handler) => {
onClose = handler;
return () => {};
},
onError: (handler) => {
onError = handler;
return () => {};
},
};
return {
transport,
sent,
triggerOpen: (options?: { preserveSent?: boolean; features?: Record<string, boolean> }) => {
onOpen();
if (!options?.preserveSent) {
// Ignore HELLO handshake payloads in assertions.
sent.length = 0;
}
onMessage(
JSON.stringify({
type: "session",
message: {
type: "status",
payload: {
status: "server_info",
serverId: `srv_test_${serverInfoOrdinal++}`,
hostname: null,
version: null,
features: options?.features ?? { ownedSubscriptions: true },
},
},
}),
);
},
triggerClose: (event?: unknown) => onClose(event),
triggerError: (event?: unknown) => onError(event),
triggerMessage: (data: unknown) => onMessage(data),
};
}
function wrapSessionMessage(message: unknown): string {
return JSON.stringify({
type: "session",
message,
});
}
function assertStr(data: string | Uint8Array | ArrayBuffer | undefined): string {
if (typeof data !== "string") throw new Error("Expected string frame");
return data;
}
function assertUint8Array(data: string | Uint8Array | ArrayBuffer | undefined): Uint8Array {
if (data instanceof Uint8Array) return data;
if (data instanceof ArrayBuffer) return new Uint8Array(data);
throw new Error("Expected binary frame");
}
function parseSentFrame(
data: string | Uint8Array | ArrayBuffer | undefined,
): Record<string, unknown> {
return z
.object({
type: z.literal("session"),
message: z.record(z.string(), z.unknown()),
})
.parse(JSON.parse(assertStr(data))).message;
}
function respondToScheduleRequest(
mock: ReturnType<typeof createMockTransport>,
request: Record<string, unknown>,
): void {
const responseType =
request.type === "schedule/create" ? "schedule/create/response" : "schedule/update/response";
mock.triggerMessage(
wrapSessionMessage({
type: responseType,
payload: {
requestId: request.requestId,
schedule: null,
error: null,
},
}),
);
}
const clients: DaemonClient[] = [];
afterEach(async () => {
await Promise.all(clients.map((client) => client.close()));
clients.length = 0;
vi.useRealTimers();
vi.unstubAllGlobals();
});
test("traces WebSocket frames, message types, and JSON parse duration", async () => {
const mock = createMockTransport();
const recorder = createTraceRecorder();
const client = new DaemonClient({
url: "ws://test",
clientId: "trace_unit_test",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
trace: recorder.trace,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ preserveSent: true });
await connectPromise;
expect(recorder.records).toEqual([
{
phase: "begin",
name: "paseo.ws.message.outbound",
args: { envelopeType: "hello", messageType: "hello" },
},
{ phase: "end" },
{
phase: "begin",
name: "paseo.ws.frame.outbound",
args: { kind: "text", size: expect.any(String) },
},
{ phase: "end" },
{
phase: "begin",
name: "paseo.ws.frame.inbound",
args: { kind: "text", size: expect.any(String) },
},
{
phase: "begin",
name: "paseo.ws.json.parse",
args: { size: expect.any(String) },
},
{ phase: "end" },
{
phase: "begin",
name: "paseo.ws.message.inbound",
args: { envelopeType: "session", messageType: "status" },
},
{ phase: "end" },
{ phase: "end" },
]);
});
test("does not infer browser automation capabilities from Electron runtime", async () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Paseo/0.1.89 Chrome/146 Electron/41.2.0 Safari/537.36",
});
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "electron_unit_test",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ preserveSent: true });
await connectPromise;
const hello = z
.object({
type: z.literal("hello"),
capabilities: z.record(z.string(), z.unknown()),
})
.parse(JSON.parse(assertStr(mock.sent[0])));
expect(hello.capabilities[CLIENT_CAPS.browserHost]).toBeUndefined();
expect(hello.capabilities[CLIENT_CAPS.selectiveAgentTimeline]).toBe(true);
});
test("advertises consumer-provided browser automation capabilities", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "browser_capability_unit_test",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
capabilities: {
[CLIENT_CAPS.browserHost]: {
supportedCommands: [...BROWSER_AUTOMATION_COMMAND_NAMES],
hostKind: "desktop app",
},
},
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ preserveSent: true });
await connectPromise;
const hello = z
.object({
type: z.literal("hello"),
capabilities: z.record(z.string(), z.unknown()),
})
.parse(JSON.parse(assertStr(mock.sent[0])));
expect(hello.capabilities[CLIENT_CAPS.browserHost]).toEqual({
supportedCommands: [...BROWSER_AUTOMATION_COMMAND_NAMES],
hostKind: "desktop app",
});
});
test.each([
{ receipts: false, structured: false },
{ receipts: true, structured: false },
{ receipts: false, structured: true },
{ receipts: true, structured: true },
])(
"legacy creation preserves the original payload with receipts=$receipts, structured=$structured",
async ({ receipts, structured }) => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "receipt-gate",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
transport.triggerOpen({ features: { agentRequestReceipts: receipts } });
await connecting;
const input: CreateAgentRequestOptions = {
config: {
provider: "codex",
cwd: "/project",
title: "Explicit title",
model: "gpt-5",
modeId: "full-access",
},
workspaceId: "workspace",
callerAgentId: "parent",
env: { CREATION_CONTEXT: "preserved" },
labels: { source: "test" },
idempotencyKey: "creation",
clientMessageId: "first-message",
initialPrompt: "Start this agent",
images: [{ data: "aGVsbG8=", mimeType: "image/png" }],
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Review this PR",
url: "https://github.com/getpaseo/paseo/pull/123",
},
],
...(structured ? { outputSchema: { type: "object" } } : {}),
};
const created = client.createAgent(input);
void created.catch(() => {});
const duplicate = client.createAgent(input);
void duplicate.catch(() => {});
expect(transport.sent).toHaveLength(1);
const request = parseSentFrame(transport.sent[0]);
expect(request).toMatchObject({
type: "create_agent_request",
config: input.config,
workspaceId: input.workspaceId,
callerAgentId: input.callerAgentId,
env: input.env,
labels: input.labels,
clientMessageId: input.clientMessageId,
initialPrompt: input.initialPrompt,
images: input.images,
attachments: input.attachments,
...(structured ? { outputSchema: input.outputSchema } : {}),
});
expect(request).not.toHaveProperty("idempotencyKey");
transport.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: request.requestId,
error: "Provider failed",
},
}),
);
await expect(created).rejects.toThrow("Provider failed");
await expect(duplicate).rejects.toThrow("Provider failed");
},
);
test.each([false, true])(
"legacy workspace creation preserves availability with receipt support=%s",
async (receipts) => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "legacy-workspace",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
transport.triggerOpen({ features: { workspaceRequestReceipts: receipts } });
await connecting;
const input = {
source: { kind: "directory" as const, path: "/project" },
idempotencyKey: "workspace",
};
const created = client.createWorkspace(input);
void created.catch(() => {});
const duplicate = client.createWorkspace(input);
void duplicate.catch(() => {});
expect(transport.sent).toHaveLength(1);
const request = parseSentFrame(transport.sent[0]);
expect(request).toMatchObject({ type: "workspace.create.request", source: input.source });
expect(request.idempotencyKey).toBe(receipts ? input.idempotencyKey : undefined);
transport.triggerMessage(
wrapSessionMessage({
type: "workspace.create.response",
payload: {
requestId: request.requestId,
workspace: null,
error: "Directory unavailable",
setupTerminalId: null,
},
}),
);
await expect(created).resolves.toMatchObject({ error: "Directory unavailable" });
await expect(duplicate).resolves.toMatchObject({ error: "Directory unavailable" });
},
);
test.each(["agent", "workspace"] as const)(
"a lost legacy %s response is not automatically replayed on reconnect",
async (kind) => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "legacy-disconnect",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
transport.triggerOpen({ features: {} });
await connecting;
const creation =
kind === "agent"
? client.createAgent({
provider: "codex",
cwd: "/project",
initialPrompt: "Start once",
idempotencyKey: "intent",
})
: client.createWorkspace({
source: { kind: "directory", path: "/project" },
idempotencyKey: "intent",
});
void creation.catch(() => {});
expect(transport.sent).toHaveLength(1);
transport.triggerClose({ code: 1006, reason: "Connection lost after dispatch" });
await expect(creation).rejects.toThrow();
const reconnecting = client.connect();
transport.triggerOpen({ features: {} });
await reconnecting;
expect(transport.sent).toEqual([]);
},
);
test("Hub management requires daemon support before dispatching requests", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "hub_feature_gate_unit_test",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
mock.triggerOpen();
await connecting;
await expect(client.getHubStatus()).rejects.toThrow(
"Update the host to use Hub relationship management.",
);
expect(mock.sent).toEqual([]);
});
test("timeline observation consumes broadcasts from a host without selective delivery", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "timeline_legacy",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
mock.triggerOpen({ features: {} });
await connecting;
const observation = client.observeTimeline(["agent"]);
await expect(observation.ready).resolves.toMatchObject({ agentIds: ["agent"] });
const updates: unknown[] = [];
observation.subscribe({ snapshot: () => {}, update: (message) => updates.push(message) });
mock.triggerMessage(
wrapSessionMessage({
type: "agent_stream",
payload: {
agentId: "agent",
timestamp: "2026-09-11T00:00:00Z",
event: { type: "turn_completed", provider: "codex" },
},
}),
);
expect(updates).toEqual([expect.objectContaining({ type: "agent_stream" })]);
await observation.release();
expect(mock.sent).toEqual([]);
});
test("normalizes legacy and dedicated agent attention notifications", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "attention_normalization",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connect = client.connect();
mock.triggerOpen();
await connect;
const notifications: unknown[] = [];
client.onAgentAttentionRequired((notification) => notifications.push(notification));
const payload = {
agentId: "agent-a",
reason: "finished",
timestamp: "2026-07-12T00:00:00.000Z",
shouldNotify: true,
} as const;
mock.triggerMessage(
wrapSessionMessage({
type: "agent_stream",
payload: {
agentId: payload.agentId,
timestamp: payload.timestamp,
event: { type: "attention_required", provider: "codex", ...payload },
},
}),
);
mock.triggerMessage(wrapSessionMessage({ type: "agent_attention_required", payload }));
expect(notifications).toEqual([payload, payload]);
});
const noopLogger: Logger = {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
type PongMode = { kind: "answer"; delayMs: number } | { kind: "silent" };
class FakeDaemon {
private onMessage: (data: unknown) => void = () => {};
private onOpen: () => void = () => {};
private onClose: (event?: unknown) => void = () => {};
private onError: (event?: unknown) => void = () => {};
private pongMode: PongMode = { kind: "answer", delayMs: 0 };
private withheldPongs = 0;
private pingsSentAt: number[] = [];
private closeEvents: Array<{ code?: number; reason?: string }> = [];
readonly transport: DaemonTransport = {
send: (data) => {
if (typeof data !== "string") {
return;
}
const frame = JSON.parse(data) as {
type?: string;
message?: { type?: string; requestId: string; clientSentAt: number };
};
const sessionPing =
frame.type === "session" && frame.message?.type === "ping" ? frame.message : null;
if (frame.type !== "ping" && !sessionPing) {
return;
}
this.pingsSentAt.push(performance.now());
if (this.withheldPongs > 0) {
this.withheldPongs -= 1;
return;
}
if (this.pongMode.kind === "silent") {
return;
}
const pong = sessionPing
? wrapSessionMessage({
type: "pong",
payload: { ...sessionPing, serverReceivedAt: Date.now(), serverSentAt: Date.now() },
})
: JSON.stringify({ type: "pong" });
if (this.pongMode.delayMs === 0) {
this.onMessage(pong);
return;
}
setTimeout(() => {
this.onMessage(pong);
}, this.pongMode.delayMs);
},
close: (code?: number, reason?: string) => {
this.closeEvents.push({ code, reason });
},
onMessage: (handler) => {
this.onMessage = (data) => handler(data, typeof data !== "string");
return () => {};
},
onOpen: (handler) => {
this.onOpen = handler;
return () => {};
},
onClose: (handler) => {
this.onClose = handler;
return () => {};
},
onError: (handler) => {
this.onError = handler;
return () => {};
},
};
openConnection(): void {
this.onOpen();
this.onMessage(
JSON.stringify({
type: "session",
message: {
type: "status",
payload: {
status: "server_info",
serverId: "srv_heartbeat_test",
hostname: null,
version: null,
},
},
}),
);
}
daemonAnswersPingsAfter(delay: "fast" | `${number}s`): void {
this.pongMode = {
kind: "answer",
delayMs: delay === "fast" ? 0 : Number.parseFloat(delay) * 1000,
};
}
daemonGoesSilent(): void {
this.pongMode = { kind: "silent" };
}
daemonWithholdsNextPongThenAnswersFast(): void {
this.withheldPongs += 1;
this.daemonAnswersPingsAfter("fast");
}
daemonClosesWith(reason: string): void {
this.onClose({ reason });
}
triggerError(event?: unknown): void {
this.onError(event);
}
pingTimestamps(): string[] {
return this.pingsSentAt.map((timestamp) => `${timestamp / 1000}s`);
}
teardownCount(): number {
return this.closeEvents.filter((event) => event.reason === "Liveness check timed out").length;
}
closesFromClient(): Array<{ code?: number; reason?: string }> {
return this.closeEvents;
}
}
class DaemonClientSession {
private readonly daemon = new FakeDaemon();
private readonly client: DaemonClient;
constructor() {
this.client = new DaemonClient({
url: "ws://test",
clientId: "clsk_heartbeat_test",
logger: noopLogger,
reconnect: { enabled: false },
transportFactory: () => this.daemon.transport,
});
clients.push(this.client);
}
async connect(): Promise<void> {
const connection = this.client.connect();
this.daemon.openConnection();
await connection;
}
async advance(ms: number): Promise<void> {
await vi.advanceTimersByTimeAsync(ms);
}
daemonAnswersPingsAfter(delay: "fast" | `${number}s`): void {
this.daemon.daemonAnswersPingsAfter(delay);
}
daemonGoesSilent(): void {
this.daemon.daemonGoesSilent();
}
daemonWithholdsNextPongThenAnswersFast(): void {
this.daemon.daemonWithholdsNextPongThenAnswersFast();
}
daemonClosesWith(reason: string): void {
this.daemon.daemonClosesWith(reason);
}
pingTimestamps(): string[] {
return this.daemon.pingTimestamps();
}
state(): ReturnType<DaemonClient["getConnectionState"]> {
return this.client.getConnectionState();
}
lastError(): string | null {
return this.client.lastError;
}
teardownCount(): number {
return this.daemon.teardownCount();
}
closesFromClient(): Array<{ code?: number; reason?: string }> {
return this.daemon.closesFromClient();
}
lastLivenessRttMs(): number | null {
return this.client.getLastLivenessRttMs();
}
measureLatency(input: { timeoutMs: number }): Promise<number> {
return this.client.measureLatency(input);
}
}
function useHeartbeatClock(): void {
vi.useFakeTimers({
toFake: ["Date", "setTimeout", "clearTimeout", "setInterval", "clearInterval", "performance"],
});
}
test("dedupes in-flight checkout status requests per agentId", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const p1 = client.getCheckoutStatus("/tmp/project");
const p2 = client.getCheckoutStatus("/tmp/project");
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
const response = {
type: "session",
message: {
type: "checkout_status_response",
payload: {
cwd: "/tmp/project",
error: null,
requestId: request.requestId,
isGit: false,
isPaseoOwnedWorktree: false,
repoRoot: null,
currentBranch: null,
isDirty: null,
baseRef: null,
aheadBehind: null,
aheadOfOrigin: null,
behindOfOrigin: null,
hasRemote: false,
remoteUrl: null,
},
},
};
mock.triggerMessage(JSON.stringify(response));
const [r1, r2] = await Promise.all([p1, p2]);
expect(r1).toMatchObject({
cwd: "/tmp/project",
requestId: request.requestId,
isGit: false,
});
expect(r2).toMatchObject({
cwd: "/tmp/project",
requestId: request.requestId,
isGit: false,
});
// After completion, a new call should issue a new request.
const p3 = client.getCheckoutStatus("/tmp/project");
expect(mock.sent).toHaveLength(2);
const request2 = parseSentFrame(mock.sent[1]);
mock.triggerMessage(
JSON.stringify({
...response,
message: {
...response.message,
payload: { ...response.message.payload, requestId: request2.requestId },
},
}),
);
await expect(p3).resolves.toMatchObject({
cwd: "/tmp/project",
requestId: request2.requestId,
isGit: false,
});
});
test("passes password as HTTP bearer header and WebSocket subprotocol", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const transportFactory = vi.fn(() => mock.transport);
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
password: "shared-secret",
logger,
reconnect: { enabled: false },
transportFactory,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
expect(transportFactory).toHaveBeenCalledWith({
url: "ws://test",
headers: { Authorization: "Bearer shared-secret" },
protocols: ["paseo.bearer.shared-secret"],
});
});
test("keeps relay upgrade credentials out of the socket request", async () => {
const mock = createMockTransport();
const requests: Array<{ url: string; headers?: Record<string, string>; protocols?: string[] }> =
[];
const client = new DaemonClient({
url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2",
clientId: "clsk_relay_auth_test",
password: "shared-secret",
authHeader: "Bearer shared-secret",
e2ee: { enabled: true, daemonPublicKeyB64: "daemon-public-key" },
reconnect: { enabled: false },
transportFactory: (request) => {
requests.push(request);
return mock.transport;
},
});
clients.push(client);
void client.connect();
await vi.waitFor(() => expect(requests).toHaveLength(1));
expect(requests[0]).toEqual({ url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2" });
});
test("refuses relay password auth without an encrypted hello", async () => {
const requests: unknown[] = [];
const client = new DaemonClient({
url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2",
clientId: "clsk_unencrypted_relay_test",
password: "shared-secret",
connectTimeoutMs: 50,
reconnect: { enabled: false },
transportFactory: (request) => {
requests.push(request);
return createMockTransport().transport;
},
});
clients.push(client);
await expect(client.connect()).rejects.toThrow("Relay credentials require E2EE");
expect(requests).toEqual([]);
});
test("stops reconnecting after a password rejection on an established connection", async () => {
const socket = createMockTransport();
let attempts = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_reconnect_auth_test",
password: "old-secret",
reconnect: { enabled: true, baseDelayMs: 1, maxDelayMs: 1 },
transportFactory: () => {
attempts += 1;
return socket.transport;
},
});
clients.push(client);
const connected = client.connect();
socket.triggerOpen();
await connected;
socket.triggerMessage(JSON.stringify({ type: "hello.rejected", reason: "incorrect_password" }));
socket.triggerClose({ code: 4003, reason: "Incorrect password" });
await new Promise((resolve) => setTimeout(resolve, 30));
expect(client.authFailureReason).toBe("incorrect_password");
expect(attempts).toBe(1);
});
test("sends a password containing spaces in hello without an invalid WebSocket subprotocol", async () => {
const mock = createMockTransport();
const transportFactory = vi.fn(() => mock.transport);
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
password: "two words",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory,
});
clients.push(client);
const connected = client.connect();
mock.triggerOpen({ preserveSent: true });
await connected;
expect(transportFactory).toHaveBeenCalledWith({
url: "ws://test",
headers: {},
});
expect(JSON.parse(assertStr(mock.sent[0]))).toMatchObject({
type: "hello",
auth: { kind: "password", password: "two words" },
});
});
test("uses a local credential over a saved password when the desktop bridge provides one", async () => {
const mock = createMockTransport();
const transportFactory = vi.fn(() => mock.transport);
const client = new DaemonClient({
url: "ws://test",
clientId: "desktop-managed",
password: "stale-password",
localCredential: async () => "current-local-token",
reconnect: { enabled: false },
transportFactory,
});
clients.push(client);
const connected = client.connect();
await vi.waitFor(() => expect(transportFactory).toHaveBeenCalled());
mock.triggerOpen({ preserveSent: true });
await connected;
expect(transportFactory).toHaveBeenCalledWith({ url: "ws://test", headers: {} });
expect(JSON.parse(assertStr(mock.sent[0]))).toMatchObject({
type: "hello",
auth: { kind: "localCredential", token: "current-local-token" },
});
});
test("uses the saved host password when the desktop bridge has no credential for the target", async () => {
const mock = createMockTransport();
const transportFactory = vi.fn(() => mock.transport);
const localCredential = vi.fn(async () => undefined);
const client = new DaemonClient({
url: "ws://remote-host",
clientId: "remote-saved-host",
password: "saved-password",
localCredential,
reconnect: { enabled: false },
transportFactory,
});
clients.push(client);
const connected = client.connect();
await vi.waitFor(() => expect(transportFactory).toHaveBeenCalled());
mock.triggerOpen({ preserveSent: true });
await connected;
expect(localCredential).toHaveBeenCalledOnce();
expect(JSON.parse(assertStr(mock.sent[0]))).toMatchObject({
type: "hello",
auth: { kind: "password", password: "saved-password" },
});
});
test("advertises client capabilities in hello", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
capabilities: {
browser_host: {
supportedCommands: ["list_tabs"],
hostKind: "desktop app",
},
},
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ preserveSent: true });
await connectPromise;
expect(mock.sent).toHaveLength(1);
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "hello",
clientId: "clsk_unit_test",
clientType: "cli",
protocolVersion: 1,
capabilities: {
all_providers: true,
selective_agent_timeline: true,
timeline_replacement_invalidation: true,
provider_snapshot_references: true,
explicit_event_subscriptions: true,
owned_subscriptions: true,
compact_provider_snapshots: true,
custom_mode_icons: true,
project_updates: true,
provider_subagents: true,
projected_subagent_timeline: true,
reasoning_merge_enum: true,
terminal_reflowable_snapshot: true,
timeline_notifications: true,
plugin_timeline_items: true,
workspace_setup_blocked: true,
hello_rejection: true,
browser_host: {
supportedCommands: ["list_tabs"],
hostKind: "desktop app",
},
},
});
});
test("allows callers to disable default client capabilities", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_capability_override_test",
reconnect: { enabled: false },
transportFactory: () => mock.transport,
capabilities: {
[CLIENT_CAPS.projectUpdates]: false,
},
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ preserveSent: true });
await connectPromise;
const hello = z
.object({
type: z.literal("hello"),
capabilities: z.record(z.string(), z.unknown()),
})
.parse(JSON.parse(assertStr(mock.sent[0])));
expect(hello.capabilities[CLIENT_CAPS.projectUpdates]).toBe(false);
});
test("sends new-agent run options when creating schedules", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.scheduleCreate({
requestId: "request-1",
prompt: "Run the task",
cadence: { type: "cron", expression: "* * * * *" },
target: {
type: "new-agent",
config: {
provider: "claude",
cwd: "/tmp/project",
thinkingOptionId: "think-hard",
archiveOnFinish: false,
isolation: "worktree",
},
},
});
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "schedule/create",
requestId: "request-1",
prompt: "Run the task",
cadence: { type: "cron", expression: "* * * * *" },
target: {
type: "new-agent",
config: {
provider: "claude",
cwd: "/tmp/project",
thinkingOptionId: "think-hard",
archiveOnFinish: false,
isolation: "worktree",
},
},
});
respondToScheduleRequest(mock, request);
await expect(createPromise).resolves.toEqual({
requestId: "request-1",
schedule: null,
error: null,
});
});
test("sends new-agent run options when updating schedules", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const updatePromise = client.scheduleUpdate({
id: "schedule-1",
requestId: "request-1",
newAgentConfig: {
thinkingOptionId: "think-hard",
archiveOnFinish: false,
isolation: "worktree",
},
});
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "schedule/update",
requestId: "request-1",
scheduleId: "schedule-1",
newAgentConfig: {
thinkingOptionId: "think-hard",
archiveOnFinish: false,
isolation: "worktree",
},
});
respondToScheduleRequest(mock, request);
await expect(updatePromise).resolves.toEqual({
requestId: "request-1",
schedule: null,
error: null,
});
});
test("sends typed browser automation execute responses", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
client.sendBrowserAutomationExecuteResponse({
type: "browser.automation.execute.response",
payload: {
requestId: "req-1",
ok: true,
result: { command: "list_tabs", tabs: [] },
},
});
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "browser.automation.execute.response",
payload: {
requestId: "req-1",
ok: true,
result: { command: "list_tabs", tabs: [] },
},
});
});
test("does not reconnect after close when ensureConnected is called", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
expect(client.getConnectionState().status).toBe("connected");
await client.close();
expect(client.getConnectionState().status).toBe("disposed");
client.ensureConnected();
expect(client.getConnectionState().status).toBe("disposed");
});
test("ensureConnected reconnects immediately without leaving the scheduled retry armed", async () => {
useHeartbeatClock();
try {
const first = createMockTransport();
const second = createMockTransport();
const third = createMockTransport();
const transports = [first, second, third];
let transportIndex = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_immediate_reconnect",
reconnect: { enabled: true, baseDelayMs: 1_500, maxDelayMs: 1_500 },
transportFactory: () => {
const transport = transports[transportIndex];
if (!transport) throw new Error("unexpected extra reconnect");
transportIndex += 1;
return transport.transport;
},
});
clients.push(client);
const initialConnect = client.connect();
first.triggerOpen();
await initialConnect;
first.triggerClose({ code: 1001, reason: "app resumed" });
expect(client.getConnectionState().status).toBe("disconnected");
client.ensureConnected();
expect(client.getConnectionState().status).toBe("connecting");
expect(transportIndex).toBe(2);
second.triggerOpen();
expect(client.getConnectionState().status).toBe("connected");
await vi.advanceTimersByTimeAsync(1_500);
expect(client.getConnectionState().status).toBe("connected");
expect(transportIndex).toBe(2);
} finally {
vi.useRealTimers();
}
});
test("connect during a scheduled retry reconnects immediately without leaving the retry armed", async () => {
useHeartbeatClock();
try {
const first = createMockTransport();
const second = createMockTransport();
const third = createMockTransport();
const transports = [first, second, third];
let transportIndex = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_explicit_reconnect",
reconnect: { enabled: true, baseDelayMs: 1_500, maxDelayMs: 1_500 },
transportFactory: () => {
const transport = transports[transportIndex];
if (!transport) throw new Error("unexpected extra reconnect");
transportIndex += 1;
return transport.transport;
},
});
clients.push(client);
const initialConnect = client.connect();
first.triggerOpen();
await initialConnect;
first.triggerClose({ code: 1001, reason: "daemon restarted" });
expect(client.getConnectionState().status).toBe("disconnected");
const reconnect = client.connect();
expect(client.getConnectionState().status).toBe("connecting");
expect(transportIndex).toBe(2);
second.triggerOpen();
await reconnect;
expect(client.getConnectionState().status).toBe("connected");
await vi.advanceTimersByTimeAsync(1_500);
expect(client.getConnectionState().status).toBe("connected");
expect(transportIndex).toBe(2);
} finally {
vi.useRealTimers();
}
});
test("disabling reconnect cancels a pending retry until explicitly resumed", async () => {
useHeartbeatClock();
try {
const first = createMockTransport();
const second = createMockTransport();
const transports = [first, second];
let transportIndex = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_paused_reconnect",
reconnect: { enabled: true, baseDelayMs: 1_500, maxDelayMs: 1_500 },
transportFactory: () => {
const transport = transports[transportIndex];
if (!transport) throw new Error("unexpected extra reconnect");
transportIndex += 1;
return transport.transport;
},
});
clients.push(client);
const initialConnect = client.connect();
first.triggerOpen();
await initialConnect;
first.triggerClose({ code: 1001, reason: "app backgrounded" });
client.setReconnectEnabled(false);
await vi.advanceTimersByTimeAsync(1_500);
expect(client.getConnectionState().status).toBe("disconnected");
expect(transportIndex).toBe(1);
client.setReconnectEnabled(true);
client.ensureConnected();
expect(client.getConnectionState().status).toBe("connecting");
expect(transportIndex).toBe(2);
} finally {
vi.useRealTimers();
}
});
test("keeps the transport connected when a session RPC ping times out", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
expect(client.getConnectionState().status).toBe("connected");
await expect(client.ping({ timeoutMs: 1 })).rejects.toThrow("Timeout waiting for message");
expect(client.getConnectionState().status).toBe("connected");
});
test("waits for the daemon to acknowledge push token revocation", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_push_revocation",
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { pushTokenRevocation: true } });
await connectPromise;
const revocation = client.unregisterPushToken("ExponentPushToken[test-device]");
const request = parseSentFrame(mock.sent.at(-1));
expect(request).toMatchObject({
type: "push.unregister.request",
token: "ExponentPushToken[test-device]",
});
mock.triggerMessage(
wrapSessionMessage({
type: "push.unregister.response",
payload: { requestId: request.requestId },
}),
);
await revocation;
});
test("bounds the wait for push token revocation", async () => {
useHeartbeatClock();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_push_revocation_timeout",
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { pushTokenRevocation: true } });
await connectPromise;
const revocation = client.unregisterPushToken("ExponentPushToken[test-device]");
const rejection = expect(revocation).rejects.toThrow("Timeout waiting for message (2000ms)");
await vi.advanceTimersByTimeAsync(1_999);
expect(client.getConnectionState().status).toBe("connected");
await vi.advanceTimersByTimeAsync(1);
await rejection;
expect(client.getConnectionState().status).toBe("connected");
});
test("defaults session RPC waiters to sixty seconds", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.fetchAgent({
agentId: "agent-1",
requestId: "req-agent-1",
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "fetch_agent_request",
requestId: "req-agent-1",
agentId: "agent-1",
});
await vi.advanceTimersByTimeAsync(59_999);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (60000ms)");
});
test("honors explicit fetchAgent timeout below the session RPC default", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.fetchAgent({
agentId: "agent-1",
requestId: "req-agent-1",
timeout: 5_000,
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "fetch_agent_request",
requestId: "req-agent-1",
agentId: "agent-1",
});
await vi.advanceTimersByTimeAsync(4_999);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (5000ms)");
});
test("preserves legacy fetchAgent id overload", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.fetchAgent("agent-1", "req-agent-legacy");
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "fetch_agent_request",
requestId: "req-agent-legacy",
agentId: "agent-1",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fetch_agent_response",
payload: {
requestId: "req-agent-legacy",
agent: null,
project: null,
error: "legacy fetch sentinel",
},
}),
);
await expect(responsePromise).rejects.toThrow("legacy fetch sentinel");
});
test("honors explicit fetchAgentTimeline timeout below the session RPC default", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.fetchAgentTimeline("agent-1", {
requestId: "req-timeline-1",
direction: "tail",
limit: 0,
projection: "projected",
timeout: 2_000,
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "fetch_agent_timeline_request",
requestId: "req-timeline-1",
agentId: "agent-1",
direction: "tail",
limit: 0,
projection: "projected",
});
await vi.advanceTimersByTimeAsync(1_999);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (2000ms)");
});
test("lists the full agent prompt index", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.listAgentTimelinePrompts("agent-1", {
requestId: "req-prompts-1",
});
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "agent.timeline.list_prompts.request",
requestId: "req-prompts-1",
agentId: "agent-1",
});
mock.triggerMessage(
wrapSessionMessage({
type: "agent.timeline.list_prompts.response",
payload: {
requestId: "req-prompts-1",
agentId: "agent-1",
epoch: "epoch-1",
prompts: [{ seq: 1, timestamp: "2026-01-01T00:00:00.000Z", preview: "First prompt" }],
error: null,
},
}),
);
await expect(responsePromise).resolves.toMatchObject({
epoch: "epoch-1",
prompts: [{ seq: 1, preview: "First prompt" }],
});
});
test("honors explicit fetchAgents timeout below the session RPC default", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.fetchAgents({
requestId: "req-agents-1",
scope: "active",
timeout: 1_200,
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "fetch_agents_request",
requestId: "req-agents-1",
scope: "active",
});
await vi.advanceTimersByTimeAsync(1_199);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1200ms)");
});
test("honors explicit shutdownServer timeout below the session RPC default", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.shutdownServer({
requestId: "req-shutdown-1",
timeout: 1_500,
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "shutdown_server_request",
requestId: "req-shutdown-1",
});
await vi.advanceTimersByTimeAsync(1_499);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)");
});
test("honors explicit getDaemonStatus timeout below the session RPC default", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { daemonStatusRpc: true } });
await connectPromise;
const responsePromise = client.getDaemonStatus({
requestId: "req-daemon-status-1",
timeout: 1_500,
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "daemon.get_status.request",
requestId: "req-daemon-status-1",
});
await vi.advanceTimersByTimeAsync(1_499);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)");
});
test("honors explicit getDaemonPairingOffer timeout below the session RPC default", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.getDaemonPairingOffer({
requestId: "req-pairing-offer-1",
timeout: 1_500,
});
let settled = false;
void responsePromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "daemon.get_pairing_offer.request",
requestId: "req-pairing-offer-1",
});
await vi.advanceTimersByTimeAsync(1_499);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)");
});
test("gates config reload on the daemon capability", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
await expect(client.reloadDaemonConfig("reload-old-host")).rejects.toThrow(
"Update the host to reload daemon configuration.",
);
expect(mock.sent).toEqual([]);
});
test("sends and parses daemon config reload", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { daemonConfigReload: true } });
await connectPromise;
const response = client.reloadDaemonConfig("reload-new-host");
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "daemon.config.reload.request",
requestId: "reload-new-host",
});
mock.triggerMessage(
wrapSessionMessage({
type: "daemon.config.reload.response",
payload: {
requestId: "reload-new-host",
appliedPaths: ["daemon.browserTools.enabled"],
restartRequiredPaths: ["daemon.listen"],
overrideControlledPaths: [],
},
}),
);
await expect(response).resolves.toEqual({
requestId: "reload-new-host",
appliedPaths: ["daemon.browserTools.enabled"],
restartRequiredPaths: ["daemon.listen"],
overrideControlledPaths: [],
});
});
test("gets a structured plugin log snapshot", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { pluginLogs: true } });
await connectPromise;
const response = client.getPluginLogs("example");
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({ type: "plugin.logs.get.request", pluginId: "example" });
mock.triggerMessage(
wrapSessionMessage({
type: "plugin.logs.get.response",
payload: {
requestId: request.requestId,
pluginId: "example",
entries: [
{
sequence: 3,
timestamp: "2026-08-16T12:00:00.000Z",
stream: "stdout",
message: "ready",
},
],
},
}),
);
await expect(response).resolves.toEqual([
{
sequence: 3,
timestamp: "2026-08-16T12:00:00.000Z",
stream: "stdout",
message: "ready",
},
]);
});
test("keeps a plugin reload pending past the session RPC deadline", async () => {
useHeartbeatClock();
try {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const reload = client.reloadPlugin("example");
let settled: "pending" | "resolved" | "rejected" = "pending";
void reload.then(
() => {
settled = "resolved";
return null;
},
() => {
settled = "rejected";
return null;
},
);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "plugin.reload.request",
requestId: expect.any(String),
pluginId: "example",
});
// A daemon that reloads for almost two minutes is still reloading, not a failed request.
await vi.advanceTimersByTimeAsync(114_034);
expect(settled).toBe("pending");
// Another plugin's reload finishing early must not settle this request.
mock.triggerMessage(
wrapSessionMessage({
type: "plugin.reload.response",
payload: {
requestId: "req-other-plugin",
plugin: { id: "other", path: "/plugins/other", enabled: true, status: "running" },
},
}),
);
await vi.advanceTimersByTimeAsync(0);
expect(settled).toBe("pending");
mock.triggerMessage(
wrapSessionMessage({
type: "plugin.reload.response",
payload: {
requestId: request.requestId,
plugin: { id: "example", path: "/plugins/example", enabled: true, status: "running" },
},
}),
);
await expect(reload).resolves.toEqual({
id: "example",
path: "/plugins/example",
enabled: true,
status: "running",
});
} finally {
vi.useRealTimers();
}
});
test("keeps enable and disable inside the session RPC deadline", async () => {
useHeartbeatClock();
try {
for (const action of ["enable", "disable"] as const) {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const pending =
action === "enable" ? client.enablePlugin("example") : client.disablePlugin("example");
let settled = false;
void pending.then(
() => {
settled = true;
return null;
},
() => {
settled = true;
return null;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: `plugin.${action}.request`,
requestId: expect.any(String),
pluginId: "example",
});
await vi.advanceTimersByTimeAsync(59_999);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(pending).rejects.toThrow("Timeout waiting for message (60000ms)");
}
} finally {
vi.useRealTimers();
}
});
test("reports a plugin reload failure that arrives after the session RPC deadline", async () => {
useHeartbeatClock();
try {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const reload = client.reloadPlugin("example");
const request = parseSentFrame(mock.sent[0]);
await vi.advanceTimersByTimeAsync(114_034);
mock.triggerMessage(
wrapSessionMessage({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: "plugin.reload.request",
error: "Request failed: Plugin failed to start: example",
code: "handler_error",
},
}),
);
await expect(reload).rejects.toThrow(
"Request failed: Plugin failed to start: example requestType=plugin.reload.request code=handler_error",
);
} finally {
vi.useRealTimers();
}
});
test("rejects a pending plugin reload when the connection closes", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const reload = client.reloadPlugin("example");
expect(parseSentFrame(mock.sent[0])).toMatchObject({ type: "plugin.reload.request" });
mock.triggerClose({ code: 1006, reason: "network lost" });
await expect(reload).rejects.toThrow(/network lost|disconnected|closed/i);
// Reconnecting must not resend the lost reload: only the caller decides to reload again.
const reconnecting = client.connect();
mock.triggerOpen();
await reconnecting;
expect(mock.sent).toEqual([]);
});
test("keeps waitForAgentUpsert initial fetch inside the requested deadline", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const waitPromise = client.waitForAgentUpsert("agent-1", () => false, 5_000);
let settled = false;
void waitPromise.then(
() => {
settled = true;
return undefined;
},
() => {
settled = true;
return undefined;
},
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "fetch_agent_request",
requestId: expect.any(String),
agentId: "agent-1",
});
await vi.advanceTimersByTimeAsync(4_999);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(waitPromise).rejects.toThrow("Timed out waiting for agent agent-1");
});
test("keeps default connect timeout shorter than session RPC waiters", async () => {
useHeartbeatClock();
try {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
let settled = false;
const pendingConnect = client.connect().then(
() => {
settled = true;
return { ok: true as const };
},
(error) => {
settled = true;
return { ok: false as const, error };
},
);
await vi.advanceTimersByTimeAsync(14_999);
expect(settled).toBe(false);
expect(client.getConnectionState().status).toBe("connecting");
await vi.advanceTimersByTimeAsync(1);
const result = await pendingConnect;
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBeInstanceOf(Error);
if (result.error instanceof Error) {
expect(result.error.message).toContain("Connection timed out");
}
}
expect(client.getConnectionState().status).toBe("disconnected");
} finally {
vi.useRealTimers();
}
});
test("foreground verification replaces a silently broken socket without waiting for heartbeats", async () => {
useHeartbeatClock();
const first = new FakeDaemon();
const second = new FakeDaemon();
first.daemonGoesSilent();
let attempts = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "foreground-recovery",
logger: noopLogger,
transportFactory: () => (++attempts === 1 ? first : second).transport,
});
clients.push(client);
const connection = client.connect();
first.openConnection();
await connection;
client.ensureConnected({ verify: true });
await vi.advanceTimersByTimeAsync(3_000);
expect(attempts).toBe(2);
second.openConnection();
expect(client.getConnectionState()).toEqual({ status: "connected" });
});
test("foreground verification preserves a healthy socket and deduplicates simultaneous checks", async () => {
useHeartbeatClock();
const daemon = new FakeDaemon();
daemon.daemonAnswersPingsAfter("0.1s");
const client = new DaemonClient({
url: "ws://test",
clientId: "healthy-resume",
logger: noopLogger,
transportFactory: () => daemon.transport,
});
clients.push(client);
const connection = client.connect();
daemon.openConnection();
await connection;
client.ensureConnected({ verify: true });
client.ensureConnected({ verify: true });
await vi.advanceTimersByTimeAsync(3_000);
expect(client.getConnectionState()).toEqual({ status: "connected" });
expect(daemon.pingTimestamps()).toEqual(["0s"]);
expect(daemon.closesFromClient()).toEqual([]);
});
test("an obsolete foreground probe cannot close a replacement connection", async () => {
useHeartbeatClock();
const first = new FakeDaemon();
const second = new FakeDaemon();
first.daemonGoesSilent();
let attempts = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "resume-race",
logger: noopLogger,
transportFactory: () => (++attempts === 1 ? first : second).transport,
});
clients.push(client);
const connection = client.connect();
first.openConnection();
await connection;
client.ensureConnected({ verify: true });
first.daemonClosesWith("network changed");
client.ensureConnected();
second.openConnection();
await vi.advanceTimersByTimeAsync(3_000);
expect(attempts).toBe(2);
expect(client.getConnectionState()).toEqual({ status: "connected" });
expect(second.closesFromClient()).toEqual([]);
});
test("stays online through ten minutes of pongs that arrive five seconds late", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("5.5s");
await session.connect();
await session.advance(10 * 60 * 1000);
expect(session.state()).toEqual({ status: "connected" });
expect(session.teardownCount()).toBe(0);
expect(session.lastError()).toBeNull();
expect(session.pingTimestamps().length).toBeGreaterThan(0);
expect(session.lastLivenessRttMs()).toBe(5500);
});
test("tears down and reports a liveness timeout after the daemon goes silent for two cycles", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonGoesSilent();
await session.connect();
await session.advance(51_000);
expect(session.state()).toEqual({
status: "disconnected",
reason: "Liveness check timed out (15000ms)",
});
expect(session.teardownCount()).toBe(1);
});
test("survives a single missed pong when answers resume", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonWithholdsNextPongThenAnswersFast();
await session.connect();
await session.advance(40_000);
expect(session.state()).toEqual({ status: "connected" });
expect(session.teardownCount()).toBe(0);
});
test("keeps the connection proven online from heartbeats alone when no other traffic flows", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("fast");
await session.connect();
await session.advance(35_000);
expect(session.state()).toEqual({ status: "connected" });
expect(session.pingTimestamps()).toEqual(["10s", "20s", "30s"]);
});
test("starts pinging one interval after connecting and holds the cadence", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("fast");
await session.connect();
await session.advance(9_999);
expect(session.pingTimestamps()).toEqual([]);
await session.advance(20_001);
expect(session.pingTimestamps()).toEqual(["10s", "20s", "30s"]);
});
test("stops pinging once the connection is gone", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("fast");
await session.connect();
await session.advance(10_000);
session.daemonClosesWith("daemon shutting down");
await session.advance(30_000);
expect(session.pingTimestamps()).toEqual(["10s"]);
});
test("sends only one ping while a slow pong is still outstanding", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("15.1s");
await session.connect();
await session.advance(20_000);
expect(session.pingTimestamps()).toEqual(["10s"]);
});
test("reports the round-trip time of the last successful heartbeat", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("2.3s");
await session.connect();
await session.advance(12_300);
expect(session.lastLivenessRttMs()).toBe(2300);
});
test("treats a pong just under the timeout as alive and just over as a miss", async () => {
useHeartbeatClock();
const alive = new DaemonClientSession();
alive.daemonAnswersPingsAfter("14.9s");
await alive.connect();
await alive.advance(24_900);
expect(alive.state()).toEqual({ status: "connected" });
const missed = new DaemonClientSession();
missed.daemonAnswersPingsAfter("15.1s");
await missed.connect();
await missed.advance(25_000);
expect(missed.state()).toEqual({ status: "connected" });
expect(missed.lastLivenessRttMs()).toBeNull();
});
test("goes red with the daemon's reason when the connection is closed", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
await session.connect();
session.daemonClosesWith("Control unresponsive");
expect(session.state()).toEqual({
status: "disconnected",
reason: "Control unresponsive",
});
expect(session.lastError()).toBe("Control unresponsive");
expect(session.closesFromClient()).toEqual([]);
});
test("two candidate latency timeouts do not tear down the live connection", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonGoesSilent();
await session.connect();
const firstMeasurement = session.measureLatency({ timeoutMs: 1000 });
const firstMeasurementError = firstMeasurement.then(
() => null,
(error) => error,
);
await session.advance(1000);
await expect(firstMeasurementError).resolves.toEqual(
expect.objectContaining({
message: "Latency measurement timed out (1000ms)",
}),
);
const secondMeasurement = session.measureLatency({ timeoutMs: 1000 });
const secondMeasurementError = secondMeasurement.then(
() => null,
(error) => error,
);
await session.advance(1000);
await expect(secondMeasurementError).resolves.toEqual(
expect.objectContaining({
message: "Latency measurement timed out (1000ms)",
}),
);
expect(session.state()).toEqual({ status: "connected" });
expect(session.teardownCount()).toBe(0);
expect(session.pingTimestamps()).toEqual(["0s", "1s"]);
});
test("a candidate measurement that times out under a heartbeat tick does not count toward teardown", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonGoesSilent();
await session.connect();
// A candidate measurement is still in flight when the +10s heartbeat tick lands,
// so the heartbeat shares the in-flight ping. Let the measurement time out.
await session.advance(9_000);
const measurement = session.measureLatency({ timeoutMs: 5_000 });
const measurementError = measurement.then(
() => null,
(error) => error,
);
await session.advance(5_500);
await expect(measurementError).resolves.toEqual(
expect.objectContaining({ message: "Latency measurement timed out (5000ms)" }),
);
// The measurement timeout must not have been recorded as a liveness failure: a
// single genuine heartbeat miss after it must still leave the connection up.
await session.advance(25_000);
expect(session.state()).toEqual({ status: "connected" });
expect(session.teardownCount()).toBe(0);
});
test("file context action RPCs correlate success and error responses", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_context_actions",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createFileEntry({
cwd: "/tmp/project",
parentPath: "src",
name: "new.ts",
kind: "file",
});
const createRequest = parseSentFrame(mock.sent.at(-1));
expect(createRequest).toEqual({
type: "fs.entry.create.request",
cwd: "/tmp/project",
parentPath: "src",
name: "new.ts",
kind: "file",
requestId: expect.any(String),
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.create.response",
payload: {
cwd: "/tmp/project",
parentPath: "src",
path: "src/new.ts",
success: true,
error: null,
requestId: createRequest.requestId,
},
}),
);
await expect(createPromise).resolves.toMatchObject({
path: "src/new.ts",
success: true,
error: null,
});
const renamePromise = client.renameFileEntry({
cwd: "/tmp/project",
path: "src/new.ts",
name: "existing.ts",
});
const renameRequest = parseSentFrame(mock.sent.at(-1));
expect(renameRequest).toMatchObject({
type: "fs.entry.rename.request",
cwd: "/tmp/project",
path: "src/new.ts",
name: "existing.ts",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.rename.response",
payload: {
cwd: "/tmp/project",
path: "src/new.ts",
renamedPath: null,
success: false,
error: '"existing.ts" already exists',
requestId: renameRequest.requestId,
},
}),
);
await expect(renamePromise).resolves.toMatchObject({
renamedPath: null,
success: false,
error: '"existing.ts" already exists',
});
const duplicatePromise = client.duplicateFileEntry({
cwd: "/tmp/project",
path: "src/new.ts",
});
const duplicateRequest = parseSentFrame(mock.sent.at(-1));
expect(duplicateRequest).toMatchObject({
type: "fs.entry.duplicate.request",
cwd: "/tmp/project",
path: "src/new.ts",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.duplicate.response",
payload: {
cwd: "/tmp/project",
path: "src/new.ts",
duplicatedPath: "src/new copy.ts",
success: true,
error: null,
requestId: duplicateRequest.requestId,
},
}),
);
await expect(duplicatePromise).resolves.toMatchObject({
duplicatedPath: "src/new copy.ts",
success: true,
error: null,
});
const deletePromise = client.deleteFileEntry({ cwd: "/tmp/project", path: "src/new.ts" });
const deleteRequest = parseSentFrame(mock.sent.at(-1));
expect(deleteRequest).toMatchObject({
type: "fs.entry.delete.request",
cwd: "/tmp/project",
path: "src/new.ts",
});
mock.triggerMessage(
wrapSessionMessage({
type: "fs.entry.delete.response",
payload: {
cwd: "/tmp/project",
path: "src/new.ts",
success: true,
error: null,
requestId: deleteRequest.requestId,
},
}),
);
await expect(deletePromise).resolves.toMatchObject({ success: true, error: null });
const discardPromise = client.checkoutDiscardChanges("/tmp/project", { paths: ["src"] });
const discardRequest = parseSentFrame(mock.sent.at(-1));
expect(discardRequest).toMatchObject({
type: "checkout.discard_changes.request",
cwd: "/tmp/project",
paths: ["src"],
});
mock.triggerMessage(
wrapSessionMessage({
type: "checkout.discard_changes.response",
payload: {
cwd: "/tmp/project",
success: false,
error: { code: "NOT_GIT_REPO", message: "Not a git repository" },
requestId: discardRequest.requestId,
},
}),
);
await expect(discardPromise).resolves.toMatchObject({
success: false,
error: { code: "NOT_GIT_REPO", message: "Not a git repository" },
});
});
test("sends plugin source identifiers unchanged for daemon-host resolution", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_plugin_source",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { pluginSourceInstallation: true } });
await connectPromise;
const installPromise = client.installPluginSource({
source: "owner/repository:plugins/review",
});
const request = parseSentFrame(mock.sent.at(-1));
expect(request).toEqual({
type: "plugin.source.install.request",
requestId: expect.any(String),
source: "owner/repository:plugins/review",
});
mock.triggerMessage(
wrapSessionMessage({
type: "plugin.source.install.response",
payload: {
requestId: request.requestId,
plugin: {
id: "review",
path: "/plugins/review",
enabled: true,
status: "running",
},
},
}),
);
await expect(installPromise).resolves.toMatchObject({ id: "review", status: "running" });
});
test("a connection loss rejects an in-flight file context action", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_context_disconnect",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const pending = client.deleteFileEntry({ cwd: "/tmp/project", path: "src/file.ts" });
mock.triggerClose({ code: 1006, reason: "network lost" });
await expect(pending).rejects.toThrow(/network lost|disconnected|closed/i);
});
test("listDirectory sends a list file explorer request and returns directory entries", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.listDirectory("/tmp/project", "src", "req-list");
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "file_explorer_request",
cwd: "/tmp/project",
path: "src",
mode: "list",
requestId: "req-list",
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "file_explorer_response",
payload: {
cwd: "/tmp/project",
path: "src",
mode: "list",
directory: {
path: "src",
entries: [
{
name: "index.ts",
path: "src/index.ts",
kind: "file",
size: 12,
modifiedAt: "2026-05-02T00:00:00.000Z",
},
],
},
file: null,
error: null,
requestId: "req-list",
},
}),
);
await expect(responsePromise).resolves.toEqual({
path: "src",
entries: [
{
name: "index.ts",
path: "src/index.ts",
kind: "file",
size: 12,
modifiedAt: "2026-05-02T00:00:00.000Z",
},
],
});
});
test("readFile hides legacy base64 behind bytes", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.readFile("/tmp/project", "logo.png", "req-file");
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "file_explorer_request",
cwd: "/tmp/project",
path: "logo.png",
mode: "file",
acceptBinary: true,
requestId: "req-file",
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "file_explorer_response",
payload: {
cwd: "/tmp/project",
path: "logo.png",
mode: "file",
directory: null,
file: {
path: "logo.png",
kind: "image",
encoding: "base64",
content: "aGVsbG8=",
mimeType: "image/png",
size: 5,
modifiedAt: "2026-05-02T00:00:00.000Z",
},
error: null,
requestId: "req-file",
},
}),
);
const result = await responsePromise;
expect(result).toMatchObject({
mime: "image/png",
size: 5,
path: "logo.png",
kind: "image",
modifiedAt: "2026-05-02T00:00:00.000Z",
});
expect(new TextDecoder().decode(result.bytes)).toBe("hello");
});
test("readFile resolves from binary file frames when the daemon supports them", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.readFile("/tmp/project", "logo.png", "req-binary");
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "file_explorer_request",
cwd: "/tmp/project",
path: "logo.png",
mode: "file",
acceptBinary: true,
requestId: "req-binary",
},
});
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileBegin,
requestId: "req-binary",
metadata: {
mime: "image/png",
size: 5,
encoding: "binary",
modifiedAt: "2026-05-02T00:00:00.000Z",
},
}),
);
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileChunk,
requestId: "req-binary",
payload: new TextEncoder().encode("hello"),
}),
);
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileEnd,
requestId: "req-binary",
}),
);
const result = await responsePromise;
expect(result).toMatchObject({
mime: "image/png",
size: 5,
path: "logo.png",
kind: "image",
modifiedAt: "2026-05-02T00:00:00.000Z",
});
expect(new TextDecoder().decode(result.bytes)).toBe("hello");
});
test("readFile drops an old daemon's over-budget binary chunks and reports the refusal", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_file_budget_compat",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.readFile("/tmp/project", "large.txt", "req-budget", 10);
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "file_explorer_request",
cwd: "/tmp/project",
path: "large.txt",
mode: "file",
acceptBinary: true,
maxBytes: 10,
requestId: "req-budget",
},
});
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileBegin,
requestId: "req-budget",
metadata: {
mime: "text/plain",
size: 100,
encoding: "utf-8",
modifiedAt: "2026-05-02T00:00:00.000Z",
},
}),
);
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileChunk,
requestId: "req-budget",
payload: new Uint8Array(100),
}),
);
mock.triggerMessage(
encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-budget" }),
);
await expect(responsePromise).rejects.toThrow("File is too large to display");
});
async function connectFileClient(clientId: string) {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId,
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
return { mock, client };
}
function sendBinaryFile(
mock: ReturnType<typeof createMockTransport>,
requestId: string,
announcedSize: number,
chunks: string[],
) {
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileBegin,
requestId,
metadata: {
mime: "application/octet-stream",
size: announcedSize,
encoding: "binary",
modifiedAt: "2026-05-02T00:00:00.000Z",
},
}),
);
for (const chunk of chunks) {
mock.triggerMessage(
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileChunk,
requestId,
payload: new TextEncoder().encode(chunk),
}),
);
}
}
test("readFile with timeout 0 outlives the default request timeout", async () => {
useHeartbeatClock();
const { mock, client } = await connectFileClient("clsk_file_no_timeout");
const responsePromise = client.readFile("/tmp/project", "slow.bin", "req-slow", undefined, 0);
await vi.advanceTimersByTimeAsync(60_001);
sendBinaryFile(mock, "req-slow", 5, ["hello"]);
mock.triggerMessage(
encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-slow" }),
);
const result = await responsePromise;
expect(result.size).toBe(5);
expect(new TextDecoder().decode(result.bytes)).toBe("hello");
});
test("readFile rejects a binary transfer that ends before the announced size", async () => {
const { mock, client } = await connectFileClient("clsk_file_short");
const responsePromise = client.readFile("/tmp/project", "short.bin", "req-short");
sendBinaryFile(mock, "req-short", 10, ["hello"]);
mock.triggerMessage(
encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-short" }),
);
await expect(responsePromise).rejects.toThrow(
"File transfer incomplete: expected 10 bytes, received 5.",
);
});
test("readFile rejects a binary transfer that exceeds the announced size", async () => {
const { mock, client } = await connectFileClient("clsk_file_long");
const responsePromise = client.readFile("/tmp/project", "long.bin", "req-long");
const settled = responsePromise.then(
() => null,
(error: unknown) => error,
);
sendBinaryFile(mock, "req-long", 4, ["hello!"]);
expect(() =>
mock.triggerMessage(
encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-long" }),
),
).not.toThrow();
expect(await settled).toMatchObject({
message: "File transfer incomplete: expected 4 bytes, received 6.",
});
});
test("uploadFile sends metadata request and file bytes as binary chunks", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.uploadFile({
fileName: "notes.txt",
mimeType: "text/plain",
bytes: new TextEncoder().encode("hello world"),
modifiedAt: "2026-05-02T00:00:00.000Z",
requestId: "req-upload",
chunkSize: 5,
});
// Other tasks must run before a multi-chunk upload has queued all its bytes.
await new Promise((resolve) => setTimeout(resolve, 0));
const framesDuringUpload = mock.sent.slice(1).map(assertUint8Array).map(decodeFileTransferFrame);
expect(framesDuringUpload.some((frame) => frame.opcode === FileTransferOpcode.FileEnd)).toBe(
false,
);
await vi.waitFor(() => {
const frames = mock.sent.slice(1).map(assertUint8Array).map(decodeFileTransferFrame);
expect(frames.at(-1)?.opcode).toBe(FileTransferOpcode.FileEnd);
});
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "file.upload.request",
fileName: "notes.txt",
mimeType: "text/plain",
size: 11,
modifiedAt: "2026-05-02T00:00:00.000Z",
requestId: "req-upload",
},
});
expect(mock.sent.slice(1).map(assertUint8Array).map(decodeFileTransferFrame)).toEqual([
{
opcode: FileTransferOpcode.FileBegin,
requestId: "req-upload",
metadata: {
mime: "text/plain",
size: 11,
encoding: "binary",
modifiedAt: "2026-05-02T00:00:00.000Z",
fileName: "notes.txt",
},
payload: new Uint8Array(),
},
{
opcode: FileTransferOpcode.FileChunk,
requestId: "req-upload",
payload: new TextEncoder().encode("hello"),
},
{
opcode: FileTransferOpcode.FileChunk,
requestId: "req-upload",
payload: new TextEncoder().encode(" worl"),
},
{
opcode: FileTransferOpcode.FileChunk,
requestId: "req-upload",
payload: new TextEncoder().encode("d"),
},
{
opcode: FileTransferOpcode.FileEnd,
requestId: "req-upload",
payload: new Uint8Array(),
},
]);
mock.triggerMessage(
wrapSessionMessage({
type: "file.upload.response",
payload: {
requestId: "req-upload",
file: {
type: "uploaded_file",
id: "upload_req-upload",
fileName: "notes.txt",
mimeType: "text/plain",
size: 11,
path: "/tmp/paseo-uploads/upload_req-upload/notes.txt",
},
error: null,
},
}),
);
await expect(responsePromise).resolves.toEqual({
requestId: "req-upload",
file: {
type: "uploaded_file",
id: "upload_req-upload",
fileName: "notes.txt",
mimeType: "text/plain",
size: 11,
path: "/tmp/paseo-uploads/upload_req-upload/notes.txt",
},
error: null,
});
});
test("normalizes workspace_setup_progress into a workspace-scoped daemon event", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const events: Array<Parameters<Parameters<typeof client.subscribe>[0]>[0]> = [];
client.subscribe((event) => {
events.push(event);
});
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
mock.triggerMessage(
wrapSessionMessage({
type: "workspace_setup_progress",
payload: {
workspaceId: "ws-feature-a",
status: "running",
detail: {
type: "worktree_setup",
worktreePath: "/tmp/project/.paseo/worktrees/feature-a",
branchName: "feature-a",
log: "phase-one\n",
commands: [
{
index: 1,
command: "npm install",
cwd: "/tmp/project/.paseo/worktrees/feature-a",
log: "phase-one\n",
status: "running",
exitCode: null,
},
],
},
error: null,
},
}),
);
expect(events).toContainEqual({
type: "workspace_setup_progress",
workspaceId: "ws-feature-a",
payload: {
workspaceId: "ws-feature-a",
status: "running",
detail: {
type: "worktree_setup",
worktreePath: "/tmp/project/.paseo/worktrees/feature-a",
branchName: "feature-a",
log: "phase-one\n",
commands: [
{
index: 1,
command: "npm install",
cwd: "/tmp/project/.paseo/worktrees/feature-a",
log: "phase-one\n",
status: "running",
exitCode: null,
},
],
},
error: null,
},
});
});
test("sends create_agent_request with workspace and caller identity", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { agentRequestReceipts: true } });
await connectPromise;
const createPromise = client.createAgent({
idempotencyKey: "one-creation",
provider: "codex",
cwd: "/tmp/project/.paseo/worktrees/feature-a",
workspaceId: "ws-feature-a",
callerAgentId: "parent-agent",
title: "Compat agent",
modeId: "default",
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual(
expect.objectContaining({
type: "create_agent_request",
idempotencyKey: "one-creation",
workspaceId: "ws-feature-a",
callerAgentId: "parent-agent",
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: request.requestId,
error: "compat test sentinel",
},
}),
);
await expect(createPromise).rejects.toThrow("compat test sentinel");
});
test("sends worktree target and autoArchive in create_agent_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createAgent({
provider: "codex",
cwd: "/tmp/project",
worktree: {
mode: "branch-off",
newBranch: "agent-lifecycle-dispatch",
base: "main",
},
autoArchive: true,
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual(
expect.objectContaining({
type: "create_agent_request",
worktree: {
mode: "branch-off",
newBranch: "agent-lifecycle-dispatch",
base: "main",
},
autoArchive: true,
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: request.requestId,
error: "worktree auto archive sentinel",
},
}),
);
await expect(createPromise).rejects.toThrow("worktree auto archive sentinel");
});
test("sends internal in create_agent_request when the host advertises internal agents", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: { backgroundWorkspaces: true } });
await connectPromise;
const createPromise = client.createAgent({
provider: "codex",
cwd: "/tmp/project",
background: true,
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual(
expect.objectContaining({
type: "create_agent_request",
config: { provider: "codex", cwd: "/tmp/project" },
background: true,
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: request.requestId,
error: "internal agent sentinel",
},
}),
);
await expect(createPromise).rejects.toThrow("internal agent sentinel");
});
test("rejects an internal create before sending when the host predates internal agents", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: {} });
await connectPromise;
await expect(
client.createAgent({ provider: "codex", cwd: "/tmp/project", background: true }),
).rejects.toThrow("Update the host to use background workspaces.");
expect(mock.sent).toHaveLength(0);
});
test("sends internal workspace creation and internal listings only to hosts that support them", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
// workspaceMultiplicity keeps the fetch on the real workspace directory RPC.
mock.triggerOpen({
features: { workspaceMultiplicity: true, backgroundWorkspaces: true },
});
await connectPromise;
void client
.createWorkspace({ source: { kind: "directory", path: "/tmp/project" }, background: true })
.catch(() => undefined);
void client.fetchWorkspaces({ filter: { includeBackground: true } }).catch(() => undefined);
void client.fetchAgents({ filter: { includeBackground: true } }).catch(() => undefined);
expect(mock.sent.map((frame) => parseSentFrame(frame))).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "workspace.create.request", background: true }),
expect.objectContaining({
type: "fetch_workspaces_request",
filter: { includeBackground: true },
}),
expect.objectContaining({
type: "fetch_agents_request",
filter: { includeBackground: true },
}),
]),
);
});
test("rejects internal workspace creation and internal listings before sending on older hosts", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen({ features: {} });
await connectPromise;
await expect(
client.createWorkspace({
source: { kind: "directory", path: "/tmp/project" },
background: true,
}),
).rejects.toThrow("Update the host to use background workspaces.");
await expect(client.fetchWorkspaces({ filter: { includeBackground: true } })).rejects.toThrow(
"Update the host to use background workspaces.",
);
expect(mock.sent).toHaveLength(0);
// Agent listings gate on the older internalAgents feature.
const olderMock = createMockTransport();
const olderClient = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => olderMock.transport,
});
clients.push(olderClient);
const olderConnect = olderClient.connect();
olderMock.triggerOpen({ features: {} });
await olderConnect;
await expect(olderClient.fetchAgents({ filter: { includeBackground: true } })).rejects.toThrow(
"Update the host to use background workspaces.",
);
expect(olderMock.sent).toHaveLength(0);
});
test("sends structured attachments with create_agent_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createAgent({
provider: "codex",
cwd: "/tmp/project",
initialPrompt: "Review this PR",
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Fix race in worktree setup",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "fix/worktree-race",
},
],
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.attachments).toEqual([
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Fix race in worktree setup",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "fix/worktree-race",
},
]);
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: request.requestId,
error: "attachment test sentinel",
},
}),
);
await expect(createPromise).rejects.toThrow("attachment test sentinel");
});
test("sends worktree base-ref fields in create_agent_request git options", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createAgent({
provider: "codex",
cwd: "/tmp/project",
requestId: "req-agent-ref",
git: {
createWorktree: true,
worktreeSlug: "review-pr-123",
refName: "feature/worktree-base-ref",
action: "checkout",
githubPrNumber: 123,
},
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.git).toEqual({
createWorktree: true,
worktreeSlug: "review-pr-123",
refName: "feature/worktree-base-ref",
action: "checkout",
githubPrNumber: 123,
});
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: request.requestId,
error: "git ref fields sentinel",
},
}),
);
await expect(createPromise).rejects.toThrow("git ref fields sentinel");
});
test("omitting create_agent_request worktree base-ref fields preserves legacy wire shape", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createAgent({
provider: "codex",
cwd: "/tmp/project",
requestId: "req-agent-legacy",
git: {
createWorktree: true,
worktreeSlug: "feature-a",
},
});
expect(assertStr(mock.sent[0])).toBe(
JSON.stringify({
type: "session",
message: {
type: "create_agent_request",
config: {
provider: "codex",
cwd: "/tmp/project",
},
git: {
createWorktree: true,
worktreeSlug: "feature-a",
},
labels: {},
requestId: "req-agent-legacy",
},
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_create_failed",
requestId: "req-agent-legacy",
error: "legacy git shape sentinel",
},
}),
);
await expect(createPromise).rejects.toThrow("legacy git shape sentinel");
});
test("sends structured first-agent context attachments with create_paseo_worktree_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createPaseoWorktree({
cwd: "/tmp/project",
worktreeSlug: "review-pr-123",
firstAgentContext: {
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Fix race in worktree setup",
url: "https://github.com/getpaseo/paseo/pull/123",
},
],
},
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
const firstAgentContext = z
.object({ attachments: z.array(z.unknown()) })
.parse(request.firstAgentContext);
expect(firstAgentContext.attachments).toEqual([
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Fix race in worktree setup",
url: "https://github.com/getpaseo/paseo/pull/123",
},
]);
mock.triggerMessage(
wrapSessionMessage({
type: "create_paseo_worktree_response",
payload: {
requestId: request.requestId,
workspace: null,
error: "worktree attachment sentinel",
setupTerminalId: null,
},
}),
);
await expect(createPromise).resolves.toEqual({
requestId: request.requestId,
workspace: null,
error: "worktree attachment sentinel",
setupTerminalId: null,
});
});
test("sends project.add.request without creating a workspace", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const addPromise = client.addProject("/tmp/project", "req-add-project");
expect(mock.sent).toHaveLength(1);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "project.add.request",
requestId: "req-add-project",
cwd: "/tmp/project",
});
mock.triggerMessage(
wrapSessionMessage({
type: "project.add.response",
payload: {
requestId: "req-add-project",
project: {
projectId: "/tmp/project",
projectDisplayName: "project",
projectCustomName: null,
projectRootPath: "/tmp/project",
projectKind: "git",
},
error: null,
},
}),
);
await expect(addPromise).resolves.toEqual({
requestId: "req-add-project",
project: {
projectId: "/tmp/project",
projectDisplayName: "project",
projectCustomName: null,
projectRootPath: "/tmp/project",
projectKind: "git",
},
error: null,
});
});
test("marks a workspace unread through the dotted RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const markPromise = client.markWorkspaceUnread("workspace-1", "req-mark-unread");
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "workspace.mark_unread.request",
workspaceId: "workspace-1",
requestId: "req-mark-unread",
});
mock.triggerMessage(
wrapSessionMessage({
type: "workspace.mark_unread.response",
payload: {
requestId: "req-mark-unread",
workspaceId: "workspace-1",
markedAgentId: "agent-1",
success: true,
error: null,
},
}),
);
await expect(markPromise).resolves.toBeUndefined();
});
test("searches GitHub repositories through the dotted RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const searchPromise = client.searchGithubRepositories(
{ query: "paseo", limit: 10 },
"req-repositories",
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "workspace.github.search_repositories.request",
query: "paseo",
limit: 10,
requestId: "req-repositories",
});
mock.triggerMessage(
wrapSessionMessage({
type: "workspace.github.search_repositories.response",
payload: {
status: "success",
requestId: "req-repositories",
repositories: [
{
id: "R_paseo",
name: "paseo",
nameWithOwner: "getpaseo/paseo",
description: "Development environment in your pocket",
visibility: "public",
updatedAt: "2026-07-15T10:00:00Z",
cloneUrl: "git@github.com:getpaseo/paseo.git",
},
],
available: true,
error: null,
},
}),
);
await expect(searchPromise).resolves.toEqual({
status: "success",
requestId: "req-repositories",
repositories: [
{
id: "R_paseo",
name: "paseo",
nameWithOwner: "getpaseo/paseo",
description: "Development environment in your pocket",
visibility: "public",
updatedAt: "2026-07-15T10:00:00Z",
cloneUrl: "git@github.com:getpaseo/paseo.git",
},
],
available: true,
error: null,
});
});
test("creates and registers a project directory through the dotted RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createProjectDirectory(
{ parentPath: "/tmp/projects", name: "new-project" },
"req-create-project-directory",
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "project.create_directory.request",
parentPath: "/tmp/projects",
name: "new-project",
requestId: "req-create-project-directory",
});
mock.triggerMessage(
wrapSessionMessage({
type: "project.create_directory.response",
payload: {
requestId: "req-create-project-directory",
directoryPath: "/tmp/projects/new-project",
project: {
projectId: "directory:/tmp/projects/new-project",
projectDisplayName: "new-project",
projectCustomName: null,
projectRootPath: "/tmp/projects/new-project",
projectKind: "non_git",
},
error: null,
errorCode: null,
},
}),
);
await expect(createPromise).resolves.toEqual({
requestId: "req-create-project-directory",
directoryPath: "/tmp/projects/new-project",
project: {
projectId: "directory:/tmp/projects/new-project",
projectDisplayName: "new-project",
projectCustomName: null,
projectRootPath: "/tmp/projects/new-project",
projectKind: "non_git",
},
error: null,
errorCode: null,
});
});
test("sends first-agent prompt context with workspace.create.request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createWorkspace(
{
source: {
kind: "directory",
path: "/tmp/project",
projectId: "local:/tmp/project",
},
firstAgentContext: {
prompt: "Fix login bug",
attachments: [],
},
},
"req-local-title",
);
expect(mock.sent).toHaveLength(1);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "workspace.create.request",
requestId: "req-local-title",
source: {
kind: "directory",
path: "/tmp/project",
projectId: "local:/tmp/project",
},
firstAgentContext: {
prompt: "Fix login bug",
attachments: [],
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "workspace.create.response",
payload: {
requestId: "req-local-title",
workspace: null,
error: "local title sentinel",
setupTerminalId: null,
},
}),
);
await expect(createPromise).resolves.toEqual({
requestId: "req-local-title",
workspace: null,
error: "local title sentinel",
setupTerminalId: null,
});
});
test("sends project.remove.request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const removePromise = client.removeProject("remote:github.com/acme/app", "req-remove-project");
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "project.remove.request",
requestId: "req-remove-project",
projectId: "remote:github.com/acme/app",
});
mock.triggerMessage(
wrapSessionMessage({
type: "project.remove.response",
payload: {
requestId: "req-remove-project",
projectId: "remote:github.com/acme/app",
accepted: true,
removedWorkspaceIds: ["ws-main"],
error: null,
},
}),
);
await expect(removePromise).resolves.toEqual({ removedWorkspaceIds: ["ws-main"] });
});
test("sends worktree base-ref fields in create_paseo_worktree_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createPaseoWorktree(
{
cwd: "/tmp/project",
projectId: "remote:github.com/acme/project",
worktreeSlug: "review-pr-123",
refName: "feature/worktree-base-ref",
action: "checkout",
githubPrNumber: 123,
},
"req-worktree-ref",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "create_paseo_worktree_request",
cwd: "/tmp/project",
projectId: "remote:github.com/acme/project",
worktreeSlug: "review-pr-123",
refName: "feature/worktree-base-ref",
action: "checkout",
githubPrNumber: 123,
requestId: "req-worktree-ref",
});
mock.triggerMessage(
wrapSessionMessage({
type: "create_paseo_worktree_response",
payload: {
requestId: request.requestId,
workspace: null,
error: "worktree ref fields sentinel",
setupTerminalId: null,
},
}),
);
await expect(createPromise).resolves.toEqual({
requestId: request.requestId,
workspace: null,
error: "worktree ref fields sentinel",
setupTerminalId: null,
});
});
test("omitting create_paseo_worktree_request worktree base-ref fields preserves legacy wire shape", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const createPromise = client.createPaseoWorktree(
{
cwd: "/tmp/project",
worktreeSlug: "feature-a",
},
"req-worktree-legacy",
);
expect(assertStr(mock.sent[0])).toBe(
JSON.stringify({
type: "session",
message: {
type: "create_paseo_worktree_request",
cwd: "/tmp/project",
worktreeSlug: "feature-a",
requestId: "req-worktree-legacy",
},
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "create_paseo_worktree_response",
payload: {
requestId: "req-worktree-legacy",
workspace: null,
error: "legacy worktree shape sentinel",
setupTerminalId: null,
},
}),
);
await expect(createPromise).resolves.toEqual({
requestId: "req-worktree-legacy",
workspace: null,
error: "legacy worktree shape sentinel",
setupTerminalId: null,
});
});
test("sends explicit shutdown_server_request via shutdownServer", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
expect(typeof client.shutdownServer).toBe("function");
const promise = client.shutdownServer({ requestId: "req-shutdown-1" });
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "shutdown_server_request",
requestId: "req-shutdown-1",
});
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "shutdown_requested",
clientId: "clsk_unit_test",
requestId: "req-shutdown-1",
},
}),
);
await expect(promise).resolves.toEqual({
status: "shutdown_requested",
clientId: "clsk_unit_test",
requestId: "req-shutdown-1",
});
});
test("restartServer remains restart-only and sends restart_server_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.restartServer("settings_update", "req-restart-1");
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "restart_server_request",
reason: "settings_update",
requestId: "req-restart-1",
});
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "restart_requested",
clientId: "clsk_unit_test",
reason: "settings_update",
requestId: "req-restart-1",
},
}),
);
await expect(promise).resolves.toEqual({
status: "restart_requested",
clientId: "clsk_unit_test",
reason: "settings_update",
requestId: "req-restart-1",
});
});
test("transitions out of connecting when connect timeout elapses", async () => {
useHeartbeatClock();
try {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
connectTimeoutMs: 100,
transportFactory: () => mock.transport,
});
clients.push(client);
const pendingConnect = client.connect().then(
() => ({ ok: true as const }),
(error) => ({ ok: false as const, error }),
);
expect(client.getConnectionState().status).toBe("connecting");
await vi.advanceTimersByTimeAsync(120);
const result = await pendingConnect;
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBeInstanceOf(Error);
if (result.error instanceof Error) {
expect(result.error.message).toContain("Connection timed out");
}
}
expect(client.getConnectionState().status).toBe("disconnected");
} finally {
vi.useRealTimers();
}
});
test("reconnects after relay close with replaced-by-new-connection reason", async () => {
useHeartbeatClock();
try {
const logger = createMockLogger();
const first = createMockTransport();
const second = createMockTransport();
const transports = [first, second];
let transportIndex = 0;
const client = new DaemonClient({
url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2",
clientId: "clsk_test",
logger,
reconnect: {
enabled: true,
baseDelayMs: 5,
maxDelayMs: 5,
},
transportFactory: () => {
const next = transports[Math.min(transportIndex, transports.length - 1)];
transportIndex += 1;
return next.transport;
},
});
clients.push(client);
const connectPromise = client.connect();
first.triggerOpen();
await connectPromise;
expect(client.getConnectionState().status).toBe("connected");
first.triggerClose({ code: 1008, reason: "Replaced by new connection" });
expect(client.getConnectionState().status).toBe("disconnected");
await vi.advanceTimersByTimeAsync(10);
expect(client.getConnectionState().status).toBe("connecting");
second.triggerOpen();
expect(client.getConnectionState().status).toBe("connected");
} finally {
vi.useRealTimers();
}
});
test("requires non-empty clientId", () => {
expect(() => {
const _client = new DaemonClient({
url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2",
clientId: "",
reconnect: { enabled: false },
});
void _client;
}).toThrow("Daemon client requires a non-empty clientId");
});
test("requires non-empty clientId for direct connections", () => {
expect(() => {
const _client = new DaemonClient({
url: "ws://127.0.0.1:6767/ws",
clientId: " ",
reconnect: { enabled: false },
});
void _client;
}).toThrow("Daemon client requires a non-empty clientId");
});
test("logs configured runtime generation in connection transition events", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
runtimeGeneration: 7,
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const transitionPayloads = logger.debug.mock.calls.filter(
([, message]) => message === "DaemonClientTransition",
);
expect(transitionPayloads.length).toBeGreaterThan(0);
for (const [payload] of transitionPayloads) {
expect(
z.object({ generation: z.number().nullable().optional() }).parse(payload).generation,
).toBe(7);
}
});
test("subscribes to checkout diff updates via RPC handshake", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const subscription = client.observeCheckoutDiff("/tmp/project", { mode: "uncommitted" });
const promise = subscription.ready;
await vi.waitFor(() => expect(mock.sent).toHaveLength(1));
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("subscribe_checkout_diff_request");
expect(request.subscriptionId).toBeUndefined();
expect(request.cwd).toBe("/tmp/project");
expect(request.compare).toEqual({ mode: "uncommitted" });
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "subscribe_checkout_diff_response",
payload: {
subscriptionId: "checkout-sub-1",
cwd: "/tmp/project",
files: [],
error: null,
requestId: request.requestId,
},
},
}),
);
await expect(promise).resolves.toEqual({
subscriptionId: "checkout-sub-1",
cwd: "/tmp/project",
files: [],
error: null,
requestId: request.requestId,
});
});
test("getCheckoutDiff reads a snapshot without creating a subscription", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.getCheckoutDiff("/tmp/project", { mode: "base", baseRef: "main" });
expect(mock.sent).toHaveLength(1);
const subscribeRequest = parseSentFrame(mock.sent[0]);
expect(subscribeRequest.type).toBe("checkout.diff.get.request");
expect(subscribeRequest.cwd).toBe("/tmp/project");
expect(subscribeRequest.compare).toEqual({
mode: "base",
baseRef: "main",
});
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout.diff.get.response",
payload: {
cwd: "/tmp/project",
files: [],
error: null,
requestId: subscribeRequest.requestId,
},
},
}),
);
await expect(promise).resolves.toEqual({
cwd: "/tmp/project",
files: [],
error: null,
requestId: subscribeRequest.requestId,
});
expect(mock.sent).toHaveLength(1);
});
test("requests branch suggestions via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.getBranchSuggestions(
{ cwd: "/tmp/project", query: "mai", limit: 5 },
"req-branches",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("branch_suggestions_request");
expect(request.cwd).toBe("/tmp/project");
expect(request.query).toBe("mai");
expect(request.limit).toBe(5);
expect(request.requestId).toBe("req-branches");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "branch_suggestions_response",
payload: {
branches: ["main"],
error: null,
requestId: "req-branches",
},
},
}),
);
await expect(promise).resolves.toEqual({
branches: ["main"],
error: null,
requestId: "req-branches",
});
});
test("reads project config via correlated RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.readProjectConfig("/repo/app", "read-project-config-1");
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "read_project_config_request",
requestId: "read-project-config-1",
repoRoot: "/repo/app",
});
mock.triggerMessage(
wrapSessionMessage({
type: "read_project_config_response",
payload: {
requestId: "read-project-config-1",
repoRoot: "/repo/app",
ok: true,
config: { worktree: { setup: "npm install" } },
revision: { mtimeMs: 10, size: 20 },
},
}),
);
await expect(promise).resolves.toEqual({
requestId: "read-project-config-1",
repoRoot: "/repo/app",
ok: true,
config: { worktree: { setup: "npm install" } },
revision: { mtimeMs: 10, size: 20 },
});
});
test("writes project config via correlated RPC and returns inline failures", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.writeProjectConfig({
requestId: "write-project-config-1",
repoRoot: "/repo/app",
config: { worktree: { setup: ["npm install"] } },
expectedRevision: { mtimeMs: 10, size: 20 },
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toEqual({
type: "write_project_config_request",
requestId: "write-project-config-1",
repoRoot: "/repo/app",
config: { worktree: { setup: ["npm install"] } },
expectedRevision: { mtimeMs: 10, size: 20 },
});
mock.triggerMessage(
wrapSessionMessage({
type: "write_project_config_response",
payload: {
requestId: "write-project-config-1",
repoRoot: "/repo/app",
ok: false,
error: {
code: "stale_project_config",
currentRevision: { mtimeMs: 11, size: 21 },
},
},
}),
);
await expect(promise).resolves.toEqual({
requestId: "write-project-config-1",
repoRoot: "/repo/app",
ok: false,
error: {
code: "stale_project_config",
currentRevision: { mtimeMs: 11, size: 21 },
},
});
});
test("requests directory suggestions via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.getDirectorySuggestions(
{
query: "proj",
limit: 10,
cwd: "/tmp/project",
includeFiles: true,
includeDirectories: true,
matchMode: "suffix",
},
"req-directories",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("directory_suggestions_request");
expect(request.query).toBe("proj");
expect(request.cwd).toBe("/tmp/project");
expect(request.includeFiles).toBe(true);
expect(request.includeDirectories).toBe(true);
expect(request.matchMode).toBe("suffix");
expect(request.limit).toBe(10);
expect(request.requestId).toBe("req-directories");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "directory_suggestions_response",
payload: {
directories: ["/Users/test/projects/paseo"],
entries: [{ path: "README.md", kind: "file" }],
error: null,
requestId: "req-directories",
},
},
}),
);
await expect(promise).resolves.toEqual({
directories: ["/Users/test/projects/paseo"],
entries: [{ path: "README.md", kind: "file" }],
error: null,
requestId: "req-directories",
});
});
test("requests checkout merge from base via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.checkoutMergeFromBase(
"/tmp/project",
{ baseRef: "main", requireCleanTarget: true },
"req-merge-from-base",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("checkout_merge_from_base_request");
expect(request.cwd).toBe("/tmp/project");
expect(request.baseRef).toBe("main");
expect(request.requireCleanTarget).toBe(true);
expect(request.requestId).toBe("req-merge-from-base");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout_merge_from_base_response",
payload: {
cwd: "/tmp/project",
requestId: "req-merge-from-base",
success: true,
error: null,
},
},
}),
);
await expect(promise).resolves.toEqual({
cwd: "/tmp/project",
requestId: "req-merge-from-base",
success: true,
error: null,
});
});
test("requests GitHub auto-merge enable via namespaced RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.checkoutGithubSetAutoMerge(
"/tmp/project",
{ enabled: true, method: "squash" },
"req-enable-auto-merge",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("checkout.github.set_auto_merge.request");
expect(request.cwd).toBe("/tmp/project");
expect(request.enabled).toBe(true);
expect(request.mergeMethod).toBe("squash");
expect(request.requestId).toBe("req-enable-auto-merge");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout.github.set_auto_merge.response",
payload: {
cwd: "/tmp/project",
enabled: true,
requestId: "req-enable-auto-merge",
success: true,
error: null,
},
},
}),
);
await expect(promise).resolves.toEqual({
cwd: "/tmp/project",
enabled: true,
requestId: "req-enable-auto-merge",
success: true,
error: null,
});
});
test("requests GitHub auto-merge disable via namespaced RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.checkoutGithubSetAutoMerge(
"/tmp/project",
{ enabled: false },
"req-disable-auto-merge",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("checkout.github.set_auto_merge.request");
expect(request.cwd).toBe("/tmp/project");
expect(request.enabled).toBe(false);
expect(request.mergeMethod).toBeUndefined();
expect(request.requestId).toBe("req-disable-auto-merge");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout.github.set_auto_merge.response",
payload: {
cwd: "/tmp/project",
enabled: false,
requestId: "req-disable-auto-merge",
success: true,
error: null,
},
},
}),
);
await expect(promise).resolves.toEqual({
cwd: "/tmp/project",
enabled: false,
requestId: "req-disable-auto-merge",
success: true,
error: null,
});
});
test("requests GitHub check details via namespaced RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.checkoutGithubGetCheckDetails(
{
cwd: "/tmp/project",
repoOwner: "getpaseo",
repoName: "paseo",
checkRunId: 12345,
workflowRunId: 456,
},
"req-check-details",
);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({
type: "checkout.github.get_check_details.request",
cwd: "/tmp/project",
repoOwner: "getpaseo",
repoName: "paseo",
checkRunId: 12345,
workflowRunId: 456,
requestId: "req-check-details",
});
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout.github.get_check_details.response",
payload: {
cwd: "/tmp/project",
requestId: "req-check-details",
success: true,
details: {
checkRunId: 12345,
workflowRunId: 456,
name: "server-tests",
status: "completed",
conclusion: "failure",
annotations: [],
failedJobs: [],
truncated: false,
},
error: null,
},
},
}),
);
await expect(promise).resolves.toEqual({
cwd: "/tmp/project",
requestId: "req-check-details",
success: true,
details: {
checkRunId: 12345,
workflowRunId: 456,
name: "server-tests",
status: "completed",
conclusion: "failure",
annotations: [],
failedJobs: [],
truncated: false,
},
error: null,
});
});
test("requests checkout pull via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.checkoutPull("/tmp/project", "req-pull");
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("checkout_pull_request");
expect(request.cwd).toBe("/tmp/project");
expect(request.requestId).toBe("req-pull");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout_pull_response",
payload: {
cwd: "/tmp/project",
requestId: "req-pull",
success: true,
error: null,
},
},
}),
);
await expect(promise).resolves.toEqual({
cwd: "/tmp/project",
requestId: "req-pull",
success: true,
error: null,
});
});
test("renames a branch via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.renameBranch({
cwd: "/tmp/project",
branch: "feature/new-name",
requestId: "req-rename-branch",
});
expect(mock.sent).toHaveLength(1);
const request = JSON.parse(assertStr(mock.sent[0])) as {
type: "session";
message: {
type: "checkout.rename_branch.request";
cwd: string;
branch: string;
requestId: string;
};
};
expect(request.message.type).toBe("checkout.rename_branch.request");
expect(request.message.cwd).toBe("/tmp/project");
expect(request.message.branch).toBe("feature/new-name");
expect(request.message.requestId).toBe("req-rename-branch");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout.rename_branch.response",
payload: {
requestId: "req-rename-branch",
success: true,
cwd: "/tmp/project",
currentBranch: "feature/new-name",
error: null,
},
},
}),
);
await expect(promise).resolves.toEqual({
requestId: "req-rename-branch",
success: true,
cwd: "/tmp/project",
currentBranch: "feature/new-name",
error: null,
});
});
test("returns renameBranch business failures", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.renameBranch({
cwd: "/tmp/project",
branch: "already-exists",
requestId: "req-rename-branch-fail",
});
expect(mock.sent).toHaveLength(1);
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "checkout.rename_branch.response",
payload: {
requestId: "req-rename-branch-fail",
success: false,
cwd: "/tmp/project",
currentBranch: null,
error: { code: "NOT_ALLOWED", message: "Branch already exists" },
},
},
}),
);
await expect(promise).resolves.toEqual({
requestId: "req-rename-branch-fail",
success: false,
cwd: "/tmp/project",
currentBranch: null,
error: { code: "NOT_ALLOWED", message: "Branch already exists" },
});
});
test("resubscribes checkout diff streams after reconnect", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const observation = client.observeCheckoutDiff("/tmp/project", { mode: "base", baseRef: "main" });
await vi.waitFor(() => expect(mock.sent).toHaveLength(1));
const first = parseSentFrame(mock.sent[0]);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_checkout_diff_response",
payload: {
requestId: first.requestId,
subscriptionId: "server-first",
cwd: "/tmp/project",
files: [],
error: null,
},
}),
);
await observation.ready;
mock.triggerClose();
const reconnect = client.connect();
mock.triggerOpen();
await reconnect;
await vi.waitFor(() => expect(mock.sent).toHaveLength(1));
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({
type: "subscribe_checkout_diff_request",
cwd: "/tmp/project",
compare: { mode: "base", baseRef: "main" },
});
expect(request.subscriptionId).toBeUndefined();
expect(request.requestId).not.toBe(first.requestId);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_checkout_diff_response",
payload: {
requestId: request.requestId,
subscriptionId: "server-second",
cwd: "/tmp/project",
files: [],
error: null,
},
}),
);
await vi.waitFor(() => expect(observation.subscriptionId).toBe("server-second"));
});
test("fetches agents via RPC with filters, sort, and pagination", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.fetchAgents({
filter: { labels: { surface: "workspace" } },
sort: [
{ key: "status_priority", direction: "asc" },
{ key: "created_at", direction: "desc" },
],
page: { limit: 25, cursor: "cursor-1" },
subscribe: {},
});
await vi.waitFor(() => expect(mock.sent).toHaveLength(1));
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("fetch_agents_request");
expect(request.sort).toEqual([
{ key: "status_priority", direction: "asc" },
{ key: "created_at", direction: "desc" },
]);
expect(request.page).toEqual({ limit: 25, cursor: "cursor-1" });
expect(request.subscribe).toEqual({});
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "fetch_agents_response",
payload: {
requestId: request.requestId,
subscriptionId: "sub-1",
entries: [],
pageInfo: {
nextCursor: null,
prevCursor: "cursor-1",
hasMore: false,
},
},
},
}),
);
await expect(promise).resolves.toEqual({
requestId: request.requestId,
subscriptionId: "sub-1",
subscription: expect.objectContaining({
subscriptionId: "sub-1",
release: expect.any(Function),
}),
entries: [],
pageInfo: {
nextCursor: null,
prevCursor: "cursor-1",
hasMore: false,
},
});
});
test("detaches an agent through the namespaced detach RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.detachAgent("child-agent");
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({
type: "agent.detach.request",
agentId: "child-agent",
});
expect(typeof request.requestId).toBe("string");
mock.triggerMessage(
wrapSessionMessage({
type: "agent.detach.response",
payload: {
requestId: request.requestId,
agentId: "child-agent",
accepted: true,
error: null,
},
}),
);
await expect(promise).resolves.toBeUndefined();
});
test("sends active-scoped fetch_agents_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.fetchAgents({
scope: "active",
page: { limit: 50 },
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({
type: "fetch_agents_request",
scope: "active",
});
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "fetch_agents_response",
payload: {
requestId: request.requestId,
entries: [],
pageInfo: {
nextCursor: null,
prevCursor: null,
hasMore: false,
},
},
},
}),
);
await expect(promise).resolves.toMatchObject({
requestId: request.requestId,
entries: [],
});
});
test("fetches paginated agent history separately from active agents", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.fetchAgentHistory({
page: { limit: 25, cursor: "cursor-1" },
sort: [{ key: "updated_at", direction: "desc" }],
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("fetch_agent_history_request");
expect(request.page).toEqual({ limit: 25, cursor: "cursor-1" });
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "fetch_agent_history_response",
payload: {
requestId: request.requestId,
entries: [],
pageInfo: {
nextCursor: null,
prevCursor: "cursor-1",
hasMore: false,
},
},
},
}),
);
await expect(promise).resolves.toEqual({
requestId: request.requestId,
entries: [],
pageInfo: {
nextCursor: null,
prevCursor: "cursor-1",
hasMore: false,
},
});
});
test("fetches scoped recent provider sessions", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.fetchRecentProviderSessions({
cwd: "/tmp/repo",
providers: ["my-claude"],
since: "2026-04-30T00:00:00.000Z",
limit: 25,
query: "invoice",
});
expect(mock.sent).toHaveLength(1);
const request = JSON.parse(String(mock.sent[0])) as {
type: "session";
message: {
type: "fetch_recent_provider_sessions_request";
requestId: string;
cwd?: string;
providers?: string[];
since?: string;
limit?: number;
query?: string;
};
};
expect(request.message).toMatchObject({
type: "fetch_recent_provider_sessions_request",
cwd: "/tmp/repo",
providers: ["my-claude"],
since: "2026-04-30T00:00:00.000Z",
limit: 25,
query: "invoice",
});
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "fetch_recent_provider_sessions_response",
payload: {
requestId: request.message.requestId,
entries: [
{
providerId: "codex",
providerLabel: "Codex",
providerHandleId: "thread-1",
cwd: "/tmp/repo",
title: "Import me",
firstPromptPreview: "first prompt",
lastPromptPreview: "last prompt",
lastActivityAt: "2026-04-30T12:34:56.000Z",
},
],
},
},
}),
);
await expect(promise).resolves.toEqual({
requestId: request.message.requestId,
entries: [
{
providerId: "codex",
providerLabel: "Codex",
providerHandleId: "thread-1",
cwd: "/tmp/repo",
title: "Import me",
firstPromptPreview: "first prompt",
lastPromptPreview: "last prompt",
lastActivityAt: "2026-04-30T12:34:56.000Z",
},
],
});
});
test("imports an agent by provider handle id", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.importAgent({
providerId: "custom-codex",
providerHandleId: "thread-1",
cwd: "/tmp/repo",
});
expect(mock.sent).toHaveLength(1);
const request = JSON.parse(String(mock.sent[0])) as {
type: "session";
message: {
type: "import_agent_request";
requestId: string;
providerId?: string;
providerHandleId?: string;
sessionId?: string;
cwd?: string;
};
};
expect(request.message).toMatchObject({
type: "import_agent_request",
providerId: "custom-codex",
providerHandleId: "thread-1",
cwd: "/tmp/repo",
});
expect(request.message).not.toHaveProperty("sessionId");
mock.triggerMessage(
wrapSessionMessage({
type: "status",
payload: {
status: "agent_resumed",
requestId: request.message.requestId,
agentId: "agent-1",
timelineSize: 0,
agent: {
id: "agent-1",
provider: "custom-codex",
cwd: "/tmp/repo",
model: null,
features: [],
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-04-30T00:00:00.000Z",
updatedAt: "2026-04-30T00:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: {
provider: "custom-codex",
sessionId: "thread-1",
nativeHandle: "thread-1",
},
title: null,
labels: {},
requiresAttention: false,
attentionReason: null,
},
},
}),
);
await expect(promise).resolves.toMatchObject({
id: "agent-1",
provider: "custom-codex",
});
});
test("uses server-provided dictation finish timeout budget", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const finishPromise = client.finishDictationStream("dict-1", 0);
const finishError = finishPromise.then(
() => null,
(error) => error,
);
expect(mock.sent).toHaveLength(1);
mock.triggerMessage(
wrapSessionMessage({
type: "dictation_stream_finish_accepted",
payload: {
dictationId: "dict-1",
timeoutMs: 100,
},
}),
);
await vi.advanceTimersByTimeAsync(5_101);
const error = await finishError;
expect(error).toBeInstanceOf(Error);
if (error instanceof Error) {
expect(error.message).toContain("Timeout waiting for dictation finalization (5100ms)");
}
vi.useRealTimers();
});
test("resolves dictation finish when final arrives after finish accepted", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const finishPromise = client.finishDictationStream("dict-2", 1);
expect(mock.sent).toHaveLength(1);
mock.triggerMessage(
wrapSessionMessage({
type: "dictation_stream_finish_accepted",
payload: {
dictationId: "dict-2",
timeoutMs: 1000,
},
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "dictation_stream_final",
payload: {
dictationId: "dict-2",
text: "hello",
},
}),
);
await expect(finishPromise).resolves.toEqual({
dictationId: "dict-2",
text: "hello",
});
});
test("cancels waiters when send fails (no leaked timeouts)", async () => {
useHeartbeatClock();
const logger = createMockLogger();
const mock = createMockTransport();
let sendCount = 0;
const transportFactory = () => ({
...mock.transport,
send: () => {
sendCount += 1;
if (sendCount > 1) {
throw new Error("boom");
}
},
});
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.getCheckoutStatus("/tmp/project");
await expect(promise).rejects.toThrow("boom");
// Ensure we didn't leave a waiter behind that will reject later.
const internal = client as unknown as { waiters: Set<unknown> };
expect(internal.waiters.size).toBe(0);
await vi.advanceTimersByTimeAsync(0);
vi.useRealTimers();
});
test("lists available providers via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.listAvailableProviders();
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("list_available_providers_request");
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "list_available_providers_response",
payload: {
providers: [
{ provider: "claude", available: true, error: null },
{ provider: "codex", available: false, error: "Missing binary" },
],
error: null,
fetchedAt: "2026-02-12T00:00:00.000Z",
requestId: request.requestId,
},
},
}),
);
await expect(promise).resolves.toEqual({
providers: [
{ provider: "claude", available: true, error: null },
{ provider: "codex", available: false, error: "Missing binary" },
],
error: null,
fetchedAt: "2026-02-12T00:00:00.000Z",
requestId: request.requestId,
});
});
test("requests provider snapshots conditionally and expands the compact response", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_provider_snapshot_test",
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.getProvidersSnapshot({ cwd: "/repo", ifNoneMatch: "previous-hash" });
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({
type: "get_providers_snapshot_request",
cwd: "/repo",
ifNoneMatch: "previous-hash",
});
mock.triggerMessage(
wrapSessionMessage({
type: "get_providers_snapshot_response",
payload: {
entries: [],
compactSnapshot: {
entries: [
{
provider: "pi",
status: "ready",
enabled: true,
models: [
{ id: "model-a", label: "Model A", thinkingSet: 0 },
{ id: "model-b", label: "Model B", thinkingSet: 0 },
],
},
],
thinkingSets: [
{
options: [{ id: "high", label: "High", isDefault: true }],
defaultOptionId: "high",
},
],
},
snapshotHash: "next-hash",
generatedAt: "2026-08-04T00:00:00.000Z",
requestId: request.requestId,
},
}),
);
const response = await promise;
expect(response.entries[0]?.models).toEqual([
{
provider: "pi",
id: "model-a",
label: "Model A",
thinkingOptions: [{ id: "high", label: "High", isDefault: true }],
defaultThinkingOptionId: "high",
},
{
provider: "pi",
id: "model-b",
label: "Model B",
thinkingOptions: [{ id: "high", label: "High", isDefault: true }],
defaultThinkingOptionId: "high",
},
]);
expect(response.entries[0]?.models?.[0]?.thinkingOptions).toBe(
response.entries[0]?.models?.[1]?.thinkingOptions,
);
});
test("lists commands with draft config via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.listCommands({
agentId: "__new_agent__",
draftConfig: {
provider: "codex",
cwd: "/tmp/project",
modeId: "bypassPermissions",
model: "gpt-5",
thinkingOptionId: "off",
},
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("list_commands_request");
expect(request.agentId).toBe("__new_agent__");
expect(request.draftConfig).toEqual({
provider: "codex",
cwd: "/tmp/project",
modeId: "bypassPermissions",
model: "gpt-5",
thinkingOptionId: "off",
});
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "list_commands_response",
payload: {
agentId: "__new_agent__",
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
error: null,
requestId: request.requestId,
},
},
}),
);
await expect(promise).resolves.toEqual({
agentId: "__new_agent__",
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
error: null,
requestId: request.requestId,
});
});
test("lists commands with explicit requestId via RPC", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.listCommands({
agentId: "agent-1",
requestId: "req-commands",
});
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("list_commands_request");
expect(request.agentId).toBe("agent-1");
expect(request.requestId).toBe("req-commands");
expect(request.draftConfig).toBeUndefined();
mock.triggerMessage(
JSON.stringify({
type: "session",
message: {
type: "list_commands_response",
payload: {
agentId: "agent-1",
commands: [],
error: null,
requestId: "req-commands",
},
},
}),
);
await expect(promise).resolves.toEqual({
agentId: "agent-1",
commands: [],
error: null,
requestId: "req-commands",
});
});
test("preserves legacy listCommands id overload", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.listCommands("agent-1", "req-commands-legacy");
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "list_commands_request",
requestId: "req-commands-legacy",
agentId: "agent-1",
});
mock.triggerMessage(
wrapSessionMessage({
type: "list_commands_response",
payload: {
agentId: "agent-1",
commands: [],
error: null,
requestId: "req-commands-legacy",
},
}),
);
await expect(promise).resolves.toEqual({
agentId: "agent-1",
commands: [],
error: null,
requestId: "req-commands-legacy",
});
});
test("preserves legacy listCommands options overload", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const promise = client.listCommands("__new_agent__", {
requestId: "req-commands-draft-legacy",
draftConfig: {
provider: "codex",
cwd: "/tmp/project",
modeId: "bypassPermissions",
model: "gpt-5",
thinkingOptionId: "off",
},
});
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "list_commands_request",
requestId: "req-commands-draft-legacy",
agentId: "__new_agent__",
draftConfig: {
provider: "codex",
cwd: "/tmp/project",
modeId: "bypassPermissions",
model: "gpt-5",
thinkingOptionId: "off",
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "list_commands_response",
payload: {
agentId: "__new_agent__",
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
error: null,
requestId: "req-commands-draft-legacy",
},
}),
);
await expect(promise).resolves.toEqual({
agentId: "__new_agent__",
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
error: null,
requestId: "req-commands-draft-legacy",
});
});
test("emits output events for the active terminal stream", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const seen: string[] = [];
const unsubscribe = client.onTerminalStreamEvent((event) => {
if (event.type !== "output") {
return;
}
seen.push(new TextDecoder().decode(event.data));
});
const subscribePromise = client.subscribeTerminal("term-1", { requestId: "sub-1" });
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscribe_terminal_request"),
);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_terminal_response",
payload: {
subscriptionId: "server-" + String(parseSentFrame(mock.sent.at(-1)).requestId),
terminalId: "term-1",
slot: 11,
error: null,
requestId: "sub-1",
},
}),
);
await subscribePromise;
mock.triggerMessage(
encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output,
slot: 11,
payload: new TextEncoder().encode("hello"),
}),
);
expect(seen).toEqual(["hello"]);
expect(mock.sent).toHaveLength(1);
unsubscribe();
});
test("emits snapshot events for the subscribed terminal stream", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const snapshots: unknown[] = [];
client.onTerminalStreamEvent((event) => {
if (event.type !== "snapshot") {
return;
}
snapshots.push(event.state);
});
const subscribePromise = client.subscribeTerminal("term-1", { requestId: "sub-2" });
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscribe_terminal_request"),
);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_terminal_response",
payload: {
subscriptionId: "server-" + String(parseSentFrame(mock.sent.at(-1)).requestId),
terminalId: "term-1",
slot: 12,
error: null,
requestId: "sub-2",
},
}),
);
await subscribePromise;
const state = {
rows: 1,
cols: 5,
grid: [[{ char: "h" }, { char: "e" }, { char: "l" }, { char: "l" }, { char: "o" }]],
scrollback: [],
cursor: { row: 0, col: 5 },
};
mock.triggerMessage(
encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Snapshot,
slot: 12,
payload: encodeTerminalSnapshotPayload(state),
}),
);
expect(snapshots).toEqual([state]);
});
test("sends explicit terminal input and resize commands", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const subscribePromise = client.subscribeTerminal("term-1", { requestId: "sub-3" });
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscribe_terminal_request"),
);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_terminal_response",
payload: {
subscriptionId: "server-" + String(parseSentFrame(mock.sent.at(-1)).requestId),
terminalId: "term-1",
slot: 13,
error: null,
requestId: "sub-3",
},
}),
);
await subscribePromise;
mock.sent.length = 0;
client.sendTerminalInput("term-1", {
type: "input",
data: "echo hello\r",
});
client.sendTerminalInput("term-1", {
type: "resize",
rows: 24,
cols: 80,
intent: "update",
});
expect(mock.sent.map(parseSentFrame)).toEqual([
{
type: "terminal_input",
terminalId: "term-1",
message: { type: "input", data: "echo hello\r" },
},
{
type: "terminal_input",
terminalId: "term-1",
message: { type: "resize", rows: 24, cols: 80, intent: "update" },
},
]);
});
test("routes concurrent terminal stream frames by slot", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const seen: string[] = [];
client.onTerminalStreamEvent((event) => {
if (event.type !== "output") {
return;
}
seen.push(`${event.terminalId}:${new TextDecoder().decode(event.data)}`);
});
const subscribeFirstPromise = client.subscribeTerminal("term-1", { requestId: "sub-multi-1" });
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscribe_terminal_request"),
);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_terminal_response",
payload: {
subscriptionId: "server-" + String(parseSentFrame(mock.sent.at(-1)).requestId),
terminalId: "term-1",
slot: 21,
error: null,
requestId: "sub-multi-1",
},
}),
);
await subscribeFirstPromise;
const subscribeSecondPromise = client.subscribeTerminal("term-2", { requestId: "sub-multi-2" });
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscribe_terminal_request"),
);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_terminal_response",
payload: {
subscriptionId: "server-" + String(parseSentFrame(mock.sent.at(-1)).requestId),
terminalId: "term-2",
slot: 22,
error: null,
requestId: "sub-multi-2",
},
}),
);
await subscribeSecondPromise;
mock.sent.length = 0;
mock.triggerMessage(
encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output,
slot: 22,
payload: new TextEncoder().encode("beta"),
}),
);
mock.triggerMessage(
encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output,
slot: 21,
payload: new TextEncoder().encode("alpha"),
}),
);
client.sendTerminalInput("term-2", {
type: "input",
data: "echo beta\r",
});
client.sendTerminalInput("term-1", {
type: "resize",
rows: 10,
cols: 20,
});
expect(seen).toEqual(["term-2:beta", "term-1:alpha"]);
expect(mock.sent.map(parseSentFrame)).toEqual([
{
type: "terminal_input",
terminalId: "term-2",
message: { type: "input", data: "echo beta\r" },
},
{
type: "terminal_input",
terminalId: "term-1",
message: { type: "resize", rows: 10, cols: 20 },
},
]);
});
test("ignores terminal stream frames after terminal_stream_exit", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const seen: string[] = [];
const unsubscribe = client.onTerminalStreamEvent((event) => {
if (event.type !== "output") {
return;
}
seen.push(new TextDecoder().decode(event.data));
});
const subscribePromise = client.subscribeTerminal("term-1", { requestId: "sub-4" });
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscribe_terminal_request"),
);
mock.triggerMessage(
wrapSessionMessage({
type: "subscribe_terminal_response",
payload: {
subscriptionId: "server-" + String(parseSentFrame(mock.sent.at(-1)).requestId),
terminalId: "term-1",
slot: 14,
error: null,
requestId: "sub-4",
},
}),
);
await subscribePromise;
mock.triggerMessage(
encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output,
slot: 14,
payload: new TextEncoder().encode("before-exit"),
}),
);
expect(seen).toEqual(["before-exit"]);
mock.triggerMessage(
wrapSessionMessage({
type: "terminal_stream_exit",
payload: {
subscriptionId: "server-sub-4",
terminalId: "term-1",
},
}),
);
mock.triggerMessage(
encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output,
slot: 14,
payload: new TextEncoder().encode("after-exit"),
}),
);
expect(seen).toEqual(["before-exit"]);
await vi.waitFor(() =>
expect(parseSentFrame(mock.sent.at(-1)).type).toBe("subscription.release.request"),
);
const release = parseSentFrame(mock.sent.at(-1));
mock.triggerMessage(
wrapSessionMessage({
type: "subscription.release.response",
payload: {
requestId: release.requestId,
subscriptionId: "server-sub-4",
released: true,
},
}),
);
await Promise.resolve();
unsubscribe();
});
test("parses canonical agent_stream tool_call payloads without crashing", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const received: unknown[] = [];
const unsubscribe = client.on("agent_stream", (msg) => {
received.push(msg);
});
mock.triggerMessage(
wrapSessionMessage({
type: "agent_stream",
payload: {
agentId: "agent_cli",
timestamp: "2026-02-08T20:20:00.000Z",
event: {
type: "timeline",
provider: "codex",
item: {
type: "tool_call",
callId: "call_cli_stream",
name: "shell",
status: "running",
detail: {
type: "shell",
command: "pwd",
},
error: null,
},
},
},
}),
);
unsubscribe();
expect(received).toHaveLength(1);
expect(received[0]).toMatchObject({
payload: {
event: {
item: {
status: "running",
error: null,
detail: { type: "shell" },
},
},
},
});
expect(logger.warn).not.toHaveBeenCalled();
});
test("drops legacy agent_stream tool_call payloads and logs validation warning", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const received: unknown[] = [];
const unsubscribe = client.on("agent_stream", (msg) => {
received.push(msg);
});
mock.triggerMessage(
wrapSessionMessage({
type: "agent_stream",
payload: {
agentId: "agent_cli",
timestamp: "2026-02-08T20:20:00.000Z",
event: {
type: "timeline",
provider: "codex",
item: {
type: "tool_call",
callId: "call_cli_stream_legacy",
name: "shell",
status: "inProgress",
detail: {
type: "unknown",
input: { command: "pwd" },
output: null,
},
},
},
},
}),
);
unsubscribe();
expect(received).toHaveLength(0);
expect(logger.warn).toHaveBeenCalled();
});
test("parses canonical fetch_agent_timeline_response payloads without crashing", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const received: unknown[] = [];
const unsubscribe = client.on("fetch_agent_timeline_response", (msg) => {
received.push(msg);
});
mock.triggerMessage(
wrapSessionMessage({
type: "fetch_agent_timeline_response",
payload: {
requestId: "req-1",
agentId: "agent_cli",
agent: null,
direction: "tail",
projection: "projected",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 1, maxSeq: 1, nextSeq: 2 },
startCursor: { epoch: "epoch-1", seq: 1 },
endCursor: { epoch: "epoch-1", seq: 1 },
hasOlder: false,
hasNewer: false,
entries: [
{
timestamp: "2026-02-08T20:20:00.000Z",
provider: "codex",
seqStart: 1,
seqEnd: 1,
sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }],
collapsed: [],
item: {
type: "tool_call",
callId: "call_cli_snapshot",
name: "shell",
status: "running",
detail: {
type: "shell",
command: "pwd",
},
error: null,
},
},
],
error: null,
},
}),
);
unsubscribe();
expect(received).toHaveLength(1);
expect(received[0]).toMatchObject({
payload: {
entries: [
{
item: {
type: "tool_call",
status: "running",
error: null,
detail: { type: "shell" },
},
},
],
},
});
expect(logger.warn).not.toHaveBeenCalled();
});
test("rejects and logs a correlated response that violates the protocol schema", async () => {
const mock = createMockTransport();
const warnings: string[] = [];
const logger: Logger = {
...noopLogger,
warn: (_fields, message) => warnings.push(message ?? ""),
};
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const response = client.fetchAgentTimeline("agent_cli", {
requestId: "req-invalid",
timeout: 1,
});
mock.triggerMessage(
wrapSessionMessage({
type: "fetch_agent_timeline_response",
payload: {
requestId: "req-invalid",
agentId: "agent_cli",
agent: null,
direction: "tail",
projection: "projected",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 1, maxSeq: 1, nextSeq: 2 },
startCursor: { epoch: "epoch-1", seq: 1 },
endCursor: { epoch: "epoch-1", seq: 1 },
hasOlder: false,
hasNewer: false,
entries: [
{
timestamp: "2026-02-08T20:20:00.000Z",
provider: "codex",
seqStart: 1,
seqEnd: 1,
sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }],
collapsed: [],
item: {
type: "tool_call",
callId: "call_cli_invalid",
name: "shell",
status: "inProgress",
detail: {
type: "unknown",
input: { command: "pwd" },
output: null,
},
},
},
],
error: null,
},
}),
);
await expect(response).rejects.toMatchObject({
requestId: "req-invalid",
message: expect.stringMatching(/validation/i),
});
expect(warnings).toEqual(["Message validation failed"]);
});
test("does not reject a request for an invalid correlated progress event", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: noopLogger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const response = client.updateDaemon("req-update");
mock.triggerMessage(
wrapSessionMessage({
type: "daemon.update.progress",
payload: {
requestId: "req-update",
phase: "verifying",
},
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "daemon.update.response",
payload: {
requestId: "req-update",
success: true,
error: null,
previousVersion: "0.1.106",
newVersion: "0.1.107",
},
}),
);
await expect(response).resolves.toMatchObject({
requestId: "req-update",
success: true,
});
});
test("sends subscribe/unsubscribe terminals messages", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const observation = client.observeTerminals({ cwd: "/tmp/project" });
await vi.waitFor(() => expect(mock.sent).toHaveLength(1));
const request = parseSentFrame(mock.sent[0]);
expect(request).toMatchObject({ type: "subscribe_terminals_request", cwd: "/tmp/project" });
expect(request.subscriptionId).toBeUndefined();
mock.triggerMessage(
wrapSessionMessage({
type: "terminals_changed",
payload: {
requestId: request.requestId,
subscriptionId: "server-terminals",
cwd: "/tmp/project",
terminals: [],
},
}),
);
await observation.ready;
const release = observation.release();
await vi.waitFor(() => expect(mock.sent).toHaveLength(2));
const requestRelease = parseSentFrame(mock.sent[1]);
expect(requestRelease).toMatchObject({
type: "subscription.release.request",
subscriptionId: "server-terminals",
});
mock.triggerMessage(
wrapSessionMessage({
type: "subscription.release.response",
payload: {
requestId: requestRelease.requestId,
subscriptionId: "server-terminals",
released: true,
},
}),
);
await release;
});
test("dispatches terminals_changed events to typed listeners", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const received: Array<{ cwd: string; names: string[] }> = [];
const unsubscribe = client.on("terminals_changed", (message) => {
received.push({
cwd: message.payload.cwd,
names: message.payload.terminals.map((terminal) => terminal.name),
});
});
mock.triggerMessage(
wrapSessionMessage({
type: "terminals_changed",
payload: {
cwd: "/tmp/project",
terminals: [
{
id: "term-1",
name: "Dev Server",
},
],
},
}),
);
unsubscribe();
expect(received).toEqual([
{
cwd: "/tmp/project",
names: ["Dev Server"],
},
]);
});
test("sends provider.usage.list.request and resolves provider.usage.list.response", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const usagePromise = client.listProviderUsage({ requestId: "usage-1" });
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "provider.usage.list.request",
requestId: "usage-1",
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "provider.usage.list.response",
payload: {
requestId: "usage-1",
fetchedAt: "2026-06-19T00:00:00.000Z",
providers: [
{
providerId: "glm",
displayName: "GLM coding plan",
status: "available",
planLabel: "GLM coding plan",
windows: [
{
id: "biweekly",
label: "Biweekly",
usedPct: 23,
remainingPct: 77,
},
],
},
],
},
}),
);
await expect(usagePromise).resolves.toEqual({
requestId: "usage-1",
fetchedAt: "2026-06-19T00:00:00.000Z",
providers: [
{
providerId: "glm",
displayName: "GLM coding plan",
status: "available",
planLabel: "GLM coding plan",
windows: [
{
id: "biweekly",
label: "Biweekly",
usedPct: 23,
remainingPct: 77,
},
],
},
],
});
});
test.each([
{
status: "available",
report: {
status: "available",
windows: [],
balances: undefined,
details: undefined,
planLabel: undefined,
},
},
{ status: "error", report: { status: "error", error: "" } },
{
status: "unavailable",
report: { status: "unavailable", problem: { kind: "no_quota", detail: "" } },
},
] as const)(
"maps released-host $status usage and filters report IDs",
async ({ status, report }) => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connected = client.connect();
mock.triggerOpen({ features: { providerUsageList: true } });
await connected;
const result = client.listUsageReports({
requestId: "legacy-usage",
reportIds: ["claude"],
forceRefresh: true,
});
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "provider.usage.list.request",
requestId: "legacy-usage",
});
const provider = {
providerId: "claude",
displayName: "Claude",
status,
windows: [],
planLabel: null,
fetchedAt: null,
error: null,
};
mock.triggerMessage(
wrapSessionMessage({
type: "provider.usage.list.response",
payload: {
requestId: "legacy-usage",
fetchedAt: "2026-09-30T00:00:00.000Z",
providers: [provider, { ...provider, providerId: "codex" }],
},
}),
);
expect(await result).toStrictEqual({
requestId: "legacy-usage",
reports: [
{
id: "claude",
sourceId: "claude",
sourceLabel: "Claude",
icon: readFileSync(
new URL("../../../plugins/claude-usage-source/icon.svg", import.meta.url),
"utf8",
),
account: {},
fetchedAt: "2026-09-30T00:00:00.000Z",
report,
},
],
});
},
);
test("sends close_items_request and resolves close_items_response", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const responsePromise = client.closeItems(
{
agentIds: ["agent-1"],
terminalIds: ["term-1"],
},
"req-close-items",
);
expect(JSON.parse(assertStr(mock.sent[0]))).toEqual({
type: "session",
message: {
type: "close_items_request",
agentIds: ["agent-1"],
terminalIds: ["term-1"],
requestId: "req-close-items",
},
});
mock.triggerMessage(
wrapSessionMessage({
type: "close_items_response",
payload: {
agents: [{ agentId: "agent-1", archivedAt: "2026-04-01T00:00:00.000Z" }],
terminals: [{ terminalId: "term-1", success: true }],
requestId: "req-close-items",
},
}),
);
await expect(responsePromise).resolves.toEqual({
agents: [{ agentId: "agent-1", archivedAt: "2026-04-01T00:00:00.000Z" }],
terminals: [{ terminalId: "term-1", success: true }],
requestId: "req-close-items",
});
});
test("waitForFinish with timeout=0 omits timeoutMs and has no client deadline", async () => {
useHeartbeatClock();
try {
const logger = createMockLogger();
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const waitPromise = client.waitForFinish("agent-wait-zero-timeout", 0);
expect(mock.sent).toHaveLength(1);
const request = parseSentFrame(mock.sent[0]);
expect(request.type).toBe("wait_for_finish_request");
expect(request.agentId).toBe("agent-wait-zero-timeout");
expect(request).not.toHaveProperty("timeoutMs");
let settled: "pending" | "resolved" | "rejected" = "pending";
void waitPromise.then(
() => {
settled = "resolved";
return null;
},
() => {
settled = "rejected";
return null;
},
);
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
expect(settled).toBe("pending");
mock.triggerMessage(
wrapSessionMessage({
type: "wait_for_finish_response",
payload: {
requestId: request.requestId,
status: "idle",
final: null,
error: null,
lastMessage: null,
},
}),
);
await expect(waitPromise).resolves.toEqual({
status: "idle",
final: null,
error: null,
lastMessage: null,
});
} finally {
vi.useRealTimers();
}
});
test("wire snapshot callers own expansion and receive hash references unchanged", async () => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "wire-catalog",
providerSnapshots: "wire",
reconnect: { enabled: false },
transportFactory: () => transport.transport,
});
clients.push(client);
const connected = client.connect();
transport.triggerOpen({ preserveSent: true });
await connected;
expect(JSON.parse(assertStr(transport.sent[0])).capabilities.provider_snapshot_references).toBe(
true,
);
const received: unknown[] = [];
client.on("providers_snapshot_update", (message) => received.push(message.payload));
const payload = {
entries: [],
snapshotHash: "content",
fetchedAt: { codex: "2026-09-06T12:00:00.000Z" },
generatedAt: "2026-09-06T12:00:00.000Z",
};
transport.triggerMessage(wrapSessionMessage({ type: "providers_snapshot_update", payload }));
expect(received).toEqual([payload]);
const request = client.getProvidersSnapshot();
const sent = parseSentFrame(transport.sent.at(-1)!);
const body = {
...payload,
compactSnapshot: {
entries: [
{
provider: "codex",
enabled: true,
status: "ready",
models: [{ id: "astra", label: "Astra" }],
},
],
thinkingSets: [],
},
requestId: sent.requestId,
};
transport.triggerMessage(
wrapSessionMessage({ type: "get_providers_snapshot_response", payload: body }),
);
expect(await request).toEqual(body);
});
test("creation lifecycle sends the keyed agent and initial prompt as one intent", async () => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "creation-contract",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connected = client.connect();
transport.triggerOpen({ features: { creationLifecycle: true, agentRequestReceipts: true } });
await connected;
const creation = client.createAgent({
idempotencyKey: "draft-one",
provider: "codex",
cwd: "/project",
workspaceId: "wks_0123456789abcdef",
initialPrompt: "Start once",
clientMessageId: "first-message",
});
void creation.catch(() => undefined);
const request = parseSentFrame(transport.sent[0]);
expect(request).toMatchObject({
type: "agent.create.request",
idempotencyKey: "draft-one",
initialPrompt: "Start once",
clientMessageId: "first-message",
});
transport.triggerMessage(
wrapSessionMessage({
type: "agent.create.response",
payload: { requestId: request.requestId, agent: null, error: "provider unavailable" },
}),
);
await expect(creation).rejects.toThrow("provider unavailable");
expect(transport.sent).toHaveLength(1);
});
test("creation lifecycle acknowledgement reaches the observer before the final response", async () => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "creation-progress",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connected = client.connect();
transport.triggerOpen({ features: { creationLifecycle: true } });
await connected;
const phases: string[] = [];
const creation = client.createAgent({
provider: "codex",
cwd: "/project",
idempotencyKey: "observe-one",
onEvent: (snapshot) => phases.push(snapshot.phase),
});
void creation.catch(() => undefined);
const request = parseSentFrame(transport.sent[0]);
transport.triggerMessage(
wrapSessionMessage({
type: "agent.create.update",
payload: {
kind: "agent",
idempotencyKey: "observe-one",
revision: 0,
phase: "accepted",
workspaceId: null,
agentId: "00000000-0000-4000-8000-000000000001",
error: null,
},
}),
);
expect(phases).toEqual(["accepted"]);
transport.triggerMessage(
wrapSessionMessage({
type: "agent.create.response",
payload: { requestId: request.requestId, agent: null, error: "provider unavailable" },
}),
);
await expect(creation).rejects.toThrow("provider unavailable");
});
test("creation reconnect observation uses connection-owned subscriptions and releases on completion", async () => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "creation-reconnect",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const features = { creationLifecycle: true, ownedSubscriptions: true };
const connect = client.connect();
transport.triggerOpen({ features });
await connect;
const phases: string[] = [];
const creation = client.createWorkspace({
idempotencyKey: "reconnect-one",
source: { kind: "directory", path: "/project" },
onEvent: (snapshot) => phases.push(snapshot.phase),
});
void creation.catch(() => undefined);
const snapshot = {
kind: "workspace",
idempotencyKey: "reconnect-one",
phase: "accepted",
revision: 0,
workspaceId: "wks_0123456789abcdef",
agentId: null,
error: null,
};
for (const subscriptionId of ["first-connection", "second-connection"]) {
transport.triggerClose();
const reconnected = client.connect();
transport.triggerOpen({ features });
await reconnected;
await expect.poll(() => transport.sent.length).toBe(1);
const request = parseSentFrame(transport.sent[0]);
expect(request).toMatchObject({
type: "creation.subscribe.request",
idempotencyKey: "reconnect-one",
});
transport.triggerMessage(
wrapSessionMessage({
type: "creation.subscribe.response",
payload: {
requestId: request.requestId,
subscriptionId,
snapshot,
error: null,
},
}),
);
}
transport.triggerMessage(
wrapSessionMessage({
type: "workspace.create.update",
payload: {
...snapshot,
subscriptionId: "second-connection",
revision: 1,
phase: "failed",
error: "Provisioning failed",
},
}),
);
expect(await creation).toMatchObject({ error: "Provisioning failed" });
await expect.poll(() => transport.sent.length).toBe(2);
const release = parseSentFrame(transport.sent.at(-1));
expect(release).toMatchObject({
type: "subscription.release.request",
subscriptionId: "second-connection",
});
transport.triggerMessage(
wrapSessionMessage({
type: "subscription.release.response",
payload: {
requestId: release.requestId,
subscriptionId: "second-connection",
},
}),
);
expect(phases).toEqual(["accepted", "failed"]);
});
test("uploadFile started while connecting waits for the connection and uploads", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "upload-while-connecting",
logger: createMockLogger(),
reconnect: { enabled: false },
suppressSendErrors: true,
transportFactory: () => mock.transport,
});
clients.push(client);
const connection = client.connect();
expect(client.getConnectionState().status).toBe("connecting");
const upload = client.uploadFile({
fileName: "notes.txt",
mimeType: "text/plain",
bytes: new TextEncoder().encode("hello world"),
modifiedAt: "2026-05-02T00:00:00.000Z",
requestId: "req-upload-connecting",
chunkSize: 5,
});
void upload.catch(() => undefined);
await new Promise((resolve) => setTimeout(resolve, 0));
mock.triggerOpen();
await connection;
await vi.waitFor(() => {
const frames = mock.sent
.filter((frame) => typeof frame !== "string")
.map(assertUint8Array)
.map(decodeFileTransferFrame);
expect(frames.map((frame) => frame.opcode)).toEqual([
FileTransferOpcode.FileBegin,
FileTransferOpcode.FileChunk,
FileTransferOpcode.FileChunk,
FileTransferOpcode.FileChunk,
FileTransferOpcode.FileEnd,
]);
});
const requestIndex = mock.sent.findIndex(
(frame) =>
typeof frame === "string" && parseSentFrame(frame).requestId === "req-upload-connecting",
);
expect(parseSentFrame(mock.sent[requestIndex])).toMatchObject({ type: "file.upload.request" });
expect(requestIndex).toBeLessThan(mock.sent.findIndex((frame) => typeof frame !== "string"));
const file = {
type: "uploaded_file" as const,
id: "upload_req-upload-connecting",
fileName: "notes.txt",
mimeType: "text/plain",
size: 11,
path: "/tmp/paseo-uploads/upload_req-upload-connecting/notes.txt",
};
mock.triggerMessage(
wrapSessionMessage({
type: "file.upload.response",
payload: { requestId: "req-upload-connecting", file, error: null },
}),
);
await expect(upload).resolves.toEqual({ requestId: "req-upload-connecting", file, error: null });
});
test("uploadFile stops sending chunks when the connection closes between sends", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "upload-interrupted",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connection = client.connect();
mock.triggerOpen();
await connection;
const upload = client.uploadFile({
fileName: "test.bin",
mimeType: "application/octet-stream",
bytes: new Uint8Array(1024 * 1024),
});
const rejection = expect(upload).rejects.toThrow();
await new Promise((resolve) => setTimeout(resolve, 0));
const beforeClose = mock.sent.length;
mock.triggerClose();
await rejection;
await new Promise((resolve) => setTimeout(resolve, 10));
expect(mock.sent).toHaveLength(beforeClose);
expect(
mock.sent
.filter((frame) => typeof frame !== "string")
.map(assertUint8Array)
.map(decodeFileTransferFrame)
.some((frame) => frame.opcode === FileTransferOpcode.FileEnd),
).toBe(false);
});
test("rejects source installation on an older host before sending a request", async () => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "source-gate",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => transport.transport,
});
clients.push(client);
const connecting = client.connect();
transport.triggerOpen({ features: { pluginGitManagement: true } });
await connecting;
const sentBefore = transport.sent.length;
await expect(client.installPluginSource({ source: "npm:review" })).rejects.toThrow(
"Update the host",
);
expect(transport.sent.length).toBe(sentBefore);
});
test("reviewed plugin updates gate before requests and preserve exact proposal data", async () => {
for (const supported of [false, true]) {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "reviewed-updates",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => transport.transport,
});
clients.push(client);
const connecting = client.connect();
transport.triggerOpen({
features: {
pluginGitManagement: true,
pluginSourceInstallation: true,
pluginSourceUpdates: supported,
},
});
await connecting;
const proposal = {
id: "review",
expected: {
identity: { kind: "npm" as const, packageName: "review", pluginPath: "." },
installationRoot: "/plugins/review/version-root",
revision: "1.0.0",
},
target: {
kind: "npm" as const,
version: "1.1.0",
resolved: "https://registry.npmjs.org/review/-/review-1.1.0.tgz",
integrity: "sha512-test",
},
};
if (!supported) {
const sent = transport.sent.length;
await expect(client.previewPluginUpdates()).rejects.toThrow("Update the host");
await expect(client.applyPluginUpdates([proposal])).rejects.toThrow("Update the host");
expect(transport.sent.length).toBe(sent);
continue;
}
const checking = client.previewPluginUpdates({ pluginId: "review" });
const check = parseSentFrame(transport.sent.at(-1));
expect(check).toMatchObject({
type: "plugin.source.update.preview.request",
pluginId: "review",
});
const preview = { id: "review", outcome: "update", links: [], proposal };
transport.triggerMessage(
wrapSessionMessage({
type: "plugin.source.update.preview.response",
payload: { requestId: check.requestId, plugins: [preview] },
}),
);
await expect(checking).resolves.toEqual([preview]);
const applying = client.applyPluginUpdates([proposal]);
const apply = parseSentFrame(transport.sent.at(-1));
expect(apply).toEqual({
type: "plugin.source.update.apply.request",
requestId: expect.any(String),
proposals: [proposal],
});
transport.triggerMessage(
wrapSessionMessage({
type: "plugin.source.update.apply.response",
payload: {
requestId: apply.requestId,
plugins: [{ id: "review", outcome: "error", error: "changed since review" }],
},
}),
);
await expect(applying).resolves.toEqual([
{ id: "review", outcome: "error", error: "changed since review" },
]);
}
});
test.each([
[undefined, false],
[{}, false],
[{ usageSources: false, providerUsageList: false }, false],
[{ usageSources: true }, true],
[{ providerUsageList: true }, true],
[{ usageSources: true, providerUsageList: true }, true],
] as const)("usage support for features %j is %s", (features, expected) => {
expect(supportsUsageReports(features)).toBe(expected);
});
test("uses modern usage RPC when both capabilities are advertised", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connected = client.connect();
mock.triggerOpen({ features: { usageSources: true, providerUsageList: true } });
await connected;
const updates: unknown[] = [];
const result = client.listUsageReports(
{
requestId: "modern-usage",
reportIds: ["claude:work"],
forceRefresh: true,
},
(report) => updates.push(report),
);
expect(parseSentFrame(mock.sent[0])).toEqual({
type: "usage.list_reports.request",
requestId: "modern-usage",
reportIds: ["claude:work"],
forceRefresh: true,
});
const payload = {
requestId: "modern-usage",
reports: [
{
id: "claude:work",
sourceId: "claude",
sourceLabel: "Claude",
account: { label: "Work" },
fetchedAt: "2026-09-30T00:00:00.000Z",
report: { status: "available", windows: [] },
},
],
};
const report = payload.reports[0];
mock.triggerMessage(
wrapSessionMessage({
type: "usage.list_reports.update",
payload: { requestId: "foreign", report },
}),
);
expect(updates).toEqual([]);
mock.triggerMessage(
wrapSessionMessage({
type: "usage.list_reports.update",
payload: { requestId: payload.requestId, report },
}),
);
expect(updates).toEqual([report]);
mock.triggerMessage(
wrapSessionMessage({
type: "usage.list_reports.response",
payload: { requestId: payload.requestId, error: null },
}),
);
mock.triggerMessage(
wrapSessionMessage({
type: "usage.list_reports.update",
payload: { requestId: payload.requestId, report },
}),
);
expect(await result).toStrictEqual(payload);
expect(updates).toEqual([report]);
});
test("rejects usage requests when the host has neither capability", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connected = client.connect();
mock.triggerOpen({ features: {} });
await connected;
await expect(client.listUsageReports()).rejects.toThrow("Update the host to see usage.");
expect(mock.sent).toEqual([]);
});
test("usage request timeout detaches its update listener", async () => {
vi.useFakeTimers();
try {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_unit_test",
logger: createMockLogger(),
reconnect: { enabled: false },
transportFactory: () => mock.transport,
});
clients.push(client);
const connected = client.connect();
mock.triggerOpen({ features: { usageSources: true } });
await connected;
const updates: unknown[] = [];
const result = client.listUsageReports({ requestId: "timeout-usage" }, (report) =>
updates.push(report),
);
const rejected = expect(result).rejects.toThrow("Timeout waiting for message (60000ms)");
await vi.advanceTimersByTimeAsync(60_001);
await rejected;
mock.triggerMessage(
wrapSessionMessage({
type: "usage.list_reports.update",
payload: {
requestId: "timeout-usage",
report: {
id: "source:late",
account: {},
sourceId: "source",
sourceLabel: "Source",
fetchedAt: "2026-01-01T00:00:00.000Z",
report: { status: "available", windows: [] },
},
},
}),
);
expect(updates).toEqual([]);
} finally {
vi.useRealTimers();
}
});
test.each([false, true])(
"agent provenance requires host support=%s while human sends remain compatible",
async (supported) => {
const transport = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "provenance",
transportFactory: () => transport.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
transport.triggerOpen({ features: { agentMessageProvenance: supported } });
await connecting;
if (!supported) {
await expect(
client.sendAgentMessage("recipient", "review", { sourceAgentId: "server::sender" }),
).rejects.toThrow("Update the Paseo host");
expect(transport.sent).toHaveLength(0);
}
const send = client.sendAgentMessage("recipient", "review", {
messageId: "delivery",
...(supported ? { sourceAgentId: "server::sender" } : {}),
});
const request = parseSentFrame(transport.sent[0]);
expect(request).toMatchObject({
type: "send_agent_message_request",
text: "review",
messageId: "delivery",
});
expect(request.sourceAgentId).toBe(supported ? "server::sender" : undefined);
transport.triggerMessage(
wrapSessionMessage({
type: "send_agent_message_response",
payload: {
requestId: request.requestId,
agentId: "recipient",
accepted: true,
error: null,
},
}),
);
await send;
},
);
test("measures an outbound message in UTF-8 bytes against the daemon's message limit", async () => {
const mock = createMockTransport();
const client = new DaemonClient({
url: "ws://test",
clientId: "message_limit_unit_test",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connectPromise = client.connect();
mock.triggerOpen();
await connectPromise;
const mib = 1024 * 1024;
// "é" is one UTF-16 unit but two UTF-8 bytes, so this text is ~98 MiB on the wire.
void client.sendMessage("agent-1", "é".repeat(49 * mib)).catch(() => undefined);
expect(mock.sent).toHaveLength(1);
// ~60M characters is under the 100 MiB limit; ~120 MiB of UTF-8 is over it.
await expect(client.sendMessage("agent-1", "é".repeat(60 * mib))).rejects.toThrow(
"Message is too large to send (120.0 MiB). The limit is 100.0 MiB",
);
expect(mock.sent).toHaveLength(1);
expect(client.getConnectionState().status).toBe("connected");
});