Merge branch 'main' into feat/help-all-command

This commit is contained in:
Alex Alecu
2026-04-08 12:07:21 +03:00
70 changed files with 2571 additions and 333 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.1.23",
"version": "7.2.0",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
-2
View File
@@ -31,8 +31,6 @@ export namespace FileIgnore {
"mypy_cache",
".history",
".gradle",
".kilocode", // kilocode_change — ignore legacy local storage (#8379)
".opencode", // kilocode_change — ignore legacy local storage (#8379)
])
const FILES = [
+188 -40
View File
@@ -10,12 +10,14 @@ import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { Scheduler } from "../scheduler"
import * as KiloSnapshot from "../kilocode/snapshot" // kilocode_change
import { Lock } from "../util/lock" // kilocode_change
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
const hour = 60 * 60 * 1000
const prune = "7.days"
export const MAX_DIFF_SIZE = 256 * 1024 // kilocode_change
const MAX_SNAPSHOT_FILE_SIZE = 2 * 1024 * 1024 // kilocode_change — skip files >2MB during snapshot add
export function init() {
Scheduler.register({
@@ -36,6 +38,7 @@ export namespace Snapshot {
.then(() => true)
.catch(() => false)
if (!exists) return
using _lock = await Lock.write(git) // kilocode_change
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}`
.quiet()
@@ -57,6 +60,7 @@ export namespace Snapshot {
const cfg = await Config.get()
if (cfg.snapshot === false) return
const git = await KiloSnapshot.prepare() // kilocode_change
using _lock = await Lock.write(git) // kilocode_change
await add(git)
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
.quiet()
@@ -75,6 +79,7 @@ export namespace Snapshot {
export async function patch(hash: string): Promise<Patch> {
const git = await KiloSnapshot.prepare() // kilocode_change
using _lock = await Lock.write(git) // kilocode_change
await add(git)
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
@@ -103,6 +108,7 @@ export namespace Snapshot {
export async function restore(snapshot: string) {
log.info("restore", { commit: snapshot })
const git = await KiloSnapshot.prepare() // kilocode_change
using _lock = await Lock.write(git) // kilocode_change
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
.quiet()
@@ -119,41 +125,136 @@ export namespace Snapshot {
}
}
export async function revert(patches: Patch[]) {
const files = new Set<string>()
for (const item of patches) {
const git = await KiloSnapshot.prepare() // kilocode_change
for (const file of item.files) {
if (files.has(file)) continue
log.info("reverting", { file, hash: item.hash })
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.exitCode !== 0) {
const relativePath = path.relative(Instance.worktree, file)
const checkTree =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (checkTree.exitCode === 0 && checkTree.text().trim()) {
log.info("file existed in snapshot but checkout failed, keeping", {
file,
})
} else {
log.info("file did not exist in snapshot, deleting", { file })
await fs.unlink(file).catch(() => {})
}
}
files.add(file)
// kilocode_change start — batched revert: group up to 100 files per git checkout (port of upstream #20564)
type RevertOp = { hash: string; file: string; rel: string }
/** Revert a single file: checkout from snapshot or delete if it didn't exist. */
async function revertSingle(git: string, worktree: string, op: RevertOp) {
log.info("reverting", { file: op.file, hash: op.hash })
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${worktree} checkout ${op.hash} -- ${op.file}`
.quiet()
.cwd(worktree)
.nothrow()
if (result.exitCode === 0) return
const tree =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${worktree} ls-tree ${op.hash} -- ${op.rel}`
.quiet()
.cwd(worktree)
.nothrow()
if (tree.exitCode === 0 && tree.text().trim()) {
log.info("file existed in snapshot but checkout failed, keeping", { file: op.file, hash: op.hash })
return
}
log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
await fs.unlink(op.file).catch(() => {})
}
/** Revert a batch of files sharing the same hash. Falls back to single-file on failure. */
async function revertBatch(git: string, worktree: string, batch: RevertOp[]) {
const hash = batch[0]!.hash
// Check which files exist in the snapshot
const tree =
await $`git -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${worktree} ls-tree --name-only ${hash} -- ${batch.map((op) => op.rel)}`
.quiet()
.cwd(worktree)
.nothrow()
if (tree.exitCode !== 0) {
log.info("batched ls-tree failed, falling back to single-file revert", { hash, files: batch.length })
for (const op of batch) await revertSingle(git, worktree, op)
return
}
const existing = new Set(tree.text().trim().split("\n").map((l) => l.trim()).filter(Boolean))
// Checkout files that exist in the snapshot
const toCheckout = batch.filter((op) => existing.has(op.rel))
if (toCheckout.length) {
log.info("reverting", { hash, files: toCheckout.length })
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${worktree} checkout ${hash} -- ${toCheckout.map((op) => op.file)}`
.quiet()
.cwd(worktree)
.nothrow()
if (result.exitCode !== 0) {
log.info("batched checkout failed, falling back to single-file revert", { hash, files: toCheckout.length })
for (const op of batch) await revertSingle(git, worktree, op)
return
}
}
// Delete files that didn't exist in the snapshot
for (const op of batch) {
if (existing.has(op.rel)) continue
log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
await fs.unlink(op.file).catch(() => {})
}
}
/** True when one path is a parent of the other (e.g. "a/b" and "a/b/c"). */
function pathsClash(a: string, b: string) {
return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
}
/** Can this op be added to the current batch? */
function canBatch(batch: RevertOp[], op: RevertOp): boolean {
if (batch.length >= 100) return false
if (op.hash !== batch[0]!.hash) return false
if (batch.some((existing) => pathsClash(existing.rel, op.rel))) return false
return true
}
/**
* Group consecutive ops into batches that share the same hash,
* have no path conflicts, and contain at most 100 files each.
*/
function groupIntoBatches(ops: RevertOp[]): RevertOp[][] {
const batches: RevertOp[][] = []
let batch: RevertOp[] = []
for (const op of ops) {
if (batch.length > 0 && !canBatch(batch, op)) {
batches.push(batch)
batch = []
}
batch.push(op)
}
if (batch.length > 0) batches.push(batch)
return batches
}
export async function revert(patches: Patch[]) {
const git = await KiloSnapshot.prepare() // kilocode_change
using _lock = await Lock.write(git) // kilocode_change
const worktree = Instance.worktree
// Deduplicate files preserving patch order
const ops: RevertOp[] = []
const seen = new Set<string>()
for (const item of patches) {
for (const file of item.files) {
if (seen.has(file)) continue
seen.add(file)
ops.push({ hash: item.hash, file, rel: path.relative(worktree, file).replaceAll("\\", "/") })
}
}
for (const batch of groupIntoBatches(ops)) {
if (batch.length === 1) {
await revertSingle(git, worktree, batch[0]!)
} else {
await revertBatch(git, worktree, batch)
}
}
}
// kilocode_change end
export async function diff(hash: string) {
const git = await KiloSnapshot.prepare() // kilocode_change
using _lock = await Lock.write(git) // kilocode_change
await add(git)
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
@@ -208,10 +309,11 @@ export namespace Snapshot {
diffCache.set(key, pending)
return pending
}
// kilocode_change end
async function diffFullUncached(from: string, to: string): Promise<FileDiff[]> {
const git = await KiloSnapshot.prepare() // kilocode_change
// kilocode_change end
using _lock = await Lock.write(git) // kilocode_change
const result: FileDiff[] = []
const status = new Map<string, "added" | "deleted" | "modified">()
@@ -277,26 +379,72 @@ export namespace Snapshot {
return KiloSnapshot.gitdir() // kilocode_change
}
// kilocode_change start — incremental add: diff-files + ls-files + size filter (port of upstream #17878)
async function add(git: string) {
await syncExclude(git)
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} add .`
const cwd = Instance.directory
const worktree = Instance.worktree
// Run diff-files and ls-files concurrently to find changed + untracked files
const [diffResult, otherResult] = await Promise.all([
$`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${worktree} diff-files --name-only -z -- .`
.quiet()
.cwd(cwd)
.nothrow(),
$`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${worktree} ls-files --others --exclude-standard -z -- .`
.quiet()
.cwd(cwd)
.nothrow(),
])
if (diffResult.exitCode !== 0 || otherResult.exitCode !== 0) {
log.warn("failed to list snapshot files", {
diffCode: diffResult.exitCode,
diffStderr: diffResult.stderr.toString(),
otherCode: otherResult.exitCode,
otherStderr: otherResult.stderr.toString(),
})
return
}
const tracked = diffResult.text().split("\0").filter(Boolean)
const all = Array.from(new Set([...tracked, ...otherResult.text().split("\0").filter(Boolean)]))
if (!all.length) {
await syncExclude(git)
return
}
// Filter out oversized files (>2MB)
const large = (
await Promise.all(
all.map(async (item) => {
const stat = await fs.stat(path.join(cwd, item)).catch(() => null)
return stat?.isFile() && stat.size > MAX_SNAPSHOT_FILE_SIZE ? item : undefined
}),
)
).filter(Boolean) as string[]
await syncExclude(git, large)
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${worktree} add --sparse .`
.quiet()
.cwd(Instance.directory)
.cwd(cwd)
.nothrow()
}
async function syncExclude(git: string) {
async function syncExclude(git: string, largeFiles: string[] = []) {
const file = await excludes()
const target = path.join(git, "info", "exclude")
await fs.mkdir(path.join(git, "info"), { recursive: true })
if (!file) {
await Filesystem.write(target, "")
return
const parts: string[] = []
if (file) {
const text = await Filesystem.readText(file).catch(() => "")
if (text.trim()) parts.push(text.trimEnd())
}
const text = await Filesystem.readText(file).catch(() => "")
await Filesystem.write(target, text)
for (const item of largeFiles) {
parts.push(`/${item.replaceAll("\\", "/")}`)
}
await Filesystem.write(target, parts.length ? parts.join("\n") + "\n" : "")
}
// kilocode_change end
async function excludes() {
const file = await $`git rev-parse --path-format=absolute --git-path info/exclude`
@@ -1178,3 +1178,414 @@ test("diffFull with whitespace changes", async () => {
},
})
})
// ── Tests for snapshot optimizations (upstream #17878, #20564) ────────
test("concurrent track() calls return consistent results", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Snapshot.track()
await Filesystem.write(`${tmp.path}/a.txt`, "concurrent-change")
const results = await Promise.all([
Snapshot.track(),
Snapshot.track(),
Snapshot.track(),
Snapshot.track(),
Snapshot.track(),
])
const hashes = results.filter(Boolean)
expect(hashes.length).toBe(5)
// All concurrent calls must return the same hash
expect(new Set(hashes).size).toBe(1)
},
})
})
test("batch revert with many files sharing same hash", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
// Create enough files to trigger batching (>1 per hash)
for (let i = 0; i < 20; i++) {
await Filesystem.write(`${dir}/file${i}.txt`, `original-${i}`)
}
await $`git add .`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
// Modify all 20 files
for (let i = 0; i < 20; i++) {
await Filesystem.write(`${tmp.path}/file${i}.txt`, `changed-${i}`)
}
// Add 5 new files
for (let i = 0; i < 5; i++) {
await Filesystem.write(`${tmp.path}/new${i}.txt`, `new-${i}`)
}
const patch = await Snapshot.patch(before!)
expect(patch.files.length).toBe(25)
await Snapshot.revert([patch])
// Modified files should be restored
for (let i = 0; i < 20; i++) {
const content = await Filesystem.readText(`${tmp.path}/file${i}.txt`)
expect(content).toBe(`original-${i}`)
}
// New files should be deleted
for (let i = 0; i < 5; i++) {
expect(
await fs
.access(`${tmp.path}/new${i}.txt`)
.then(() => true)
.catch(() => false),
).toBe(false)
}
},
})
})
test("batch revert with files in nested subdirectories", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await fs.mkdir(`${dir}/a/b/c`, { recursive: true })
await fs.mkdir(`${dir}/d/e`, { recursive: true })
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${dir}/a/b/c/file${i}.txt`, `deep-${i}`)
await Filesystem.write(`${dir}/d/e/file${i}.txt`, `other-${i}`)
}
await $`git add .`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${tmp.path}/a/b/c/file${i}.txt`, `modified-${i}`)
await Filesystem.write(`${tmp.path}/d/e/file${i}.txt`, `modified-${i}`)
}
await Snapshot.revert([await Snapshot.patch(before!)])
for (let i = 0; i < 10; i++) {
expect(await Filesystem.readText(`${tmp.path}/a/b/c/file${i}.txt`)).toBe(`deep-${i}`)
expect(await Filesystem.readText(`${tmp.path}/d/e/file${i}.txt`)).toBe(`other-${i}`)
}
},
})
})
test("incremental add skips new files larger than 2MB", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
// Create a file larger than 2MB
const largeContent = "x".repeat(3 * 1024 * 1024)
await Filesystem.write(`${tmp.path}/large.bin`, largeContent)
// Also create a small file
await Filesystem.write(`${tmp.path}/small.txt`, "small change")
const after = await Snapshot.track()
expect(after).toBeTruthy()
const patch = await Snapshot.patch(before!)
// Small file should be tracked, large file should be excluded
const files = patch.files.map((f) => path.basename(f))
expect(files).toContain("small.txt")
expect(files).not.toContain("large.bin")
},
})
})
test("incremental add: tracked files that grow past 2MB are still visible in patch", async () => {
// The 2MB size filter only prevents NEW large files from being added to the
// snapshot index. Already-tracked files that grow past 2MB still appear in
// patch/diff because those compare against the working tree. This matches
// upstream OpenCode behavior.
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Filesystem.write(`${tmp.path}/growing.txt`, "small")
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Filesystem.write(`${tmp.path}/growing.txt`, "x".repeat(3 * 1024 * 1024))
await Filesystem.write(`${tmp.path}/a.txt`, "changed")
const patch = await Snapshot.patch(before!)
const files = patch.files.map((f) => path.basename(f))
expect(files).toContain("a.txt")
expect(files).toContain("growing.txt")
},
})
})
test("concurrent patch() calls return consistent results", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Filesystem.write(`${tmp.path}/a.txt`, "changed")
const results = await Promise.all([
Snapshot.patch(before!),
Snapshot.patch(before!),
Snapshot.patch(before!),
])
// All should report the same changed files
for (const result of results) {
expect(result.files).toContain(fwd(tmp.path, "a.txt"))
}
},
})
})
test("batch revert with mix of modified, new, and deleted files", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
for (let i = 0; i < 15; i++) {
await Filesystem.write(`${dir}/file${i}.txt`, `original-${i}`)
}
await $`git add .`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
// Modify some files
for (let i = 0; i < 5; i++) {
await Filesystem.write(`${tmp.path}/file${i}.txt`, `modified-${i}`)
}
// Delete some files
for (let i = 5; i < 10; i++) {
await fs.unlink(`${tmp.path}/file${i}.txt`)
}
// Add new files
for (let i = 0; i < 5; i++) {
await Filesystem.write(`${tmp.path}/added${i}.txt`, `added-${i}`)
}
await Snapshot.revert([await Snapshot.patch(before!)])
// Modified files restored
for (let i = 0; i < 5; i++) {
expect(await Filesystem.readText(`${tmp.path}/file${i}.txt`)).toBe(`original-${i}`)
}
// Deleted files restored
for (let i = 5; i < 10; i++) {
expect(await Filesystem.readText(`${tmp.path}/file${i}.txt`)).toBe(`original-${i}`)
}
// Untouched files unchanged
for (let i = 10; i < 15; i++) {
expect(await Filesystem.readText(`${tmp.path}/file${i}.txt`)).toBe(`original-${i}`)
}
// New files removed
for (let i = 0; i < 5; i++) {
expect(
await fs
.access(`${tmp.path}/added${i}.txt`)
.then(() => true)
.catch(() => false),
).toBe(false)
}
},
})
})
test("batch revert with multiple patches from different snapshots", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${dir}/file${i}.txt`, `v0-${i}`)
}
await $`git add .`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
// Snapshot A
const snapA = await Snapshot.track()
expect(snapA).toBeTruthy()
// Modify first 5 files
for (let i = 0; i < 5; i++) {
await Filesystem.write(`${tmp.path}/file${i}.txt`, `v1-${i}`)
}
// Snapshot B (captures v1 state)
const snapB = await Snapshot.track()
expect(snapB).toBeTruthy()
// Modify last 5 files
for (let i = 5; i < 10; i++) {
await Filesystem.write(`${tmp.path}/file${i}.txt`, `v2-${i}`)
}
// Revert with patches from two different hashes
const patchA = await Snapshot.patch(snapA!)
const patchB = await Snapshot.patch(snapB!)
// patchA covers files 0-4 (changed in v1) + files 5-9 (changed in v2)
// patchB covers files 5-9 (changed in v2)
// Reverting [patchA, patchB]: patchA's hash wins for all files (first seen)
await Snapshot.revert([patchA, patchB])
// All files should be at v0 (snapA's state)
for (let i = 0; i < 10; i++) {
expect(await Filesystem.readText(`${tmp.path}/file${i}.txt`)).toBe(`v0-${i}`)
}
},
})
})
test("concurrent track calls each produce a valid snapshot", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${dir}/file${i}.txt`, `original-${i}`)
}
await $`git add .`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Snapshot.track() // warm up
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${tmp.path}/file${i}.txt`, `changed-${i}`)
}
// Fire 5 concurrent tracks, then verify each hash is a usable snapshot
const hashes = (await Promise.all([
Snapshot.track(),
Snapshot.track(),
Snapshot.track(),
Snapshot.track(),
Snapshot.track(),
])).filter(Boolean) as string[]
expect(hashes.length).toBe(5)
// Every hash should produce a valid diffFull against itself (empty diff)
for (const hash of hashes) {
const diff = await Snapshot.diffFull(hash, hash)
expect(diff).toEqual([])
}
// Every hash should be usable for restore without error
await Snapshot.restore(hashes[0]!)
for (let i = 0; i < 10; i++) {
expect(await Filesystem.readText(`${tmp.path}/file${i}.txt`)).toBe(`changed-${i}`)
}
},
})
})
test("track after revert produces clean snapshot", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${dir}/file${i}.txt`, `original-${i}`)
}
await $`git add .`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
for (let i = 0; i < 10; i++) {
await Filesystem.write(`${tmp.path}/file${i}.txt`, `changed-${i}`)
}
await Snapshot.revert([await Snapshot.patch(before!)])
// After revert, a new track should match the original snapshot
const after = await Snapshot.track()
expect(after).toBeTruthy()
expect(after).toBe(before)
},
})
})
test("incremental add tracks newly created files", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
// Create several new files in subdirectories
await fs.mkdir(`${tmp.path}/newdir/sub`, { recursive: true })
await Filesystem.write(`${tmp.path}/newdir/one.txt`, "one")
await Filesystem.write(`${tmp.path}/newdir/sub/two.txt`, "two")
await Filesystem.write(`${tmp.path}/three.txt`, "three")
const patch = await Snapshot.patch(before!)
const files = patch.files.map((f) => path.basename(f))
expect(files).toContain("one.txt")
expect(files).toContain("two.txt")
expect(files).toContain("three.txt")
},
})
})
test("incremental add tracks modified and deleted files", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
// Modify one file, delete the other
await Filesystem.write(`${tmp.path}/a.txt`, "modified-a")
await fs.unlink(`${tmp.path}/b.txt`)
const patch = await Snapshot.patch(before!)
const files = patch.files.map((f) => path.basename(f))
expect(files).toContain("a.txt")
expect(files).toContain("b.txt")
},
})
})