| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- 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 * as fs from "fs/promises"
- const ctx = {
- sessionID: "test",
- messageID: "",
- toolCallID: "",
- agent: "build",
- abort: AbortSignal.any([]),
- metadata: () => {},
- }
- const patchTool = await PatchTool.init()
- describe("tool.patch", () => {
- test("should validate required parameters", async () => {
- await Instance.provide({
- directory: "/tmp",
- fn: async () => {
- await expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow(
- "patchText is required",
- )
- },
- })
- })
- test("should validate patch format", async () => {
- await Instance.provide({
- directory: "/tmp",
- fn: async () => {
- await 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`
- await expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow(
- "No file changes found in patch",
- )
- },
- })
- })
- test("should reject files outside working directory", async () => {
- await Instance.provide({
- directory: "/tmp",
- fn: async () => {
- const maliciousPatch = `*** Begin Patch
- *** Add File: /etc/passwd
- +malicious content
- *** End Patch`
- await expect(patchTool.execute({ patchText: maliciousPatch }, ctx)).rejects.toThrow(
- "is not in the current working directory",
- )
- },
- })
- })
- 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}')
- },
- })
- })
- })
|