85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
// A host with several routes connects through the first one that answers.
|
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
import type { Server } from "bun";
|
|
|
|
import { T3Host } from "../src/t3/host.ts";
|
|
|
|
const ENVIRONMENT_ID = "env-laptop";
|
|
let servers: Server<undefined>[] = [];
|
|
afterEach(() => {
|
|
for (const server of servers) server.stop(true);
|
|
servers = [];
|
|
});
|
|
|
|
/**
|
|
* Just enough of a T3 server: the descriptor, a ticket, config, and an empty shell snapshot.
|
|
* While `state.up` is false its descriptor returns 503, like a route that is down.
|
|
*/
|
|
function fakeT3(state = { up: true }) {
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
fetch(req, srv) {
|
|
const { pathname } = new URL(req.url);
|
|
if (pathname === "/.well-known/t3/environment") {
|
|
if (!state.up) return new Response("down", { status: 503 });
|
|
return Response.json({ environmentId: ENVIRONMENT_ID, label: "framework-gib", serverVersion: "0.1.5", orchestrationProtocolVersion: 2 });
|
|
}
|
|
if (pathname === "/api/auth/websocket-ticket/rpc") return Response.json({ ticket: "t" });
|
|
if (pathname === "/ws" && srv.upgrade(req)) return undefined;
|
|
return new Response("not found", { status: 404 });
|
|
},
|
|
websocket: {
|
|
message(ws, data) {
|
|
const frame = JSON.parse(String(data)) as { _tag: string; id: string; tag: string };
|
|
if (frame._tag !== "Request") return;
|
|
if (frame.tag === "server.getConfig") {
|
|
ws.send(JSON.stringify({ _tag: "Exit", requestId: frame.id, exit: { _tag: "Success", value: {} } }));
|
|
} else if (frame.tag === "orchestration.subscribeShell") {
|
|
const snapshot = { snapshotSequence: 1, projects: [], threads: [] };
|
|
ws.send(JSON.stringify({ _tag: "Chunk", requestId: frame.id, values: [{ kind: "snapshot", snapshot }] }));
|
|
}
|
|
},
|
|
},
|
|
});
|
|
servers.push(server);
|
|
return `http://localhost:${server.port}`;
|
|
}
|
|
|
|
/** Resolves once `check` passes, polling every few milliseconds. */
|
|
async function until(check: () => boolean) {
|
|
while (!check()) await Bun.sleep(5);
|
|
}
|
|
|
|
describe("T3Host routes", () => {
|
|
it("falls back to the next route when the preferred one does not answer", async () => {
|
|
const working = fakeT3();
|
|
// Port 9 (discard) refuses connections, standing in for a LAN route that is down.
|
|
const refused = "http://127.0.0.1:9";
|
|
const spare = "https://t3laptop.example.org";
|
|
const host = new T3Host({ id: ENVIRONMENT_ID, label: "Laptop", urls: [refused, working, spare], token: "x", environmentId: ENVIRONMENT_ID });
|
|
host.start();
|
|
await until(() => host.status === "online");
|
|
expect(host.url).toBe(working);
|
|
expect(host.routeAttempts.get(refused)).toMatchObject({ ok: false, error: "refused" });
|
|
expect(host.routeAttempts.get(working)).toMatchObject({ ok: true, error: null });
|
|
expect(host.routeAttempts.has(spare)).toBe(false);
|
|
host.stop();
|
|
});
|
|
|
|
it("moves back to the preferred route on reconnect once it answers", async () => {
|
|
const preferredState = { up: false };
|
|
const preferred = fakeT3(preferredState);
|
|
const fallback = fakeT3();
|
|
const host = new T3Host({ id: ENVIRONMENT_ID, label: "Laptop", urls: [preferred, fallback], token: "x", environmentId: ENVIRONMENT_ID });
|
|
host.start();
|
|
await until(() => host.status === "online");
|
|
expect(host.url).toBe(fallback);
|
|
|
|
preferredState.up = true;
|
|
host.reconnect();
|
|
await until(() => host.status === "online" && host.url === preferred);
|
|
expect(host.lastOnlineAt).not.toBeNull();
|
|
host.stop();
|
|
});
|
|
});
|