singularity-forge/packages/daemon/src/session-manager.test.ts
2026-05-05 14:46:18 +02:00

995 lines
27 KiB
TypeScript

/**
* SessionManager unit tests.
*
* Uses the MockRpcClient + TestableSessionManager pattern (K008) to test
* session lifecycle, event handling, cost tracking, blocker detection,
* and cleanup without spawning real SF processes.
*/
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import { afterEach, describe, it } from "vitest";
import { Logger } from "./logger.js";
import { SessionManager } from "./session-manager.js";
import type { ManagedSession, PendingBlocker } from "./types.js";
import { MAX_EVENTS } from "./types.js";
// ---------------------------------------------------------------------------
// Mock RpcClient (duck-typed to match RpcClient interface)
// ---------------------------------------------------------------------------
class MockRpcClient {
started = false;
stopped = false;
aborted = false;
prompted: string[] = [];
switchedSessions: string[] = [];
private eventListeners: Array<(event: Record<string, unknown>) => void> = [];
uiResponses: Array<{ requestId: string; response: Record<string, unknown> }> =
[];
/** Control — set to make start() reject */
startError: Error | null = null;
/** Control — set to make init() reject */
initError: Error | null = null;
/** Control — override sessionId from init */
initSessionId = "mock-session-001";
cwd: string;
args: string[];
constructor(options?: Record<string, unknown>) {
this.cwd = (options?.cwd as string) ?? "";
this.args = (options?.args as string[]) ?? [];
}
async start(): Promise<void> {
if (this.startError) throw this.startError;
this.started = true;
}
async stop(): Promise<void> {
this.stopped = true;
}
async init(): Promise<{ sessionId: string; version: string }> {
if (this.initError) throw this.initError;
return { sessionId: this.initSessionId, version: "2.51.0" };
}
onEvent(listener: (event: Record<string, unknown>) => void): () => void {
this.eventListeners.push(listener);
return () => {
const idx = this.eventListeners.indexOf(listener);
if (idx >= 0) this.eventListeners.splice(idx, 1);
};
}
async prompt(message: string): Promise<void> {
this.prompted.push(message);
if (message === "/sf pause") {
queueMicrotask(() => {
this.emitEvent({
type: "extension_ui_request",
id: "pause-notice",
method: "notify",
message: "Auto-mode paused: daemon reload requested",
});
});
}
}
async abort(): Promise<void> {
this.aborted = true;
}
sendUIResponse(requestId: string, response: Record<string, unknown>): void {
this.uiResponses.push({ requestId, response });
}
async getState(): Promise<{ sessionFile: string; sessionId: string }> {
return {
sessionFile: `/tmp/${this.initSessionId}.jsonl`,
sessionId: this.initSessionId,
};
}
async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {
this.switchedSessions.push(sessionPath);
return { cancelled: false };
}
/** Test helper — emit an event to all listeners */
emitEvent(event: Record<string, unknown>): void {
for (const listener of this.eventListeners) {
listener(event);
}
}
}
// ---------------------------------------------------------------------------
// TestableSessionManager — injects mock clients without module mocking (K008)
// ---------------------------------------------------------------------------
class TestableSessionManager extends SessionManager {
lastClient: MockRpcClient | null = null;
allClients: MockRpcClient[] = [];
private sessionCounter = 0;
nextInitError: Error | null = null;
nextStartError: Error | null = null;
protected override createRpcClient(
_cliPath: string,
cwd: string,
args: string[],
): any {
this.sessionCounter++;
const client = new MockRpcClient({ cwd, args });
client.initSessionId = `mock-session-${String(this.sessionCounter).padStart(3, "0")}`;
this.lastClient = client;
this.allClients.push(client);
return client;
}
override async startSession(options: {
projectDir: string;
command?: string;
model?: string;
bare?: boolean;
cliPath?: string;
}): Promise<string> {
const { projectDir } = options;
if (!projectDir || projectDir.trim() === "") {
throw new Error("projectDir is required and cannot be empty");
}
const resolvedDir = resolve(projectDir);
const projectName = basename(resolvedDir);
// Check duplicate via getSessionByDir
const existing = this.getSessionByDir(resolvedDir);
if (existing) {
throw new Error(
`Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`,
);
}
const client = this.createRpcClient("mock-sf", resolvedDir, []);
if (this.nextStartError) {
client.startError = this.nextStartError;
this.nextStartError = null;
}
if (this.nextInitError) {
client.initError = this.nextInitError;
this.nextInitError = null;
}
// Build session shell
const session: ManagedSession = {
sessionId: "",
projectDir: resolvedDir,
projectName,
status: "starting",
reloadState: "running",
client: client as any, // duck-typed mock
events: [],
pendingBlocker: null,
cost: {
totalCost: 0,
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
},
startTime: Date.now(),
startOptions: { ...options, projectDir: resolvedDir },
};
// Insert into internal sessions map
(this as any).sessions.set(resolvedDir, session);
try {
await client.start();
const initResult = await client.init();
session.sessionId = initResult.sessionId;
session.status = "running";
// Wire event tracking using parent's handleEvent
session.unsubscribe = client.onEvent((event: Record<string, unknown>) => {
(this as any).handleEvent(session, event);
});
// Kick off autonomous mode
const command = options.command ?? "/sf autonomous";
await client.prompt(command);
// Emit lifecycle events (matching parent behavior)
(this as any).logger.info("session started", {
sessionId: session.sessionId,
projectDir: resolvedDir,
});
this.emit("session:started", {
sessionId: session.sessionId,
projectDir: resolvedDir,
projectName,
});
return session.sessionId;
} catch (err) {
session.status = "error";
session.error = err instanceof Error ? err.message : String(err);
try {
await client.stop();
} catch {
/* swallow */
}
(this as any).logger.error("session error", {
sessionId: session.sessionId,
projectDir: resolvedDir,
error: session.error,
});
this.emit("session:error", {
sessionId: session.sessionId,
projectDir: resolvedDir,
projectName,
error: session.error,
});
throw new Error(
`Failed to start session for ${resolvedDir}: ${session.error}`,
);
}
}
}
// ---------------------------------------------------------------------------
// Logger spy helper
// ---------------------------------------------------------------------------
interface LogCall {
level: string;
msg: string;
data?: Record<string, unknown>;
}
class SpyLogger {
calls: LogCall[] = [];
private tmpDir: string;
logger: Logger;
constructor() {
this.tmpDir = mkdtempSync(join(tmpdir(), "sm-test-"));
this.logger = new Logger({
filePath: join(this.tmpDir, "test.log"),
level: "debug",
});
// Intercept write calls by wrapping the logger methods
const original = {
debug: this.logger.debug.bind(this.logger),
info: this.logger.info.bind(this.logger),
warn: this.logger.warn.bind(this.logger),
error: this.logger.error.bind(this.logger),
};
this.logger.debug = (msg: string, data?: Record<string, unknown>) => {
this.calls.push({ level: "debug", msg, data });
original.debug(msg, data);
};
this.logger.info = (msg: string, data?: Record<string, unknown>) => {
this.calls.push({ level: "info", msg, data });
original.info(msg, data);
};
this.logger.warn = (msg: string, data?: Record<string, unknown>) => {
this.calls.push({ level: "warn", msg, data });
original.warn(msg, data);
};
this.logger.error = (msg: string, data?: Record<string, unknown>) => {
this.calls.push({ level: "error", msg, data });
original.error(msg, data);
};
}
async cleanup(): Promise<void> {
await this.logger.close();
try {
rmSync(this.tmpDir, { recursive: true, force: true });
} catch {
/* ignore */
}
}
findCalls(level: string, msgSubstring: string): LogCall[] {
return this.calls.filter(
(c) => c.level === level && c.msg.includes(msgSubstring),
);
}
}
// ---------------------------------------------------------------------------
// Test Helpers
// ---------------------------------------------------------------------------
let allManagers: TestableSessionManager[] = [];
let allSpyLoggers: SpyLogger[] = [];
function createManager(): { manager: TestableSessionManager; spy: SpyLogger } {
const spy = new SpyLogger();
const manager = new TestableSessionManager(spy.logger);
allManagers.push(manager);
allSpyLoggers.push(spy);
return { manager, spy };
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("SessionManager", () => {
afterEach(async () => {
for (const m of allManagers) {
try {
await m.cleanup();
} catch {
/* swallow */
}
}
allManagers = [];
for (const s of allSpyLoggers) {
await s.cleanup();
}
allSpyLoggers = [];
});
// ---- Lifecycle: start → running → completed ----
it("start → running → completed lifecycle", async () => {
const { manager, spy } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/test-project",
});
assert.ok(sessionId);
const session = manager.getSession(sessionId);
assert.ok(session);
assert.equal(session.status, "running");
assert.equal(session.projectName, "test-project");
// Simulate terminal notification
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "n1",
method: "notify",
message: "Auto-mode stopped: completed all tasks",
});
assert.equal(session.status, "completed");
// Verify logger calls
const startedLogs = spy.findCalls("info", "session started");
assert.equal(startedLogs.length, 1);
const completedLogs = spy.findCalls("info", "session completed");
assert.equal(completedLogs.length, 1);
});
it("runtime epoch mismatch restarts child and resumes prior session file", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/reload-project",
});
const originalClient = manager.lastClient!;
const restarted = new Promise<void>((resolve) => {
manager.once("session:restarted", () => resolve());
});
originalClient.emitEvent({
type: "runtime_heartbeat",
sessionId,
sessionFile: "/tmp/reload-session.jsonl",
unitType: "execute-task",
unitId: "M001/S01/T01",
runtimeEpoch: 100,
sourceEpoch: 200,
emittedAt: Date.now(),
});
await restarted;
const session = manager.getSession("mock-session-002")!;
assert.ok(session);
assert.equal(originalClient.stopped, true);
assert.equal(manager.allClients.length, 2);
const replacement = manager.allClients[1];
assert.deepEqual(replacement.switchedSessions, [
"/tmp/mock-session-001.jsonl",
]);
assert.deepEqual(replacement.prompted, ["/sf autonomous"]);
assert.equal(session.reloadState, "running");
});
// ---- Lifecycle: start → running → blocked → resolve → running → completed ----
it("start → blocked → resolve → running → completed lifecycle", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/test-project-2",
});
const session = manager.getSession(sessionId)!;
// Simulate blocking UI request (non-fire-and-forget method)
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "blocker-1",
method: "confirm",
title: "Merge PR?",
message: "Should I merge this PR?",
});
assert.equal(session.status, "blocked");
assert.ok(session.pendingBlocker);
assert.equal(session.pendingBlocker!.id, "blocker-1");
assert.equal(session.pendingBlocker!.method, "confirm");
// Resolve the blocker
await manager.resolveBlocker(sessionId, "yes");
assert.equal(session.status, "running");
assert.equal(session.pendingBlocker, null);
// Verify UI response was sent
const client = manager.lastClient!;
assert.equal(client.uiResponses.length, 1);
assert.equal(client.uiResponses[0].requestId, "blocker-1");
// Complete the session
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "n2",
method: "notify",
message: "Auto-mode stopped: all done",
});
assert.equal(session.status, "completed");
});
// ---- Lifecycle: start → error (init failure) ----
it("start → error when init fails", async () => {
const { manager, spy } = createManager();
manager.nextInitError = new Error("Connection refused");
await assert.rejects(
() => manager.startSession({ projectDir: "/tmp/test-error-project" }),
(err: Error) => {
assert.ok(err.message.includes("Connection refused"));
return true;
},
);
// Session should still exist in map with error status
const session = manager.getSessionByDir("/tmp/test-error-project");
assert.ok(session);
assert.equal(session.status, "error");
assert.ok(session.error?.includes("Connection refused"));
// Logger should have error call
const errorLogs = spy.findCalls("error", "session error");
assert.equal(errorLogs.length, 1);
});
// ---- Duplicate session prevention ----
it("rejects duplicate session for same projectDir", async () => {
const { manager } = createManager();
await manager.startSession({ projectDir: "/tmp/dup-test" });
await assert.rejects(
() => manager.startSession({ projectDir: "/tmp/dup-test" }),
(err: Error) => {
assert.ok(err.message.includes("Session already active"));
return true;
},
);
});
// ---- Cancel session ----
it("cancels a running session", async () => {
const { manager, spy } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/cancel-test",
});
const session = manager.getSession(sessionId)!;
const client = manager.lastClient!;
await manager.cancelSession(sessionId);
assert.equal(session.status, "cancelled");
assert.ok(client.aborted);
assert.ok(client.stopped);
const cancelLogs = spy.findCalls("info", "session cancelled");
assert.equal(cancelLogs.length, 1);
});
// ---- Cost accumulation (K004 cumulative-max) ----
it("accumulates cost using cumulative-max pattern (K004)", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/cost-test",
});
const session = manager.getSession(sessionId)!;
const client = manager.lastClient!;
// First cost update
client.emitEvent({
type: "cost_update",
runId: "run-1",
turnCost: 0.01,
cumulativeCost: 0.01,
tokens: { input: 100, output: 50, cacheRead: 20, cacheWrite: 10 },
});
assert.equal(session.cost.totalCost, 0.01);
assert.equal(session.cost.tokens.input, 100);
// Second cost update — cumulative values should increase
client.emitEvent({
type: "cost_update",
runId: "run-1",
turnCost: 0.02,
cumulativeCost: 0.03,
tokens: { input: 250, output: 120, cacheRead: 40, cacheWrite: 20 },
});
assert.equal(session.cost.totalCost, 0.03);
assert.equal(session.cost.tokens.input, 250);
assert.equal(session.cost.tokens.output, 120);
// Third update with lower values — max should hold
client.emitEvent({
type: "cost_update",
runId: "run-2",
turnCost: 0.005,
cumulativeCost: 0.02, // lower than 0.03 — should NOT replace
tokens: { input: 50, output: 30, cacheRead: 5, cacheWrite: 2 },
});
assert.equal(session.cost.totalCost, 0.03); // max held
assert.equal(session.cost.tokens.input, 250); // max held
});
// ---- Ring buffer event trimming ----
it("trims events when exceeding MAX_EVENTS", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/ringbuf-test",
});
const session = manager.getSession(sessionId)!;
const client = manager.lastClient!;
// Push MAX_EVENTS + 20 events
for (let i = 0; i < MAX_EVENTS + 20; i++) {
client.emitEvent({
type: "assistant_message",
id: `msg-${i}`,
content: `Event ${i}`,
});
}
assert.equal(session.events.length, MAX_EVENTS);
// Oldest events should be trimmed — first event should be #20
const firstEvent = session.events[0] as Record<string, unknown>;
assert.equal(firstEvent.id, "msg-20");
});
// ---- Blocker detection (non-fire-and-forget extension_ui_request) ----
it("detects blocker from non-fire-and-forget extension_ui_request", async () => {
const { manager, spy } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/blocker-test",
});
const session = manager.getSession(sessionId)!;
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "sel-1",
method: "select",
title: "Choose deployment target",
options: ["staging", "production"],
});
assert.equal(session.status, "blocked");
assert.ok(session.pendingBlocker);
assert.equal(session.pendingBlocker!.method, "select");
const blockedLogs = spy.findCalls("info", "session blocked");
assert.equal(blockedLogs.length, 1);
});
// ---- Fire-and-forget methods do NOT block ----
it("fire-and-forget methods do not trigger blocker", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/faf-test",
});
const session = manager.getSession(sessionId)!;
// setStatus is fire-and-forget
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "st-1",
method: "setStatus",
statusKey: "build",
statusText: "Building...",
});
assert.equal(session.status, "running");
assert.equal(session.pendingBlocker, null);
});
// ---- Terminal detection (auto-mode stopped notification) ----
it("detects terminal from auto-mode stopped notification", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/terminal-test",
});
const session = manager.getSession(sessionId)!;
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "n1",
method: "notify",
message: "Step-mode stopped: user requested",
});
assert.equal(session.status, "completed");
});
// ---- getAllSessions returns all tracked sessions ----
it("getAllSessions returns all tracked sessions", async () => {
const { manager } = createManager();
await manager.startSession({ projectDir: "/tmp/proj-a" });
await manager.startSession({ projectDir: "/tmp/proj-b" });
await manager.startSession({ projectDir: "/tmp/proj-c" });
const all = manager.getAllSessions();
assert.equal(all.length, 3);
const dirs = all.map((s) => s.projectDir).sort();
assert.ok(dirs[0].endsWith("proj-a"));
assert.ok(dirs[1].endsWith("proj-b"));
assert.ok(dirs[2].endsWith("proj-c"));
});
// ---- cleanup stops all active sessions ----
it("cleanup stops all active sessions", async () => {
const { manager } = createManager();
await manager.startSession({ projectDir: "/tmp/cleanup-a" });
await manager.startSession({ projectDir: "/tmp/cleanup-b" });
const clients = [...manager.allClients];
assert.equal(clients.length, 2);
await manager.cleanup();
const all = manager.getAllSessions();
for (const s of all) {
assert.equal(s.status, "cancelled");
}
// Both clients should have been stopped
for (const c of clients) {
assert.ok(c.stopped);
}
});
// ---- EventEmitter: session:started ----
it("emits session:started event", async () => {
const { manager } = createManager();
let emittedData: Record<string, unknown> | undefined;
manager.on("session:started", (data: Record<string, unknown>) => {
emittedData = data;
});
const sessionId = await manager.startSession({
projectDir: "/tmp/emit-start",
});
assert.ok(emittedData);
assert.equal(emittedData.sessionId, sessionId);
assert.equal(emittedData.projectName, "emit-start");
});
// ---- EventEmitter: session:blocked ----
it("emits session:blocked event", async () => {
const { manager } = createManager();
let emittedData: Record<string, unknown> | undefined;
manager.on("session:blocked", (data: Record<string, unknown>) => {
emittedData = data;
});
await manager.startSession({ projectDir: "/tmp/emit-blocked" });
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "b-1",
method: "input",
title: "Enter API key",
});
assert.ok(emittedData);
assert.equal((emittedData.blocker as PendingBlocker).id, "b-1");
});
// ---- EventEmitter: session:completed ----
it("emits session:completed event", async () => {
const { manager } = createManager();
let emittedData: Record<string, unknown> | undefined;
manager.on("session:completed", (data: Record<string, unknown>) => {
emittedData = data;
});
await manager.startSession({ projectDir: "/tmp/emit-completed" });
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "n1",
method: "notify",
message: "Auto-mode stopped: success",
});
assert.ok(emittedData);
assert.equal(emittedData.projectName, "emit-completed");
});
// ---- EventEmitter: session:error ----
it("emits session:error event on init failure", async () => {
const { manager } = createManager();
let emittedData: Record<string, unknown> | undefined;
manager.on("session:error", (data: Record<string, unknown>) => {
emittedData = data;
});
manager.nextInitError = new Error("Process crashed");
try {
await manager.startSession({ projectDir: "/tmp/emit-error" });
} catch {
/* expected */
}
assert.ok(emittedData);
assert.ok((emittedData.error as string).includes("Process crashed"));
});
// ---- EventEmitter: session:event ----
it("emits session:event for every forwarded event", async () => {
const { manager } = createManager();
const events: Record<string, unknown>[] = [];
manager.on("session:event", (data) => {
events.push(data);
});
await manager.startSession({ projectDir: "/tmp/emit-event" });
manager.lastClient!.emitEvent({
type: "assistant_message",
id: "a1",
content: "Hello",
});
manager.lastClient!.emitEvent({ type: "tool_use", id: "t1", name: "read" });
assert.equal(events.length, 2);
});
// ---- Empty projectDir rejection ----
it("rejects empty projectDir", async () => {
const { manager } = createManager();
await assert.rejects(
() => manager.startSession({ projectDir: "" }),
(err: Error) => {
assert.ok(err.message.includes("projectDir is required"));
return true;
},
);
await assert.rejects(
() => manager.startSession({ projectDir: " " }),
(err: Error) => {
assert.ok(err.message.includes("projectDir is required"));
return true;
},
);
});
// ---- Logger receives structured calls ----
it("logger receives structured calls during lifecycle", async () => {
const { manager, spy } = createManager();
const _sessionId = await manager.startSession({
projectDir: "/tmp/log-test",
});
// Should have 'session started' info log
const started = spy.findCalls("info", "session started");
assert.equal(started.length, 1);
assert.ok(started[0].data?.sessionId);
assert.ok(started[0].data?.projectDir);
// Emit an event — should produce debug log
manager.lastClient!.emitEvent({
type: "assistant_message",
id: "a1",
content: "hi",
});
const debugLogs = spy.findCalls("debug", "session event");
assert.ok(debugLogs.length >= 1);
assert.ok(debugLogs[0].data?.type);
});
// ---- getResult returns structured status ----
it("getResult returns structured status", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/result-test",
});
const result = manager.getResult(sessionId);
assert.equal(result.sessionId, sessionId);
assert.equal(result.status, "running");
assert.equal(result.reloadState, "running");
assert.equal(result.projectName, "result-test");
assert.equal(result.error, null);
assert.equal(result.lastHeartbeat, null);
assert.equal(result.pendingBlocker, null);
assert.ok(typeof result.durationMs === "number");
assert.ok(result.cost);
assert.ok(Array.isArray(result.recentEvents));
});
// ---- getResult throws for unknown session ----
it("getResult throws for unknown sessionId", () => {
const { manager } = createManager();
assert.throws(
() => manager.getResult("nonexistent"),
(err: Error) => err.message.includes("Session not found"),
);
});
// ---- resolveBlocker throws when no blocker pending ----
it("resolveBlocker throws when no blocker pending", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/no-blocker",
});
await assert.rejects(
() => manager.resolveBlocker(sessionId, "yes"),
(err: Error) => err.message.includes("No pending blocker"),
);
});
// ---- cancelSession throws for unknown session ----
it("cancelSession throws for unknown sessionId", async () => {
const { manager } = createManager();
await assert.rejects(
() => manager.cancelSession("nonexistent"),
(err: Error) => err.message.includes("Session not found"),
);
});
// ---- Blocked notification detected as blocker, not terminal ----
it("blocked notification sets status to blocked, not completed", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/tmp/blocked-notify",
});
const session = manager.getSession(sessionId)!;
manager.lastClient!.emitEvent({
type: "extension_ui_request",
id: "bn-1",
method: "notify",
message: "Auto-mode stopped: Blocked: waiting for approval",
});
assert.equal(session.status, "blocked");
assert.ok(session.pendingBlocker);
});
// ---- projectName is basename of resolved projectDir ----
it("projectName is basename of projectDir", async () => {
const { manager } = createManager();
const sessionId = await manager.startSession({
projectDir: "/home/user/projects/my-app",
});
const session = manager.getSession(sessionId)!;
assert.equal(session.projectName, "my-app");
});
// ---- Default command starts autonomous mode ----
it("sends autonomous command when no command is provided", async () => {
const { manager } = createManager();
await manager.startSession({ projectDir: "/tmp/default-autonomous" });
const client = manager.lastClient!;
assert.ok(client.prompted.includes("/sf autonomous"));
});
// ---- Custom command is sent instead of default ----
it("sends custom command when provided", async () => {
const { manager } = createManager();
await manager.startSession({
projectDir: "/tmp/custom-cmd",
command: "/sf quick fix-typo",
});
const client = manager.lastClient!;
assert.ok(client.prompted.includes("/sf quick fix-typo"));
assert.ok(!client.prompted.includes("/sf autonomous"));
});
// ---- getSessionByDir returns session by directory lookup ----
it("getSessionByDir returns session by directory", async () => {
const { manager } = createManager();
await manager.startSession({ projectDir: "/tmp/dir-lookup" });
const session = manager.getSessionByDir("/tmp/dir-lookup");
assert.ok(session);
assert.equal(session.projectName, "dir-lookup");
});
});