Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/opencode/src/server/event.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { BusEvent } from "@/bus/bus-event"
import { PermissionNext } from "@/permission/next"
import z from "zod"

export const ConnectedEvent = BusEvent.define("server.connected", z.object({}))
export const DisposedEvent = BusEvent.define("global.disposed", z.object({}))

// Lazy getter to ensure PermissionNext is loaded before accessing Event.Asked
export const Event = {
Connected: BusEvent.define("server.connected", z.object({})),
Disposed: BusEvent.define("global.disposed", z.object({})),
Connected: ConnectedEvent,
Disposed: DisposedEvent,
get PermissionAsked() {
return PermissionNext.Event.Asked
},
}
5 changes: 2 additions & 3 deletions packages/opencode/src/server/routes/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import { Log } from "../../util/log"
import { lazy } from "../../util/lazy"
import { Config } from "../../config/config"
import { errors } from "../error"
import { DisposedEvent } from "../event"

const log = Log.create({ service: "server" })

export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))

export const GlobalRoutes = lazy(() =>
new Hono()
.get(
Expand Down Expand Up @@ -173,7 +172,7 @@ export const GlobalRoutes = lazy(() =>
GlobalBus.emit("event", {
directory: "global",
payload: {
type: GlobalDisposedEvent.type,
type: DisposedEvent.type,
properties: {},
},
})
Expand Down
51 changes: 51 additions & 0 deletions packages/opencode/test/server/event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, test } from "bun:test"
import { Event } from "../../src/server/event"
import { PermissionNext } from "../../src/permission/next"

describe("server/event", () => {
test("exports permission.asked event with correct schema", () => {
// Verify the PermissionAsked event exists
expect(Event.PermissionAsked).toBeDefined()

// Verify it references the same event object from permission/next.ts
expect(Event.PermissionAsked).toBe(PermissionNext.Event.Asked)

// Verify the event type is correct
expect(Event.PermissionAsked.type).toBe("permission.asked")

// Verify the schema has the expected structure
const samplePayload = {
id: "perm_123",
sessionID: "ses_456",
permission: "bash",
patterns: ["*"],
metadata: {},
always: [],
tool: {
messageID: "msg_789",
callID: "call_abc",
},
}

// Verify the payload can be parsed by the schema
const result = Event.PermissionAsked.properties.safeParse(samplePayload)
expect(result.success).toBe(true)

// Verify optional tool field can be omitted
const resultWithoutTool = Event.PermissionAsked.properties.safeParse({
...samplePayload,
tool: undefined,
})
expect(resultWithoutTool.success).toBe(true)
})

test("lazy getter prevents module load order issues", () => {
// Access via getter multiple times to ensure it's evaluated correctly
const event1 = Event.PermissionAsked
const event2 = Event.PermissionAsked

// Both accesses should return the same object (singleton)
expect(event1).toBe(event2)
expect(event1).toBe(PermissionNext.Event.Asked)
})
})