Files
Kilo-Org_kilocode/packages/opencode/test/kilocode/worktree-diff.test.ts
T
Marius 9dd1f631b4 revert: restore patch fields for diff compatibility (#8970)
Reverts #8967 and #8953. OpenCode is migrating to use patch strings as
the primary diff transport — dropping them breaks forward compatibility
with upstream's SessionReview rendering pipeline, which falls back to
patch when before/after are absent.
2026-04-15 13:05:44 +02:00

224 lines
8.1 KiB
TypeScript

import { test, expect, describe } from "bun:test"
import { $ } from "bun"
import { tmpdir } from "../fixture/fixture"
import path from "path"
/**
* Tests for the worktree diff logic used by GET /experimental/worktree/diff.
* Reproduces the exact git commands from the endpoint to verify they work
* for tracked, staged, and untracked files.
*/
describe("worktree diff git commands", () => {
async function setupRepo() {
const tmp = await tmpdir({
git: true,
init: async (dir) => {
// Create an initial file and commit it so we have a base
await Bun.write(path.join(dir, "existing.txt"), "hello\n")
await $`git add .`.cwd(dir).quiet()
await $`git commit -m "add existing.txt"`.cwd(dir).quiet()
},
})
return tmp
}
test("git diff sees committed changes but NOT untracked files", async () => {
await using tmp = await setupRepo()
const dir = tmp.path
// Get the current HEAD as our "ancestor" (simulating merge-base)
const headResult = await $`git rev-parse HEAD`.cwd(dir).quiet()
const ancestor = headResult.stdout.toString().trim()
// Create an untracked file (agent writes a file but doesn't stage it)
await Bun.write(path.join(dir, "life.py"), 'print("hello world")\n')
// Verify the file exists
const exists = await Bun.file(path.join(dir, "life.py")).exists()
expect(exists).toBe(true)
// git diff --name-status does NOT see untracked files
const nameStatus = await $`git -c core.quotepath=false diff --name-status --no-renames ${ancestor}`
.cwd(dir)
.quiet()
.nothrow()
const nameStatusOutput = nameStatus.stdout.toString().trim()
console.log("git diff --name-status output:", JSON.stringify(nameStatusOutput))
expect(nameStatusOutput).toBe("") // empty — life.py is untracked
// git ls-files --others DOES see untracked files
const untracked = await $`git ls-files --others --exclude-standard`.cwd(dir).quiet().nothrow()
const untrackedOutput = untracked.stdout.toString().trim()
console.log("git ls-files --others output:", JSON.stringify(untrackedOutput))
expect(untrackedOutput).toContain("life.py")
})
test("git diff sees staged (added) files", async () => {
await using tmp = await setupRepo()
const dir = tmp.path
const headResult = await $`git rev-parse HEAD`.cwd(dir).quiet()
const ancestor = headResult.stdout.toString().trim()
// Create and stage a new file
await Bun.write(path.join(dir, "staged.py"), 'print("staged")\n')
await $`git add staged.py`.cwd(dir).quiet()
const nameStatus = await $`git -c core.quotepath=false diff --name-status --no-renames ${ancestor}`
.cwd(dir)
.quiet()
.nothrow()
const nameStatusOutput = nameStatus.stdout.toString().trim()
console.log("git diff --name-status (staged):", JSON.stringify(nameStatusOutput))
// git diff <ancestor> (no --cached) compares ancestor to working tree,
// which includes staged changes
expect(nameStatusOutput).toContain("staged.py")
})
test("git diff sees modifications to tracked files", async () => {
await using tmp = await setupRepo()
const dir = tmp.path
const headResult = await $`git rev-parse HEAD`.cwd(dir).quiet()
const ancestor = headResult.stdout.toString().trim()
// Modify existing tracked file without staging
await Bun.write(path.join(dir, "existing.txt"), "hello\nmodified\n")
const nameStatus = await $`git -c core.quotepath=false diff --name-status --no-renames ${ancestor}`
.cwd(dir)
.quiet()
.nothrow()
const nameStatusOutput = nameStatus.stdout.toString().trim()
console.log("git diff --name-status (modified):", JSON.stringify(nameStatusOutput))
expect(nameStatusOutput).toContain("existing.txt")
})
test("full diff pipeline: tracked + untracked combined", async () => {
await using tmp = await setupRepo()
const dir = tmp.path
const headResult = await $`git rev-parse HEAD`.cwd(dir).quiet()
const ancestor = headResult.stdout.toString().trim()
// Modify existing file (tracked change)
await Bun.write(path.join(dir, "existing.txt"), "hello\nmodified\n")
// Create untracked file
await Bun.write(path.join(dir, "new-file.py"), 'print("new")\n')
// Step 1: git diff for tracked changes
const nameStatus = await $`git -c core.quotepath=false diff --name-status --no-renames ${ancestor}`
.cwd(dir)
.quiet()
.nothrow()
const tracked = new Set<string>()
const trackedFiles: string[] = []
for (const line of nameStatus.stdout.toString().trim().split("\n")) {
if (!line) continue
const parts = line.split("\t")
const file = parts.slice(1).join("\t")
if (file) {
tracked.add(file)
trackedFiles.push(file)
}
}
// Step 2: git ls-files for untracked
const untrackedResult = await $`git ls-files --others --exclude-standard`.cwd(dir).quiet().nothrow()
const untrackedFiles: string[] = []
for (const file of untrackedResult.stdout.toString().trim().split("\n")) {
if (!file || tracked.has(file)) continue
untrackedFiles.push(file)
}
console.log("tracked files:", trackedFiles)
console.log("untracked files:", untrackedFiles)
expect(trackedFiles).toContain("existing.txt")
expect(trackedFiles).not.toContain("new-file.py")
expect(untrackedFiles).toContain("new-file.py")
expect(untrackedFiles).not.toContain("existing.txt")
// Combined = both
const allFiles = [...trackedFiles, ...untrackedFiles]
expect(allFiles).toContain("existing.txt")
expect(allFiles).toContain("new-file.py")
})
test("worktree scenario: branch with no new commits, only untracked files", async () => {
// This is the exact scenario from the screenshot:
// - Worktree created from main
// - Agent writes life.py (never committed/staged)
// - merge-base HEAD main = HEAD (no divergence)
// - git diff shows nothing, git ls-files --others shows life.py
await using tmp = await setupRepo()
const dir = tmp.path
// Simulate: worktree is on same commit as base (no new commits)
// merge-base HEAD HEAD = HEAD
const mergeBase = await $`git merge-base HEAD HEAD`.cwd(dir).quiet()
const ancestor = mergeBase.stdout.toString().trim()
console.log("ancestor (same as HEAD):", ancestor)
// Agent writes a file
await Bun.write(
path.join(dir, "life.py"),
`
import random
import time
import os
def create_board(rows, cols):
return [[random.choice([0, 1]) for _ in range(cols)] for _ in range(rows)]
print("Game of Life")
`,
)
// git diff: nothing (HEAD == ancestor, no tracked changes)
const nameStatus = await $`git -c core.quotepath=false diff --name-status --no-renames ${ancestor}`
.cwd(dir)
.quiet()
.nothrow()
const nameStatusRaw = nameStatus.stdout.toString().trim()
console.log("nameStatus raw:", JSON.stringify(nameStatusRaw))
// git ls-files --others: should find life.py
const untrackedResult = await $`git ls-files --others --exclude-standard`.cwd(dir).quiet().nothrow()
const untrackedRaw = untrackedResult.stdout.toString().trim()
console.log("untracked raw:", JSON.stringify(untrackedRaw))
expect(untrackedRaw).toContain("life.py")
// Now simulate the full endpoint logic
const seen = new Set<string>()
const diffs: { file: string; status: string }[] = []
// Process tracked changes
for (const line of nameStatusRaw.split("\n")) {
if (!line) continue
const parts = line.split("\t")
const file = parts.slice(1).join("\t")
if (file) {
seen.add(file)
diffs.push({ file, status: "modified" })
}
}
// Process untracked files
if (untrackedResult.exitCode === 0) {
for (const file of untrackedRaw.split("\n")) {
if (!file || seen.has(file)) continue
const f = Bun.file(path.join(dir, file))
if (!(await f.exists())) continue
diffs.push({ file, status: "added" })
}
}
console.log("final diffs:", diffs)
expect(diffs.length).toBe(1)
expect(diffs[0]!.file).toBe("life.py")
expect(diffs[0]!.status).toBe("added")
})
})