Stabilize watcher test readiness (#28194)
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { realpath } from "fs/promises"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
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 { TestInstance, provideInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
|
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(
|
return Effect.acquireUseRelease(
|
||||||
wait(directory, check),
|
wait(directory, check),
|
||||||
({ deferred }) =>
|
({ deferred }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* trigger
|
yield* trigger
|
||||||
return yield* Deferred.await(deferred).pipe(
|
return yield* Deferred.await(deferred).pipe(Effect.timeoutOption(timeout))
|
||||||
Effect.timeoutOrElse({
|
|
||||||
duration: "5 seconds",
|
|
||||||
orElse: () => Effect.fail(new Error("timed out waiting for file watcher update")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
({ cleanup }) => Effect.sync(cleanup),
|
({ 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`. */
|
/** Effect that asserts no matching event arrives within `ms`. */
|
||||||
function noUpdate<E>(
|
function noUpdate<E>(
|
||||||
directory: string,
|
directory: string,
|
||||||
@@ -125,22 +152,25 @@ function ready(directory: string) {
|
|||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const git = yield* Git.Service
|
const git = yield* Git.Service
|
||||||
|
|
||||||
yield* nextUpdate(
|
yield* eventuallyUpdate(
|
||||||
directory,
|
directory,
|
||||||
(evt) => evt.file === file && evt.event === "add",
|
(evt) => evt.file === file,
|
||||||
fs.writeFileString(file, "ready"),
|
() => fs.writeFileString(file, `ready-${Math.random()}`),
|
||||||
).pipe(Effect.ensuring(fs.remove(file, { force: true }).pipe(Effect.ignore)), Effect.asVoid)
|
).pipe(Effect.ensuring(fs.remove(file, { force: true }).pipe(Effect.ignore)), Effect.asVoid)
|
||||||
|
|
||||||
if (!(yield* fs.existsSafe(head))) return
|
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()
|
const hash = (yield* git.run(["rev-parse", "HEAD"], { cwd: directory })).text()
|
||||||
yield* nextUpdate(
|
yield* eventuallyUpdate(
|
||||||
directory,
|
directory,
|
||||||
(evt) => evt.file === head && evt.event !== "unlink",
|
(evt) => (evt.file === head || evt.file === realHead) && evt.event !== "unlink",
|
||||||
fs
|
() => {
|
||||||
.writeFileString(path.join(directory, ".git", "refs", "heads", branch), hash.trim() + "\n")
|
const branch = `watch-${Math.random().toString(36).slice(2)}`
|
||||||
.pipe(Effect.andThen(fs.writeFileString(head, `ref: refs/heads/${branch}\n`))),
|
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)
|
).pipe(Effect.asVoid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -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. |
|
| 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. |
|
| 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. |
|
| 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
|
## Profiling Results
|
||||||
|
|
||||||
@@ -107,11 +108,12 @@ Targeted 3-run baselines:
|
|||||||
|
|
||||||
Full-suite sanity checks:
|
Full-suite sanity checks:
|
||||||
|
|
||||||
| Command | Result | Notes |
|
| Command | Result | Notes |
|
||||||
| -------------------- | -------: | -------------------------------------------------------------------- |
|
| -------------------- | -------: | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `bun run bench:test` | 225.069s | Before continuing prompt/session work. |
|
| `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` | 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` | 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
|
## Dead Ends
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user