test: add tests for SDK recovery modules (empty-content-recovery, recover-empty-content-message)

This commit is contained in:
YeonGyu-Kim 2026-02-16 18:20:32 +09:00
parent 8e0d1341b6
commit 66e66e5d73
2 changed files with 312 additions and 0 deletions

View File

@ -0,0 +1,166 @@
import { describe, it, expect, mock, beforeEach } from "bun:test"
import { fixEmptyMessagesWithSDK } from "./empty-content-recovery-sdk"
const mockReplaceEmptyTextParts = mock(() => Promise.resolve(false))
const mockInjectTextPart = mock(() => Promise.resolve(false))
mock.module("../session-recovery/storage/empty-text", () => ({
replaceEmptyTextPartsAsync: mockReplaceEmptyTextParts,
}))
mock.module("../session-recovery/storage/text-part-injector", () => ({
injectTextPartAsync: mockInjectTextPart,
}))
function createMockClient(messages: Array<{ info?: { id?: string }; parts?: Array<{ type?: string; text?: string }> }>) {
return {
session: {
messages: mock(() => Promise.resolve({ data: messages })),
},
} as never
}
describe("fixEmptyMessagesWithSDK", () => {
beforeEach(() => {
mockReplaceEmptyTextParts.mockReset()
mockInjectTextPart.mockReset()
mockReplaceEmptyTextParts.mockReturnValue(Promise.resolve(false))
mockInjectTextPart.mockReturnValue(Promise.resolve(false))
})
it("returns fixed=false when no empty messages exist", async () => {
//#given
const client = createMockClient([
{ info: { id: "msg_1" }, parts: [{ type: "text", text: "Hello" }] },
])
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
})
//#then
expect(result.fixed).toBe(false)
expect(result.fixedMessageIds).toEqual([])
expect(result.scannedEmptyCount).toBe(0)
})
it("fixes empty message via replace when scanning all", async () => {
//#given
const client = createMockClient([
{ info: { id: "msg_1" }, parts: [{ type: "text", text: "" }] },
])
mockReplaceEmptyTextParts.mockReturnValue(Promise.resolve(true))
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
})
//#then
expect(result.fixed).toBe(true)
expect(result.fixedMessageIds).toContain("msg_1")
expect(result.scannedEmptyCount).toBe(1)
})
it("falls back to inject when replace fails", async () => {
//#given
const client = createMockClient([
{ info: { id: "msg_1" }, parts: [] },
])
mockReplaceEmptyTextParts.mockReturnValue(Promise.resolve(false))
mockInjectTextPart.mockReturnValue(Promise.resolve(true))
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
})
//#then
expect(result.fixed).toBe(true)
expect(result.fixedMessageIds).toContain("msg_1")
})
it("fixes target message by index when provided", async () => {
//#given
const client = createMockClient([
{ info: { id: "msg_0" }, parts: [{ type: "text", text: "ok" }] },
{ info: { id: "msg_1" }, parts: [] },
])
mockReplaceEmptyTextParts.mockReturnValue(Promise.resolve(true))
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
messageIndex: 1,
})
//#then
expect(result.fixed).toBe(true)
expect(result.fixedMessageIds).toContain("msg_1")
expect(result.scannedEmptyCount).toBe(0)
})
it("skips messages without info.id", async () => {
//#given
const client = createMockClient([
{ parts: [] },
{ info: {}, parts: [] },
])
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
})
//#then
expect(result.fixed).toBe(false)
expect(result.scannedEmptyCount).toBe(0)
})
it("treats thinking-only messages as empty", async () => {
//#given
const client = createMockClient([
{ info: { id: "msg_1" }, parts: [{ type: "thinking", text: "hmm" }] },
])
mockReplaceEmptyTextParts.mockReturnValue(Promise.resolve(true))
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
})
//#then
expect(result.fixed).toBe(true)
expect(result.fixedMessageIds).toContain("msg_1")
})
it("treats tool_use messages as non-empty", async () => {
//#given
const client = createMockClient([
{ info: { id: "msg_1" }, parts: [{ type: "tool_use" }] },
])
//#when
const result = await fixEmptyMessagesWithSDK({
sessionID: "ses_1",
client,
placeholderText: "[recovered]",
})
//#then
expect(result.fixed).toBe(false)
expect(result.scannedEmptyCount).toBe(0)
})
})

View File

@ -0,0 +1,146 @@
import { describe, it, expect, mock, beforeEach } from "bun:test"
import { recoverEmptyContentMessageFromSDK } from "./recover-empty-content-message-sdk"
import type { MessageData } from "./types"
function createMockClient(messages: MessageData[]) {
return {
session: {
messages: mock(() => Promise.resolve({ data: messages })),
},
} as never
}
function createDeps(overrides?: Partial<Parameters<typeof recoverEmptyContentMessageFromSDK>[4]>) {
return {
placeholderText: "[recovered]",
replaceEmptyTextPartsAsync: mock(() => Promise.resolve(false)),
injectTextPartAsync: mock(() => Promise.resolve(false)),
findMessagesWithEmptyTextPartsFromSDK: mock(() => Promise.resolve([] as string[])),
...overrides,
}
}
const emptyMsg: MessageData = { info: { id: "msg_1", role: "assistant" }, parts: [] }
const contentMsg: MessageData = { info: { id: "msg_2", role: "assistant" }, parts: [{ type: "text", text: "Hello" }] }
const thinkingOnlyMsg: MessageData = { info: { id: "msg_3", role: "assistant" }, parts: [{ type: "thinking", text: "hmm" }] }
describe("recoverEmptyContentMessageFromSDK", () => {
it("returns false when no empty messages exist", async () => {
//#given
const client = createMockClient([contentMsg])
const deps = createDeps()
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", contentMsg, new Error("test"), deps,
)
//#then
expect(result).toBe(false)
})
it("fixes messages with empty text parts via replace", async () => {
//#given
const client = createMockClient([emptyMsg])
const deps = createDeps({
findMessagesWithEmptyTextPartsFromSDK: mock(() => Promise.resolve(["msg_1"])),
replaceEmptyTextPartsAsync: mock(() => Promise.resolve(true)),
})
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", emptyMsg, new Error("test"), deps,
)
//#then
expect(result).toBe(true)
})
it("injects text part into thinking-only messages", async () => {
//#given
const client = createMockClient([thinkingOnlyMsg])
const deps = createDeps({
injectTextPartAsync: mock(() => Promise.resolve(true)),
})
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", thinkingOnlyMsg, new Error("test"), deps,
)
//#then
expect(result).toBe(true)
expect(deps.injectTextPartAsync).toHaveBeenCalledWith(
client, "ses_1", "msg_3", "[recovered]",
)
})
it("targets message by index from error", async () => {
//#given
const client = createMockClient([contentMsg, emptyMsg])
const error = new Error("messages: index 1 has empty content")
const deps = createDeps({
replaceEmptyTextPartsAsync: mock(() => Promise.resolve(true)),
})
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", emptyMsg, error, deps,
)
//#then
expect(result).toBe(true)
})
it("falls back to failedID when targetIndex fix fails", async () => {
//#given
const failedMsg: MessageData = { info: { id: "msg_fail" }, parts: [] }
const client = createMockClient([contentMsg])
const deps = createDeps({
replaceEmptyTextPartsAsync: mock(() => Promise.resolve(false)),
injectTextPartAsync: mock(() => Promise.resolve(true)),
})
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", failedMsg, new Error("test"), deps,
)
//#then
expect(result).toBe(true)
expect(deps.injectTextPartAsync).toHaveBeenCalledWith(
client, "ses_1", "msg_fail", "[recovered]",
)
})
it("returns false when SDK throws during message read", async () => {
//#given
const client = { session: { messages: mock(() => Promise.reject(new Error("SDK error"))) } } as never
const deps = createDeps()
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", emptyMsg, new Error("test"), deps,
)
//#then
expect(result).toBe(false)
})
it("scans all empty messages when no target index available", async () => {
//#given
const empty1: MessageData = { info: { id: "e1" }, parts: [] }
const empty2: MessageData = { info: { id: "e2" }, parts: [] }
const client = createMockClient([empty1, empty2])
const replaceMock = mock(() => Promise.resolve(true))
const deps = createDeps({ replaceEmptyTextPartsAsync: replaceMock })
//#when
const result = await recoverEmptyContentMessageFromSDK(
client, "ses_1", empty1, new Error("test"), deps,
)
//#then
expect(result).toBe(true)
})
})