فهرست منبع

Stabilize watcher test readiness (#28194)

Kit Langton 3 ماه پیش
والد
کامیت
3ab67f3280
2فایلهای تغییر یافته به همراه54 افزوده شده و 22 حذف شده
  1. 47 17
      packages/opencode/test/file/watcher.test.ts
  2. 7 5
      perf/test-suite.md

+ 47 - 17
packages/opencode/test/file/watcher.test.ts

@@ -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)
   })
 }

+ 7 - 5
perf/test-suite.md

@@ -67,6 +67,7 @@ Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/
 | Remaining prompt behavior tests mostly do not require repository state                                    | Removed git setup from safe loop/reference/error fixtures; restored shell queue/cancel cases   | 23.400s   | 19.610s | keep     | Safety review found shell runner readiness depends on git-backed setup in several tests; current single rerun passes.       |
 | Session processor effect tests do not require repository state                                            | Removed git setup from all processor-effect temp server fixtures                               | 12.500s   | 9.230s  | keep     | Two targeted reruns passed after the change: 9.61s, 9.23s.                                                                  |
 | HTTP listen PTY ticket tests restart the same listener topology twice                                     | Folded directory-scoped ticket regression into the broader unsafe-ticket test                  | 7.051s    | 6.170s  | keep     | Two targeted reruns passed after the change: 6.76s, 6.17s; still covers mint failure and successful same-directory upgrade. |
+| File watcher readiness can write before async native subscriptions are active                             | Retried short readiness writes and accepted symlink-realpath HEAD events                       | failed    | 4.62s   | keep     | Three sequential focused watcher runs passed: 4.62s, 4.57s, 4.64s; full suite no longer failed in `watcher.test.ts`.        |
 
 ## Profiling Results
 
@@ -107,11 +108,12 @@ Targeted 3-run baselines:
 
 Full-suite sanity checks:
 
-| Command              |   Result | Notes                                                                |
-| -------------------- | -------: | -------------------------------------------------------------------- |
-| `bun run bench:test` | 225.069s | Before continuing prompt/session work.                               |
-| `bun run bench:test` | 186.729s | After prompt, processor, and PTY wins before safety review restores. |
-| `bun run bench:test` | 202.317s | After restoring prompt shell coverage and SDK VCS parity coverage.   |
+| Command              |   Result | Notes                                                                                                                                           |
+| -------------------- | -------: | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| `bun run bench:test` | 225.069s | Before continuing prompt/session work.                                                                                                          |
+| `bun run bench:test` | 186.729s | After prompt, processor, and PTY wins before safety review restores.                                                                            |
+| `bun run bench:test` | 202.317s | After restoring prompt shell coverage and SDK VCS parity coverage.                                                                              |
+| `bun run bench:test` |   failed | Watcher blocker cleared; current run later failed in focused-passing `tool/skill.test.ts` and prompt shell timeout cases under full-suite load. |
 
 ## Dead Ends