From ed892fbd371286460fffe55701a5852d653da9f9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 31 Oct 2026 13:20:40 -0700 Subject: [PATCH] fix(core): stop logging errors when optional watched folders are missing (#60955) --- packages/core/src/filesystem/watcher.ts | 23 +++++++----- packages/core/test/filesystem/watcher.test.ts | 35 +++++++++++++++---- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts index ae359e36658..47c4bd255bf 100644 --- a/packages/core/src/filesystem/watcher.ts +++ b/packages/core/src/filesystem/watcher.ts @@ -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)), ), ) } diff --git a/packages/core/test/filesystem/watcher.test.ts b/packages/core/test/filesystem/watcher.test.ts index b50ac8b9978..13214d5ca81 100644 --- a/packages/core/test/filesystem/watcher.test.ts +++ b/packages/core/test/filesystem/watcher.test.ts @@ -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* () {