From a84826061dbe6b1af37828d5e3ab5a564ab881f7 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 21 Oct 2025 15:04:22 +0000 Subject: [PATCH 1/2] release: v0.15.11 --- bun.lock | 22 +++++++++++----------- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index 69a5e3e9f..32cbea998 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -64,7 +64,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -88,7 +88,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -109,7 +109,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -147,7 +147,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -163,7 +163,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.15.10", + "version": "0.15.11", "bin": { "opencode": "./bin/opencode", }, @@ -226,7 +226,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -246,7 +246,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.15.10", + "version": "0.15.11", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -257,7 +257,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -270,7 +270,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@kobalte/core": "catalog:", "@solidjs/meta": "catalog:", @@ -291,7 +291,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 1c638e5cd..42d559791 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.15.10" + "version": "0.15.11" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 835f88911..bb182180d 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.15.10", + "version": "0.15.11", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6d5ef7181..20f372484 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.15.10", + "version": "0.15.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 5fbc12051..843c1c0e4 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "0.15.10", + "version": "0.15.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index a6df47e13..7f8521739 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.15.10", + "version": "0.15.11", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index 81e418c96..4c96a36b1 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.15.10", + "version": "0.15.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index de52826fe..01b2495c5 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.15.10", + "version": "0.15.11", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c7960112f..1427bb902 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.15.10", + "version": "0.15.11", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 1e8a38115..e4c8a6275 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.15.10", + "version": "0.15.11", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/slack/package.json b/packages/slack/package.json index 0da1175fb..20e7623bf 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "0.15.10", + "version": "0.15.11", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 576db85ae..8f0ea019c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "0.15.10", + "version": "0.15.11", "type": "module", "exports": { ".": "./src/components/index.ts", diff --git a/packages/web/package.json b/packages/web/package.json index eb260f13f..e1d606cf8 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.15.10", + "version": "0.15.11", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index d477908ca..4fed5b75e 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.15.10", + "version": "0.15.11", "publisher": "sst-dev", "repository": { "type": "git", From e9996342a7b9c8ef3144dcb7d25c4832a0b2b6c6 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 21 Oct 2025 11:54:18 -0400 Subject: [PATCH 2/2] core: provide line-level statistics in file diffs to help users understand the scale of changes --- packages/opencode/src/session/index.ts | 11 +- packages/opencode/src/snapshot/index.ts | 88 +++--- .../opencode/test/snapshot/snapshot.test.ts | 262 ++++++++++++++---- 3 files changed, 246 insertions(+), 115 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 9e7f3db13..23b97077b 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -147,12 +147,7 @@ export namespace Session { }) }) - export async function createNext(input: { - id?: string - title?: string - parentID?: string - directory: string - }) { + export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) { const result: Info = { id: Identifier.descending("session", input.id), version: Installation.VERSION, @@ -372,9 +367,7 @@ export namespace Session { .add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000)) .add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000)) .add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000)) - .add( - new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000), - ) + .add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000)) .toNumber(), tokens, } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 6a363a6f1..4301694a8 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -26,15 +26,8 @@ export namespace Snapshot { .nothrow() log.info("initialized") } - await $`git --git-dir ${git} add .` - .quiet() - .cwd(Instance.directory) - .nothrow() - const hash = await $`git --git-dir ${git} write-tree` - .quiet() - .cwd(Instance.directory) - .nothrow() - .text() + await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() + const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text() log.info("tracking", { hash, cwd: Instance.directory, git }) return hash.trim() } @@ -47,14 +40,8 @@ export namespace Snapshot { export async function patch(hash: string): Promise { const git = gitdir() - await $`git --git-dir ${git} add .` - .quiet() - .cwd(Instance.directory) - .nothrow() - const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .` - .quiet() - .cwd(Instance.directory) - .nothrow() + await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() + const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.quiet().cwd(Instance.directory).nothrow() // If git diff fails, return empty patch if (result.exitCode !== 0) { @@ -77,11 +64,10 @@ export namespace Snapshot { export async function restore(snapshot: string) { log.info("restore", { commit: snapshot }) const git = gitdir() - const result = - await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` - .quiet() - .cwd(Instance.worktree) - .nothrow() + const result = await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` + .quiet() + .cwd(Instance.worktree) + .nothrow() if (result.exitCode !== 0) { log.error("failed to restore snapshot", { @@ -100,18 +86,16 @@ export namespace Snapshot { for (const file of item.files) { if (files.has(file)) continue log.info("reverting", { file, hash: item.hash }) - const result = - await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` + const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` + .quiet() + .cwd(Instance.worktree) + .nothrow() + if (result.exitCode !== 0) { + const relativePath = path.relative(Instance.worktree, file) + const checkTree = await $`git --git-dir=${git} ls-tree ${item.hash} -- ${relativePath}` .quiet() .cwd(Instance.worktree) .nothrow() - if (result.exitCode !== 0) { - const relativePath = path.relative(Instance.worktree, file) - const checkTree = - await $`git --git-dir=${git} 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, @@ -128,14 +112,8 @@ export namespace Snapshot { export async function diff(hash: string) { const git = gitdir() - await $`git --git-dir ${git} add .` - .quiet() - .cwd(Instance.directory) - .nothrow() - const result = await $`git --git-dir=${git} diff ${hash} -- .` - .quiet() - .cwd(Instance.worktree) - .nothrow() + await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() + const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).nothrow() if (result.exitCode !== 0) { log.warn("failed to get diff", { @@ -153,37 +131,33 @@ export namespace Snapshot { export const FileDiff = z .object({ file: z.string(), - left: z.string(), - right: z.string(), + before: z.string(), + after: z.string(), + additions: z.number(), + deletions: z.number(), }) .meta({ ref: "FileDiff", }) export type FileDiff = z.infer - export async function diffFull( - from: string, - to: string, - ): Promise { + export async function diffFull(from: string, to: string): Promise { const git = gitdir() const result: FileDiff[] = [] - for await (const line of $`git --git-dir=${git} diff --name-only ${from} ${to} -- .` + for await (const line of $`git --git-dir=${git} diff --numstat ${from} ${to} -- .` .quiet() .cwd(Instance.directory) .nothrow() .lines()) { if (!line) continue - const left = await $`git --git-dir=${git} show ${from}:${line}` - .quiet() - .nothrow() - .text() - const right = await $`git --git-dir=${git} show ${to}:${line}` - .quiet() - .nothrow() - .text() + const [additions, deletions, file] = line.split("\t") + const before = await $`git --git-dir=${git} show ${from}:${file}`.quiet().nothrow().text() + const after = await $`git --git-dir=${git} show ${to}:${file}`.quiet().nothrow().text() result.push({ - file: line, - left, - right, + file, + before, + after, + additions: parseInt(additions), + deletions: parseInt(deletions), }) } return result diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index a9b65a149..b72717cd1 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -33,9 +33,7 @@ test("tracks deleted files correctly", async () => { await $`rm ${tmp.path}/a.txt`.quiet() - expect((await Snapshot.patch(before!)).files).toContain( - `${tmp.path}/a.txt`, - ) + expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/a.txt`) }, }) }) @@ -93,15 +91,11 @@ test("multiple file operations", async () => { await Snapshot.revert([await Snapshot.patch(before!)]) - expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe( - tmp.extra.aContent, - ) + expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(tmp.extra.aContent) expect(await Bun.file(`${tmp.path}/c.txt`).exists()).toBe(false) // Note: revert currently only removes files, not directories // The empty directory will remain - expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe( - tmp.extra.bContent, - ) + expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(tmp.extra.bContent) }, }) }) @@ -129,10 +123,7 @@ test("binary file handling", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write( - `${tmp.path}/image.png`, - new Uint8Array([0x89, 0x50, 0x4e, 0x47]), - ) + await Bun.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47])) const patch = await Snapshot.patch(before!) expect(patch.files).toContain(`${tmp.path}/image.png`) @@ -153,9 +144,7 @@ test("symlink handling", async () => { await $`ln -s ${tmp.path}/a.txt ${tmp.path}/link.txt`.quiet() - expect((await Snapshot.patch(before!)).files).toContain( - `${tmp.path}/link.txt`, - ) + expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/link.txt`) }, }) }) @@ -170,9 +159,7 @@ test("large file handling", async () => { await Bun.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024)) - expect((await Snapshot.patch(before!)).files).toContain( - `${tmp.path}/large.txt`, - ) + expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`) }, }) }) @@ -190,9 +177,7 @@ test("nested directory revert", async () => { await Snapshot.revert([await Snapshot.patch(before!)]) - expect( - await Bun.file(`${tmp.path}/level1/level2/level3/deep.txt`).exists(), - ).toBe(false) + expect(await Bun.file(`${tmp.path}/level1/level2/level3/deep.txt`).exists()).toBe(false) }, }) }) @@ -226,9 +211,7 @@ test("revert with empty patches", async () => { expect(Snapshot.revert([])).resolves.toBeUndefined() // Should not crash with patches that have empty file lists - expect( - Snapshot.revert([{ hash: "dummy", files: [] }]), - ).resolves.toBeUndefined() + expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined() }, }) }) @@ -543,13 +526,9 @@ test("restore function", async () => { await Snapshot.restore(before!) expect(await Bun.file(`${tmp.path}/a.txt`).exists()).toBe(true) - expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe( - tmp.extra.aContent, - ) + expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(tmp.extra.aContent) expect(await Bun.file(`${tmp.path}/new.txt`).exists()).toBe(true) // New files should remain - expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe( - tmp.extra.bContent, - ) + expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(tmp.extra.bContent) }, }) }) @@ -601,14 +580,12 @@ test("revert preserves file that existed in snapshot when deleted then recreated expect(await Bun.file(`${tmp.path}/newfile.txt`).exists()).toBe(false) expect(await Bun.file(`${tmp.path}/existing.txt`).exists()).toBe(true) - expect(await Bun.file(`${tmp.path}/existing.txt`).text()).toBe( - "original content", - ) + expect(await Bun.file(`${tmp.path}/existing.txt`).text()).toBe("original content") }, }) }) -test("diffFull function", async () => { +test("diffFull with new file additions", async () => { await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, @@ -617,26 +594,103 @@ test("diffFull function", async () => { expect(before).toBeTruthy() await Bun.write(`${tmp.path}/new.txt`, "new content") + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(1) + + const newFileDiff = diffs[0] + expect(newFileDiff.file).toBe("new.txt") + expect(newFileDiff.before).toBe("") + expect(newFileDiff.after).toBe("new content") + expect(newFileDiff.additions).toBe(1) + expect(newFileDiff.deletions).toBe(0) + }, + }) +}) + +test("diffFull with file modifications", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + await Bun.write(`${tmp.path}/b.txt`, "modified content") const after = await Snapshot.track() expect(after).toBeTruthy() const diffs = await Snapshot.diffFull(before!, after!) - expect(diffs.length).toBe(2) + expect(diffs.length).toBe(1) - const newFileDiff = diffs.find((d) => d.file === "new.txt") - expect(newFileDiff).toBeDefined() - expect(newFileDiff!.left).toBe("") - expect(newFileDiff!.right).toBe("new content") - - const modifiedFileDiff = diffs.find((d) => d.file === "b.txt") - expect(modifiedFileDiff).toBeDefined() - expect(modifiedFileDiff!.left).toBe(tmp.extra.bContent) - expect(modifiedFileDiff!.right).toBe("modified content") + const modifiedFileDiff = diffs[0] + expect(modifiedFileDiff.file).toBe("b.txt") + expect(modifiedFileDiff.before).toBe(tmp.extra.bContent) + expect(modifiedFileDiff.after).toBe("modified content") + expect(modifiedFileDiff.additions).toBeGreaterThan(0) + expect(modifiedFileDiff.deletions).toBeGreaterThan(0) }, }) +}) +test("diffFull with file deletions", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await $`rm ${tmp.path}/a.txt`.quiet() + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(1) + + const removedFileDiff = diffs[0] + expect(removedFileDiff.file).toBe("a.txt") + expect(removedFileDiff.before).toBe(tmp.extra.aContent) + expect(removedFileDiff.after).toBe("") + expect(removedFileDiff.additions).toBe(0) + expect(removedFileDiff.deletions).toBe(1) + }, + }) +}) + +test("diffFull with multiple line additions", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Bun.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3") + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(1) + + const multiDiff = diffs[0] + expect(multiDiff.file).toBe("multi.txt") + expect(multiDiff.before).toBe("") + expect(multiDiff.after).toBe("line1\nline2\nline3") + expect(multiDiff.additions).toBe(3) + expect(multiDiff.deletions).toBe(0) + }, + }) +}) + +test("diffFull with addition and deletion", async () => { + await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, fn: async () => { @@ -654,13 +708,123 @@ test("diffFull function", async () => { const addedFileDiff = diffs.find((d) => d.file === "added.txt") expect(addedFileDiff).toBeDefined() - expect(addedFileDiff!.left).toBe("") - expect(addedFileDiff!.right).toBe("added content") + expect(addedFileDiff!.before).toBe("") + expect(addedFileDiff!.after).toBe("added content") + expect(addedFileDiff!.additions).toBe(1) + expect(addedFileDiff!.deletions).toBe(0) const removedFileDiff = diffs.find((d) => d.file === "a.txt") expect(removedFileDiff).toBeDefined() - expect(removedFileDiff!.left).toBe(tmp.extra.aContent) - expect(removedFileDiff!.right).toBe("") + expect(removedFileDiff!.before).toBe(tmp.extra.aContent) + expect(removedFileDiff!.after).toBe("") + expect(removedFileDiff!.additions).toBe(0) + expect(removedFileDiff!.deletions).toBe(1) + }, + }) +}) + +test("diffFull with multiple additions and deletions", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Bun.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3") + await Bun.write(`${tmp.path}/multi2.txt`, "single line") + await $`rm ${tmp.path}/a.txt`.quiet() + await $`rm ${tmp.path}/b.txt`.quiet() + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(4) + + const multi1Diff = diffs.find((d) => d.file === "multi1.txt") + expect(multi1Diff).toBeDefined() + expect(multi1Diff!.additions).toBe(3) + expect(multi1Diff!.deletions).toBe(0) + + const multi2Diff = diffs.find((d) => d.file === "multi2.txt") + expect(multi2Diff).toBeDefined() + expect(multi2Diff!.additions).toBe(1) + expect(multi2Diff!.deletions).toBe(0) + + const removedADiff = diffs.find((d) => d.file === "a.txt") + expect(removedADiff).toBeDefined() + expect(removedADiff!.additions).toBe(0) + expect(removedADiff!.deletions).toBe(1) + + const removedBDiff = diffs.find((d) => d.file === "b.txt") + expect(removedBDiff).toBeDefined() + expect(removedBDiff!.additions).toBe(0) + expect(removedBDiff!.deletions).toBe(1) + }, + }) +}) + +test("diffFull with no changes", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(0) + }, + }) +}) + +test("diffFull with binary file changes", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Bun.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03])) + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(1) + + const binaryDiff = diffs[0] + expect(binaryDiff.file).toBe("binary.bin") + expect(binaryDiff.before).toBe("") + }, + }) +}) + +test("diffFull with whitespace changes", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await Bun.write(`${tmp.path}/whitespace.txt`, "line1\nline2") + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Bun.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n") + + const after = await Snapshot.track() + expect(after).toBeTruthy() + + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.length).toBe(1) + + const whitespaceDiff = diffs[0] + expect(whitespaceDiff.file).toBe("whitespace.txt") + expect(whitespaceDiff.additions).toBeGreaterThan(0) }, }) })