Squashed 'third_party/pi/' content from commit d981de122

git-subtree-dir: third_party/pi
git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
2026-09-14 10:40:08 -04:00
commit 67b361f0e9
1669 changed files with 397627 additions and 0 deletions

View File

@@ -0,0 +1,502 @@
import type { ServiceCall } from "@earendil-works/chord";
import { BACKGROUND_CONTEXT, type SessionMetadata } from "@earendil-works/pi-agent-core";
import { afterEach, describe, expect, test } from "vitest";
import type { ByteConnection, ByteConnectionHandler } from "../src/connection.ts";
import { SessionAmbiguousError } from "../src/errors.ts";
import { Server } from "../src/server.ts";
import {
createTestServerServices,
Deferred,
ProtocolTestClient,
TestServerHost,
type WireChannel,
} from "../src/testing/index.ts";
import type { ServerHost } from "../src/types.ts";
const serverId = "00000000-0000-4000-8000-000000000001";
const servers = new Set<Server>();
function createServer(host: ServerHost, id = serverId): Server {
const server = new Server(host, { listeners: [], serverId: id });
servers.add(server);
return server;
}
function connect(server: Server): ProtocolTestClient {
let handler: ByteConnectionHandler;
let client: ProtocolTestClient;
let closed = false;
const connection: ByteConnection = {
get closed() {
return closed;
},
async send(chunk) {
client.receive(chunk);
},
close(finalChunk) {
if (finalChunk) client.receive(finalChunk);
closed = true;
client.markClosed();
},
};
const channel: WireChannel = {
async send(chunk) {
handler.onData(chunk);
},
async sendFragmented(chunk, splitAt) {
handler.onData(chunk.subarray(0, splitAt));
handler.onData(chunk.subarray(splitAt));
},
async close() {
if (closed) return;
closed = true;
handler.onClose();
client.markClosed();
},
};
client = new ProtocolTestClient(channel);
handler = server.accept(connection);
return client;
}
function sessionCall(member: string, args: ServiceCall["args"] = []): ServiceCall {
return { serviceId: "test.session", member, args };
}
function latestAttachmentId(client: ProtocolTestClient, sessionId: string): string {
for (const message of [...client.messages].reverse()) {
if (message.type === "attachment" && message.attachment?.sessionId === sessionId) {
return message.attachment.attachmentId;
}
}
throw new Error(`Missing attachment for ${sessionId}`);
}
afterEach(async () => {
await Promise.all([...servers].map((server) => server.close()));
servers.clear();
});
describe("Session protocol", () => {
test("handshake identifies the logical server without listing sessions", async () => {
const host = new TestServerHost();
await host.seed();
const client = connect(createServer(host));
expect(await client.hello()).toMatchObject({ type: "hello", serverId });
expect(host.harnesses.size).toBe(0);
});
test("rejects a semantically invalid service call after envelope decoding", async () => {
const host = new TestServerHost();
const client = connect(createServer(host));
await client.hello();
const response = client.next((message) => message.type === "response" && message.id === "invalid-call");
await client.sendMessage({
type: "request",
id: "invalid-call",
target: { serverId },
call: { arbitrary: true },
});
expect(await response).toMatchObject({ ok: false, error: { code: "invalid_request" } });
});
test("attach passes concrete repository metadata to the Harness host", async () => {
type BackendMetadata = SessionMetadata & { path: string; modifiedAt: number };
const metadata: BackendMetadata = {
id: "session-1",
createdAt: 1,
storageVersion: 1,
cwd: "/workspace",
path: "/sessions/session-1.jsonl",
modifiedAt: 2,
};
let received: BackendMetadata | undefined;
const host: ServerHost<BackendMetadata> = {
serverServices: createTestServerServices(),
resolveSession: async () => metadata,
openSession: async (candidate) => {
received = candidate;
return {
attachClient: () => ({ invokeService: async () => undefined, release() {} }),
close: async () => {},
};
},
};
const client = connect(createServer(host));
await client.hello();
await expect(client.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
expect(received).toBe(metadata);
});
test("routes opaque server services and publishes attachment changes out of band", async () => {
const backing = new TestServerHost();
await backing.seed("session-1");
let releaseCount = 0;
const host: ServerHost = {
resolveSession: (sessionId, context) => backing.resolveSession(sessionId, context),
openSession: (metadata, context) => backing.openSession(metadata, context),
serverServices: {
attachClient(presentation) {
return {
async invokeService(call, _publish, context) {
if (call.serviceId !== "pi.session-management") throw new Error("Unexpected service");
if (call.member === "attach" && typeof call.args[0] === "string") {
await presentation.attachSession(call.args[0], context);
return null;
}
if (call.member === "detach") {
await presentation.detachSession(context);
return null;
}
throw new Error("Unexpected service member");
},
release() {
releaseCount += 1;
},
};
},
},
};
const client = connect(createServer(host));
await client.hello();
const attached = client.next((message) => message.type === "attachment" && message.attachment !== null);
await expect(client.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
await expect(attached).resolves.toMatchObject({
type: "attachment",
attachment: { sessionId: "session-1", attachmentId: expect.any(String) },
});
expect(backing.latestHarness("session-1").attachedClients).toBe(1);
const detached = client.next((message) => message.type === "attachment" && message.attachment === null);
await expect(
client.requestService({ serverId }, { serviceId: "pi.session-management", member: "detach", args: [] }),
).resolves.toMatchObject({ ok: true });
await expect(detached).resolves.toMatchObject({ type: "attachment", attachment: null });
expect(backing.latestHarness("session-1").attachedClients).toBe(0);
await client.close();
await expect.poll(() => releaseCount).toBe(1);
});
test("permits multiple client attachments per Session", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const server = createServer(host);
const first = connect(server);
const second = connect(server);
await Promise.all([first.hello(), second.hello()]);
await expect(first.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
await expect(first.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
expect(host.latestHarness("session-1").attachedClients).toBe(1);
await expect(second.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
expect(host.harnesses.get("session-1")).toHaveLength(1);
expect(host.latestHarness("session-1").attachedClients).toBe(2);
await first.close();
await expect.poll(() => host.latestHarness("session-1").attachedClients).toBe(1);
await expect(second.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
});
test("clears connection ownership when attachment release fails", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const errors: Error[] = [];
const server = new Server(host, { listeners: [], serverId, onError: (error) => errors.push(error) });
servers.add(server);
const first = connect(server);
const second = connect(server);
await Promise.all([first.hello(), second.hello()]);
await first.attach(serverId, "session-1");
const harness = host.latestHarness("session-1");
const releaseError = new Error("release failed");
harness.failAttachmentRelease = releaseError;
await first.close();
await expect.poll(() => harness.attachmentReleaseCount).toBe(1);
await expect.poll(() => errors).toContain(releaseError);
harness.failAttachmentRelease = undefined;
await expect(second.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
});
test("requires the requesting client to hold the targeted Session attachment", async () => {
const host = new TestServerHost();
await Promise.all([host.seed("session-1"), host.seed("session-2")]);
const server = createServer(host);
const attached = connect(server);
const unattached = connect(server);
await Promise.all([attached.hello(), unattached.hello()]);
await expect(unattached.requestSessionService(serverId, "session-1", sessionCall("run"))).resolves.toMatchObject({
ok: false,
error: { code: "session_not_attached" },
});
await attached.attach(serverId, "session-1");
await expect(attached.requestSessionService(serverId, "session-2", sessionCall("run"))).resolves.toMatchObject({
ok: false,
error: { code: "session_not_attached" },
});
await expect(
attached.requestSessionService(serverId, "session-1", sessionCall("run", ["Hello"])),
).resolves.toMatchObject({ ok: true, result: { ok: true } });
expect(host.latestHarness("session-1").serviceCalls).toEqual([sessionCall("run", ["Hello"])]);
});
test("rejects a stale attachment route after switching Sessions", async () => {
const host = new TestServerHost();
await Promise.all([host.seed("session-1"), host.seed("session-2")]);
const client = connect(createServer(host));
await client.hello();
await client.attach(serverId, "session-1");
const firstAttachmentId = latestAttachmentId(client, "session-1");
await client.attach(serverId, "session-2");
await expect(
client.requestService(
{ serverId, sessionId: "session-1", attachmentId: firstAttachmentId },
sessionCall("run", ["stale"]),
),
).resolves.toMatchObject({ ok: false, error: { code: "session_not_attached" } });
expect(host.latestHarness("session-1").serviceCalls).toEqual([]);
});
test("preserves opaque service results and bounds adapter defects", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const client = connect(createServer(host));
await client.hello();
await client.attach(serverId, "session-1");
const harness = host.latestHarness("session-1");
harness.nextServiceResult = { accepted: false, reason: "closed" };
await expect(client.requestSessionService(serverId, "session-1", sessionCall("run"))).resolves.toMatchObject({
ok: true,
result: { accepted: false, reason: "closed" },
});
harness.nextServiceError = new Error("private adapter detail");
await expect(client.requestSessionService(serverId, "session-1", sessionCall("run"))).resolves.toMatchObject({
ok: false,
error: { code: "internal_error", message: "Internal server error" },
});
});
test("admits concurrent service calls to the attached Session", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const client = connect(createServer(host));
await client.hello();
await client.attach(serverId, "session-1");
const harness = host.latestHarness("session-1");
const gate = harness.gateNextServiceCall();
const first = client.requestSessionService(serverId, "session-1", sessionCall("run", ["first"]));
await gate.entered.promise;
const second = client.requestSessionService(serverId, "session-1", sessionCall("run", ["second"]));
await expect(second).resolves.toMatchObject({ ok: true });
expect(harness.serviceCalls).toEqual([sessionCall("run", ["first"]), sessionCall("run", ["second"])]);
gate.release.resolve(undefined);
await expect(first).resolves.toMatchObject({ ok: true });
});
test("keeps attachment demand until an accepted service call settles after disconnect", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const client = connect(createServer(host));
await client.hello();
await client.attach(serverId, "session-1");
const harness = host.latestHarness("session-1");
const gate = harness.gateNextServiceCall();
const calling = client.requestSessionService(serverId, "session-1", sessionCall("run"));
const disconnectedCall = expect(calling).rejects.toThrow(/closed/i);
await gate.entered.promise;
await client.close();
expect(harness.attachedClients).toBe(1);
gate.release.resolve(undefined);
await disconnectedCall;
await expect.poll(() => harness.attachedClients).toBe(0);
});
test("rejects requests addressed to another server before repository access", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const client = connect(createServer(host));
await client.hello();
await expect(client.attach("00000000-0000-4000-8000-000000000002", "session-1")).resolves.toMatchObject({
ok: false,
error: { code: "wrong_server" },
});
expect(host.harnesses.size).toBe(0);
});
test("reports an unknown session without creating a Harness", async () => {
const host = new TestServerHost();
const client = connect(createServer(host));
await client.hello();
await expect(client.attach(serverId, "missing")).resolves.toMatchObject({
ok: false,
error: { code: "session_not_found" },
});
expect(host.harnesses.size).toBe(0);
});
test("rejects an ambiguous session ID without creating a Harness", async () => {
type BackendMetadata = SessionMetadata & { path: string };
const host: ServerHost<BackendMetadata> = {
serverServices: createTestServerServices(),
resolveSession: async () => {
throw new SessionAmbiguousError();
},
openSession: async () => {
throw new Error("must not create a Harness for an ambiguous session");
},
};
const client = connect(createServer(host));
await client.hello();
await expect(client.attach(serverId, "duplicate")).resolves.toMatchObject({
ok: false,
error: { code: "session_ambiguous" },
});
});
test("invalidates a terminated Harness handle and allows a later attach", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const client = connect(createServer(host));
await client.hello();
await client.attach(serverId, "session-1");
const firstHarness = host.latestHarness("session-1");
await firstHarness.terminate(new Error("worker crashed"));
await firstHarness.terminated;
await expect.poll(() => firstHarness.attachedClients).toBe(0);
expect(firstHarness.attachmentReleaseCount).toBe(1);
await expect(client.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
expect(host.harnesses.get("session-1")).toHaveLength(2);
});
test("connection loss releases its attachment, while server shutdown closes the Harness", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const server = createServer(host);
const client = connect(server);
await client.hello();
await client.attach(serverId, "session-1");
const harness = host.latestHarness("session-1");
await client.close();
await expect.poll(() => harness.attachedClients).toBe(0);
expect(harness.closeCount).toBe(0);
await server.close();
expect(harness.closeCount).toBe(1);
});
});
describe("routed Session acquisition failures", () => {
test("releases a lease acquired concurrently with Harness termination", async () => {
const metadata: SessionMetadata = { id: "session-1", createdAt: 1, storageVersion: 1 };
const acquiring = new Deferred<void>();
const continueAcquiring = new Deferred<void>();
const terminated = new Deferred<Error | undefined>();
let releaseCount = 0;
const host: ServerHost = {
serverServices: createTestServerServices(),
resolveSession: async () => metadata,
openSession: async () => ({
terminated: terminated.promise,
attachClient: async () => {
acquiring.resolve(undefined);
await continueAcquiring.promise;
return {
invokeService: async () => undefined,
release: () => {
releaseCount += 1;
},
};
},
close: async () => {},
}),
};
const client = connect(createServer(host));
await client.hello();
const attach = client.attach(serverId, "session-1");
await acquiring.promise;
terminated.resolve(new Error("worker crashed"));
continueAcquiring.resolve(undefined);
await expect(attach).resolves.toMatchObject({ ok: false, error: { code: "server_draining" } });
expect(releaseCount).toBe(1);
});
test("shares a Harness creation failure, releases the Session, and allows a later retry", async () => {
const host = new TestServerHost();
await host.seed("session-1");
host.nextOpenSessionError = new Error("Harness creation failed");
const server = createServer(host);
const first = connect(server);
const second = connect(server);
await Promise.all([first.hello(), second.hello()]);
const gate = host.gateNextOpenSession();
const firstAttach = first.attach(serverId, "session-1");
await gate.entered.promise;
const secondAttach = second.attach(serverId, "session-1");
gate.release.resolve(undefined);
await expect(Promise.all([firstAttach, secondAttach])).resolves.toMatchObject([
{ ok: false, error: { code: "internal_error" } },
{ ok: false, error: { code: "internal_error" } },
]);
expect(host.openSessionCount).toBe(1);
await expect(first.attach(serverId, "session-1")).resolves.toMatchObject({ ok: true });
expect(host.openSessionCount).toBe(2);
expect(host.harnesses.get("session-1")).toHaveLength(1);
});
test("closes a Harness acquired while server shutdown is in progress", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const server = createServer(host);
const client = connect(server);
await client.hello();
const gate = host.gateNextOpenSession();
const attach = client.attach(serverId, "session-1");
await gate.entered.promise;
const closing = server.close();
gate.release.resolve(undefined);
await closing;
await expect(attach).rejects.toThrow(/closed/i);
expect(host.latestHarness("session-1").closeCount).toBe(1);
});
test("fails shutdown when an in-flight acquisition cannot release its Harness", async () => {
const host = new TestServerHost();
await host.seed("session-1");
const cleanupError = new Error("close failed");
host.nextHarnessCloseError = cleanupError;
const server = createServer(host);
const client = connect(server);
await client.hello();
const gate = host.gateNextOpenSession();
const attach = client.attach(serverId, "session-1");
await gate.entered.promise;
const closing = server.close();
gate.release.resolve(undefined);
await expect(closing).rejects.toThrow(/Failed to close routed Sessions/);
await expect(server.closed).rejects.toThrow(/Failed to close routed Sessions/);
await expect(attach).rejects.toThrow(/closed/i);
expect(host.latestHarness("session-1").closeCount).toBe(1);
servers.delete(server);
await host.latestHarness("session-1").close(BACKGROUND_CONTEXT);
});
});

View File

@@ -0,0 +1,9 @@
import { createServer } from "node:net";
const path = process.argv[2];
if (!path) throw new Error("Socket path argument is required");
const server = createServer();
server.listen(path, () => {
if (process.send) process.send("listening");
});

View File

@@ -0,0 +1,52 @@
import { describe, expect, test } from "vitest";
import type { ByteConnectionAcceptor } from "../src/connection.ts";
import type { ServerListener } from "../src/listener.ts";
import { createTestServer } from "../src/testing/index.ts";
class TestListener implements ServerListener {
accept?: ByteConnectionAcceptor;
startCount = 0;
closeCount = 0;
readonly startError: Error | undefined;
constructor(startError?: Error) {
this.startError = startError;
}
async start(accept: ByteConnectionAcceptor): Promise<void> {
this.startCount += 1;
this.accept = accept;
if (this.startError) throw this.startError;
}
async close(): Promise<void> {
this.closeCount += 1;
}
}
describe("Server listener composition", () => {
test("starts and closes every configured listener", async () => {
const first = new TestListener();
const second = new TestListener();
const { server } = createTestServer({ listeners: [first, second] });
await server.start();
expect(first.accept).toBeTypeOf("function");
expect(second.accept).toBeTypeOf("function");
await server.close();
expect(first.closeCount).toBe(1);
expect(second.closeCount).toBe(1);
});
test("closes previously started listeners when startup fails", async () => {
const first = new TestListener();
const failure = new Error("listener failed");
const second = new TestListener(failure);
const { server } = createTestServer({ listeners: [first, second] });
await expect(server.start()).rejects.toBe(failure);
expect(first.closeCount).toBe(1);
expect(second.closeCount).toBe(0);
});
});

View File

@@ -0,0 +1,168 @@
import { encodeCbor, encodeClientMessage, encodeFrame, PROTOCOL_VERSION } from "@earendil-works/pi-protocol";
import { afterEach, expect, test } from "vitest";
import type { ByteConnection, ByteConnectionHandler } from "../src/connection.ts";
import { Server } from "../src/server.ts";
import { ProtocolTestClient, TestServerHost, type WireChannel } from "../src/testing/index.ts";
let server: Server | undefined;
function connect(): ProtocolTestClient {
server = new Server(new TestServerHost(), { listeners: [], serverId: "00000000-0000-4000-8000-000000000001" });
let handler: ByteConnectionHandler;
let client: ProtocolTestClient;
let closed = false;
const connection: ByteConnection = {
get closed() {
return closed;
},
async send(chunk) {
client.receive(chunk);
},
close(finalChunk) {
if (finalChunk) client.receive(finalChunk);
closed = true;
client.markClosed();
},
};
const channel: WireChannel = {
async send(chunk) {
handler.onData(chunk);
},
async sendFragmented(chunk, splitAt) {
handler.onData(chunk.subarray(0, splitAt));
handler.onData(chunk.subarray(splitAt));
},
async close() {
closed = true;
handler.onClose();
client.markClosed();
},
};
client = new ProtocolTestClient(channel);
handler = server.accept(connection);
return client;
}
afterEach(async () => {
await server?.close();
server = undefined;
});
test("requires hello as the first message", async () => {
const client = connect();
await client.sendMessage({
type: "request",
id: "request-1",
target: { serverId: "00000000-0000-4000-8000-000000000001" },
call: { serviceId: "pi.session-directory", member: "list", args: [] },
});
await expect(client.next((message) => message.type === "hello_error")).resolves.toMatchObject({
type: "hello_error",
error: { code: "invalid_request" },
});
await client.waitForClose();
});
test("rejects unsupported protocol versions", async () => {
const client = connect();
await expect(client.hello(PROTOCOL_VERSION + 1)).resolves.toMatchObject({
type: "hello_error",
error: { code: "version" },
});
await client.waitForClose();
});
test("accepts fragmented hello and request frames", async () => {
const client = connect();
const hello = encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION });
const helloResponse = client.next((message) => message.type === "hello");
await client.sendFragmentedMessage({ type: "hello", version: PROTOCOL_VERSION }, Math.floor(hello.byteLength / 2));
await expect(helloResponse).resolves.toMatchObject({
type: "hello",
serverId: "00000000-0000-4000-8000-000000000001",
});
const response = client.next((message) => message.type === "response");
const request = {
type: "request" as const,
id: "request-1",
target: { serverId: "00000000-0000-4000-8000-000000000001" },
call: { serviceId: "pi.session-directory" as const, member: "list" as const, args: [] as [] },
};
const frame = encodeClientMessage(request);
await client.sendFragmentedMessage(request, Math.floor(frame.byteLength / 2));
await expect(response).resolves.toMatchObject({ ok: false, error: { code: "internal_error" } });
});
test.each([
["malformed CBOR", encodeFrame(Uint8Array.of(0xff))],
["schema-invalid CBOR", encodeFrame(encodeCbor({ type: "hello", version: 1, extra: true }))],
["oversized frame", new Uint8Array([1, 0, 0, 1])],
] as const)("rejects hostile framed input: %s", async (_label, bytes) => {
const client = connect();
await client.sendBytes(bytes);
await expect(client.next((message) => message.type === "hello_error")).resolves.toMatchObject({
type: "hello_error",
error: { code: "invalid_request" },
});
await client.waitForClose();
});
test("rejects a second hello after completing the handshake", async () => {
const client = connect();
await client.hello();
await client.sendMessage({ type: "hello", version: PROTOCOL_VERSION });
await expect(client.next((message) => message.type === "hello_error")).resolves.toMatchObject({
type: "hello_error",
error: { code: "invalid_request", message: expect.stringMatching(/first message/) },
});
await client.waitForClose();
});
test("processes a hello and request coalesced in one byte chunk", async () => {
const client = connect();
const hello = encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION });
const request = encodeClientMessage({
type: "request",
id: "request-1",
target: { serverId: "00000000-0000-4000-8000-000000000001" },
call: { serviceId: "pi.session-directory", member: "list", args: [] },
});
const wire = new Uint8Array(hello.byteLength + request.byteLength);
wire.set(hello);
wire.set(request, hello.byteLength);
await client.sendBytes(wire);
await expect(client.next((message) => message.type === "hello")).resolves.toMatchObject({ type: "hello" });
await expect(client.next((message) => message.type === "response")).resolves.toMatchObject({
type: "response",
id: "request-1",
ok: false,
error: { code: "internal_error" },
});
});
test("reports a truncated final frame when the peer closes", async () => {
const errors: Error[] = [];
server = new Server(new TestServerHost(), {
listeners: [],
serverId: "00000000-0000-4000-8000-000000000001",
onError: (error) => errors.push(error),
});
let closed = false;
const connection: ByteConnection = {
get closed() {
return closed;
},
async send() {},
close() {
closed = true;
},
};
const handler: ByteConnectionHandler = server.accept(connection);
handler.onData(new Uint8Array([0, 0, 0, 2, 1]));
handler.onClose();
expect(closed).toBe(false);
expect(errors).toEqual([expect.objectContaining({ message: expect.stringMatching(/truncated/i) })]);
});

View File

@@ -0,0 +1,127 @@
import { lstat, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ServerMessageDecoder } from "@earendil-works/pi-protocol";
import { afterEach, expect, test } from "vitest";
import type { ByteConnection } from "../src/connection.ts";
import { Server } from "../src/index.ts";
import type { ServerListener } from "../src/listener.ts";
import { TestServerHost } from "../src/testing/index.ts";
import { createUnixServer } from "../src/transports/unix/index.ts";
const host = new TestServerHost();
let server: Server | undefined;
let tempDirectory: string | undefined;
async function makeSocketPath(): Promise<string> {
tempDirectory = await mkdtemp(join(tmpdir(), "pss-"));
return join(tempDirectory, "server.sock");
}
afterEach(async () => {
await server?.close();
if (tempDirectory) await rm(tempDirectory, { recursive: true, force: true });
server = undefined;
tempDirectory = undefined;
});
test("requires explicit listeners and a canonical UUIDv4 server identity", () => {
expect(() => Reflect.construct(Server, [host, {}])).toThrow(/listeners/);
expect(() => new Server(host, { listeners: [], serverId: "" })).toThrow(/serverId/);
expect(() => new Server(host, { listeners: [], serverId: "invalid-server" })).toThrow(/serverId/);
});
test("rejects concurrent start calls without leaking the Unix listener", async () => {
const path = await makeSocketPath();
server = createUnixServer(host, { path, serverId: "00000000-0000-4000-8000-000000000001" });
const starting = server.start();
await expect(server.start()).rejects.toThrow(/starting/);
await starting;
await server.close();
await expect(lstat(path)).rejects.toMatchObject({ code: "ENOENT" });
});
test("handshake timeout closes with a final hello_error frame", async () => {
let resolveClosed: (() => void) | undefined;
const closed = new Promise<void>((resolve) => {
resolveClosed = resolve;
});
class TimedOutConnection implements ByteConnection {
closed = false;
finalChunk?: Uint8Array;
send(): Promise<void> {
return Promise.reject(new Error("handshake timeout must use the terminal close frame"));
}
close(finalChunk?: Uint8Array): void {
this.finalChunk = finalChunk;
this.closed = true;
resolveClosed?.();
}
}
const core = new Server(host, {
listeners: [],
serverId: "00000000-0000-4000-8000-000000000001",
maxFrameLength: 1024,
handshakeTimeoutMs: 10,
});
const connection = new TimedOutConnection();
core.accept(connection);
await closed;
expect(connection.closed).toBe(true);
expect(connection.finalChunk).toBeInstanceOf(Uint8Array);
const messages = new ServerMessageDecoder().push(connection.finalChunk!);
expect(messages).toMatchObject([{ type: "hello_error", error: { code: "invalid_request" } }]);
await core.close();
});
test("rejects timeout values above Node's maximum timer delay", () => {
const path = "/tmp/pi-server-timeout-test.sock";
expect(() =>
createUnixServer(host, {
path,
serverId: "00000000-0000-4000-8000-000000000001",
handshakeTimeoutMs: 2_147_483_648,
}),
).toThrow(/handshakeTimeoutMs/);
expect(() =>
createUnixServer(host, {
path,
serverId: "00000000-0000-4000-8000-000000000001",
gracefulCloseTimeoutMs: 2_147_483_648,
}),
).toThrow(/gracefulCloseTimeoutMs/);
});
test("rejects pending-byte limits smaller than one maximum frame", async () => {
const path = await makeSocketPath();
expect(() =>
createUnixServer(host, {
path,
serverId: "00000000-0000-4000-8000-000000000001",
maxFrameLength: 128,
maxPendingBytes: 131,
}),
).toThrow(/maxPendingBytes/);
});
test("rejects close and closed when listener shutdown fails", async () => {
const failure = new Error("listener close failed");
const listener: ServerListener = {
start: async () => {},
close: async () => {
throw failure;
},
};
const core = new Server(host, {
listeners: [listener],
serverId: "00000000-0000-4000-8000-000000000001",
});
await core.start();
await expect(core.close()).rejects.toBe(failure);
await expect(core.closed).rejects.toBe(failure);
});

View File

@@ -0,0 +1,65 @@
import { EventEmitter } from "node:events";
import type { Socket } from "node:net";
import { encodeServerMessage, type ServerHelloError, ServerMessageDecoder } from "@earendil-works/pi-protocol";
import { expect, test, vi } from "vitest";
import { UnixByteConnection } from "../src/transports/unix/listener.ts";
class ControlledSocket extends EventEmitter {
destroyed = false;
writable = true;
writableLength = 0;
ended = false;
finalChunk?: Uint8Array;
private writeCallback?: (error?: Error | null) => void;
write(_chunk: Uint8Array, callback: (error?: Error | null) => void): boolean {
this.writableLength = 1;
this.writeCallback = callback;
return false;
}
end(chunk?: Uint8Array): this {
this.ended = true;
this.finalChunk = chunk?.slice();
return this;
}
destroy(): this {
if (this.destroyed) return this;
this.destroyed = true;
this.writable = false;
this.emit("close");
return this;
}
completeWrite(): void {
const callback = this.writeCallback;
if (!callback) throw new Error("No pending write");
this.writeCallback = undefined;
this.writableLength = 0;
callback(null);
}
}
test("queues a final protocol error behind pending output before closing", async () => {
const socket = new ControlledSocket();
const connection = new UnixByteConnection(socket as unknown as Socket, 1_000, 64 * 1024);
const pendingWrite = connection.send(new Uint8Array([1, 2, 3]));
await vi.waitFor(() => expect(socket.writableLength).toBe(1));
const finalMessage: ServerHelloError = {
type: "hello_error",
error: { code: "invalid_request", message: "Protocol violation" },
};
const closing = connection.close(encodeServerMessage(finalMessage));
expect(socket.ended).toBe(false);
expect(socket.destroyed).toBe(false);
socket.completeWrite();
await pendingWrite;
await vi.waitFor(() => expect(socket.ended).toBe(true));
expect(new ServerMessageDecoder().push(socket.finalChunk!)).toEqual([finalMessage]);
connection.markClosed();
await closing;
});

View File

@@ -0,0 +1,143 @@
import { type ChildProcess, fork } from "node:child_process";
import { once } from "node:events";
import { lstat, mkdtemp, readdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import type { Server } from "../src/index.ts";
import { connectUnixTestClient, type ProtocolTestClient, TestServerHost } from "../src/testing/index.ts";
import { createUnixServer, getUnixSocketPath } from "../src/transports/unix/index.ts";
const servers = new Set<Server>();
const clients = new Set<ProtocolTestClient>();
const children = new Set<ChildProcess>();
const tempDirectories = new Set<string>();
async function makeSocketPath(nested = false): Promise<string> {
const directory = await mkdtemp(join("/tmp", "ps-"));
tempDirectories.add(directory);
return nested ? join(directory, "p", "n", "server.sock") : join(directory, "server.sock");
}
function makeServer(path: string): Server {
const server = createUnixServer(new TestServerHost(), { path, serverId: "00000000-0000-4000-8000-000000000001" });
servers.add(server);
return server;
}
afterEach(async () => {
for (const child of children) {
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
}
await Promise.all([...children].map((child) => (child.exitCode === null ? once(child, "exit") : undefined)));
children.clear();
await Promise.all([...clients].map((client) => client.close()));
clients.clear();
await Promise.all([...servers].map((server) => server.close()));
servers.clear();
await Promise.all([...tempDirectories].map((directory) => rm(directory, { recursive: true, force: true })));
tempDirectories.clear();
});
test("creates an in-memory server ID and derives its explicit Unix socket path", async () => {
const directory = await mkdtemp(join("/tmp", "pi-server-"));
tempDirectories.add(directory);
const serverId = "00000000-0000-4000-8000-000000000001";
const path = getUnixSocketPath(serverId, directory);
expect(path).toBe(join(directory, `${serverId}.sock`));
const first = createUnixServer(new TestServerHost(), { serverId, path });
servers.add(first);
await first.start();
const firstClient = await connectUnixTestClient(path);
clients.add(firstClient);
expect(await firstClient.hello()).toMatchObject({ serverId });
await firstClient.close();
clients.delete(firstClient);
await first.close();
servers.delete(first);
const replacement = createUnixServer(new TestServerHost(), { serverId, path });
servers.add(replacement);
await replacement.start();
const replacementClient = await connectUnixTestClient(path);
clients.add(replacementClient);
expect(await replacementClient.hello()).toMatchObject({ serverId });
});
describe("Unix listener filesystem lifecycle", () => {
test("rejects a live listener without unlinking it", async () => {
const path = await makeSocketPath();
const first = makeServer(path);
await first.start();
const firstIdentity = await lstat(path);
const second = makeServer(path);
await expect(second.start()).rejects.toThrow(/already running/);
const currentIdentity = await lstat(path);
expect(currentIdentity.isSocket()).toBe(true);
expect({ dev: currentIdentity.dev, ino: currentIdentity.ino }).toEqual({
dev: firstIdentity.dev,
ino: firstIdentity.ino,
});
const client = await connectUnixTestClient(path);
clients.add(client);
expect(await client.hello()).toMatchObject({ type: "hello" });
});
test("never unlinks a regular file at the configured path", async () => {
const path = await makeSocketPath();
await writeFile(path, "do not remove", { mode: 0o640 });
const server = makeServer(path);
await expect(server.start()).rejects.toThrow(/non-socket/);
expect(await readFile(path, "utf8")).toBe("do not remove");
});
test("creates nested temp parents, restricts permissions, and removes its own socket", async () => {
const path = await makeSocketPath(true);
const server = makeServer(path);
await server.start();
const stats = await lstat(path);
expect(stats.isSocket()).toBe(true);
if (process.platform !== "win32") expect(stats.mode & 0o777).toBe(0o600);
expect(await readdir(dirname(path))).toEqual(["server.sock"]);
await server.close();
await expect(lstat(path)).rejects.toMatchObject({ code: "ENOENT" });
});
test("does not remove a replacement inode during shutdown", async () => {
const path = await makeSocketPath();
const server = makeServer(path);
await server.start();
await unlink(path);
await writeFile(path, "replacement");
const closing = server.close();
expect(await readFile(path, "utf8")).toBe("replacement");
await closing;
expect(await readFile(path, "utf8")).toBe("replacement");
});
test("removes a genuinely stale socket before binding", async () => {
const path = await makeSocketPath();
const child = fork(new URL("fixtures/stale-socket-server.mjs", import.meta.url), [path], {
stdio: ["ignore", "ignore", "inherit", "ipc"],
});
children.add(child);
await once(child, "message");
const staleIdentity = await lstat(path);
expect(staleIdentity.isSocket()).toBe(true);
child.kill("SIGKILL");
await once(child, "exit");
children.delete(child);
const server = makeServer(path);
await server.start();
const liveIdentity = await lstat(path);
expect(liveIdentity.isSocket()).toBe(true);
const client = await connectUnixTestClient(path);
clients.add(client);
expect(await client.hello()).toMatchObject({ type: "hello" });
});
});