|
1 | 1 | import { describe, expect } from "bun:test" |
2 | 2 | import path from "path" |
| 3 | +import { realpath } from "fs/promises" |
3 | 4 | import { AppFileSystem } from "@opencode-ai/core/filesystem" |
4 | | -import { ConfigProvider, Deferred, Effect, Layer, Option } from "effect" |
| 5 | +import { ConfigProvider, Deferred, Duration, Effect, Layer, Option } from "effect" |
5 | 6 | import { TestInstance, provideInstance } from "../fixture/fixture" |
6 | 7 | import { testEffect } from "../lib/effect" |
7 | 8 | import { GlobalBus, type GlobalEvent } from "../../src/bus/global" |
@@ -78,23 +79,49 @@ function wait(directory: string, check: (evt: WatcherEvent) => boolean) { |
78 | 79 | }) |
79 | 80 | } |
80 | 81 |
|
81 | | -function nextUpdate<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) { |
| 82 | +function maybeNextUpdate<E>( |
| 83 | + directory: string, |
| 84 | + check: (evt: WatcherEvent) => boolean, |
| 85 | + trigger: Effect.Effect<void, E>, |
| 86 | + timeout: Duration.Input = "5 seconds", |
| 87 | +) { |
82 | 88 | return Effect.acquireUseRelease( |
83 | 89 | wait(directory, check), |
84 | 90 | ({ deferred }) => |
85 | 91 | Effect.gen(function* () { |
86 | 92 | yield* trigger |
87 | | - return yield* Deferred.await(deferred).pipe( |
88 | | - Effect.timeoutOrElse({ |
89 | | - duration: "5 seconds", |
90 | | - orElse: () => Effect.fail(new Error("timed out waiting for file watcher update")), |
91 | | - }), |
92 | | - ) |
| 93 | + return yield* Deferred.await(deferred).pipe(Effect.timeoutOption(timeout)) |
93 | 94 | }), |
94 | 95 | ({ cleanup }) => Effect.sync(cleanup), |
95 | 96 | ) |
96 | 97 | } |
97 | 98 |
|
| 99 | +function nextUpdate<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) { |
| 100 | + return Effect.gen(function* () { |
| 101 | + const result = yield* maybeNextUpdate(directory, check, trigger) |
| 102 | + if (Option.isSome(result)) return result.value |
| 103 | + return yield* Effect.fail(new Error("timed out waiting for file watcher update")) |
| 104 | + }) |
| 105 | +} |
| 106 | + |
| 107 | +function eventuallyUpdate<E>( |
| 108 | + directory: string, |
| 109 | + check: (evt: WatcherEvent) => boolean, |
| 110 | + trigger: () => Effect.Effect<void, E>, |
| 111 | +) { |
| 112 | + return Effect.gen(function* () { |
| 113 | + while (true) { |
| 114 | + const result = yield* maybeNextUpdate(directory, check, trigger(), "250 millis") |
| 115 | + if (Option.isSome(result)) return result.value |
| 116 | + } |
| 117 | + }).pipe( |
| 118 | + Effect.timeoutOrElse({ |
| 119 | + duration: "5 seconds", |
| 120 | + orElse: () => Effect.fail(new Error("timed out waiting for file watcher readiness")), |
| 121 | + }), |
| 122 | + ) |
| 123 | +} |
| 124 | + |
98 | 125 | /** Effect that asserts no matching event arrives within `ms`. */ |
99 | 126 | function noUpdate<E>( |
100 | 127 | directory: string, |
@@ -125,22 +152,25 @@ function ready(directory: string) { |
125 | 152 | const fs = yield* AppFileSystem.Service |
126 | 153 | const git = yield* Git.Service |
127 | 154 |
|
128 | | - yield* nextUpdate( |
| 155 | + yield* eventuallyUpdate( |
129 | 156 | directory, |
130 | | - (evt) => evt.file === file && evt.event === "add", |
131 | | - fs.writeFileString(file, "ready"), |
| 157 | + (evt) => evt.file === file, |
| 158 | + () => fs.writeFileString(file, `ready-${Math.random()}`), |
132 | 159 | ).pipe(Effect.ensuring(fs.remove(file, { force: true }).pipe(Effect.ignore)), Effect.asVoid) |
133 | 160 |
|
134 | 161 | if (!(yield* fs.existsSafe(head))) return |
135 | 162 |
|
136 | | - const branch = `watch-${Math.random().toString(36).slice(2)}` |
| 163 | + const realHead = yield* Effect.promise(() => realpath(head).catch(() => head)) |
137 | 164 | const hash = (yield* git.run(["rev-parse", "HEAD"], { cwd: directory })).text() |
138 | | - yield* nextUpdate( |
| 165 | + yield* eventuallyUpdate( |
139 | 166 | directory, |
140 | | - (evt) => evt.file === head && evt.event !== "unlink", |
141 | | - fs |
142 | | - .writeFileString(path.join(directory, ".git", "refs", "heads", branch), hash.trim() + "\n") |
143 | | - .pipe(Effect.andThen(fs.writeFileString(head, `ref: refs/heads/${branch}\n`))), |
| 167 | + (evt) => (evt.file === head || evt.file === realHead) && evt.event !== "unlink", |
| 168 | + () => { |
| 169 | + const branch = `watch-${Math.random().toString(36).slice(2)}` |
| 170 | + return fs |
| 171 | + .writeFileString(path.join(directory, ".git", "refs", "heads", branch), hash.trim() + "\n") |
| 172 | + .pipe(Effect.andThen(fs.writeFileString(head, `ref: refs/heads/${branch}\n`))) |
| 173 | + }, |
144 | 174 | ).pipe(Effect.asVoid) |
145 | 175 | }) |
146 | 176 | } |
|
0 commit comments