diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts index e4ea0d6cebd..bd6fdcad3b4 120000 --- a/packages/app/src/custom-elements.d.ts +++ b/packages/app/src/custom-elements.d.ts @@ -1 +1,2 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +/// +export {} diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts index e4ea0d6cebd..bd6fdcad3b4 120000 --- a/packages/enterprise/src/custom-elements.d.ts +++ b/packages/enterprise/src/custom-elements.d.ts @@ -1 +1,2 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +/// +export {} diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7b6f028fcd9..79b1b395e63 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -7,6 +7,151 @@ import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { existsSync } from "fs" + +type PaneSource = "option" | "positional" + +const PANE_COUNTS = new Set([2, 4, 6]) + +function parsePaneCount(value: string | number | undefined): number | null { + if (value === undefined || value === null) return null + const count = typeof value === "number" ? value : Number(value) + if (!Number.isInteger(count)) return null + return PANE_COUNTS.has(count) ? count : null +} + +function buildChildArgs( + rawArgs: string[], + paneCount: number, + paneSource: PaneSource, +): string[] { + const result: string[] = [] + let removedPositional = false + let skipNext = false + let seenDoubleDash = false + const paneString = String(paneCount) + + for (const arg of rawArgs) { + if (skipNext) { + skipNext = false + continue + } + if (arg === "--") { + seenDoubleDash = true + result.push(arg) + continue + } + if (!seenDoubleDash && paneSource === "option") { + if (arg === "--panes") { + skipNext = true + continue + } + if (arg.startsWith("--panes=")) { + continue + } + } + if (!seenDoubleDash && paneSource === "positional" && !removedPositional && !arg.startsWith("-") && arg === paneString) { + removedPositional = true + continue + } + result.push(arg) + } + + return result +} + +function buildSplitCommand( + direction: "V" | "H", + size: string, + cwd: string, + profileId: string | undefined, + childArgs: string[], +): string[] { + const args = ["split-pane", `-${direction}`, "--size", size] + if (profileId) args.push("-p", profileId) + args.push("-d", cwd, "opencode", ...childArgs) + return args +} + +function buildPaneLayout( + paneCount: number, + cwd: string, + profileId: string | undefined, + childArgs: string[], +): string[] | null { + const splitV33 = buildSplitCommand("V", "0.33", cwd, profileId, childArgs) + const splitV50 = buildSplitCommand("V", "0.5", cwd, profileId, childArgs) + const splitH50 = buildSplitCommand("H", "0.5", cwd, profileId, childArgs) + + if (paneCount === 2) { + return ["-w", "0", ...splitV50] + } + if (paneCount === 4) { + return [ + "-w", + "0", + ...splitV50, + ";", + "move-focus", + "left", + ";", + ...splitH50, + ";", + "move-focus", + "right", + ";", + ...splitH50, + ] + } + if (paneCount === 6) { + return [ + "-w", + "0", + ...splitV33, + ";", + ...splitV50, + ";", + ...splitH50, + ";", + "move-focus", + "left", + ";", + ...splitH50, + ";", + "move-focus", + "left", + ";", + ...splitH50, + ] + } + return null +} + +function splitWindowsTerminalPanes( + paneCount: number, + cwd: string, + childArgs: string[], +): boolean { + if (process.platform !== "win32") return false + if (!process.env.WT_SESSION) return false + const wt = Bun.which("wt") ?? Bun.which("wt.exe") + if (!wt) return false + + const profileId = process.env.WT_PROFILE_ID + const layout = buildPaneLayout(paneCount, cwd, profileId, childArgs) + if (!layout) return false + + try { + Bun.spawn({ + cmd: [wt, ...layout], + stdout: "ignore", + stderr: "ignore", + }) + return true + } catch { + return false + } +} declare global { const OPENCODE_WORKER_PATH: string @@ -21,6 +166,10 @@ export const TuiThreadCommand = cmd({ type: "string", describe: "path to start opencode in", }) + .option("panes", { + type: "number", + describe: "open multiple Windows Terminal panes (2, 4, 6)", + }) .option("model", { type: "string", alias: ["m"], @@ -47,7 +196,23 @@ export const TuiThreadCommand = cmd({ handler: async (args) => { // Resolve relative paths against PWD to preserve behavior when using --cwd flag const baseCwd = process.env.PWD ?? process.cwd() - const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd() + const rawArgs = process.argv.slice(2) + const paneFromOption = parsePaneCount(args.panes) + let paneCount = paneFromOption + let paneSource: PaneSource | null = paneFromOption ? "option" : null + let projectArg = args.project + if (!paneCount) { + const fromProject = parsePaneCount(args.project) + if (fromProject) { + const candidate = path.resolve(baseCwd, String(args.project)) + if (!existsSync(candidate)) { + paneCount = fromProject + paneSource = "positional" + projectArg = undefined + } + } + } + const cwd = projectArg ? path.resolve(baseCwd, projectArg) : process.cwd() const localWorker = new URL("./worker.ts", import.meta.url) const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) const workerPath = await iife(async () => { @@ -55,6 +220,11 @@ export const TuiThreadCommand = cmd({ if (await Bun.file(distWorker).exists()) return distWorker return localWorker }) + if (paneCount && paneSource) { + const childArgs = buildChildArgs(rawArgs, paneCount, paneSource) + splitWindowsTerminalPanes(paneCount, cwd, childArgs) + } + try { process.chdir(cwd) } catch (e) { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ead3a0149b4..f6def884c0b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1,6 +1,7 @@ import { Log } from "../util/log" import path from "path" import { pathToFileURL } from "url" +import { createRequire } from "module" import os from "os" import z from "zod" import { Filesystem } from "../util/filesystem" @@ -23,6 +24,24 @@ import { existsSync } from "fs" export namespace Config { const log = Log.create({ service: "config" }) + function resolvePluginSpecifier(plugin: string, configFilepath: string) { + const baseUrl = pathToFileURL(configFilepath).href + try { + return import.meta.resolve(plugin, baseUrl) + } catch {} + try { + const req = createRequire(configFilepath) + return pathToFileURL(req.resolve(plugin)).href + } catch {} + try { + const resolved = Bun.resolveSync(plugin, configFilepath) + return resolved.startsWith("file://") + ? resolved + : pathToFileURL(resolved).href + } catch {} + return null + } + // Custom merge function that concatenates array fields instead of replacing them function mergeConfigConcatArrays(target: Info, source: Info): Info { const merged = mergeDeep(target, source) @@ -1170,9 +1189,8 @@ export namespace Config { if (data.plugin) { for (let i = 0; i < data.plugin.length; i++) { const plugin = data.plugin[i] - try { - data.plugin[i] = import.meta.resolve!(plugin, configFilepath) - } catch (err) {} + const resolved = resolvePluginSpecifier(plugin, configFilepath) + if (resolved) data.plugin[i] = resolved } } return data diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 7230f67afeb..5beda1b8fbc 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,5 +1,3 @@ -import { sep } from "node:path" - export namespace FileIgnore { const FOLDERS = new Set([ "node_modules", @@ -64,18 +62,19 @@ export namespace FileIgnore { whitelist?: Bun.Glob[] }, ) { + const normalized = filepath.replaceAll("\\", "/") for (const glob of opts?.whitelist || []) { - if (glob.match(filepath)) return false + if (glob.match(normalized)) return false } - const parts = filepath.split(sep) + const parts = normalized.split("/") for (let i = 0; i < parts.length; i++) { if (FOLDERS.has(parts[i])) return true } const extra = opts?.extra || [] for (const glob of [...FILE_GLOBS, ...extra]) { - if (glob.match(filepath)) return true + if (glob.match(normalized)) return true } return false diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index 91d52065f6f..b2e107c3e69 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -76,25 +76,29 @@ export namespace Patch { startIdx: number, ): { filePath: string; movePath?: string; nextIdx: number } | null { const line = lines[startIdx] + const addPrefix = "*** Add File:" + const deletePrefix = "*** Delete File:" + const updatePrefix = "*** Update File:" + const movePrefix = "*** Move to:" - if (line.startsWith("*** Add File:")) { - const filePath = line.split(":", 2)[1]?.trim() + if (line.startsWith(addPrefix)) { + const filePath = line.slice(addPrefix.length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } - if (line.startsWith("*** Delete File:")) { - const filePath = line.split(":", 2)[1]?.trim() + if (line.startsWith(deletePrefix)) { + const filePath = line.slice(deletePrefix.length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } - if (line.startsWith("*** Update File:")) { - const filePath = line.split(":", 2)[1]?.trim() + if (line.startsWith(updatePrefix)) { + const filePath = line.slice(updatePrefix.length).trim() let movePath: string | undefined let nextIdx = startIdx + 1 // Check for move directive - if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { - movePath = lines[nextIdx].split(":", 2)[1]?.trim() + if (nextIdx < lines.length && lines[nextIdx].startsWith(movePrefix)) { + movePath = lines[nextIdx].slice(movePrefix.length).trim() nextIdx++ } diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 35fdd4717b2..3813b50f845 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -140,7 +140,7 @@ export namespace Project { .then((x) => { const dirname = path.dirname(x.trim()) if (dirname === ".") return sandbox - return dirname + return path.resolve(sandbox, dirname) }) .catch(() => undefined) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index e06a3f157cb..5236f29b1c7 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -117,6 +117,7 @@ export const BashTool = Tool.define("bash", async () => { .nothrow() .text() .then((x) => x.trim()) + .then((x) => (x ? x : path.resolve(cwd, arg))) log.info("resolved path", { arg, resolved }) if (resolved) { // Git Bash on Windows returns Unix-style paths like /c/Users/... diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 087eb0c628c..7dcbcc55cb6 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "../fixture/fixture" import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" +import { createRequire } from "module" test("loads config with defaults when no files exist", async () => { await using tmp = await tmpdir() @@ -400,9 +401,24 @@ test("resolves scoped npm plugins in config", async () => { const pluginEntries = config.plugin ?? [] const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href - const expected = import.meta.resolve("@scope/plugin", baseUrl) - - expect(pluginEntries.includes(expected)).toBe(true) + let expected: string | null = null + try { + expected = import.meta.resolve("@scope/plugin", baseUrl) + } catch {} + if (!expected) { + try { + expected = pathToFileURL(createRequire(baseUrl).resolve("@scope/plugin")).href + } catch {} + } + if (!expected) { + try { + expected = pathToFileURL(Bun.resolveSync("@scope/plugin", path.join(tmp.path, "opencode.json"))).href + } catch {} + } + + expect(expected).toBeTruthy() + + expect(pluginEntries.includes(expected!)).toBe(true) const scopedEntry = pluginEntries.find((entry) => entry === expected) expect(scopedEntry).toBeDefined() diff --git a/packages/opencode/test/ide/ide.test.ts b/packages/opencode/test/ide/ide.test.ts index 4d70140197f..74a456606b0 100644 --- a/packages/opencode/test/ide/ide.test.ts +++ b/packages/opencode/test/ide/ide.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, afterEach } from "bun:test" import { Ide } from "../../src/ide" describe("ide", () => { - const original = structuredClone(process.env) + const original = Object.fromEntries(Object.entries(process.env)) afterEach(() => { Object.keys(process.env).forEach((key) => { diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index d44e606746e..8253cc84099 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -54,8 +54,8 @@ describe("Project.fromDirectory with worktrees", () => { test("should set worktree to root when called from a worktree", async () => { await using tmp = await tmpdir({ git: true }) - - const worktreePath = path.join(tmp.path, "..", "worktree-test") + const baseName = path.basename(tmp.path) + const worktreePath = path.join(tmp.path, "..", `${baseName}-worktree-test`) await $`git worktree add ${worktreePath} -b test-branch`.cwd(tmp.path).quiet() const { project, sandbox } = await Project.fromDirectory(worktreePath) @@ -70,9 +70,9 @@ describe("Project.fromDirectory with worktrees", () => { test("should accumulate multiple worktrees in sandboxes", async () => { await using tmp = await tmpdir({ git: true }) - - const worktree1 = path.join(tmp.path, "..", "worktree-1") - const worktree2 = path.join(tmp.path, "..", "worktree-2") + const baseName = path.basename(tmp.path) + const worktree1 = path.join(tmp.path, "..", `${baseName}-worktree-1`) + const worktree2 = path.join(tmp.path, "..", `${baseName}-worktree-2`) await $`git worktree add ${worktree1} -b branch-1`.cwd(tmp.path).quiet() await $`git worktree add ${worktree2} -b branch-2`.cwd(tmp.path).quiet() diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 72415c1411e..6354b389d96 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -5,6 +5,8 @@ import { tmpdir } from "../fixture/fixture" import path from "path" import fs from "fs/promises" +const normalizePath = (value: string) => value.replaceAll("\\", "/") + async function createGlobalSkill(homeDir: string) { const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill") await fs.mkdir(skillDir, { recursive: true }) @@ -49,8 +51,8 @@ Instructions here. expect(skills.length).toBe(1) const testSkill = skills.find((s) => s.name === "test-skill") expect(testSkill).toBeDefined() - expect(testSkill!.description).toBe("A test skill for verification.") - expect(testSkill!.location).toContain("skill/test-skill/SKILL.md") + expect(testSkill!.description).toBe("A test skill for verification.") + expect(normalizePath(testSkill!.location)).toContain("skill/test-skill/SKILL.md") }, }) }) @@ -144,7 +146,7 @@ description: A skill in the .claude/skills directory. expect(skills.length).toBe(1) const claudeSkill = skills.find((s) => s.name === "claude-skill") expect(claudeSkill).toBeDefined() - expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md") + expect(normalizePath(claudeSkill!.location)).toContain(".claude/skills/claude-skill/SKILL.md") }, }) }) @@ -164,7 +166,7 @@ test("discovers global skills from ~/.claude/skills/ directory", async () => { expect(skills.length).toBe(1) expect(skills[0].name).toBe("global-test-skill") expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.") - expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md") + expect(normalizePath(skills[0].location)).toContain(".claude/skills/global-test-skill/SKILL.md") }, }) } finally { diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index cf933f81286..7c8aa08761f 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -3,6 +3,16 @@ import { $ } from "bun" import { Snapshot } from "../../src/snapshot" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import path from "path" + +const normalizePath = (value: string) => value.replaceAll("\\", "/") +const expectFilesToContain = (files: string[], expected: string) => { + expect(files.map(normalizePath)).toContain(normalizePath(expected)) +} +const expectFilesNotToContain = (files: string[], expected: string) => { + expect(files.map(normalizePath)).not.toContain(normalizePath(expected)) +} +const testPosix = process.platform === "win32" ? test.skip : test async function bootstrap() { return tmpdir({ @@ -33,7 +43,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`) + expectFilesToContain((await Snapshot.patch(before!)).files, `${tmp.path}/a.txt`) }, }) }) @@ -126,7 +136,7 @@ test("binary file handling", async () => { 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`) + expectFilesToContain(patch.files, `${tmp.path}/image.png`) await Snapshot.revert([patch]) expect(await Bun.file(`${tmp.path}/image.png`).exists()).toBe(false) @@ -134,7 +144,7 @@ test("binary file handling", async () => { }) }) -test("symlink handling", async () => { +testPosix("symlink handling", async () => { await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, @@ -144,7 +154,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`) + expectFilesToContain((await Snapshot.patch(before!)).files, `${tmp.path}/link.txt`) }, }) }) @@ -159,7 +169,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`) + expectFilesToContain((await Snapshot.patch(before!)).files, `${tmp.path}/large.txt`) }, }) }) @@ -195,9 +205,9 @@ test("special characters in filenames", async () => { await Bun.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES") const files = (await Snapshot.patch(before!)).files - expect(files).toContain(`${tmp.path}/file with spaces.txt`) - expect(files).toContain(`${tmp.path}/file-with-dashes.txt`) - expect(files).toContain(`${tmp.path}/file_with_underscores.txt`) + expectFilesToContain(files, `${tmp.path}/file with spaces.txt`) + expectFilesToContain(files, `${tmp.path}/file-with-dashes.txt`) + expectFilesToContain(files, `${tmp.path}/file_with_underscores.txt`) }, }) }) @@ -295,13 +305,15 @@ test("very long filenames", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - const longName = "a".repeat(200) + ".txt" + const maxNameLength = + process.platform === "win32" ? Math.max(20, 240 - tmp.path.length) : 200 + const longName = "a".repeat(maxNameLength) + ".txt" const longFile = `${tmp.path}/${longName}` await Bun.write(longFile, "long filename content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(longFile) + expectFilesToContain(patch.files, longFile) await Snapshot.revert([patch]) expect(await Bun.file(longFile).exists()).toBe(false) @@ -322,14 +334,14 @@ test("hidden files", async () => { await Bun.write(`${tmp.path}/.config`, "config content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/.hidden`) - expect(patch.files).toContain(`${tmp.path}/.gitignore`) - expect(patch.files).toContain(`${tmp.path}/.config`) + expectFilesToContain(patch.files, `${tmp.path}/.hidden`) + expectFilesToContain(patch.files, `${tmp.path}/.gitignore`) + expectFilesToContain(patch.files, `${tmp.path}/.config`) }, }) }) -test("nested symlinks", async () => { +testPosix("nested symlinks", async () => { await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, @@ -343,13 +355,13 @@ test("nested symlinks", async () => { await $`ln -s ${tmp.path}/sub ${tmp.path}/sub-link`.quiet() const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/sub/dir/link.txt`) - expect(patch.files).toContain(`${tmp.path}/sub-link`) + expectFilesToContain(patch.files, `${tmp.path}/sub/dir/link.txt`) + expectFilesToContain(patch.files, `${tmp.path}/sub-link`) }, }) }) -test("file permissions and ownership changes", async () => { +testPosix("file permissions and ownership changes", async () => { await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, @@ -402,11 +414,11 @@ test("gitignore changes", async () => { const patch = await Snapshot.patch(before!) // Should track gitignore itself - expect(patch.files).toContain(`${tmp.path}/.gitignore`) + expectFilesToContain(patch.files, `${tmp.path}/.gitignore`) // Should track normal files - expect(patch.files).toContain(`${tmp.path}/normal.txt`) + expectFilesToContain(patch.files, `${tmp.path}/normal.txt`) // Should not track ignored files (git won't see them) - expect(patch.files).not.toContain(`${tmp.path}/test.ignored`) + expectFilesNotToContain(patch.files, `${tmp.path}/test.ignored`) }, }) }) @@ -451,7 +463,7 @@ test("snapshot state isolation between projects", async () => { const before1 = await Snapshot.track() await Bun.write(`${tmp1.path}/project1.txt`, "project1 content") const patch1 = await Snapshot.patch(before1!) - expect(patch1.files).toContain(`${tmp1.path}/project1.txt`) + expectFilesToContain(patch1.files, `${tmp1.path}/project1.txt`) }, }) @@ -461,10 +473,10 @@ test("snapshot state isolation between projects", async () => { const before2 = await Snapshot.track() await Bun.write(`${tmp2.path}/project2.txt`, "project2 content") const patch2 = await Snapshot.patch(before2!) - expect(patch2.files).toContain(`${tmp2.path}/project2.txt`) + expectFilesToContain(patch2.files, `${tmp2.path}/project2.txt`) // Ensure project1 files don't appear in project2 - expect(patch2.files).not.toContain(`${tmp1?.path}/project1.txt`) + expectFilesNotToContain(patch2.files, `${tmp1?.path}/project1.txt`) }, }) }) @@ -492,7 +504,7 @@ test("patch detects changes in secondary worktree", async () => { await Bun.write(worktreeFile, "worktree content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(worktreeFile) + expectFilesToContain(patch.files, worktreeFile) }, }) } finally { @@ -658,7 +670,7 @@ test("revert should not delete files that existed but were deleted in snapshot", await Bun.write(`${tmp.path}/a.txt`, "recreated content") const patch = await Snapshot.patch(snapshot2!) - expect(patch.files).toContain(`${tmp.path}/a.txt`) + expectFilesToContain(patch.files, `${tmp.path}/a.txt`) await Snapshot.revert([patch]) @@ -682,8 +694,8 @@ test("revert preserves file that existed in snapshot when deleted then recreated await Bun.write(`${tmp.path}/newfile.txt`, "new") const patch = await Snapshot.patch(snapshot!) - expect(patch.files).toContain(`${tmp.path}/existing.txt`) - expect(patch.files).toContain(`${tmp.path}/newfile.txt`) + expectFilesToContain(patch.files, `${tmp.path}/existing.txt`) + expectFilesToContain(patch.files, `${tmp.path}/newfile.txt`) await Snapshot.revert([patch])