assistant-cloud
Version:
Cloud integration for assistant-ui
776 lines (700 loc) • 24.7 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CloudMessagePersistence } from "./CloudMessagePersistence";
import type { AssistantCloud } from "./AssistantCloud";
import { CloudAPIError } from "./AssistantCloudAPI";
import {
createFormattedPersistence,
type MessageFormatAdapter,
} from "./FormattedCloudPersistence";
const run = {
lease_id: "lease_1",
heartbeat_interval_ms: 20_000,
};
const createCloud = () => {
const messages = {
create: vi.fn().mockResolvedValue({ message_id: "msg_1", run }),
update: vi.fn().mockResolvedValue(undefined),
list: vi.fn(),
delete: vi.fn(),
};
const runs = {
heartbeat: vi.fn().mockResolvedValue({
heartbeat_interval_ms: 20_000,
}),
end: vi.fn().mockResolvedValue(undefined),
open: vi.fn(),
};
const cloud = { threads: { messages, runs } } as unknown as AssistantCloud;
return { cloud, messages, runs };
};
const running = { role: "assistant", status: { type: "running" } };
const done = { role: "assistant", content: [{ type: "text", text: "done" }] };
describe("CloudMessagePersistence run leases", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("opens a run, heartbeats on the server's interval and ends it after the final write", async () => {
const { cloud, messages, runs } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
expect(messages.create).toHaveBeenCalledWith("t1", {
parent_id: null,
external_id: "local-a",
format: "aui/v0",
content: running,
open_run: true,
});
expect(persistence.hasOpenRun("local-a")).toBe(true);
await vi.advanceTimersByTimeAsync(19_999);
expect(runs.heartbeat).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
expect(runs.heartbeat).toHaveBeenCalledWith("t1", "lease_1");
await vi.advanceTimersByTimeAsync(20_000);
expect(runs.heartbeat).toHaveBeenCalledTimes(2);
await persistence.update("t1", "local-a", "aui/v0", done, "done");
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
lease_id: "lease_1",
});
expect(runs.end).toHaveBeenCalledWith("t1", "lease_1", { reason: "done" });
expect(persistence.hasOpenRun("local-a")).toBe(false);
await vi.advanceTimersByTimeAsync(60_000);
expect(runs.heartbeat).toHaveBeenCalledTimes(2);
});
it("routes the runtime's final append of a leased message to a lease write and an end", async () => {
const { cloud, messages, runs } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await persistence.append("t1", "local-a", null, "aui/v0", done);
expect(messages.create).toHaveBeenCalledTimes(1);
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
lease_id: "lease_1",
});
expect(runs.end).toHaveBeenCalledWith("t1", "lease_1", { reason: "done" });
});
it("waits for an in-flight running append before writing the final answer", async () => {
const { cloud, messages } = createCloud();
let resolveCreate!: (value: unknown) => void;
messages.create.mockReturnValueOnce(
new Promise((resolve) => {
resolveCreate = resolve;
}),
);
const persistence = new CloudMessagePersistence(cloud);
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const final = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
done,
"cancelled",
);
resolveCreate({ message_id: "msg_1", run });
await Promise.all([placeholder, final]);
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
lease_id: "lease_1",
});
});
it("creates the row with the final answer when the in-flight running append fails", async () => {
const { cloud, messages } = createCloud();
let rejectCreate!: (error: unknown) => void;
messages.create
.mockReturnValueOnce(
new Promise((_, reject) => {
rejectCreate = reject;
}),
)
.mockResolvedValueOnce({ message_id: "msg_2" });
const persistence = new CloudMessagePersistence(cloud);
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const final = persistence.append("t1", "local-a", null, "aui/v0", done);
rejectCreate(new Error("network"));
await expect(placeholder).rejects.toThrow("network");
await final;
expect(messages.create).toHaveBeenLastCalledWith("t1", {
parent_id: null,
external_id: "local-a",
format: "aui/v0",
content: done,
});
expect(await persistence.getRemoteId("local-a")).toBe("msg_2");
});
it("drops a final write that waited on an in-flight append across a reset", async () => {
const { cloud, messages } = createCloud();
let rejectCreate!: (error: unknown) => void;
messages.create.mockReturnValueOnce(
new Promise((_, reject) => {
rejectCreate = reject;
}),
);
const persistence = new CloudMessagePersistence(cloud);
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const final = persistence.append("t1", "local-a", null, "aui/v0", done);
persistence.reset();
rejectCreate(new Error("network"));
await expect(placeholder).rejects.toThrow("network");
await final;
expect(messages.create).toHaveBeenCalledTimes(1);
expect(messages.update).not.toHaveBeenCalled();
});
it("drops a final write that waited on an in-flight append the message was deleted during", async () => {
const { cloud, messages } = createCloud();
let rejectCreate!: (error: unknown) => void;
messages.create.mockReturnValueOnce(
new Promise((_, reject) => {
rejectCreate = reject;
}),
);
const persistence = new CloudMessagePersistence(cloud);
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const final = persistence.append("t1", "local-a", null, "aui/v0", done);
const deleted = persistence.delete("t1", ["local-a"]);
rejectCreate(new Error("network"));
await expect(placeholder).rejects.toThrow("network");
await Promise.all([final, deleted]);
expect(messages.create).toHaveBeenCalledTimes(1);
expect(persistence.isPersisted("local-a")).toBe(false);
});
it("keeps a final write that waited across a delete that failed", async () => {
const { cloud, messages } = createCloud();
let resolveCreate!: (value: unknown) => void;
messages.create.mockReturnValueOnce(
new Promise((resolve) => {
resolveCreate = resolve;
}),
);
messages.delete.mockRejectedValueOnce(new Error("delete failed"));
const persistence = new CloudMessagePersistence(cloud);
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const final = persistence.append("t1", "local-a", null, "aui/v0", done);
const deleted = persistence.delete("t1", ["local-a"]);
resolveCreate({ message_id: "msg_1" });
await expect(deleted).rejects.toThrow("delete failed");
await Promise.all([placeholder, final]);
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
});
});
it("applies writes waiting on one lease-less append in append order", async () => {
const { cloud, messages } = createCloud();
let resolveCreate!: (value: unknown) => void;
messages.create.mockReturnValueOnce(
new Promise((resolve) => {
resolveCreate = resolve;
}),
);
const order: string[] = [];
let releaseFirst!: () => void;
messages.update
.mockImplementationOnce(async (_t, _m, body) => {
await new Promise<void>((resolve) => {
releaseFirst = resolve;
});
order.push(body.content.content[0].text);
})
.mockImplementationOnce(async (_t, _m, body) => {
order.push(body.content.content[0].text);
});
const persistence = new CloudMessagePersistence(cloud);
const checkpoint = {
role: "assistant",
content: [{ type: "text", text: "partial" }],
};
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const first = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
checkpoint,
);
const second = persistence.append("t1", "local-a", null, "aui/v0", done);
resolveCreate({ message_id: "msg_1" });
await vi.advanceTimersByTimeAsync(0);
releaseFirst();
await Promise.all([placeholder, first, second]);
expect(order).toEqual(["partial", "done"]);
});
it("writes the final answer over an in-flight running append that got no lease", async () => {
const { cloud, messages } = createCloud();
let resolveCreate!: (value: unknown) => void;
messages.create.mockReturnValueOnce(
new Promise((resolve) => {
resolveCreate = resolve;
}),
);
const persistence = new CloudMessagePersistence(cloud);
const placeholder = persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const final = persistence.append("t1", "local-a", null, "aui/v0", done);
resolveCreate({ message_id: "msg_1" });
await Promise.all([placeholder, final]);
expect(messages.create).toHaveBeenCalledTimes(1);
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
});
});
it("checkpoints renew the lease and serialize behind earlier writes", async () => {
const { cloud, messages, runs } = createCloud();
const order: string[] = [];
messages.update.mockImplementation(async (_t, _m, body) => {
order.push(`write:${body.content.text ?? "final"}`);
await new Promise((resolve) => setTimeout(resolve, 10));
});
runs.end.mockImplementation(async () => {
order.push("end");
});
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
const checkpoint = persistence.update(
"t1",
"local-a",
"aui/v0",
{ text: "partial" },
"running",
);
const final = persistence.update("t1", "local-a", "aui/v0", done, "error");
await vi.advanceTimersByTimeAsync(20);
await Promise.all([checkpoint, final]);
expect(order).toEqual(["write:partial", "write:final", "end"]);
expect(runs.end).toHaveBeenCalledWith("t1", "lease_1", {
reason: "error",
});
});
it.each([
["run_ended", new CloudAPIError("ended", 409, "run_ended")],
["lease_mismatch", new CloudAPIError("mismatch", 409, "lease_mismatch")],
])(
"closes the run and drops later writes when a write answers %s",
async (_name, error) => {
const { cloud, messages, runs } = createCloud();
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
messages.update.mockRejectedValueOnce(error);
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await expect(
persistence.update("t1", "local-a", "aui/v0", running, "running"),
).resolves.toBeUndefined();
expect(persistence.hasOpenRun("local-a")).toBe(false);
expect(persistence.getResolvedRemoteId("local-a")).toBe("msg_1");
await persistence.update("t1", "local-a", "aui/v0", done, "done");
await persistence.update("t1", "local-a", "aui/v0", done);
expect(messages.update).toHaveBeenCalledTimes(1);
expect(runs.end).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledTimes(1);
warn.mockRestore();
},
);
it("keeps a plain final write after the run's own end closed the lease", async () => {
const { cloud, messages } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await persistence.update("t1", "local-a", "aui/v0", done, "cancelled");
await persistence.update("t1", "local-a", "aui/v0", done);
expect(messages.update).toHaveBeenLastCalledWith("t1", "msg_1", {
content: done,
});
// A runtime that appends its settled message after the lease closed rewrites the row instead of storing a second one.
await persistence.append("t1", "local-a", null, "aui/v0", done);
expect(messages.create).toHaveBeenCalledTimes(1);
expect(messages.update).toHaveBeenCalledTimes(3);
expect(messages.update).toHaveBeenLastCalledWith("t1", "msg_1", {
content: done,
});
});
it("schedules heartbeats from the server's interval, not a client constant", async () => {
const { cloud, messages, runs } = createCloud();
messages.create.mockResolvedValueOnce({
message_id: "msg_1",
run: {
lease_id: "lease_1",
heartbeat_interval_ms: 20_000,
},
});
runs.heartbeat.mockResolvedValueOnce({
heartbeat_interval_ms: 45_000,
});
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await vi.advanceTimersByTimeAsync(19_999);
expect(runs.heartbeat).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
expect(runs.heartbeat).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(44_999);
expect(runs.heartbeat).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(runs.heartbeat).toHaveBeenCalledTimes(2);
expect(persistence.hasOpenRun("local-a")).toBe(true);
});
it("tolerates a refused end but rethrows a server failure", async () => {
const { cloud, runs } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
runs.end.mockRejectedValueOnce(
new CloudAPIError("Run not found", 404, "not_found"),
);
await persistence.append("t1", "a", null, "aui/v0", running, "running");
await expect(
persistence.update("t1", "a", "aui/v0", done, "done"),
).resolves.toBeUndefined();
runs.end.mockRejectedValueOnce(new CloudAPIError("down", 503));
await persistence.append("t1", "b", null, "aui/v0", running, "running");
await expect(
persistence.update("t1", "b", "aui/v0", done, "done"),
).rejects.toThrow("down");
});
it("stops heartbeating when the server no longer knows the run", async () => {
const { cloud, runs } = createCloud();
vi.spyOn(console, "warn").mockImplementation(() => {});
runs.heartbeat.mockRejectedValueOnce(
new CloudAPIError("ended", 409, "run_ended"),
);
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await vi.advanceTimersByTimeAsync(20_000);
expect(persistence.hasOpenRun("local-a")).toBe(false);
const gone = new CloudMessagePersistence(cloud);
runs.heartbeat.mockRejectedValueOnce(
new CloudAPIError("Run not found", 404, "not_found"),
);
await gone.append("t1", "local-b", null, "aui/v0", running, "running");
await vi.advanceTimersByTimeAsync(20_000);
expect(gone.hasOpenRun("local-b")).toBe(false);
vi.mocked(console.warn).mockRestore();
});
it("keeps writing a run that comes back late after a long quiet stretch", async () => {
const { cloud, messages, runs } = createCloud();
runs.heartbeat.mockRejectedValueOnce(new TypeError("offline"));
const persistence = new CloudMessagePersistence(cloud);
await persistence.append("t1", "a", null, "aui/v0", running, "running");
await vi.advanceTimersByTimeAsync(120_000);
expect(persistence.hasOpenRun("a")).toBe(true);
await persistence.update("t1", "a", "aui/v0", done, "done");
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
lease_id: "lease_1",
});
expect(runs.end).toHaveBeenCalledWith("t1", "lease_1", { reason: "done" });
});
it("reads a running lease as stale once it went staleAfter without a beat", () => {
const { cloud } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
expect(
persistence.runStateOf({
status: "running",
last_heartbeat_ms_ago: 5_000,
}),
).toEqual({ status: "in-progress", staleInMs: 25_000 });
expect(
persistence.runStateOf(
{ status: "running", last_heartbeat_ms_ago: 5_000 },
25_000,
),
).toEqual({ status: "stale" });
expect(
persistence.runStateOf({
status: "ended",
last_heartbeat_ms_ago: 90_000,
}),
).toBeUndefined();
expect(persistence.runStateOf(undefined)).toBeUndefined();
const patient = new CloudMessagePersistence(cloud, { staleAfter: 120_000 });
expect(
patient.runStateOf({ status: "running", last_heartbeat_ms_ago: 60_000 }),
).toEqual({ status: "in-progress", staleInMs: 60_000 });
});
it("keeps heartbeating through a network error", async () => {
const { cloud, runs } = createCloud();
runs.heartbeat.mockRejectedValueOnce(new TypeError("offline"));
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await vi.advanceTimersByTimeAsync(40_000);
expect(runs.heartbeat).toHaveBeenCalledTimes(2);
expect(persistence.hasOpenRun("local-a")).toBe(true);
});
it("stores the message without a lease on a server that predates run leases", async () => {
const { cloud, messages } = createCloud();
messages.create
.mockRejectedValueOnce(new CloudAPIError("bad request", 400))
.mockResolvedValueOnce({ message_id: "msg_1" });
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
expect(messages.create).toHaveBeenLastCalledWith("t1", {
parent_id: null,
external_id: "local-a",
format: "aui/v0",
content: running,
});
expect(persistence.hasOpenRun("local-a")).toBe(false);
expect(persistence.getResolvedRemoteId("local-a")).toBe("msg_1");
});
it("rethrows other create failures of a running append", async () => {
const { cloud, messages } = createCloud();
messages.create.mockRejectedValueOnce(
new CloudAPIError("busy", 409, "run_in_progress"),
);
const persistence = new CloudMessagePersistence(cloud);
await expect(
persistence.append("t1", "local-a", null, "aui/v0", running, "running"),
).rejects.toThrow("busy");
});
it("rethrows a lease write failure that is not a refusal", async () => {
const { cloud, messages } = createCloud();
messages.update.mockRejectedValueOnce(new CloudAPIError("down", 503));
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
await expect(
persistence.update("t1", "local-a", "aui/v0", done, "done"),
).rejects.toThrow("down");
expect(persistence.hasOpenRun("local-a")).toBe(true);
});
it("stops every heartbeat on reset", async () => {
const { cloud, runs } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"local-a",
null,
"aui/v0",
running,
"running",
);
persistence.reset();
await vi.advanceTimersByTimeAsync(60_000);
expect(runs.heartbeat).not.toHaveBeenCalled();
});
it("moves the lease and mapping when the runtime renames the running message", async () => {
const { cloud, messages, runs } = createCloud();
const persistence = new CloudMessagePersistence(cloud);
await persistence.append(
"t1",
"placeholder",
null,
"aui/v0",
running,
"running",
);
await persistence.rename("placeholder", "backend-id");
expect(persistence.hasOpenRun("placeholder")).toBe(false);
expect(persistence.hasOpenRun("backend-id")).toBe(true);
expect(persistence.getResolvedRemoteId("backend-id")).toBe("msg_1");
await vi.advanceTimersByTimeAsync(20_000);
expect(runs.heartbeat).toHaveBeenCalledTimes(1);
await persistence.append("t1", "backend-id", null, "aui/v0", done);
expect(messages.create).toHaveBeenCalledTimes(1);
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: done,
lease_id: "lease_1",
});
await persistence.rename("missing", "other");
expect(persistence.getResolvedRemoteId("other")).toBeUndefined();
await persistence.append("t1", "plain", null, "aui/v0", done);
await persistence.rename("plain", "renamed");
expect(persistence.getResolvedRemoteId("renamed")).toBe("msg_1");
expect(persistence.hasOpenRun("renamed")).toBe(false);
});
describe("through a format adapter", () => {
type UIMessage = { id: string; parts: { text: string }[] };
const adapter: MessageFormatAdapter<UIMessage, Record<string, unknown>> = {
format: "ai-sdk/v6",
encode: ({ message }) => ({ parts: message.parts }),
decode: (stored) => ({
parentId: stored.parent_id,
message: {
id: stored.id,
parts: stored.content.parts as UIMessage["parts"],
},
}),
getId: (message) => message.id,
};
const partial = { id: "ui-1", parts: [{ text: "hel" }] };
const final = { id: "ui-1", parts: [{ text: "hello" }] };
it("opens a run on a running append, writes as the lease holder and ends it on the final write", async () => {
const { cloud, messages, runs } = createCloud();
const formatted = createFormattedPersistence(
new CloudMessagePersistence(cloud),
adapter,
);
await formatted.append(
"t1",
{ parentId: null, message: partial },
"running",
);
expect(messages.create).toHaveBeenCalledWith("t1", {
parent_id: null,
external_id: "ui-1",
format: "ai-sdk/v6",
content: { parts: partial.parts },
open_run: true,
});
await formatted.update!(
"t1",
{ parentId: null, message: partial },
"ui-1",
"running",
);
expect(messages.update).toHaveBeenLastCalledWith("t1", "msg_1", {
content: { parts: partial.parts },
lease_id: "lease_1",
});
expect(runs.end).not.toHaveBeenCalled();
await formatted.update!(
"t1",
{ parentId: null, message: final },
"ui-1",
"done",
);
expect(messages.update).toHaveBeenLastCalledWith("t1", "msg_1", {
content: { parts: final.parts },
lease_id: "lease_1",
});
expect(runs.end).toHaveBeenCalledWith("t1", "lease_1", {
reason: "done",
});
});
it("ends the lease on a settled append without a phase", async () => {
const { cloud, messages, runs } = createCloud();
const formatted = createFormattedPersistence(
new CloudMessagePersistence(cloud),
adapter,
);
await formatted.append(
"t1",
{ parentId: null, message: partial },
"running",
);
await formatted.append("t1", { parentId: null, message: final });
expect(messages.create).toHaveBeenCalledTimes(1);
expect(messages.update).toHaveBeenCalledWith("t1", "msg_1", {
content: { parts: final.parts },
lease_id: "lease_1",
});
expect(runs.end).toHaveBeenCalledWith("t1", "lease_1", {
reason: "done",
});
});
});
});