refactor(core): move database schema ownership (#29068)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Dax
2026-05-30 21:08:38 -04:00
committed by GitHub
parent 6bcb9cb9bb
commit 7f571d36ea
390 changed files with 11127 additions and 9164 deletions

View File

@@ -1,16 +1,19 @@
import { describe, expect, test } from "bun:test"
import { Effect, Option } from "effect"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { Database } from "@opencode-ai/core/database/database"
import { Effect, Layer, Option } from "effect"
import { Session as SessionNs } from "@/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { NotFoundError } from "@/storage/storage"
import * as Log from "@opencode-ai/core/util/log"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
void Log.init({ print: false })
const it = testEffect(SessionNs.defaultLayer)
const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Database.defaultLayer))
const withSession = <A, E, R>(
fn: (input: { session: SessionNs.Interface; sessionID: SessionID }) => Effect.Effect<A, E, R>,
@@ -45,7 +48,7 @@ const fill = Effect.fn("Test.fill")(function* (
model: { providerID: "test", modelID: "test" },
tools: {},
mode: "",
} as unknown as MessageV2.Info)
} as unknown as SessionLegacy.Info)
yield* session.updatePart({
id: PartID.ascending(),
sessionID,
@@ -69,7 +72,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
model: { providerID: "test", modelID: "test" },
tools: {},
mode: "",
} as unknown as MessageV2.Info)
} as unknown as SessionLegacy.Info)
if (text) {
yield* session.updatePart({
id: PartID.ascending(),
@@ -85,7 +88,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
const addAssistant = Effect.fn("Test.addAssistant")(function* (
sessionID: SessionID,
parentID: MessageID,
opts?: { summary?: boolean; finish?: string; error?: MessageV2.Assistant["error"] },
opts?: { summary?: boolean; finish?: string; error?: SessionLegacy.Assistant["error"] },
) {
const session = yield* SessionNs.Service
const id = MessageID.ascending()
@@ -95,8 +98,8 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
role: "assistant",
time: { created: Date.now() },
parentID,
modelID: ModelID.make("test"),
providerID: ProviderID.make("test"),
modelID: ProviderV2.ModelID.make("test"),
providerID: ProviderV2.ID.make("test"),
mode: "",
agent: "default",
path: { cwd: "/", root: "/" },
@@ -105,7 +108,7 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
summary: opts?.summary,
finish: opts?.finish,
error: opts?.error,
} as unknown as MessageV2.Info)
} as unknown as SessionLegacy.Info)
return id
})
@@ -310,7 +313,7 @@ describe("MessageV2.stream", () => {
Effect.gen(function* () {
const ids = yield* fill(sessionID, 5)
const items = Array.from(MessageV2.stream(sessionID))
const items = yield* MessageV2.stream(sessionID)
expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse())
}),
),
@@ -319,7 +322,7 @@ describe("MessageV2.stream", () => {
it.instance("yields nothing for empty session", () =>
withSession(({ sessionID }) =>
Effect.gen(function* () {
const items = Array.from(MessageV2.stream(sessionID))
const items = yield* MessageV2.stream(sessionID)
expect(items).toHaveLength(0)
}),
),
@@ -330,7 +333,7 @@ describe("MessageV2.stream", () => {
Effect.gen(function* () {
const ids = yield* fill(sessionID, 1)
const items = Array.from(MessageV2.stream(sessionID))
const items = yield* MessageV2.stream(sessionID)
expect(items).toHaveLength(1)
expect(items[0].info.id).toBe(ids[0])
}),
@@ -342,7 +345,7 @@ describe("MessageV2.stream", () => {
Effect.gen(function* () {
yield* fill(sessionID, 3)
const items = Array.from(MessageV2.stream(sessionID))
const items = yield* MessageV2.stream(sessionID)
for (const item of items) {
expect(item.parts).toHaveLength(1)
expect(item.parts[0].type).toBe("text")
@@ -356,7 +359,7 @@ describe("MessageV2.stream", () => {
Effect.gen(function* () {
const ids = yield* fill(sessionID, 60)
const items = Array.from(MessageV2.stream(sessionID))
const items = yield* MessageV2.stream(sessionID)
expect(items).toHaveLength(60)
expect(items[0].info.id).toBe(ids[ids.length - 1])
expect(items[59].info.id).toBe(ids[0])
@@ -364,17 +367,13 @@ describe("MessageV2.stream", () => {
),
)
it.instance("is a sync generator", () =>
it.instance("returns an Effect", () =>
withSession(({ sessionID }) =>
Effect.gen(function* () {
yield* fill(sessionID, 1)
const gen = MessageV2.stream(sessionID)
const first = gen.next()
// sync generator returns { value, done } directly, not a Promise
expect(first).toHaveProperty("value")
expect(first).toHaveProperty("done")
expect(first.done).toBe(false)
const result = yield* MessageV2.stream(sessionID)
expect(result).toHaveLength(1)
}),
),
)
@@ -386,10 +385,10 @@ describe("MessageV2.parts", () => {
Effect.gen(function* () {
const [id] = yield* fill(sessionID, 1)
const result = MessageV2.parts(id)
const result = yield* MessageV2.parts(id)
expect(result).toHaveLength(1)
expect(result[0].type).toBe("text")
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
}),
),
)
@@ -399,7 +398,7 @@ describe("MessageV2.parts", () => {
Effect.gen(function* () {
const id = yield* addUser(sessionID)
const result = MessageV2.parts(id)
const result = yield* MessageV2.parts(id)
expect(result).toEqual([])
}),
),
@@ -425,11 +424,11 @@ describe("MessageV2.parts", () => {
text: "third",
})
const result = MessageV2.parts(id)
const result = yield* MessageV2.parts(id)
expect(result).toHaveLength(3)
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
expect((result[1] as MessageV2.TextPart).text).toBe("second")
expect((result[2] as MessageV2.TextPart).text).toBe("third")
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
expect((result[1] as SessionLegacy.TextPart).text).toBe("second")
expect((result[2] as SessionLegacy.TextPart).text).toBe("third")
}),
),
)
@@ -437,7 +436,7 @@ describe("MessageV2.parts", () => {
it.instance("returns empty for non-existent message id", () =>
Effect.gen(function* () {
yield* SessionNs.Service
const result = MessageV2.parts(MessageID.ascending())
const result = yield* MessageV2.parts(MessageID.ascending())
expect(result).toEqual([])
}),
)
@@ -447,7 +446,7 @@ describe("MessageV2.parts", () => {
Effect.gen(function* () {
const [id] = yield* fill(sessionID, 1)
const result = MessageV2.parts(id)
const result = yield* MessageV2.parts(id)
expect(result[0].sessionID).toBe(sessionID)
expect(result[0].messageID).toBe(id)
}),
@@ -466,7 +465,7 @@ describe("MessageV2.get", () => {
expect(result.info.sessionID).toBe(sessionID)
expect(result.info.role).toBe("user")
expect(result.parts).toHaveLength(1)
expect((result.parts[0] as MessageV2.TextPart).text).toBe("m0")
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("m0")
}),
),
)
@@ -536,7 +535,7 @@ describe("MessageV2.get", () => {
const result = yield* MessageV2.get({ sessionID, messageID: aid })
expect(result.info.role).toBe("assistant")
expect(result.parts).toHaveLength(1)
expect((result.parts[0] as MessageV2.TextPart).text).toBe("response")
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("response")
}),
),
)
@@ -604,7 +603,7 @@ describe("MessageV2.filterCompacted", () => {
Effect.gen(function* () {
const ids = yield* fill(sessionID, 5)
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
expect(result).toHaveLength(5)
// reversed from newest-first to chronological
expect(result.map((item) => item.info.id)).toEqual(ids)
@@ -638,7 +637,7 @@ describe("MessageV2.filterCompacted", () => {
text: "new response",
})
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
// Includes compaction boundary: u1, a1, u2, a2
expect(result[0].info.id).toBe(u1)
expect(result.length).toBe(4)
@@ -660,7 +659,7 @@ describe("MessageV2.filterCompacted", () => {
yield* addCompactionPart(sessionID, u1)
yield* addUser(sessionID, "world")
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
expect(result).toHaveLength(2)
}),
),
@@ -672,14 +671,14 @@ describe("MessageV2.filterCompacted", () => {
const u1 = yield* addUser(sessionID, "hello")
yield* addCompactionPart(sessionID, u1)
const error = new MessageV2.APIError({
const error = new SessionLegacy.APIError({
message: "boom",
isRetryable: true,
}).toObject() as MessageV2.Assistant["error"]
}).toObject() as SessionLegacy.Assistant["error"]
yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn", error })
yield* addUser(sessionID, "retry")
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
// Error assistant doesn't add to completed, so compaction boundary never triggers
expect(result).toHaveLength(3)
}),
@@ -696,7 +695,7 @@ describe("MessageV2.filterCompacted", () => {
yield* addAssistant(sessionID, u1, { summary: true })
yield* addUser(sessionID, "next")
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
expect(result).toHaveLength(3)
}),
),
@@ -746,7 +745,7 @@ describe("MessageV2.filterCompacted", () => {
text: "third reply",
})
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
expect(result.map((item) => item.info.id)).toEqual([c1, s1, u2, a2, u3, a3])
}),
@@ -799,11 +798,11 @@ describe("MessageV2.filterCompacted", () => {
text: "third reply",
})
const parentFiltered = MessageV2.filterCompacted(MessageV2.stream(created.id))
const parentFiltered = MessageV2.filterCompacted(yield* MessageV2.stream(created.id))
expect(parentFiltered.map((item) => item.info.id)).toEqual([c1, s1, u2, a2, u3, a3])
const forked = yield* session.fork({ sessionID: created.id })
const childFiltered = MessageV2.filterCompacted(MessageV2.stream(forked.id))
const childFiltered = MessageV2.filterCompacted(yield* MessageV2.stream(forked.id))
expect(childFiltered).toHaveLength(parentFiltered.length)
const tailPart = childFiltered.flatMap((m) => m.parts).find((p) => p.type === "compaction")
@@ -869,7 +868,7 @@ describe("MessageV2.filterCompacted", () => {
text: "third reply",
})
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
expect(result.map((item) => item.info.id)).toEqual([c1, s1, a3, u3, a4])
}),
@@ -941,7 +940,7 @@ describe("MessageV2.filterCompacted", () => {
text: "fourth reply",
})
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
expect(result.map((item) => item.info.id)).toEqual([c2, s2, u3, a3, u4, a4])
}),
@@ -951,7 +950,7 @@ describe("MessageV2.filterCompacted", () => {
test("works with array input", () => {
// filterCompacted accepts any Iterable, not just generators
const id = MessageID.ascending()
const items: MessageV2.WithParts[] = [
const items: SessionLegacy.WithParts[] = [
{
info: {
id,
@@ -960,8 +959,8 @@ describe("MessageV2.filterCompacted", () => {
time: { created: 1 },
agent: "test",
model: { providerID: "test", modelID: "test" },
} as unknown as MessageV2.Info,
parts: [{ type: "text", text: "hello" }] as unknown as MessageV2.Part[],
} as unknown as SessionLegacy.Info,
parts: [{ type: "text", text: "hello" }] as unknown as SessionLegacy.Part[],
},
]
const result = MessageV2.filterCompacted(items)
@@ -1014,7 +1013,7 @@ describe("MessageV2 consistency", () => {
const [id] = yield* fill(sessionID, 1)
const got = yield* MessageV2.get({ sessionID, messageID: id })
const standalone = MessageV2.parts(id)
const standalone = yield* MessageV2.parts(id)
expect(got.parts).toEqual(standalone)
}),
),
@@ -1025,9 +1024,9 @@ describe("MessageV2 consistency", () => {
Effect.gen(function* () {
yield* fill(sessionID, 7)
const streamed = Array.from(MessageV2.stream(sessionID))
const streamed = yield* MessageV2.stream(sessionID)
const paged = [] as MessageV2.WithParts[]
const paged = [] as SessionLegacy.WithParts[]
let cursor: string | undefined
while (true) {
const result = yield* MessageV2.page({ sessionID, limit: 3, before: cursor })
@@ -1048,8 +1047,9 @@ describe("MessageV2 consistency", () => {
Effect.gen(function* () {
yield* fill(sessionID, 4)
const filtered = MessageV2.filterCompacted(MessageV2.stream(sessionID))
const all = Array.from(MessageV2.stream(sessionID)).reverse()
const stream = yield* MessageV2.stream(sessionID)
const filtered = MessageV2.filterCompacted(stream)
const all = stream.toReversed()
expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id))
}),