Procházet zdrojové kódy

refactor(worktree): remove async facade exports (#22369)

Kit Langton před 3 dny
rodič
revize
36745caa2a

+ 12 - 7
packages/opencode/src/control-plane/adaptors/worktree.ts

@@ -1,4 +1,5 @@
 import z from "zod"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Worktree } from "@/worktree"
 import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
 
@@ -12,7 +13,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
   name: "Worktree",
   description: "Create a git worktree",
   async configure(info) {
-    const worktree = await Worktree.makeWorktreeInfo(undefined)
+    const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.makeWorktreeInfo()))
     return {
       ...info,
       name: worktree.name,
@@ -22,15 +23,19 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
   },
   async create(info) {
     const config = WorktreeConfig.parse(info)
-    await Worktree.createFromInfo({
-      name: config.name,
-      directory: config.directory,
-      branch: config.branch,
-    })
+    await AppRuntime.runPromise(
+      Worktree.Service.use((svc) =>
+        svc.createFromInfo({
+          name: config.name,
+          directory: config.directory,
+          branch: config.branch,
+        }),
+      ),
+    )
   },
   async remove(info) {
     const config = WorktreeConfig.parse(info)
-    await Worktree.remove({ directory: config.directory })
+    await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
   },
   target(info) {
     const config = WorktreeConfig.parse(info)

+ 3 - 3
packages/opencode/src/server/instance/experimental.ts

@@ -254,7 +254,7 @@ export const ExperimentalRoutes = lazy(() =>
       validator("json", Worktree.CreateInput.optional()),
       async (c) => {
         const body = c.req.valid("json")
-        const worktree = await Worktree.create(body)
+        const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.create(body)))
         return c.json(worktree)
       },
     )
@@ -301,7 +301,7 @@ export const ExperimentalRoutes = lazy(() =>
       validator("json", Worktree.RemoveInput),
       async (c) => {
         const body = c.req.valid("json")
-        await Worktree.remove(body)
+        await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove(body)))
         await Project.removeSandbox(Instance.project.id, body.directory)
         return c.json(true)
       },
@@ -327,7 +327,7 @@ export const ExperimentalRoutes = lazy(() =>
       validator("json", Worktree.ResetInput),
       async (c) => {
         const body = c.req.valid("json")
-        await Worktree.reset(body)
+        await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.reset(body)))
         return c.json(true)
       },
     )

+ 0 - 22
packages/opencode/src/worktree/index.ts

@@ -18,7 +18,6 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import { NodePath } from "@effect/platform-node"
 import { AppFileSystem } from "@/filesystem"
 import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
-import { makeRuntime } from "@/effect/run-service"
 import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { InstanceState } from "@/effect/instance-state"
 
@@ -598,25 +597,4 @@ export namespace Worktree {
     Layer.provide(AppFileSystem.defaultLayer),
     Layer.provide(NodePath.layer),
   )
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function makeWorktreeInfo(name?: string) {
-    return runPromise((svc) => svc.makeWorktreeInfo(name))
-  }
-
-  export async function createFromInfo(info: Info, startCommand?: string) {
-    return runPromise((svc) => svc.createFromInfo(info, startCommand))
-  }
-
-  export async function create(input?: CreateInput) {
-    return runPromise((svc) => svc.create(input))
-  }
-
-  export async function remove(input: RemoveInput) {
-    return runPromise((svc) => svc.remove(input))
-  }
-
-  export async function reset(input: ResetInput) {
-    return runPromise((svc) => svc.reset(input))
-  }
 }

+ 119 - 89
packages/opencode/test/project/worktree-remove.test.ts

@@ -1,96 +1,126 @@
-import { describe, expect, test } from "bun:test"
 import { $ } from "bun"
-import fs from "fs/promises"
+import { describe, expect } from "bun:test"
+import * as fs from "fs/promises"
 import path from "path"
-import { Instance } from "../../src/project/instance"
+import { Effect, Layer } from "effect"
+import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { Worktree } from "../../src/worktree"
-import { Filesystem } from "../../src/util/filesystem"
-import { tmpdir } from "../fixture/fixture"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
-const wintest = process.platform === "win32" ? test : test.skip
+const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const wintest = process.platform === "win32" ? it.live : it.live.skip
 
 describe("Worktree.remove", () => {
-  test("continues when git remove exits non-zero after detaching", async () => {
-    await using tmp = await tmpdir({ git: true })
-    const root = tmp.path
-    const name = `remove-regression-${Date.now().toString(36)}`
-    const branch = `opencode/${name}`
-    const dir = path.join(root, "..", name)
-
-    await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
-    await $`git reset --hard`.cwd(dir).quiet()
-
-    const real = (await $`which git`.quiet().text()).trim()
-    expect(real).toBeTruthy()
-
-    const bin = path.join(root, "bin")
-    const shim = path.join(bin, "git")
-    await fs.mkdir(bin, { recursive: true })
-    await Bun.write(
-      shim,
-      [
-        "#!/bin/bash",
-        `REAL_GIT=${JSON.stringify(real)}`,
-        'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
-        '  "$REAL_GIT" "$@" >/dev/null 2>&1',
-        '  echo "fatal: failed to remove worktree: Directory not empty" >&2',
-        "  exit 1",
-        "fi",
-        'exec "$REAL_GIT" "$@"',
-      ].join("\n"),
-    )
-    await fs.chmod(shim, 0o755)
-
-    const prev = process.env.PATH ?? ""
-    process.env.PATH = `${bin}${path.delimiter}${prev}`
-
-    const ok = await (async () => {
-      try {
-        return await Instance.provide({
-          directory: root,
-          fn: () => Worktree.remove({ directory: dir }),
-        })
-      } finally {
-        process.env.PATH = prev
-      }
-    })()
-
-    expect(ok).toBe(true)
-    expect(await Filesystem.exists(dir)).toBe(false)
-
-    const list = await $`git worktree list --porcelain`.cwd(root).quiet().text()
-    expect(list).not.toContain(`worktree ${dir}`)
-
-    const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
-    expect(ref.exitCode).not.toBe(0)
-  })
-
-  wintest("stops fsmonitor before removing a worktree", async () => {
-    await using tmp = await tmpdir({ git: true })
-    const root = tmp.path
-    const name = `remove-fsmonitor-${Date.now().toString(36)}`
-    const branch = `opencode/${name}`
-    const dir = path.join(root, "..", name)
-
-    await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
-    await $`git reset --hard`.cwd(dir).quiet()
-    await $`git config core.fsmonitor true`.cwd(dir).quiet()
-    await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
-    await Bun.write(path.join(dir, "tracked.txt"), "next\n")
-    await $`git diff`.cwd(dir).quiet()
-
-    const before = await $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow()
-    expect(before.exitCode).toBe(0)
-
-    const ok = await Instance.provide({
-      directory: root,
-      fn: () => Worktree.remove({ directory: dir }),
-    })
-
-    expect(ok).toBe(true)
-    expect(await Filesystem.exists(dir)).toBe(false)
-
-    const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
-    expect(ref.exitCode).not.toBe(0)
-  })
+  it.live("continues when git remove exits non-zero after detaching", () =>
+    provideTmpdirInstance(
+      (root) =>
+        Effect.gen(function* () {
+          const svc = yield* Worktree.Service
+          const name = `remove-regression-${Date.now().toString(36)}`
+          const branch = `opencode/${name}`
+          const dir = path.join(root, "..", name)
+
+          yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
+          yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
+
+          const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
+          expect(real).toBeTruthy()
+
+          const bin = path.join(root, "bin")
+          const shim = path.join(bin, "git")
+          yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
+          yield* Effect.promise(() =>
+            Bun.write(
+              shim,
+              [
+                "#!/bin/bash",
+                `REAL_GIT=${JSON.stringify(real)}`,
+                'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
+                '  "$REAL_GIT" "$@" >/dev/null 2>&1',
+                '  echo "fatal: failed to remove worktree: Directory not empty" >&2',
+                "  exit 1",
+                "fi",
+                'exec "$REAL_GIT" "$@"',
+              ].join("\n"),
+            ),
+          )
+          yield* Effect.promise(() => fs.chmod(shim, 0o755))
+
+          const prev = yield* Effect.acquireRelease(
+            Effect.sync(() => {
+              const prev = process.env.PATH ?? ""
+              process.env.PATH = `${bin}${path.delimiter}${prev}`
+              return prev
+            }),
+            (prev) =>
+              Effect.sync(() => {
+                process.env.PATH = prev
+              }),
+          )
+          void prev
+
+          const ok = yield* svc.remove({ directory: dir })
+
+          expect(ok).toBe(true)
+          expect(
+            yield* Effect.promise(() =>
+              fs
+                .stat(dir)
+                .then(() => true)
+                .catch(() => false),
+            ),
+          ).toBe(false)
+
+          const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
+          expect(list).not.toContain(`worktree ${dir}`)
+
+          const ref = yield* Effect.promise(() =>
+            $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
+          )
+          expect(ref.exitCode).not.toBe(0)
+        }),
+      { git: true },
+    ),
+  )
+
+  wintest("stops fsmonitor before removing a worktree", () =>
+    provideTmpdirInstance(
+      (root) =>
+        Effect.gen(function* () {
+          const svc = yield* Worktree.Service
+          const name = `remove-fsmonitor-${Date.now().toString(36)}`
+          const branch = `opencode/${name}`
+          const dir = path.join(root, "..", name)
+
+          yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
+          yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
+          yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
+          yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
+          yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
+          yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
+
+          const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
+          expect(before.exitCode).toBe(0)
+
+          const ok = yield* svc.remove({ directory: dir })
+
+          expect(ok).toBe(true)
+          expect(
+            yield* Effect.promise(() =>
+              fs
+                .stat(dir)
+                .then(() => true)
+                .catch(() => false),
+            ),
+          ).toBe(false)
+
+          const ref = yield* Effect.promise(() =>
+            $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
+          )
+          expect(ref.exitCode).not.toBe(0)
+        }),
+      { git: true },
+    ),
+  )
 })

+ 169 - 128
packages/opencode/test/project/worktree.test.ts

@@ -1,16 +1,16 @@
 import { $ } from "bun"
-import { afterEach, describe, expect, test } from "bun:test"
-
-const wintest = process.platform !== "win32" ? test : test.skip
-import fs from "fs/promises"
+import { afterEach, describe, expect } from "bun:test"
+import * as fs from "fs/promises"
 import path from "path"
+import { Cause, Effect, Exit, Layer } from "effect"
+import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { Instance } from "../../src/project/instance"
 import { Worktree } from "../../src/worktree"
-import { tmpdir } from "../fixture/fixture"
+import { provideInstance, provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
-function withInstance(directory: string, fn: () => Promise<any>) {
-  return Instance.provide({ directory, fn })
-}
+const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const wintest = process.platform !== "win32" ? it.live : it.live.skip
 
 function normalize(input: string) {
   return input.replace(/\\/g, "/").toLowerCase()
@@ -40,134 +40,175 @@ describe("Worktree", () => {
   afterEach(() => Instance.disposeAll())
 
   describe("makeWorktreeInfo", () => {
-    test("returns info with name, branch, and directory", async () => {
-      await using tmp = await tmpdir({ git: true })
-
-      const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo())
-
-      expect(info.name).toBeDefined()
-      expect(typeof info.name).toBe("string")
-      expect(info.branch).toBe(`opencode/${info.name}`)
-      expect(info.directory).toContain(info.name)
-    })
-
-    test("uses provided name as base", async () => {
-      await using tmp = await tmpdir({ git: true })
-
-      const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("my-feature"))
-
-      expect(info.name).toBe("my-feature")
-      expect(info.branch).toBe("opencode/my-feature")
-    })
-
-    test("slugifies the provided name", async () => {
-      await using tmp = await tmpdir({ git: true })
-
-      const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("My Feature Branch!"))
-
-      expect(info.name).toBe("my-feature-branch")
-    })
-
-    test("throws NotGitError for non-git directories", async () => {
-      await using tmp = await tmpdir()
-
-      await expect(withInstance(tmp.path, () => Worktree.makeWorktreeInfo())).rejects.toThrow("WorktreeNotGitError")
-    })
+    it.live("returns info with name, branch, and directory", () =>
+      provideTmpdirInstance(
+        () =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const info = yield* svc.makeWorktreeInfo()
+
+            expect(info.name).toBeDefined()
+            expect(typeof info.name).toBe("string")
+            expect(info.branch).toBe(`opencode/${info.name}`)
+            expect(info.directory).toContain(info.name)
+          }),
+        { git: true },
+      ),
+    )
+
+    it.live("uses provided name as base", () =>
+      provideTmpdirInstance(
+        () =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const info = yield* svc.makeWorktreeInfo("my-feature")
+
+            expect(info.name).toBe("my-feature")
+            expect(info.branch).toBe("opencode/my-feature")
+          }),
+        { git: true },
+      ),
+    )
+
+    it.live("slugifies the provided name", () =>
+      provideTmpdirInstance(
+        () =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const info = yield* svc.makeWorktreeInfo("My Feature Branch!")
+
+            expect(info.name).toBe("my-feature-branch")
+          }),
+        { git: true },
+      ),
+    )
+
+    it.live("throws NotGitError for non-git directories", () =>
+      provideTmpdirInstance(() =>
+        Effect.gen(function* () {
+          const svc = yield* Worktree.Service
+          const exit = yield* Effect.exit(svc.makeWorktreeInfo())
+
+          expect(Exit.isFailure(exit)).toBe(true)
+          if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError)
+        }),
+      ),
+    )
   })
 
   describe("create + remove lifecycle", () => {
-    test("create returns worktree info and remove cleans up", async () => {
-      await using tmp = await tmpdir({ git: true })
-
-      const info = await withInstance(tmp.path, () => Worktree.create())
-
-      expect(info.name).toBeDefined()
-      expect(info.branch).toStartWith("opencode/")
-      expect(info.directory).toBeDefined()
-
-      // Wait for bootstrap to complete
-      await Bun.sleep(1000)
-
-      const ok = await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
-      expect(ok).toBe(true)
-    })
-
-    test("create returns after setup and fires Event.Ready after bootstrap", async () => {
-      await using tmp = await tmpdir({ git: true })
-      const ready = waitReady()
-
-      const info = await withInstance(tmp.path, () => Worktree.create())
-
-      // create returns before bootstrap completes, but the worktree already exists
-      expect(info.name).toBeDefined()
-      expect(info.branch).toStartWith("opencode/")
-
-      const text = await $`git worktree list --porcelain`.cwd(tmp.path).quiet().text()
-      const dir = await fs.realpath(info.directory).catch(() => info.directory)
-      expect(normalize(text)).toContain(normalize(dir))
-
-      // Event.Ready fires after bootstrap finishes in the background
-      const props = await ready
-      expect(props.name).toBe(info.name)
-      expect(props.branch).toBe(info.branch)
-
-      // Cleanup
-      await withInstance(info.directory, () => Instance.dispose())
-      await Bun.sleep(100)
-      await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
-    })
-
-    test("create with custom name", async () => {
-      await using tmp = await tmpdir({ git: true })
-      const ready = waitReady()
-
-      const info = await withInstance(tmp.path, () => Worktree.create({ name: "test-workspace" }))
-
-      expect(info.name).toBe("test-workspace")
-      expect(info.branch).toBe("opencode/test-workspace")
-
-      // Cleanup
-      await ready
-      await withInstance(info.directory, () => Instance.dispose())
-      await Bun.sleep(100)
-      await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
-    })
+    it.live("create returns worktree info and remove cleans up", () =>
+      provideTmpdirInstance(
+        () =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const info = yield* svc.create()
+
+            expect(info.name).toBeDefined()
+            expect(info.branch).toStartWith("opencode/")
+            expect(info.directory).toBeDefined()
+
+            yield* Effect.promise(() => Bun.sleep(1000))
+
+            const ok = yield* svc.remove({ directory: info.directory })
+            expect(ok).toBe(true)
+          }),
+        { git: true },
+      ),
+    )
+
+    it.live("create returns after setup and fires Event.Ready after bootstrap", () =>
+      provideTmpdirInstance(
+        (dir) =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const ready = waitReady()
+            const info = yield* svc.create()
+
+            expect(info.name).toBeDefined()
+            expect(info.branch).toStartWith("opencode/")
+
+            const text = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(dir).quiet().text())
+            const next = yield* Effect.promise(() => fs.realpath(info.directory).catch(() => info.directory))
+            expect(normalize(text)).toContain(normalize(next))
+
+            const props = yield* Effect.promise(() => ready)
+            expect(props.name).toBe(info.name)
+            expect(props.branch).toBe(info.branch)
+
+            yield* Effect.promise(() => Instance.dispose()).pipe(provideInstance(info.directory))
+            yield* Effect.promise(() => Bun.sleep(100))
+            yield* svc.remove({ directory: info.directory })
+          }),
+        { git: true },
+      ),
+    )
+
+    it.live("create with custom name", () =>
+      provideTmpdirInstance(
+        () =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const ready = waitReady()
+            const info = yield* svc.create({ name: "test-workspace" })
+
+            expect(info.name).toBe("test-workspace")
+            expect(info.branch).toBe("opencode/test-workspace")
+
+            yield* Effect.promise(() => ready)
+            yield* Effect.promise(() => Instance.dispose()).pipe(provideInstance(info.directory))
+            yield* Effect.promise(() => Bun.sleep(100))
+            yield* svc.remove({ directory: info.directory })
+          }),
+        { git: true },
+      ),
+    )
   })
 
   describe("createFromInfo", () => {
-    wintest("creates and bootstraps git worktree", async () => {
-      await using tmp = await tmpdir({ git: true })
-
-      const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("from-info-test"))
-      await withInstance(tmp.path, () => Worktree.createFromInfo(info))
-
-      // Worktree should exist in git (normalize slashes for Windows)
-      const list = await $`git worktree list --porcelain`.cwd(tmp.path).quiet().text()
-      const normalizedList = list.replace(/\\/g, "/")
-      const normalizedDir = info.directory.replace(/\\/g, "/")
-      expect(normalizedList).toContain(normalizedDir)
-
-      // Cleanup
-      await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
-    })
+    wintest("creates and bootstraps git worktree", () =>
+      provideTmpdirInstance(
+        (dir) =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const info = yield* svc.makeWorktreeInfo("from-info-test")
+            yield* svc.createFromInfo(info)
+
+            const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(dir).quiet().text())
+            const normalizedList = list.replace(/\\/g, "/")
+            const normalizedDir = info.directory.replace(/\\/g, "/")
+            expect(normalizedList).toContain(normalizedDir)
+
+            yield* svc.remove({ directory: info.directory })
+          }),
+        { git: true },
+      ),
+    )
   })
 
   describe("remove edge cases", () => {
-    test("remove non-existent directory succeeds silently", async () => {
-      await using tmp = await tmpdir({ git: true })
-
-      const ok = await withInstance(tmp.path, () =>
-        Worktree.remove({ directory: path.join(tmp.path, "does-not-exist") }),
-      )
-      expect(ok).toBe(true)
-    })
-
-    test("throws NotGitError for non-git directories", async () => {
-      await using tmp = await tmpdir()
-
-      await expect(withInstance(tmp.path, () => Worktree.remove({ directory: "/tmp/fake" }))).rejects.toThrow(
-        "WorktreeNotGitError",
-      )
-    })
+    it.live("remove non-existent directory succeeds silently", () =>
+      provideTmpdirInstance(
+        (dir) =>
+          Effect.gen(function* () {
+            const svc = yield* Worktree.Service
+            const ok = yield* svc.remove({ directory: path.join(dir, "does-not-exist") })
+            expect(ok).toBe(true)
+          }),
+        { git: true },
+      ),
+    )
+
+    it.live("throws NotGitError for non-git directories", () =>
+      provideTmpdirInstance(() =>
+        Effect.gen(function* () {
+          const svc = yield* Worktree.Service
+          const exit = yield* Effect.exit(svc.remove({ directory: "/tmp/fake" }))
+
+          expect(Exit.isFailure(exit)).toBe(true)
+          if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError)
+        }),
+      ),
+    )
   })
 })