worktree-remove.test.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { $ } from "bun"
  2. import { describe, expect } from "bun:test"
  3. import * as fs from "fs/promises"
  4. import path from "path"
  5. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  6. import { Effect } from "effect"
  7. import { InstanceBootstrap } from "../../src/project/bootstrap"
  8. import { InstanceStore } from "../../src/project/instance-store"
  9. import { Worktree } from "../../src/worktree"
  10. import { TestInstance } from "../fixture/fixture"
  11. import { testEffect } from "../lib/effect"
  12. const it = testEffect(LayerNode.compile(Worktree.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]))
  13. const wintest = process.platform === "win32" ? it.instance : it.instance.skip
  14. describe("Worktree.remove", () => {
  15. it.instance(
  16. "continues when git remove exits non-zero after detaching",
  17. () =>
  18. Effect.gen(function* () {
  19. const root = (yield* TestInstance).directory
  20. const svc = yield* Worktree.Service
  21. const name = `remove-regression-${Date.now().toString(36)}`
  22. const branch = `opencode/${name}`
  23. const dir = path.join(root, "..", name)
  24. yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
  25. yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
  26. const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
  27. expect(real).toBeTruthy()
  28. const bin = path.join(root, "bin")
  29. const shim = path.join(bin, "git")
  30. yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
  31. yield* Effect.promise(() =>
  32. Bun.write(
  33. shim,
  34. [
  35. "#!/bin/bash",
  36. `REAL_GIT=${JSON.stringify(real)}`,
  37. 'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
  38. ' "$REAL_GIT" "$@" >/dev/null 2>&1',
  39. ' echo "fatal: failed to remove worktree: Directory not empty" >&2',
  40. " exit 1",
  41. "fi",
  42. 'exec "$REAL_GIT" "$@"',
  43. ].join("\n"),
  44. ),
  45. )
  46. yield* Effect.promise(() => fs.chmod(shim, 0o755))
  47. const prev = yield* Effect.acquireRelease(
  48. Effect.sync(() => {
  49. const prev = process.env.PATH ?? ""
  50. process.env.PATH = `${bin}${path.delimiter}${prev}`
  51. return prev
  52. }),
  53. (prev) =>
  54. Effect.sync(() => {
  55. process.env.PATH = prev
  56. }),
  57. )
  58. void prev
  59. const ok = yield* svc.remove({ directory: dir })
  60. expect(ok).toBe(true)
  61. expect(
  62. yield* Effect.promise(() =>
  63. fs
  64. .stat(dir)
  65. .then(() => true)
  66. .catch(() => false),
  67. ),
  68. ).toBe(false)
  69. const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
  70. expect(list).not.toContain(`worktree ${dir}`)
  71. const ref = yield* Effect.promise(() =>
  72. $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
  73. )
  74. expect(ref.exitCode).not.toBe(0)
  75. }),
  76. { git: true },
  77. )
  78. wintest(
  79. "stops fsmonitor before removing a worktree",
  80. () =>
  81. Effect.gen(function* () {
  82. const root = (yield* TestInstance).directory
  83. const svc = yield* Worktree.Service
  84. const name = `remove-fsmonitor-${Date.now().toString(36)}`
  85. const branch = `opencode/${name}`
  86. const dir = path.join(root, "..", name)
  87. yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
  88. yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
  89. yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
  90. yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
  91. yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
  92. yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
  93. const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
  94. expect(before.exitCode).toBe(0)
  95. const ok = yield* svc.remove({ directory: dir })
  96. expect(ok).toBe(true)
  97. expect(
  98. yield* Effect.promise(() =>
  99. fs
  100. .stat(dir)
  101. .then(() => true)
  102. .catch(() => false),
  103. ),
  104. ).toBe(false)
  105. const ref = yield* Effect.promise(() =>
  106. $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
  107. )
  108. expect(ref.exitCode).not.toBe(0)
  109. }),
  110. { git: true },
  111. )
  112. })