diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 81a45c34e..81b4cf783 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,12 +1,14 @@ # Changelog ## [Unreleased] + ### Added - Added `llama.cpp` to the interactive `/login` provider list, accepting an optional API key while defaulting to local no-auth mode. ### Removed +- Removed the `setNextRequestDebugPath`, `clearNextRequestDebugPath`, and `getNextRequestDebugPath` utility functions for request debugging, as request/response recording now relies exclusively on the `PI_REQ_DEBUG` environment variable. - Removed Wafer Pass (`wafer-pass`) login support; Wafer Serverless remains available as `wafer-serverless`. ### Fixed diff --git a/packages/ai/src/utils/request-debug.ts b/packages/ai/src/utils/request-debug.ts index e7f6fcf83..6b716a12f 100644 --- a/packages/ai/src/utils/request-debug.ts +++ b/packages/ai/src/utils/request-debug.ts @@ -1,6 +1,5 @@ import { Buffer } from "node:buffer"; import * as fs from "node:fs/promises"; -import * as path from "node:path"; import type { FetchImpl } from "../types"; const REQUEST_DEBUG_ENV = "PI_REQ_DEBUG"; @@ -9,7 +8,6 @@ const textEncoder = new TextEncoder(); const utf8Decoder = new TextDecoder("utf-8", { fatal: true }); let nextSessionId = 1; -let nextRequestDebugPath: string | undefined; type DebugFetch = FetchImpl & { [DEBUG_FETCH_MARKER]?: true }; type RequestBodyInit = NonNullable; @@ -55,25 +53,7 @@ function isRequestDebugEnvEnabled(): boolean { } export function isRequestDebugEnabled(): boolean { - return isRequestDebugEnvEnabled() || nextRequestDebugPath !== undefined; -} - -export function setNextRequestDebugPath(requestPath: string): void { - nextRequestDebugPath = requestPath; -} - -export function clearNextRequestDebugPath(): void { - nextRequestDebugPath = undefined; -} - -export function getNextRequestDebugPath(): string | undefined { - return nextRequestDebugPath; -} - -function consumeNextRequestDebugPath(): string | undefined { - const requestPath = nextRequestDebugPath; - nextRequestDebugPath = undefined; - return requestPath; + return isRequestDebugEnvEnabled(); } export function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl { @@ -249,19 +229,6 @@ function copyResponseMetadata(target: Response, source: Response): void { } async function reserveRequestDebugFile(): Promise { - const explicitPath = consumeNextRequestDebugPath(); - if (explicitPath) { - await fs.mkdir(path.dirname(explicitPath), { recursive: true }); - const handle = await fs.open(explicitPath, "w"); - return { - id: nextSessionId++, - requestPath: explicitPath, - responsePath: `${explicitPath}.res.log`, - handle, - overwrite: true, - }; - } - for (;;) { const id = nextSessionId++; const requestPath = `rr-session-${id}.json`; diff --git a/packages/ai/test/request-debug.test.ts b/packages/ai/test/request-debug.test.ts index 10574e5af..724b63585 100644 --- a/packages/ai/test/request-debug.test.ts +++ b/packages/ai/test/request-debug.test.ts @@ -6,27 +6,25 @@ import { clearCustomApis, registerCustomApi } from "@oh-my-pi/pi-ai/api-registry import { stream } from "@oh-my-pi/pi-ai/stream"; import type { AssistantMessage, FetchImpl, Model, ModelSpec } from "@oh-my-pi/pi-ai/types"; import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream"; -import { - clearNextRequestDebugPath, - getNextRequestDebugPath, - setNextRequestDebugPath, - wrapFetchForRequestDebug, -} from "@oh-my-pi/pi-ai/utils/request-debug"; +import { wrapFetchForRequestDebug } from "@oh-my-pi/pi-ai/utils/request-debug"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; const enc = new TextEncoder(); let previousDebugFlag: string | undefined; +let previousCwd: string; let tempDir: string | undefined; beforeEach(async () => { previousDebugFlag = Bun.env.PI_REQ_DEBUG; + previousCwd = process.cwd(); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-req-debug-")); + process.chdir(tempDir); }); afterEach(async () => { clearCustomApis(); - clearNextRequestDebugPath(); + process.chdir(previousCwd); if (previousDebugFlag === undefined) delete Bun.env.PI_REQ_DEBUG; else Bun.env.PI_REQ_DEBUG = previousDebugFlag; if (tempDir) await fs.rm(tempDir, { recursive: true, force: true }); @@ -72,6 +70,25 @@ function splitResponseLog(bytes: Uint8Array): { headers: string; body: Uint8Arra }; } +/** Find the latest rr-session-*.json written to the temp dir by PI_REQ_DEBUG + * and derive its matching .res.log (rr-session-N.res.log, not .json.res.log). */ +async function findDebugFiles(): Promise<{ requestPath: string; responsePath: string }> { + const entries = await fs.readdir(tempDir!); + const jsonFiles = entries.filter(f => /^rr-session-\d+\.json$/.test(f)); + expect(jsonFiles.length).toBeGreaterThan(0); + jsonFiles.sort((a, b) => { + const na = Number(a.match(/\d+/)![0]); + const nb = Number(b.match(/\d+/)![0]); + return na - nb; + }); + const latest = jsonFiles[jsonFiles.length - 1]!; + const id = latest.match(/\d+/)![0]; + return { + requestPath: path.join(tempDir!, latest), + responsePath: path.join(tempDir!, `rr-session-${id}.res.log`), + }; +} + describe("PI_REQ_DEBUG request/response recording", () => { it("leaves fetch untouched when the flag is disabled", () => { delete Bun.env.PI_REQ_DEBUG; @@ -79,10 +96,8 @@ describe("PI_REQ_DEBUG request/response recording", () => { expect(wrapFetchForRequestDebug(fetchImpl)).toBe(fetchImpl); }); - it("records only the next fetch to an explicit request path", async () => { - delete Bun.env.PI_REQ_DEBUG; - const requestPath = path.join(tempDir!, "nested", "next-request.json"); - setNextRequestDebugPath(requestPath); + it("records every fetch while the env flag is enabled", async () => { + Bun.env.PI_REQ_DEBUG = "1"; let calls = 0; const fetchImpl: FetchImpl = async () => { calls += 1; @@ -104,22 +119,21 @@ describe("PI_REQ_DEBUG request/response recording", () => { await second.text(); expect(calls).toBe(2); - expect(getNextRequestDebugPath()).toBeUndefined(); + const { requestPath, responsePath } = await findDebugFiles(); const request = JSON.parse(await fs.readFile(requestPath, "utf8")) as Record; expect(request).toMatchObject({ protocol: "http", method: "POST", - url: "https://provider.test/first", - body: { first: true }, + url: "https://provider.test/second", + body: { second: true }, }); - const log = splitResponseLog(await fs.readFile(`${requestPath}.res.log`)); - expect(log.headers).toContain("x-call: 1"); - expect(new TextDecoder().decode(log.body)).toBe("first"); + const log = splitResponseLog(await fs.readFile(responsePath)); + expect(log.headers).toContain("x-call: 2"); + expect(new TextDecoder().decode(log.body)).toBe("second"); }); it("records request JSON before fetch and raw response bytes after headers", async () => { - const requestPath = path.join(tempDir!, "v1-messages.json"); - setNextRequestDebugPath(requestPath); + Bun.env.PI_REQ_DEBUG = "1"; const responseBody = new Uint8Array([0x66, 0x69, 0x72, 0x73, 0x74, 0x00, 0xff, 0x0a]); const fetchImpl: FetchImpl = async () => chunkedResponse([responseBody.subarray(0, 5), responseBody.subarray(5)]); const wrapped = wrapFetchForRequestDebug(fetchImpl); @@ -131,7 +145,7 @@ describe("PI_REQ_DEBUG request/response recording", () => { }); expect(new Uint8Array(await response.arrayBuffer())).toEqual(responseBody); - const responsePath = `${requestPath}.res.log`; + const { requestPath, responsePath } = await findDebugFiles(); const request = JSON.parse(await fs.readFile(requestPath, "utf8")) as Record; expect(request).toMatchObject({ protocol: "http", @@ -149,8 +163,7 @@ describe("PI_REQ_DEBUG request/response recording", () => { }); it("keeps the partial response log when the response body is cancelled", async () => { - const requestPath = path.join(tempDir!, "partial.json"); - setNextRequestDebugPath(requestPath); + Bun.env.PI_REQ_DEBUG = "1"; const firstChunk = enc.encode("partial"); let sent = false; const fetchImpl: FetchImpl = async () => @@ -171,15 +184,14 @@ describe("PI_REQ_DEBUG request/response recording", () => { expect(firstRead.value).toEqual(firstChunk); await reader.cancel("turn aborted"); - const responsePath = `${requestPath}.res.log`; + const { responsePath } = await findDebugFiles(); const log = splitResponseLog(await fs.readFile(responsePath)); expect(log.headers).toContain("HTTP 201 Created"); expect(log.body).toEqual(firstChunk); }); it("wraps provider fetch options with request debug recording", async () => { - const requestPath = path.join(tempDir!, "provider.json"); - setNextRequestDebugPath(requestPath); + Bun.env.PI_REQ_DEBUG = "1"; const fetchMock: FetchImpl = async () => new Response("ok", { headers: { "x-debug": "yes" } }); registerCustomApi("req-debug-test", (_model, _context, options) => { const events = new AssistantMessageEventStream(); @@ -233,7 +245,7 @@ describe("PI_REQ_DEBUG request/response recording", () => { ); await events.result(); - const responsePath = `${requestPath}.res.log`; + const { requestPath, responsePath } = await findDebugFiles(); const request = JSON.parse(await fs.readFile(requestPath, "utf8")) as Record; expect(request.url).toBe("https://provider.test/custom"); expect(request.body).toEqual({ ok: true }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 505aad553..d4f40294e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog ## [Unreleased] +### Added + +- Added LLM request JSON export functionality to `/dump` + +### Removed + +- Removed `/debug dump-next-request` command +- Removed Wafer Pass from CLI credential help; Wafer Serverless remains available. ### Fixed @@ -8,15 +16,8 @@ - Fixed lazy-initialized LSP servers (basedpyright/pyright, and likely gopls/rust-analyzer) hanging on the first request: the message reader matched incoming messages against pending client requests by id before checking for a `method`, so a server-originated `workspace/configuration` pull whose id collided with an in-flight request was swallowed as a bogus response, leaving the pull unanswered and the server wedged. The reader now routes any message carrying a `method` as a server request before id-matching ([#3001](https://github.com/can1357/oh-my-pi/issues/3001)) - Fixed `omp --approval-mode=yolo acp` and other global option flags placed before a subcommand being rewritten to `launch` with the subcommand swallowed as prompt text; the CLI resolver now skips leading global flags (using the launch parser's value-consumption contract) and dispatches the real subcommand with the flags applied, so ACP mode honors the configured approval policy. ([#2970](https://github.com/can1357/oh-my-pi/issues/2970)) - Fixed `/mcp enable` and `/mcp disable` reconnecting unrelated MCP servers by scoping toggle reconnect/disconnect work to the named server. ([#3157](https://github.com/can1357/oh-my-pi/issues/3157)) - -### Fixed - - Stopped the local llama.cpp no-auth login placeholder from being sent as a discovery bearer token. -### Removed - -- Removed Wafer Pass from CLI credential help; Wafer Serverless remains available. - ## [16.1.8] - 2026-06-20 ### Added @@ -12222,4 +12223,4 @@ Initial public release. ## [0.7.6] - 2025-11-13 -Previous releases did not maintain a changelog. +Previous releases did not maintain a changelog. \ No newline at end of file diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index 774fd9623..ba28d8a94 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -85,15 +85,30 @@ export class CommandController { } } - handleDumpCommand() { + async handleDumpCommand(): Promise { try { const formatted = this.ctx.session.formatSessionAsText(); if (!formatted) { this.ctx.showError("No messages to dump yet."); return; } - copyToClipboard(formatted); - this.ctx.showStatus("Session copied to clipboard"); + // Build the LLM request JSON sidecar first so its path (and a + // raw-context warning) can be appended to the copied transcript. + let sidecarPath: string | undefined; + let sidecarError: string | undefined; + try { + sidecarPath = await this.ctx.session.dumpLlmRequestToTmpDir(); + } catch (error: unknown) { + sidecarError = error instanceof Error ? error.message : "Unknown error"; + } + const doc = sidecarPath + ? `${formatted}\n\n---\nLLM request JSON: ${sidecarPath}\nThis file persists on disk and may contain raw context/secrets — treat accordingly.` + : formatted; + await copyToClipboard(doc); + const statusParts = ["Session copied to clipboard"]; + if (sidecarPath) statusParts.push(`LLM request JSON: ${sidecarPath}`); + if (sidecarError) statusParts.push(`LLM request JSON unavailable: ${sidecarError}`); + this.ctx.showStatus(statusParts.join("\n")); } catch (error: unknown) { this.ctx.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`); } diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index adda22d24..89bbb082c 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -3491,7 +3491,7 @@ export class InteractiveMode implements InteractiveModeContext { return this.#commandController.handleExportCommand(text); } - handleDumpCommand() { + async handleDumpCommand(): Promise { return this.#commandController.handleDumpCommand(); } diff --git a/packages/coding-agent/src/modes/types.ts b/packages/coding-agent/src/modes/types.ts index ec1efcc1f..b171437fa 100644 --- a/packages/coding-agent/src/modes/types.ts +++ b/packages/coding-agent/src/modes/types.ts @@ -290,7 +290,7 @@ export interface InteractiveModeContext { handleHotkeysCommand(): void; handleToolsCommand(): void; handleContextCommand(): void; - handleDumpCommand(): void; + handleDumpCommand(): Promise; handleAdvisorDumpCommand(isRaw?: boolean): void; handleDebugTranscriptCommand(): Promise; handleClearCommand(): Promise; diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index debbfc65f..fa5146cb8 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -103,7 +103,7 @@ import { resolveServiceTier, streamSimple, } from "@oh-my-pi/pi-ai"; -import { stripToolDescriptions } from "@oh-my-pi/pi-ai/utils/schema"; +import { stripToolDescriptions, toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema"; import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop"; import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id"; import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking"; @@ -12606,6 +12606,41 @@ export class AgentSession { }); } + /** + * Dump the current session's LLM-facing request context as JSON to a + * auto-named file in `os.tmpdir()`. This is the synchronous + * `convertToLlm`-boundary snapshot — system prompt, tools (wire schemas), + * thinking/service tier, and converted messages — with no network round-trip + * and no arming flag, so advisor/side requests cannot intercept it. + * + * The file persists on disk and may contain the same raw context/secrets + * as `/dump`; treat the path accordingly. + * + * @returns the written file path, or `undefined` when there are no messages. + */ + async dumpLlmRequestToTmpDir(): Promise { + const messages = this.messages; + if (messages.length === 0) return undefined; + const llmMessages = await this.convertMessagesToLlm(messages); + const payload = { + model: this.agent.state.model ?? null, + thinkingLevel: this.#thinkingLevel ?? null, + serviceTier: this.agent.serviceTier ?? null, + systemPrompt: this.agent.state.systemPrompt, + tools: this.agent.state.tools.map(tool => ({ + name: tool.name, + description: tool.description, + parameters: toolWireSchema(tool), + ...(tool.strict !== undefined ? { strict: tool.strict } : {}), + ...(tool.customWireName ? { customWireName: tool.customWireName } : {}), + })), + messages: llmMessages, + }; + const filePath = path.join(os.tmpdir(), `omp-llm-request-${Snowflake.next()}.json`); + await Bun.write(filePath, `${JSON.stringify(payload, null, 2)}\n`); + return filePath; + } + /** * Enable or disable the advisor for this session. The setting is overridden for the session, * and the runtime is started or stopped to match. diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index d499483d9..ff9709ab6 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -1,8 +1,6 @@ import * as fs from "node:fs/promises"; -import * as os from "node:os"; import * as path from "node:path"; import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth"; -import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug"; import { type AutocompleteItem, Spacer } from "@oh-my-pi/pi-tui"; import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils"; import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest"; @@ -192,46 +190,6 @@ async function handleUsageResetCommand( await output(describeRedeemOutcome(outcome, target.label)); } -const DEBUG_DUMP_NEXT_REQUEST_USAGE = "Usage: /debug dump-next-request "; - -function resolveDebugRequestDumpPath(target: string, cwd: string): string { - const expanded = - target === "~" - ? os.homedir() - : target.startsWith("~/") || target.startsWith("~\\") - ? path.join(os.homedir(), target.slice(2)) - : target; - return path.resolve(cwd, expanded); -} - -async function handleDebugSubcommand( - args: string, - cwd: string, - output: (text: string) => Promise | void, -): Promise { - const { verb, rest } = parseSubcommand(args); - switch (verb) { - case "": - await output(DEBUG_DUMP_NEXT_REQUEST_USAGE); - return commandConsumed(); - case "dump-next-request": - case "dump-request": - case "next-request": { - if (!rest) { - await output(DEBUG_DUMP_NEXT_REQUEST_USAGE); - return commandConsumed(); - } - const requestPath = resolveDebugRequestDumpPath(rest, cwd); - setNextRequestDebugPath(requestPath); - await output(`Next AI provider request will be dumped to ${requestPath}`); - return commandConsumed(); - } - default: - await output(`Unknown /debug subcommand "${verb}". ${DEBUG_DUMP_NEXT_REQUEST_USAGE}`); - return commandConsumed(); - } -} - /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */ function parseShakeMode(args: string): ShakeMode | { error: string } { const verb = args.trim().toLowerCase(); @@ -612,16 +570,33 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ }, { name: "dump", - description: "Copy session transcript to clipboard", - acpDescription: "Return full transcript as plain text", + description: "Copy session transcript to clipboard (and write LLM request JSON to tmp)", + acpDescription: "Return full transcript as plain text, with LLM request JSON path", allowArgs: true, handle: async (_command, runtime) => { const text = runtime.session.formatSessionAsText(); - await runtime.output(text || "No messages to dump yet."); + if (!text) { + await runtime.output("No messages to dump yet."); + return commandConsumed(); + } + let sidecarPath: string | undefined; + try { + sidecarPath = await runtime.session.dumpLlmRequestToTmpDir(); + } catch { + // Sidecar is best-effort; the transcript is still output below. + } + const lines = [text]; + if (sidecarPath) + lines.push( + "", + `LLM request JSON: ${sidecarPath}`, + "This file persists on disk and may contain raw context/secrets — treat accordingly.", + ); + await runtime.output(lines.join("\n")); return commandConsumed(); }, - handleTui: (_command, runtime) => { - runtime.ctx.handleDumpCommand(); + handleTui: async (_command, runtime) => { + await runtime.ctx.handleDumpCommand(); runtime.ctx.editor.setText(""); }, }, @@ -1509,25 +1484,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ { name: "debug", description: "Open debug tools selector", - allowArgs: true, - subcommands: [ - { - name: "dump-next-request", - description: "Dump the next AI provider HTTP request as JSON", - usage: "", - }, - ], - handle: async (command, runtime) => - handleDebugSubcommand(command.args, runtime.cwd, text => runtime.output(text)), - handleTui: async (command, runtime) => { - const args = command.args.trim(); - if (args.length === 0) { - runtime.ctx.showDebugSelector(); - } else { - await handleDebugSubcommand(args, runtime.ctx.sessionManager.getCwd(), text => - runtime.ctx.showStatus(text), - ); - } + handleTui: async (_command, runtime) => { + await runtime.ctx.showDebugSelector(); runtime.ctx.editor.setText(""); }, }, diff --git a/packages/coding-agent/test/acp-builtins.test.ts b/packages/coding-agent/test/acp-builtins.test.ts index 7742d5674..272a7b139 100644 --- a/packages/coding-agent/test/acp-builtins.test.ts +++ b/packages/coding-agent/test/acp-builtins.test.ts @@ -28,6 +28,7 @@ interface FakeAcpBuiltinSession { fetchUsageReports?: () => Promise; getAsyncJobSnapshot: (opts?: { recentLimit?: number }) => { running: unknown[]; recent: unknown[] } | null; formatSessionAsText: () => string; + dumpLlmRequestToTmpDir: () => Promise; getLastAssistantText: () => string | undefined; messages: unknown[]; settings: Settings; @@ -100,6 +101,7 @@ function createRuntime() { async refreshBaseSystemPrompt() {}, getAsyncJobSnapshot: () => null, formatSessionAsText: () => "", + dumpLlmRequestToTmpDir: async () => undefined, getLastAssistantText: () => undefined, messages: [], model: undefined, @@ -321,9 +323,25 @@ describe("ACP builtin slash commands", () => { }); // /dump - it("dump: outputs transcript when present", async () => { + it("dump: outputs transcript with LLM request JSON path when sidecar succeeds", async () => { const { output, runtime } = createRuntime(); runtime.session.formatSessionAsText = () => "Session content here"; + runtime.session.dumpLlmRequestToTmpDir = async () => "/tmp/omp-llm-request-test.json"; + + const result = await executeAcpBuiltinSlashCommand("/dump", runtime); + + expect(result).toEqual({ consumed: true }); + expect(output[0]).toContain("Session content here"); + expect(output[0]).toContain("LLM request JSON: /tmp/omp-llm-request-test.json"); + expect(output[0]).toContain("persists on disk"); + }); + + it("dump: outputs transcript without sidecar when dumpLlmRequestToTmpDir throws", async () => { + const { output, runtime } = createRuntime(); + runtime.session.formatSessionAsText = () => "Session content here"; + runtime.session.dumpLlmRequestToTmpDir = async () => { + throw new Error("convert failed"); + }; const result = await executeAcpBuiltinSlashCommand("/dump", runtime); diff --git a/packages/coding-agent/test/slash-commands/debug.test.ts b/packages/coding-agent/test/slash-commands/debug.test.ts index a32240714..b7008bf08 100644 --- a/packages/coding-agent/test/slash-commands/debug.test.ts +++ b/packages/coding-agent/test/slash-commands/debug.test.ts @@ -1,11 +1,8 @@ -import { afterEach, describe, expect, it, vi } from "bun:test"; -import * as os from "node:os"; -import * as path from "node:path"; -import { clearNextRequestDebugPath, getNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug"; +import { describe, expect, it, vi } from "bun:test"; import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry"; -function createRuntimeHarness(cwd: string) { +function createRuntimeHarness() { const setText = vi.fn(); const showStatus = vi.fn(); const showDebugSelector = vi.fn(); @@ -16,7 +13,6 @@ function createRuntimeHarness(cwd: string) { runtime: { ctx: { editor: { setText } as unknown as InteractiveModeContext["editor"], - sessionManager: { getCwd: () => cwd } as unknown as InteractiveModeContext["sessionManager"], showStatus, showDebugSelector, } as unknown as InteractiveModeContext, @@ -24,32 +20,14 @@ function createRuntimeHarness(cwd: string) { }; } -afterEach(() => { - clearNextRequestDebugPath(); -}); - describe("/debug slash command", () => { - it("opens the debug selector without arguments", async () => { - const harness = createRuntimeHarness(path.join(os.tmpdir(), "omp-debug-cwd")); + it("opens the debug selector", async () => { + const harness = createRuntimeHarness(); expect(await executeBuiltinSlashCommand("/debug", harness.runtime)).toBe(true); expect(harness.showDebugSelector).toHaveBeenCalledTimes(1); expect(harness.showStatus).not.toHaveBeenCalled(); expect(harness.setText).toHaveBeenCalledWith(""); - expect(getNextRequestDebugPath()).toBeUndefined(); - }); - - it("arms a one-shot provider request dump path", async () => { - const cwd = path.join(os.tmpdir(), "omp-debug-cwd"); - const harness = createRuntimeHarness(cwd); - const expectedPath = path.resolve(cwd, "request.json"); - - expect(await executeBuiltinSlashCommand("/debug dump-next-request request.json", harness.runtime)).toBe(true); - - expect(harness.showDebugSelector).not.toHaveBeenCalled(); - expect(harness.showStatus).toHaveBeenCalledWith(`Next AI provider request will be dumped to ${expectedPath}`); - expect(harness.setText).toHaveBeenCalledWith(""); - expect(getNextRequestDebugPath()).toBe(expectedPath); }); });