فهرست منبع

test(question): wait on question events (#27124)

Kit Langton 3 ماه پیش
والد
کامیت
855bda8384
2فایلهای تغییر یافته به همراه32 افزوده شده و 14 حذف شده
  1. 18 11
      packages/opencode/test/question/question.test.ts
  2. 14 3
      packages/opencode/test/tool/question.test.ts

+ 18 - 11
packages/opencode/test/question/question.test.ts

@@ -1,5 +1,5 @@
 import { afterEach, expect } from "bun:test"
 import { afterEach, expect } from "bun:test"
-import { Cause, Effect, Exit, Fiber, Layer } from "effect"
+import { Cause, Effect, Exit, Fiber, Layer, Queue } from "effect"
 import { Question } from "../../src/question"
 import { Question } from "../../src/question"
 import { Instance } from "../../src/project/instance"
 import { Instance } from "../../src/project/instance"
 import { InstanceRuntime } from "../../src/project/instance-runtime"
 import { InstanceRuntime } from "../../src/project/instance-runtime"
@@ -8,8 +8,11 @@ import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped
 import { SessionID } from "../../src/session/schema"
 import { SessionID } from "../../src/session/schema"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Bus } from "../../src/bus"
 
 
-const it = testEffect(Layer.mergeAll(Question.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(
+  Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(Bus.layer)), CrossSpawnSpawner.defaultLayer),
+)
 
 
 const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
 const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
   sessionID: SessionID
   sessionID: SessionID
@@ -44,15 +47,19 @@ const rejectAll = Effect.gen(function* () {
   yield* Effect.forEach(yield* listEffect, (req) => rejectEffect(req.id), { discard: true })
   yield* Effect.forEach(yield* listEffect, (req) => rejectEffect(req.id), { discard: true })
 })
 })
 
 
-const waitForPending = (count: number) =>
-  Effect.gen(function* () {
-    for (let i = 0; i < 100; i++) {
-      const pending = yield* listEffect
-      if (pending.length === count) return pending
-      yield* Effect.sleep("10 millis")
-    }
-    return yield* Effect.fail(new Error(`timed out waiting for ${count} pending question request(s)`))
-  })
+const waitForPending = Effect.fn("QuestionTest.waitForPending")(function* (count: number) {
+  const question = yield* Question.Service
+  const bus = yield* Bus.Service
+  const asked = yield* Queue.unbounded<void>()
+  const off = yield* bus.subscribeCallback(Question.Event.Asked, () => Queue.offerUnsafe(asked, undefined))
+  yield* Effect.addFinalizer(() => Effect.sync(off))
+
+  for (;;) {
+    const pending = yield* question.list()
+    if (pending.length === count) return pending
+    yield* Queue.take(asked).pipe(Effect.timeout("2 seconds"))
+  }
+})
 
 
 it.instance(
 it.instance(
   "ask - remains pending until answered",
   "ask - remains pending until answered",

+ 14 - 3
packages/opencode/test/tool/question.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect } from "bun:test"
 import { describe, expect } from "bun:test"
-import { Effect, Fiber, Layer } from "effect"
+import { Effect, Fiber, Layer, Queue } from "effect"
 import { QuestionTool } from "../../src/tool/question"
 import { QuestionTool } from "../../src/tool/question"
 import { Question } from "../../src/question"
 import { Question } from "../../src/question"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { SessionID, MessageID } from "../../src/session/schema"
@@ -7,6 +7,7 @@ import { Agent } from "../../src/agent/agent"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Truncate } from "@/tool/truncate"
 import { Truncate } from "@/tool/truncate"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { Bus } from "../../src/bus"
 
 
 const ctx = {
 const ctx = {
   sessionID: SessionID.make("ses_test-session"),
   sessionID: SessionID.make("ses_test-session"),
@@ -20,15 +21,25 @@ const ctx = {
 }
 }
 
 
 const it = testEffect(
 const it = testEffect(
-  Layer.mergeAll(Question.defaultLayer, CrossSpawnSpawner.defaultLayer, Truncate.defaultLayer, Agent.defaultLayer),
+  Layer.mergeAll(
+    Question.layer.pipe(Layer.provideMerge(Bus.layer)),
+    CrossSpawnSpawner.defaultLayer,
+    Truncate.defaultLayer,
+    Agent.defaultLayer,
+  ),
 )
 )
 
 
 const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Question.Interface) {
 const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Question.Interface) {
+  const bus = yield* Bus.Service
+  const asked = yield* Queue.unbounded<void>()
+  const off = yield* bus.subscribeCallback(Question.Event.Asked, () => Queue.offerUnsafe(asked, undefined))
+  yield* Effect.addFinalizer(() => Effect.sync(off))
+
   for (;;) {
   for (;;) {
     const items = yield* question.list()
     const items = yield* question.list()
     const item = items[0]
     const item = items[0]
     if (item) return item
     if (item) return item
-    yield* Effect.sleep("10 millis")
+    yield* Queue.take(asked).pipe(Effect.timeout("2 seconds"))
   }
   }
 })
 })