fix(coding-agent): handle missing session cwd
This commit is contained in:
@@ -5,6 +5,7 @@ import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agen
|
|||||||
import type { SessionStartEvent } from "./extensions/index.js";
|
import type { SessionStartEvent } from "./extensions/index.js";
|
||||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||||
import type { CreateAgentSessionResult } from "./sdk.js";
|
import type { CreateAgentSessionResult } from "./sdk.js";
|
||||||
|
import { assertSessionCwdExists } from "./session-cwd.js";
|
||||||
import { SessionManager } from "./session-manager.js";
|
import { SessionManager } from "./session-manager.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,14 +125,15 @@ export class AgentSessionRuntime {
|
|||||||
this._modelFallbackMessage = result.modelFallbackMessage;
|
this._modelFallbackMessage = result.modelFallbackMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {
|
async switchSession(sessionPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
|
||||||
const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);
|
const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);
|
||||||
if (beforeResult.cancelled) {
|
if (beforeResult.cancelled) {
|
||||||
return beforeResult;
|
return beforeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousSessionFile = this.session.sessionFile;
|
const previousSessionFile = this.session.sessionFile;
|
||||||
const sessionManager = SessionManager.open(sessionPath);
|
const sessionManager = SessionManager.open(sessionPath, undefined, cwdOverride);
|
||||||
|
assertSessionCwdExists(sessionManager, this.cwd);
|
||||||
await this.teardownCurrent();
|
await this.teardownCurrent();
|
||||||
this.apply(
|
this.apply(
|
||||||
await this.createRuntime({
|
await this.createRuntime({
|
||||||
@@ -246,7 +248,7 @@ export class AgentSessionRuntime {
|
|||||||
return { cancelled: false, selectedText };
|
return { cancelled: false, selectedText };
|
||||||
}
|
}
|
||||||
|
|
||||||
async importFromJsonl(inputPath: string): Promise<{ cancelled: boolean }> {
|
async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
|
||||||
const resolvedPath = resolve(inputPath);
|
const resolvedPath = resolve(inputPath);
|
||||||
if (!existsSync(resolvedPath)) {
|
if (!existsSync(resolvedPath)) {
|
||||||
throw new Error(`File not found: ${resolvedPath}`);
|
throw new Error(`File not found: ${resolvedPath}`);
|
||||||
@@ -268,7 +270,8 @@ export class AgentSessionRuntime {
|
|||||||
copyFileSync(resolvedPath, destinationPath);
|
copyFileSync(resolvedPath, destinationPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionManager = SessionManager.open(destinationPath, sessionDir);
|
const sessionManager = SessionManager.open(destinationPath, sessionDir, cwdOverride);
|
||||||
|
assertSessionCwdExists(sessionManager, this.cwd);
|
||||||
await this.teardownCurrent();
|
await this.teardownCurrent();
|
||||||
this.apply(
|
this.apply(
|
||||||
await this.createRuntime({
|
await this.createRuntime({
|
||||||
@@ -302,6 +305,7 @@ export async function createAgentSessionRuntime(
|
|||||||
sessionStartEvent?: SessionStartEvent;
|
sessionStartEvent?: SessionStartEvent;
|
||||||
},
|
},
|
||||||
): Promise<AgentSessionRuntime> {
|
): Promise<AgentSessionRuntime> {
|
||||||
|
assertSessionCwdExists(options.sessionManager, options.cwd);
|
||||||
const result = await createRuntime(options);
|
const result = await createRuntime(options);
|
||||||
if (process.cwd() !== result.services.cwd) {
|
if (process.cwd() !== result.services.cwd) {
|
||||||
process.chdir(result.services.cwd);
|
process.chdir(result.services.cwd);
|
||||||
|
|||||||
59
packages/coding-agent/src/core/session-cwd.ts
Normal file
59
packages/coding-agent/src/core/session-cwd.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
|
|
||||||
|
export interface SessionCwdIssue {
|
||||||
|
sessionFile?: string;
|
||||||
|
sessionCwd: string;
|
||||||
|
fallbackCwd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionCwdSource {
|
||||||
|
getCwd(): string;
|
||||||
|
getSessionFile(): string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMissingSessionCwdIssue(
|
||||||
|
sessionManager: SessionCwdSource,
|
||||||
|
fallbackCwd: string,
|
||||||
|
): SessionCwdIssue | undefined {
|
||||||
|
const sessionFile = sessionManager.getSessionFile();
|
||||||
|
if (!sessionFile) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionCwd = sessionManager.getCwd();
|
||||||
|
if (!sessionCwd || existsSync(sessionCwd)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionFile,
|
||||||
|
sessionCwd,
|
||||||
|
fallbackCwd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMissingSessionCwdError(issue: SessionCwdIssue): string {
|
||||||
|
const sessionFile = issue.sessionFile ? `\nSession file: ${issue.sessionFile}` : "";
|
||||||
|
return `Stored session working directory does not exist: ${issue.sessionCwd}${sessionFile}\nCurrent working directory: ${issue.fallbackCwd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMissingSessionCwdPrompt(issue: SessionCwdIssue): string {
|
||||||
|
return `cwd from session file does not exist\n${issue.sessionCwd}\n\ncontinue in current cwd\n${issue.fallbackCwd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MissingSessionCwdError extends Error {
|
||||||
|
readonly issue: SessionCwdIssue;
|
||||||
|
|
||||||
|
constructor(issue: SessionCwdIssue) {
|
||||||
|
super(formatMissingSessionCwdError(issue));
|
||||||
|
this.name = "MissingSessionCwdError";
|
||||||
|
this.issue = issue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertSessionCwdExists(sessionManager: SessionCwdSource, fallbackCwd: string): void {
|
||||||
|
const issue = getMissingSessionCwdIssue(sessionManager, fallbackCwd);
|
||||||
|
if (issue) {
|
||||||
|
throw new MissingSessionCwdError(issue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1270,12 +1270,13 @@ export class SessionManager {
|
|||||||
* Open a specific session file.
|
* Open a specific session file.
|
||||||
* @param path Path to session file
|
* @param path Path to session file
|
||||||
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
|
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
|
||||||
|
* @param cwdOverride Optional cwd override instead of the session header cwd.
|
||||||
*/
|
*/
|
||||||
static open(path: string, sessionDir?: string): SessionManager {
|
static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager {
|
||||||
// Extract cwd from session header if possible, otherwise use process.cwd()
|
// Extract cwd from session header if possible, otherwise use process.cwd()
|
||||||
const entries = loadEntriesFromFile(path);
|
const entries = loadEntriesFromFile(path);
|
||||||
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
||||||
const cwd = header?.cwd ?? process.cwd();
|
const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
|
||||||
// If no sessionDir provided, derive from file's parent directory
|
// If no sessionDir provided, derive from file's parent directory
|
||||||
const dir = sessionDir ?? resolve(path, "..");
|
const dir = sessionDir ?? resolve(path, "..");
|
||||||
return new SessionManager(cwd, dir, path, true);
|
return new SessionManager(cwd, dir, path, true);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
||||||
|
import { ProcessTerminal, setKeybindings, TUI } from "@mariozechner/pi-tui";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { createInterface } from "readline";
|
import { createInterface } from "readline";
|
||||||
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js";
|
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js";
|
||||||
@@ -23,16 +24,24 @@ import {
|
|||||||
} from "./core/agent-session-services.js";
|
} from "./core/agent-session-services.js";
|
||||||
import { AuthStorage } from "./core/auth-storage.js";
|
import { AuthStorage } from "./core/auth-storage.js";
|
||||||
import { exportFromFile } from "./core/export-html/index.js";
|
import { exportFromFile } from "./core/export-html/index.js";
|
||||||
|
import { KeybindingsManager } from "./core/keybindings.js";
|
||||||
import type { ModelRegistry } from "./core/model-registry.js";
|
import type { ModelRegistry } from "./core/model-registry.js";
|
||||||
import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
||||||
import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
|
import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
|
||||||
import type { CreateAgentSessionOptions } from "./core/sdk.js";
|
import type { CreateAgentSessionOptions } from "./core/sdk.js";
|
||||||
|
import {
|
||||||
|
formatMissingSessionCwdPrompt,
|
||||||
|
getMissingSessionCwdIssue,
|
||||||
|
MissingSessionCwdError,
|
||||||
|
type SessionCwdIssue,
|
||||||
|
} from "./core/session-cwd.js";
|
||||||
import { SessionManager } from "./core/session-manager.js";
|
import { SessionManager } from "./core/session-manager.js";
|
||||||
import { SettingsManager } from "./core/settings-manager.js";
|
import { SettingsManager } from "./core/settings-manager.js";
|
||||||
import { printTimings, resetTimings, time } from "./core/timings.js";
|
import { printTimings, resetTimings, time } from "./core/timings.js";
|
||||||
import { allTools } from "./core/tools/index.js";
|
import { allTools } from "./core/tools/index.js";
|
||||||
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
|
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
|
||||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||||
|
import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";
|
||||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
||||||
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
|
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
|
||||||
import { isLocalPath } from "./utils/paths.js";
|
import { isLocalPath } from "./utils/paths.js";
|
||||||
@@ -375,6 +384,40 @@ function resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | u
|
|||||||
return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));
|
return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptForMissingSessionCwd(
|
||||||
|
issue: SessionCwdIssue,
|
||||||
|
settingsManager: SettingsManager,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
initTheme(settingsManager.getTheme());
|
||||||
|
setKeybindings(KeybindingsManager.create());
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());
|
||||||
|
ui.setClearOnShrink(settingsManager.getClearOnShrink());
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
const finish = (result: string | undefined) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
ui.stop();
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selector = new ExtensionSelectorComponent(
|
||||||
|
formatMissingSessionCwdPrompt(issue),
|
||||||
|
["Continue", "Cancel"],
|
||||||
|
(option) => finish(option === "Continue" ? issue.fallbackCwd : undefined),
|
||||||
|
() => finish(undefined),
|
||||||
|
{ tui: ui },
|
||||||
|
);
|
||||||
|
ui.addChild(selector);
|
||||||
|
ui.setFocus(selector);
|
||||||
|
ui.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function main(args: string[]) {
|
export async function main(args: string[]) {
|
||||||
resetTimings();
|
resetTimings();
|
||||||
const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
|
const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
|
||||||
@@ -448,12 +491,21 @@ export async function main(args: string[]) {
|
|||||||
// settings, resources, provider registrations, and models must be resolved only after
|
// settings, resources, provider registrations, and models must be resolved only after
|
||||||
// the target session cwd is known. The startup-cwd settings manager is used only for
|
// the target session cwd is known. The startup-cwd settings manager is used only for
|
||||||
// sessionDir lookup during session selection.
|
// sessionDir lookup during session selection.
|
||||||
const sessionManager = await createSessionManager(
|
const sessionDir = parsed.sessionDir ?? startupSettingsManager.getSessionDir();
|
||||||
parsed,
|
let sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);
|
||||||
cwd,
|
const missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);
|
||||||
parsed.sessionDir ?? startupSettingsManager.getSessionDir(),
|
if (missingSessionCwdIssue) {
|
||||||
startupSettingsManager,
|
if (appMode === "interactive") {
|
||||||
);
|
const selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);
|
||||||
|
if (!selectedCwd) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
sessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);
|
||||||
|
} else {
|
||||||
|
console.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
time("createSessionManager");
|
time("createSessionManager");
|
||||||
|
|
||||||
const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
|
const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|||||||
import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
||||||
import { DefaultPackageManager } from "../../core/package-manager.js";
|
import { DefaultPackageManager } from "../../core/package-manager.js";
|
||||||
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
||||||
|
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
||||||
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
||||||
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
||||||
import type { SourceInfo } from "../../core/source-info.js";
|
import type { SourceInfo } from "../../core/source-info.js";
|
||||||
@@ -1719,6 +1720,14 @@ export class InteractiveMode {
|
|||||||
return result === "Yes";
|
return result === "Yes";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async promptForMissingSessionCwd(error: MissingSessionCwdError): Promise<string | undefined> {
|
||||||
|
const confirmed = await this.showExtensionConfirm(
|
||||||
|
"Session cwd not found",
|
||||||
|
formatMissingSessionCwdPrompt(error.issue),
|
||||||
|
);
|
||||||
|
return confirmed ? error.issue.fallbackCwd : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a text input for extensions.
|
* Show a text input for extensions.
|
||||||
*/
|
*/
|
||||||
@@ -3843,6 +3852,21 @@ export class InteractiveMode {
|
|||||||
this.renderCurrentSessionState();
|
this.renderCurrentSessionState();
|
||||||
this.showStatus("Resumed session");
|
this.showStatus("Resumed session");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof MissingSessionCwdError) {
|
||||||
|
const selectedCwd = await this.promptForMissingSessionCwd(error);
|
||||||
|
if (!selectedCwd) {
|
||||||
|
this.showStatus("Resume cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await this.runtimeHost.switchSession(sessionPath, selectedCwd);
|
||||||
|
if (result.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.handleRuntimeSessionChange();
|
||||||
|
this.renderCurrentSessionState();
|
||||||
|
this.showStatus("Resumed session in current cwd");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.handleFatalRuntimeError("Failed to resume session", error);
|
await this.handleFatalRuntimeError("Failed to resume session", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4109,6 +4133,22 @@ export class InteractiveMode {
|
|||||||
this.renderCurrentSessionState();
|
this.renderCurrentSessionState();
|
||||||
this.showStatus(`Session imported from: ${inputPath}`);
|
this.showStatus(`Session imported from: ${inputPath}`);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof MissingSessionCwdError) {
|
||||||
|
const selectedCwd = await this.promptForMissingSessionCwd(error);
|
||||||
|
if (!selectedCwd) {
|
||||||
|
this.showStatus("Import cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await this.runtimeHost.importFromJsonl(inputPath, selectedCwd);
|
||||||
|
if (result.cancelled) {
|
||||||
|
this.showStatus("Import cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.handleRuntimeSessionChange();
|
||||||
|
this.renderCurrentSessionState();
|
||||||
|
this.showStatus(`Session imported from: ${inputPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.handleFatalRuntimeError("Failed to import session", error);
|
await this.handleFatalRuntimeError("Failed to import session", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
packages/coding-agent/test/session-cwd.test.ts
Normal file
91
packages/coding-agent/test/session-cwd.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "../src/core/agent-session-runtime.js";
|
||||||
|
import { getMissingSessionCwdIssue, MissingSessionCwdError } from "../src/core/session-cwd.js";
|
||||||
|
import { SessionManager } from "../src/core/session-manager.js";
|
||||||
|
|
||||||
|
function createTempDir(name: string): string {
|
||||||
|
const dir = join(tmpdir(), `${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSessionFile(path: string, cwd: string): void {
|
||||||
|
writeFileSync(
|
||||||
|
path,
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
version: 3,
|
||||||
|
id: "session-id",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
cwd,
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("session cwd handling", () => {
|
||||||
|
const cleanupPaths: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const path of cleanupPaths.splice(0)) {
|
||||||
|
rmSync(path, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects missing session cwd from persisted sessions", () => {
|
||||||
|
const fallbackCwd = createTempDir("pi-session-cwd-fallback");
|
||||||
|
const missingCwd = join(fallbackCwd, "does-not-exist");
|
||||||
|
const sessionDir = createTempDir("pi-session-cwd-session-dir");
|
||||||
|
const sessionFile = join(sessionDir, "session.jsonl");
|
||||||
|
cleanupPaths.push(fallbackCwd, sessionDir);
|
||||||
|
writeSessionFile(sessionFile, missingCwd);
|
||||||
|
|
||||||
|
const sessionManager = SessionManager.open(sessionFile);
|
||||||
|
const issue = getMissingSessionCwdIssue(sessionManager, fallbackCwd);
|
||||||
|
expect(issue).toEqual({
|
||||||
|
sessionFile: sessionManager.getSessionFile(),
|
||||||
|
sessionCwd: missingCwd,
|
||||||
|
fallbackCwd,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports overriding the effective cwd when opening a session", () => {
|
||||||
|
const fallbackCwd = createTempDir("pi-session-cwd-override");
|
||||||
|
const missingCwd = join(fallbackCwd, "does-not-exist");
|
||||||
|
const sessionDir = createTempDir("pi-session-cwd-override-session-dir");
|
||||||
|
const sessionFile = join(sessionDir, "session.jsonl");
|
||||||
|
cleanupPaths.push(fallbackCwd, sessionDir);
|
||||||
|
writeSessionFile(sessionFile, missingCwd);
|
||||||
|
|
||||||
|
const sessionManager = SessionManager.open(sessionFile, undefined, fallbackCwd);
|
||||||
|
expect(sessionManager.getCwd()).toBe(fallbackCwd);
|
||||||
|
expect(getMissingSessionCwdIssue(sessionManager, fallbackCwd)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws a controlled error before runtime creation when the stored cwd is missing", async () => {
|
||||||
|
const fallbackCwd = createTempDir("pi-session-cwd-runtime");
|
||||||
|
const missingCwd = join(fallbackCwd, "does-not-exist");
|
||||||
|
const sessionDir = createTempDir("pi-session-cwd-runtime-session-dir");
|
||||||
|
const sessionFile = join(sessionDir, "session.jsonl");
|
||||||
|
cleanupPaths.push(fallbackCwd, sessionDir);
|
||||||
|
writeSessionFile(sessionFile, missingCwd);
|
||||||
|
|
||||||
|
const sessionManager = SessionManager.open(sessionFile);
|
||||||
|
let createRuntimeCalled = false;
|
||||||
|
const createRuntime: CreateAgentSessionRuntimeFactory = async () => {
|
||||||
|
createRuntimeCalled = true;
|
||||||
|
throw new Error("should not be called");
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
createAgentSessionRuntime(createRuntime, {
|
||||||
|
cwd: fallbackCwd,
|
||||||
|
agentDir: fallbackCwd,
|
||||||
|
sessionManager,
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(MissingSessionCwdError);
|
||||||
|
expect(createRuntimeCalled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user