Skip to content
Open
Show file tree
Hide file tree
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
14 changes: 13 additions & 1 deletion packages/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export interface ServerOptions {
onReady?: (url: string, isRemote: boolean, port: number) => void;
/** OpenCode client for querying available agents (OpenCode only) */
opencodeClient?: OpencodeClient;
/** Optional session ID for isolating storage and decisions per session */
sessionId?: string;
/** Working directory for the project (defaults to process.cwd()) */
cwd?: string;
/** When set to "archive", server runs in read-only archive browser mode */
mode?: "archive";
/** Custom plan save path — used by archive mode to find saved plans */
Expand All @@ -94,6 +98,10 @@ export interface ServerResult {
url: string;
/** Whether running in remote mode */
isRemote: boolean;
/** Session ID (provided or auto-generated) */
sessionId: string;
/** Working directory for the project */
cwd: string;
/** Wait for user decision (approve/deny) */
waitForDecision: () => Promise<{
approved: boolean;
Expand Down Expand Up @@ -125,7 +133,9 @@ const RETRY_DELAY_MS = 500;
export async function startPlannotatorServer(
options: ServerOptions
): Promise<ServerResult> {
const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath } = options;
const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath, sessionId: optSessionId, cwd: optCwd } = options;
const sessionId = optSessionId ?? crypto.randomUUID();
const cwd = optCwd ?? process.cwd();

const isRemote = isRemoteSession();
const configuredPort = getServerPort();
Expand Down Expand Up @@ -618,6 +628,8 @@ export async function startPlannotatorServer(
port,
url: serverUrl,
isRemote,
sessionId,
cwd,
waitForDecision: () => decisionPromise,
...(donePromise && { waitForDone: () => donePromise }),
stop: () => server.stop(),
Expand Down
84 changes: 84 additions & 0 deletions packages/server/session-id-option.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* PR F: Tests for optional sessionId and cwd on ServerOptions
*
* Adding optional sessionId/cwd fields enables future session isolation
* without breaking any existing callers.
*/
import { describe, test, expect, afterAll, beforeAll } from "bun:test";

describe("optional sessionId/cwd on ServerOptions", () => {
const controllers: AbortController[] = [];
let savedPort: string | undefined;
let savedRemote: string | undefined;

beforeAll(() => {
savedPort = process.env.PLANNOTATOR_PORT;
savedRemote = process.env.PLANNOTATOR_REMOTE;
delete process.env.PLANNOTATOR_PORT;
delete process.env.PLANNOTATOR_REMOTE;
delete process.env.PLANNOTATOR_SERVER_URL;
});

afterAll(() => {
for (const c of controllers) c.abort();
if (savedPort) process.env.PLANNOTATOR_PORT = savedPort;
if (savedRemote) process.env.PLANNOTATOR_REMOTE = savedRemote;
});

test("ServerResult includes sessionId when provided", async () => {
const { startPlannotatorServer } = await import("./index");

const controller = new AbortController();
controllers.push(controller);
const server = await startPlannotatorServer({
plan: "# Test",
signal: controller.signal,
sessionId: "my-custom-session-id",
});

expect(server.sessionId).toBe("my-custom-session-id");
});

test("ServerResult auto-generates sessionId when not provided", async () => {
const { startPlannotatorServer } = await import("./index");

const controller = new AbortController();
controllers.push(controller);
const server = await startPlannotatorServer({
plan: "# Test",
signal: controller.signal,
});

expect(server.sessionId).toBeDefined();
expect(server.sessionId.length).toBeGreaterThan(0);
// Should be a UUID format
expect(server.sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});

test("ServerResult includes cwd from options", async () => {
const { startPlannotatorServer } = await import("./index");

const controller = new AbortController();
controllers.push(controller);
const server = await startPlannotatorServer({
plan: "# Test",
signal: controller.signal,
cwd: "/tmp/custom-cwd",
});

expect(server.cwd).toBe("/tmp/custom-cwd");
});

test("ServerResult defaults cwd to process.cwd()", async () => {
const { startPlannotatorServer } = await import("./index");

const controller = new AbortController();
controllers.push(controller);
const server = await startPlannotatorServer({
plan: "# Test",
signal: controller.signal,
});

expect(server.cwd).toBe(process.cwd());
});
});