assistant-cloud
Version:
Cloud integration for assistant-ui
230 lines (198 loc) • 7.83 kB
text/typescript
import { describe, expect, it, vi } from "vitest";
import type { AssistantCloud } from "../AssistantCloud";
import {
CloudEngagementReporter,
type EngagementIdResolver,
} from "../CloudEngagementReporter";
const createCloud = () => {
const track = vi.fn();
const cloud = { events: { track } } as unknown as AssistantCloud;
return { cloud, track };
};
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
describe("CloudEngagementReporter", () => {
it("resolves ids through the resolver and awaits the thread only for a send", async () => {
const { cloud, track } = createCloud();
const resolveIds = vi.fn<EngagementIdResolver>(
async (threadId, messageId) => ({
thread_id: `remote-${threadId}`,
...(messageId ? { message_id: `remote-${messageId}` } : undefined),
}),
);
const reporter = new CloudEngagementReporter(cloud, resolveIds);
reporter.messageSent("t1", { messageId: "m1", chars: 12, attachments: 1 });
reporter.messageCopied("t1", "m2");
await flush();
expect(resolveIds).toHaveBeenNthCalledWith(1, "t1", "m1", {
awaitThread: true,
});
expect(resolveIds).toHaveBeenNthCalledWith(2, "t1", "m2", {
awaitThread: false,
});
expect(track).toHaveBeenCalledWith({
kind: "message_sent",
thread_id: "remote-t1",
message_id: "remote-m1",
props: { chars: 12, attachments: 1 },
});
expect(track).toHaveBeenCalledWith({
kind: "message_copied",
thread_id: "remote-t1",
message_id: "remote-m2",
});
});
it("drops an event its resolver declines", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud, (threadId) =>
threadId === "known" ? { thread_id: "remote-known" } : undefined,
);
reporter.messageSent("unknown", { chars: 3, attachments: 0 });
reporter.messageSent("known", { chars: 5, attachments: 0 });
await flush();
expect(track).toHaveBeenCalledOnce();
expect(track).toHaveBeenCalledWith({
kind: "message_sent",
thread_id: "remote-known",
props: { chars: 5, attachments: 0 },
});
});
it("reports tool approval decisions with their resolved message IDs", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(
cloud,
(threadId, messageId) => ({
thread_id: `remote-${threadId}`,
...(messageId !== undefined
? { message_id: `remote-${messageId}` }
: {}),
}),
);
reporter.toolApproved("t1", "m1", "tool-1", "send_email");
reporter.toolRejected("t1", "m2", "tool-2", "delete_account");
await flush();
expect(track).toHaveBeenNthCalledWith(1, {
kind: "tool_approved",
thread_id: "remote-t1",
message_id: "remote-m1",
props: { toolCallId: "tool-1", toolName: "send_email" },
});
expect(track).toHaveBeenNthCalledWith(2, {
kind: "tool_rejected",
thread_id: "remote-t1",
message_id: "remote-m2",
props: { toolCallId: "tool-2", toolName: "delete_account" },
});
});
it("measures a stop against the run start and reports it once per run", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud);
vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
reporter.runStopped("t1");
reporter.runStarted("t1");
vi.setSystemTime(new Date("2023-01-01T00:00:01.500Z"));
reporter.runStopped("t1");
reporter.runStopped("t1");
await flush();
expect(track).toHaveBeenCalledOnce();
expect(track).toHaveBeenCalledWith({
kind: "run_stopped",
thread_id: "t1",
value: 1500,
});
});
it("carries the time since the previous run ended on the next send", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud);
vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
reporter.runStarted("t1");
reporter.runEnded("t1");
vi.setSystemTime(new Date("2023-01-01T00:00:04.000Z"));
reporter.messageSent("t1", { chars: 3, attachments: 0 });
await flush();
expect(track).toHaveBeenCalledWith(
expect.objectContaining({ kind: "message_sent", value: 4000 }),
);
});
it("measures the next send from a stopped run, not the last completed one", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud);
vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
reporter.runStarted("t1");
vi.setSystemTime(new Date("2023-01-01T00:01:40.000Z"));
reporter.runEnded("t1");
// a second run the user stops rather than lets finish
vi.setSystemTime(new Date("2023-01-01T00:03:20.000Z"));
reporter.runStarted("t1");
vi.setSystemTime(new Date("2023-01-01T00:05:00.000Z"));
reporter.runStopped("t1");
vi.setSystemTime(new Date("2023-01-01T00:05:10.000Z"));
reporter.messageSent("t1", { chars: 3, attachments: 0 });
await flush();
// a stopped run is still a run that ended, so the idle gap is the 10s
// since the stop, not the 210s since the last completed run
const sent = track.mock.calls
.map(([event]) => event as { kind: string; value?: number })
.find((event) => event.kind === "message_sent");
expect(sent?.value).toBe(10_000);
});
it("shows one error per run and one suggestion list per thread", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud);
reporter.runStarted("t1");
reporter.errorShown("t1", { messageId: "m1", reason: "error" });
reporter.errorShown("t1", { messageId: "m1", reason: "error" });
reporter.runStarted("t1");
reporter.errorShown("t1", { reason: "timeout" });
reporter.suggestionsShown("t1", 3);
reporter.suggestionsShown("t1", 3);
await flush();
const kinds = track.mock.calls.map(([event]) => event.kind);
expect(kinds).toEqual(["error_shown", "error_shown", "suggestions_shown"]);
expect(track).toHaveBeenCalledWith({
kind: "suggestions_shown",
thread_id: "t1",
value: 3,
});
});
it("drops a thread switch the cloud cannot attribute and survives a failing resolver", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud, (threadId) =>
threadId === "known" ? { thread_id: "remote" } : {},
);
reporter.threadSwitched("unknown");
reporter.threadSwitched("known");
const failing = new CloudEngagementReporter(cloud, () => {
throw new Error("no ids");
});
failing.messageCopied("t1");
await flush();
expect(track).toHaveBeenCalledOnce();
expect(track).toHaveBeenCalledWith({
kind: "thread_switched",
thread_id: "remote",
});
});
it("keeps the state of the most recently touched threads only", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud);
for (let index = 0; index < 300; index++) {
reporter.suggestionsShown(`t${index}`, 1);
}
reporter.suggestionsShown("t0", 1);
reporter.suggestionsShown("t299", 1);
await flush();
expect(track).toHaveBeenCalledTimes(301);
});
it("keeps a started run until it stops, however many threads ran since", async () => {
const { cloud, track } = createCloud();
const reporter = new CloudEngagementReporter(cloud);
for (let index = 0; index < 300; index++) {
reporter.runStarted(`t${index}`);
}
reporter.runStopped("t0");
await flush();
expect(track).toHaveBeenCalledWith(
expect.objectContaining({ kind: "run_stopped", thread_id: "t0" }),
);
});
});