chore: initialize qiming workspace repository
This commit is contained in:
161
qimingcode/packages/opencode/test/bus/bus-effect.test.ts
Normal file
161
qimingcode/packages/opencode/test/bus/bus-effect.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect, Layer, Schema, Stream } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const TestEvent = {
|
||||
Ping: BusEvent.define("test.effect.ping", Schema.Struct({ value: Schema.Number })),
|
||||
Pong: BusEvent.define("test.effect.pong", Schema.Struct({ message: Schema.String })),
|
||||
}
|
||||
|
||||
const node = CrossSpawnSpawner.defaultLayer
|
||||
|
||||
const live = Layer.mergeAll(Bus.layer, node)
|
||||
|
||||
const it = testEffect(live)
|
||||
|
||||
describe("Bus (Effect-native)", () => {
|
||||
it.live("publish + subscribe stream delivers events", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const received: number[] = []
|
||||
const done = yield* Deferred.make<void>()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.sync(() => {
|
||||
received.push(evt.properties.value)
|
||||
if (received.length === 2) Deferred.doneUnsafe(done, Effect.void)
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("10 millis")
|
||||
yield* bus.publish(TestEvent.Ping, { value: 1 })
|
||||
yield* bus.publish(TestEvent.Ping, { value: 2 })
|
||||
yield* Deferred.await(done)
|
||||
|
||||
expect(received).toEqual([1, 2])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("subscribe filters by event type", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const pings: number[] = []
|
||||
const done = yield* Deferred.make<void>()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.sync(() => {
|
||||
pings.push(evt.properties.value)
|
||||
Deferred.doneUnsafe(done, Effect.void)
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("10 millis")
|
||||
yield* bus.publish(TestEvent.Pong, { message: "ignored" })
|
||||
yield* bus.publish(TestEvent.Ping, { value: 42 })
|
||||
yield* Deferred.await(done)
|
||||
|
||||
expect(pings).toEqual([42])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("subscribeAll receives all types", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const types: string[] = []
|
||||
const done = yield* Deferred.make<void>()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribeAll(), (evt) =>
|
||||
Effect.sync(() => {
|
||||
types.push(evt.type)
|
||||
if (types.length === 2) Deferred.doneUnsafe(done, Effect.void)
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("10 millis")
|
||||
yield* bus.publish(TestEvent.Ping, { value: 1 })
|
||||
yield* bus.publish(TestEvent.Pong, { message: "hi" })
|
||||
yield* Deferred.await(done)
|
||||
|
||||
expect(types).toContain("test.effect.ping")
|
||||
expect(types).toContain("test.effect.pong")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("multiple subscribers each receive the event", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const a: number[] = []
|
||||
const b: number[] = []
|
||||
const doneA = yield* Deferred.make<void>()
|
||||
const doneB = yield* Deferred.make<void>()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.sync(() => {
|
||||
a.push(evt.properties.value)
|
||||
Deferred.doneUnsafe(doneA, Effect.void)
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.sync(() => {
|
||||
b.push(evt.properties.value)
|
||||
Deferred.doneUnsafe(doneB, Effect.void)
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("10 millis")
|
||||
yield* bus.publish(TestEvent.Ping, { value: 99 })
|
||||
yield* Deferred.await(doneA)
|
||||
yield* Deferred.await(doneB)
|
||||
|
||||
expect(a).toEqual([99])
|
||||
expect(b).toEqual([99])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("subscribeAll stream sees InstanceDisposed on disposal", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const types: string[] = []
|
||||
const seen = yield* Deferred.make<void>()
|
||||
const disposed = yield* Deferred.make<void>()
|
||||
|
||||
// Set up subscriber inside the instance
|
||||
yield* Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
yield* Stream.runForEach(bus.subscribeAll(), (evt) =>
|
||||
Effect.sync(() => {
|
||||
types.push(evt.type)
|
||||
if (evt.type === TestEvent.Ping.type) Deferred.doneUnsafe(seen, Effect.void)
|
||||
if (evt.type === Bus.InstanceDisposed.type) Deferred.doneUnsafe(disposed, Effect.void)
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("10 millis")
|
||||
yield* bus.publish(TestEvent.Ping, { value: 1 })
|
||||
yield* Deferred.await(seen)
|
||||
}).pipe(provideInstance(dir))
|
||||
|
||||
// Dispose from OUTSIDE the instance scope
|
||||
yield* Effect.promise(() => Instance.disposeAll())
|
||||
yield* Deferred.await(disposed).pipe(Effect.timeout("2 seconds"))
|
||||
|
||||
expect(types).toContain("test.effect.ping")
|
||||
expect(types).toContain(Bus.InstanceDisposed.type)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = BusEvent.define("test.integration", Schema.Struct({ value: Schema.Number }))
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
return Instance.provide({ directory, fn })
|
||||
}
|
||||
|
||||
describe("Bus integration: acquireRelease subscriber pattern", () => {
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
test("subscriber via callback facade receives events and cleans up on unsub", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: number[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
const unsub = Bus.subscribe(TestEvent, (evt) => {
|
||||
received.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent, { value: 1 })
|
||||
await Bus.publish(TestEvent, { value: 2 })
|
||||
await Bun.sleep(10)
|
||||
|
||||
expect(received).toEqual([1, 2])
|
||||
|
||||
unsub()
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent, { value: 3 })
|
||||
await Bun.sleep(10)
|
||||
|
||||
expect(received).toEqual([1, 2])
|
||||
})
|
||||
})
|
||||
|
||||
test("subscribeAll receives events from multiple types", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: Array<{ type: string; value?: number }> = []
|
||||
|
||||
const OtherEvent = BusEvent.define("test.other", Schema.Struct({ value: Schema.Number }))
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribeAll((evt) => {
|
||||
received.push({ type: evt.type, value: evt.properties.value })
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent, { value: 10 })
|
||||
await Bus.publish(OtherEvent, { value: 20 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(received).toEqual([
|
||||
{ type: "test.integration", value: 10 },
|
||||
{ type: "test.other", value: 20 },
|
||||
])
|
||||
})
|
||||
|
||||
test("subscriber cleanup on instance disposal interrupts the stream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: number[] = []
|
||||
let disposed = false
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribeAll((evt) => {
|
||||
if (evt.type === Bus.InstanceDisposed.type) {
|
||||
disposed = true
|
||||
return
|
||||
}
|
||||
received.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent, { value: 1 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
await Instance.disposeAll()
|
||||
await Bun.sleep(50)
|
||||
|
||||
expect(received).toEqual([1])
|
||||
expect(disposed).toBe(true)
|
||||
})
|
||||
})
|
||||
219
qimingcode/packages/opencode/test/bus/bus.test.ts
Normal file
219
qimingcode/packages/opencode/test/bus/bus.test.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = {
|
||||
Ping: BusEvent.define("test.ping", Schema.Struct({ value: Schema.Number })),
|
||||
Pong: BusEvent.define("test.pong", Schema.Struct({ message: Schema.String })),
|
||||
}
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
return Instance.provide({ directory, fn })
|
||||
}
|
||||
|
||||
describe("Bus", () => {
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
describe("publish + subscribe", () => {
|
||||
test("subscriber is live immediately after subscribe returns", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: number[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
received.push(evt.properties.value)
|
||||
})
|
||||
await Bus.publish(TestEvent.Ping, { value: 42 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(received).toEqual([42])
|
||||
})
|
||||
|
||||
test("subscriber receives matching events", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: number[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
received.push(evt.properties.value)
|
||||
})
|
||||
// Give the subscriber fiber time to start consuming
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Ping, { value: 42 })
|
||||
await Bus.publish(TestEvent.Ping, { value: 99 })
|
||||
// Give subscriber time to process
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(received).toEqual([42, 99])
|
||||
})
|
||||
|
||||
test("subscriber does not receive events of other types", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const pings: number[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
pings.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Pong, { message: "hello" })
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(pings).toEqual([1])
|
||||
})
|
||||
|
||||
test("publish with no subscribers does not throw", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("unsubscribe", () => {
|
||||
test("unsubscribe stops delivery", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: number[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
const unsub = Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
received.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
await Bun.sleep(10)
|
||||
unsub()
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Ping, { value: 2 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(received).toEqual([1])
|
||||
})
|
||||
})
|
||||
|
||||
describe("subscribeAll", () => {
|
||||
test("subscribeAll is live immediately after subscribe returns", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: string[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribeAll((evt) => {
|
||||
received.push(evt.type)
|
||||
})
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(received).toEqual(["test.ping"])
|
||||
})
|
||||
|
||||
test("receives all event types", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: string[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribeAll((evt) => {
|
||||
received.push(evt.type)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
await Bus.publish(TestEvent.Pong, { message: "hi" })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(received).toContain("test.ping")
|
||||
expect(received).toContain("test.pong")
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiple subscribers", () => {
|
||||
test("all subscribers for same event type are called", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a: number[] = []
|
||||
const b: number[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
a.push(evt.properties.value)
|
||||
})
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
b.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Ping, { value: 7 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(a).toEqual([7])
|
||||
expect(b).toEqual([7])
|
||||
})
|
||||
})
|
||||
|
||||
describe("instance isolation", () => {
|
||||
test("events in one directory do not reach subscribers in another", async () => {
|
||||
await using tmpA = await tmpdir()
|
||||
await using tmpB = await tmpdir()
|
||||
const receivedA: number[] = []
|
||||
const receivedB: number[] = []
|
||||
|
||||
await withInstance(tmpA.path, async () => {
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
receivedA.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
await withInstance(tmpB.path, async () => {
|
||||
Bus.subscribe(TestEvent.Ping, (evt) => {
|
||||
receivedB.push(evt.properties.value)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
await withInstance(tmpA.path, async () => {
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
await withInstance(tmpB.path, async () => {
|
||||
await Bus.publish(TestEvent.Ping, { value: 2 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
expect(receivedA).toEqual([1])
|
||||
expect(receivedB).toEqual([2])
|
||||
})
|
||||
})
|
||||
|
||||
describe("instance disposal", () => {
|
||||
test("InstanceDisposed is delivered to wildcard subscribers before stream ends", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: string[] = []
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribeAll((evt) => {
|
||||
received.push(evt.type)
|
||||
})
|
||||
await Bun.sleep(10)
|
||||
await Bus.publish(TestEvent.Ping, { value: 1 })
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
// Instance.disposeAll triggers the finalizer which publishes InstanceDisposed
|
||||
await Instance.disposeAll()
|
||||
await Bun.sleep(50)
|
||||
|
||||
expect(received).toContain("test.ping")
|
||||
expect(received).toContain(Bus.InstanceDisposed.type)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user