Преглед изворни кода

test: migrate mcp oauth browser tests (#27345)

Kit Langton пре 3 месеци
родитељ
комит
76c91c6e33
1 измењених фајлова са 108 додато и 143 уклоњено
  1. 108 143
      packages/opencode/test/mcp/oauth-browser.test.ts

+ 108 - 143
packages/opencode/test/mcp/oauth-browser.test.ts

@@ -1,15 +1,19 @@
-import { test, expect, mock, beforeEach } from "bun:test"
+import { expect, mock, beforeEach } from "bun:test"
 import { EventEmitter } from "events"
 import { EventEmitter } from "events"
-import { Effect } from "effect"
+import { Deferred, Effect, Layer, Option } from "effect"
+import type { Duration } from "effect"
+import { testEffect } from "../lib/effect"
 import type { MCP as MCPNS } from "../../src/mcp/index"
 import type { MCP as MCPNS } from "../../src/mcp/index"
 
 
 // Track open() calls and control failure behavior
 // Track open() calls and control failure behavior
 let openShouldFail = false
 let openShouldFail = false
 let openCalledWith: string | undefined
 let openCalledWith: string | undefined
+let openDeferred: Deferred.Deferred<string> | undefined
 
 
 void mock.module("open", () => ({
 void mock.module("open", () => ({
   default: async (url: string) => {
   default: async (url: string) => {
     openCalledWith = url
     openCalledWith = url
+    if (openDeferred) Effect.runSync(Deferred.succeed(openDeferred, url).pipe(Effect.ignore))
 
 
     // Return a mock subprocess that emits an error if openShouldFail is true
     // Return a mock subprocess that emits an error if openShouldFail is true
     const subprocess = new EventEmitter()
     const subprocess = new EventEmitter()
@@ -97,173 +101,134 @@ void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
 beforeEach(() => {
 beforeEach(() => {
   openShouldFail = false
   openShouldFail = false
   openCalledWith = undefined
   openCalledWith = undefined
+  openDeferred = undefined
   transportCalls.length = 0
   transportCalls.length = 0
 })
 })
 
 
 // Import modules after mocking
 // Import modules after mocking
 const { MCP } = await import("../../src/mcp/index")
 const { MCP } = await import("../../src/mcp/index")
-const { AppRuntime } = await import("../../src/effect/app-runtime")
 const { Bus } = await import("../../src/bus")
 const { Bus } = await import("../../src/bus")
+const { Config } = await import("../../src/config/config")
+const { McpAuth } = await import("../../src/mcp/auth")
 const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
 const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
-const { Instance } = await import("../../src/project/instance")
-const { WithInstance } = await import("../../src/project/with-instance")
-const { tmpdir } = await import("../fixture/fixture")
+const { AppFileSystem } = await import("@opencode-ai/core/filesystem")
+const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
+const mcpTest = testEffect(
+  MCP.layer.pipe(
+    Layer.provide(McpAuth.defaultLayer),
+    Layer.provideMerge(Bus.layer),
+    Layer.provide(Config.defaultLayer),
+    Layer.provide(CrossSpawnSpawner.defaultLayer),
+    Layer.provide(AppFileSystem.defaultLayer),
+  ),
+)
 const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
 const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
 
 
-test("BrowserOpenFailed event is published when open() throws", async () => {
-  await using tmp = await tmpdir({
-    init: async (dir) => {
-      await Bun.write(
-        `${dir}/opencode.json`,
-        JSON.stringify({
-          $schema: "https://opencode.ai/config.json",
-          mcp: {
-            "test-oauth-server": {
-              type: "remote",
-              url: "https://example.com/mcp",
-            },
-          },
-        }),
-      )
+const config = (name: string) => ({
+  mcp: {
+    [name]: {
+      type: "remote" as const,
+      url: "https://example.com/mcp",
     },
     },
-  })
-
-  await WithInstance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      openShouldFail = true
-
-      const events: Array<{ mcpName: string; url: string }> = []
-      const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
-        events.push(evt.properties)
-      })
-
-      // Run authenticate with a timeout to avoid waiting forever for the callback
-      // Attach a handler immediately so callback shutdown rejections
-      // don't show up as unhandled between tests.
-      const authPromise = AppRuntime.runPromise(
-        Effect.gen(function* () {
-          const mcp = yield* service
-          return yield* mcp.authenticate("test-oauth-server")
-        }),
-      ).catch(() => undefined)
-
-      // Config.get() can be slow in tests, so give it plenty of time.
-      await new Promise((resolve) => setTimeout(resolve, 2_000))
-
-      // Stop the callback server and cancel any pending auth
-      await McpOAuthCallback.stop()
-
-      await authPromise
+  },
+})
 
 
-      unsubscribe()
+const withCallbackStop = Effect.addFinalizer(() => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore))
+
+const awaitWithTimeout = <A, E, R>(
+  self: Effect.Effect<A, E, R>,
+  message: string,
+  duration: Duration.Input = "5 seconds",
+) =>
+  self.pipe(
+    Effect.timeoutOrElse({
+      duration,
+      orElse: () => Effect.fail(new Error(message)),
+    }),
+  )
+
+const trackBrowserOpen = Effect.gen(function* () {
+  const opened = yield* Deferred.make<string>()
+  openDeferred = opened
+  yield* Effect.addFinalizer(() => Effect.sync(() => (openDeferred = undefined)))
+  return opened
+})
 
 
-      // Verify the BrowserOpenFailed event was published
-      expect(events.length).toBe(1)
-      expect(events[0].mcpName).toBe("test-oauth-server")
-      expect(events[0].url).toContain("https://")
-    },
+const trackBrowserOpenFailed = Effect.gen(function* () {
+  const bus = yield* Bus.Service
+  const event = yield* Deferred.make<{ mcpName: string; url: string }>()
+  const unsubscribe = yield* bus.subscribeCallback(MCP.BrowserOpenFailed, (evt) => {
+    Effect.runSync(Deferred.succeed(event, evt.properties).pipe(Effect.ignore))
   })
   })
+  yield* Effect.addFinalizer(() => Effect.sync(unsubscribe))
+  return event
 })
 })
 
 
-test("BrowserOpenFailed event is NOT published when open() succeeds", async () => {
-  await using tmp = await tmpdir({
-    init: async (dir) => {
-      await Bun.write(
-        `${dir}/opencode.json`,
-        JSON.stringify({
-          $schema: "https://opencode.ai/config.json",
-          mcp: {
-            "test-oauth-server-2": {
-              type: "remote",
-              url: "https://example.com/mcp",
-            },
-          },
-        }),
-      )
-    },
+const authenticateScoped = (name: string) =>
+  Effect.gen(function* () {
+    const mcp = yield* service
+    yield* mcp.authenticate(name).pipe(Effect.ignore, Effect.catchCause(() => Effect.void), Effect.forkScoped)
   })
   })
 
 
-  await WithInstance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      openShouldFail = false
-
-      const events: Array<{ mcpName: string; url: string }> = []
-      const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
-        events.push(evt.properties)
-      })
+mcpTest.instance(
+  "BrowserOpenFailed event is published when open() throws",
+  () =>
+    Effect.gen(function* () {
+      yield* withCallbackStop
+      openShouldFail = true
 
 
-      // Run authenticate with a timeout to avoid waiting forever for the callback
-      const authPromise = AppRuntime.runPromise(
-        Effect.gen(function* () {
-          const mcp = yield* service
-          return yield* mcp.authenticate("test-oauth-server-2")
-        }),
-      ).catch(() => undefined)
+      const event = yield* trackBrowserOpenFailed
+      yield* authenticateScoped("test-oauth-server")
 
 
-      // Config.get() can be slow in tests; also covers the ~500ms open() error-detection window.
-      await new Promise((resolve) => setTimeout(resolve, 2_000))
+      const failure = yield* awaitWithTimeout(
+        Deferred.await(event),
+        "Timed out waiting for BrowserOpenFailed event",
+      )
 
 
-      // Stop the callback server and cancel any pending auth
-      await McpOAuthCallback.stop()
+      expect(failure.mcpName).toBe("test-oauth-server")
+      expect(failure.url).toContain("https://")
+    }),
+  { config: config("test-oauth-server") },
+)
+
+mcpTest.instance(
+  "BrowserOpenFailed event is NOT published when open() succeeds",
+  () =>
+    Effect.gen(function* () {
+      yield* withCallbackStop
+      openShouldFail = false
 
 
-      await authPromise
+      const opened = yield* trackBrowserOpen
+      const event = yield* trackBrowserOpenFailed
+      yield* authenticateScoped("test-oauth-server-2")
 
 
-      unsubscribe()
+      yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()")
+      const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
 
 
-      // Verify NO BrowserOpenFailed event was published
-      expect(events.length).toBe(0)
-      // Verify open() was still called
+      expect(failure).toEqual(Option.none())
       expect(openCalledWith).toBeDefined()
       expect(openCalledWith).toBeDefined()
-    },
-  })
-})
-
-test("open() is called with the authorization URL", async () => {
-  await using tmp = await tmpdir({
-    init: async (dir) => {
-      await Bun.write(
-        `${dir}/opencode.json`,
-        JSON.stringify({
-          $schema: "https://opencode.ai/config.json",
-          mcp: {
-            "test-oauth-server-3": {
-              type: "remote",
-              url: "https://example.com/mcp",
-            },
-          },
-        }),
-      )
-    },
-  })
-
-  await WithInstance.provide({
-    directory: tmp.path,
-    fn: async () => {
+    }),
+  { config: config("test-oauth-server-2") },
+)
+
+mcpTest.instance(
+  "open() is called with the authorization URL",
+  () =>
+    Effect.gen(function* () {
+      yield* withCallbackStop
       openShouldFail = false
       openShouldFail = false
       openCalledWith = undefined
       openCalledWith = undefined
 
 
-      // Run authenticate with a timeout to avoid waiting forever for the callback
-      const authPromise = AppRuntime.runPromise(
-        Effect.gen(function* () {
-          const mcp = yield* service
-          return yield* mcp.authenticate("test-oauth-server-3")
-        }),
-      ).catch(() => undefined)
-
-      // Config.get() can be slow in tests; also covers the ~500ms open() error-detection window.
-      await new Promise((resolve) => setTimeout(resolve, 2_000))
-
-      // Stop the callback server and cancel any pending auth
-      await McpOAuthCallback.stop()
+      const opened = yield* trackBrowserOpen
+      const event = yield* trackBrowserOpenFailed
+      yield* authenticateScoped("test-oauth-server-3")
 
 
-      await authPromise
+      const url = yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()")
+      const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
 
 
-      // Verify open was called with a URL
-      expect(openCalledWith).toBeDefined()
-      expect(typeof openCalledWith).toBe("string")
-      expect(openCalledWith!).toContain("https://")
-    },
-  })
-})
+      expect(failure).toEqual(Option.none())
+      expect(typeof url).toBe("string")
+      expect(url).toContain("https://")
+    }),
+  { config: config("test-oauth-server-3") },
+)