From e0e9414cbdc32d3bd08c8c4ea239fb9e80091c8b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 12 May 2026 00:41:30 +0000 Subject: [PATCH] chore: generate --- packages/opencode/test/file/index.test.ts | 56 +- .../test/session/messages-pagination.test.ts | 998 ++++++++++-------- 2 files changed, 578 insertions(+), 476 deletions(-) diff --git a/packages/opencode/test/file/index.test.ts b/packages/opencode/test/file/index.test.ts index 9250841404..8b48fff5e4 100644 --- a/packages/opencode/test/file/index.test.ts +++ b/packages/opencode/test/file/index.test.ts @@ -115,7 +115,9 @@ describe("file/index Filesystem patterns", () => { it.instance("handles multi-line text files", () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "multiline.txt"), "line1\nline2\nline3", "utf-8")) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "multiline.txt"), "line1\nline2\nline3", "utf-8"), + ) const result = yield* read("multiline.txt") expect(result.content).toBe("line1\nline2\nline3") @@ -141,7 +143,9 @@ describe("file/index Filesystem patterns", () => { it.instance("returns empty for binary non-image files", () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "binary.so"), Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "binary.so"), Buffer.from([0x7f, 0x45, 0x4c, 0x46])), + ) const result = yield* read("binary.so") expect(result.type).toBe("binary") @@ -250,7 +254,9 @@ describe("file/index Filesystem patterns", () => { yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "readonly.txt"), "content", "utf-8")) const nonExistentPath = path.join(test.directory, "does-not-exist.txt") - expect(Exit.isFailure(yield* Effect.promise(() => Filesystem.readText(nonExistentPath)).pipe(Effect.exit))).toBe(true) + expect( + Exit.isFailure(yield* Effect.promise(() => Filesystem.readText(nonExistentPath)).pipe(Effect.exit)), + ).toBe(true) const result = yield* read("does-not-exist.txt") expect(result.content).toBe("") @@ -261,7 +267,9 @@ describe("file/index Filesystem patterns", () => { Effect.gen(function* () { const test = yield* TestInstance const nonExistentPath = path.join(test.directory, "does-not-exist.bin") - const buffer = yield* Effect.promise(() => Filesystem.readArrayBuffer(nonExistentPath).catch(() => new ArrayBuffer(0))) + const buffer = yield* Effect.promise(() => + Filesystem.readArrayBuffer(nonExistentPath).catch(() => new ArrayBuffer(0)), + ) expect(buffer.byteLength).toBe(0) }), ) @@ -279,7 +287,9 @@ describe("file/index Filesystem patterns", () => { it.instance("treats .ts files as text", () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "test.ts"), "export const value = 1", "utf-8")) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "test.ts"), "export const value = 1", "utf-8"), + ) const result = yield* read("test.ts") expect(result.type).toBe("text") @@ -290,7 +300,9 @@ describe("file/index Filesystem patterns", () => { it.instance("treats .mts files as text", () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "test.mts"), "export const value = 1", "utf-8")) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "test.mts"), "export const value = 1", "utf-8"), + ) const result = yield* read("test.mts") expect(result.type).toBe("text") @@ -301,7 +313,9 @@ describe("file/index Filesystem patterns", () => { it.instance("treats .sh files as text", () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "test.sh"), "#!/usr/bin/env bash\necho hello", "utf-8")) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "test.sh"), "#!/usr/bin/env bash\necho hello", "utf-8"), + ) const result = yield* read("test.sh") expect(result.type).toBe("text") @@ -334,7 +348,9 @@ describe("file/index Filesystem patterns", () => { it.instance("returns base64 encoding for images", () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "test.jpg"), Buffer.from([0xff, 0xd8, 0xff, 0xe0]))) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "test.jpg"), Buffer.from([0xff, 0xd8, 0xff, 0xe0])), + ) const result = yield* read("test.jpg") expect(result.encoding).toBe("base64") @@ -384,7 +400,9 @@ describe("file/index Filesystem patterns", () => { () => Effect.gen(function* () { const test = yield* TestInstance - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "new.txt"), "line1\nline2\nline3\n", "utf-8")) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "new.txt"), "line1\nline2\nline3\n", "utf-8"), + ) const result = yield* status() const entry = result.find((file) => file.path === "new.txt") @@ -457,10 +475,14 @@ describe("file/index Filesystem patterns", () => { Effect.gen(function* () { const test = yield* TestInstance const filepath = path.join(test.directory, "data.bin") - yield* Effect.promise(() => fs.writeFile(filepath, Buffer.from(Array.from({ length: 256 }, (_, index) => index)))) + yield* Effect.promise(() => + fs.writeFile(filepath, Buffer.from(Array.from({ length: 256 }, (_, index) => index))), + ) yield* gitAddAll(test.directory) yield* gitCommit(test.directory, "add binary") - yield* Effect.promise(() => fs.writeFile(filepath, Buffer.from(Array.from({ length: 512 }, (_, index) => index % 256)))) + yield* Effect.promise(() => + fs.writeFile(filepath, Buffer.from(Array.from({ length: 512 }, (_, index) => index % 256))), + ) const result = yield* status() const entry = result.find((file) => file.path === "data.bin") @@ -481,7 +503,9 @@ describe("file/index Filesystem patterns", () => { const test = yield* TestInstance yield* Effect.promise(() => fs.mkdir(path.join(test.directory, "subdir"))) yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "file.txt"), "content", "utf-8")) - yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "subdir", "nested.txt"), "nested", "utf-8")) + yield* Effect.promise(() => + fs.writeFile(path.join(test.directory, "subdir", "nested.txt"), "nested", "utf-8"), + ) const nodes = yield* list() expect(nodes.length).toBeGreaterThanOrEqual(2) @@ -633,8 +657,12 @@ describe("file/index Filesystem patterns", () => { const result = yield* search({ query: "", type: "directory" }) expect(result.length).toBeGreaterThan(0) - const firstHidden = result.findIndex((dir) => dir.split("/").some((part) => part.startsWith(".") && part.length > 1)) - const lastVisible = result.findLastIndex((dir) => !dir.split("/").some((part) => part.startsWith(".") && part.length > 1)) + const firstHidden = result.findIndex((dir) => + dir.split("/").some((part) => part.startsWith(".") && part.length > 1), + ) + const lastVisible = result.findLastIndex( + (dir) => !dir.split("/").some((part) => part.startsWith(".") && part.length > 1), + ) if (firstHidden >= 0 && lastVisible >= 0) { expect(firstHidden).toBeGreaterThan(lastVisible) } diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index 49828a9b62..e1714a9015 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -126,53 +126,61 @@ const addCompactionPart = Effect.fn("Test.addCompactionPart")(function* ( describe("MessageV2.page", () => { it.instance("returns sync result", () => - withSession(({ sessionID }) => Effect.gen(function* () { - yield* fill(sessionID, 2) + withSession(({ sessionID }) => + Effect.gen(function* () { + yield* fill(sessionID, 2) - const result = MessageV2.page({ sessionID, limit: 10 }) - expect(result).toBeDefined() - expect(result.items).toBeArray() - })), + const result = MessageV2.page({ sessionID, limit: 10 }) + expect(result).toBeDefined() + expect(result.items).toBeArray() + }), + ), ) it.instance("pages backward with opaque cursors", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 6) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 6) - const a = MessageV2.page({ sessionID, limit: 2 }) - expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) - expect(a.items.every((item) => item.parts.length === 1)).toBe(true) - expect(a.more).toBe(true) - expect(a.cursor).toBeTruthy() + const a = MessageV2.page({ sessionID, limit: 2 }) + expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) + expect(a.items.every((item) => item.parts.length === 1)).toBe(true) + expect(a.more).toBe(true) + expect(a.cursor).toBeTruthy() - const b = MessageV2.page({ sessionID, limit: 2, before: a.cursor! }) - expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(-4, -2)) - expect(b.more).toBe(true) - expect(b.cursor).toBeTruthy() + const b = MessageV2.page({ sessionID, limit: 2, before: a.cursor! }) + expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(-4, -2)) + expect(b.more).toBe(true) + expect(b.cursor).toBeTruthy() - const c = MessageV2.page({ sessionID, limit: 2, before: b.cursor! }) - expect(c.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) - expect(c.more).toBe(false) - expect(c.cursor).toBeUndefined() - })), + const c = MessageV2.page({ sessionID, limit: 2, before: b.cursor! }) + expect(c.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) + expect(c.more).toBe(false) + expect(c.cursor).toBeUndefined() + }), + ), ) it.instance("returns items in chronological order within a page", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 4) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 4) - const result = MessageV2.page({ sessionID, limit: 4 }) - expect(result.items.map((item) => item.info.id)).toEqual(ids) - })), + const result = MessageV2.page({ sessionID, limit: 4 }) + expect(result.items.map((item) => item.info.id)).toEqual(ids) + }), + ), ) it.instance("returns empty items for session with no messages", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const result = MessageV2.page({ sessionID, limit: 10 }) - expect(result.items).toEqual([]) - expect(result.more).toBe(false) - expect(result.cursor).toBeUndefined() - })), + withSession(({ sessionID }) => + Effect.gen(function* () { + const result = MessageV2.page({ sessionID, limit: 10 }) + expect(result.items).toEqual([]) + expect(result.more).toBe(false) + expect(result.cursor).toBeUndefined() + }), + ), ) it.instance("throws NotFoundError for non-existent session", () => @@ -183,69 +191,79 @@ describe("MessageV2.page", () => { ) it.instance("handles exact limit boundary", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 3) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 3) - const result = MessageV2.page({ sessionID, limit: 3 }) - expect(result.items.map((item) => item.info.id)).toEqual(ids) - expect(result.more).toBe(false) - expect(result.cursor).toBeUndefined() - })), + const result = MessageV2.page({ sessionID, limit: 3 }) + expect(result.items.map((item) => item.info.id)).toEqual(ids) + expect(result.more).toBe(false) + expect(result.cursor).toBeUndefined() + }), + ), ) it.instance("limit of 1 returns single newest message", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 5) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 5) - const result = MessageV2.page({ sessionID, limit: 1 }) - expect(result.items).toHaveLength(1) - expect(result.items[0].info.id).toBe(ids[ids.length - 1]) - expect(result.more).toBe(true) - })), + const result = MessageV2.page({ sessionID, limit: 1 }) + expect(result.items).toHaveLength(1) + expect(result.items[0].info.id).toBe(ids[ids.length - 1]) + expect(result.more).toBe(true) + }), + ), ) it.instance("hydrates multiple parts per message", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: id, - type: "text", - text: "extra", - }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: id, + type: "text", + text: "extra", + }) - const result = MessageV2.page({ sessionID, limit: 10 }) - expect(result.items).toHaveLength(1) - expect(result.items[0].parts).toHaveLength(2) - })), + const result = MessageV2.page({ sessionID, limit: 10 }) + expect(result.items).toHaveLength(1) + expect(result.items[0].parts).toHaveLength(2) + }), + ), ) it.instance("accepts cursors from fractional timestamps", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 4, (i: number) => 1000.5 + i) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 4, (i: number) => 1000.5 + i) - const a = MessageV2.page({ sessionID, limit: 2 }) - const b = MessageV2.page({ sessionID, limit: 2, before: a.cursor! }) + const a = MessageV2.page({ sessionID, limit: 2 }) + const b = MessageV2.page({ sessionID, limit: 2, before: a.cursor! }) - expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) - expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) - })), + expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) + expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) + }), + ), ) it.instance("messages with same timestamp are ordered by id", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 4, () => 1000) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 4, () => 1000) - const a = MessageV2.page({ sessionID, limit: 2 }) - expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) - expect(a.more).toBe(true) + const a = MessageV2.page({ sessionID, limit: 2 }) + expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) + expect(a.more).toBe(true) - const b = MessageV2.page({ sessionID, limit: 2, before: a.cursor! }) - expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) - expect(b.more).toBe(false) - })), + const b = MessageV2.page({ sessionID, limit: 2, before: a.cursor! }) + expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) + expect(b.more).toBe(false) + }), + ), ) it.instance("does not return messages from other sessions", () => @@ -269,128 +287,148 @@ describe("MessageV2.page", () => { ) it.instance("large limit returns all messages without cursor", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 10) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 10) - const result = MessageV2.page({ sessionID, limit: 100 }) - expect(result.items).toHaveLength(10) - expect(result.items.map((item) => item.info.id)).toEqual(ids) - expect(result.more).toBe(false) - expect(result.cursor).toBeUndefined() - })), + const result = MessageV2.page({ sessionID, limit: 100 }) + expect(result.items).toHaveLength(10) + expect(result.items.map((item) => item.info.id)).toEqual(ids) + expect(result.more).toBe(false) + expect(result.cursor).toBeUndefined() + }), + ), ) }) describe("MessageV2.stream", () => { it.instance("yields items newest first", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 5) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 5) - const items = Array.from(MessageV2.stream(sessionID)) - expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse()) - })), + const items = Array.from(MessageV2.stream(sessionID)) + expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse()) + }), + ), ) it.instance("yields nothing for empty session", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const items = Array.from(MessageV2.stream(sessionID)) - expect(items).toHaveLength(0) - })), + withSession(({ sessionID }) => + Effect.gen(function* () { + const items = Array.from(MessageV2.stream(sessionID)) + expect(items).toHaveLength(0) + }), + ), ) it.instance("yields single message", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 1) - const items = Array.from(MessageV2.stream(sessionID)) - expect(items).toHaveLength(1) - expect(items[0].info.id).toBe(ids[0]) - })), + const items = Array.from(MessageV2.stream(sessionID)) + expect(items).toHaveLength(1) + expect(items[0].info.id).toBe(ids[0]) + }), + ), ) it.instance("hydrates parts for each yielded message", () => - withSession(({ sessionID }) => Effect.gen(function* () { - yield* fill(sessionID, 3) + withSession(({ sessionID }) => + Effect.gen(function* () { + yield* fill(sessionID, 3) - const items = Array.from(MessageV2.stream(sessionID)) - for (const item of items) { - expect(item.parts).toHaveLength(1) - expect(item.parts[0].type).toBe("text") - } - })), + const items = Array.from(MessageV2.stream(sessionID)) + for (const item of items) { + expect(item.parts).toHaveLength(1) + expect(item.parts[0].type).toBe("text") + } + }), + ), ) it.instance("handles sets exceeding internal page size", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 60) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 60) - const items = Array.from(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]) - })), + const items = Array.from(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]) + }), + ), ) it.instance("is a sync generator", () => - withSession(({ sessionID }) => Effect.gen(function* () { - yield* fill(sessionID, 1) + 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 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) + }), + ), ) }) describe("MessageV2.parts", () => { it.instance("returns parts for a message", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - const result = MessageV2.parts(id) - expect(result).toHaveLength(1) - expect(result[0].type).toBe("text") - expect((result[0] as MessageV2.TextPart).text).toBe("m0") - })), + const result = MessageV2.parts(id) + expect(result).toHaveLength(1) + expect(result[0].type).toBe("text") + expect((result[0] as MessageV2.TextPart).text).toBe("m0") + }), + ), ) it.instance("returns empty array for message with no parts", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const id = yield* addUser(sessionID) + withSession(({ sessionID }) => + Effect.gen(function* () { + const id = yield* addUser(sessionID) - const result = MessageV2.parts(id) - expect(result).toEqual([]) - })), + const result = MessageV2.parts(id) + expect(result).toEqual([]) + }), + ), ) it.instance("returns multiple parts in order", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: id, - type: "text", - text: "second", - }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: id, - type: "text", - text: "third", - }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: id, + type: "text", + text: "second", + }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: id, + type: "text", + text: "third", + }) - const result = 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") - })), + const result = 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") + }), + ), ) it.instance("returns empty for non-existent message id", () => @@ -402,36 +440,40 @@ describe("MessageV2.parts", () => { ) it.instance("parts contain sessionID and messageID", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - const result = MessageV2.parts(id) - expect(result[0].sessionID).toBe(sessionID) - expect(result[0].messageID).toBe(id) - })), + const result = MessageV2.parts(id) + expect(result[0].sessionID).toBe(sessionID) + expect(result[0].messageID).toBe(id) + }), + ), ) }) describe("MessageV2.get", () => { it.instance("returns message with hydrated parts", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - const result = MessageV2.get({ sessionID, messageID: id }) - expect(result.info.id).toBe(id) - 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") - })), + const result = MessageV2.get({ sessionID, messageID: id }) + expect(result.info.id).toBe(id) + 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") + }), + ), ) it.instance("throws NotFoundError for non-existent message", () => - withSession(({ sessionID }) => Effect.gen(function* () { - expect(() => MessageV2.get({ sessionID, messageID: MessageID.ascending() })).toThrow( - "NotFoundError", - ) - })), + withSession(({ sessionID }) => + Effect.gen(function* () { + expect(() => MessageV2.get({ sessionID, messageID: MessageID.ascending() })).toThrow("NotFoundError") + }), + ), ) it.instance("scopes by session id", () => @@ -451,192 +493,212 @@ describe("MessageV2.get", () => { ) it.instance("returns message with multiple parts", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: id, - type: "text", - text: "extra", - }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: id, + type: "text", + text: "extra", + }) - const result = MessageV2.get({ sessionID, messageID: id }) - expect(result.parts).toHaveLength(2) - })), + const result = MessageV2.get({ sessionID, messageID: id }) + expect(result.parts).toHaveLength(2) + }), + ), ) it.instance("returns assistant message with correct role", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const uid = yield* addUser(sessionID, "hello") - const aid = yield* addAssistant(sessionID, uid) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const uid = yield* addUser(sessionID, "hello") + const aid = yield* addAssistant(sessionID, uid) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: aid, - type: "text", - text: "response", - }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: aid, + type: "text", + text: "response", + }) - const result = 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") - })), + const result = 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") + }), + ), ) it.instance("returns message with zero parts", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const id = yield* addUser(sessionID) + withSession(({ sessionID }) => + Effect.gen(function* () { + const id = yield* addUser(sessionID) - const result = MessageV2.get({ sessionID, messageID: id }) - expect(result.info.id).toBe(id) - expect(result.parts).toEqual([]) - })), + const result = MessageV2.get({ sessionID, messageID: id }) + expect(result.info.id).toBe(id) + expect(result.parts).toEqual([]) + }), + ), ) }) describe("MessageV2.filterCompacted", () => { it.instance("returns all messages when no compaction", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const ids = yield* fill(sessionID, 5) + withSession(({ sessionID }) => + Effect.gen(function* () { + const ids = yield* fill(sessionID, 5) - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - expect(result).toHaveLength(5) - // reversed from newest-first to chronological - expect(result.map((item) => item.info.id)).toEqual(ids) - })), + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + expect(result).toHaveLength(5) + // reversed from newest-first to chronological + expect(result.map((item) => item.info.id)).toEqual(ids) + }), + ), ) it.instance("stops at compaction boundary and returns chronological order", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - // Chronological: u1(+compaction part), a1(summary, parentID=u1), u2, a2 - // Stream (newest first): a2, u2, a1(adds u1 to completed), u1(in completed + compaction) -> break - const u1 = yield* addUser(sessionID, "first question") - const a1 = yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a1, - type: "text", - text: "summary", - }) - yield* addCompactionPart(sessionID, u1) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + // Chronological: u1(+compaction part), a1(summary, parentID=u1), u2, a2 + // Stream (newest first): a2, u2, a1(adds u1 to completed), u1(in completed + compaction) -> break + const u1 = yield* addUser(sessionID, "first question") + const a1 = yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a1, + type: "text", + text: "summary", + }) + yield* addCompactionPart(sessionID, u1) - const u2 = yield* addUser(sessionID, "new question") - const a2 = yield* addAssistant(sessionID, u2) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a2, - type: "text", - text: "new response", - }) + const u2 = yield* addUser(sessionID, "new question") + const a2 = yield* addAssistant(sessionID, u2) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a2, + type: "text", + text: "new response", + }) - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - // Includes compaction boundary: u1, a1, u2, a2 - expect(result[0].info.id).toBe(u1) - expect(result.length).toBe(4) - })), + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + // Includes compaction boundary: u1, a1, u2, a2 + expect(result[0].info.id).toBe(u1) + expect(result.length).toBe(4) + }), + ), ) - it.live("handles empty iterable", () => Effect.sync(() => { - const result = MessageV2.filterCompacted([]) - expect(result).toEqual([]) - })) + it.live("handles empty iterable", () => + Effect.sync(() => { + const result = MessageV2.filterCompacted([]) + expect(result).toEqual([]) + }), + ) it.instance("does not break on compaction part without matching summary", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const u1 = yield* addUser(sessionID, "hello") - yield* addCompactionPart(sessionID, u1) - yield* addUser(sessionID, "world") + withSession(({ sessionID }) => + Effect.gen(function* () { + const u1 = yield* addUser(sessionID, "hello") + yield* addCompactionPart(sessionID, u1) + yield* addUser(sessionID, "world") - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - expect(result).toHaveLength(2) - })), + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + expect(result).toHaveLength(2) + }), + ), ) it.instance("skips assistant with error even if marked as summary", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const u1 = yield* addUser(sessionID, "hello") - yield* addCompactionPart(sessionID, u1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const u1 = yield* addUser(sessionID, "hello") + yield* addCompactionPart(sessionID, u1) - const error = new MessageV2.APIError({ - message: "boom", - isRetryable: true, - }).toObject() as MessageV2.Assistant["error"] - yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn", error }) - yield* addUser(sessionID, "retry") + const error = new MessageV2.APIError({ + message: "boom", + isRetryable: true, + }).toObject() as MessageV2.Assistant["error"] + yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn", error }) + yield* addUser(sessionID, "retry") - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - // Error assistant doesn't add to completed, so compaction boundary never triggers - expect(result).toHaveLength(3) - })), + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + // Error assistant doesn't add to completed, so compaction boundary never triggers + expect(result).toHaveLength(3) + }), + ), ) it.instance("skips assistant without finish even if marked as summary", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const u1 = yield* addUser(sessionID, "hello") - yield* addCompactionPart(sessionID, u1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const u1 = yield* addUser(sessionID, "hello") + yield* addCompactionPart(sessionID, u1) - // summary=true but no finish - yield* addAssistant(sessionID, u1, { summary: true }) - yield* addUser(sessionID, "next") + // summary=true but no finish + yield* addAssistant(sessionID, u1, { summary: true }) + yield* addUser(sessionID, "next") - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - expect(result).toHaveLength(3) - })), + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + expect(result).toHaveLength(3) + }), + ), ) it.instance("ignores original tail when compaction stores tail_start_id", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const u1 = yield* addUser(sessionID, "first") - const a1 = yield* addAssistant(sessionID, u1, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a1, - type: "text", - text: "first reply", - }) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const u1 = yield* addUser(sessionID, "first") + const a1 = yield* addAssistant(sessionID, u1, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a1, + type: "text", + text: "first reply", + }) - const u2 = yield* addUser(sessionID, "second") - const a2 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a2, - type: "text", - text: "second reply", - }) + const u2 = yield* addUser(sessionID, "second") + const a2 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a2, + type: "text", + text: "second reply", + }) - const c1 = yield* addUser(sessionID) - yield* addCompactionPart(sessionID, c1, u2) - const s1 = yield* addAssistant(sessionID, c1, { summary: true, finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: s1, - type: "text", - text: "summary", - }) + const c1 = yield* addUser(sessionID) + yield* addCompactionPart(sessionID, c1, u2) + const s1 = yield* addAssistant(sessionID, c1, { summary: true, finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: s1, + type: "text", + text: "summary", + }) - const u3 = yield* addUser(sessionID, "third") - const a3 = yield* addAssistant(sessionID, u3, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a3, - type: "text", - text: "third reply", - }) + const u3 = yield* addUser(sessionID, "third") + const a3 = yield* addAssistant(sessionID, u3, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a3, + type: "text", + text: "third reply", + }) - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - expect(result.map((item) => item.info.id)).toEqual([c1, s1, u3, a3]) - })), + expect(result.map((item) => item.info.id)).toEqual([c1, s1, u3, a3]) + }), + ), ) it.instance("fork keeps legacy tail_start_id without replaying the tail", () => @@ -704,130 +766,134 @@ describe("MessageV2.filterCompacted", () => { ) it.instance("does not replay an assistant tail when compaction starts inside a turn", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const u1 = yield* addUser(sessionID, "first") - const a1 = yield* addAssistant(sessionID, u1, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a1, - type: "text", - text: "first reply", - }) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const u1 = yield* addUser(sessionID, "first") + const a1 = yield* addAssistant(sessionID, u1, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a1, + type: "text", + text: "first reply", + }) - const u2 = yield* addUser(sessionID, "second") - const a2 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a2, - type: "text", - text: "second reply", - }) - const a3 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a3, - type: "text", - text: "tail reply", - }) + const u2 = yield* addUser(sessionID, "second") + const a2 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a2, + type: "text", + text: "second reply", + }) + const a3 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a3, + type: "text", + text: "tail reply", + }) - const c1 = yield* addUser(sessionID) - yield* addCompactionPart(sessionID, c1, a3) - const s1 = yield* addAssistant(sessionID, c1, { summary: true, finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: s1, - type: "text", - text: "summary", - }) + const c1 = yield* addUser(sessionID) + yield* addCompactionPart(sessionID, c1, a3) + const s1 = yield* addAssistant(sessionID, c1, { summary: true, finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: s1, + type: "text", + text: "summary", + }) - const u3 = yield* addUser(sessionID, "third") - const a4 = yield* addAssistant(sessionID, u3, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a4, - type: "text", - text: "third reply", - }) + const u3 = yield* addUser(sessionID, "third") + const a4 = yield* addAssistant(sessionID, u3, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a4, + type: "text", + text: "third reply", + }) - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - expect(result.map((item) => item.info.id)).toEqual([c1, s1, u3, a4]) - })), + expect(result.map((item) => item.info.id)).toEqual([c1, s1, u3, a4]) + }), + ), ) it.instance("prefers latest compaction boundary when repeated compactions exist", () => - withSession(({ session, sessionID }) => Effect.gen(function* () { - const u1 = yield* addUser(sessionID, "first") - const a1 = yield* addAssistant(sessionID, u1, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a1, - type: "text", - text: "first reply", - }) + withSession(({ session, sessionID }) => + Effect.gen(function* () { + const u1 = yield* addUser(sessionID, "first") + const a1 = yield* addAssistant(sessionID, u1, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a1, + type: "text", + text: "first reply", + }) - const u2 = yield* addUser(sessionID, "second") - const a2 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a2, - type: "text", - text: "second reply", - }) + const u2 = yield* addUser(sessionID, "second") + const a2 = yield* addAssistant(sessionID, u2, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a2, + type: "text", + text: "second reply", + }) - const c1 = yield* addUser(sessionID) - yield* addCompactionPart(sessionID, c1, u2) - const s1 = yield* addAssistant(sessionID, c1, { summary: true, finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: s1, - type: "text", - text: "summary one", - }) + const c1 = yield* addUser(sessionID) + yield* addCompactionPart(sessionID, c1, u2) + const s1 = yield* addAssistant(sessionID, c1, { summary: true, finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: s1, + type: "text", + text: "summary one", + }) - const u3 = yield* addUser(sessionID, "third") - const a3 = yield* addAssistant(sessionID, u3, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a3, - type: "text", - text: "third reply", - }) + const u3 = yield* addUser(sessionID, "third") + const a3 = yield* addAssistant(sessionID, u3, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a3, + type: "text", + text: "third reply", + }) - const c2 = yield* addUser(sessionID) - yield* addCompactionPart(sessionID, c2, u3) - const s2 = yield* addAssistant(sessionID, c2, { summary: true, finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: s2, - type: "text", - text: "summary two", - }) + const c2 = yield* addUser(sessionID) + yield* addCompactionPart(sessionID, c2, u3) + const s2 = yield* addAssistant(sessionID, c2, { summary: true, finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: s2, + type: "text", + text: "summary two", + }) - const u4 = yield* addUser(sessionID, "fourth") - const a4 = yield* addAssistant(sessionID, u4, { finish: "end_turn" }) - yield* session.updatePart({ - id: PartID.ascending(), - sessionID, - messageID: a4, - type: "text", - text: "fourth reply", - }) + const u4 = yield* addUser(sessionID, "fourth") + const a4 = yield* addAssistant(sessionID, u4, { finish: "end_turn" }) + yield* session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: a4, + type: "text", + text: "fourth reply", + }) - const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + const result = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - expect(result.map((item) => item.info.id)).toEqual([c2, s2, u4, a4]) - })), + expect(result.map((item) => item.info.id)).toEqual([c2, s2, u4, a4]) + }), + ), ) test("works with array input", () => { @@ -876,57 +942,65 @@ describe("MessageV2.cursor", () => { describe("MessageV2 consistency", () => { it.instance("page hydration matches get for each message", () => - withSession(({ sessionID }) => Effect.gen(function* () { - yield* fill(sessionID, 3) + withSession(({ sessionID }) => + Effect.gen(function* () { + yield* fill(sessionID, 3) - const paged = MessageV2.page({ sessionID, limit: 10 }) - for (const item of paged.items) { - const got = MessageV2.get({ sessionID, messageID: item.info.id as MessageID }) - expect(got.info).toEqual(item.info) - expect(got.parts).toEqual(item.parts) - } - })), + const paged = MessageV2.page({ sessionID, limit: 10 }) + for (const item of paged.items) { + const got = MessageV2.get({ sessionID, messageID: item.info.id as MessageID }) + expect(got.info).toEqual(item.info) + expect(got.parts).toEqual(item.parts) + } + }), + ), ) it.instance("parts from get match standalone parts call", () => - withSession(({ sessionID }) => Effect.gen(function* () { - const [id] = yield* fill(sessionID, 1) + withSession(({ sessionID }) => + Effect.gen(function* () { + const [id] = yield* fill(sessionID, 1) - const got = MessageV2.get({ sessionID, messageID: id }) - const standalone = MessageV2.parts(id) - expect(got.parts).toEqual(standalone) - })), + const got = MessageV2.get({ sessionID, messageID: id }) + const standalone = MessageV2.parts(id) + expect(got.parts).toEqual(standalone) + }), + ), ) it.instance("stream collects same messages as exhaustive page iteration", () => - withSession(({ sessionID }) => Effect.gen(function* () { - yield* fill(sessionID, 7) + withSession(({ sessionID }) => + Effect.gen(function* () { + yield* fill(sessionID, 7) - const streamed = Array.from(MessageV2.stream(sessionID)) + const streamed = Array.from(MessageV2.stream(sessionID)) - const paged = [] as MessageV2.WithParts[] - let cursor: string | undefined - while (true) { - const result = MessageV2.page({ sessionID, limit: 3, before: cursor }) - for (let i = result.items.length - 1; i >= 0; i--) { - paged.push(result.items[i]) + const paged = [] as MessageV2.WithParts[] + let cursor: string | undefined + while (true) { + const result = MessageV2.page({ sessionID, limit: 3, before: cursor }) + for (let i = result.items.length - 1; i >= 0; i--) { + paged.push(result.items[i]) + } + if (!result.more || !result.cursor) break + cursor = result.cursor } - if (!result.more || !result.cursor) break - cursor = result.cursor - } - expect(streamed.map((m) => m.info.id)).toEqual(paged.map((m) => m.info.id)) - })), + expect(streamed.map((m) => m.info.id)).toEqual(paged.map((m) => m.info.id)) + }), + ), ) it.instance("filterCompacted of full stream returns same as Array.from when no compaction", () => - withSession(({ sessionID }) => Effect.gen(function* () { - yield* fill(sessionID, 4) + withSession(({ sessionID }) => + Effect.gen(function* () { + yield* fill(sessionID, 4) - const filtered = MessageV2.filterCompacted(MessageV2.stream(sessionID)) - const all = Array.from(MessageV2.stream(sessionID)).reverse() + const filtered = MessageV2.filterCompacted(MessageV2.stream(sessionID)) + const all = Array.from(MessageV2.stream(sessionID)).reverse() - expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id)) - })), + expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id)) + }), + ), ) })