refactor(schema): declare event durability at definition level (#35172)
This commit is contained in:
parent
bd8d858bf7
commit
de476aa51b
53 changed files with 700 additions and 1165 deletions
|
|
@ -25,6 +25,7 @@ function prompted(messageID: string): V2Event {
|
|||
return {
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
|
||||
data: { timestamp: 1, sessionID: "ses_1", messageID, prompt: { text: "hello" }, delivery: "steer" },
|
||||
}
|
||||
}
|
||||
|
|
@ -39,11 +40,7 @@ function settled(outcome: "success" | "interrupted" = "success"): V2Event {
|
|||
|
||||
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
||||
// live events the prompt admission triggers, keyed by the generated message ID.
|
||||
async function run(input: {
|
||||
turn: (messageID: string) => V2Event[]
|
||||
pendingForms?: FormInfo[]
|
||||
attached?: boolean
|
||||
}) {
|
||||
async function run(input: { turn: (messageID: string) => V2Event[]; pendingForms?: FormInfo[]; attached?: boolean }) {
|
||||
const sdk = new OpencodeClient()
|
||||
const values: V2Event[] = [{ id: "evt_connected", type: "server.connected", data: {} }]
|
||||
let wake: (() => void) | undefined
|
||||
|
|
@ -64,8 +61,9 @@ async function run(input: {
|
|||
)
|
||||
spyOn(sdk.v2.session.permission, "list").mockImplementation(() => ok({ data: [] }) as never)
|
||||
spyOn(sdk.v2.session.question, "list").mockImplementation(() => ok({ data: [] }) as never)
|
||||
spyOn(sdk.v2.session.form, "list").mockImplementation((request) =>
|
||||
ok({ data: input.pendingForms?.filter((item) => item.sessionID === request.sessionID) ?? [] }) as never,
|
||||
spyOn(sdk.v2.session.form, "list").mockImplementation(
|
||||
(request) =>
|
||||
ok({ data: input.pendingForms?.filter((item) => item.sessionID === request.sessionID) ?? [] }) as never,
|
||||
)
|
||||
spyOn(sdk.v2.session.form, "cancel").mockImplementation(() => ok(undefined) as never)
|
||||
spyOn(sdk.v2.session, "prompt").mockImplementation((request) => {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ function connected(id = "evt_connected") {
|
|||
return { id, type: "server.connected", data: {} } satisfies RunV2Event
|
||||
}
|
||||
|
||||
function durable(sessionID: string, seq = 0, version = 1) {
|
||||
return { aggregateID: sessionID, seq, version }
|
||||
}
|
||||
|
||||
function footer() {
|
||||
const commits: StreamCommit[] = []
|
||||
const events: FooterEvent[] = []
|
||||
|
|
@ -94,7 +98,10 @@ function sdk(input: {
|
|||
const client = new OpencodeClient()
|
||||
let subscription = 0
|
||||
spyOn(client.v2.event, "subscribe").mockImplementation(
|
||||
() => Promise.resolve({ stream: input.streams[subscription++]?.stream ?? feed().stream }) as ReturnType<typeof client.v2.event.subscribe>,
|
||||
() =>
|
||||
Promise.resolve({ stream: input.streams[subscription++]?.stream ?? feed().stream }) as ReturnType<
|
||||
typeof client.v2.event.subscribe
|
||||
>,
|
||||
)
|
||||
spyOn(client.v2.session, "messages").mockImplementation((request) =>
|
||||
ok({
|
||||
|
|
@ -193,32 +200,33 @@ describe("V2 mini transport", () => {
|
|||
})
|
||||
while (!admitted) await Bun.sleep(0)
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_text",
|
||||
type: "session.next.text.delta",
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
textID: "txt_1",
|
||||
delta: "answer",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 4, sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_text",
|
||||
type: "session.next.text.delta",
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
textID: "txt_1",
|
||||
delta: "answer",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 4, sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
await turn
|
||||
|
||||
expect(ui.commits.map((item) => item.text)).toEqual(["previous prompt", "answer"])
|
||||
|
|
@ -245,9 +253,7 @@ describe("V2 mini transport", () => {
|
|||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request:
|
||||
| Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0]
|
||||
| undefined
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0] | undefined
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((input) => {
|
||||
|
|
@ -256,6 +262,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -341,9 +348,7 @@ describe("V2 mini transport", () => {
|
|||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request:
|
||||
| Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0]
|
||||
| undefined
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0] | undefined
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((input) => {
|
||||
|
|
@ -352,6 +357,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -440,9 +446,7 @@ describe("V2 mini transport", () => {
|
|||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
let request:
|
||||
| Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0]
|
||||
| undefined
|
||||
let request: Parameters<OpencodeClient["v2"]["session"]["prompt"]>[0] | undefined
|
||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||
spyOn(client.v2.session, "prompt").mockImplementation((input) => {
|
||||
|
|
@ -451,6 +455,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -593,7 +598,9 @@ describe("V2 mini transport", () => {
|
|||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
return ok({ data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 } })
|
||||
return ok({
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
|
||||
const turn = transport.runPromptTurn({
|
||||
|
|
@ -663,7 +670,9 @@ describe("V2 mini transport", () => {
|
|||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
return ok({ data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 } })
|
||||
return ok({
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
|
||||
const turn = transport.runPromptTurn({
|
||||
|
|
@ -805,6 +814,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_reasoning",
|
||||
type: "session.next.reasoning.ended",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -841,7 +851,9 @@ describe("V2 mini transport", () => {
|
|||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
return ok({ data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 } })
|
||||
return ok({
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
const interrupted = spyOn(client.v2.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
|
||||
|
|
@ -896,7 +908,9 @@ describe("V2 mini transport", () => {
|
|||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
return ok({ data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 } })
|
||||
return ok({
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
|
||||
const turn = transport.runPromptTurn({
|
||||
|
|
@ -911,6 +925,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -952,7 +967,9 @@ describe("V2 mini transport", () => {
|
|||
const messageID = request.id ?? "msg_prompt"
|
||||
const prompt = request.prompt ?? { text: "" }
|
||||
admitted = true
|
||||
return ok({ data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 } })
|
||||
return ok({
|
||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
||||
})
|
||||
})
|
||||
const interrupted = spyOn(client.v2.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||
const controller = new AbortController()
|
||||
|
|
@ -969,6 +986,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -1031,13 +1049,13 @@ describe("V2 mini transport", () => {
|
|||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
const states = () =>
|
||||
ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
const states = () => ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
transport.selectSubagent("ses_child")
|
||||
|
||||
events.push({
|
||||
id: "evt_child_step",
|
||||
type: "session.next.step.started",
|
||||
durable: durable("ses_child"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_child",
|
||||
|
|
@ -1107,13 +1125,13 @@ describe("V2 mini transport", () => {
|
|||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
const states = () =>
|
||||
ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
const states = () => ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
|
||||
// Both events arrive while session.get is still in flight.
|
||||
events.push({
|
||||
id: "evt_child_step",
|
||||
type: "session.next.step.started",
|
||||
durable: durable("ses_child"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_child",
|
||||
|
|
@ -1165,13 +1183,13 @@ describe("V2 mini transport", () => {
|
|||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
const states = () =>
|
||||
ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
const states = () => ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
|
||||
// Child event arrives first and gets buffered behind the gated session.get.
|
||||
events.push({
|
||||
id: "evt_child_step",
|
||||
type: "session.next.step.started",
|
||||
durable: durable("ses_child"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_child",
|
||||
|
|
@ -1184,6 +1202,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_parent_call",
|
||||
type: "session.next.tool.called",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
|
|
@ -1197,6 +1216,7 @@ describe("V2 mini transport", () => {
|
|||
events.push({
|
||||
id: "evt_parent_success",
|
||||
type: "session.next.tool.success",
|
||||
durable: durable("ses_1", 1),
|
||||
data: {
|
||||
timestamp: 4,
|
||||
sessionID: "ses_1",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import { SessionEvent } from "@opencode-ai/core/session/event"
|
|||
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
|
||||
function durable(sessionID: SessionID, seq = 0, version = 1) {
|
||||
return { aggregateID: sessionID, seq: EventV2.Seq.make(seq), version: EventV2.Version.make(version) }
|
||||
}
|
||||
|
||||
test.skip("step snapshots carry over to assistant messages", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
|
|
@ -18,6 +22,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -39,6 +44,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.ended",
|
||||
durable: durable(sessionID, 1, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -71,6 +77,7 @@ test.skip("text ended populates assistant text content", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -89,6 +96,7 @@ test.skip("text ended populates assistant text content", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.text.started",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -102,6 +110,7 @@ test.skip("text ended populates assistant text content", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.text.ended",
|
||||
durable: durable(sessionID, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -127,6 +136,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -145,6 +155,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.tool.input.started",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -159,6 +170,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.tool.called",
|
||||
durable: durable(sessionID, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -175,6 +187,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.tool.success",
|
||||
durable: durable(sessionID, 3),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -205,6 +218,7 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id,
|
||||
type: "session.next.compaction.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
messageID: compactionID,
|
||||
|
|
@ -246,6 +260,7 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.compaction.ended",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
messageID: compactionID,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue