Просмотр исходного кода

fix: restore recent test regressions and upgrade effect beta (#18158)

Luke Parker 1 месяц назад
Родитель
Сommit
5d2f8d77f9

+ 10 - 7
bun.lock

@@ -46,7 +46,7 @@
         "@solidjs/router": "catalog:",
         "@thisbeyond/solid-dnd": "0.7.5",
         "diff": "catalog:",
-        "effect": "4.0.0-beta.31",
+        "effect": "catalog:",
         "fuzzysort": "catalog:",
         "ghostty-web": "github:anomalyco/ghostty-web#main",
         "luxon": "catalog:",
@@ -227,7 +227,7 @@
         "@solid-primitives/storage": "catalog:",
         "@solidjs/meta": "catalog:",
         "@solidjs/router": "0.15.4",
-        "effect": "4.0.0-beta.31",
+        "effect": "catalog:",
         "electron-log": "^5",
         "electron-store": "^10",
         "electron-updater": "^6",
@@ -324,7 +324,7 @@
         "@ai-sdk/xai": "2.0.51",
         "@aws-sdk/credential-providers": "3.993.0",
         "@clack/prompts": "1.0.0-alpha.1",
-        "@effect/platform-node": "4.0.0-beta.31",
+        "@effect/platform-node": "catalog:",
         "@gitlab/gitlab-ai-provider": "3.6.0",
         "@gitlab/opencode-gitlab-auth": "1.3.3",
         "@hono/standard-validator": "0.1.5",
@@ -594,6 +594,7 @@
   },
   "catalog": {
     "@cloudflare/workers-types": "4.20251008.0",
+    "@effect/platform-node": "4.0.0-beta.35",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@octokit/rest": "22.0.0",
@@ -617,7 +618,7 @@
     "dompurify": "3.3.1",
     "drizzle-kit": "1.0.0-beta.16-ea816b6",
     "drizzle-orm": "1.0.0-beta.16-ea816b6",
-    "effect": "4.0.0-beta.31",
+    "effect": "4.0.0-beta.35",
     "fuzzysort": "3.1.0",
     "hono": "4.10.7",
     "hono-openapi": "1.1.2",
@@ -975,9 +976,9 @@
 
     "@effect/language-service": ["@effect/[email protected]", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
 
-    "@effect/platform-node": ["@effect/[email protected]1", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.31", "mime": "^4.1.0", "undici": "^7.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.31", "ioredis": "^5.7.0" } }, "sha512-KmVZwGsQRBMZZYPJwpL2vj6sxjBzfXhyA8RgsH5/cmckDTsZpVTyqODQ/FFzmCnMWuYjZoJGPghTDrVVDn/6ZA=="],
+    "@effect/platform-node": ["@effect/[email protected]5", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.35", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35", "ioredis": "^5.7.0" } }, "sha512-HPc2xZASl9F9y/xJ01bQgFD6Jf9XP4Fcv/BlVTvG0Yr/uN63lwKZYr/VXor5K5krHfBDeCBD8y7/SICPYZoq3A=="],
 
-    "@effect/platform-node-shared": ["@effect/[email protected]3", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-jaJnvYz1IiPZyN//fCJsvwnmujJS5KD8noCVVLhb4ZGCWKhQpt0x2iuax6HFzMlPEQSfl04GLU+PVKh0nkzPyA=="],
+    "@effect/platform-node-shared": ["@effect/[email protected]5", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35" } }, "sha512-9bPqNV988itKJ7MQoJuzmR014DB9EZRDOnhJt/+iJlb8qLoR9HnCzNJb9gfBdYhFmVYc8DMsQxG81rdJzpv9tg=="],
 
     "@electron/asar": ["@electron/[email protected]", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
 
@@ -2751,7 +2752,7 @@
 
     "ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
 
-    "effect": ["[email protected]1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="],
+    "effect": ["[email protected]5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-64j8dgJmoEMeq6Y3WLYcZIRqPZ5E/lqnULCf6QW5te3hQ/sa13UodWLGwBEviEqBoq72U8lArhVX+T7ntzhJGQ=="],
 
     "ejs": ["[email protected]", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
 
@@ -5019,6 +5020,8 @@
 
     "@dot/log/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 
+    "@effect/platform-node/undici": ["[email protected]", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
+
     "@effect/platform-node-shared/ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
 
     "@electron/asar/commander": ["[email protected]", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],

+ 2 - 1
package.json

@@ -25,6 +25,7 @@
       "packages/slack"
     ],
     "catalog": {
+      "@effect/platform-node": "4.0.0-beta.35",
       "@types/bun": "1.3.9",
       "@octokit/rest": "22.0.0",
       "@hono/zod-validator": "0.4.2",
@@ -44,7 +45,7 @@
       "dompurify": "3.3.1",
       "drizzle-kit": "1.0.0-beta.16-ea816b6",
       "drizzle-orm": "1.0.0-beta.16-ea816b6",
-      "effect": "4.0.0-beta.31",
+      "effect": "4.0.0-beta.35",
       "ai": "5.0.124",
       "hono": "4.10.7",
       "hono-openapi": "1.1.2",

+ 12 - 6
packages/app/e2e/prompt/prompt-multiline.spec.ts

@@ -7,12 +7,18 @@ test("shift+enter inserts a newline without submitting", async ({ page, gotoSess
   await expect(page).toHaveURL(/\/session\/?$/)
 
   const prompt = page.locator(promptSelector)
-  await prompt.click()
-  await page.keyboard.type("line one")
-  await page.keyboard.press("Shift+Enter")
-  await page.keyboard.type("line two")
+  await prompt.focus()
+  await expect(prompt).toBeFocused()
+
+  await prompt.pressSequentially("line one")
+  await expect(prompt).toBeFocused()
+
+  await prompt.press("Shift+Enter")
+  await expect(page).toHaveURL(/\/session\/?$/)
+  await expect(prompt).toBeFocused()
+
+  await prompt.pressSequentially("line two")
 
   await expect(page).toHaveURL(/\/session\/?$/)
-  await expect(prompt).toContainText("line one")
-  await expect(prompt).toContainText("line two")
+  await expect.poll(() => prompt.evaluate((el) => el.innerText)).toBe("line one\nline two")
 })

+ 1 - 1
packages/app/package.json

@@ -56,7 +56,7 @@
     "@solidjs/router": "catalog:",
     "@thisbeyond/solid-dnd": "0.7.5",
     "diff": "catalog:",
-    "effect": "4.0.0-beta.31",
+    "effect": "catalog:",
     "fuzzysort": "catalog:",
     "ghostty-web": "github:anomalyco/ghostty-web#main",
     "luxon": "catalog:",

+ 1 - 0
packages/app/src/pages/layout.tsx

@@ -566,6 +566,7 @@ export default function Layout(props: ParentProps) {
   const [autoselecting] = createResource(async () => {
     await ready.promise
     await layout.ready.promise
+    if (!untrack(() => state.autoselect)) return
 
     const list = layout.projects.list()
     const last = server.projects.last()

+ 1 - 1
packages/desktop-electron/package.json

@@ -30,7 +30,7 @@
     "@solid-primitives/storage": "catalog:",
     "@solidjs/meta": "catalog:",
     "@solidjs/router": "0.15.4",
-    "effect": "4.0.0-beta.31",
+    "effect": "catalog:",
     "electron-log": "^5",
     "electron-store": "^10",
     "electron-updater": "^6",

+ 1 - 1
packages/opencode/package.json

@@ -82,7 +82,6 @@
     "@ai-sdk/xai": "2.0.51",
     "@aws-sdk/credential-providers": "3.993.0",
     "@clack/prompts": "1.0.0-alpha.1",
-    "@effect/platform-node": "4.0.0-beta.31",
     "@gitlab/gitlab-ai-provider": "3.6.0",
     "@gitlab/opencode-gitlab-auth": "1.3.3",
     "@hono/standard-validator": "0.1.5",
@@ -98,6 +97,7 @@
     "@openrouter/ai-sdk-provider": "1.5.4",
     "@opentui/core": "0.1.87",
     "@opentui/solid": "0.1.87",
+    "@effect/platform-node": "catalog:",
     "@parcel/watcher": "2.5.1",
     "@pierre/diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

+ 8 - 1
packages/opencode/src/file/watcher.ts

@@ -51,6 +51,13 @@ export namespace FileWatcher {
     if (process.platform === "linux") return "inotify"
   }
 
+  function protecteds(dir: string) {
+    return Protected.paths().filter((item) => {
+      const rel = path.relative(dir, item)
+      return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
+    })
+  }
+
   export const hasNativeBinding = () => !!watcher()
 
   export class Service extends ServiceMap.Service<Service, {}>()("@opencode/FileWatcher") {}
@@ -105,7 +112,7 @@ export namespace FileWatcher {
       const cfgIgnores = cfg.watcher?.ignore ?? []
 
       if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
-        yield* subscribe(instance.directory, [...FileIgnore.PATTERNS, ...cfgIgnores, ...Protected.paths()])
+        yield* subscribe(instance.directory, [...FileIgnore.PATTERNS, ...cfgIgnores, ...protecteds(instance.directory)])
       }
 
       if (instance.project.vcs === "git") {

+ 1 - 0
packages/opencode/src/util/process.ts

@@ -98,6 +98,7 @@ export namespace Process {
         reject(error)
       })
     })
+    void exited.catch(() => undefined)
 
     if (opts.abort) {
       opts.abort.addEventListener("abort", abort, { once: true })

+ 26 - 28
packages/opencode/test/file/watcher.test.ts

@@ -2,7 +2,7 @@ import { $ } from "bun"
 import { afterEach, describe, expect, test } from "bun:test"
 import fs from "fs/promises"
 import path from "path"
-import { Deferred, Effect, Fiber, Option } from "effect"
+import { Deferred, Effect, Option } from "effect"
 import { tmpdir } from "../fixture/fixture"
 import { watcherConfigLayer, withServices } from "../fixture/instance"
 import { FileWatcher } from "../../src/file/watcher"
@@ -25,6 +25,7 @@ function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
     directory,
     FileWatcher.layer,
     async (rt) => {
+      await rt.runPromise(FileWatcher.Service.use(() => Effect.void))
       await Effect.runPromise(ready(directory))
       await Effect.runPromise(body)
     },
@@ -54,24 +55,29 @@ function listen(directory: string, check: (evt: WatcherEvent) => boolean, hit: (
 }
 
 function wait(directory: string, check: (evt: WatcherEvent) => boolean) {
-  return Effect.callback<WatcherEvent>((resume) => {
-    const cleanup = listen(directory, check, (evt) => {
-      cleanup()
-      resume(Effect.succeed(evt))
+  return Effect.gen(function* () {
+    const deferred = yield* Deferred.make<WatcherEvent>()
+    const cleanup = yield* Effect.sync(() => {
+      let off = () => {}
+      off = listen(directory, check, (evt) => {
+        off()
+        Deferred.doneUnsafe(deferred, Effect.succeed(evt))
+      })
+      return off
     })
-    return Effect.sync(cleanup)
-  }).pipe(Effect.timeout("5 seconds"))
+    return { cleanup, deferred }
+  })
 }
 
 function nextUpdate<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) {
   return Effect.acquireUseRelease(
-    wait(directory, check).pipe(Effect.forkChild({ startImmediately: true })),
-    (fiber) =>
+    wait(directory, check),
+    ({ deferred }) =>
       Effect.gen(function* () {
         yield* trigger
-        return yield* Fiber.join(fiber)
+        return yield* Deferred.await(deferred).pipe(Effect.timeout("5 seconds"))
       }),
-    Fiber.interrupt,
+    ({ cleanup }) => Effect.sync(cleanup),
   )
 }
 
@@ -82,23 +88,15 @@ function noUpdate<E>(
   trigger: Effect.Effect<void, E>,
   ms = 500,
 ) {
-  return Effect.gen(function* () {
-    const deferred = yield* Deferred.make<WatcherEvent>()
-
-    yield* Effect.acquireUseRelease(
-      Effect.sync(() =>
-        listen(directory, check, (evt) => {
-          Effect.runSync(Deferred.succeed(deferred, evt))
-        }),
-      ),
-      () =>
-        Effect.gen(function* () {
-          yield* trigger
-          expect(yield* Deferred.await(deferred).pipe(Effect.timeoutOption(`${ms} millis`))).toEqual(Option.none())
-        }),
-      (cleanup) => Effect.sync(cleanup),
-    )
-  })
+  return Effect.acquireUseRelease(
+    wait(directory, check),
+    ({ deferred }) =>
+      Effect.gen(function* () {
+        yield* trigger
+        expect(yield* Deferred.await(deferred).pipe(Effect.timeoutOption(`${ms} millis`))).toEqual(Option.none())
+      }),
+    ({ cleanup }) => Effect.sync(cleanup),
+  )
 }
 
 function ready(directory: string) {

+ 16 - 0
packages/opencode/test/util/process.test.ts

@@ -109,4 +109,20 @@ describe("util.process", () => {
 
     expect(await proc.exited).toBe(0)
   })
+
+  test("rejects missing commands without leaking unhandled errors", async () => {
+    await using tmp = await tmpdir()
+    const cmd = path.join(tmp.path, "missing" + (process.platform === "win32" ? ".cmd" : ""))
+    const err = await Process.spawn([cmd], {
+      stdin: "pipe",
+      stdout: "pipe",
+      stderr: "pipe",
+    }).exited.catch((err) => err)
+
+    expect(err).toBeInstanceOf(Error)
+    if (!(err instanceof Error)) throw err
+    expect(err).toMatchObject({
+      code: "ENOENT",
+    })
+  })
 })