fix(core): stop logging errors when optional watched folders are missing (#60955)

This commit is contained in:
Kit Langton
2026-10-31 13:20:40 -07:00
committed by GitHub
Unverified
parent a1b616b3b9
commit ed892fbd37
2 changed files with 43 additions and 15 deletions
+14 -9
View File
@@ -5,6 +5,7 @@ import { createWrapper } from "@parcel/watcher/wrapper.js"
import type ParcelWatcher from "@parcel/watcher"
import { makeGlobalNode } from "@opencode/util/effect/app-node"
import { FSUtil } from "@opencode/util/fs-util"
import { isRecord } from "@opencode/util/record"
import { Cause, Context, Effect, Layer, PubSub, RcMap, Stream } from "effect"
import { lazy } from "../util/lazy.js"
import { watch } from "node:fs"
@@ -205,14 +206,16 @@ export const nativeLayer = Layer.succeed(
if (input.type === "file" || input.type === "entries") {
const directory = input.type === "file" ? path.dirname(input.target) : input.target
const names = new Set(input.type === "file" ? [path.basename(input.target)] : input.names)
return Effect.try(() =>
watch(directory, { recursive: false }, (_event, file) => {
if (file && !names.has(file)) return
for (const name of file ? [file] : names) {
input.publish({ path: path.join(directory, name), type: "update" })
}
}),
).pipe(
return Effect.try({
try: () =>
watch(directory, { recursive: false }, (_event, file) => {
if (file && !names.has(file)) return
for (const name of file ? [file] : names) {
input.publish({ path: path.join(directory, name), type: "update" })
}
}),
catch: (error) => error,
}).pipe(
Effect.map((subscription): Subscription => {
subscription.on("error", (error: unknown) =>
Effect.runFork(Effect.logError("watcher callback failed", { path: directory, error })),
@@ -221,7 +224,9 @@ export const nativeLayer = Layer.succeed(
}),
// A missing directory (ENOENT) ends subscriber streams instead of crashing the watch.
Effect.catch((error) =>
Effect.logError("failed to watch directory", { path: directory, error }).pipe(Effect.as(undefined)),
isRecord(error) && error.code === "ENOENT"
? Effect.undefined
: Effect.logError("failed to watch directory", { path: directory, error }).pipe(Effect.as(undefined)),
),
)
}
+29 -6
View File
@@ -2,7 +2,7 @@ import { $ } from "bun"
import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Deferred, Duration, Effect, Fiber, Layer, Option, Schedule, Stream } from "effect"
import { Deferred, Duration, Effect, Fiber, Layer, Logger, Option, Schedule, Stream } from "effect"
import { Config } from "@opencode/core/config"
import { ConfigLocationWatcherPlugin } from "@opencode/core/config/plugin/location-watcher"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
@@ -54,15 +54,38 @@ function countingNative() {
}
describe("Watcher lifecycle", () => {
it.live("ends the stream when the watched directory is missing", () =>
withTmp((directory) =>
it.live("ends the stream without logging an error when the watched directory is missing", () => {
const errors: unknown[] = []
const logger = Logger.make(({ logLevel, message }) => {
if (logLevel === "Error") errors.push(message)
})
return withTmp((directory) =>
Effect.gen(function* () {
const watcher = yield* Watcher.Service
const updates = yield* watcher.subscribe({ path: path.join(directory, "missing", "a.ts"), type: "file" })
expect(Option.isNone(yield* Stream.runHead(updates))).toBe(true)
}).pipe(Effect.provide(AppNodeBuilder.build(Watcher.node))),
),
)
expect(errors).toEqual([])
}).pipe(Effect.provide(AppNodeBuilder.build(Watcher.node)), Effect.provide(Logger.layer([logger]))),
)
})
it.live("still logs an error when watching fails for a reason other than ENOENT", () => {
const errors: unknown[] = []
const logger = Logger.make(({ logLevel, message }) => {
if (logLevel === "Error") errors.push(message)
})
return withTmp((directory) =>
Effect.gen(function* () {
const watcher = yield* Watcher.Service
const updates = yield* watcher.subscribe({
path: path.join(directory, "bad\0dir", "a.ts"),
type: "file",
})
expect(Option.isNone(yield* Stream.runHead(updates))).toBe(true)
expect(errors).toHaveLength(1)
}).pipe(Effect.provide(AppNodeBuilder.build(Watcher.node)), Effect.provide(Logger.layer([logger]))),
)
})
it.effect("signals readiness after acquisition and buffers updates published by the ready callback", () =>
Effect.gen(function* () {