refactor(opencode): stop legacy v2 event emission (#33993)

This commit is contained in:
Kit Langton 2026-06-26 04:42:38 +02:00 committed by GitHub
commit a1f093a748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 111 additions and 555 deletions

View file

@ -854,12 +854,12 @@ describe("session.compaction.process", () => {
const msg = yield* createUserMessage(session.id, "hello")
const msgs = yield* ssn.messages({ sessionID: session.id })
const done = yield* Deferred.make<void, Error>()
let seen = false
const seen: string[] = []
const unsub = yield* events.listen((evt) => {
seen.push(evt.type)
if (evt.type !== SessionCompaction.Event.Compacted.type) return Effect.void
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id)
return Effect.void
seen = true
Deferred.doneUnsafe(done, Effect.void)
return Effect.void
})
@ -874,7 +874,8 @@ describe("session.compaction.process", () => {
yield* Deferred.await(done).pipe(Effect.timeout("500 millis"))
expect(result).toBe("continue")
expect(seen).toBe(true)
expect(seen).toContain(SessionCompaction.Event.Compacted.type)
expect(seen.filter((type) => type.startsWith("session.next."))).toEqual([])
}),
)

View file

@ -24,7 +24,6 @@ import { raw, reply, TestLLMServer } from "../lib/llm-server"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { LLMEvent } from "@opencode-ai/llm"
@ -981,10 +980,9 @@ itProviderError.live("session.processor effect tests fail provider-executed erro
const parent = yield* user(chat.id, "provider tool error")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
const seen: string[] = []
const off = yield* events.listen((event) => {
if (event.type === SessionEvent.Tool.Failed.type)
settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
seen.push(event.type)
return Effect.void
})
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
@ -1011,19 +1009,15 @@ itProviderError.live("session.processor effect tests fail provider-executed erro
const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
expect(call?.state.status).toBe("error")
if (call?.state.status === "error") expect(call.state.error).toBe("provider boom")
expect(settlements).toHaveLength(1)
expect(settlements[0]?.data).toMatchObject({
callID: "call-1",
error: { type: "unknown", message: "provider boom" },
result: { type: "error", value: "provider boom" },
provider: { executed: true },
})
expect(seen).toContain(MessageV2.Event.PartUpdated.type)
expect(seen).toContain(MessageV2.Event.Updated.type)
expect(seen.filter((type) => type.startsWith("session.next."))).toEqual([])
}),
{ config: cfg },
),
)
itFragmentFailure.live("session.processor effect tests flush partial v2 fragments before step failure", () =>
itFragmentFailure.live("session.processor effect tests retain partial legacy parts without v2 events", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -1035,14 +1029,8 @@ itFragmentFailure.live("session.processor effect tests flush partial v2 fragment
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const seen: string[] = []
let text: string | undefined
let reasoning: string | undefined
const off = yield* events.listen((event) => {
seen.push(event.type)
if (event.type === SessionEvent.Text.Ended.type)
text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
if (event.type === SessionEvent.Reasoning.Ended.type)
reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
return Effect.void
})
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
@ -1067,12 +1055,16 @@ itFragmentFailure.live("session.processor effect tests flush partial v2 fragment
).toBe("stop")
yield* off
const failed = seen.indexOf(SessionEvent.Step.Failed.type)
expect(failed).toBeGreaterThan(-1)
expect(seen.indexOf(SessionEvent.Text.Ended.type)).toBeLessThan(failed)
expect(seen.indexOf(SessionEvent.Reasoning.Ended.type)).toBeLessThan(failed)
expect(text).toBe("partial")
expect(reasoning).toBe("thinking")
const parts = yield* MessageV2.parts(msg.id)
expect(parts).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "text", text: "partial" }),
expect.objectContaining({ type: "reasoning", text: "thinking" }),
]),
)
expect(seen).toContain(MessageV2.Event.PartUpdated.type)
expect(seen).toContain(Session.Event.Error.type)
expect(seen.filter((type) => type.startsWith("session.next."))).toEqual([])
}),
{ config: cfg },
),

View file

@ -8,7 +8,7 @@ import { FetchHttpClient } from "effect/unstable/http"
import { expect } from "bun:test"
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { fileURLToPath } from "url"
import { NamedError } from "@opencode-ai/core/util/error"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "@/background/job"
@ -317,11 +317,6 @@ const writeText = Effect.fn("test.writeText")(function* (file: string, text: str
yield* fs.writeWithDirs(file, text)
})
const ensureDir = Effect.fn("test.ensureDir")(function* (dir: string) {
const fs = yield* FSUtil.Service
yield* fs.ensureDir(dir)
})
const writeConfig = Effect.fn("test.writeConfig")(function* (dir: string, config: Partial<ConfigV1.Info>) {
yield* writeText(
path.join(dir, "opencode.json"),
@ -554,6 +549,54 @@ withMcpInstructions.instance(
15_000,
)
it.instance("legacy prompt emits message events without session.next events", () =>
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({
title: "Pinned",
agent: "plan",
model: { providerID: ProviderV2.ID.make("old"), id: ModelV2.ID.make("old-model") },
})
const seen: string[] = []
const off = yield* events.listen((event) => {
seen.push(event.type)
return Effect.void
})
const first = yield* prompt.prompt({
sessionID: chat.id,
agent: "build",
model: ref,
noReply: true,
parts: [{ type: "text", text: "hello" }],
})
const second = yield* prompt.prompt({
sessionID: chat.id,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "again" }],
})
yield* off
expect(first.info.role).toBe("user")
expect(second.info.role).toBe("user")
if (first.info.role === "user" && second.info.role === "user") {
expect(first.info.model).toEqual(ref)
expect(second.info.model).toEqual(ref)
}
expect(yield* sessions.get(chat.id)).toMatchObject({
agent: "build",
model: { providerID: ref.providerID, id: ref.modelID },
})
expect(seen).toContain(Session.Event.Updated.type)
expect(seen).toContain(MessageV2.Event.Updated.type)
expect(seen).toContain(MessageV2.Event.PartUpdated.type)
expect(seen.filter((type) => type.startsWith("session.next."))).toEqual([])
}),
)
it.instance("loop surfaces content-filter finishes as session errors", () =>
Effect.gen(function* () {
const { llm } = yield* useServerConfig(providerCfg)