wip
This commit is contained in:
parent
58f7da6e9f
commit
78f8cc9418
11 changed files with 289 additions and 273 deletions
|
|
@ -70,8 +70,8 @@ export const AgentCommand = cmd({
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getAvailableTools(agent: Agent.Info) {
|
async function getAvailableTools(agent: Agent.Info) {
|
||||||
const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
return ToolRegistry.tools(providerID, agent)
|
return ToolRegistry.tools(model, agent)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ import { TodoWriteTool } from "@/tool/todo"
|
||||||
import type { GrepTool } from "@/tool/grep"
|
import type { GrepTool } from "@/tool/grep"
|
||||||
import type { ListTool } from "@/tool/ls"
|
import type { ListTool } from "@/tool/ls"
|
||||||
import type { EditTool } from "@/tool/edit"
|
import type { EditTool } from "@/tool/edit"
|
||||||
import type { PatchTool } from "@/tool/patch"
|
import type { ApplyPatchTool } from "@/tool/apply_patch.ts"
|
||||||
import type { WebFetchTool } from "@/tool/webfetch"
|
import type { WebFetchTool } from "@/tool/webfetch"
|
||||||
import type { TaskTool } from "@/tool/task"
|
import type { TaskTool } from "@/tool/task"
|
||||||
import type { QuestionTool } from "@/tool/question"
|
import type { QuestionTool } from "@/tool/question"
|
||||||
|
|
@ -1835,7 +1835,7 @@ function Edit(props: ToolProps<typeof EditTool>) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Patch(props: ToolProps<typeof PatchTool>) {
|
function Patch(props: ToolProps<typeof ApplyPatchTool>) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ export const ExperimentalRoutes = lazy(() =>
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { provider } = c.req.valid("query")
|
const { provider, model } = c.req.valid("query")
|
||||||
const tools = await ToolRegistry.tools(provider)
|
const tools = await ToolRegistry.tools({ providerID: provider, modelID: model })
|
||||||
return c.json(
|
return c.json(
|
||||||
tools.map((t) => ({
|
tools.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
|
|
|
||||||
|
|
@ -685,7 +685,10 @@ export namespace SessionPrompt {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const item of await ToolRegistry.tools(input.model.providerID, input.agent)) {
|
for (const item of await ToolRegistry.tools(
|
||||||
|
{ modelID: input.model.api.id, providerID: input.model.providerID },
|
||||||
|
input.agent,
|
||||||
|
)) {
|
||||||
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
|
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
|
||||||
tools[item.id] = tool({
|
tools[item.id] = tool({
|
||||||
id: item.id as any,
|
id: item.id as any,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const PatchParams = z.object({
|
||||||
patchText: z.string().describe("The full patch text that describes all changes to be made"),
|
patchText: z.string().describe("The full patch text that describes all changes to be made"),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const PatchTool = Tool.define("patch", {
|
export const ApplyPatchTool = Tool.define("apply_patch", {
|
||||||
description:
|
description:
|
||||||
"Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.",
|
"Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.",
|
||||||
parameters: PatchParams,
|
parameters: PatchParams,
|
||||||
1
packages/opencode/src/tool/apply_patch.txt
Normal file
1
packages/opencode/src/tool/apply_patch.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.
|
||||||
|
|
@ -37,7 +37,7 @@ export const BatchTool = Tool.define("batch", async () => {
|
||||||
const discardedCalls = params.tool_calls.slice(10)
|
const discardedCalls = params.tool_calls.slice(10)
|
||||||
|
|
||||||
const { ToolRegistry } = await import("./registry")
|
const { ToolRegistry } = await import("./registry")
|
||||||
const availableTools = await ToolRegistry.tools("")
|
const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" })
|
||||||
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
|
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
|
||||||
|
|
||||||
const executeCall = async (call: (typeof toolCalls)[0]) => {
|
const executeCall = async (call: (typeof toolCalls)[0]) => {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
do not use
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { Log } from "@/util/log"
|
||||||
import { LspTool } from "./lsp"
|
import { LspTool } from "./lsp"
|
||||||
import { Truncate } from "./truncation"
|
import { Truncate } from "./truncation"
|
||||||
import { PlanExitTool, PlanEnterTool } from "./plan"
|
import { PlanExitTool, PlanEnterTool } from "./plan"
|
||||||
|
import { ApplyPatchTool } from "./apply_patch"
|
||||||
|
|
||||||
export namespace ToolRegistry {
|
export namespace ToolRegistry {
|
||||||
const log = Log.create({ service: "tool.registry" })
|
const log = Log.create({ service: "tool.registry" })
|
||||||
|
|
@ -108,6 +109,7 @@ export namespace ToolRegistry {
|
||||||
WebSearchTool,
|
WebSearchTool,
|
||||||
CodeSearchTool,
|
CodeSearchTool,
|
||||||
SkillTool,
|
SkillTool,
|
||||||
|
ApplyPatchTool,
|
||||||
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
|
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
|
||||||
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
|
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
|
||||||
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),
|
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),
|
||||||
|
|
@ -119,15 +121,26 @@ export namespace ToolRegistry {
|
||||||
return all().then((x) => x.map((t) => t.id))
|
return all().then((x) => x.map((t) => t.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tools(providerID: string, agent?: Agent.Info) {
|
export async function tools(
|
||||||
|
model: {
|
||||||
|
providerID: string
|
||||||
|
modelID: string
|
||||||
|
},
|
||||||
|
agent?: Agent.Info,
|
||||||
|
) {
|
||||||
const tools = await all()
|
const tools = await all()
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
tools
|
tools
|
||||||
.filter((t) => {
|
.filter((t) => {
|
||||||
// Enable websearch/codesearch for zen users OR via enable flag
|
// Enable websearch/codesearch for zen users OR via enable flag
|
||||||
if (t.id === "codesearch" || t.id === "websearch") {
|
if (t.id === "codesearch" || t.id === "websearch") {
|
||||||
return providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
|
return model.providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usePatch = model.modelID.includes("gpt") && !model.modelID.includes("oss")
|
||||||
|
if (t.id === "apply_patch") return usePatch
|
||||||
|
if (t.id === "edit") return !usePatch
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.map(async (t) => {
|
.map(async (t) => {
|
||||||
|
|
|
||||||
261
packages/opencode/test/tool/apply_patch.test.ts
Normal file
261
packages/opencode/test/tool/apply_patch.test.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import path from "path"
|
||||||
|
import { ApplyPatchTool } from "../../src/tool/apply_patch"
|
||||||
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
import { PermissionNext } from "../../src/permission/next"
|
||||||
|
import * as fs from "fs/promises"
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
sessionID: "test",
|
||||||
|
messageID: "",
|
||||||
|
callID: "",
|
||||||
|
agent: "build",
|
||||||
|
abort: AbortSignal.any([]),
|
||||||
|
metadata: () => {},
|
||||||
|
ask: async () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// const patchTool = await PatchTool.init()
|
||||||
|
|
||||||
|
// describe("tool.patch", () => {
|
||||||
|
// test("should validate required parameters", async () => {
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: "/tmp",
|
||||||
|
// fn: async () => {
|
||||||
|
// expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should validate patch format", async () => {
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: "/tmp",
|
||||||
|
// fn: async () => {
|
||||||
|
// expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should handle empty patch", async () => {
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: "/tmp",
|
||||||
|
// fn: async () => {
|
||||||
|
// const emptyPatch = `*** Begin Patch
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("No file changes found in patch")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test.skip("should ask permission for files outside working directory", async () => {
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: "/tmp",
|
||||||
|
// fn: async () => {
|
||||||
|
// const maliciousPatch = `*** Begin Patch
|
||||||
|
// *** Add File: /etc/passwd
|
||||||
|
// +malicious content
|
||||||
|
// *** End Patch`
|
||||||
|
// patchTool.execute({ patchText: maliciousPatch }, ctx)
|
||||||
|
// // TODO: this sucks
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
// const pending = await PermissionNext.list()
|
||||||
|
// expect(pending.find((p) => p.sessionID === ctx.sessionID)).toBeDefined()
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should handle simple add file operation", async () => {
|
||||||
|
// await using fixture = await tmpdir()
|
||||||
|
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: fixture.path,
|
||||||
|
// fn: async () => {
|
||||||
|
// const patchText = `*** Begin Patch
|
||||||
|
// *** Add File: test-file.txt
|
||||||
|
// +Hello World
|
||||||
|
// +This is a test file
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// const result = await patchTool.execute({ patchText }, ctx)
|
||||||
|
|
||||||
|
// expect(result.title).toContain("files changed")
|
||||||
|
// expect(result.metadata.diff).toBeDefined()
|
||||||
|
// expect(result.output).toContain("Patch applied successfully")
|
||||||
|
|
||||||
|
// // Verify file was created
|
||||||
|
// const filePath = path.join(fixture.path, "test-file.txt")
|
||||||
|
// const content = await fs.readFile(filePath, "utf-8")
|
||||||
|
// expect(content).toBe("Hello World\nThis is a test file")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should handle file with context update", async () => {
|
||||||
|
// await using fixture = await tmpdir()
|
||||||
|
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: fixture.path,
|
||||||
|
// fn: async () => {
|
||||||
|
// const patchText = `*** Begin Patch
|
||||||
|
// *** Add File: config.js
|
||||||
|
// +const API_KEY = "test-key"
|
||||||
|
// +const DEBUG = false
|
||||||
|
// +const VERSION = "1.0"
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// const result = await patchTool.execute({ patchText }, ctx)
|
||||||
|
|
||||||
|
// expect(result.title).toContain("files changed")
|
||||||
|
// expect(result.metadata.diff).toBeDefined()
|
||||||
|
// expect(result.output).toContain("Patch applied successfully")
|
||||||
|
|
||||||
|
// // Verify file was created with correct content
|
||||||
|
// const filePath = path.join(fixture.path, "config.js")
|
||||||
|
// const content = await fs.readFile(filePath, "utf-8")
|
||||||
|
// expect(content).toBe('const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"')
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should handle multiple file operations", async () => {
|
||||||
|
// await using fixture = await tmpdir()
|
||||||
|
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: fixture.path,
|
||||||
|
// fn: async () => {
|
||||||
|
// const patchText = `*** Begin Patch
|
||||||
|
// *** Add File: file1.txt
|
||||||
|
// +Content of file 1
|
||||||
|
// *** Add File: file2.txt
|
||||||
|
// +Content of file 2
|
||||||
|
// *** Add File: file3.txt
|
||||||
|
// +Content of file 3
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// const result = await patchTool.execute({ patchText }, ctx)
|
||||||
|
|
||||||
|
// expect(result.title).toContain("3 files changed")
|
||||||
|
// expect(result.metadata.diff).toBeDefined()
|
||||||
|
// expect(result.output).toContain("Patch applied successfully")
|
||||||
|
|
||||||
|
// // Verify all files were created
|
||||||
|
// for (let i = 1; i <= 3; i++) {
|
||||||
|
// const filePath = path.join(fixture.path, `file${i}.txt`)
|
||||||
|
// const content = await fs.readFile(filePath, "utf-8")
|
||||||
|
// expect(content).toBe(`Content of file ${i}`)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should create parent directories when adding nested files", async () => {
|
||||||
|
// await using fixture = await tmpdir()
|
||||||
|
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: fixture.path,
|
||||||
|
// fn: async () => {
|
||||||
|
// const patchText = `*** Begin Patch
|
||||||
|
// *** Add File: deep/nested/file.txt
|
||||||
|
// +Deep nested content
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// const result = await patchTool.execute({ patchText }, ctx)
|
||||||
|
|
||||||
|
// expect(result.title).toContain("files changed")
|
||||||
|
// expect(result.output).toContain("Patch applied successfully")
|
||||||
|
|
||||||
|
// // Verify nested file was created
|
||||||
|
// const nestedPath = path.join(fixture.path, "deep", "nested", "file.txt")
|
||||||
|
// const exists = await fs
|
||||||
|
// .access(nestedPath)
|
||||||
|
// .then(() => true)
|
||||||
|
// .catch(() => false)
|
||||||
|
// expect(exists).toBe(true)
|
||||||
|
|
||||||
|
// const content = await fs.readFile(nestedPath, "utf-8")
|
||||||
|
// expect(content).toBe("Deep nested content")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should generate proper unified diff in metadata", async () => {
|
||||||
|
// await using fixture = await tmpdir()
|
||||||
|
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: fixture.path,
|
||||||
|
// fn: async () => {
|
||||||
|
// // First create a file with simple content
|
||||||
|
// const patchText1 = `*** Begin Patch
|
||||||
|
// *** Add File: test.txt
|
||||||
|
// +line 1
|
||||||
|
// +line 2
|
||||||
|
// +line 3
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// await patchTool.execute({ patchText: patchText1 }, ctx)
|
||||||
|
|
||||||
|
// // Now create an update patch
|
||||||
|
// const patchText2 = `*** Begin Patch
|
||||||
|
// *** Update File: test.txt
|
||||||
|
// @@
|
||||||
|
// line 1
|
||||||
|
// -line 2
|
||||||
|
// +line 2 updated
|
||||||
|
// line 3
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// const result = await patchTool.execute({ patchText: patchText2 }, ctx)
|
||||||
|
|
||||||
|
// expect(result.metadata.diff).toBeDefined()
|
||||||
|
// expect(result.metadata.diff).toContain("@@")
|
||||||
|
// expect(result.metadata.diff).toContain("-line 2")
|
||||||
|
// expect(result.metadata.diff).toContain("+line 2 updated")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("should handle complex patch with multiple operations", async () => {
|
||||||
|
// await using fixture = await tmpdir()
|
||||||
|
|
||||||
|
// await Instance.provide({
|
||||||
|
// directory: fixture.path,
|
||||||
|
// fn: async () => {
|
||||||
|
// const patchText = `*** Begin Patch
|
||||||
|
// *** Add File: new.txt
|
||||||
|
// +This is a new file
|
||||||
|
// +with multiple lines
|
||||||
|
// *** Add File: existing.txt
|
||||||
|
// +old content
|
||||||
|
// +new line
|
||||||
|
// +more content
|
||||||
|
// *** Add File: config.json
|
||||||
|
// +{
|
||||||
|
// + "version": "1.0",
|
||||||
|
// + "debug": true
|
||||||
|
// +}
|
||||||
|
// *** End Patch`
|
||||||
|
|
||||||
|
// const result = await patchTool.execute({ patchText }, ctx)
|
||||||
|
|
||||||
|
// expect(result.title).toContain("3 files changed")
|
||||||
|
// expect(result.metadata.diff).toBeDefined()
|
||||||
|
// expect(result.output).toContain("Patch applied successfully")
|
||||||
|
|
||||||
|
// // Verify all files were created
|
||||||
|
// const newPath = path.join(fixture.path, "new.txt")
|
||||||
|
// const newContent = await fs.readFile(newPath, "utf-8")
|
||||||
|
// expect(newContent).toBe("This is a new file\nwith multiple lines")
|
||||||
|
|
||||||
|
// const existingPath = path.join(fixture.path, "existing.txt")
|
||||||
|
// const existingContent = await fs.readFile(existingPath, "utf-8")
|
||||||
|
// expect(existingContent).toBe("old content\nnew line\nmore content")
|
||||||
|
|
||||||
|
// const configPath = path.join(fixture.path, "config.json")
|
||||||
|
// const configContent = await fs.readFile(configPath, "utf-8")
|
||||||
|
// expect(configContent).toBe('{\n "version": "1.0",\n "debug": true\n}')
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
@ -1,261 +0,0 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import path from "path"
|
|
||||||
import { PatchTool } from "../../src/tool/patch"
|
|
||||||
import { Instance } from "../../src/project/instance"
|
|
||||||
import { tmpdir } from "../fixture/fixture"
|
|
||||||
import { PermissionNext } from "../../src/permission/next"
|
|
||||||
import * as fs from "fs/promises"
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
sessionID: "test",
|
|
||||||
messageID: "",
|
|
||||||
callID: "",
|
|
||||||
agent: "build",
|
|
||||||
abort: AbortSignal.any([]),
|
|
||||||
metadata: () => {},
|
|
||||||
ask: async () => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchTool = await PatchTool.init()
|
|
||||||
|
|
||||||
describe("tool.patch", () => {
|
|
||||||
test("should validate required parameters", async () => {
|
|
||||||
await Instance.provide({
|
|
||||||
directory: "/tmp",
|
|
||||||
fn: async () => {
|
|
||||||
expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should validate patch format", async () => {
|
|
||||||
await Instance.provide({
|
|
||||||
directory: "/tmp",
|
|
||||||
fn: async () => {
|
|
||||||
expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should handle empty patch", async () => {
|
|
||||||
await Instance.provide({
|
|
||||||
directory: "/tmp",
|
|
||||||
fn: async () => {
|
|
||||||
const emptyPatch = `*** Begin Patch
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("No file changes found in patch")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test.skip("should ask permission for files outside working directory", async () => {
|
|
||||||
await Instance.provide({
|
|
||||||
directory: "/tmp",
|
|
||||||
fn: async () => {
|
|
||||||
const maliciousPatch = `*** Begin Patch
|
|
||||||
*** Add File: /etc/passwd
|
|
||||||
+malicious content
|
|
||||||
*** End Patch`
|
|
||||||
patchTool.execute({ patchText: maliciousPatch }, ctx)
|
|
||||||
// TODO: this sucks
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
const pending = await PermissionNext.list()
|
|
||||||
expect(pending.find((p) => p.sessionID === ctx.sessionID)).toBeDefined()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should handle simple add file operation", async () => {
|
|
||||||
await using fixture = await tmpdir()
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: fixture.path,
|
|
||||||
fn: async () => {
|
|
||||||
const patchText = `*** Begin Patch
|
|
||||||
*** Add File: test-file.txt
|
|
||||||
+Hello World
|
|
||||||
+This is a test file
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
const result = await patchTool.execute({ patchText }, ctx)
|
|
||||||
|
|
||||||
expect(result.title).toContain("files changed")
|
|
||||||
expect(result.metadata.diff).toBeDefined()
|
|
||||||
expect(result.output).toContain("Patch applied successfully")
|
|
||||||
|
|
||||||
// Verify file was created
|
|
||||||
const filePath = path.join(fixture.path, "test-file.txt")
|
|
||||||
const content = await fs.readFile(filePath, "utf-8")
|
|
||||||
expect(content).toBe("Hello World\nThis is a test file")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should handle file with context update", async () => {
|
|
||||||
await using fixture = await tmpdir()
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: fixture.path,
|
|
||||||
fn: async () => {
|
|
||||||
const patchText = `*** Begin Patch
|
|
||||||
*** Add File: config.js
|
|
||||||
+const API_KEY = "test-key"
|
|
||||||
+const DEBUG = false
|
|
||||||
+const VERSION = "1.0"
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
const result = await patchTool.execute({ patchText }, ctx)
|
|
||||||
|
|
||||||
expect(result.title).toContain("files changed")
|
|
||||||
expect(result.metadata.diff).toBeDefined()
|
|
||||||
expect(result.output).toContain("Patch applied successfully")
|
|
||||||
|
|
||||||
// Verify file was created with correct content
|
|
||||||
const filePath = path.join(fixture.path, "config.js")
|
|
||||||
const content = await fs.readFile(filePath, "utf-8")
|
|
||||||
expect(content).toBe('const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should handle multiple file operations", async () => {
|
|
||||||
await using fixture = await tmpdir()
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: fixture.path,
|
|
||||||
fn: async () => {
|
|
||||||
const patchText = `*** Begin Patch
|
|
||||||
*** Add File: file1.txt
|
|
||||||
+Content of file 1
|
|
||||||
*** Add File: file2.txt
|
|
||||||
+Content of file 2
|
|
||||||
*** Add File: file3.txt
|
|
||||||
+Content of file 3
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
const result = await patchTool.execute({ patchText }, ctx)
|
|
||||||
|
|
||||||
expect(result.title).toContain("3 files changed")
|
|
||||||
expect(result.metadata.diff).toBeDefined()
|
|
||||||
expect(result.output).toContain("Patch applied successfully")
|
|
||||||
|
|
||||||
// Verify all files were created
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
const filePath = path.join(fixture.path, `file${i}.txt`)
|
|
||||||
const content = await fs.readFile(filePath, "utf-8")
|
|
||||||
expect(content).toBe(`Content of file ${i}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should create parent directories when adding nested files", async () => {
|
|
||||||
await using fixture = await tmpdir()
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: fixture.path,
|
|
||||||
fn: async () => {
|
|
||||||
const patchText = `*** Begin Patch
|
|
||||||
*** Add File: deep/nested/file.txt
|
|
||||||
+Deep nested content
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
const result = await patchTool.execute({ patchText }, ctx)
|
|
||||||
|
|
||||||
expect(result.title).toContain("files changed")
|
|
||||||
expect(result.output).toContain("Patch applied successfully")
|
|
||||||
|
|
||||||
// Verify nested file was created
|
|
||||||
const nestedPath = path.join(fixture.path, "deep", "nested", "file.txt")
|
|
||||||
const exists = await fs
|
|
||||||
.access(nestedPath)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
expect(exists).toBe(true)
|
|
||||||
|
|
||||||
const content = await fs.readFile(nestedPath, "utf-8")
|
|
||||||
expect(content).toBe("Deep nested content")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should generate proper unified diff in metadata", async () => {
|
|
||||||
await using fixture = await tmpdir()
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: fixture.path,
|
|
||||||
fn: async () => {
|
|
||||||
// First create a file with simple content
|
|
||||||
const patchText1 = `*** Begin Patch
|
|
||||||
*** Add File: test.txt
|
|
||||||
+line 1
|
|
||||||
+line 2
|
|
||||||
+line 3
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
await patchTool.execute({ patchText: patchText1 }, ctx)
|
|
||||||
|
|
||||||
// Now create an update patch
|
|
||||||
const patchText2 = `*** Begin Patch
|
|
||||||
*** Update File: test.txt
|
|
||||||
@@
|
|
||||||
line 1
|
|
||||||
-line 2
|
|
||||||
+line 2 updated
|
|
||||||
line 3
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
const result = await patchTool.execute({ patchText: patchText2 }, ctx)
|
|
||||||
|
|
||||||
expect(result.metadata.diff).toBeDefined()
|
|
||||||
expect(result.metadata.diff).toContain("@@")
|
|
||||||
expect(result.metadata.diff).toContain("-line 2")
|
|
||||||
expect(result.metadata.diff).toContain("+line 2 updated")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should handle complex patch with multiple operations", async () => {
|
|
||||||
await using fixture = await tmpdir()
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: fixture.path,
|
|
||||||
fn: async () => {
|
|
||||||
const patchText = `*** Begin Patch
|
|
||||||
*** Add File: new.txt
|
|
||||||
+This is a new file
|
|
||||||
+with multiple lines
|
|
||||||
*** Add File: existing.txt
|
|
||||||
+old content
|
|
||||||
+new line
|
|
||||||
+more content
|
|
||||||
*** Add File: config.json
|
|
||||||
+{
|
|
||||||
+ "version": "1.0",
|
|
||||||
+ "debug": true
|
|
||||||
+}
|
|
||||||
*** End Patch`
|
|
||||||
|
|
||||||
const result = await patchTool.execute({ patchText }, ctx)
|
|
||||||
|
|
||||||
expect(result.title).toContain("3 files changed")
|
|
||||||
expect(result.metadata.diff).toBeDefined()
|
|
||||||
expect(result.output).toContain("Patch applied successfully")
|
|
||||||
|
|
||||||
// Verify all files were created
|
|
||||||
const newPath = path.join(fixture.path, "new.txt")
|
|
||||||
const newContent = await fs.readFile(newPath, "utf-8")
|
|
||||||
expect(newContent).toBe("This is a new file\nwith multiple lines")
|
|
||||||
|
|
||||||
const existingPath = path.join(fixture.path, "existing.txt")
|
|
||||||
const existingContent = await fs.readFile(existingPath, "utf-8")
|
|
||||||
expect(existingContent).toBe("old content\nnew line\nmore content")
|
|
||||||
|
|
||||||
const configPath = path.join(fixture.path, "config.json")
|
|
||||||
const configContent = await fs.readFile(configPath, "utf-8")
|
|
||||||
expect(configContent).toBe('{\n "version": "1.0",\n "debug": true\n}')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue