Files
Kilo-Org_kilocode/packages/opencode/test/kilocode/diff-full.test.ts
Mark IJbema 5d5fab36bb fix(cli): repair broken imports and typecheck after opencode v1.14.29 merge
Fixes 257 typecheck errors surfaced after the upstream merge:

- Update imports to new module paths after upstream PR #24554 removed
  module barrel files (@/config, @/session, @/util, etc.) and PR #24309
  renamed @opencode-ai/shared to @opencode-ai/core.
- Migrate loadMode to use Info.zod.safeParse for consistency with
  loadAgents and ConfigCommand (unblocks KilocodeConfig.handleInvalid).
- Drop removed Npm.outdated mock from tests.
- Add missing Config.defaultLayer to bash-permission-metadata test runtime.
- Fix Tips component signature (kilocode drops upstream's 'connected' prop).
- Fix toast.tsx undefined 'duration' reference (should be toastOptions.duration).
- Move filesystem-containment test from deleted shared package to core.
2026-04-30 17:16:31 +02:00

228 lines
8.4 KiB
TypeScript

// kilocode_change - new file
//
// Tests for the git-based diff generator that replaced the JS Myers path.
import { $ } from "bun"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import path from "path"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { DiffFull } from "../../src/kilocode/snapshot/diff-full"
import { Filesystem } from "../../src/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
Log.init({ print: false })
// Mirror the caller's git-wrapping in `snapshot/index.ts`: always disable
// quotepath so non-ASCII filenames come through unescaped.
const gitResult = (dir: string) => (args: string[]) =>
Effect.promise(async () => {
const p = Bun.spawn(["git", "-c", "core.quotepath=false", ...args], {
cwd: dir,
stdout: "pipe",
stderr: "pipe",
})
const [text, stderr] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()])
const code = await p.exited
return { code, text, stderr }
})
const gitText = (dir: string) => (args: string[]) =>
Effect.promise(async () => {
const p = Bun.spawn(["git", "-c", "core.quotepath=false", ...args], {
cwd: dir,
stdout: "pipe",
stderr: "pipe",
})
await p.exited
return await new Response(p.stdout).text()
})
const commit = async (dir: string, message: string) => {
await $`git add -A`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m ${message}`.cwd(dir).quiet()
const head = await $`git rev-parse HEAD`.cwd(dir).quiet()
return head.stdout.toString().trim()
}
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer))
describe("DiffFull.batch", () => {
it.live("produces one patch per modified file", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "a.txt"), "alpha\nbeta\ngamma\n")
await Filesystem.write(path.join(dir, "b.txt"), "one\ntwo\nthree\n")
await Filesystem.write(path.join(dir, "c.txt"), "red\nblue\ngreen\n")
})
const from = yield* Effect.promise(() => commit(dir, "v1"))
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "a.txt"), "alpha\nBETA\ngamma\n")
await Filesystem.write(path.join(dir, "b.txt"), "one\nTWO\nthree\n")
await Filesystem.write(path.join(dir, "c.txt"), "red\nBLUE\ngreen\n")
})
const to = yield* Effect.promise(() => commit(dir, "v2"))
const result = yield* DiffFull.batch(gitResult(dir), from, to, ["a.txt", "b.txt", "c.txt"])
expect(result.size).toBe(3)
expect(result.get("a.txt")).toMatch(/^diff --git /)
expect(result.get("a.txt")).toContain("-beta")
expect(result.get("a.txt")).toContain("+BETA")
expect(result.get("b.txt")).toContain("-two")
expect(result.get("b.txt")).toContain("+TWO")
expect(result.get("c.txt")).toContain("-blue")
expect(result.get("c.txt")).toContain("+BLUE")
}),
)
it.live("tolerates paths with spaces and unicode", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const names = ["my file.txt", "héllo.txt", "a b c.md"]
yield* Effect.promise(async () => {
for (const name of names) {
await Filesystem.write(path.join(dir, name), "before\n")
}
})
const from = yield* Effect.promise(() => commit(dir, "v1"))
yield* Effect.promise(async () => {
for (const name of names) {
await Filesystem.write(path.join(dir, name), "after\n")
}
})
const to = yield* Effect.promise(() => commit(dir, "v2"))
const result = yield* DiffFull.batch(gitResult(dir), from, to, names)
expect(result.size).toBe(names.length)
for (const name of names) {
const patch = result.get(name)
expect(patch).toBeDefined()
expect(patch).toContain("-before")
expect(patch).toContain("+after")
}
}),
)
it.live("returns an empty map without spawning for an empty file list", () =>
Effect.gen(function* () {
let calls = 0
const stub = (_: string[]) =>
Effect.sync(() => {
calls += 1
return { code: 0, text: "", stderr: "" }
})
const result = yield* DiffFull.batch(stub, "from", "to", [])
expect(result.size).toBe(0)
expect(calls).toBe(0)
}),
)
it.live("returns an empty map on git failure", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "a.txt"), "hi\n")
})
yield* Effect.promise(() => commit(dir, "v1"))
const result = yield* DiffFull.batch(gitResult(dir), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "HEAD", [
"a.txt",
])
expect(result.size).toBe(0)
}),
)
it.live(
"chunks 1200 files across three spawns and returns all entries",
() =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const names = Array.from({ length: 1200 }, (_, i) => `f${i.toString().padStart(4, "0")}.txt`)
yield* Effect.promise(async () => {
await Promise.all(names.map((name) => Filesystem.write(path.join(dir, name), "before\n")))
})
const from = yield* Effect.promise(() => commit(dir, "v1"))
yield* Effect.promise(async () => {
await Promise.all(names.map((name) => Filesystem.write(path.join(dir, name), "after\n")))
})
const to = yield* Effect.promise(() => commit(dir, "v2"))
let calls = 0
const counting = (cmd: string[]) => {
calls += 1
return gitResult(dir)(cmd)
}
const result = yield* DiffFull.batch(counting, from, to, names)
expect(result.size).toBe(1200)
expect(calls).toBe(3)
expect(result.get("f0000.txt")).toContain("-before")
expect(result.get("f1199.txt")).toContain("+after")
}),
30_000,
)
})
describe("DiffFull.file", () => {
it.live("returns a structured + unified diff for a modified working-tree file", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "foo.txt"), "line1\nline2\nline3\n")
})
yield* Effect.promise(() => commit(dir, "v1"))
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "foo.txt"), "line1\nCHANGED\nline3\n")
})
const got = yield* DiffFull.file(gitText(dir), "foo.txt")
expect(got).not.toBeNull()
expect(got!.text).toMatch(/^diff --git /)
expect(got!.text).toContain("-line2")
expect(got!.text).toContain("+CHANGED")
expect(got!.patch.oldFileName).toBe("foo.txt")
expect(got!.patch.newFileName).toBe("foo.txt")
expect(got!.patch.hunks.length).toBeGreaterThan(0)
}),
)
it.live("returns null for an unmodified file", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "foo.txt"), "unchanged\n")
})
yield* Effect.promise(() => commit(dir, "v1"))
const got = yield* DiffFull.file(gitText(dir), "foo.txt")
expect(got).toBeNull()
}),
)
it.live(
"handles a 15,000-line file in under 500 ms",
() =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const before = Array.from({ length: 15_000 }, (_, i) => `v1_line_${i}`).join("\n") + "\n"
const after = Array.from({ length: 15_000 }, (_, i) => `v2_line_${i}`).join("\n") + "\n"
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "big.txt"), before)
})
yield* Effect.promise(() => commit(dir, "v1"))
yield* Effect.promise(async () => {
await Filesystem.write(path.join(dir, "big.txt"), after)
})
const start = Date.now()
const got = yield* DiffFull.file(gitText(dir), "big.txt")
const elapsed = Date.now() - start
expect(got).not.toBeNull()
expect(got!.patch.hunks.length).toBeGreaterThan(0)
expect(elapsed).toBeLessThan(500)
}),
10_000,
)
})