diff --git a/packages/opencode/test/session/prompt-missing-file.test.ts b/packages/opencode/test/session/prompt-missing-file.test.ts deleted file mode 100644 index c3f52f56c..000000000 --- a/packages/opencode/test/session/prompt-missing-file.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import path from "path" -import { describe, expect, test } from "bun:test" -import { Instance } from "../../src/project/instance" -import { Session } from "../../src/session" -import { MessageV2 } from "../../src/session/message-v2" -import { SessionPrompt } from "../../src/session/prompt" -import { tmpdir } from "../fixture/fixture" - -describe("session.prompt missing file", () => { - test("does not fail the prompt when a file part is missing", async () => { - await using tmp = await tmpdir({ - git: true, - config: { - agent: { - build: { - model: "openai/gpt-5.2", - }, - }, - }, - }) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const session = await Session.create({}) - - const missing = path.join(tmp.path, "does-not-exist.ts") - const msg = await SessionPrompt.prompt({ - sessionID: session.id, - agent: "build", - noReply: true, - parts: [ - { type: "text", text: "please review @does-not-exist.ts" }, - { - type: "file", - mime: "text/plain", - url: `file://${missing}`, - filename: "does-not-exist.ts", - }, - ], - }) - - if (msg.info.role !== "user") throw new Error("expected user message") - - const hasFailure = msg.parts.some( - (part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"), - ) - expect(hasFailure).toBe(true) - - await Session.remove(session.id) - }, - }) - }) - - test("keeps stored part order stable when file resolution is async", async () => { - await using tmp = await tmpdir({ - git: true, - config: { - agent: { - build: { - model: "openai/gpt-5.2", - }, - }, - }, - }) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const session = await Session.create({}) - - const missing = path.join(tmp.path, "still-missing.ts") - const msg = await SessionPrompt.prompt({ - sessionID: session.id, - agent: "build", - noReply: true, - parts: [ - { - type: "file", - mime: "text/plain", - url: `file://${missing}`, - filename: "still-missing.ts", - }, - { type: "text", text: "after-file" }, - ], - }) - - if (msg.info.role !== "user") throw new Error("expected user message") - - const stored = await MessageV2.get({ - sessionID: session.id, - messageID: msg.info.id, - }) - const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text) - - expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true) - expect(text[1]?.includes("Read tool failed to read")).toBe(true) - expect(text[2]).toBe("after-file") - - await Session.remove(session.id) - }, - }) - }) -}) diff --git a/packages/opencode/test/session/prompt-special-chars.test.ts b/packages/opencode/test/session/prompt-special-chars.test.ts deleted file mode 100644 index dce0b0049..000000000 --- a/packages/opencode/test/session/prompt-special-chars.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import path from "path" -import { describe, expect, test } from "bun:test" -import { fileURLToPath } from "url" -import { Instance } from "../../src/project/instance" -import { Log } from "../../src/util/log" -import { Session } from "../../src/session" -import { SessionPrompt } from "../../src/session/prompt" -import { MessageV2 } from "../../src/session/message-v2" -import { tmpdir } from "../fixture/fixture" - -Log.init({ print: false }) - -describe("session.prompt special characters", () => { - test("handles filenames with # character", async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - await Bun.write(path.join(dir, "file#name.txt"), "special content\n") - }, - }) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const session = await Session.create({}) - const template = "Read @file#name.txt" - const parts = await SessionPrompt.resolvePromptParts(template) - const fileParts = parts.filter((part) => part.type === "file") - - expect(fileParts.length).toBe(1) - expect(fileParts[0].filename).toBe("file#name.txt") - - // Verify the URL is properly encoded (# should be %23) - expect(fileParts[0].url).toContain("%23") - - // Verify the URL can be correctly converted back to a file path - const decodedPath = fileURLToPath(fileParts[0].url) - expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt")) - - const message = await SessionPrompt.prompt({ - sessionID: session.id, - parts, - noReply: true, - }) - const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id }) - - // Verify the file content was read correctly - const textParts = stored.parts.filter((part) => part.type === "text") - const hasContent = textParts.some((part) => part.text.includes("special content")) - expect(hasContent).toBe(true) - - await Session.remove(session.id) - }, - }) - }) -}) diff --git a/packages/opencode/test/session/prompt-variant.test.ts b/packages/opencode/test/session/prompt-variant.test.ts deleted file mode 100644 index 83ae175c6..000000000 --- a/packages/opencode/test/session/prompt-variant.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { Instance } from "../../src/project/instance" -import { Session } from "../../src/session" -import { SessionPrompt } from "../../src/session/prompt" -import { tmpdir } from "../fixture/fixture" - -describe("session.prompt agent variant", () => { - test("applies agent variant only when using agent model", async () => { - const prev = process.env.OPENAI_API_KEY - process.env.OPENAI_API_KEY = "test-openai-key" - - try { - await using tmp = await tmpdir({ - git: true, - config: { - agent: { - build: { - model: "openai/gpt-5.2", - variant: "xhigh", - }, - }, - }, - }) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const session = await Session.create({}) - - const other = await SessionPrompt.prompt({ - sessionID: session.id, - agent: "build", - model: { providerID: "opencode", modelID: "kimi-k2.5-free" }, - noReply: true, - parts: [{ type: "text", text: "hello" }], - }) - if (other.info.role !== "user") throw new Error("expected user message") - expect(other.info.variant).toBeUndefined() - - const match = await SessionPrompt.prompt({ - sessionID: session.id, - agent: "build", - noReply: true, - parts: [{ type: "text", text: "hello again" }], - }) - if (match.info.role !== "user") throw new Error("expected user message") - expect(match.info.model).toEqual({ providerID: "openai", modelID: "gpt-5.2" }) - expect(match.info.variant).toBe("xhigh") - - const override = await SessionPrompt.prompt({ - sessionID: session.id, - agent: "build", - noReply: true, - variant: "high", - parts: [{ type: "text", text: "hello third" }], - }) - if (override.info.role !== "user") throw new Error("expected user message") - expect(override.info.variant).toBe("high") - - await Session.remove(session.id) - }, - }) - } finally { - if (prev === undefined) delete process.env.OPENAI_API_KEY - else process.env.OPENAI_API_KEY = prev - } - }) -}) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts new file mode 100644 index 000000000..e8a8c65b0 --- /dev/null +++ b/packages/opencode/test/session/prompt.test.ts @@ -0,0 +1,211 @@ +import path from "path" +import { describe, expect, test } from "bun:test" +import { fileURLToPath } from "url" +import { Instance } from "../../src/project/instance" +import { Session } from "../../src/session" +import { MessageV2 } from "../../src/session/message-v2" +import { SessionPrompt } from "../../src/session/prompt" +import { Log } from "../../src/util/log" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +describe("session.prompt missing file", () => { + test("does not fail the prompt when a file part is missing", async () => { + await using tmp = await tmpdir({ + git: true, + config: { + agent: { + build: { + model: "openai/gpt-5.2", + }, + }, + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + + const missing = path.join(tmp.path, "does-not-exist.ts") + const msg = await SessionPrompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [ + { type: "text", text: "please review @does-not-exist.ts" }, + { + type: "file", + mime: "text/plain", + url: `file://${missing}`, + filename: "does-not-exist.ts", + }, + ], + }) + + if (msg.info.role !== "user") throw new Error("expected user message") + + const hasFailure = msg.parts.some( + (part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"), + ) + expect(hasFailure).toBe(true) + + await Session.remove(session.id) + }, + }) + }) + + test("keeps stored part order stable when file resolution is async", async () => { + await using tmp = await tmpdir({ + git: true, + config: { + agent: { + build: { + model: "openai/gpt-5.2", + }, + }, + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + + const missing = path.join(tmp.path, "still-missing.ts") + const msg = await SessionPrompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [ + { + type: "file", + mime: "text/plain", + url: `file://${missing}`, + filename: "still-missing.ts", + }, + { type: "text", text: "after-file" }, + ], + }) + + if (msg.info.role !== "user") throw new Error("expected user message") + + const stored = await MessageV2.get({ + sessionID: session.id, + messageID: msg.info.id, + }) + const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text) + + expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true) + expect(text[1]?.includes("Read tool failed to read")).toBe(true) + expect(text[2]).toBe("after-file") + + await Session.remove(session.id) + }, + }) + }) +}) + +describe("session.prompt special characters", () => { + test("handles filenames with # character", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write(path.join(dir, "file#name.txt"), "special content\n") + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const template = "Read @file#name.txt" + const parts = await SessionPrompt.resolvePromptParts(template) + const fileParts = parts.filter((part) => part.type === "file") + + expect(fileParts.length).toBe(1) + expect(fileParts[0].filename).toBe("file#name.txt") + expect(fileParts[0].url).toContain("%23") + + const decodedPath = fileURLToPath(fileParts[0].url) + expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt")) + + const message = await SessionPrompt.prompt({ + sessionID: session.id, + parts, + noReply: true, + }) + const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id }) + const textParts = stored.parts.filter((part) => part.type === "text") + const hasContent = textParts.some((part) => part.text.includes("special content")) + expect(hasContent).toBe(true) + + await Session.remove(session.id) + }, + }) + }) +}) + +describe("session.prompt agent variant", () => { + test("applies agent variant only when using agent model", async () => { + const prev = process.env.OPENAI_API_KEY + process.env.OPENAI_API_KEY = "test-openai-key" + + try { + await using tmp = await tmpdir({ + git: true, + config: { + agent: { + build: { + model: "openai/gpt-5.2", + variant: "xhigh", + }, + }, + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + + const other = await SessionPrompt.prompt({ + sessionID: session.id, + agent: "build", + model: { providerID: "opencode", modelID: "kimi-k2.5-free" }, + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + if (other.info.role !== "user") throw new Error("expected user message") + expect(other.info.variant).toBeUndefined() + + const match = await SessionPrompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello again" }], + }) + if (match.info.role !== "user") throw new Error("expected user message") + expect(match.info.model).toEqual({ providerID: "openai", modelID: "gpt-5.2" }) + expect(match.info.variant).toBe("xhigh") + + const override = await SessionPrompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + variant: "high", + parts: [{ type: "text", text: "hello third" }], + }) + if (override.info.role !== "user") throw new Error("expected user message") + expect(override.info.variant).toBe("high") + + await Session.remove(session.id) + }, + }) + } finally { + if (prev === undefined) delete process.env.OPENAI_API_KEY + else process.env.OPENAI_API_KEY = prev + } + }) +})