5d5fab36bb
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.
228 lines
8.4 KiB
TypeScript
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,
|
|
)
|
|
})
|