Stabilize watcher test readiness (#28194)

This commit is contained in:
Kit Langton
2026-05-18 12:51:46 -04:00
committed by GitHub
parent a2e6bd503b
commit 3ab67f3280
2 changed files with 54 additions and 22 deletions
+47 -17
View File
@@ -1,7 +1,8 @@
import { describe, expect } from "bun:test"
import path from "path"
import { realpath } from "fs/promises"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { ConfigProvider, Deferred, Effect, Layer, Option } from "effect"
import { ConfigProvider, Deferred, Duration, Effect, Layer, Option } from "effect"
import { TestInstance, provideInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
@@ -78,23 +79,49 @@ function wait(directory: string, check: (evt: WatcherEvent) => boolean) {
})
}
function nextUpdate<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) {
function maybeNextUpdate<E>(
directory: string,
check: (evt: WatcherEvent) => boolean,
trigger: Effect.Effect<void, E>,
timeout: Duration.Input = "5 seconds",
) {
return Effect.acquireUseRelease(
wait(directory, check),
({ deferred }) =>
Effect.gen(function* () {
yield* trigger
return yield* Deferred.await(deferred).pipe(
Effect.timeoutOrElse({
duration: "5 seconds",
orElse: () => Effect.fail(new Error("timed out waiting for file watcher update")),
}),
)
return yield* Deferred.await(deferred).pipe(Effect.timeoutOption(timeout))
}),
({ cleanup }) => Effect.sync(cleanup),
)
}
function nextUpdate<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) {
return Effect.gen(function* () {
const result = yield* maybeNextUpdate(directory, check, trigger)
if (Option.isSome(result)) return result.value
return yield* Effect.fail(new Error("timed out waiting for file watcher update"))
})
}
function eventuallyUpdate<E>(
directory: string,
check: (evt: WatcherEvent) => boolean,
trigger: () => Effect.Effect<void, E>,
) {
return Effect.gen(function* () {
while (true) {
const result = yield* maybeNextUpdate(directory, check, trigger(), "250 millis")
if (Option.isSome(result)) return result.value
}
}).pipe(
Effect.timeoutOrElse({
duration: "5 seconds",
orElse: () => Effect.fail(new Error("timed out waiting for file watcher readiness")),
}),
)
}
/** Effect that asserts no matching event arrives within `ms`. */
function noUpdate<E>(
directory: string,
@@ -125,22 +152,25 @@ function ready(directory: string) {
const fs = yield* AppFileSystem.Service
const git = yield* Git.Service
yield* nextUpdate(
yield* eventuallyUpdate(
directory,
(evt) => evt.file === file && evt.event === "add",
fs.writeFileString(file, "ready"),
(evt) => evt.file === file,
() => fs.writeFileString(file, `ready-${Math.random()}`),
).pipe(Effect.ensuring(fs.remove(file, { force: true }).pipe(Effect.ignore)), Effect.asVoid)
if (!(yield* fs.existsSafe(head))) return
const branch = `watch-${Math.random().toString(36).slice(2)}`
const realHead = yield* Effect.promise(() => realpath(head).catch(() => head))
const hash = (yield* git.run(["rev-parse", "HEAD"], { cwd: directory })).text()
yield* nextUpdate(
yield* eventuallyUpdate(
directory,
(evt) => evt.file === head && evt.event !== "unlink",
fs
.writeFileString(path.join(directory, ".git", "refs", "heads", branch), hash.trim() + "\n")
.pipe(Effect.andThen(fs.writeFileString(head, `ref: refs/heads/${branch}\n`))),
(evt) => (evt.file === head || evt.file === realHead) && evt.event !== "unlink",
() => {
const branch = `watch-${Math.random().toString(36).slice(2)}`
return fs
.writeFileString(path.join(directory, ".git", "refs", "heads", branch), hash.trim() + "\n")
.pipe(Effect.andThen(fs.writeFileString(head, `ref: refs/heads/${branch}\n`)))
},
).pipe(Effect.asVoid)
})
}