mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
169 lines
7.3 KiB
TypeScript
169 lines
7.3 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { ai, Message } from "@opencode/ai/effect"
|
|
import { LLMClient, RequestExecutor } from "@opencode/ai/route"
|
|
import { configure } from "@opencode/ai/providers/openai"
|
|
import { SessionModelTransport } from "../src/session/model-transport"
|
|
import { WebSocketConstructor } from "../src/effect/websocket-constructor"
|
|
import { Session } from "@opencode/schema/session"
|
|
import { Effect, Layer, Schema } from "effect"
|
|
import { FetchHttpClient } from "effect/http"
|
|
import { testEffect } from "./lib/effect"
|
|
import { makeWebSocketServer } from "./lib/websocket-server"
|
|
|
|
const runtime = Layer.mergeAll(
|
|
LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer), Layer.provide(FetchHttpClient.layer)),
|
|
SessionModelTransport.layer.pipe(Layer.provide(WebSocketConstructor.layer)),
|
|
)
|
|
const it = testEffect(runtime)
|
|
const sessionID = Session.ID.make("ses_checkpoint_transport")
|
|
const checkpoint = { type: "compaction", encrypted_content: "opaque" }
|
|
const fixture = (mode: "success" | "invalid" | "fallback") =>
|
|
Effect.gen(function* () {
|
|
const requests: Array<Record<string, unknown>> = []
|
|
const http: Array<Record<string, unknown>> = []
|
|
let disconnect: (() => void) | undefined
|
|
const server = yield* makeWebSocketServer({
|
|
upgrade: () => mode !== "fallback" || disconnect === undefined,
|
|
async http(request) {
|
|
http.push(Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))(await request.json()))
|
|
return new Response(
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
id: "resp_http",
|
|
output: [checkpoint],
|
|
usage: { input_tokens: 70, output_tokens: 7 },
|
|
},
|
|
})}\n\n`,
|
|
{ headers: { "content-type": "text/event-stream", "x-response": "http" } },
|
|
)
|
|
},
|
|
open(socket) {
|
|
disconnect = () => socket.close()
|
|
},
|
|
message(socket, message) {
|
|
const body = JSON.parse(message.toString())
|
|
requests.push(body)
|
|
const id = `resp_${requests.length}`
|
|
const send = (event: unknown) => socket.send(JSON.stringify(event))
|
|
if (body.input.at(-1)?.type !== "compaction_trigger") {
|
|
const item = {
|
|
type: "message",
|
|
id: `msg_${requests.length}`,
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
}
|
|
send({ type: "response.created", response: { id } })
|
|
send({ type: "response.output_item.added", output_index: 0, item })
|
|
send({ type: "response.output_text.delta", item_id: item.id, delta: "Hello" })
|
|
send({ type: "response.output_item.done", output_index: 0, item })
|
|
send({ type: "response.completed", response: { id, output: [item] } })
|
|
return
|
|
}
|
|
send({ type: "response.created", response: { id } })
|
|
send({ type: "response.output_item.done", output_index: 0, item: checkpoint })
|
|
send({
|
|
type: "response.completed",
|
|
response: {
|
|
id,
|
|
output: mode === "invalid" ? [checkpoint, { ...checkpoint, encrypted_content: "second" }] : [checkpoint],
|
|
usage: { input_tokens: 100, output_tokens: 10 },
|
|
},
|
|
})
|
|
},
|
|
})
|
|
return {
|
|
requests,
|
|
headers: server.state.headers,
|
|
http,
|
|
opens: () => server.state.opens,
|
|
disconnect: () => disconnect?.(),
|
|
request: ai.request({
|
|
model: configure({
|
|
apiKey: "fixture",
|
|
baseURL: server.url.replace(/^ws/, "http").replace(/responses$/, ""),
|
|
headers: { "chatgpt-account-id": "account", "x-codex-beta-features": "remote_compaction_v2" },
|
|
providerOptions: { parallelToolCalls: true },
|
|
}).responses("fixture"),
|
|
prompt: "First",
|
|
promptCacheKey: "session-key",
|
|
}),
|
|
}
|
|
})
|
|
|
|
it.live("trigger reuses the append baseline and clears it before the next generation", () =>
|
|
Effect.gen(function* () {
|
|
const server = yield* fixture("success")
|
|
const transport = yield* SessionModelTransport.Service
|
|
const webSocket = transport.bind(sessionID)
|
|
const first = yield* LLMClient.generate(server.request, { webSocket })
|
|
const compacted = yield* ai.compact(
|
|
{ ...server.request, messages: [...server.request.messages, first.message] },
|
|
{ mechanism: "trigger", webSocket },
|
|
)
|
|
expect(server.requests[1]).toMatchObject({
|
|
previous_response_id: "resp_1",
|
|
input: [{ type: "compaction_trigger" }],
|
|
})
|
|
expect(compacted.responseID).toBe("resp_2")
|
|
expect(compacted.usage).toMatchObject({ inputTokens: 100, outputTokens: 10 })
|
|
expect(compacted.checkpoint.encrypted).toBe("opaque")
|
|
const messages = [Message.assistant(compacted.checkpoint), Message.user("Continue")]
|
|
yield* LLMClient.generate({ ...server.request, messages }, { webSocket })
|
|
expect(server.requests[2]?.previous_response_id).toBeUndefined()
|
|
expect(server.requests[2]?.input).toMatchObject([
|
|
{ type: "compaction", encrypted_content: "opaque" },
|
|
{ role: "user", content: [{ type: "input_text", text: "Continue" }] },
|
|
])
|
|
expect(server.requests[1]?.stream).toBeUndefined()
|
|
expect(server.requests[1]?.store).toBe(false)
|
|
expect(server.headers[0]).toMatchObject({
|
|
authorization: "Bearer fixture",
|
|
"chatgpt-account-id": "account",
|
|
"x-codex-beta-features": "remote_compaction_v2",
|
|
})
|
|
expect(server.opens()).toBe(1)
|
|
expect(server.http).toHaveLength(0)
|
|
}),
|
|
)
|
|
|
|
it.live("invalid trigger output does not retry or commit a continuation checkpoint", () =>
|
|
Effect.gen(function* () {
|
|
const server = yield* fixture("invalid")
|
|
const transport = yield* SessionModelTransport.Service
|
|
const webSocket = transport.bind(sessionID)
|
|
const first = yield* LLMClient.generate(server.request, { webSocket })
|
|
const input = { ...server.request, messages: [...server.request.messages, first.message] }
|
|
const error = yield* ai.compact(input, { mechanism: "trigger", webSocket }).pipe(Effect.flip)
|
|
expect(error.reason._tag).toBe("InvalidProviderOutput")
|
|
expect(server.requests).toHaveLength(2)
|
|
expect(server.http).toHaveLength(0)
|
|
yield* LLMClient.generate(input, { webSocket })
|
|
expect(server.requests[2]?.previous_response_id).toBeUndefined()
|
|
expect(server.requests[2]?.input).toHaveLength(2)
|
|
}),
|
|
)
|
|
|
|
it.live("trigger recovers over SSE with complete input after the old socket closes and reconnect is rejected", () =>
|
|
Effect.gen(function* () {
|
|
const server = yield* fixture("fallback")
|
|
const transport = yield* SessionModelTransport.Service
|
|
const webSocket = transport.bind(sessionID)
|
|
const first = yield* LLMClient.generate(server.request, { webSocket })
|
|
server.disconnect()
|
|
// Let the real close event reach the connector before the next send.
|
|
yield* Effect.sleep("20 millis")
|
|
const result = yield* ai.compact(
|
|
{ ...server.request, messages: [...server.request.messages, first.message] },
|
|
{ mechanism: "trigger", webSocket },
|
|
)
|
|
expect(result.responseID).toBe("resp_http")
|
|
expect(result.usage).toMatchObject({ inputTokens: 70, outputTokens: 7 })
|
|
expect(server.http).toHaveLength(1)
|
|
expect(server.http[0]?.previous_response_id).toBeUndefined()
|
|
expect(server.http[0]?.stream).toBe(true)
|
|
expect(server.http[0]?.input).toHaveLength(3)
|
|
expect(server.requests).toHaveLength(1)
|
|
}),
|
|
)
|