diff --git a/packages/coding-agent/src/tools/read-format.ts b/packages/coding-agent/src/tools/read-format.ts index cc3fe52b9..0f5167610 100644 --- a/packages/coding-agent/src/tools/read-format.ts +++ b/packages/coding-agent/src/tools/read-format.ts @@ -492,7 +492,7 @@ export function buildInMemoryMultiRangeResult( ): AgentToolResult { const displayMode = resolveFileDisplayMode(session, { raw: options.raw, immutable: options.immutable }); const details = options.details ?? {}; - const allLines = text.split("\n"); + const allLines = options.raw === true ? text.split("\n") : splitAddressableFileLines(text); const totalLines = allLines.length; details.totalLines = totalLines; const shouldAddHashLines = displayMode.hashLines; diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index aa272e938..94ada99f5 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -2,7 +2,7 @@ * Tests for AgentSession concurrent prompt guard. */ -import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -43,11 +43,27 @@ const originalSchedulerWait = scheduler.wait.bind(scheduler); function collapseSchedulerSettleDelays(): void { vi.spyOn(scheduler, "wait").mockImplementation((_delayMs, options) => originalSchedulerWait(0, options)); } +let sharedDir: string; +let sharedAuthStorage: AuthStorage; +let sharedModelRegistry: ModelRegistry; + +beforeAll(async () => { + sharedDir = path.join(os.tmpdir(), `pi-concurrent-shared-${Snowflake.next()}`); + fs.mkdirSync(sharedDir, { recursive: true }); + sharedAuthStorage = await AuthStorage.create(path.join(sharedDir, "auth.db")); + sharedAuthStorage.setRuntimeApiKey("anthropic", "test-key"); + sharedAuthStorage.setRuntimeApiKey("openai-codex", "test-key"); + sharedModelRegistry = new ModelRegistry(sharedAuthStorage, path.join(sharedDir, "models.yml")); +}); + +afterAll(() => { + sharedAuthStorage.close(); + removeSyncWithRetries(sharedDir); +}); describe("AgentSession concurrent prompt guard", () => { let session: AgentSession; let tempDir: string; - const authStorages: AuthStorage[] = []; beforeEach(() => { // Collapse scheduler settle delays so the post-abort auto-continue and @@ -61,9 +77,6 @@ describe("AgentSession concurrent prompt guard", () => { if (session) { await session.dispose(); } - for (const authStorage of authStorages.splice(0)) { - authStorage.close(); - } if (tempDir && fs.existsSync(tempDir)) { removeSyncWithRetries(tempDir); } @@ -104,11 +117,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(settingsOverrides); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -289,11 +298,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -372,11 +377,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); await session.prompt("First message"); @@ -443,11 +444,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); await session.prompt("First message"); @@ -487,11 +484,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); vi.spyOn(session.goalRuntime, "onAgentEnd").mockImplementation(() => { settleReached.resolve(); @@ -543,10 +536,7 @@ describe("AgentSession concurrent prompt guard", () => { ); const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); + const modelRegistry = sharedModelRegistry; const extensionRunner = new ExtensionRunner( [extension], extensionRuntime, @@ -614,11 +604,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); await session.prompt("First message"); @@ -647,11 +633,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); await session.prompt("First message"); @@ -680,11 +662,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); await session.prompt("First message"); @@ -720,11 +698,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); session.setClientBridge({ capabilities: {}, @@ -765,11 +739,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -803,11 +773,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -839,11 +805,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-idle-followup.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models-idle-followup.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -878,11 +840,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry }); const observedIsStreamingAtAgentEnd: boolean[] = []; @@ -926,10 +884,7 @@ describe("AgentSession concurrent prompt guard", () => { } as unknown as ExtensionRunner; const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, extensionRunner }); const { promise: publicAgentEnd, resolve: onPublicAgentEnd } = Promise.withResolvers(); @@ -961,11 +916,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-acp-idle.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models-acp-idle.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -1027,11 +978,7 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-acp-async.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models-acp-async.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; const ownerId = "acp-session-a"; const deliveryGate = Promise.withResolvers(); let deliveryStarted = false; @@ -1106,10 +1053,7 @@ describe("AgentSession concurrent prompt guard", () => { it("scopes ACP async job snapshots and drains to the owning session id", async () => { const model = getBundledModel("anthropic", "claude-sonnet-4-5")!; - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-acp-scope.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models-acp-scope.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); + const modelRegistry = sharedModelRegistry; const settings = Settings.isolated(); const deliveryGate = Promise.withResolvers(); const delivered: string[] = []; @@ -1179,7 +1123,6 @@ describe("AgentSession concurrent prompt guard", () => { describe("AgentSession TTSR resume gate", () => { let session: AgentSession; let tempDir: string; - const authStorages: AuthStorage[] = []; beforeEach(() => { tempDir = path.join(os.tmpdir(), `pi-ttsr-gate-test-${Snowflake.next()}`); @@ -1190,9 +1133,6 @@ describe("AgentSession TTSR resume gate", () => { if (session) { await session.dispose(); } - for (const authStorage of authStorages.splice(0)) { - authStorage.close(); - } if (tempDir && fs.existsSync(tempDir)) { removeSyncWithRetries(tempDir); } @@ -1314,11 +1254,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-int.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -1379,10 +1315,7 @@ describe("AgentSession TTSR resume gate", () => { "todo.enabled": false, "todo.reminders": false, }); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-will-continue.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); + const modelRegistry = sharedModelRegistry; const extensionRuntime = new ExtensionRuntime(); const extension = await loadExtensionFromFactory( pi => { @@ -1580,10 +1513,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-abort-reason.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, ttsrManager }); await session.prompt("Write some Rust code"); @@ -1698,10 +1628,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-abort-reason.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, ttsrManager }); await session.prompt("Write some Rust code"); @@ -1768,11 +1695,7 @@ describe("AgentSession TTSR resume gate", () => { }); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-rel.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, settings, modelRegistry, ttsrManager }); await session.prompt("Write some Rust code"); @@ -1844,11 +1767,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-def.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -1915,11 +1834,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-abt.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -2027,11 +1942,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-tool.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -2136,11 +2047,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-never-tool.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -2272,11 +2179,7 @@ describe("AgentSession TTSR resume gate", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-dup.db")); - authStorages.push(authStorage); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - + const modelRegistry = sharedModelRegistry; session = new AgentSession({ agent, sessionManager, @@ -2302,9 +2205,7 @@ describe("AgentSession TTSR resume gate", () => { it("prompt() waits for context-promotion continuation to finish", async () => { collapseSchedulerSettleDelays(); - const authStorage = await AuthStorage.create(path.join(tempDir, "testauth-promo.db")); - authStorages.push(authStorage); - authStorage.setRuntimeApiKey("openai-codex", "test-key"); + const authStorage = sharedAuthStorage; // The bundled catalog has no codex model whose promotion target carries a // strictly larger window (gpt-5.5's bundled target gpt-5.4 is same-window), // so pin gpt-5.5 (272k) -> gpt-5.6-sol (372k) via modelOverrides. diff --git a/packages/coding-agent/test/agent-session-eager-compaction.test.ts b/packages/coding-agent/test/agent-session-eager-compaction.test.ts index a7ad2b7e4..9bcd1ed83 100644 --- a/packages/coding-agent/test/agent-session-eager-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-eager-compaction.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; import { type } from "@oh-my-pi/omptype"; import { Agent, type AgentMessage, type AgentTool } from "@oh-my-pi/pi-agent-core"; @@ -122,8 +122,24 @@ function emitHighUsageTurn(session: AgentSession): void { describe("AgentSession eager prelude re-injection after compaction", () => { let tempDir: TempDir; + let sharedDir: TempDir; + let sharedAuthStorage: AuthStorage; + let sharedModelRegistry: ModelRegistry; const cleanups: Array<() => Promise> = []; + beforeAll(async () => { + sharedDir = TempDir.createSync("@pi-agent-session-eager-compaction-shared-"); + sharedAuthStorage = await AuthStorage.create(path.join(sharedDir.path(), "auth.db")); + sharedAuthStorage.setRuntimeApiKey("anthropic", "test-key"); + sharedAuthStorage.setRuntimeApiKey("openai-codex", "test-key"); + sharedModelRegistry = new ModelRegistry(sharedAuthStorage, path.join(sharedDir.path(), "models.yml")); + }); + + afterAll(() => { + sharedAuthStorage.close(); + sharedDir.removeSync(); + }); + beforeEach(() => { tempDir = TempDir.createSync("@pi-agent-session-eager-compaction-"); cleanups.length = 0; @@ -153,9 +169,7 @@ describe("AgentSession eager prelude re-injection after compaction", () => { // not shift the headroom math. const model = { ...selectedModel, contextWindow: 200_000, maxTokens: 64_000 }; - const authStorage = await AuthStorage.create(path.join(tempDir.path(), `testauth-${cleanups.length}.db`)); - authStorage.setRuntimeApiKey(model.provider, "test-key"); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), `models-${cleanups.length}.yml`)); + const modelRegistry = sharedModelRegistry; const settings = Settings.isolated({ "compaction.enabled": true, "compaction.autoContinue": true, @@ -249,10 +263,7 @@ describe("AgentSession eager prelude re-injection after compaction", () => { return promise; }; - cleanups.push(async () => { - await session.dispose(); - authStorage.close(); - }); + cleanups.push(() => session.dispose()); return { session, observedCalls, sessionManager, waitForCall }; } @@ -293,7 +304,6 @@ describe("AgentSession eager prelude re-injection after compaction", () => { // Reminder-only: the post-compaction nudge never forces a tool on the resumed turn. expect(continuation.toolChoice).toBeUndefined(); }); - it("does not re-inject the eager task reminder when task.eager is default", async () => { const { session, waitForCall } = await createHarness({ "task.eager": "default" }); stubCompaction(); @@ -302,7 +312,6 @@ describe("AgentSession eager prelude re-injection after compaction", () => { expect(continuation.messageTexts.some(text => text.includes(TASK_DELEGATION_MARKER))).toBe(false); }); - it("does not re-inject the eager task reminder when task.eager is preferred", async () => { const { session, waitForCall } = await createHarness({ "task.eager": "preferred" }); stubCompaction(); @@ -311,7 +320,6 @@ describe("AgentSession eager prelude re-injection after compaction", () => { expect(continuation.messageTexts.some(text => text.includes(TASK_DELEGATION_MARKER))).toBe(false); }); - it("does not re-inject the eager task reminder for subagent sessions", async () => { const { session, waitForCall } = await createHarness({}, { agentId: "SubAgent", agentKind: "sub" }); stubCompaction(); @@ -320,7 +328,6 @@ describe("AgentSession eager prelude re-injection after compaction", () => { expect(continuation.messageTexts.some(text => text.includes(TASK_DELEGATION_MARKER))).toBe(false); }); - it("does not re-inject the eager task reminder in plan mode", async () => { const { session, waitForCall } = await createHarness(); session.setPlanModeState({ enabled: true, planFilePath: path.join(tempDir.path(), "plan.md") }); diff --git a/packages/coding-agent/test/agent-session-eager-todo.test.ts b/packages/coding-agent/test/agent-session-eager-todo.test.ts index 5a249e407..ab76b63cd 100644 --- a/packages/coding-agent/test/agent-session-eager-todo.test.ts +++ b/packages/coding-agent/test/agent-session-eager-todo.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; import { type } from "@oh-my-pi/omptype"; import { Agent, type AgentMessage, type AgentTool } from "@oh-my-pi/pi-agent-core"; @@ -99,7 +99,9 @@ describe("AgentSession eager todo enforcement", () => { let session: AgentSession; let streamCallCount = 0; let scriptedResponses: AssistantMessage[] = []; - let authStorage: AuthStorage | undefined; + let sharedDir: TempDir; + let sharedAuthStorage: AuthStorage; + let sharedModelRegistry: ModelRegistry; const observedCalls: ObservedPromptCall[] = []; async function createSession( @@ -109,9 +111,7 @@ describe("AgentSession eager todo enforcement", () => { const model = getBundledModel("anthropic", "claude-sonnet-4-5"); if (!model) throw new Error("Expected claude-sonnet-4-5 model to exist"); - authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db")); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml")); + const modelRegistry = sharedModelRegistry; const settings = Settings.isolated({ "compaction.enabled": false, "todo.enabled": true, @@ -196,8 +196,6 @@ describe("AgentSession eager todo enforcement", () => { sessionOverride: Partial = {}, ): Promise { await session.dispose(); - authStorage?.close(); - authStorage = undefined; streamCallCount = 0; scriptedResponses = []; observedCalls.length = 0; @@ -215,6 +213,18 @@ describe("AgentSession eager todo enforcement", () => { return promise; } + beforeAll(async () => { + sharedDir = TempDir.createSync("@pi-agent-session-eager-todo-shared-"); + sharedAuthStorage = await AuthStorage.create(path.join(sharedDir.path(), "auth.db")); + sharedAuthStorage.setRuntimeApiKey("anthropic", "test-key"); + sharedModelRegistry = new ModelRegistry(sharedAuthStorage, path.join(sharedDir.path(), "models.yml")); + }); + + afterAll(() => { + sharedAuthStorage.close(); + sharedDir.removeSync(); + }); + beforeEach(async () => { tempDir = TempDir.createSync("@pi-agent-session-eager-todo-"); streamCallCount = 0; @@ -227,9 +237,7 @@ describe("AgentSession eager todo enforcement", () => { if (session) { await session.dispose(); } - authStorage?.close(); vi.restoreAllMocks(); - authStorage = undefined; tempDir.removeSync(); }); @@ -508,7 +516,6 @@ describe("AgentSession eager todo enforcement", () => { it("prepends the eager todo reminder without forcing the todo tool when todo.eager is preferred", async () => { await session.dispose(); - authStorage?.close(); await createSession({ "todo.eager": "preferred" }); await session.prompt("list all work trees"); diff --git a/packages/coding-agent/test/agent-session-empty-stop-guard.test.ts b/packages/coding-agent/test/agent-session-empty-stop-guard.test.ts index 7328036cb..a57ae6039 100644 --- a/packages/coding-agent/test/agent-session-empty-stop-guard.test.ts +++ b/packages/coding-agent/test/agent-session-empty-stop-guard.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "bun:test"; +import { afterAll, afterEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; import { scheduler } from "node:timers/promises"; import { type } from "@oh-my-pi/omptype"; @@ -18,12 +18,20 @@ const recordToolSchema = type({ value: type("string") }); type Harness = { session: AgentSession; - authStorage: AuthStorage; tempDir: TempDir; }; type SettingsOverrides = Partial>; const activeHarnesses: Harness[] = []; +const sharedDir = TempDir.createSync("@pi-empty-stop-guard-shared-"); +const sharedAuthStorage = await AuthStorage.create(path.join(sharedDir.path(), "auth.db")); +sharedAuthStorage.setRuntimeApiKey("mock", "test-key"); +const sharedModelRegistry = new ModelRegistry(sharedAuthStorage, path.join(sharedDir.path(), "models.yml")); + +afterAll(() => { + sharedAuthStorage.close(); + sharedDir.removeSync(); +}); const recordTool: AgentTool = { name: "record", @@ -89,12 +97,11 @@ async function createHarness( } = {}, ): Promise { const tempDir = TempDir.createSync("@pi-empty-stop-guard-"); - const authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db")); - authStorage.setRuntimeApiKey("mock", "test-key"); + const authStorage = sharedAuthStorage; const mock = createMockModel({ provider: options.provider, id: options.id, responses }); authStorage.setRuntimeApiKey(mock.provider, "test-key"); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml")); + const modelRegistry = sharedModelRegistry; const settings = Settings.isolated({ "compaction.enabled": false, "retry.enabled": false, @@ -129,7 +136,7 @@ async function createHarness( toolRegistry: new Map(tools.map(tool => [tool.name, tool])), extensionRunner: options.extensionRunner, }); - const harness = { session, authStorage, tempDir }; + const harness = { session, tempDir }; activeHarnesses.push(harness); return { ...harness, mock }; } @@ -176,7 +183,6 @@ async function expectPromptCompletes(prompt: Promise): Promise { afterEach(async () => { for (const harness of activeHarnesses.splice(0)) { await harness.session.dispose(); - harness.authStorage.close(); harness.tempDir.removeSync(); } vi.restoreAllMocks(); diff --git a/packages/coding-agent/test/agent-session-goal-midrun-compaction.test.ts b/packages/coding-agent/test/agent-session-goal-midrun-compaction.test.ts index 3fb9540b6..25101f333 100644 --- a/packages/coding-agent/test/agent-session-goal-midrun-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-goal-midrun-compaction.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; import { type } from "@oh-my-pi/omptype"; import { Agent, type AgentMessage, type AgentTool } from "@oh-my-pi/pi-agent-core"; @@ -47,8 +47,23 @@ function highUsage(input: number) { describe("AgentSession mid-run threshold compaction", () => { let tempDir: TempDir; + let sharedDir: TempDir; + let sharedAuthStorage: AuthStorage; + let sharedModelRegistry: ModelRegistry; const cleanups: Array<() => Promise> = []; + beforeAll(async () => { + sharedDir = TempDir.createSync("@pi-agent-goal-midrun-compaction-shared-"); + sharedAuthStorage = await AuthStorage.create(path.join(sharedDir.path(), "auth.db")); + sharedAuthStorage.setRuntimeApiKey("anthropic", "test-key"); + sharedModelRegistry = new ModelRegistry(sharedAuthStorage, path.join(sharedDir.path(), "models.yml")); + }); + + afterAll(() => { + sharedAuthStorage.close(); + sharedDir.removeSync(); + }); + beforeEach(() => { tempDir = TempDir.createSync("@pi-agent-goal-midrun-compaction-"); cleanups.length = 0; @@ -73,9 +88,7 @@ describe("AgentSession mid-run threshold compaction", () => { const model = getBundledModel("anthropic", "claude-sonnet-4-5"); if (!model) throw new Error("Expected claude-sonnet-4-5 model to exist"); - const authStorage = await AuthStorage.create(path.join(tempDir.path(), `testauth-${cleanups.length}.db`)); - authStorage.setRuntimeApiKey("anthropic", "test-key"); - const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), `models-${cleanups.length}.yml`)); + const modelRegistry = sharedModelRegistry; const settings = Settings.isolated({ "compaction.enabled": true, "compaction.strategy": "context-full", @@ -148,10 +161,7 @@ describe("AgentSession mid-run threshold compaction", () => { extensionRunner: options.extensionRunner, }); - cleanups.push(async () => { - await session.dispose(); - authStorage.close(); - }); + cleanups.push(() => session.dispose()); return { session, sessionManager, observedContexts }; } @@ -283,16 +293,12 @@ describe("AgentSession mid-run threshold compaction", () => { extensionRuntime, "assistant-display-variant", ); - const extensionAuthStorage = await AuthStorage.create(path.join(tempDir.path(), "extension-auth-variant.db")); - cleanups.push(async () => { - extensionAuthStorage.close(); - }); const extensionRunner = new ExtensionRunner( [extension], extensionRuntime, tempDir.path(), SessionManager.inMemory(), - new ModelRegistry(extensionAuthStorage, path.join(tempDir.path(), "extension-models-variant.yml")), + sharedModelRegistry, ); const { session, observedContexts } = await createHarness({}, { extensionRunner }); const compactSpy = mockCompaction("MID-RUN-COMPACTED-WITH-CONTENT-VARIANT"); diff --git a/packages/coding-agent/test/read-multi-range.test.ts b/packages/coding-agent/test/read-multi-range.test.ts index f85a1da33..6e0267da4 100644 --- a/packages/coding-agent/test/read-multi-range.test.ts +++ b/packages/coding-agent/test/read-multi-range.test.ts @@ -2,8 +2,12 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; +import { Patch, Patcher } from "@oh-my-pi/hashline"; import type { AgentToolResult } from "@oh-my-pi/pi-agent-core"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; +import { getFileSnapshotStore } from "@oh-my-pi/pi-coding-agent/edit/file-snapshot-store"; +import { HashlineFilesystem } from "@oh-my-pi/pi-coding-agent/edit/hashline/filesystem"; +import { writethroughNoop } from "@oh-my-pi/pi-coding-agent/lsp"; import type { ClientBridge } from "@oh-my-pi/pi-coding-agent/session/client-bridge"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import type { ReadToolDetails } from "@oh-my-pi/pi-coding-agent/tools/read"; @@ -285,4 +289,32 @@ describe("read tool multi-range selector", () => { expect(text).not.toContain("bridge three"); expect(text).not.toContain("disk one"); }); + + it("keeps ACP multi-range blanks editable without exposing the EOF sentinel", async () => { + const filePath = path.join(tmpDir, "bridge.txt"); + const bridgeText = "first\n\nlast\n"; + await fs.writeFile(filePath, bridgeText); + const bridge: ClientBridge = { + capabilities: { readTextFile: true }, + readTextFile: async () => bridgeText, + }; + const session = createSession(tmpDir, bridge); + const text = textOutput(await new ReadTool(session).execute("call-bridge-eof", { path: `${filePath}:1-2,3-3` })); + const header = text.split("\n")[0] ?? ""; + expect(header).toMatch(/^\[bridge\.txt#[0-9A-F]{4}\]$/); + expect(text).toContain("1:first\n2:"); + expect(text).not.toContain("\n4:"); + + const patch = Patch.parse(`${header}\nCUT 2`, { cwd: tmpDir }); + const filesystem = new HashlineFilesystem({ + session, + writethrough: writethroughNoop, + beginDeferredDiagnosticsForPath: () => { + throw new Error("deferred diagnostics are unused"); + }, + }); + await new Patcher({ fs: filesystem, snapshots: getFileSnapshotStore(session) }).apply(patch); + + expect(await fs.readFile(filePath, "utf8")).toBe("first\nlast\n"); + }); });