assistant-cloud 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/AssistantCloud.d.ts +2 -1
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +2 -0
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +11 -2
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +17 -1
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +4 -2
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudEvents.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.js +4 -0
- package/dist/AssistantCloudEvents.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +1 -0
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js +2 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/CloudEngagementReporter.d.ts +74 -0
- package/dist/CloudEngagementReporter.d.ts.map +1 -0
- package/dist/CloudEngagementReporter.js +132 -0
- package/dist/CloudEngagementReporter.js.map +1 -0
- package/dist/CloudMessagePersistence.d.ts +8 -9
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +18 -11
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/CloudRunReporter.d.ts +20 -0
- package/dist/CloudRunReporter.d.ts.map +1 -0
- package/dist/CloudRunReporter.js +38 -0
- package/dist/CloudRunReporter.js.map +1 -0
- package/dist/FormattedCloudPersistence.d.ts +3 -8
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/FormattedCloudPersistence.js +3 -8
- package/dist/FormattedCloudPersistence.js.map +1 -1
- package/dist/ai-sdk/index.d.ts +33 -0
- package/dist/ai-sdk/index.d.ts.map +1 -0
- package/dist/ai-sdk/index.js +147 -0
- package/dist/ai-sdk/index.js.map +1 -0
- package/dist/generateThreadTitle.js +1 -0
- package/dist/generateThreadTitle.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/instrumentMcpSampling.js +12 -2
- package/dist/instrumentMcpSampling.js.map +1 -1
- package/dist/runTelemetry.d.ts +17 -1
- package/dist/runTelemetry.d.ts.map +1 -1
- package/dist/runTelemetry.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +19 -6
- package/src/AssistantCloud.ts +3 -0
- package/src/AssistantCloudAPI.ts +31 -1
- package/src/AssistantCloudAuthStrategy.ts +4 -2
- package/src/AssistantCloudEvents.test.ts +27 -2
- package/src/AssistantCloudEvents.ts +5 -0
- package/src/AssistantCloudRuns.ts +1 -0
- package/src/CloudEngagementReporter.ts +231 -0
- package/src/CloudMessagePersistence.ts +24 -13
- package/src/CloudRunReporter.ts +38 -0
- package/src/FormattedCloudPersistence.ts +3 -8
- package/src/ai-sdk/index.test.ts +258 -0
- package/src/ai-sdk/index.ts +222 -0
- package/src/generateThreadTitle.test.ts +32 -0
- package/src/generateThreadTitle.ts +1 -0
- package/src/index.ts +11 -1
- package/src/instrumentMcpSampling.test.ts +102 -0
- package/src/instrumentMcpSampling.ts +16 -2
- package/src/runTelemetry.ts +17 -0
- package/src/tests/AssistantCloud.test.ts +33 -1
- package/src/tests/AssistantCloudAPI.test.ts +51 -0
- package/src/tests/AssistantCloudAuthStrategy.test.ts +22 -0
- package/src/tests/AssistantCloudProjects.test.ts +1 -5
- package/src/tests/CloudEngagementReporter.test.ts +184 -0
- package/src/tests/CloudMessagePersistence.test.ts +235 -0
- package/src/tests/CloudRunReporter.test.ts +100 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantCloud } from "../AssistantCloud";
|
|
3
|
+
import {
|
|
4
|
+
CloudEngagementReporter,
|
|
5
|
+
type EngagementIdResolver,
|
|
6
|
+
} from "../CloudEngagementReporter";
|
|
7
|
+
|
|
8
|
+
const createCloud = () => {
|
|
9
|
+
const track = vi.fn();
|
|
10
|
+
const cloud = { events: { track } } as unknown as AssistantCloud;
|
|
11
|
+
return { cloud, track };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
15
|
+
|
|
16
|
+
describe("CloudEngagementReporter", () => {
|
|
17
|
+
it("resolves ids through the resolver and awaits the thread only for a send", async () => {
|
|
18
|
+
const { cloud, track } = createCloud();
|
|
19
|
+
const resolveIds = vi.fn<EngagementIdResolver>(
|
|
20
|
+
async (threadId, messageId) => ({
|
|
21
|
+
thread_id: `remote-${threadId}`,
|
|
22
|
+
...(messageId ? { message_id: `remote-${messageId}` } : undefined),
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
const reporter = new CloudEngagementReporter(cloud, resolveIds);
|
|
26
|
+
|
|
27
|
+
reporter.messageSent("t1", { messageId: "m1", chars: 12, attachments: 1 });
|
|
28
|
+
reporter.messageCopied("t1", "m2");
|
|
29
|
+
await flush();
|
|
30
|
+
|
|
31
|
+
expect(resolveIds).toHaveBeenNthCalledWith(1, "t1", "m1", {
|
|
32
|
+
awaitThread: true,
|
|
33
|
+
});
|
|
34
|
+
expect(resolveIds).toHaveBeenNthCalledWith(2, "t1", "m2", {
|
|
35
|
+
awaitThread: false,
|
|
36
|
+
});
|
|
37
|
+
expect(track).toHaveBeenCalledWith({
|
|
38
|
+
kind: "message_sent",
|
|
39
|
+
thread_id: "remote-t1",
|
|
40
|
+
message_id: "remote-m1",
|
|
41
|
+
props: { chars: 12, attachments: 1 },
|
|
42
|
+
});
|
|
43
|
+
expect(track).toHaveBeenCalledWith({
|
|
44
|
+
kind: "message_copied",
|
|
45
|
+
thread_id: "remote-t1",
|
|
46
|
+
message_id: "remote-m2",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reports tool approval decisions with their resolved message IDs", async () => {
|
|
51
|
+
const { cloud, track } = createCloud();
|
|
52
|
+
const reporter = new CloudEngagementReporter(
|
|
53
|
+
cloud,
|
|
54
|
+
(threadId, messageId) => ({
|
|
55
|
+
thread_id: `remote-${threadId}`,
|
|
56
|
+
...(messageId !== undefined
|
|
57
|
+
? { message_id: `remote-${messageId}` }
|
|
58
|
+
: {}),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
reporter.toolApproved("t1", "m1", "tool-1", "send_email");
|
|
63
|
+
reporter.toolRejected("t1", "m2", "tool-2", "delete_account");
|
|
64
|
+
await flush();
|
|
65
|
+
|
|
66
|
+
expect(track).toHaveBeenNthCalledWith(1, {
|
|
67
|
+
kind: "tool_approved",
|
|
68
|
+
thread_id: "remote-t1",
|
|
69
|
+
message_id: "remote-m1",
|
|
70
|
+
props: { toolCallId: "tool-1", toolName: "send_email" },
|
|
71
|
+
});
|
|
72
|
+
expect(track).toHaveBeenNthCalledWith(2, {
|
|
73
|
+
kind: "tool_rejected",
|
|
74
|
+
thread_id: "remote-t1",
|
|
75
|
+
message_id: "remote-m2",
|
|
76
|
+
props: { toolCallId: "tool-2", toolName: "delete_account" },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("measures a stop against the run start and reports it once per run", async () => {
|
|
81
|
+
const { cloud, track } = createCloud();
|
|
82
|
+
const reporter = new CloudEngagementReporter(cloud);
|
|
83
|
+
|
|
84
|
+
vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
|
|
85
|
+
reporter.runStopped("t1");
|
|
86
|
+
reporter.runStarted("t1");
|
|
87
|
+
vi.setSystemTime(new Date("2023-01-01T00:00:01.500Z"));
|
|
88
|
+
reporter.runStopped("t1");
|
|
89
|
+
reporter.runStopped("t1");
|
|
90
|
+
await flush();
|
|
91
|
+
|
|
92
|
+
expect(track).toHaveBeenCalledOnce();
|
|
93
|
+
expect(track).toHaveBeenCalledWith({
|
|
94
|
+
kind: "run_stopped",
|
|
95
|
+
thread_id: "t1",
|
|
96
|
+
value: 1500,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("carries the time since the previous run ended on the next send", async () => {
|
|
101
|
+
const { cloud, track } = createCloud();
|
|
102
|
+
const reporter = new CloudEngagementReporter(cloud);
|
|
103
|
+
|
|
104
|
+
vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
|
|
105
|
+
reporter.runStarted("t1");
|
|
106
|
+
reporter.runEnded("t1");
|
|
107
|
+
vi.setSystemTime(new Date("2023-01-01T00:00:04.000Z"));
|
|
108
|
+
reporter.messageSent("t1", { chars: 3, attachments: 0 });
|
|
109
|
+
await flush();
|
|
110
|
+
|
|
111
|
+
expect(track).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({ kind: "message_sent", value: 4000 }),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("shows one error per run and one suggestion list per thread", async () => {
|
|
117
|
+
const { cloud, track } = createCloud();
|
|
118
|
+
const reporter = new CloudEngagementReporter(cloud);
|
|
119
|
+
|
|
120
|
+
reporter.runStarted("t1");
|
|
121
|
+
reporter.errorShown("t1", { messageId: "m1", reason: "error" });
|
|
122
|
+
reporter.errorShown("t1", { messageId: "m1", reason: "error" });
|
|
123
|
+
reporter.runStarted("t1");
|
|
124
|
+
reporter.errorShown("t1", { reason: "timeout" });
|
|
125
|
+
reporter.suggestionsShown("t1", 3);
|
|
126
|
+
reporter.suggestionsShown("t1", 3);
|
|
127
|
+
await flush();
|
|
128
|
+
|
|
129
|
+
const kinds = track.mock.calls.map(([event]) => event.kind);
|
|
130
|
+
expect(kinds).toEqual(["error_shown", "error_shown", "suggestions_shown"]);
|
|
131
|
+
expect(track).toHaveBeenCalledWith({
|
|
132
|
+
kind: "suggestions_shown",
|
|
133
|
+
thread_id: "t1",
|
|
134
|
+
value: 3,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("drops a thread switch the cloud cannot attribute and survives a failing resolver", async () => {
|
|
139
|
+
const { cloud, track } = createCloud();
|
|
140
|
+
const reporter = new CloudEngagementReporter(cloud, (threadId) =>
|
|
141
|
+
threadId === "known" ? { thread_id: "remote" } : {},
|
|
142
|
+
);
|
|
143
|
+
reporter.threadSwitched("unknown");
|
|
144
|
+
reporter.threadSwitched("known");
|
|
145
|
+
const failing = new CloudEngagementReporter(cloud, () => {
|
|
146
|
+
throw new Error("no ids");
|
|
147
|
+
});
|
|
148
|
+
failing.messageCopied("t1");
|
|
149
|
+
await flush();
|
|
150
|
+
|
|
151
|
+
expect(track).toHaveBeenCalledOnce();
|
|
152
|
+
expect(track).toHaveBeenCalledWith({
|
|
153
|
+
kind: "thread_switched",
|
|
154
|
+
thread_id: "remote",
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("keeps the state of the most recently touched threads only", async () => {
|
|
159
|
+
const { cloud, track } = createCloud();
|
|
160
|
+
const reporter = new CloudEngagementReporter(cloud);
|
|
161
|
+
for (let index = 0; index < 300; index++) {
|
|
162
|
+
reporter.suggestionsShown(`t${index}`, 1);
|
|
163
|
+
}
|
|
164
|
+
reporter.suggestionsShown("t0", 1);
|
|
165
|
+
reporter.suggestionsShown("t299", 1);
|
|
166
|
+
await flush();
|
|
167
|
+
|
|
168
|
+
expect(track).toHaveBeenCalledTimes(301);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("keeps a started run until it stops, however many threads ran since", async () => {
|
|
172
|
+
const { cloud, track } = createCloud();
|
|
173
|
+
const reporter = new CloudEngagementReporter(cloud);
|
|
174
|
+
for (let index = 0; index < 300; index++) {
|
|
175
|
+
reporter.runStarted(`t${index}`);
|
|
176
|
+
}
|
|
177
|
+
reporter.runStopped("t0");
|
|
178
|
+
await flush();
|
|
179
|
+
|
|
180
|
+
expect(track).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({ kind: "run_stopped", thread_id: "t0" }),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -252,6 +252,217 @@ describe("CloudMessagePersistence", () => {
|
|
|
252
252
|
await persistence.load("thread-1");
|
|
253
253
|
|
|
254
254
|
expect(persistence.isPersisted("msg-1")).toBe(true);
|
|
255
|
+
expect(await persistence.getRemoteId("msg-1")).toBe("msg-1");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("preserves a pending append mapping when load returns the same ID", async () => {
|
|
259
|
+
const messages = createCloudMessages(1);
|
|
260
|
+
let resolveLoad!: (value: { messages: typeof messages }) => void;
|
|
261
|
+
let resolveAppend!: (value: { message_id: string }) => void;
|
|
262
|
+
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
|
|
263
|
+
() =>
|
|
264
|
+
new Promise((resolve) => {
|
|
265
|
+
resolveLoad = resolve;
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
|
|
269
|
+
() =>
|
|
270
|
+
new Promise((resolve) => {
|
|
271
|
+
resolveAppend = resolve;
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const load = persistence.load("thread-1");
|
|
276
|
+
const append = persistence.append(
|
|
277
|
+
"thread-1",
|
|
278
|
+
"message-1",
|
|
279
|
+
null,
|
|
280
|
+
"aui/v0",
|
|
281
|
+
{},
|
|
282
|
+
);
|
|
283
|
+
resolveLoad({ messages });
|
|
284
|
+
await load;
|
|
285
|
+
resolveAppend({ message_id: "remote-2" });
|
|
286
|
+
await append;
|
|
287
|
+
|
|
288
|
+
expect(await persistence.getRemoteId("message-1")).toBe("remote-2");
|
|
289
|
+
await persistence.update("thread-1", "message-1", "aui/v0", {
|
|
290
|
+
text: "updated",
|
|
291
|
+
});
|
|
292
|
+
expect(cloud.threads.messages.update).toHaveBeenCalledWith(
|
|
293
|
+
"thread-1",
|
|
294
|
+
"remote-2",
|
|
295
|
+
{ content: { text: "updated" } },
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("preserves a resolved append mapping when load returns the same ID", async () => {
|
|
300
|
+
const messages = createCloudMessages(1);
|
|
301
|
+
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
|
|
302
|
+
message_id: "remote-1",
|
|
303
|
+
});
|
|
304
|
+
vi.mocked(cloud.threads.messages.list).mockResolvedValue({
|
|
305
|
+
messages,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await persistence.append("thread-1", "message-1", null, "aui/v0", {});
|
|
309
|
+
await persistence.load("thread-1");
|
|
310
|
+
|
|
311
|
+
expect(await persistence.getRemoteId("message-1")).toBe("remote-1");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("keeps the loaded ID when a pending append for it fails", async () => {
|
|
315
|
+
const messages = createCloudMessages(1);
|
|
316
|
+
const failure = new Error("create failed");
|
|
317
|
+
let resolveLoad!: (value: { messages: typeof messages }) => void;
|
|
318
|
+
let rejectAppend!: (error: Error) => void;
|
|
319
|
+
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
|
|
320
|
+
() =>
|
|
321
|
+
new Promise((resolve) => {
|
|
322
|
+
resolveLoad = resolve;
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
|
|
326
|
+
() =>
|
|
327
|
+
new Promise((_resolve, reject) => {
|
|
328
|
+
rejectAppend = reject;
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const load = persistence.load("thread-1");
|
|
333
|
+
const append = persistence.append(
|
|
334
|
+
"thread-1",
|
|
335
|
+
"message-1",
|
|
336
|
+
null,
|
|
337
|
+
"aui/v0",
|
|
338
|
+
{},
|
|
339
|
+
);
|
|
340
|
+
resolveLoad({ messages });
|
|
341
|
+
await load;
|
|
342
|
+
rejectAppend(failure);
|
|
343
|
+
|
|
344
|
+
await expect(append).rejects.toBe(failure);
|
|
345
|
+
expect(persistence.isPersisted("message-1")).toBe(true);
|
|
346
|
+
expect(await persistence.getRemoteId("message-1")).toBe("message-1");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("does not restore a loaded ID after reset when its pending append fails", async () => {
|
|
350
|
+
const messages = createCloudMessages(1);
|
|
351
|
+
const failure = new Error("create failed");
|
|
352
|
+
let rejectAppend!: (error: Error) => void;
|
|
353
|
+
vi.mocked(cloud.threads.messages.list).mockResolvedValue({ messages });
|
|
354
|
+
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
|
|
355
|
+
() =>
|
|
356
|
+
new Promise((_resolve, reject) => {
|
|
357
|
+
rejectAppend = reject;
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const append = persistence.append(
|
|
362
|
+
"thread-1",
|
|
363
|
+
"message-1",
|
|
364
|
+
null,
|
|
365
|
+
"aui/v0",
|
|
366
|
+
{},
|
|
367
|
+
);
|
|
368
|
+
await persistence.load("thread-1");
|
|
369
|
+
persistence.reset();
|
|
370
|
+
rejectAppend(failure);
|
|
371
|
+
|
|
372
|
+
await expect(append).rejects.toBe(failure);
|
|
373
|
+
expect(persistence.isPersisted("message-1")).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("does not restore IDs from a load that finishes after reset", async () => {
|
|
377
|
+
const oldMessages = createCloudMessages(1);
|
|
378
|
+
const newMessages = [{ ...oldMessages[0]!, id: "new-message" }];
|
|
379
|
+
let resolveOld!: (value: { messages: typeof oldMessages }) => void;
|
|
380
|
+
vi.mocked(cloud.threads.messages.list)
|
|
381
|
+
.mockImplementationOnce(
|
|
382
|
+
() =>
|
|
383
|
+
new Promise((resolve) => {
|
|
384
|
+
resolveOld = resolve;
|
|
385
|
+
}),
|
|
386
|
+
)
|
|
387
|
+
.mockResolvedValueOnce({ messages: newMessages });
|
|
388
|
+
|
|
389
|
+
const oldLoad = persistence.load("old-thread");
|
|
390
|
+
persistence.reset();
|
|
391
|
+
await persistence.load("new-thread");
|
|
392
|
+
resolveOld({ messages: oldMessages });
|
|
393
|
+
|
|
394
|
+
expect(await oldLoad).toEqual(oldMessages);
|
|
395
|
+
expect(persistence.isPersisted("message-1")).toBe(false);
|
|
396
|
+
expect(persistence.isPersisted("new-message")).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("keeps a new append mapping when an old load returns the same ID", async () => {
|
|
400
|
+
const messages = createCloudMessages(1);
|
|
401
|
+
let resolveOld!: (value: { messages: typeof messages }) => void;
|
|
402
|
+
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
|
|
403
|
+
() =>
|
|
404
|
+
new Promise((resolve) => {
|
|
405
|
+
resolveOld = resolve;
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
|
|
409
|
+
message_id: "new-remote-id",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const oldLoad = persistence.load("old-thread");
|
|
413
|
+
persistence.reset();
|
|
414
|
+
await persistence.append("new-thread", "message-1", null, "aui/v0", {});
|
|
415
|
+
resolveOld({ messages });
|
|
416
|
+
await oldLoad;
|
|
417
|
+
|
|
418
|
+
expect(await persistence.getRemoteId("message-1")).toBe("new-remote-id");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("does not populate any old IDs when reset happens between history pages", async () => {
|
|
422
|
+
const messages = createCloudMessages(201);
|
|
423
|
+
let resolveLast!: (value: { messages: typeof messages }) => void;
|
|
424
|
+
vi.mocked(cloud.threads.messages.list)
|
|
425
|
+
.mockResolvedValueOnce({ messages: messages.slice(0, 200) })
|
|
426
|
+
.mockImplementationOnce(
|
|
427
|
+
() =>
|
|
428
|
+
new Promise((resolve) => {
|
|
429
|
+
resolveLast = resolve;
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const load = persistence.load("old-thread");
|
|
434
|
+
await vi.waitFor(() =>
|
|
435
|
+
expect(cloud.threads.messages.list).toHaveBeenCalledTimes(2),
|
|
436
|
+
);
|
|
437
|
+
persistence.reset();
|
|
438
|
+
resolveLast({ messages: messages.slice(200) });
|
|
439
|
+
|
|
440
|
+
expect(await load).toEqual(messages);
|
|
441
|
+
expect(
|
|
442
|
+
messages.some((message) => persistence.isPersisted(message.id)),
|
|
443
|
+
).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("propagates an old load failure without disturbing post-reset state", async () => {
|
|
447
|
+
let rejectOld!: (error: Error) => void;
|
|
448
|
+
vi.mocked(cloud.threads.messages.list).mockImplementationOnce(
|
|
449
|
+
() =>
|
|
450
|
+
new Promise((_resolve, reject) => {
|
|
451
|
+
rejectOld = reject;
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
|
|
455
|
+
message_id: "new-remote-id",
|
|
456
|
+
});
|
|
457
|
+
const failure = new Error("history failed");
|
|
458
|
+
const oldLoad = persistence.load("old-thread");
|
|
459
|
+
const rejected = expect(oldLoad).rejects.toBe(failure);
|
|
460
|
+
persistence.reset();
|
|
461
|
+
await persistence.append("new-thread", "new-message", null, "aui/v0", {});
|
|
462
|
+
rejectOld(failure);
|
|
463
|
+
|
|
464
|
+
await rejected;
|
|
465
|
+
expect(await persistence.getRemoteId("new-message")).toBe("new-remote-id");
|
|
255
466
|
});
|
|
256
467
|
|
|
257
468
|
it("maps prototype-named loaded messages", async () => {
|
|
@@ -383,6 +594,30 @@ describe("CloudMessagePersistence", () => {
|
|
|
383
594
|
expect(persistence.isPersisted("local-1")).toBe(false);
|
|
384
595
|
});
|
|
385
596
|
|
|
597
|
+
it("does not restore an append mapping after reset", async () => {
|
|
598
|
+
let resolveAppend!: (value: { message_id: string }) => void;
|
|
599
|
+
vi.mocked(cloud.threads.messages.create).mockImplementationOnce(
|
|
600
|
+
() =>
|
|
601
|
+
new Promise((resolve) => {
|
|
602
|
+
resolveAppend = resolve;
|
|
603
|
+
}),
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const append = persistence.append(
|
|
607
|
+
"thread-1",
|
|
608
|
+
"local-1",
|
|
609
|
+
null,
|
|
610
|
+
"aui/v0",
|
|
611
|
+
{},
|
|
612
|
+
);
|
|
613
|
+
persistence.reset();
|
|
614
|
+
resolveAppend({ message_id: "remote-1" });
|
|
615
|
+
|
|
616
|
+
await expect(append).resolves.toBeUndefined();
|
|
617
|
+
expect(persistence.isPersisted("local-1")).toBe(false);
|
|
618
|
+
expect(await persistence.getRemoteId("local-1")).toBeUndefined();
|
|
619
|
+
});
|
|
620
|
+
|
|
386
621
|
it("reset clears all ID mappings", async () => {
|
|
387
622
|
vi.mocked(cloud.threads.messages.create).mockResolvedValue({
|
|
388
623
|
message_id: "remote-1",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantCloud } from "../AssistantCloud";
|
|
3
|
+
import { CloudRunReporter } from "../CloudRunReporter";
|
|
4
|
+
|
|
5
|
+
const createCloud = (telemetry: AssistantCloud["telemetry"]) => {
|
|
6
|
+
const report = vi.fn().mockResolvedValue({ run_id: "run_1" });
|
|
7
|
+
const cloud = { telemetry, runs: { report } } as unknown as AssistantCloud;
|
|
8
|
+
return { cloud, report };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("CloudRunReporter", () => {
|
|
12
|
+
it("sends nothing while telemetry is disabled", async () => {
|
|
13
|
+
const { cloud, report } = createCloud({ enabled: false });
|
|
14
|
+
await new CloudRunReporter(cloud).report({
|
|
15
|
+
threadId: "thread_1",
|
|
16
|
+
status: "completed",
|
|
17
|
+
});
|
|
18
|
+
expect(report).not.toHaveBeenCalled();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("stamps the cloud's environment, release and tags and applies beforeReport last", async () => {
|
|
22
|
+
const beforeReport = vi.fn((report) => ({ ...report, model_id: "gpt" }));
|
|
23
|
+
const { cloud, report } = createCloud({
|
|
24
|
+
enabled: true,
|
|
25
|
+
environment: "production",
|
|
26
|
+
release: "1.2.3",
|
|
27
|
+
tags: ["web"],
|
|
28
|
+
beforeReport,
|
|
29
|
+
});
|
|
30
|
+
await new CloudRunReporter(cloud).report({
|
|
31
|
+
threadId: "thread_1",
|
|
32
|
+
status: "completed",
|
|
33
|
+
outputText: "hi",
|
|
34
|
+
});
|
|
35
|
+
expect(beforeReport).toHaveBeenCalledWith(
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
thread_id: "thread_1",
|
|
38
|
+
environment: "production",
|
|
39
|
+
release: "1.2.3",
|
|
40
|
+
tags: ["web"],
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
expect(report).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining({ thread_id: "thread_1", model_id: "gpt" }),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("skips a report the hook vetoes without consuming its key", async () => {
|
|
49
|
+
const { cloud, report } = createCloud({
|
|
50
|
+
enabled: true,
|
|
51
|
+
beforeReport: vi
|
|
52
|
+
.fn()
|
|
53
|
+
.mockReturnValueOnce(null)
|
|
54
|
+
.mockImplementation((r) => r),
|
|
55
|
+
});
|
|
56
|
+
const reporter = new CloudRunReporter(cloud);
|
|
57
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
58
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
59
|
+
expect(report).toHaveBeenCalledOnce();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("reports a keyed run once and an unkeyed run every time", async () => {
|
|
63
|
+
const { cloud, report } = createCloud({ enabled: true });
|
|
64
|
+
const reporter = new CloudRunReporter(cloud);
|
|
65
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
66
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
67
|
+
await reporter.report({ threadId: "t", status: "completed" });
|
|
68
|
+
await reporter.report({ threadId: "t", status: "completed" });
|
|
69
|
+
expect(report).toHaveBeenCalledTimes(3);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("swallows a failed send and reads the cloud through a getter", async () => {
|
|
73
|
+
const { cloud } = createCloud({ enabled: true });
|
|
74
|
+
(cloud.runs.report as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
75
|
+
new Error("offline"),
|
|
76
|
+
);
|
|
77
|
+
await expect(
|
|
78
|
+
new CloudRunReporter(() => cloud).report({
|
|
79
|
+
threadId: "t",
|
|
80
|
+
status: "error",
|
|
81
|
+
}),
|
|
82
|
+
).resolves.toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("resolves when the hook throws and reports nothing", async () => {
|
|
86
|
+
const { cloud, report } = createCloud({
|
|
87
|
+
enabled: true,
|
|
88
|
+
beforeReport: () => {
|
|
89
|
+
throw new Error("hook");
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
await expect(
|
|
93
|
+
new CloudRunReporter(cloud).report({
|
|
94
|
+
threadId: "t",
|
|
95
|
+
status: "completed",
|
|
96
|
+
}),
|
|
97
|
+
).resolves.toBeUndefined();
|
|
98
|
+
expect(report).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/version.ts
ADDED