| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- import { describe, expect, test } from "bun:test"
- import { AIError, TransportReason } from "@opencode-ai/ai"
- import type {
- ChannelObservation,
- WebSocketChannelExchange,
- WebSocketConnection,
- WebSocketConnector,
- } from "@opencode-ai/ai/route"
- import { SessionModelTransport } from "@opencode-ai/core/session/model-transport"
- import { Session } from "@opencode-ai/schema/session"
- import { Deferred, Effect, Fiber, Metric, Queue, Stream } from "effect"
- import { TestClock } from "effect/testing"
- import { Headers } from "effect/unstable/http"
- const session = Session.ID.make("ses_transport")
- const otherSession = Session.ID.make("ses_transport_other")
- const queue = <A, E = never>() => Effect.runSync(Queue.unbounded<A, E>())
- const error = (message: string, delivery?: TransportReason["delivery"]) =>
- new AIError({
- module: "test",
- method: "websocket",
- reason: new TransportReason({ message, transport: "websocket", operation: "write", phase: "send", delivery }),
- })
- const exchange = (
- id: string,
- input: {
- readonly headers?: Record<string, string>
- readonly fallback?: () => Stream.Stream<string, AIError>
- readonly rotateAfterMs?: number
- } = {},
- ): WebSocketChannelExchange => ({
- id,
- connect: {
- url: "wss://provider.test/responses",
- headers: Headers.fromInput(input.headers),
- rotateAfterMs: input.rotateAfterMs,
- },
- fallback: input.fallback ?? (() => Stream.make(`fallback:${id}`)),
- driver: {
- create: () => Effect.succeed({ message: id, mode: "full" }),
- observe: (_create, frame): Effect.Effect<ChannelObservation, AIError> =>
- Effect.succeed({ type: "completed", frame }),
- },
- })
- const run = <A, E>(connector: WebSocketConnector, effect: Effect.Effect<A, E, SessionModelTransport.Service>) =>
- Effect.runPromise(effect.pipe(Effect.provide(SessionModelTransport.makeLayer(connector)), Effect.scoped))
- const runWithTestClock = <A, E>(
- connector: WebSocketConnector,
- effect: Effect.Effect<A, E, SessionModelTransport.Service>,
- ) =>
- Effect.runPromise(
- effect.pipe(
- Effect.provide(SessionModelTransport.makeLayer(connector)),
- Effect.scoped,
- Effect.provide(TestClock.layer()),
- ),
- )
- const collect = (executor: ReturnType<SessionModelTransport.Interface["bind"]>, item: WebSocketChannelExchange) =>
- Effect.gen(function* () {
- const execution = yield* executor.execute(item)
- return Array.from(yield* Stream.runCollect(execution.frames))
- }).pipe(Effect.scoped)
- const collectComplete = (
- executor: ReturnType<SessionModelTransport.Interface["bind"]>,
- item: WebSocketChannelExchange,
- ) =>
- Effect.gen(function* () {
- const execution = yield* executor.execute(item)
- return Array.from(yield* Stream.runCollect(execution.frames.pipe(Stream.onEnd(execution.complete))))
- }).pipe(Effect.scoped)
- const automatic = () => {
- const connections: Array<{
- readonly messages: Queue.Queue<string | Uint8Array, AIError>
- closed: number
- sent: string[]
- }> = []
- const connector: WebSocketConnector = {
- open: () =>
- Effect.gen(function* () {
- const messages = yield* Queue.unbounded<string | Uint8Array, AIError>()
- const record = { messages, closed: 0, sent: [] as string[] }
- connections.push(record)
- const connection: WebSocketConnection = {
- sendText: (message) =>
- Effect.sync(() => {
- record.sent.push(message)
- Queue.offerUnsafe(messages, `completed:${message}`)
- }),
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => {
- record.closed++
- }).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }
- return connection
- }),
- }
- return { connector, connections }
- }
- describe("SessionModelTransport", () => {
- test("commits checkpoints only after successful outer completion", async () => {
- const messages = queue<string | Uint8Array, AIError>()
- const checkpoints: Array<unknown> = []
- const candidate = { protocol: "test", value: { response: "one" } }
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: (message) =>
- Effect.sync(() => Queue.offerUnsafe(messages, `completed:${message}`)).pipe(Effect.asVoid),
- messages: Stream.fromQueue(messages),
- close: Queue.shutdown(messages).pipe(Effect.asVoid),
- }),
- }
- const item = (id: string): WebSocketChannelExchange => ({
- ...exchange(id),
- driver: {
- create: (checkpoint) =>
- Effect.sync(() => {
- checkpoints.push(checkpoint)
- return { message: id, mode: checkpoint ? "incremental" : "full" }
- }),
- observe: (_create, frame) => Effect.succeed({ type: "completed", frame, checkpoint: candidate }),
- },
- })
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* collectComplete(executor, item("first"))
- yield* collect(executor, item("second"))
- yield* collect(executor, item("third"))
- expect(checkpoints).toEqual([undefined, candidate, undefined])
- }),
- )
- })
- test("does not carry a checkpoint across physical connection rotation", async () => {
- const fixture = automatic()
- const checkpoints: Array<unknown> = []
- const candidate = { protocol: "test", value: { response: "one" } }
- const item = (id: string, authorization: string): WebSocketChannelExchange => ({
- ...exchange(id, { headers: { authorization } }),
- driver: {
- create: (checkpoint) =>
- Effect.sync(() => {
- checkpoints.push(checkpoint)
- return { message: id, mode: checkpoint ? "incremental" : "full" }
- }),
- observe: (_create, frame) => Effect.succeed({ type: "completed", frame, checkpoint: candidate }),
- },
- })
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* collectComplete(executor, item("first", "one"))
- yield* collect(executor, item("second", "two"))
- expect(checkpoints).toEqual([undefined, undefined])
- expect(fixture.connections).toHaveLength(2)
- }),
- )
- })
- test("clears a rejected checkpoint before the runner retries full", async () => {
- const fixture = automatic()
- const checkpoints: Array<unknown> = []
- const candidate = { protocol: "test", value: { response: "one" } }
- const item = (id: string): WebSocketChannelExchange => ({
- ...exchange(id),
- driver: {
- create: (checkpoint) =>
- Effect.sync(() => {
- checkpoints.push(checkpoint)
- return { message: id, mode: checkpoint ? "incremental" : "full" }
- }),
- observe: (_create, frame) =>
- id === "rejected"
- ? Effect.succeed({
- type: "rejected",
- recovery: "retry-full",
- error: new AIError({
- module: "test",
- method: "stream",
- reason: new TransportReason({
- message: "missing response",
- transport: "websocket",
- operation: "read",
- phase: "receive",
- delivery: "rejected",
- recovery: "retry-full",
- }),
- }),
- })
- : Effect.succeed({ type: "completed", frame, checkpoint: candidate }),
- },
- })
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* collectComplete(executor, item("first"))
- yield* Effect.result(collect(executor, item("rejected")))
- yield* collect(executor, item("retry"))
- expect(checkpoints).toEqual([undefined, candidate, undefined])
- expect(fixture.connections).toHaveLength(1)
- }),
- )
- })
- test("rotates after the provider rejects the connection generation", async () => {
- const fixture = automatic()
- const rejected: WebSocketChannelExchange = {
- ...exchange("rejected"),
- driver: {
- create: () => Effect.succeed({ message: "rejected", mode: "incremental" }),
- observe: () =>
- Effect.succeed({
- type: "rejected",
- recovery: "rotate-and-retry-full",
- error: new AIError({
- module: "test",
- method: "stream",
- reason: new TransportReason({
- message: "connection limit",
- transport: "websocket",
- operation: "read",
- phase: "receive",
- delivery: "rejected",
- recovery: "rotate-and-retry-full",
- }),
- }),
- }),
- },
- }
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* Effect.result(collect(executor, rejected))
- yield* collect(executor, exchange("retry"))
- expect(fixture.connections).toHaveLength(2)
- expect(fixture.connections[0]?.closed).toBe(1)
- }),
- )
- })
- test("reuses one physical connection for sequential Session calls", async () => {
- const fixture = automatic()
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- expect(yield* collect(transport.bind(session), exchange("first"))).toEqual(["completed:first"])
- expect(yield* collect(transport.bind(session), exchange("second"))).toEqual(["completed:second"])
- expect(fixture.connections).toHaveLength(1)
- expect(fixture.connections[0]?.sent).toEqual(["first", "second"])
- }),
- )
- })
- test("serializes concurrent calls for one Session", async () => {
- const started = Deferred.makeUnsafe<void>()
- const release = Deferred.makeUnsafe<void>()
- const messages = queue<string | Uint8Array, AIError>()
- const sent: string[] = []
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: (message) =>
- Effect.gen(function* () {
- sent.push(message)
- if (message === "first") {
- yield* Deferred.succeed(started, undefined)
- yield* Deferred.await(release)
- }
- Queue.offerUnsafe(messages, `completed:${message}`)
- }),
- messages: Stream.fromQueue(messages),
- close: Queue.shutdown(messages).pipe(Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- const first = yield* collect(executor, exchange("first")).pipe(Effect.forkChild({ startImmediately: true }))
- yield* Deferred.await(started)
- const second = yield* collect(executor, exchange("second")).pipe(Effect.forkChild({ startImmediately: true }))
- yield* Effect.yieldNow
- expect(sent).toEqual(["first"])
- yield* Deferred.succeed(release, undefined)
- yield* Fiber.join(first)
- yield* Fiber.join(second)
- expect(sent).toEqual(["first", "second"])
- }),
- )
- })
- test("isolates connections and permits concurrency across Sessions", async () => {
- const started = queue<string>()
- const release = Deferred.makeUnsafe<void>()
- let opened = 0
- const connector: WebSocketConnector = {
- open: () =>
- Effect.gen(function* () {
- opened++
- const messages = yield* Queue.unbounded<string | Uint8Array, AIError>()
- return {
- sendText: (message) =>
- Effect.gen(function* () {
- Queue.offerUnsafe(started, message)
- yield* Deferred.await(release)
- Queue.offerUnsafe(messages, `completed:${message}`)
- }),
- messages: Stream.fromQueue(messages),
- close: Queue.shutdown(messages).pipe(Effect.asVoid),
- }
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const first = yield* collect(transport.bind(session), exchange("first")).pipe(
- Effect.forkChild({ startImmediately: true }),
- )
- const second = yield* collect(transport.bind(otherSession), exchange("second")).pipe(
- Effect.forkChild({ startImmediately: true }),
- )
- expect(new Set([yield* Queue.take(started), yield* Queue.take(started)])).toEqual(new Set(["first", "second"]))
- expect(opened).toBe(2)
- yield* Deferred.succeed(release, undefined)
- yield* Fiber.join(first)
- yield* Fiber.join(second)
- }),
- )
- })
- test("cancels a queued call without affecting the active exchange", async () => {
- const started = Deferred.makeUnsafe<void>()
- const release = Deferred.makeUnsafe<void>()
- const messages = queue<string | Uint8Array, AIError>()
- const sent: string[] = []
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: (message) =>
- Effect.gen(function* () {
- sent.push(message)
- yield* Deferred.succeed(started, undefined)
- yield* Deferred.await(release)
- Queue.offerUnsafe(messages, `completed:${message}`)
- }),
- messages: Stream.fromQueue(messages),
- close: Queue.shutdown(messages).pipe(Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- const active = yield* collect(executor, exchange("active")).pipe(Effect.forkChild({ startImmediately: true }))
- yield* Deferred.await(started)
- const queued = yield* collect(executor, exchange("queued")).pipe(Effect.forkChild({ startImmediately: true }))
- yield* Fiber.interrupt(queued)
- expect(sent).toEqual(["active"])
- yield* Deferred.succeed(release, undefined)
- expect(yield* Fiber.join(active)).toEqual(["completed:active"])
- }),
- )
- })
- test("closes the connection when an active exchange is interrupted", async () => {
- const started = Deferred.makeUnsafe<void>()
- const messages = queue<string | Uint8Array, AIError>()
- let closed = 0
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)),
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const fiber = yield* collect(transport.bind(session), exchange("first")).pipe(
- Effect.forkChild({ startImmediately: true }),
- )
- yield* Deferred.await(started)
- yield* Fiber.interrupt(fiber)
- expect(closed).toBe(1)
- }),
- )
- })
- test("closes an active exchange without waiting for its Session permit", async () => {
- const started = Deferred.makeUnsafe<void>()
- const messages = queue<string | Uint8Array, AIError>()
- let closed = 0
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: () => Deferred.succeed(started, undefined),
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const running = yield* collect(transport.bind(session), exchange("active")).pipe(
- Effect.forkChild({ startImmediately: true }),
- )
- yield* Deferred.await(started)
- yield* transport.close(session)
- const result = yield* Effect.result(Fiber.join(running))
- expect(result).toMatchObject({
- _tag: "Failure",
- failure: { reason: { _tag: "Transport", code: "close", delivery: "ambiguous" } },
- })
- expect(closed).toBe(1)
- }),
- )
- })
- test("times out an idle accepted request and poisons its socket", async () => {
- const started = Deferred.makeUnsafe<void>()
- const messages = queue<string | Uint8Array, AIError>()
- let closed = 0
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: () => Deferred.succeed(started, undefined),
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- }
- await runWithTestClock(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const running = yield* collect(transport.bind(session), exchange("idle")).pipe(
- Effect.forkChild({ startImmediately: true }),
- )
- yield* Deferred.await(started)
- yield* Effect.yieldNow
- yield* TestClock.adjust("5 minutes")
- const result = yield* Effect.result(Fiber.join(running))
- expect(result).toMatchObject({
- _tag: "Failure",
- failure: { reason: { _tag: "Transport", code: "idle-timeout", delivery: "ambiguous" } },
- })
- expect(closed).toBe(1)
- }),
- )
- })
- test("closes a newly opened connection when request creation is interrupted", async () => {
- const opened = Deferred.makeUnsafe<void>()
- const messages = queue<string | Uint8Array, AIError>()
- let closed = 0
- const connector: WebSocketConnector = {
- open: () =>
- Deferred.succeed(opened, undefined).pipe(
- Effect.as({
- sendText: () => Effect.void,
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- ),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const item = exchange("first")
- const fiber = yield* collect(transport.bind(session), {
- ...item,
- driver: { create: () => Effect.never, observe: item.driver.observe },
- }).pipe(Effect.forkChild({ startImmediately: true }))
- yield* Deferred.await(opened)
- yield* Fiber.interrupt(fiber)
- expect(closed).toBe(1)
- }),
- )
- })
- test("falls back once when connection setup fails before send", async () => {
- let fallbacks = 0
- const connector: WebSocketConnector = { open: () => Effect.fail(error("upgrade rejected", "not-sent")) }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const result = yield* collect(
- transport.bind(session),
- exchange("first", {
- fallback: () => {
- fallbacks++
- return Stream.make("http")
- },
- }),
- )
- expect(result).toEqual(["http"])
- expect(fallbacks).toBe(1)
- }),
- )
- })
- test("does not fall back after an ambiguous send failure", async () => {
- const messages = queue<string | Uint8Array, AIError>()
- let fallbacks = 0
- let closed = 0
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: () => Effect.fail(error("send failed")),
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const result = yield* Effect.result(
- collect(
- transport.bind(session),
- exchange("first", {
- fallback: () => {
- fallbacks++
- return Stream.make("http")
- },
- }),
- ),
- )
- expect(result).toMatchObject({
- _tag: "Failure",
- failure: { reason: { _tag: "Transport", phase: "send", delivery: "ambiguous" } },
- })
- expect(fallbacks).toBe(0)
- expect(closed).toBe(1)
- }),
- )
- })
- test("rotates when handshake affinity or connection age changes", async () => {
- const fixture = automatic()
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* collect(executor, exchange("first", { headers: { authorization: "one" } }))
- yield* collect(executor, exchange("second", { headers: { authorization: "one" } }))
- yield* collect(executor, exchange("third", { headers: { authorization: "two" } }))
- yield* Effect.sleep("5 millis")
- yield* collect(executor, exchange("fourth", { headers: { authorization: "two" }, rotateAfterMs: 1 }))
- expect(fixture.connections).toHaveLength(3)
- expect(fixture.connections.slice(0, 2).map((item) => item.closed)).toEqual([1, 1])
- }),
- )
- })
- test("poisons a socket that receives data while idle", async () => {
- const fixture = automatic()
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* collect(executor, exchange("first"))
- const connection = fixture.connections[0]
- if (!connection) throw new Error("Expected connection")
- Queue.offerUnsafe(connection.messages, "late")
- yield* Effect.yieldNow
- yield* collect(executor, exchange("second"))
- expect(fixture.connections).toHaveLength(2)
- expect(fixture.connections[0]?.closed).toBe(1)
- }),
- )
- })
- test("poisons instead of dropping data when the inbound queue overflows", async () => {
- const messages = queue<string | Uint8Array, AIError>()
- let closed = 0
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: () =>
- Effect.sync(() => {
- for (let index = 0; index <= 129; index++) Queue.offerUnsafe(messages, `frame:${index}`)
- }),
- messages: Stream.fromQueue(messages),
- close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const item = exchange("first")
- const result = yield* Effect.result(
- collect(transport.bind(session), {
- ...item,
- driver: {
- create: item.driver.create,
- observe: (_create, frame) => Effect.sleep("1 millis").pipe(Effect.as({ type: "frame" as const, frame })),
- },
- }),
- )
- expect(result).toMatchObject({
- _tag: "Failure",
- failure: { reason: { _tag: "Transport", code: "queue-overflow", delivery: "accepted" } },
- })
- expect(closed).toBe(1)
- }),
- )
- })
- test("poisons unsupported binary frames after provider observation", async () => {
- const messages = queue<string | Uint8Array, AIError>()
- const closed = Deferred.makeUnsafe<void>()
- const connector: WebSocketConnector = {
- open: () =>
- Effect.succeed({
- sendText: () => Effect.sync(() => Queue.offerUnsafe(messages, new Uint8Array([1]))).pipe(Effect.asVoid),
- messages: Stream.fromQueue(messages),
- close: Deferred.succeed(closed, undefined).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
- }),
- }
- await run(
- connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const result = yield* Effect.result(collect(transport.bind(session), exchange("first")))
- expect(result).toMatchObject({
- _tag: "Failure",
- failure: { reason: { _tag: "Transport", code: "message", delivery: "accepted" } },
- })
- yield* Deferred.await(closed)
- }),
- )
- })
- test("closes individual and all owned connections", async () => {
- const fixture = automatic()
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- yield* collect(transport.bind(session), exchange("first"))
- yield* collect(transport.bind(otherSession), exchange("second"))
- yield* transport.close(session)
- expect(fixture.connections.map((item) => item.closed)).toEqual([1, 0])
- yield* transport.closeAll
- expect(fixture.connections.map((item) => item.closed)).toEqual([1, 1])
- }),
- )
- })
- test("closes owned connections when the Location scope ends", async () => {
- const fixture = automatic()
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- yield* collect(transport.bind(session), exchange("first"))
- expect(fixture.connections[0]?.closed).toBe(0)
- }),
- )
- expect(fixture.connections[0]?.closed).toBe(1)
- })
- test("records metadata-only lifecycle metrics", async () => {
- const fixture = automatic()
- await run(
- fixture.connector,
- Effect.gen(function* () {
- const transport = yield* SessionModelTransport.Service
- const executor = transport.bind(session)
- yield* collect(executor, exchange("first", { headers: { authorization: "secret-one" } }))
- yield* collect(executor, exchange("second", { headers: { authorization: "secret-one" } }))
- yield* collect(executor, exchange("third", { headers: { authorization: "secret-two" } }))
- const snapshots = yield* Metric.snapshot
- const lifecycle = snapshots.filter((item) => item.id === "opencode_session_websocket_events_total")
- const names = new Set(lifecycle.map((item) => item.attributes?.event))
- expect(Array.from(names)).toEqual(
- expect.arrayContaining(["connect", "reuse", "rotation", "reconnect", "send", "terminal"]),
- )
- expect(JSON.stringify(lifecycle)).not.toContain("secret-one")
- expect(JSON.stringify(lifecycle)).not.toContain("secret-two")
- }),
- )
- })
- })
|