Archived
fix: harden agent profile boundaries
This commit is contained in:
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { agentSessionDirEnvKeys } from "../../config.js";
|
||||
import { createPiSessionManagerGateway, defaultPiSessionDir, defaultPiSessionsRoot, filterSessionsForCwd, SessionDirResolver } from "./piSessionManagerGateway.js";
|
||||
import type { PiSessionListEntry } from "./piSessionService.js";
|
||||
import type { PiSessionManager } from "./piSessionService.js";
|
||||
@@ -24,7 +25,7 @@ afterEach(async () => {
|
||||
|
||||
describe("SessionDirResolver", () => {
|
||||
it("uses Pi default session storage when no Pi override is configured", () => {
|
||||
const resolver = new SessionDirResolver({ agentDir, env: {} });
|
||||
const resolver = new SessionDirResolver(piProfileOptions());
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "pi-default", sessionDir: defaultPiSessionDir(cwd, agentDir), usesConfiguredSessionDir: false });
|
||||
expect(defaultPiSessionsRoot(agentDir)).toBe(join(agentDir, "sessions"));
|
||||
@@ -34,7 +35,7 @@ describe("SessionDirResolver", () => {
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: ".pi/sessions" }, null, 2)}\n`, "utf8");
|
||||
|
||||
const resolver = new SessionDirResolver({ agentDir, env: {} });
|
||||
const resolver = new SessionDirResolver(piProfileOptions());
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "settings", sessionDir: join(cwd, ".pi", "sessions"), usesConfiguredSessionDir: true });
|
||||
});
|
||||
@@ -45,7 +46,7 @@ describe("SessionDirResolver", () => {
|
||||
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "global-sessions") }, null, 2)}\n`, "utf8");
|
||||
await writeFile(join(cwd, ".pi", "settings.json"), `${JSON.stringify({ sessionDir: ".workspace-sessions" }, null, 2)}\n`, "utf8");
|
||||
|
||||
const resolver = new SessionDirResolver({ agentDir, env: {} });
|
||||
const resolver = new SessionDirResolver(piProfileOptions());
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "settings", sessionDir: join(cwd, ".workspace-sessions"), usesConfiguredSessionDir: true });
|
||||
});
|
||||
@@ -55,7 +56,7 @@ describe("SessionDirResolver", () => {
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "settings-sessions") }, null, 2)}\n`, "utf8");
|
||||
|
||||
const resolver = new SessionDirResolver({ agentDir, env: { PI_CODING_AGENT_SESSION_DIR: envDir } });
|
||||
const resolver = new SessionDirResolver(piProfileOptions({ PI_CODING_AGENT_SESSION_DIR: envDir }));
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
|
||||
});
|
||||
@@ -65,10 +66,22 @@ describe("SessionDirResolver", () => {
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "settings-sessions") }, null, 2)}\n`, "utf8");
|
||||
|
||||
const resolver = new SessionDirResolver({ agentDir, env: { PI_WEB_AGENT_SESSION_DIR: envDir } });
|
||||
const resolver = new SessionDirResolver(piProfileOptions({ PI_WEB_AGENT_SESSION_DIR: envDir }));
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
|
||||
});
|
||||
|
||||
it("snapshots the daemon epoch's injected session-directory environment", () => {
|
||||
const firstDir = join(tempDir, "first-env-sessions");
|
||||
const env = { PI_WEB_AGENT_SESSION_DIR: firstDir };
|
||||
const sessionDirEnvKeys = ["PI_WEB_AGENT_SESSION_DIR"];
|
||||
const resolver = new SessionDirResolver({ agentDir, env, sessionDirEnvKeys });
|
||||
|
||||
env.PI_WEB_AGENT_SESSION_DIR = join(tempDir, "mutated-env-sessions");
|
||||
sessionDirEnvKeys[0] = "OTHER_SESSION_DIR";
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: firstDir, usesConfiguredSessionDir: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pi session manager gateway", () => {
|
||||
@@ -76,7 +89,7 @@ describe("Pi session manager gateway", () => {
|
||||
const otherCwd = join(tempDir, "other-workspace");
|
||||
await writeSessionFile(defaultPiSessionDir(cwd, agentDir), "session-a", cwd);
|
||||
await writeSessionFile(defaultPiSessionDir(otherCwd, agentDir), "session-b", otherCwd);
|
||||
const gateway = createPiSessionManagerGateway({ agentDir, env: {} });
|
||||
const gateway = createPiSessionManagerGateway(piProfileOptions());
|
||||
|
||||
if (gateway.listAll === undefined) throw new Error("Expected legacy listing support");
|
||||
await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: "session-a", cwd }), expect.objectContaining({ id: "session-b", cwd: otherCwd })]));
|
||||
@@ -86,7 +99,7 @@ describe("Pi session manager gateway", () => {
|
||||
const envSessionDir = join(tempDir, "env-sessions");
|
||||
await writeSessionFile(defaultPiSessionDir(cwd, agentDir), "default-session", cwd);
|
||||
await writeSessionFile(envSessionDir, "env-session", cwd);
|
||||
const gateway = createPiSessionManagerGateway({ agentDir, env: { PI_CODING_AGENT_SESSION_DIR: envSessionDir } });
|
||||
const gateway = createPiSessionManagerGateway(piProfileOptions({ PI_CODING_AGENT_SESSION_DIR: envSessionDir }));
|
||||
|
||||
if (gateway.listAll === undefined) throw new Error("Expected legacy listing support");
|
||||
await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: "default-session", cwd }), expect.objectContaining({ id: "env-session", cwd })]));
|
||||
@@ -99,6 +112,7 @@ describe("Pi session manager gateway", () => {
|
||||
const gateway = createPiSessionManagerGateway({
|
||||
agentDir,
|
||||
env: { [envKey]: envSessionDir },
|
||||
sessionDirEnvKeys: [envKey],
|
||||
});
|
||||
|
||||
if (gateway.listAll === undefined) throw new Error("Expected legacy listing support");
|
||||
@@ -111,7 +125,7 @@ describe("Pi session manager gateway", () => {
|
||||
const otherCwd = join(tempDir, "other-workspace");
|
||||
await writeSessionFile(sharedSessionDir, "session-a", cwd);
|
||||
await writeSessionFile(sharedSessionDir, "session-b", otherCwd);
|
||||
const gateway = createPiSessionManagerGateway({ agentDir, env: { PI_CODING_AGENT_SESSION_DIR: sharedSessionDir } });
|
||||
const gateway = createPiSessionManagerGateway(piProfileOptions({ PI_CODING_AGENT_SESSION_DIR: sharedSessionDir }));
|
||||
|
||||
await expect(gateway.list(cwd)).resolves.toMatchObject([{ id: "session-a", cwd }]);
|
||||
const created = gateway.create(cwd);
|
||||
@@ -125,7 +139,7 @@ describe("Pi session manager gateway", () => {
|
||||
// hiding every session outside the daemon's own launch directory.
|
||||
expect(cwd).not.toBe(process.cwd());
|
||||
await writeSessionFile(defaultPiSessionDir(cwd, agentDir), "session-elsewhere", cwd);
|
||||
const gateway = createPiSessionManagerGateway({ agentDir, env: {} });
|
||||
const gateway = createPiSessionManagerGateway(piProfileOptions());
|
||||
|
||||
await expect(gateway.list(cwd)).resolves.toMatchObject([{ id: "session-elsewhere", cwd }]);
|
||||
});
|
||||
@@ -153,12 +167,16 @@ describe("session listing canonicalization", () => {
|
||||
// Headers are written by the Pi CLI / SDK consumers and may contain
|
||||
// unnormalized paths (trailing separators, redundant segments).
|
||||
await writeSessionFile(defaultPiSessionDir(cwd, agentDir), "session-messy", `${cwd}${sep}.${sep}`);
|
||||
const gateway = createPiSessionManagerGateway({ agentDir, env: {} });
|
||||
const gateway = createPiSessionManagerGateway(piProfileOptions());
|
||||
|
||||
await expect(gateway.list(cwd)).resolves.toMatchObject([{ id: "session-messy", cwd }]);
|
||||
});
|
||||
});
|
||||
|
||||
function piProfileOptions(env: NodeJS.ProcessEnv = {}) {
|
||||
return { agentDir, env, sessionDirEnvKeys: agentSessionDirEnvKeys() };
|
||||
}
|
||||
|
||||
function hasSessionDir(manager: PiSessionManager): manager is PiSessionManager & { getSessionDir(): string } {
|
||||
return "getSessionDir" in manager && typeof manager.getSessionDir === "function";
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { readdir } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { agentSessionDirEnvKeys, effectiveAgentConfig } from "../../config.js";
|
||||
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
||||
import type { PiSessionListEntry, PiSessionManager, PiSessionManagerGateway } from "./piSessionService.js";
|
||||
|
||||
@@ -16,20 +15,23 @@ export interface SessionDirResolution {
|
||||
}
|
||||
|
||||
export interface SessionDirResolverOptions {
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
sessionDirEnvKeys?: readonly string[];
|
||||
agentDir: string;
|
||||
env: Readonly<NodeJS.ProcessEnv>;
|
||||
sessionDirEnvKeys: readonly string[];
|
||||
}
|
||||
|
||||
export class SessionDirResolver {
|
||||
private readonly agentDir: string;
|
||||
private readonly env: NodeJS.ProcessEnv;
|
||||
private readonly sessionDirEnvKeys: readonly string[];
|
||||
private readonly envSessionDir: string | undefined;
|
||||
private readonly homeDir: string;
|
||||
|
||||
constructor(options: SessionDirResolverOptions = {}) {
|
||||
this.agentDir = options.agentDir ?? effectiveAgentConfig().dir;
|
||||
this.env = options.env ?? process.env;
|
||||
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? agentSessionDirEnvKeys();
|
||||
constructor(options: SessionDirResolverOptions) {
|
||||
this.agentDir = options.agentDir;
|
||||
this.envSessionDir = options.sessionDirEnvKeys
|
||||
.map((key) => options.env[key])
|
||||
.find((value) => value !== undefined && value !== "");
|
||||
const configuredHome = options.env["HOME"];
|
||||
this.homeDir = configuredHome !== undefined && configuredHome !== "" && isAbsolute(configuredHome) ? configuredHome : homedir();
|
||||
}
|
||||
|
||||
defaultSessionsRoot(): string {
|
||||
@@ -37,34 +39,28 @@ export class SessionDirResolver {
|
||||
}
|
||||
|
||||
globalEnvSessionDir(): string | undefined {
|
||||
const envSessionDir = this.envSessionDir();
|
||||
if (envSessionDir === undefined) return undefined;
|
||||
const expanded = expandTildePath(envSessionDir);
|
||||
if (this.envSessionDir === undefined) return undefined;
|
||||
const expanded = expandTildePath(this.envSessionDir, this.homeDir);
|
||||
return isAbsolute(expanded) ? expanded : undefined;
|
||||
}
|
||||
|
||||
resolve(cwd: string): SessionDirResolution {
|
||||
const envSessionDir = this.envSessionDir();
|
||||
if (envSessionDir !== undefined) {
|
||||
return { source: "env", sessionDir: resolveConfiguredPath(envSessionDir, cwd), usesConfiguredSessionDir: true };
|
||||
if (this.envSessionDir !== undefined) {
|
||||
return { source: "env", sessionDir: resolveConfiguredPath(this.envSessionDir, cwd, this.homeDir), usesConfiguredSessionDir: true };
|
||||
}
|
||||
|
||||
const settingsSessionDir = SettingsManager.create(cwd, this.agentDir).getSessionDir();
|
||||
if (settingsSessionDir !== undefined && settingsSessionDir !== "") {
|
||||
return { source: "settings", sessionDir: resolveConfiguredPath(settingsSessionDir, cwd), usesConfiguredSessionDir: true };
|
||||
return { source: "settings", sessionDir: resolveConfiguredPath(settingsSessionDir, cwd, this.homeDir), usesConfiguredSessionDir: true };
|
||||
}
|
||||
|
||||
return { source: "pi-default", sessionDir: defaultPiSessionDir(cwd, this.agentDir), usesConfiguredSessionDir: false };
|
||||
}
|
||||
|
||||
private envSessionDir(): string | undefined {
|
||||
return this.sessionDirEnvKeys.map((key) => this.env[key]).find((value) => value !== undefined && value !== "");
|
||||
}
|
||||
}
|
||||
|
||||
export type PiSessionManagerGatewayOptions = SessionDirResolverOptions;
|
||||
|
||||
export function createPiSessionManagerGateway(options: PiSessionManagerGatewayOptions = {}): PiSessionManagerGateway {
|
||||
export function createPiSessionManagerGateway(options: PiSessionManagerGatewayOptions): PiSessionManagerGateway {
|
||||
return new SettingsAwarePiSessionManagerGateway(new SessionDirResolver(options));
|
||||
}
|
||||
|
||||
@@ -105,7 +101,7 @@ export async function listSessionsInDir(sessionDir: string): Promise<PiSessionLi
|
||||
return sessions.map((session) => ({ ...session, cwd: canonicalizeStoredCwd(session.cwd) }));
|
||||
}
|
||||
|
||||
export async function listSessionsInDefaultPiStore(storeRoot = defaultPiSessionsRoot()): Promise<PiSessionListEntry[]> {
|
||||
export async function listSessionsInDefaultPiStore(storeRoot: string): Promise<PiSessionListEntry[]> {
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
entries = await readdir(storeRoot, { withFileTypes: true });
|
||||
@@ -130,11 +126,11 @@ function uniqueSessionsByPath(sessions: readonly PiSessionListEntry[]): PiSessio
|
||||
return [...byPath.values()].sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
}
|
||||
|
||||
export function defaultPiSessionsRoot(agentDir = effectiveAgentConfig().dir): string {
|
||||
export function defaultPiSessionsRoot(agentDir: string): string {
|
||||
return join(agentDir, "sessions");
|
||||
}
|
||||
|
||||
export function defaultPiSessionDir(cwd: string, agentDir = effectiveAgentConfig().dir): string {
|
||||
export function defaultPiSessionDir(cwd: string, agentDir: string): string {
|
||||
return sessionDirInDefaultPiStore(defaultPiSessionsRoot(agentDir), cwd);
|
||||
}
|
||||
|
||||
@@ -143,13 +139,13 @@ export function sessionDirInDefaultPiStore(storeRoot: string, cwd: string): stri
|
||||
return join(storeRoot, safePath);
|
||||
}
|
||||
|
||||
export function resolveConfiguredPath(path: string, cwd: string): string {
|
||||
const expanded = expandTildePath(path);
|
||||
export function resolveConfiguredPath(path: string, cwd: string, homeDir: string): string {
|
||||
const expanded = expandTildePath(path, homeDir);
|
||||
return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
|
||||
}
|
||||
|
||||
function expandTildePath(path: string): string {
|
||||
if (path === "~") return homedir();
|
||||
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
||||
function expandTildePath(path: string, homeDir: string): string {
|
||||
if (path === "~") return homeDir;
|
||||
if (path.startsWith("~/")) return join(homeDir, path.slice(2));
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { PiSessionService } from "./piSessionService.js";
|
||||
import { CapturingSessionEventHub, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef } from "./piSessionService.testSupport.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
|
||||
describe("PiSessionService archive and cleanup", () => {
|
||||
it("archives a session subtree within the root workspace", async () => {
|
||||
const archivedInputs: string[] = [];
|
||||
@@ -12,6 +14,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const otherWorkspaceChild = { ...sessionRecord("other-child", "/other"), path: "/sessions/other-child.jsonl", parentSessionPath: root.path };
|
||||
const fake = fakeRuntime("root", { sessionFile: root.path });
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([{ sessionId: "archived-child", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: archivedChild.path, archivePath: "/archive/archived-child.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 1, firstMessage: "archived", parentSessionPath: root.path }]),
|
||||
@@ -45,6 +48,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
it("permanently deletes archived sessions through the archive store", async () => {
|
||||
const deletedSessionIds: string[] = [];
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([]),
|
||||
get: (sessionId) => Promise.resolve(sessionId === "archived" || "archived".startsWith(sessionId)
|
||||
@@ -78,6 +82,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const open = vi.fn(() => { throw new Error("bulk archive should not open inactive runtimes"); });
|
||||
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-01-03T00:00:00.000Z" }))));
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([]),
|
||||
get: () => Promise.resolve(undefined),
|
||||
@@ -112,6 +117,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
let createCalls = 0;
|
||||
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-01-03T00:00:00.000Z" }))));
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
createCalls += 1;
|
||||
return Promise.resolve(busy.runtime);
|
||||
@@ -152,6 +158,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const busy = fakeRuntime("busy-archived", { isStreaming: true });
|
||||
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(busy.runtime),
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([busyRecord, idleRecord]),
|
||||
@@ -188,6 +195,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
||||
const listCalls: string[] = [];
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([
|
||||
{ sessionId: "legacy-a", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z" },
|
||||
@@ -230,6 +238,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const archived = { sessionId: "archived-old", cwd: "/old-project", archivedAt: "2026-04-01T00:00:00.000Z", archivePath: "/archive/archived-old.jsonl" };
|
||||
const otherArchived = { sessionId: "archived-other", cwd: "/other-project", archivedAt: "2026-04-01T00:00:00.000Z", archivePath: "/archive/archived-other.jsonl" };
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([archived, otherArchived]),
|
||||
@@ -282,6 +291,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-06-25T00:00:00.000Z", archivePath: `/archive/${input.sessionId}.jsonl` }))));
|
||||
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([
|
||||
@@ -323,6 +333,7 @@ describe("PiSessionService archive and cleanup", () => {
|
||||
const fake = fakeRuntime("busy-open", { isStreaming: true, sessionManager: fakeSessionManager("/old-project"), sessionFile: "/sessions/busy-open.jsonl" });
|
||||
const archivedInputs: string[] = [];
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
archiveStore: {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { PiSessionService, type PiAgentSession, type PiSessionRuntime } from "./piSessionService.js";
|
||||
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
|
||||
function deferred<T = void>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
@@ -20,12 +22,15 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime();
|
||||
let createCalls = 0;
|
||||
const createAgentRuntime: RuntimeCreator = async () => {
|
||||
let runtimeAgentDir: string | undefined;
|
||||
const createAgentRuntime: RuntimeCreator = async (_createRuntime, options) => {
|
||||
createCalls += 1;
|
||||
runtimeAgentDir = options.agentDir;
|
||||
await Promise.resolve();
|
||||
return fake.runtime;
|
||||
};
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -34,6 +39,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const session = await service.start("/workspace");
|
||||
|
||||
expect(createCalls).toBe(1);
|
||||
expect(runtimeAgentDir).toBe(TEST_AGENT_DIR);
|
||||
expect(fake.calls.bindExtensions).toHaveLength(1);
|
||||
expect(session).toMatchObject({ id: "session-1", cwd: "/workspace", messageCount: 0 });
|
||||
expect(service.activeCount()).toBe(1);
|
||||
@@ -53,6 +59,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
let service: PiSessionService | undefined;
|
||||
try {
|
||||
service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -79,6 +86,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const fake = fakeRuntime("legacy-session");
|
||||
const open = vi.fn(() => fakeSessionManager());
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: {
|
||||
create: () => fakeSessionManager(),
|
||||
@@ -127,6 +135,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const gateway = sessionGateway([sessionRecord(sessionId)]);
|
||||
const open = vi.spyOn(gateway, "open");
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: emptyArchiveStore(),
|
||||
createAgentRuntime,
|
||||
sessionManager: gateway,
|
||||
@@ -180,6 +189,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
: Promise.resolve(runtime);
|
||||
};
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: emptyArchiveStore(),
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([sessionRecord(sessionId)]),
|
||||
@@ -219,6 +229,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const runtimeResult = deferred<PiSessionRuntime>();
|
||||
const fake = fakeRuntime(sessionId);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: emptyArchiveStore(),
|
||||
createAgentRuntime: () => {
|
||||
createStarted.resolve();
|
||||
@@ -252,6 +263,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
let rebindSession: ((session: PiAgentSession) => Promise<void>) | undefined;
|
||||
fake.runtime.setRebindSession = (callback) => { rebindSession = callback; };
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -278,6 +290,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
},
|
||||
});
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -312,6 +325,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
},
|
||||
});
|
||||
service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("idle-session")]),
|
||||
heartbeatIntervalMs: 1_000,
|
||||
@@ -347,6 +361,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
},
|
||||
});
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("completion-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -365,6 +380,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
|
||||
it("uses injected archive and session-manager gateways for listing", async () => {
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-01T00:00:00.000Z" }]),
|
||||
get: () => Promise.resolve(undefined),
|
||||
@@ -394,6 +410,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
|
||||
it("lists archived records that have been moved out of the active session directory", async () => {
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: "/sessions/archived.jsonl", archivePath: "/archive/archived.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 2, firstMessage: "bye" }]),
|
||||
get: () => Promise.resolve(undefined),
|
||||
@@ -424,6 +441,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime("runtime-reload-session");
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("runtime-reload-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -456,6 +474,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
return runtime;
|
||||
};
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([sessionRecord("reload-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -479,6 +498,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
it("refuses to reload a session that has active work in progress", async () => {
|
||||
const fake = fakeRuntime("busy-session", { isStreaming: true });
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("busy-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -493,6 +513,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
|
||||
it("refuses to reload an archived session", async () => {
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([]),
|
||||
get: (sessionId) => Promise.resolve(sessionId === "archived" || "archived".startsWith(sessionId)
|
||||
@@ -514,6 +535,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
it("reconciles workspace activity when listing only archived sessions", async () => {
|
||||
const reconciliations: { cwd: string; sessionIds: string[] }[] = [];
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
archiveStore: {
|
||||
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: "/sessions/archived.jsonl", archivePath: "/archive/archived.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 2, firstMessage: "bye" }]),
|
||||
get: () => Promise.resolve(undefined),
|
||||
|
||||
@@ -5,10 +5,13 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { PiSessionService } from "./piSessionService.js";
|
||||
import { CapturingSessionEventHub, fakeRuntime, runtimeCreator, sessionGateway, sessionRecord, sessionRef, TEST_MODEL_ID, TEST_MODEL_PROVIDER, testModel, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
|
||||
describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
it("sends prompts to an injected runtime without touching the SDK runtime", async () => {
|
||||
const fake = fakeRuntime("prompt-session");
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("prompt-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -26,6 +29,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
});
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("echo-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -55,6 +59,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
return fake.runtime;
|
||||
};
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([sessionRecord("prompt-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -90,6 +95,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime("name-session", { model, agent: { streamFn } });
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("name-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -111,6 +117,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
getFollowUpMessages: () => ["then do this"],
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("status-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -131,6 +138,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
getFollowUpMessages: () => ["already queued"],
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("dedupe-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -146,6 +154,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime("queued-session", { isStreaming: true });
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("queued-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -171,6 +180,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("compacting-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -215,6 +225,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
it("clears queued messages when aborting active work", async () => {
|
||||
const fake = fakeRuntime("abort-session");
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("abort-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -231,6 +242,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
it("clears prompts queued during compaction when aborting active work", async () => {
|
||||
const fake = fakeRuntime("abort-compaction-session", { isCompacting: true });
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("abort-compaction-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
@@ -255,6 +267,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
const fake = fakeRuntime("auth-session", { model, modelRegistry });
|
||||
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
modelRegistry,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("auth-session")]),
|
||||
@@ -285,6 +298,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
it("clears queued messages when stopping a session runtime", async () => {
|
||||
const fake = fakeRuntime("stop-session");
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([sessionRecord("stop-session")]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
|
||||
@@ -3,12 +3,15 @@ import { PiSessionService, type PiAgentSession } from "./piSessionService.js";
|
||||
import type { SpawnTargetDecision } from "./spawnTargetResolver.js";
|
||||
import { CapturingSessionEventHub, fakeRuntime, runtimeCreator, sessionGateway, testModel, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
|
||||
describe("PiSessionService", () => {
|
||||
describe("spawnSession", () => {
|
||||
function spawnService(decision: SpawnTargetDecision) {
|
||||
const fake = fakeRuntime("spawned-1", { sessionFile: "/tmp/spawned-1.jsonl" });
|
||||
const log: { details: Record<string, unknown>; message: string }[] = [];
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([]),
|
||||
spawnTargets: { resolveSpawnTarget: () => Promise.resolve(decision) },
|
||||
@@ -41,6 +44,7 @@ describe("PiSessionService", () => {
|
||||
return fake.runtime;
|
||||
};
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([]),
|
||||
spawnTargets: { resolveSpawnTarget: () => Promise.resolve({ allowed: true, cwd: "/workspace-feature" }) },
|
||||
@@ -75,6 +79,7 @@ describe("PiSessionService", () => {
|
||||
it("is disabled when no spawn target resolver is configured", async () => {
|
||||
const fake = fakeRuntime("spawned-x");
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
|
||||
@@ -6,6 +6,8 @@ import { PiSessionService, type PiAgentSession } from "./piSessionService.js";
|
||||
import type { SpawnTargetDecision } from "./spawnTargetResolver.js";
|
||||
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, testModel, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
|
||||
describe("PiSessionService", () => {
|
||||
describe("spawnSubsession", () => {
|
||||
function subsessionService(decision: SpawnTargetDecision, heartbeatIntervalMs = 60_000) {
|
||||
@@ -32,6 +34,7 @@ describe("PiSessionService", () => {
|
||||
isArchived: (sessionId: string) => Promise.resolve(archived.has(sessionId)),
|
||||
};
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([]),
|
||||
archiveStore,
|
||||
@@ -72,6 +75,7 @@ describe("PiSessionService", () => {
|
||||
return runtime;
|
||||
};
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: sessionGateway([]),
|
||||
archiveStore: emptyArchiveStore(),
|
||||
@@ -111,6 +115,7 @@ describe("PiSessionService", () => {
|
||||
const runtimes = [parent.runtime, child.runtime];
|
||||
let index = 0;
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
const runtime = runtimes[index] ?? child.runtime;
|
||||
index += 1;
|
||||
@@ -162,6 +167,7 @@ describe("PiSessionService", () => {
|
||||
let index = 0;
|
||||
const open = vi.fn(() => childManager);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
const runtime = runtimes[index] ?? child.runtime;
|
||||
index += 1;
|
||||
@@ -203,6 +209,7 @@ describe("PiSessionService", () => {
|
||||
}),
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||
archiveStore: emptyArchiveStore(),
|
||||
@@ -227,6 +234,7 @@ describe("PiSessionService", () => {
|
||||
}),
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||
archiveStore: emptyArchiveStore(),
|
||||
@@ -248,6 +256,7 @@ describe("PiSessionService", () => {
|
||||
}),
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||
archiveStore: emptyArchiveStore(),
|
||||
@@ -268,6 +277,7 @@ describe("PiSessionService", () => {
|
||||
sessionManager: fakeSessionManager("/workspace", { getEntries: () => [] }),
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([childRecord]), open: () => fakeSessionManager() },
|
||||
archiveStore: emptyArchiveStore(),
|
||||
@@ -288,6 +298,7 @@ describe("PiSessionService", () => {
|
||||
}),
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(forkedParent.runtime),
|
||||
sessionManager: { create: () => forkedParent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||
archiveStore: emptyArchiveStore(),
|
||||
@@ -326,6 +337,7 @@ describe("PiSessionService", () => {
|
||||
let index = 0;
|
||||
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: (_createRuntime, options) => {
|
||||
delegationCapabilities.push(options.delegationToolsEnabled);
|
||||
const runtime = runtimes[index] ?? parent.runtime;
|
||||
@@ -388,6 +400,7 @@ describe("PiSessionService", () => {
|
||||
return childManager;
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
const runtime = runtimes[index] ?? parent.runtime;
|
||||
index += 1;
|
||||
@@ -445,6 +458,7 @@ describe("PiSessionService", () => {
|
||||
let index = 0;
|
||||
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
const runtime = runtimes[index] ?? parent.runtime;
|
||||
index += 1;
|
||||
@@ -509,6 +523,7 @@ describe("PiSessionService", () => {
|
||||
throw new Error(`unexpected open path ${path}`);
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: {
|
||||
create: () => parentManager,
|
||||
@@ -584,6 +599,7 @@ describe("PiSessionService", () => {
|
||||
throw new Error(`unexpected open path ${path}`);
|
||||
});
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime,
|
||||
sessionManager: {
|
||||
create: () => copiedParentManager,
|
||||
@@ -641,6 +657,7 @@ describe("PiSessionService", () => {
|
||||
let index = 0;
|
||||
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
const runtime = runtimes[index] ?? parent.runtime;
|
||||
index += 1;
|
||||
@@ -693,6 +710,7 @@ describe("PiSessionService", () => {
|
||||
let index = 0;
|
||||
const open = vi.fn((path: string) => path === actualParentFile ? parent.session.sessionManager : childManager);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: () => {
|
||||
const runtime = runtimes[index] ?? parent.runtime;
|
||||
index += 1;
|
||||
@@ -733,6 +751,7 @@ describe("PiSessionService", () => {
|
||||
const child = fakeRuntime("child-fork-1", { sessionFile: childFile, sessionManager: childManager });
|
||||
const open = vi.fn(() => childManager);
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(child.runtime),
|
||||
sessionManager: {
|
||||
create: () => childManager,
|
||||
@@ -842,6 +861,7 @@ describe("PiSessionService", () => {
|
||||
it("is disabled when no spawn target resolver is configured", async () => {
|
||||
const fake = fakeRuntime("nope");
|
||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||
sessionManager: sessionGateway([]),
|
||||
heartbeatIntervalMs: 60_000,
|
||||
|
||||
@@ -25,8 +25,6 @@ import type { ActiveSession } from "./sessionRuntimeStore.js";
|
||||
import { createModelRegistryForAgentDir, type AuthChange } from "./authService.js";
|
||||
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
|
||||
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
||||
import { effectiveAgentConfig } from "../../config.js";
|
||||
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
||||
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
||||
import type { SavedPromptAttachment, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkFailure, SessionBulkMutationRef } from "../../shared/apiTypes.js";
|
||||
@@ -380,9 +378,9 @@ function createPiWebEditToolDefinition(cwd: string) {
|
||||
}
|
||||
|
||||
export interface PiSessionServiceDependencies {
|
||||
agentDir: string;
|
||||
sessionManager: PiSessionManagerGateway;
|
||||
archiveStore?: SessionArchiveRepository;
|
||||
agentDir?: string;
|
||||
sessionManager?: PiSessionManagerGateway;
|
||||
createRuntime?: PiWebCreateAgentSessionRuntimeFactory;
|
||||
createAgentRuntime?: CreateAgentRuntime;
|
||||
modelRegistry?: ModelRegistryInstance;
|
||||
@@ -441,10 +439,10 @@ export class PiSessionService implements SessionRouteService {
|
||||
private readonly logger: PiSessionLogger;
|
||||
private readonly now: () => Date;
|
||||
|
||||
constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies = {}) {
|
||||
constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies) {
|
||||
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
||||
this.agentDir = deps.agentDir ?? effectiveAgentConfig().dir;
|
||||
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
|
||||
this.agentDir = deps.agentDir;
|
||||
this.sessionManager = deps.sessionManager;
|
||||
this.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir);
|
||||
this.spawnTargets = deps.spawnTargets;
|
||||
this.logger = deps.logger ?? noopLogger;
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { SessionRouteLookup, SessionRouteService } from "./sessionService.j
|
||||
import { registerSessionRoutes } from "./sessionRoutes.js";
|
||||
import type { NormalizedSessionCleanupRequest } from "./sessionCleanup.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
|
||||
let app: FastifyInstance;
|
||||
let service: PiSessionService;
|
||||
let sessionManager: RejectingSessionManager;
|
||||
@@ -18,7 +20,7 @@ beforeEach(async () => {
|
||||
await app.register(fastifyWebsocket);
|
||||
sessionManager = new RejectingSessionManager();
|
||||
const eventHub = new SessionEventHub();
|
||||
service = new PiSessionService(eventHub, { sessionManager, heartbeatIntervalMs: 60_000 });
|
||||
service = new PiSessionService(eventHub, { agentDir: TEST_AGENT_DIR, sessionManager, heartbeatIntervalMs: 60_000 });
|
||||
registerSessionRoutes(app, service, eventHub);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user