diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index 3cfd453624..54d3dab3d6 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -11,12 +11,24 @@ export type GlobalEvent = { class GlobalBusEmitter extends EventEmitter<{ event: [GlobalEvent] }> { - override emit(eventName: "event", event: GlobalEvent): boolean { - if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) { + // altimate_change start — upstream_fix: keep the override assignable to the base. + // `EventEmitter` declares `emit` across several overloads, one of which is + // `(eventName: string | symbol, ...args: any[])`. An override has to be + // assignable to all of them, and a lone `(eventName: "event", event: + // GlobalEvent)` is not — newer `@types/node` rejects it with "Type 'any[]' is + // not assignable to type '[event: GlobalEvent]'", which fails `bun typecheck` + // and so blocks `git push` on unmodified code. The public overload keeps call + // sites typed; the implementation signature is what satisfies the base. + override emit(eventName: "event", event: GlobalEvent): boolean + override emit(eventName: string | symbol, ...args: any[]): boolean + override emit(eventName: string | symbol, ...args: any[]): boolean { + const event = args[0] as GlobalEvent | undefined + if (eventName === "event" && event?.payload && typeof event.payload === "object" && !("id" in event.payload)) { event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending") } - return super.emit(eventName, event) + return super.emit(eventName as "event", ...(args as [GlobalEvent])) } + // altimate_change end } export const GlobalBus = new GlobalBusEmitter() diff --git a/packages/opencode/test/bus/global-emit.test.ts b/packages/opencode/test/bus/global-emit.test.ts new file mode 100644 index 0000000000..82f572a8d9 --- /dev/null +++ b/packages/opencode/test/bus/global-emit.test.ts @@ -0,0 +1,57 @@ +// altimate_change start — upstream_fix: pin the override's assignability to the base. +import { describe, expect, test } from "bun:test" +import { GlobalBus, type GlobalEvent } from "@/bus/global" + +describe("GlobalBusEmitter.emit", () => { + // The regression this guards is a COMPILE error, not a runtime one: a lone + // `(eventName: "event", event: GlobalEvent)` override is not assignable to + // the base `EventEmitter` overload set, which newer `@types/node` rejects + // with "Type 'any[]' is not assignable to type '[event: GlobalEvent]'". That + // failed `bun typecheck`, and so blocked `git push` via the pre-push hook, on + // code nobody had touched. This assignment only compiles while the override + // keeps the wide implementation signature, so `tsgo` fails if it is narrowed + // again — the test body below merely keeps the reference alive. + test("stays assignable to the base EventEmitter signature", () => { + const wide: (eventName: string | symbol, ...args: any[]) => boolean = GlobalBus.emit.bind(GlobalBus) + expect(typeof wide).toBe("function") + }) + + test("stamps an id onto a payload that has none", () => { + const seen: GlobalEvent[] = [] + const on = (event: GlobalEvent) => void seen.push(event) + GlobalBus.on("event", on) + try { + GlobalBus.emit("event", { payload: { kind: "test" } }) + expect(seen).toHaveLength(1) + expect(typeof seen[0]!.payload.id).toBe("string") + expect(seen[0]!.payload.id).toStartWith("evt") + } finally { + GlobalBus.off("event", on) + } + }) + + test("leaves an existing id alone", () => { + const seen: GlobalEvent[] = [] + const on = (event: GlobalEvent) => void seen.push(event) + GlobalBus.on("event", on) + try { + GlobalBus.emit("event", { payload: { id: "evt_already_set" } }) + expect(seen[0]!.payload.id).toBe("evt_already_set") + } finally { + GlobalBus.off("event", on) + } + }) + + test("prefers the syncEvent id when the payload has none", () => { + const seen: GlobalEvent[] = [] + const on = (event: GlobalEvent) => void seen.push(event) + GlobalBus.on("event", on) + try { + GlobalBus.emit("event", { payload: { syncEvent: { id: "evt_from_sync" } } }) + expect(seen[0]!.payload.id).toBe("evt_from_sync") + } finally { + GlobalBus.off("event", on) + } + }) +}) +// altimate_change end