Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 83 additions & 18 deletions tests/daemon-sessions.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
import { describe, expect, test } from "bun:test";
import { execSync } from "node:child_process";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
test,
} from "bun:test";
import { once } from "node:events";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import {
createConnection,
createServer,
type Server,
type Socket,
} from "node:net";
import { join } from "node:path";

import {
validateControlPlaneRouteTarget,
validateRawRequestTargetPath,
} from "../src/daemon/control-plane-route-validation.ts";
import { startRequestTargetProxy } from "../src/daemon/request-target-proxy.ts";
import { handleSessionRoutes } from "../src/daemon/routes/sessions.ts";

/**
* Check if tmux is available (required for session tests).
*/
function isTmuxAvailable(): boolean {
try {
execSync("which tmux", { stdio: "ignore" });
return true;
} catch {
return false;
import { exec } from "../src/lib/shell.ts";
import { restoreEnv } from "./helpers/env.ts";

/** Real session-route tests require tmux, always on a task-owned socket. */
const tmuxPath = Bun.which("tmux");
const hasTmux = tmuxPath !== null;
let sessionRoot = "";
let sessionSocket = "";
let savedPath: string | undefined;
let pathSaved = false;

/** Never address the default server, even when a route case fails after create. */
function privateTmux(args: readonly string[]) {
if (!(tmuxPath && sessionSocket)) {
throw new Error("Session route fixture has no private tmux socket.");
}
return exec([tmuxPath, "-f", "/dev/null", "-S", sessionSocket, ...args], {
stdin: "ignore",
unsetEnvKeys: ["TMUX"],
timeoutMs: 3000,
});
}

const hasTmux = isTmuxAvailable();

/**
* Helper to create a mock Request.
*/
Expand Down Expand Up @@ -510,6 +527,46 @@ describe("request target proxy", () => {
});

describe.skipIf(!hasTmux)("handleSessionRoutes", () => {
beforeAll(async () => {
// Short path for Darwin's Unix socket limit; each process owns a new server.
sessionRoot = await mkdtemp("/tmp/hack-session-routes-");
sessionSocket = join(sessionRoot, "tmux.sock");
await mkdir(join(sessionRoot, "bin"));
// The route executes real tmux through this shim. Never inherit a caller's
// socket or config; only the fixture's private server can be created/stopped.
// The finite pane command also caps a server's life if the test process dies.
const quotedTmux = `'${tmuxPath?.replaceAll("'", "'\\''")}'`;
await writeFile(
join(sessionRoot, "bin", "tmux"),
`#!/bin/sh\nunset TMUX\nif [ "$1" = new-session ]; then\n exec ${quotedTmux} -f /dev/null -S '${sessionSocket}' "$@" /bin/sleep 300\nfi\nexec ${quotedTmux} -f /dev/null -S '${sessionSocket}' "$@"\n`,
{ mode: 0o700 }
);
});
beforeEach(() => {
savedPath = process.env.PATH;
pathSaved = true;
process.env.PATH = `${join(sessionRoot, "bin")}:${savedPath ?? "/usr/bin:/bin"}`;
});
afterEach(async () => {
try {
await privateTmux(["kill-server"]);
expect((await privateTmux(["list-sessions"])).exitCode).toBe(1);
} finally {
if (pathSaved) {
restoreEnv("PATH", savedPath);
pathSaved = false;
}
}
});
afterAll(async () => {
try {
await privateTmux(["kill-server"]);
} finally {
await rm(sessionRoot, { recursive: true, force: true });
sessionRoot = "";
sessionSocket = "";
}
});
describe("route matching", () => {
test("returns null for non-session routes", async () => {
const req = mockRequest({ method: "GET", path: "/v1/status" });
Expand Down Expand Up @@ -595,8 +652,10 @@ describe.skipIf(!hasTmux)("handleSessionRoutes", () => {
});
const url = new URL(req.url);
const result = await handleSessionRoutes({ req, url });
// May fail if tmux isn't available, but should not fail on validation
expect(result?.status).not.toBe(400);
expect(result?.status).toBe(201);
expect(
(await privateTmux(["has-session", "-t", "=myproject123"])).exitCode
).toBe(0);
});

test("accepts names with dashes", async () => {
Expand All @@ -607,7 +666,10 @@ describe.skipIf(!hasTmux)("handleSessionRoutes", () => {
});
const url = new URL(req.url);
const result = await handleSessionRoutes({ req, url });
expect(result?.status).not.toBe(400);
expect(result?.status).toBe(201);
expect(
(await privateTmux(["has-session", "-t", "=my-project"])).exitCode
).toBe(0);
});

test("accepts names with underscores", async () => {
Expand All @@ -618,7 +680,10 @@ describe.skipIf(!hasTmux)("handleSessionRoutes", () => {
});
const url = new URL(req.url);
const result = await handleSessionRoutes({ req, url });
expect(result?.status).not.toBe(400);
expect(result?.status).toBe(201);
expect(
(await privateTmux(["has-session", "-t", "=my_project"])).exitCode
).toBe(0);
});

test("rejects names with dots", async () => {
Expand Down
Loading