mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
261 lines
8.4 KiB
TypeScript
261 lines
8.4 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { PiSessionDetachedError, PiSessionOwnershipError } from "../src/index.ts";
|
|
import { connectClient, MemoryByteServer, sessionSnapshot } from "./support.ts";
|
|
|
|
describe("PiClient", () => {
|
|
test("keeps multiple session handles independent and enforces detach", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request") return;
|
|
const request = message.request;
|
|
if (request.command === "attach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot(request.sessionId) },
|
|
});
|
|
}
|
|
if (request.command === "detach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "detach", sessionId: request.sessionId },
|
|
});
|
|
}
|
|
});
|
|
|
|
const first = await client.attachSession("session-1");
|
|
const second = await client.attachSession("session-2");
|
|
expect(first.attached).toBe(true);
|
|
expect(second.attached).toBe(true);
|
|
await first.detach();
|
|
expect(first.attached).toBe(false);
|
|
expect(second.attached).toBe(true);
|
|
await expect(first.abort()).rejects.toBeInstanceOf(PiSessionDetachedError);
|
|
});
|
|
|
|
test("detaches a shared session only after its final lease is released", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
const requests: string[] = [];
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request") return;
|
|
requests.push(message.request.command);
|
|
if (message.request.command === "attach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot("session-1") },
|
|
});
|
|
}
|
|
if (message.request.command === "detach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "detach", sessionId: "session-1" },
|
|
});
|
|
}
|
|
});
|
|
|
|
const first = await client.attachSession("session-1");
|
|
const second = await client.attachSession("session-1");
|
|
expect(second).not.toBe(first);
|
|
expect(requests).toEqual(["attach"]);
|
|
|
|
await first.detach();
|
|
expect(first.attached).toBe(false);
|
|
expect(second.attached).toBe(true);
|
|
expect(requests).toEqual(["attach"]);
|
|
|
|
await second.detach();
|
|
expect(second.attached).toBe(false);
|
|
expect(requests).toEqual(["attach", "detach"]);
|
|
});
|
|
|
|
test("enforces exclusive and shared lease modes", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request") return;
|
|
if (message.request.command === "attach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot("session-1") },
|
|
});
|
|
}
|
|
if (message.request.command === "detach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "detach", sessionId: "session-1" },
|
|
});
|
|
}
|
|
});
|
|
|
|
const shared = await client.acquireSession("session-1", { mode: "shared" });
|
|
await expect(client.acquireSession("session-1", { mode: "exclusive" })).rejects.toBeInstanceOf(
|
|
PiSessionOwnershipError,
|
|
);
|
|
await shared.dispose();
|
|
|
|
const exclusive = await client.acquireSession("session-1", { mode: "exclusive" });
|
|
await expect(client.acquireSession("session-1", { mode: "shared" })).rejects.toBeInstanceOf(
|
|
PiSessionOwnershipError,
|
|
);
|
|
await exclusive[Symbol.asyncDispose]();
|
|
});
|
|
|
|
test("invalidated leases dispose without protocol cleanup", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request" || message.request.command !== "attach") return;
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot("session-1") },
|
|
});
|
|
});
|
|
const lease = await client.acquireSession("session-1", { mode: "exclusive" });
|
|
|
|
client.disconnect();
|
|
|
|
await expect(lease.dispose()).resolves.toBeUndefined();
|
|
expect(lease.active).toBe(false);
|
|
});
|
|
|
|
test("rejects commands while releasing and restores an explicit detach after failure", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
const requests: Array<{ id: string; command: string }> = [];
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request") return;
|
|
requests.push({ id: message.id, command: message.request.command });
|
|
});
|
|
const acquiring = client.acquireSession("session-1", { mode: "exclusive" });
|
|
const attachRequest = requests.at(-1);
|
|
if (!attachRequest) throw new Error("Missing attach request");
|
|
server.send({
|
|
type: "response",
|
|
id: attachRequest.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot("session-1") },
|
|
});
|
|
const lease = await acquiring;
|
|
|
|
const firstDetach = lease.detach();
|
|
const failedDetachRequest = requests.at(-1);
|
|
await expect(lease.abort()).rejects.toBeInstanceOf(PiSessionDetachedError);
|
|
if (!failedDetachRequest) throw new Error("Missing detach request");
|
|
server.send({
|
|
type: "response",
|
|
id: failedDetachRequest.id,
|
|
ok: false,
|
|
error: { code: "invalid_request", message: "retry" },
|
|
});
|
|
await expect(firstDetach).rejects.toThrow("retry");
|
|
expect(lease.active).toBe(true);
|
|
|
|
const secondDetach = lease.detach();
|
|
const successfulDetachRequest = requests.at(-1);
|
|
if (!successfulDetachRequest) throw new Error("Missing retry detach request");
|
|
server.send({
|
|
type: "response",
|
|
id: successfulDetachRequest.id,
|
|
ok: true,
|
|
result: { command: "detach", sessionId: "session-1" },
|
|
});
|
|
await secondDetach;
|
|
expect(lease.active).toBe(false);
|
|
});
|
|
|
|
test("serializes reacquisition behind final lease detachment", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
const requests: Array<{ id: string; command: string }> = [];
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request") return;
|
|
requests.push({ id: message.id, command: message.request.command });
|
|
});
|
|
|
|
const firstAttachment = client.attachSession("session-1");
|
|
const firstAttachRequest = requests.at(-1);
|
|
if (!firstAttachRequest) throw new Error("Missing first attach request");
|
|
server.send({
|
|
type: "response",
|
|
id: firstAttachRequest.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot("session-1") },
|
|
});
|
|
const first = await firstAttachment;
|
|
const detaching = first.detach();
|
|
const detachRequest = requests.at(-1);
|
|
if (!detachRequest) throw new Error("Missing detach request");
|
|
const reacquiring = client.attachSession("session-1");
|
|
expect(requests.map(({ command }) => command)).toEqual(["attach", "detach"]);
|
|
|
|
server.send({
|
|
type: "response",
|
|
id: detachRequest.id,
|
|
ok: true,
|
|
result: { command: "detach", sessionId: "session-1" },
|
|
});
|
|
await detaching;
|
|
await Promise.resolve();
|
|
const secondAttachRequest = requests.at(-1);
|
|
expect(secondAttachRequest?.command).toBe("attach");
|
|
if (!secondAttachRequest) throw new Error("Missing second attach request");
|
|
server.send({
|
|
type: "response",
|
|
id: secondAttachRequest.id,
|
|
ok: true,
|
|
result: { command: "attach", session: sessionSnapshot("session-1", { revision: 2 }) },
|
|
});
|
|
|
|
await expect(reacquiring).resolves.toMatchObject({ attached: true });
|
|
});
|
|
|
|
test("accepts a lower revision after detaching and reacquiring the same session", async () => {
|
|
const server = new MemoryByteServer();
|
|
const client = await connectClient(server);
|
|
let attachCount = 0;
|
|
server.onMessage((message) => {
|
|
if (message.type !== "request") return;
|
|
if (message.request.command === "attach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: {
|
|
command: "attach",
|
|
session: sessionSnapshot("session-1", { revision: attachCount++ === 0 ? 10 : 0 }),
|
|
},
|
|
});
|
|
}
|
|
if (message.request.command === "detach") {
|
|
server.send({
|
|
type: "response",
|
|
id: message.id,
|
|
ok: true,
|
|
result: { command: "detach", sessionId: "session-1" },
|
|
});
|
|
}
|
|
});
|
|
|
|
const first = await client.attachSession("session-1");
|
|
expect(first.snapshot?.revision).toBe(10);
|
|
await first.detach();
|
|
const reopened = await client.attachSession("session-1");
|
|
expect(reopened).not.toBe(first);
|
|
expect(reopened.snapshot?.revision).toBe(0);
|
|
});
|
|
});
|