test: share session message expectations (#45454)

This commit is contained in:
Kit Langton
2026-08-27 10:58:12 -04:00
committed by GitHub
parent 89cb5146d4
commit 83ff4253f5
5 changed files with 289 additions and 474 deletions
+26
View File
@@ -0,0 +1,26 @@
export * as Expected from "./session-message.js"
// Partial expected values, not input fixtures. Keep each assertion's matcher and fields explicit.
export const user = <Text>(text: Text) => ({ type: "user" as const, text })
export const text = <Text>(text: Text) => ({ type: "text" as const, text })
export const reasoning = <Text>(text: Text) => ({ type: "reasoning" as const, text })
export const assistant = <const Fields extends object, Content>(fields: Fields, content: Content) => ({
...fields,
type: "assistant" as const,
content,
})
export const completedTool = <Identity extends object, Fields extends object>(identity: Identity, fields: Fields) => ({
...identity,
type: "tool" as const,
state: { ...fields, status: "completed" as const },
})
export const failedTool = <Identity extends object, Fields extends object>(identity: Identity, fields: Fields) => ({
...identity,
type: "tool" as const,
state: { ...fields, status: "error" as const },
})
+15 -22
View File
@@ -32,6 +32,7 @@ import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store"
import { SessionTransfer } from "@opencode-ai/core/session/transfer"
import { Workspace } from "@opencode-ai/core/workspace"
import { Expected } from "./lib/session-message"
import { testEffect } from "./lib/effect"
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import { promptLocationLayer } from "./fixture/prompt-location"
@@ -186,7 +187,7 @@ describe("Session.create", () => {
"session.inbox.enqueued",
])
expect(yield* session.messages({ sessionID: created.id })).toMatchObject([
{ id: expect.any(String), type: "user", text: "Preserved history" },
{ id: expect.any(String), ...Expected.user("Preserved history") },
])
expect(yield* SessionInbox.find(db, pending.id)).toMatchObject({ payload: { text: "Preserved inbox" } })
expect(
@@ -449,10 +450,7 @@ describe("Session.create", () => {
expect(forked).toMatchObject({ title: "Parent (fork #1)", fork: { sessionID: parent.id } })
expect(forked.parentID).toBeUndefined()
expect(forkContext).toMatchObject([
{ type: "user", text: "First" },
{ type: "synthetic", text: "parent note" },
])
expect(forkContext).toMatchObject([Expected.user("First"), { type: "synthetic", text: "parent note" }])
expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id))
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({
@@ -637,13 +635,10 @@ describe("Session.create", () => {
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
expect(yield* session.context(parent.id)).toMatchObject([
{ type: "user", text: "Run both tools" },
{
type: "assistant",
content: [{ type: "tool", id: "call_running", state: { status: "running" } }],
},
Expected.user("Run both tools"),
Expected.assistant({}, [{ type: "tool", id: "call_running", state: { status: "running" } }]),
])
expect(yield* session.context(forked.id)).toMatchObject([{ type: "user", text: "Run both tools" }])
expect(yield* session.context(forked.id)).toMatchObject([Expected.user("Run both tools")])
}),
)
@@ -670,10 +665,10 @@ describe("Session.create", () => {
const running = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
expect(yield* session.context(parent.id)).toMatchObject([
{ type: "user", text: "Run a shell" },
Expected.user("Run a shell"),
{ type: "shell", command: "sleep 10", status: "running" },
])
expect(yield* session.context(running.id)).toMatchObject([{ type: "user", text: "Run a shell" }])
expect(yield* session.context(running.id)).toMatchObject([Expected.user("Run a shell")])
yield* bus.publish(SessionEvent.Shell.Ended, {
sessionID: parent.id,
@@ -682,9 +677,9 @@ describe("Session.create", () => {
})
const completed = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
expect(yield* session.context(running.id)).toMatchObject([{ type: "user", text: "Run a shell" }])
expect(yield* session.context(running.id)).toMatchObject([Expected.user("Run a shell")])
expect(yield* session.context(completed.id)).toMatchObject([
{ type: "user", text: "Run a shell" },
Expected.user("Run a shell"),
{ type: "shell", command: "sleep 10", status: "exited", output: { output: "complete" } },
])
}),
@@ -760,8 +755,8 @@ describe("Session.create", () => {
expect(yield* session.context(beforeFirst.id)).toEqual([])
expect(beforeFirst).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
expect(yield* session.context(complete.id)).toMatchObject([
{ type: "user", text: "First" },
{ type: "user", text: "Second" },
Expected.user("First"),
Expected.user("Second"),
{ type: "assistant", finish: "stop" },
])
expect(complete).toMatchObject({
@@ -936,7 +931,7 @@ describe("Session.create", () => {
yield* Effect.forEach(serialized.slice(2), (event) => bus.replay(event), { discard: true })
expect(yield* SessionInbox.find(db, admitted.id)).toBeUndefined()
expect(yield* store.context(created.id)).toMatchObject([
{ id: admitted.id, type: "user", text: "Replay lifecycle" },
{ id: admitted.id, ...Expected.user("Replay lifecycle") },
])
expect(
(yield* db
@@ -1160,9 +1155,7 @@ describe("SessionTransfer", () => {
recent: "pending",
})
expect((yield* transfer.export({ sessionID: source.id })).messages).toMatchObject([
{ type: "user", text: "Settled" },
])
expect((yield* transfer.export({ sessionID: source.id })).messages).toMatchObject([Expected.user("Settled")])
}),
)
@@ -1307,7 +1300,7 @@ describe("SessionTransfer", () => {
expect(imported).toMatchObject({ id: sessionID, title: "Exported", location })
expect(imported.time).toMatchObject({ idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(150) })
expect(messages).toMatchObject([
{ id: sourceMessageID, type: "user", text: "Imported message" },
{ id: sourceMessageID, ...Expected.user("Imported message") },
{ id: errorMessageID, type: "compaction", error: { type: "test_error", message: "Original error" } },
])
expect(yield* Bus.latestSequence(db, sessionID)).toBe(2)
File diff suppressed because it is too large Load Diff
+16 -28
View File
@@ -42,6 +42,7 @@ import { tmpdir } from "./fixture/tmpdir"
import { tempGlobalLayer } from "./fixture/global"
import { testEffect } from "./lib/effect"
import { permissionLayer } from "./lib/permission"
import { Expected } from "./lib/session-message"
import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool"
const sessionID = Session.ID.make("ses_shell_tool_test")
@@ -654,10 +655,7 @@ describe("ShellTool ordinary shell syntax", () => {
value: { status: "completed", metadata: { exit: 0 } },
})
if (Exit.isSuccess(result.exit))
expect(result.exit.value.content?.[0]).toEqual({
type: "text",
text: isWindows ? "hello\r\n" : "hello\n",
})
expect(result.exit.value.content?.[0]).toEqual(Expected.text(isWindows ? "hello\r\n" : "hello\n"))
}),
pwsh ?? "pwsh",
))
@@ -728,10 +726,9 @@ describe("ShellTool", () => {
expect(settled.status).toBe("completed")
expect(settled.metadata).toMatchObject({ exit: 0, truncated: false })
expect(settled.content?.[0]).toEqual({ type: "text", text: "hello" })
expect(settled.content?.[1]).toMatchObject({
type: "text",
text: expect.stringContaining("Command exited with code 0."),
})
expect(settled.content?.[1]).toMatchObject(
Expected.text(expect.stringContaining("Command exited with code 0.")),
)
expect(assertions).toMatchObject([
{
sessionID,
@@ -789,10 +786,9 @@ describe("ShellTool", () => {
),
Effect.andThen((settled) =>
Effect.sync(() =>
expect(settled.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining(realpathSync(path.join(tmp.path, "src"))),
}),
expect(settled.content?.[0]).toMatchObject(
Expected.text(expect.stringContaining(realpathSync(path.join(tmp.path, "src")))),
),
),
),
)
@@ -1121,10 +1117,9 @@ describe("ShellTool", () => {
expect(settled.status).toBe("completed")
expect(settled.metadata).toMatchObject({ exit: 7, truncated: false })
expect(settled.content?.[0]).toEqual({ type: "text", text: "body" })
expect(settled.content?.[1]).toMatchObject({
type: "text",
text: expect.stringContaining("Command exited with code 7"),
})
expect(settled.content?.[1]).toMatchObject(
Expected.text(expect.stringContaining("Command exited with code 7")),
)
}),
),
)
@@ -1151,10 +1146,9 @@ describe("ShellTool", () => {
if (!content || content.type !== "text") throw new Error("Expected text content")
expect(content.text.includes("output-start")).toBe(false)
expect(content.text.includes("output-end")).toBe(true)
expect(content).toMatchObject({
type: "text",
text: expect.stringContaining("output truncated; full output saved to:"),
})
expect(content).toMatchObject(
Expected.text(expect.stringContaining("output truncated; full output saved to:")),
)
}),
),
)
@@ -1260,14 +1254,8 @@ describe("ShellTool", () => {
Effect.andThen((settled) =>
Effect.sync(() => {
expect(settled.metadata).toMatchObject({ timeout: true, truncated: false })
expect(settled.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining("before timeout"),
})
expect(settled.content?.[1]).toMatchObject({
type: "text",
text: expect.stringContaining("Command timed out"),
})
expect(settled.content?.[0]).toMatchObject(Expected.text(expect.stringContaining("before timeout")))
expect(settled.content?.[1]).toMatchObject(Expected.text(expect.stringContaining("Command timed out")))
}),
),
)
+3 -2
View File
@@ -5,6 +5,7 @@ import type { OpenCodeEvent } from "@opencode-ai/client"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Bus } from "@opencode-ai/core/bus"
import { Event } from "@opencode-ai/schema/event"
import { Expected } from "../../../../core/test/lib/session-message"
import { createEffect, onMount, type ParentProps } from "solid-js"
import { ConfigProvider } from "../../../src/config"
import { ClientProvider, useClient } from "../../../src/context/client"
@@ -2825,7 +2826,7 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
})
await wait(() => sync.session.message.list(sessionID)?.length === 1)
const admitted = sync.session.message.list(sessionID)?.[0]
expect(admitted).toMatchObject({ id: messageID, type: "user", text: "hello" })
expect(admitted).toMatchObject({ id: messageID, ...Expected.user("hello") })
expect(admitted?.metadata).toBeUndefined()
expect(sync.session.pending.list(sessionID)).toEqual([
{
@@ -3250,7 +3251,7 @@ test("hydrates durable pending prompts into the visible transcript", async () =>
await mounted
await sync.session.pending.sync(sessionID)
expect(sync.session.message.list(sessionID)).toEqual([
{ id: item.id, type: "user", text: "waiting", time: { created: 5 } },
{ id: item.id, ...Expected.user("waiting"), time: { created: 5 } },
])
await sync.session.message.sync(sessionID)