refactor(coding-agent): replace AgentSessionRuntimeHost with closure-based AgentSessionRuntime
- Replace AgentSessionRuntimeHost and bootstrap abstractions with AgentSessionRuntime - Runtime creation is now closure-based via CreateAgentSessionRuntimeFactory - Factory closes over process-global fixed inputs, recreates cwd-bound services per effective cwd - Session config (model, thinking, tools, scoped models) re-resolved per target cwd - CLI resource paths resolved once at startup as absolute paths - Swap lifecycle: teardown old, create next, apply next (hard fail on creation error) - Unified diagnostics model (info/warning/error) for args, services, session resolution, resources - No logging or process exits inside creation/parsing logic - Removed session_directory support - Removed session_switch and session_fork extension events (use session_start with reason) - Moved package/config CLI to package-manager-cli.ts - Fixed theme init for --resume session picker - Fixed flaky reftable footer test (content-based polling) - Fixed silent drop of unknown single-dash CLI flags - Added error diagnostics for missing explicit CLI resource paths - Updated SDK docs, examples, plans, exports, tests, changelog fixes #2753
This commit is contained in:
@@ -14,9 +14,11 @@ import { getModel } from "@mariozechner/pi-ai";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { AgentSession } from "../src/core/agent-session.js";
|
||||
import {
|
||||
type AgentSessionRuntimeHost,
|
||||
type AgentSessionRuntime,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
AgentSessionRuntimeHost as RuntimeHost,
|
||||
createAgentSessionServices,
|
||||
} from "../src/core/agent-session-runtime.js";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
@@ -25,7 +27,7 @@ import { API_KEY } from "./utilities.js";
|
||||
|
||||
describe.skipIf(!API_KEY)("AgentSession forking", () => {
|
||||
let session: AgentSession;
|
||||
let runtimeHost: AgentSessionRuntimeHost;
|
||||
let runtimeHost: AgentSessionRuntime;
|
||||
let tempDir: string;
|
||||
let sessionManager: SessionManager;
|
||||
|
||||
@@ -50,23 +52,38 @@ describe.skipIf(!API_KEY)("AgentSession forking", () => {
|
||||
const authStorage = AuthStorage.create(join(tempDir, "auth.json"));
|
||||
authStorage.setRuntimeApiKey("anthropic", API_KEY!);
|
||||
|
||||
const bootstrap = {
|
||||
const servicesOptions = {
|
||||
agentDir: tempDir,
|
||||
authStorage,
|
||||
model,
|
||||
tools: codingTools,
|
||||
resourceLoader: {
|
||||
resourceLoaderOptions: {
|
||||
noExtensions: true,
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({
|
||||
...servicesOptions,
|
||||
cwd,
|
||||
});
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
model,
|
||||
tools: codingTools,
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
runtimeHost = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: tempDir,
|
||||
agentDir: tempDir,
|
||||
sessionManager,
|
||||
});
|
||||
runtimeHost = new RuntimeHost(bootstrap, runtime);
|
||||
session = runtimeHost.session;
|
||||
session.subscribe(() => {});
|
||||
return session;
|
||||
|
||||
@@ -4,9 +4,10 @@ import { join } from "node:path";
|
||||
import { fauxAssistantMessage, registerFauxProvider } from "@mariozechner/pi-ai";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
type AgentSessionRuntimeBootstrap,
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
} from "../src/core/agent-session-runtime.js";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
@@ -19,7 +20,7 @@ import type {
|
||||
|
||||
type RecordedSessionEvent = SessionBeforeSwitchEvent | SessionBeforeForkEvent | SessionStartEvent;
|
||||
|
||||
describe("AgentSessionRuntimeHost session lifecycle events", () => {
|
||||
describe("AgentSessionRuntime session lifecycle events", () => {
|
||||
const cleanups: Array<() => Promise<void> | void> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -38,22 +39,38 @@ describe("AgentSessionRuntimeHost session lifecycle events", () => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
|
||||
|
||||
const bootstrap: AgentSessionRuntimeBootstrap = {
|
||||
const runtimeOptions = {
|
||||
agentDir: tempDir,
|
||||
authStorage,
|
||||
model: faux.getModel(),
|
||||
resourceLoader: {
|
||||
resourceLoaderOptions: {
|
||||
extensionFactories: [extensionFactory],
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({
|
||||
...runtimeOptions,
|
||||
cwd,
|
||||
});
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
model: faux.getModel(),
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const runtimeHost = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: tempDir,
|
||||
agentDir: tempDir,
|
||||
sessionManager: SessionManager.create(tempDir),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
await runtimeHost.session.bindExtensions({});
|
||||
|
||||
cleanups.push(async () => {
|
||||
|
||||
@@ -270,9 +270,20 @@ describe("parseArgs", () => {
|
||||
expect(result.messages).toEqual(["explain this"]);
|
||||
});
|
||||
|
||||
test("ignores unknown flags starting with -", () => {
|
||||
test("captures unknown long flags with string values", () => {
|
||||
const result = parseArgs(["--unknown-flag", "message"]);
|
||||
expect(result.messages).toEqual(["message"]);
|
||||
expect(result.messages).toEqual([]);
|
||||
expect(result.unknownFlags.get("unknown-flag")).toBe("message");
|
||||
});
|
||||
|
||||
test("captures unknown boolean long flags", () => {
|
||||
const result = parseArgs(["--unknown-flag"]);
|
||||
expect(result.unknownFlags.get("unknown-flag")).toBe(true);
|
||||
});
|
||||
|
||||
test("captures unknown long flags with equals syntax", () => {
|
||||
const result = parseArgs(["--unknown-flag=value"]);
|
||||
expect(result.unknownFlags.get("unknown-flag")).toBe("value");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ function createArgs(messages: string[] = []): Args {
|
||||
messages: [...messages],
|
||||
fileArgs: [],
|
||||
unknownFlags: new Map(),
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { KeybindingsManager, migrateKeybindingsConfigFile } from "../src/core/keybindings.js";
|
||||
import { ENV_AGENT_DIR } from "../src/config.js";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.js";
|
||||
import { runMigrations } from "../src/migrations.js";
|
||||
|
||||
describe("keybindings migration", () => {
|
||||
const tempDirs: string[] = [];
|
||||
@@ -25,8 +27,14 @@ describe("keybindings migration", () => {
|
||||
cursorUp: ["up", "ctrl+p"],
|
||||
expandTools: "ctrl+x",
|
||||
});
|
||||
|
||||
expect(migrateKeybindingsConfigFile(agentDir)).toBe(true);
|
||||
const previousAgentDir = process.env[ENV_AGENT_DIR];
|
||||
process.env[ENV_AGENT_DIR] = agentDir;
|
||||
runMigrations(agentDir);
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env[ENV_AGENT_DIR];
|
||||
} else {
|
||||
process.env[ENV_AGENT_DIR] = previousAgentDir;
|
||||
}
|
||||
|
||||
const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "keybindings.json"), "utf-8")) as Record<
|
||||
string,
|
||||
@@ -43,8 +51,14 @@ describe("keybindings migration", () => {
|
||||
expandTools: "ctrl+x",
|
||||
"app.tools.expand": "ctrl+y",
|
||||
});
|
||||
|
||||
expect(migrateKeybindingsConfigFile(agentDir)).toBe(true);
|
||||
const previousAgentDir = process.env[ENV_AGENT_DIR];
|
||||
process.env[ENV_AGENT_DIR] = agentDir;
|
||||
runMigrations(agentDir);
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env[ENV_AGENT_DIR];
|
||||
} else {
|
||||
process.env[ENV_AGENT_DIR] = previousAgentDir;
|
||||
}
|
||||
|
||||
const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "keybindings.json"), "utf-8")) as Record<
|
||||
string,
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ENV_AGENT_DIR } from "../src/config.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
state: {
|
||||
hookSessionDir: undefined as string | undefined,
|
||||
capturedSessionDir: undefined as string | undefined,
|
||||
},
|
||||
createAgentSession: vi.fn(async (options: { sessionManager?: { getSessionDir(): string } }) => {
|
||||
mocks.state.capturedSessionDir = options.sessionManager?.getSessionDir();
|
||||
return {
|
||||
session: {
|
||||
model: { id: "test-model", provider: "test", reasoning: false },
|
||||
thinkingLevel: "off",
|
||||
setThinkingLevel: vi.fn(),
|
||||
},
|
||||
modelFallbackMessage: undefined,
|
||||
};
|
||||
}),
|
||||
runPrintMode: vi.fn(async () => 0),
|
||||
selectSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../src/core/sdk.js", async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
createAgentSession: mocks.createAgentSession,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../src/modes/index.js", async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
runPrintMode: mocks.runPrintMode,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../src/cli/session-picker.js", async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
selectSession: mocks.selectSession,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../src/core/resource-loader.js", async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
DefaultResourceLoader: class {
|
||||
async reload(): Promise<void> {}
|
||||
|
||||
getExtensions() {
|
||||
const handlers = new Map();
|
||||
if (mocks.state.hookSessionDir) {
|
||||
handlers.set("session_directory", [async () => ({ sessionDir: mocks.state.hookSessionDir })]);
|
||||
}
|
||||
return {
|
||||
extensions: [{ path: "/mock-extension.ts", handlers, flags: new Map() }],
|
||||
errors: [],
|
||||
runtime: {
|
||||
pendingProviderRegistrations: [],
|
||||
flagValues: new Map(),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("sessionDir precedence", () => {
|
||||
let tempDir: string;
|
||||
let agentDir: string;
|
||||
let projectDir: string;
|
||||
let originalCwd: string;
|
||||
let originalAgentDir: string | undefined;
|
||||
let originalExitCode: typeof process.exitCode;
|
||||
let originalIsTTY: boolean | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mocks.state.hookSessionDir = "./hook-sessions";
|
||||
mocks.state.capturedSessionDir = undefined;
|
||||
mocks.createAgentSession.mockClear();
|
||||
mocks.runPrintMode.mockClear();
|
||||
mocks.selectSession.mockReset();
|
||||
|
||||
tempDir = join(tmpdir(), `pi-session-dir-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
agentDir = join(tempDir, "agent");
|
||||
projectDir = join(tempDir, "project");
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
mkdirSync(join(projectDir, ".pi"), { recursive: true });
|
||||
|
||||
originalCwd = process.cwd();
|
||||
originalAgentDir = process.env[ENV_AGENT_DIR];
|
||||
originalExitCode = process.exitCode;
|
||||
originalIsTTY = process.stdin.isTTY;
|
||||
process.exitCode = undefined;
|
||||
process.env[ENV_AGENT_DIR] = agentDir;
|
||||
process.chdir(projectDir);
|
||||
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
process.exitCode = originalExitCode;
|
||||
Object.defineProperty(process.stdin, "isTTY", { value: originalIsTTY, configurable: true });
|
||||
if (originalAgentDir === undefined) {
|
||||
delete process.env[ENV_AGENT_DIR];
|
||||
} else {
|
||||
process.env[ENV_AGENT_DIR] = originalAgentDir;
|
||||
}
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("prefers settings sessionDir over the session_directory hook for new sessions", async () => {
|
||||
writeFileSync(join(projectDir, ".pi", "settings.json"), JSON.stringify({ sessionDir: "./settings-sessions" }));
|
||||
|
||||
const { main } = await import("../src/main.js");
|
||||
await main(["--print", "test prompt"]);
|
||||
|
||||
expect(mocks.state.capturedSessionDir).toBe("./settings-sessions");
|
||||
expect(mocks.runPrintMode).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("prefers CLI --session-dir over settings and the session_directory hook", async () => {
|
||||
writeFileSync(join(projectDir, ".pi", "settings.json"), JSON.stringify({ sessionDir: "./settings-sessions" }));
|
||||
|
||||
const { main } = await import("../src/main.js");
|
||||
await main(["--print", "--session-dir", "./cli-sessions", "test prompt"]);
|
||||
|
||||
expect(mocks.state.capturedSessionDir).toBe("./cli-sessions");
|
||||
expect(mocks.runPrintMode).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("uses settings sessionDir ahead of the session_directory hook for --resume", async () => {
|
||||
writeFileSync(join(projectDir, ".pi", "settings.json"), JSON.stringify({ sessionDir: "./settings-sessions" }));
|
||||
|
||||
const { SessionManager } = await import("../src/core/session-manager.js");
|
||||
const listSpy = vi.spyOn(SessionManager, "list");
|
||||
mocks.selectSession.mockImplementation(async (listCurrent: (onProgress: () => void) => Promise<unknown>) => {
|
||||
await listCurrent(() => {});
|
||||
return join(projectDir, "picked-session.jsonl");
|
||||
});
|
||||
|
||||
const { main } = await import("../src/main.js");
|
||||
await main(["--print", "--resume"]);
|
||||
|
||||
expect(listSpy).toHaveBeenCalledWith(expect.any(String), "./settings-sessions", expect.any(Function));
|
||||
expect(mocks.state.capturedSessionDir).toBe("./settings-sessions");
|
||||
expect(mocks.runPrintMode).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -155,7 +155,7 @@ describe("SettingsManager", () => {
|
||||
});
|
||||
|
||||
describe("reload", () => {
|
||||
it("should reload global settings from disk", () => {
|
||||
it("should reload global settings from disk", async () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
@@ -176,21 +176,21 @@ describe("SettingsManager", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
manager.reload();
|
||||
await manager.reload();
|
||||
|
||||
expect(manager.getTheme()).toBe("light");
|
||||
expect(manager.getExtensionPaths()).toEqual(["/after.ts"]);
|
||||
expect(manager.getDefaultModel()).toBe("claude-sonnet");
|
||||
});
|
||||
|
||||
it("should keep previous settings when file is invalid", () => {
|
||||
it("should keep previous settings when file is invalid", async () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ theme: "dark" }));
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
writeFileSync(settingsPath, "{ invalid json");
|
||||
manager.reload();
|
||||
await manager.reload();
|
||||
|
||||
expect(manager.getTheme()).toBe("dark");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Coding agent suite tests
|
||||
|
||||
Use `test/suite/` for the new harness-based test suite around `AgentSession` and `AgentSessionRuntimeHost`.
|
||||
Use `test/suite/` for the new harness-based test suite around `AgentSession` and `AgentSessionRuntime`.
|
||||
|
||||
Rules:
|
||||
- Use `test/suite/harness.ts`
|
||||
|
||||
@@ -4,13 +4,15 @@ import { join } from "node:path";
|
||||
import { fauxAssistantMessage, registerFauxProvider } from "@mariozechner/pi-ai";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
type AgentSessionRuntimeBootstrap,
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
} from "../../src/core/agent-session-runtime.js";
|
||||
import { AuthStorage } from "../../src/core/auth-storage.js";
|
||||
import { SessionManager } from "../../src/core/session-manager.js";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionFactory,
|
||||
SessionBeforeForkEvent,
|
||||
SessionBeforeSwitchEvent,
|
||||
@@ -19,7 +21,7 @@ import type {
|
||||
|
||||
type RecordedSessionEvent = SessionBeforeSwitchEvent | SessionBeforeForkEvent | SessionStartEvent;
|
||||
|
||||
describe("AgentSessionRuntimeHost characterization", () => {
|
||||
describe("AgentSessionRuntime characterization", () => {
|
||||
const cleanups: Array<() => Promise<void> | void> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -48,14 +50,14 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
|
||||
|
||||
const bootstrap: AgentSessionRuntimeBootstrap = {
|
||||
const runtimeOptions = {
|
||||
agentDir: tempDir,
|
||||
authStorage,
|
||||
model: options?.bootstrapModel === false ? undefined : faux.getModel(),
|
||||
thinkingLevel: options?.bootstrapThinkingLevel === false ? undefined : undefined,
|
||||
resourceLoader: {
|
||||
resourceLoaderOptions: {
|
||||
extensionFactories: [
|
||||
(pi) => {
|
||||
(pi: ExtensionAPI) => {
|
||||
pi.registerProvider(faux.getModel().provider, {
|
||||
baseUrl: faux.getModel().baseUrl,
|
||||
apiKey: "faux-key",
|
||||
@@ -79,11 +81,28 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
noThemes: true,
|
||||
},
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({
|
||||
...runtimeOptions,
|
||||
cwd,
|
||||
});
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
model: runtimeOptions.model,
|
||||
thinkingLevel: runtimeOptions.thinkingLevel,
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const runtimeHost = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: tempDir,
|
||||
agentDir: tempDir,
|
||||
sessionManager: SessionManager.create(tempDir),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
await runtimeHost.session.bindExtensions({});
|
||||
|
||||
cleanups.push(async () => {
|
||||
@@ -99,7 +118,7 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
|
||||
it("emits session_before_switch and session_start for new and resume flows", async () => {
|
||||
const events: RecordedSessionEvent[] = [];
|
||||
const { runtimeHost } = await createRuntimeHost((pi) => {
|
||||
const { runtimeHost } = await createRuntimeHost((pi: ExtensionAPI) => {
|
||||
pi.on("session_before_switch", (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
@@ -140,7 +159,7 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
it("honors session_before_switch cancellation for new and resume", async () => {
|
||||
const events: RecordedSessionEvent[] = [];
|
||||
let cancelReason: "new" | "resume" | undefined;
|
||||
const { runtimeHost } = await createRuntimeHost((pi) => {
|
||||
const { runtimeHost } = await createRuntimeHost((pi: ExtensionAPI) => {
|
||||
pi.on("session_before_switch", (event) => {
|
||||
events.push(event);
|
||||
if (event.reason === cancelReason) {
|
||||
@@ -175,7 +194,7 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
it("emits session_before_fork and session_start and honors cancellation", async () => {
|
||||
const events: RecordedSessionEvent[] = [];
|
||||
let cancelNextFork = false;
|
||||
const { runtimeHost } = await createRuntimeHost((pi) => {
|
||||
const { runtimeHost } = await createRuntimeHost((pi: ExtensionAPI) => {
|
||||
pi.on("session_before_fork", (event) => {
|
||||
events.push(event);
|
||||
if (cancelNextFork) {
|
||||
@@ -222,39 +241,60 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
const { runtimeHost, faux, tempDir } = await createRuntimeHost(() => {}, { cwd: firstDir });
|
||||
const otherAuthStorage = AuthStorage.inMemory();
|
||||
otherAuthStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
|
||||
const otherRuntime = await createAgentSessionRuntime(
|
||||
{
|
||||
agentDir: tempDir,
|
||||
authStorage: otherAuthStorage,
|
||||
resourceLoader: {
|
||||
extensionFactories: [
|
||||
(pi) => {
|
||||
pi.registerProvider(faux.getModel().provider, {
|
||||
baseUrl: faux.getModel().baseUrl,
|
||||
apiKey: "faux-key",
|
||||
api: faux.api,
|
||||
models: faux.models.map((registeredModel) => ({
|
||||
id: registeredModel.id,
|
||||
name: registeredModel.name,
|
||||
api: registeredModel.api,
|
||||
reasoning: registeredModel.reasoning,
|
||||
input: registeredModel.input,
|
||||
cost: registeredModel.cost,
|
||||
contextWindow: registeredModel.contextWindow,
|
||||
maxTokens: registeredModel.maxTokens,
|
||||
})),
|
||||
});
|
||||
},
|
||||
],
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
const otherRuntimeOptions = {
|
||||
agentDir: tempDir,
|
||||
authStorage: otherAuthStorage,
|
||||
resourceLoaderOptions: {
|
||||
extensionFactories: [
|
||||
(pi: ExtensionAPI) => {
|
||||
pi.registerProvider(faux.getModel().provider, {
|
||||
baseUrl: faux.getModel().baseUrl,
|
||||
apiKey: "faux-key",
|
||||
api: faux.api,
|
||||
models: faux.models.map((registeredModel) => ({
|
||||
id: registeredModel.id,
|
||||
name: registeredModel.name,
|
||||
api: registeredModel.api,
|
||||
reasoning: registeredModel.reasoning,
|
||||
input: registeredModel.input,
|
||||
cost: registeredModel.cost,
|
||||
contextWindow: registeredModel.contextWindow,
|
||||
maxTokens: registeredModel.maxTokens,
|
||||
})),
|
||||
});
|
||||
},
|
||||
],
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
{ cwd: secondDir, sessionManager: SessionManager.create(secondDir) },
|
||||
);
|
||||
};
|
||||
const createOtherRuntime: CreateAgentSessionRuntimeFactory = async ({
|
||||
cwd,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
}) => {
|
||||
const services = await createAgentSessionServices({
|
||||
...otherRuntimeOptions,
|
||||
cwd,
|
||||
});
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const otherRuntime = await createAgentSessionRuntime(createOtherRuntime, {
|
||||
cwd: secondDir,
|
||||
agentDir: tempDir,
|
||||
sessionManager: SessionManager.create(secondDir),
|
||||
});
|
||||
cleanups.push(async () => {
|
||||
otherRuntime.session.dispose();
|
||||
await otherRuntime.dispose();
|
||||
});
|
||||
await otherRuntime.session.prompt("other");
|
||||
const otherSessionFile = otherRuntime.session.sessionFile!;
|
||||
@@ -274,39 +314,60 @@ describe("AgentSessionRuntimeHost characterization", () => {
|
||||
mkdirSync(otherDir, { recursive: true });
|
||||
const otherAuthStorage = AuthStorage.inMemory();
|
||||
otherAuthStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
|
||||
const otherRuntime = await createAgentSessionRuntime(
|
||||
{
|
||||
agentDir: tempDir,
|
||||
authStorage: otherAuthStorage,
|
||||
resourceLoader: {
|
||||
extensionFactories: [
|
||||
(pi) => {
|
||||
pi.registerProvider(faux.getModel().provider, {
|
||||
baseUrl: faux.getModel().baseUrl,
|
||||
apiKey: "faux-key",
|
||||
api: faux.api,
|
||||
models: faux.models.map((registeredModel) => ({
|
||||
id: registeredModel.id,
|
||||
name: registeredModel.name,
|
||||
api: registeredModel.api,
|
||||
reasoning: registeredModel.reasoning,
|
||||
input: registeredModel.input,
|
||||
cost: registeredModel.cost,
|
||||
contextWindow: registeredModel.contextWindow,
|
||||
maxTokens: registeredModel.maxTokens,
|
||||
})),
|
||||
});
|
||||
},
|
||||
],
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
const otherRuntimeOptions = {
|
||||
agentDir: tempDir,
|
||||
authStorage: otherAuthStorage,
|
||||
resourceLoaderOptions: {
|
||||
extensionFactories: [
|
||||
(pi: ExtensionAPI) => {
|
||||
pi.registerProvider(faux.getModel().provider, {
|
||||
baseUrl: faux.getModel().baseUrl,
|
||||
apiKey: "faux-key",
|
||||
api: faux.api,
|
||||
models: faux.models.map((registeredModel) => ({
|
||||
id: registeredModel.id,
|
||||
name: registeredModel.name,
|
||||
api: registeredModel.api,
|
||||
reasoning: registeredModel.reasoning,
|
||||
input: registeredModel.input,
|
||||
cost: registeredModel.cost,
|
||||
contextWindow: registeredModel.contextWindow,
|
||||
maxTokens: registeredModel.maxTokens,
|
||||
})),
|
||||
});
|
||||
},
|
||||
],
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
{ cwd: otherDir, sessionManager: SessionManager.create(otherDir) },
|
||||
);
|
||||
};
|
||||
const createOtherRuntime: CreateAgentSessionRuntimeFactory = async ({
|
||||
cwd,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
}) => {
|
||||
const services = await createAgentSessionServices({
|
||||
...otherRuntimeOptions,
|
||||
cwd,
|
||||
});
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const otherRuntime = await createAgentSessionRuntime(createOtherRuntime, {
|
||||
cwd: otherDir,
|
||||
agentDir: tempDir,
|
||||
sessionManager: SessionManager.create(otherDir),
|
||||
});
|
||||
cleanups.push(async () => {
|
||||
otherRuntime.session.dispose();
|
||||
await otherRuntime.dispose();
|
||||
});
|
||||
await otherRuntime.session.setModel(faux.getModel("faux-2")!);
|
||||
otherRuntime.session.setThinkingLevel("off");
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { registerFauxProvider } from "@mariozechner/pi-ai";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
} from "../../../src/core/agent-session-runtime.js";
|
||||
import { AuthStorage } from "../../../src/core/auth-storage.js";
|
||||
import { SessionManager } from "../../../src/core/session-manager.js";
|
||||
|
||||
describe("issue #2753 reload stale resource settings", () => {
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
cleanups.pop()?.();
|
||||
}
|
||||
});
|
||||
|
||||
it("applies updated top-level prompt settings on reload after startup", async () => {
|
||||
const tempDir = join(tmpdir(), `pi-2753-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
const agentDir = join(tempDir, "agent");
|
||||
const promptsDir = join(agentDir, "prompts");
|
||||
mkdirSync(promptsDir, { recursive: true });
|
||||
writeFileSync(join(promptsDir, "test.md"), "Echo test prompt\n");
|
||||
|
||||
const faux = registerFauxProvider({
|
||||
models: [{ id: "faux-1", reasoning: false }],
|
||||
});
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
|
||||
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({
|
||||
cwd,
|
||||
agentDir,
|
||||
authStorage,
|
||||
resourceLoaderOptions: {
|
||||
extensionFactories: [
|
||||
(pi) => {
|
||||
pi.registerProvider(faux.getModel().provider, {
|
||||
baseUrl: faux.getModel().baseUrl,
|
||||
apiKey: "faux-key",
|
||||
api: faux.api,
|
||||
models: faux.models.map((registeredModel) => ({
|
||||
id: registeredModel.id,
|
||||
name: registeredModel.name,
|
||||
api: registeredModel.api,
|
||||
reasoning: registeredModel.reasoning,
|
||||
input: registeredModel.input,
|
||||
cost: registeredModel.cost,
|
||||
contextWindow: registeredModel.contextWindow,
|
||||
maxTokens: registeredModel.maxTokens,
|
||||
})),
|
||||
});
|
||||
},
|
||||
],
|
||||
noSkills: true,
|
||||
noThemes: true,
|
||||
},
|
||||
});
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
model: faux.getModel(),
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: tempDir,
|
||||
agentDir,
|
||||
sessionManager: SessionManager.create(tempDir),
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
runtime.session.dispose();
|
||||
faux.unregister();
|
||||
if (existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
expect(runtime.session.promptTemplates.map((prompt) => prompt.name)).toContain("test");
|
||||
|
||||
writeFileSync(join(agentDir, "settings.json"), `${JSON.stringify({ prompts: ["-prompts/test.md"] }, null, 2)}\n`);
|
||||
|
||||
await runtime.session.reload();
|
||||
|
||||
expect(runtime.services.settingsManager.getGlobalSettings().prompts).toEqual(["-prompts/test.md"]);
|
||||
expect(runtime.session.promptTemplates.map((prompt) => prompt.name)).not.toContain("test");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user