assistant-cloud
Version:
Cloud integration for assistant-ui
862 lines (752 loc) • 28 kB
text/typescript
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CloudMessagePersistence } from "../CloudMessagePersistence";
import type { AssistantCloud } from "../AssistantCloud";
import { CloudAPIError } from "../AssistantCloudAPI";
function createMockCloud() {
return {
threads: {
messages: {
create: vi.fn(),
list: vi.fn(),
update: vi.fn(),
delete: vi.fn().mockResolvedValue(undefined),
},
},
} as unknown as AssistantCloud;
}
function createCloudMessages(count: number) {
return Array.from({ length: count }, (_, index) => ({
id: `message-${index + 1}`,
parent_id: null,
height: index,
created_at: "2025-01-01T00:00:00Z" as unknown as Date,
updated_at: "2025-01-01T00:00:00Z" as unknown as Date,
format: "aui/v0",
content: {},
}));
}
describe("CloudMessagePersistence", () => {
let cloud: AssistantCloud;
let persistence: CloudMessagePersistence;
beforeEach(() => {
vi.restoreAllMocks();
cloud = createMockCloud();
persistence = new CloudMessagePersistence(cloud);
});
it("waits for a pending append before deleting its remote ID", async () => {
let resolveAppend!: (value: { message_id: string }) => void;
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveAppend = resolve;
}),
);
const append = persistence.append(
"thread-1",
"local-1",
null,
"aui/v0",
{},
);
const deletion = persistence.delete("thread-1", ["local-1"]);
expect(cloud.threads.messages.delete).not.toHaveBeenCalled();
resolveAppend({ message_id: "msg_1" });
await append;
await deletion;
expect(cloud.threads.messages.delete).toHaveBeenCalledWith("thread-1", [
"msg_1",
]);
expect(persistence.isPersisted("local-1")).toBe(false);
});
it("skips unmapped IDs and failed appends without a delete request", async () => {
await persistence.delete("thread-1", ["missing"]);
let rejectAppend!: (error: Error) => void;
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((_, reject) => {
rejectAppend = reject;
}),
);
const append = persistence.append(
"thread-1",
"local-1",
null,
"aui/v0",
{},
);
const rejected = expect(append).rejects.toThrow("append failed");
const deletion = persistence.delete("thread-1", ["local-1"]);
rejectAppend(new Error("append failed"));
await rejected;
await deletion;
expect(cloud.threads.messages.delete).not.toHaveBeenCalled();
vi.mocked(cloud.threads.messages.create).mockResolvedValueOnce({
message_id: "msg_child",
});
await persistence.append("thread-1", "child", "local-1", "aui/v0", {});
expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
});
it("creates a failed parent again before its child and grandchild", async () => {
vi.mocked(cloud.threads.messages.create)
.mockRejectedValueOnce(new Error("parent create failed"))
.mockResolvedValueOnce({ message_id: "msg_parent" })
.mockResolvedValueOnce({ message_id: "msg_child" })
.mockResolvedValueOnce({ message_id: "msg_grandchild" });
await expect(
persistence.append("thread-1", "parent", null, "aui/v0", { n: 0 }),
).rejects.toThrow("parent create failed");
await persistence.append("thread-1", "child", "parent", "aui/v0", {
n: 1,
});
await persistence.append("thread-1", "grandchild", "child", "aui/v0", {
n: 2,
});
expect(
vi
.mocked(cloud.threads.messages.create)
.mock.calls.map(([, body]) => [body.parent_id, body.content]),
).toEqual([
[null, { n: 0 }],
[null, { n: 0 }],
["msg_parent", { n: 1 }],
["msg_child", { n: 2 }],
]);
});
it("creates a parent again when its create fails while the child waits", async () => {
let rejectParent!: (error: Error) => void;
vi.mocked(cloud.threads.messages.create)
.mockImplementationOnce(
() =>
new Promise((_, reject) => {
rejectParent = reject;
}),
)
.mockResolvedValueOnce({ message_id: "msg_parent" })
.mockResolvedValueOnce({ message_id: "msg_child" });
const parent = persistence.append("thread-1", "parent", null, "aui/v0", {});
const child = persistence.append(
"thread-1",
"child",
"parent",
"aui/v0",
{},
);
rejectParent(new Error("parent create failed"));
await expect(parent).rejects.toThrow("parent create failed");
await child;
expect(
vi.mocked(cloud.threads.messages.create).mock.calls[2]![1].parent_id,
).toBe("msg_parent");
});
it("stores a child as a root when its parent still cannot be created", async () => {
vi.mocked(cloud.threads.messages.create)
.mockRejectedValueOnce(new Error("content too large"))
.mockRejectedValueOnce(new Error("content too large"))
.mockResolvedValueOnce({ message_id: "msg_child" })
.mockResolvedValueOnce({ message_id: "msg_grandchild" });
await expect(
persistence.append("thread-1", "parent", null, "aui/v0", {}),
).rejects.toThrow("content too large");
await persistence.append("thread-1", "child", "parent", "aui/v0", {});
await persistence.append("thread-1", "grandchild", "child", "aui/v0", {});
expect(
vi
.mocked(cloud.threads.messages.create)
.mock.calls.map(([, body]) => body.parent_id),
).toEqual([null, null, null, "msg_child"]);
expect(persistence.getResolvedRemoteId("grandchild")).toBe(
"msg_grandchild",
);
});
it("repeats a failed parent's create under the same external id", async () => {
vi.mocked(cloud.threads.messages.create)
.mockRejectedValueOnce(new Error("response lost"))
.mockResolvedValueOnce({ message_id: "msg_parent" })
.mockResolvedValueOnce({ message_id: "msg_child" });
await expect(
persistence.append("thread-1", "parent", null, "aui/v0", {}),
).rejects.toThrow("response lost");
await persistence.append("thread-1", "child", "parent", "aui/v0", {});
expect(
vi
.mocked(cloud.threads.messages.create)
.mock.calls.map(([, body]) => body.external_id),
).toEqual(["parent", "parent", "child"]);
});
it("omits an external id longer than the API accepts", async () => {
const longId = "m".repeat(256);
vi.mocked(cloud.threads.messages.create).mockResolvedValueOnce({
message_id: "msg_long",
});
await persistence.append("thread-1", longId, null, "aui/v0", {});
expect(
vi.mocked(cloud.threads.messages.create).mock.calls[0]![1],
).not.toHaveProperty("external_id");
expect(persistence.getResolvedRemoteId(longId)).toBe("msg_long");
});
it("does not create a deleted failed parent again", async () => {
vi.mocked(cloud.threads.messages.create)
.mockRejectedValueOnce(new Error("parent create failed"))
.mockResolvedValueOnce({ message_id: "msg_child" });
await expect(
persistence.append("thread-1", "parent", null, "aui/v0", {}),
).rejects.toThrow("parent create failed");
await persistence.delete("thread-1", ["parent"]);
await persistence.append("thread-1", "child", "parent", "aui/v0", {});
expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
});
it("keeps a replacement mapping made during deletion", async () => {
persistence.record("local-1", "msg_1");
let resolveDelete!: () => void;
vi.mocked(cloud.threads.messages.delete).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveDelete = resolve;
}),
);
const deletion = persistence.delete("thread-1", ["local-1"]);
await vi.waitFor(() =>
expect(cloud.threads.messages.delete).toHaveBeenCalledOnce(),
);
persistence.record("local-1", "msg_2");
resolveDelete();
await deletion;
expect(await persistence.getRemoteId("local-1")).toBe("msg_2");
});
it("treats a 404 as already deleted and clears the mapping", async () => {
persistence.record("local-1", "msg_1");
vi.mocked(cloud.threads.messages.delete).mockRejectedValueOnce(
new CloudAPIError("Messages not found", 404),
);
await persistence.delete("thread-1", ["local-1"]);
expect(persistence.isPersisted("local-1")).toBe(false);
});
it("propagates a server error and retains the mapping", async () => {
persistence.record("local-1", "msg_1");
const failure = new CloudAPIError("failed", 500);
vi.mocked(cloud.threads.messages.delete).mockRejectedValueOnce(failure);
await expect(persistence.delete("thread-1", ["local-1"])).rejects.toBe(
failure,
);
expect(persistence.isPersisted("local-1")).toBe(true);
});
it("appends a message and maps local ID to remote ID", async () => {
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: "remote-1",
});
await persistence.append("thread-1", "local-1", null, "aui/v0", {
text: "hello",
});
expect(persistence.isPersisted("local-1")).toBe(true);
expect(await persistence.getRemoteId("local-1")).toBe("remote-1");
expect(persistence.getResolvedRemoteId("local-1")).toBe("remote-1");
});
it.each(["__proto__", "constructor", "toString"])(
"supports prototype-named local ID %s",
async (messageId) => {
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: `remote-${messageId}`,
});
expect(persistence.isPersisted(messageId)).toBe(false);
await persistence.append("thread-1", messageId, null, "aui/v0", {
text: "hello",
});
expect(persistence.isPersisted(messageId)).toBe(true);
expect(await persistence.getRemoteId(messageId)).toBe(
`remote-${messageId}`,
);
},
);
it("uses the current client without losing ID mappings", async () => {
const firstCloud = createMockCloud();
const secondCloud = createMockCloud();
let currentCloud = firstCloud;
persistence = new CloudMessagePersistence(() => currentCloud);
vi.mocked(firstCloud.threads.messages.create).mockResolvedValue({
message_id: "remote-parent",
});
vi.mocked(secondCloud.threads.messages.create).mockResolvedValue({
message_id: "remote-child",
});
await persistence.append("thread-1", "parent", null, "aui/v0", {
text: "parent",
});
currentCloud = secondCloud;
await persistence.append("thread-1", "child", "parent", "aui/v0", {
text: "child",
});
expect(secondCloud.threads.messages.create).toHaveBeenCalledWith(
"thread-1",
expect.objectContaining({ parent_id: "remote-parent" }),
);
expect(await persistence.getRemoteId("child")).toBe("remote-child");
});
it("resolves parent ID from a concurrent append", async () => {
// Parent creation is delayed — the promise won't resolve immediately
let resolveParent!: (v: { message_id: string }) => void;
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveParent = resolve;
}),
);
vi.mocked(cloud.threads.messages.create).mockResolvedValueOnce({
message_id: "remote-child",
});
// Start parent append (doesn't resolve yet)
const parentPromise = persistence.append(
"thread-1",
"parent",
null,
"aui/v0",
{ text: "parent" },
);
// Start child append — it will await the parent's promise for its remote ID
const childPromise = persistence.append(
"thread-1",
"child",
"parent",
"aui/v0",
{ text: "child" },
);
// Now resolve the parent
resolveParent({ message_id: "remote-parent" });
await parentPromise;
await childPromise;
// The child's create call should have used the parent's resolved remote ID
const childCreateCall = vi.mocked(cloud.threads.messages.create).mock
.calls[1]!;
expect(childCreateCall[1]).toMatchObject({
parent_id: "remote-parent",
});
});
it("deduplicates concurrent child appends while the parent is pending", async () => {
let resolveParent!: (value: { message_id: string }) => void;
vi.mocked(cloud.threads.messages.create)
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveParent = resolve;
}),
)
.mockResolvedValue({ message_id: "remote-child" });
const parent = persistence.append("thread-1", "parent", null, "aui/v0", {
text: "parent",
});
const firstChild = persistence.append(
"thread-1",
"child",
"parent",
"aui/v0",
{ text: "child" },
);
const secondChild = persistence.append(
"thread-1",
"child",
"parent",
"aui/v0",
{ text: "child" },
);
expect(persistence.isPersisted("child")).toBe(true);
resolveParent({ message_id: "remote-parent" });
await Promise.all([parent, firstChild, secondChild]);
expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
});
it("resolves remote IDs throughout a concurrent message chain", async () => {
let resolveParent!: (value: { message_id: string }) => void;
vi.mocked(cloud.threads.messages.create)
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveParent = resolve;
}),
)
.mockResolvedValueOnce({ message_id: "remote-child" })
.mockResolvedValueOnce({ message_id: "remote-grandchild" });
const parent = persistence.append("thread-1", "parent", null, "aui/v0", {
text: "parent",
});
const child = persistence.append("thread-1", "child", "parent", "aui/v0", {
text: "child",
});
const grandchild = persistence.append(
"thread-1",
"grandchild",
"child",
"aui/v0",
{ text: "grandchild" },
);
resolveParent({ message_id: "remote-parent" });
await Promise.all([parent, child, grandchild]);
expect(cloud.threads.messages.create).toHaveBeenCalledWith("thread-1", {
parent_id: "remote-parent",
external_id: "child",
format: "aui/v0",
content: { text: "child" },
});
expect(cloud.threads.messages.create).toHaveBeenCalledWith("thread-1", {
parent_id: "remote-child",
external_id: "grandchild",
format: "aui/v0",
content: { text: "grandchild" },
});
});
it("re-appends a message after its mapping has settled", async () => {
vi.mocked(cloud.threads.messages.create)
.mockResolvedValueOnce({ message_id: "remote-1" })
.mockResolvedValueOnce({ message_id: "remote-2" });
await persistence.append("thread-1", "local-1", null, "aui/v0", {
text: "first",
});
await persistence.append("thread-1", "local-1", null, "aui/v0", {
text: "second",
});
expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
expect(await persistence.getRemoteId("local-1")).toBe("remote-2");
});
it("loaded messages are marked as persisted and not re-created", async () => {
vi.mocked(cloud.threads.messages.list).mockResolvedValue({
messages: [
{
id: "msg-1",
parent_id: null,
height: 0,
created_at: "2025-01-01T00:00:00Z" as unknown as Date,
updated_at: "2025-01-01T00:00:00Z" as unknown as Date,
format: "aui/v0",
content: { text: "loaded" },
},
],
});
await persistence.load("thread-1");
expect(persistence.isPersisted("msg-1")).toBe(true);
expect(await persistence.getRemoteId("msg-1")).toBe("msg-1");
});
it("preserves a pending append mapping when load returns the same ID", async () => {
const messages = createCloudMessages(1);
let resolveLoad!: (value: { messages: typeof messages }) => void;
let resolveAppend!: (value: { message_id: string }) => void;
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveLoad = resolve;
}),
);
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveAppend = resolve;
}),
);
const load = persistence.load("thread-1");
const append = persistence.append(
"thread-1",
"message-1",
null,
"aui/v0",
{},
);
resolveLoad({ messages });
await load;
resolveAppend({ message_id: "remote-2" });
await append;
expect(await persistence.getRemoteId("message-1")).toBe("remote-2");
await persistence.update("thread-1", "message-1", "aui/v0", {
text: "updated",
});
expect(cloud.threads.messages.update).toHaveBeenCalledWith(
"thread-1",
"remote-2",
{ content: { text: "updated" } },
);
});
it("preserves a resolved append mapping when load returns the same ID", async () => {
const messages = createCloudMessages(1);
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: "remote-1",
});
vi.mocked(cloud.threads.messages.list).mockResolvedValue({
messages,
});
await persistence.append("thread-1", "message-1", null, "aui/v0", {});
await persistence.load("thread-1");
expect(await persistence.getRemoteId("message-1")).toBe("remote-1");
});
it("keeps the loaded ID when a pending append for it fails", async () => {
const messages = createCloudMessages(1);
const failure = new Error("create failed");
let resolveLoad!: (value: { messages: typeof messages }) => void;
let rejectAppend!: (error: Error) => void;
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveLoad = resolve;
}),
);
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectAppend = reject;
}),
);
const load = persistence.load("thread-1");
const append = persistence.append(
"thread-1",
"message-1",
null,
"aui/v0",
{},
);
resolveLoad({ messages });
await load;
rejectAppend(failure);
await expect(append).rejects.toBe(failure);
expect(persistence.isPersisted("message-1")).toBe(true);
expect(await persistence.getRemoteId("message-1")).toBe("message-1");
});
it("does not restore a loaded ID after reset when its pending append fails", async () => {
const messages = createCloudMessages(1);
const failure = new Error("create failed");
let rejectAppend!: (error: Error) => void;
vi.mocked(cloud.threads.messages.list).mockResolvedValue({ messages });
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectAppend = reject;
}),
);
const append = persistence.append(
"thread-1",
"message-1",
null,
"aui/v0",
{},
);
await persistence.load("thread-1");
persistence.reset();
rejectAppend(failure);
await expect(append).rejects.toBe(failure);
expect(persistence.isPersisted("message-1")).toBe(false);
});
it("does not restore IDs from a load that finishes after reset", async () => {
const oldMessages = createCloudMessages(1);
const newMessages = [{ ...oldMessages[0]!, id: "new-message" }];
let resolveOld!: (value: { messages: typeof oldMessages }) => void;
vi.mocked(cloud.threads.messages.list)
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveOld = resolve;
}),
)
.mockResolvedValueOnce({ messages: newMessages });
const oldLoad = persistence.load("old-thread");
persistence.reset();
await persistence.load("new-thread");
resolveOld({ messages: oldMessages });
expect(await oldLoad).toEqual(oldMessages);
expect(persistence.isPersisted("message-1")).toBe(false);
expect(persistence.isPersisted("new-message")).toBe(true);
});
it("keeps a new append mapping when an old load returns the same ID", async () => {
const messages = createCloudMessages(1);
let resolveOld!: (value: { messages: typeof messages }) => void;
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveOld = resolve;
}),
);
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: "new-remote-id",
});
const oldLoad = persistence.load("old-thread");
persistence.reset();
await persistence.append("new-thread", "message-1", null, "aui/v0", {});
resolveOld({ messages });
await oldLoad;
expect(await persistence.getRemoteId("message-1")).toBe("new-remote-id");
});
it("does not populate any old IDs when reset happens between history pages", async () => {
const messages = createCloudMessages(201);
let resolveLast!: (value: { messages: typeof messages }) => void;
vi.mocked(cloud.threads.messages.list)
.mockResolvedValueOnce({ messages: messages.slice(0, 200) })
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveLast = resolve;
}),
);
const load = persistence.load("old-thread");
await vi.waitFor(() =>
expect(cloud.threads.messages.list).toHaveBeenCalledTimes(2),
);
persistence.reset();
resolveLast({ messages: messages.slice(200) });
expect(await load).toEqual(messages);
expect(
messages.some((message) => persistence.isPersisted(message.id)),
).toBe(false);
});
it("propagates an old load failure without disturbing post-reset state", async () => {
let rejectOld!: (error: Error) => void;
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectOld = reject;
}),
);
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: "new-remote-id",
});
const failure = new Error("history failed");
const oldLoad = persistence.load("old-thread");
const rejected = expect(oldLoad).rejects.toBe(failure);
persistence.reset();
await persistence.append("new-thread", "new-message", null, "aui/v0", {});
rejectOld(failure);
await rejected;
expect(await persistence.getRemoteId("new-message")).toBe("new-remote-id");
});
it("maps prototype-named loaded messages", async () => {
vi.mocked(cloud.threads.messages.list).mockResolvedValue({
messages: [
{
id: "__proto__",
parent_id: null,
height: 0,
created_at: "2025-01-01T00:00:00Z" as unknown as Date,
updated_at: "2025-01-01T00:00:00Z" as unknown as Date,
format: "aui/v0",
content: { text: "loaded" },
},
],
});
await persistence.load("thread-1");
expect(persistence.isPersisted("__proto__")).toBe(true);
expect(await persistence.getRemoteId("__proto__")).toBe("__proto__");
});
it("follows the message cursor across pages in server order", async () => {
const rows = createCloudMessages(450);
vi.mocked(cloud.threads.messages.list).mockImplementation(
async (_threadId, query) => {
const start = query?.after
? rows.findIndex((row) => row.id === query.after) + 1
: 0;
return { messages: rows.slice(start, start + 200) };
},
);
const messages = await persistence.load("thread-1", "aui/v0");
expect(messages.map((message) => message.id)).toEqual(
rows.map((row) => row.id),
);
expect(cloud.threads.messages.list).toHaveBeenCalledTimes(3);
expect(cloud.threads.messages.list).toHaveBeenNthCalledWith(1, "thread-1", {
format: "aui/v0",
limit: 200,
});
expect(cloud.threads.messages.list).toHaveBeenNthCalledWith(3, "thread-1", {
format: "aui/v0",
limit: 200,
after: "message-400",
});
});
it("stops without duplicates when a later cursor stops resolving", async () => {
const rows = createCloudMessages(450);
vi.mocked(cloud.threads.messages.list).mockImplementation(
async (_threadId, query) => {
// The third request replays page one, the way the endpoint answers a
// cursor whose message no longer resolves.
const start =
query?.after && query.after !== "message-400"
? rows.findIndex((row) => row.id === query.after) + 1
: 0;
return { messages: rows.slice(start, start + 200) };
},
);
const messages = await persistence.load("thread-1", "aui/v0");
expect(messages.map((message) => message.id)).toEqual(
rows.slice(0, 400).map((row) => row.id),
);
expect(cloud.threads.messages.list).toHaveBeenCalledTimes(3);
});
it("stops when a page does not advance the cursor", async () => {
vi.mocked(cloud.threads.messages.list).mockResolvedValue({
messages: createCloudMessages(200),
});
const messages = await persistence.load("thread-1", "aui/v0");
expect(messages).toHaveLength(200);
expect(cloud.threads.messages.list).toHaveBeenCalledTimes(2);
});
it("updates an already-persisted message", async () => {
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: "remote-1",
});
vi.mocked(cloud.threads.messages.update).mockResolvedValue(undefined);
await persistence.append("thread-1", "local-1", null, "aui/v0", {
text: "original",
});
await persistence.update("thread-1", "local-1", "aui/v0", {
text: "updated",
});
expect(cloud.threads.messages.update).toHaveBeenCalledWith(
"thread-1",
"remote-1",
{ content: { text: "updated" } },
);
});
it("warns and skips update when no remote id is mapped", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
await persistence.update("thread-1", "unmapped-1", "aui/v0", {
text: "x",
});
expect(cloud.threads.messages.update).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledWith(
"Skipping update for message unmapped-1: no remote id is mapped.",
);
});
it("cleans up ID mapping on append failure", async () => {
vi.mocked(cloud.threads.messages.create).mockRejectedValue(
new Error("network error"),
);
await expect(
persistence.append("thread-1", "local-1", null, "aui/v0", {
text: "fail",
}),
).rejects.toThrow("network error");
expect(persistence.isPersisted("local-1")).toBe(false);
});
it("does not restore an append mapping after reset", async () => {
let resolveAppend!: (value: { message_id: string }) => void;
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveAppend = resolve;
}),
);
const append = persistence.append(
"thread-1",
"local-1",
null,
"aui/v0",
{},
);
persistence.reset();
resolveAppend({ message_id: "remote-1" });
await expect(append).resolves.toBeUndefined();
expect(persistence.isPersisted("local-1")).toBe(false);
expect(await persistence.getRemoteId("local-1")).toBeUndefined();
});
it("reset clears all ID mappings", async () => {
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
message_id: "remote-1",
});
await persistence.append("thread-1", "local-1", null, "aui/v0", {
text: "hello",
});
expect(persistence.isPersisted("local-1")).toBe(true);
persistence.reset();
expect(persistence.isPersisted("local-1")).toBe(false);
});
});