Skip to content
203 changes: 203 additions & 0 deletions tests/web/pi-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import assert from "node:assert/strict";
import fsPromises from "node:fs/promises";
import { syncBuiltinESMExports } from "node:module";
import {
mkdir,
mkdtemp,
Expand Down Expand Up @@ -147,6 +149,207 @@ test("snapshot pins current and selected sessions while bounding the projection"
}
});

test("discovers default Pi sessions as bounded read-only projections", async (t) => {
const root = await mkdtemp(join(tmpdir(), "openpi-web-terminal-history-"));
const sessionDirectory = join(root, "web-sessions");
const agentDirectory = join(root, "pi-agent");
const previousAgentDirectory = process.env.PI_CODING_AGENT_DIR;
process.env.PI_CODING_AGENT_DIR = agentDirectory;
try {
await mkdir(sessionDirectory, { recursive: true });
const current = SessionManager.inMemory(root);
const terminal = SessionManager.create(root);
persistSession(terminal, "terminal history", 2);
const terminalPath = terminal.getSessionFile();
assert.ok(terminalPath);
const unrelatedWorkspace = join(root, "unrelated-workspace");
await mkdir(unrelatedWorkspace);
const unrelated = SessionManager.create(unrelatedWorkspace);
persistSession(unrelated, "unrelated first message", 3);
unrelated.appendMessage({
role: "user",
content: "unrelated-only-token",
timestamp: 4,
});
const unrelatedPath = unrelated.getSessionFile();
assert.ok(unrelatedPath);
const fileBefore = await readFile(terminalPath);
const originalOpen = fsPromises.open;
const openedPaths: string[] = [];
t.mock.method(
fsPromises,
"open",
(...args: Parameters<typeof originalOpen>) => {
openedPaths.push(String(args[0]));
assert.notEqual(String(args[0]), unrelatedPath);
return originalOpen(...args);
},
);
syncBuiltinESMExports();
t.after(() => {
t.mock.restoreAll();
syncBuiltinESMExports();
});
const adapter = new PiWebAdapter(
runtimeFor(root, sessionDirectory, current),
);
const listAll = SessionManager.listAll;
SessionManager.listAll = async () => {
throw new Error("unrelated Session discovery must not be used");
};
try {
const listed = await adapter.listReadOnlyTerminalSessions({ limit: 1 });
assert.equal(listed.total, 1);
assert.equal("allMessagesText" in listed.sessions[0]!, false);
assert.deepEqual(listed.sessions[0], {
id: terminal.getSessionId(),
path: terminalPath,
cwd: root,
modified: listed.sessions[0]?.modified,
created: listed.sessions[0]?.created,
messageCount: 2,
firstMessage: "terminal history",
source: "pi-default",
origin: "terminal",
readOnly: true,
});
const inspected = await adapter.getReadOnlyTerminalSession(terminalPath);
assert.equal(inspected.readOnly, true);
assert.equal(inspected.source, "pi-default");
assert.equal(inspected.preview.messages.length, 2);
assert.equal("allMessagesText" in inspected, false);
assert.ok(openedPaths.includes(terminalPath));
assert.equal(
(
await adapter.listReadOnlyTerminalSessions({
query: "unrelated-only-token",
})
).total,
0,
);
await assert.rejects(
adapter.getReadOnlyTerminalSession(unrelatedPath),
(error: unknown) =>
error instanceof Error &&
(error as { code?: string }).code === "SESSION_NOT_FOUND",
);
const cancelled = AbortSignal.abort();
const opensBefore = openedPaths.length;
await assert.rejects(
adapter.getReadOnlyTerminalSession(terminalPath, { signal: cancelled }),
{ name: "AbortError" },
);
await assert.rejects(
adapter.listReadOnlyTerminalSessions({ signal: cancelled }),
{ name: "AbortError" },
);
assert.equal(openedPaths.length, opensBefore);

const controller = new AbortController();
let targetOpens = 0;
t.mock.method(
fsPromises,
"open",
async (...args: Parameters<typeof originalOpen>) => {
const handle = await originalOpen(...args);
if (String(args[0]) === terminalPath && ++targetOpens === 2) {
const read = handle.read.bind(handle);
t.mock.method(
handle,
"read",
async (...readArgs: Parameters<typeof read>) => {
const result = await read(...readArgs);
controller.abort();
return result;
},
);
}
return handle;
},
);
syncBuiltinESMExports();
await assert.rejects(
adapter.getReadOnlyTerminalSession(terminalPath, {
signal: controller.signal,
}),
{ name: "AbortError" },
);
assert.equal(
targetOpens,
2,
"cancellation occurs during the preview, after metadata admission",
);
} finally {
SessionManager.listAll = listAll;
}
assert.equal((await SessionManager.listAll(sessionDirectory)).length, 0);
assert.deepEqual(await readFile(terminalPath), fileBefore);
assert.equal(
(await adapter.listSessions()).some(
(session) => session.path === terminalPath,
),
false,
);
assert.equal(
(await adapter.getSnapshot()).currentSessionId,
current.getSessionId(),
);
const hidden = new PiWebAdapter(
runtimeFor(root, sessionDirectory, current),
);
await hidden.removeWorkspace(root);
const unboundRuntime = {
...runtimeFor(root, sessionDirectory, current),
workspaceSelected: false,
};
const unbound = new PiWebAdapter(unboundRuntime);
const originalReaddir = fsPromises.readdir;
t.mock.method(
fsPromises,
"readdir",
(...args: Parameters<typeof originalReaddir>) => {
assert.ok(
!String(args[0]).startsWith(agentDirectory),
"unavailable workspace must not walk the default store",
);
return originalReaddir(...args);
},
);
syncBuiltinESMExports();
await assert.rejects(hidden.listReadOnlyTerminalSessions());
await assert.rejects(unbound.listReadOnlyTerminalSessions());

const firstKept = terminal.appendMessage({
role: "user",
content: "kept after compaction",
timestamp: 5,
});
terminal.appendCompaction("summary before kept window", firstKept, 100);
const compacted = await adapter.getReadOnlyTerminalSession(terminalPath);
assert.ok(
JSON.stringify(compacted.preview.messages).includes(
"kept after compaction",
),
);
assert.ok(
!JSON.stringify(compacted.preview.messages).includes("terminal history"),
);
assert.ok(compacted.preview.messages.length <= 80);
assert.ok(compacted.preview.retainedBytes <= 1024 * 1024);
await assert.rejects(
readFile(join(sessionDirectory, "archived-sessions.json")),
{ code: "ENOENT" },
);
} finally {
if (previousAgentDirectory === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousAgentDirectory;
}
await rm(root, { recursive: true, force: true });
}
});

test("an unbound Web runtime never projects its bootstrap cwd as a workspace or Session", async () => {
const root = await mkdtemp(join(tmpdir(), "openpi-web-unbound-"));
const bootstrap = join(root, ".bootstrap-workspace");
Expand Down
157 changes: 157 additions & 0 deletions tests/web/web-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,163 @@ test("serves workspaces through a runtime isolated from terminal sessions", asyn
}
});

test("serves terminal Sessions through a read-only bounded endpoint", async () => {
const root = await mkdtemp(join(tmpdir(), "openpi-web-terminal-host-"));
const previousAgentDirectory = process.env.PI_CODING_AGENT_DIR;
process.env.PI_CODING_AGENT_DIR = join(root, "pi-agent");
const sessionManager = SessionManager.inMemory(root);
const terminal = SessionManager.create(root);
terminal.appendMessage({
role: "user",
content: "terminal endpoint",
timestamp: 1,
});
terminal.appendMessage({
role: "assistant",
content: [],
api: "openai-responses",
provider: "fixture",
model: "fixture",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: 1,
});
const runtime: WebRuntimeController = {
cwd: root,
workspaceSelected: true,
sessionDirectory: join(root, "web-sessions"),
sessionManager,
isIdle: () => true,
getActiveTurn: () => undefined,
cancelTurn: async (options) => ({ ...options, state: "stale-turn" }),
sendPrompt: async () => ({ pendingFollowUps: 0 }),
newSession: async () => ({ cancelled: false }),
switchSession: async () => ({ cancelled: false }),
listModels: () => [],
setModel: async () => {
throw new Error("not available");
},
subscribe: () => () => {},
dispose: async () => {},
};
const host = new WebHost({ runtime });
try {
await host.start();
const launched = new URL(host.url);
const token = new URLSearchParams(launched.hash.slice(1)).get("token");
assert.ok(token);
const headers = { Authorization: `Bearer ${token}` };
const listed = await fetch(
`${launched.origin}/api/terminal-sessions?limit=1`,
{
headers,
},
);
assert.equal(listed.status, 200);
const page = (await listed.json()) as {
sessions: Array<{
path: string;
source: string;
origin: string;
readOnly: boolean;
}>;
total: number;
};
assert.equal(page.total, 1);
assert.equal(page.sessions[0]?.path, terminal.getSessionFile());
assert.equal(page.sessions[0]?.source, "pi-default");
assert.equal(page.sessions[0]?.origin, "terminal");
assert.equal(page.sessions[0]?.readOnly, true);
assert.equal(JSON.stringify(page).includes("allMessagesText"), false);
assert.equal(
(
await fetch(
`${launched.origin}/api/terminal-sessions?query=${"x".repeat(201)}`,
{ headers },
)
).status,
400,
);
assert.equal(
(
await fetch(`${launched.origin}/api/terminal-sessions?cursor=nope`, {
headers,
})
).status,
400,
);
assert.equal(
(
await fetch(`${launched.origin}/api/terminal-sessions?limit=101`, {
headers,
})
).status,
400,
);
const missing = await fetch(
`${launched.origin}/api/terminal-sessions?path=${encodeURIComponent(join(root, "missing.jsonl"))}`,
{ headers },
);
assert.equal(missing.status, 404);
assert.deepEqual(await missing.json(), {
code: "SESSION_NOT_FOUND",
error: "Terminal Session is not available",
});
const inspected = await fetch(
`${launched.origin}/api/terminal-sessions?path=${encodeURIComponent(terminal.getSessionFile()!)}`,
{ headers },
);
assert.equal(inspected.status, 200);
const details = (await inspected.json()) as {
readOnly: boolean;
preview: { messages: unknown[]; retainedBytes: number };
};
assert.equal(details.readOnly, true);
assert.equal(details.preview.messages.length, 2);
assert.ok(details.preview.retainedBytes > 0);
assert.equal(JSON.stringify(details).includes("allMessagesText"), false);
for (const method of ["POST", "PATCH", "DELETE"]) {
const rejected = await fetch(`${launched.origin}/api/terminal-sessions`, {
method,
headers,
});
assert.equal(rejected.status, 405);
}
const capabilities = await fetch(`${launched.origin}/api/capabilities`, {
headers,
});
assert.equal(
(await capabilities.json()).sessionId,
sessionManager.getSessionId(),
);
const webSessions = await fetch(`${launched.origin}/api/sessions`, {
headers,
});
assert.ok(!JSON.stringify(await webSessions.json()).includes("pi-default"));
} finally {
await host.stop();
if (previousAgentDirectory === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousAgentDirectory;
}
await rm(root, { recursive: true, force: true });
}
});

test("an unbound Host exposes no bootstrap Session and rejects prompt bypasses", async () => {
const root = await mkdtemp(join(tmpdir(), "openpi-web-unbound-host-"));
const bootstrap = join(root, ".bootstrap-workspace");
Expand Down
Loading
Loading