Files
pi/packages/coding-agent/test/experimental-session-worker-manager.test.ts
Mario Zechner 48dd1e2f0f feat(coding-agent): port the experimental client/server to pi-durable
Session workers now host a durable Harness over a per-session sqlite storage.
The server keeps each session in <sessionDir>/<id>/ with meta.json and
session.sqlite and never opens the storage itself. Workers hold retirement
while the Harness task graph has live tasks and resume interrupted work on
open.

Transcript serves the root conversation's durable view directly, Models
follows the conversation's agent document, and AgentController maps prompt,
steer, follow-up, queue cancellation, abort, and compaction onto the root
conversation, plus waitForPrompt. navigate, nextRun, and resume are dropped
(see experimental/services/README.md TODO). The durable TUI and the worker
share harness-setup.ts.

pi-server exports its own SessionMetadata and no longer depends on
pi-agent-core. Chord accepts readonly JSON types in service contracts without
walking them, which recursive JSON types like pi-ai's made too deep.
2026-10-01 14:45:01 +02:00

309 lines
10 KiB
TypeScript

import type { ServiceCall } from "@earendil-works/chord";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { afterEach, describe, expect, test, vi } from "vitest";
import type { CoordinatorConnectionEvent } from "../src/experimental/coordinator.ts";
import type { SessionCatalogMetadata } from "../src/experimental/session-catalog.ts";
import { SessionWorkerManager } from "../src/experimental/session-worker-manager.ts";
const metadata: SessionCatalogMetadata = {
id: "session-1",
createdAt: 1,
cwd: "/tmp",
path: "/tmp/session-1",
};
class FakeCoordinator {
readonly controlPath = "/tmp/control.sock";
readonly serverConnectionId = "server-generation-1";
readonly wasReplaced = false;
readonly sent: { peerId: string; payload: unknown }[] = [];
readonly #listeners = new Set<(event: CoordinatorConnectionEvent) => void>();
onSend?: (peerId: string, payload: Record<string, unknown>) => void;
onEvent(listener: (event: CoordinatorConnectionEvent) => void): () => void {
this.#listeners.add(listener);
return () => this.#listeners.delete(listener);
}
async send(peerId: string, payload: unknown): Promise<void> {
this.sent.push({ peerId, payload });
this.onSend?.(peerId, asObject(payload));
}
async broadcast(payload: unknown): Promise<void> {
if (asObject(payload).type !== "discover_workers") return;
this.emit({
type: "message",
from: "worker-1",
payload: {
type: "worker_ready",
token: "worker-token",
sessionKey: metadata.path,
sessionId: metadata.id,
pid: 123,
metadata,
pluginManifestPaths: [],
},
});
}
emit(event: CoordinatorConnectionEvent): void {
for (const listener of this.#listeners) listener(event);
}
}
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
async function createAttachedWorker(): Promise<{
coordinator: FakeCoordinator;
workers: SessionWorkerManager;
handle: Awaited<ReturnType<SessionWorkerManager["openSession"]>>;
attachment: Awaited<ReturnType<Awaited<ReturnType<SessionWorkerManager["openSession"]>>["attachClient"]>>;
release(): Promise<void>;
}> {
const coordinator = new FakeCoordinator();
const workers = new SessionWorkerManager(coordinator, "/tmp");
await workers.discover(new Set(["worker-1"]));
const handle = await workers.openSession(metadata, BACKGROUND_CONTEXT, []);
coordinator.onSend = (peerId, payload) => {
if (payload.type !== "session_demand") return;
queueMicrotask(() =>
coordinator.emit({
type: "message",
from: peerId,
payload: {
type: "demand_applied",
token: "worker-token",
sessionKey: metadata.path,
requestId: payload.requestId,
attachmentId: payload.attachmentId,
attached: payload.attached,
},
}),
);
};
const attachment = await handle.attachClient!(BACKGROUND_CONTEXT);
return {
coordinator,
workers,
handle,
attachment,
release: () => Promise.resolve(attachment.release(BACKGROUND_CONTEXT)),
};
}
describe("Session worker lifecycle failures", () => {
test("adopts a discovered worker with its existing Session plugin selection", async () => {
const coordinator = new FakeCoordinator();
const workers = new SessionWorkerManager(coordinator, "/tmp");
await workers.discover(new Set(["worker-1"]));
expect(coordinator.sent).not.toContainEqual({ peerId: "worker-1", payload: { type: "shutdown" } });
expect(workers.workerPids.size).toBe(1);
expect(() => workers.assertSessionPluginManifestPaths(metadata, [])).not.toThrow();
workers.detach();
});
test("rejects a different plugin selection for an active Session without stopping it", async () => {
const { workers } = await createAttachedWorker();
expect(() => workers.assertSessionPluginManifestPaths(metadata, ["/tmp/plugin/chord-facets.json"])).toThrow(
"active with a different plugin selection",
);
expect(workers.workerPids.size).toBe(1);
workers.detach();
});
test("compensates a timed-out attachment before rejecting it", async () => {
vi.useFakeTimers();
const coordinator = new FakeCoordinator();
const workers = new SessionWorkerManager(coordinator, "/tmp");
await workers.discover(new Set(["worker-1"]));
const handle = await workers.openSession(metadata, BACKGROUND_CONTEXT, []);
const demands: { attachmentId: string; attached: boolean }[] = [];
coordinator.onSend = (peerId, payload) => {
if (
payload.type !== "session_demand" ||
typeof payload.attachmentId !== "string" ||
typeof payload.attached !== "boolean"
) {
return;
}
demands.push({ attachmentId: payload.attachmentId, attached: payload.attached });
if (payload.attached) return;
queueMicrotask(() =>
coordinator.emit({
type: "message",
from: peerId,
payload: {
type: "demand_applied",
token: "worker-token",
sessionKey: metadata.path,
requestId: payload.requestId,
attachmentId: payload.attachmentId,
attached: false,
},
}),
);
};
const attaching = expect(handle.attachClient!(BACKGROUND_CONTEXT)).rejects.toThrow("timed out");
await vi.advanceTimersByTimeAsync(5_000);
await attaching;
expect(demands).toHaveLength(2);
expect(demands[0]).toMatchObject({ attachmentId: expect.any(String), attached: true });
expect(demands[1]).toEqual({ attachmentId: demands[0]!.attachmentId, attached: false });
workers.detach();
});
test("kills a worker when timed-out demand cannot be reconciled", async () => {
vi.useFakeTimers();
const kill = vi.spyOn(process, "kill").mockReturnValue(true);
const coordinator = new FakeCoordinator();
const workers = new SessionWorkerManager(coordinator, "/tmp");
await workers.discover(new Set(["worker-1"]));
const handle = await workers.openSession(metadata, BACKGROUND_CONTEXT, []);
coordinator.onSend = () => {};
const attaching = expect(handle.attachClient!(BACKGROUND_CONTEXT)).rejects.toThrow("worker was terminated");
await vi.advanceTimersByTimeAsync(5_000);
await vi.advanceTimersByTimeAsync(5_000);
await vi.advanceTimersByTimeAsync(10_000);
await attaching;
expect(kill).toHaveBeenCalledWith(123, "SIGKILL");
expect(workers.workerPids.size).toBe(0);
workers.detach();
});
test("bounds Harness-driven worker shutdown", async () => {
vi.useFakeTimers();
const kill = vi.spyOn(process, "kill").mockReturnValue(true);
const { coordinator, workers, handle, release } = await createAttachedWorker();
await release();
coordinator.onSend = () => {};
const closing = handle.close(BACKGROUND_CONTEXT);
await vi.advanceTimersByTimeAsync(10_000);
await closing;
expect(kill).toHaveBeenCalledWith(123, "SIGKILL");
expect(workers.workerPids.size).toBe(0);
workers.detach();
});
});
describe("Session worker operations", () => {
const serviceCall = { serviceId: "test.session", member: "run", args: ["Hello"] } satisfies ServiceCall;
test("correlates service results to the worker generation and attachment", async () => {
const { coordinator, workers, attachment, release } = await createAttachedWorker();
coordinator.onSend = (peerId, payload) => {
if (payload.type !== "operation") return;
const scope = asObject(payload.scope);
queueMicrotask(() => {
coordinator.emit({
type: "message",
from: peerId,
payload: {
type: "operation_response",
token: "worker-token",
sessionKey: metadata.path,
response: {
type: "operation_result",
requestId: payload.requestId,
scope,
result: { accepted: true },
},
},
});
});
};
await expect(attachment.invokeService(serviceCall, () => {}, BACKGROUND_CONTEXT)).resolves.toEqual({
accepted: true,
});
const operation = coordinator.sent
.map(({ payload }) => asObject(payload))
.find(({ type }) => type === "operation");
expect(operation).toMatchObject({
scope: { serverConnectionId: "server-generation-1", attachmentId: expect.any(String) },
call: serviceCall,
});
workers.detach();
await release();
});
test("rejects a correlated response with mismatched worker identity", async () => {
const { coordinator, workers, attachment, release } = await createAttachedWorker();
coordinator.onSend = (peerId, payload) => {
if (payload.type !== "operation") return;
queueMicrotask(() =>
coordinator.emit({
type: "message",
from: peerId,
payload: {
type: "operation_response",
token: "wrong-token",
sessionKey: metadata.path,
response: {
type: "operation_result",
requestId: payload.requestId,
scope: payload.scope,
result: { accepted: true },
},
},
}),
);
};
await expect(attachment.invokeService(serviceCall, () => {}, BACKGROUND_CONTEXT)).rejects.toThrow(
/mismatched operation response/,
);
workers.detach();
await release();
});
test("rejects a null request scope", async () => {
const { coordinator, workers, attachment, release } = await createAttachedWorker();
coordinator.onSend = (peerId, payload) => {
if (payload.type !== "operation") return;
queueMicrotask(() =>
coordinator.emit({
type: "message",
from: peerId,
payload: {
type: "operation_response",
token: "worker-token",
sessionKey: metadata.path,
response: {
type: "operation_result",
requestId: payload.requestId,
scope: null,
result: { accepted: true },
},
},
}),
);
};
await expect(attachment.invokeService(serviceCall, () => {}, BACKGROUND_CONTEXT)).rejects.toThrow(
/invalid operation response/,
);
workers.detach();
await release();
});
test("rejects pending service calls on replacement without stopping the worker", async () => {
const { coordinator, workers, attachment } = await createAttachedWorker();
coordinator.onSend = () => {};
const calling = attachment.invokeService(serviceCall, () => {}, BACKGROUND_CONTEXT);
workers.detach();
await expect(calling).rejects.toThrow(/replaced during a worker operation/);
expect(coordinator.sent.map(({ payload }) => asObject(payload).type)).not.toContain("shutdown");
expect(workers.workerPids.size).toBe(0);
});
});
function asObject(value: unknown): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Expected object");
return value as Record<string, unknown>;
}