Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337import { describe, expect, test } from "bun:test"import { Skill } from "@opencode/schema/skill"import type { ImageAttachmentPart, Prompt } from "@/composer/state"import { buildPromptRequest } from "./request"
function inline(filename: string, mime: string, extra?: Partial<ImageAttachmentPart>) { return { type: "image" as const, id: `img_${filename}`, filename, mime, dataUrl: `data:${mime};base64,AAA`, ...extra }}
describe("buildPromptRequest", () => { test("builds text, files, and agents from the prompt", () => { const prompt: Prompt = [ { type: "text", content: "hello", start: 0, end: 5 }, { type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 5, end: 16, selection: { startLine: 4, startChar: 1, endLine: 6, endChar: 1 }, }, { type: "agent", name: "planner", content: "@planner", start: 16, end: 24 }, ]
const result = buildPromptRequest({ prompt, context: [{ key: "ctx:1", type: "file", path: "src/bar.ts", comment: "check this" }], images: [inline("a.png", "image/png")], text: "hello @src/foo.ts @planner", sessionDirectory: "/repo", })
expect(result.text).toContain("hello @src/foo.ts @planner") expect(result.text).toContain("check this") expect(result.displayText).toBe("hello @src/foo.ts @planner") expect(result.comments).toMatchObject([{ path: "src/bar.ts", comment: "check this" }]) expect(result.agents).toEqual([{ name: "planner", mention: { start: 16, end: 24, text: "@planner" } }]) expect(result.files.some((file) => file.uri.startsWith("file:///repo/src/foo.ts"))).toBe(true) expect(result.files.find((file) => file.uri.startsWith("file:///repo/src/foo.ts"))?.mention).toEqual({ start: 5, end: 16, text: "@src/foo.ts", }) })
test("keeps multiple uploaded attachments in order", () => { const result = buildPromptRequest({ prompt: [{ type: "text", content: "check these", start: 0, end: 11 }], context: [], images: [inline("a.png", "image/png"), inline("b.pdf", "application/pdf")], text: "check these", sessionDirectory: "/repo", })
const uploads = result.files.filter((file) => file.uri.startsWith("data:"))
expect(uploads).toHaveLength(2) expect(uploads.map((file) => file.name)).toEqual(["a.png", "b.pdf"]) })
test("preserves an external attachment source path for the model", () => { const result = buildPromptRequest({ prompt: [], context: [], images: [ inline("opencode.global.dat", "text/plain", { sourcePath: "C:\\Users\\Luke\\AppData\\Roaming\\ai.opencode.desktop.beta\\opencode.global.dat", }), ], text: "inspect this", sessionDirectory: "C:\\Repos\\sst\\opencode", })
expect(result.files[0]?.name).toBe( "C:\\Users\\Luke\\AppData\\Roaming\\ai.opencode.desktop.beta\\opencode.global.dat", ) })
test("preserves reference aliases as directory files", () => { const result = buildPromptRequest({ prompt: [ { type: "file", path: "/repo/../docs", content: "@docs", start: 0, end: 5, mime: "application/x-directory", filename: "docs", }, ], context: [], images: [], text: "@docs", sessionDirectory: "/repo/app", })
expect(result.files[0]).toEqual({ uri: "file:///repo/../docs", mime: "application/x-directory", name: "docs", mention: { start: 0, end: 5, text: "@docs" }, }) })
test("deduplicates context files when prompt already includes same path", () => { const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
const result = buildPromptRequest({ prompt, context: [ { key: "ctx:dup", type: "file", path: "src/foo.ts" }, { key: "ctx:comment", type: "file", path: "src/foo.ts", comment: "focus here" }, ], images: [], text: "@src/foo.ts", sessionDirectory: "/repo", })
const fooFiles = result.files.filter((file) => file.uri.startsWith("file:///repo/src/foo.ts"))
expect(fooFiles).toHaveLength(2) expect(result.text).toContain("focus here") })
test("adds files for @mentions inside comment text", () => { const result = buildPromptRequest({ prompt: [{ type: "text", content: "look", start: 0, end: 4 }], context: [ { key: "ctx:comment-mention", type: "file", path: "src/review.ts", comment: "Compare with @src/shared.ts and @src/review.ts.", }, ], images: [], text: "look", sessionDirectory: "/repo", })
expect(result.files).toHaveLength(2) expect(result.files.some((file) => file.uri === "file:///repo/src/review.ts")).toBe(true) expect(result.files.some((file) => file.uri === "file:///repo/src/shared.ts")).toBe(true) })
test("handles Windows paths correctly (simulated on macOS)", () => { const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@src\\foo.ts", sessionDirectory: "D:\\projects\\myapp", // Windows path })
const file = result.files[0] expect(file).toBeDefined() // URL should be parseable expect(() => new URL(file!.uri)).not.toThrow() // Should not have encoded backslashes in wrong place expect(file!.uri).not.toContain("%5C") // Should have normalized to forward slashes expect(file!.uri).toContain("/src/foo.ts") })
test("handles Windows absolute path with special characters", () => { const prompt: Prompt = [{ type: "file", path: "file#name.txt", content: "@file#name.txt", start: 0, end: 14 }]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@file#name.txt", sessionDirectory: "C:\\Users\\test\\Documents", // Windows path })
const file = result.files[0] expect(file).toBeDefined() // URL should be parseable expect(() => new URL(file!.uri)).not.toThrow() // Special chars should be encoded expect(file!.uri).toContain("file%23name.txt") // Should have Windows drive letter properly encoded expect(file!.uri).toMatch(/file:\/\/\/[A-Z]:/) })
test("handles Linux absolute paths correctly", () => { const prompt: Prompt = [{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 0, end: 10 }]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@src/app.ts", sessionDirectory: "/home/user/project", })
expect(result.files[0]?.uri).toBe("file:///home/user/project/src/app.ts") })
test("handles macOS paths correctly", () => { const prompt: Prompt = [{ type: "file", path: "README.md", content: "@README.md", start: 0, end: 9 }]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@README.md", sessionDirectory: "/Users/kelvin/Projects/opencode", })
expect(result.files[0]?.uri).toBe("file:///Users/kelvin/Projects/opencode/README.md") })
test("handles context files with Windows paths", () => { const result = buildPromptRequest({ prompt: [], context: [ { key: "ctx:1", type: "file", path: "src\\utils\\helper.ts" }, { key: "ctx:2", type: "file", path: "test\\unit.test.ts", comment: "check tests" }, ], images: [], text: "test", sessionDirectory: "D:\\workspace\\app", })
expect(result.files).toHaveLength(2)
// All file URLs should be valid result.files.forEach((file) => { expect(() => new URL(file.uri)).not.toThrow() expect(file.uri).not.toContain("%5C") // No encoded backslashes }) })
test("handles absolute Windows paths (user manually specifies full path)", () => { const prompt: Prompt = [ { type: "file", path: "D:\\other\\project\\file.ts", content: "@D:\\other\\project\\file.ts", start: 0, end: 25 }, ]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@D:\\other\\project\\file.ts", sessionDirectory: "C:\\current\\project", })
const file = result.files[0] expect(file).toBeDefined() // Should handle absolute path that differs from sessionDirectory expect(() => new URL(file!.uri)).not.toThrow() expect(file!.uri).toContain("/D:/other/project/file.ts") })
test("handles selection with query parameters on Windows", () => { const prompt: Prompt = [ { type: "file", path: "src\\App.tsx", content: "@src\\App.tsx", start: 0, end: 11, selection: { startLine: 10, startChar: 0, endLine: 20, endChar: 5 }, }, ]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@src\\App.tsx", sessionDirectory: "C:\\project", })
const file = result.files[0] expect(file).toBeDefined() // Should have query parameters expect(file!.uri).toContain("?start=10&end=20") // Should be valid URL expect(() => new URL(file!.uri)).not.toThrow() // Query params should parse correctly const url = new URL(file!.uri) expect(url.searchParams.get("start")).toBe("10") expect(url.searchParams.get("end")).toBe("20") })
test("handles file paths with dots and special segments on Windows", () => { const prompt: Prompt = [ { type: "file", path: "..\\..\\shared\\util.ts", content: "@..\\..\\shared\\util.ts", start: 0, end: 21 }, ]
const result = buildPromptRequest({ prompt, context: [], images: [], text: "@..\\..\\shared\\util.ts", sessionDirectory: "C:\\projects\\myapp\\src", })
const file = result.files[0] expect(file).toBeDefined() // Should be valid URL expect(() => new URL(file!.uri)).not.toThrow() // Should preserve .. segments (backend normalizes) expect(file!.uri).toContain("/..") })
test("keeps skill mentions out of file attachments", () => { const skill = { id: "skill-review", name: "review", } const result = buildPromptRequest({ prompt: [ { type: "skill", id: Skill.ID.make(skill.id), name: Skill.Name.make(skill.name), content: "@review", start: 0, end: 7, }, ], context: [], images: [], text: "@review", sessionDirectory: "/repo", })
expect(result.files).toEqual([]) expect(result.skills).toEqual([{ id: skill.id, name: skill.name, mention: { start: 0, end: 7, text: "@review" } }]) })})