assistant-cloud 0.1.43 → 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 +66 -0
- package/dist/AssistantCloud.d.ts +6 -1
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +13 -5
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +28 -3
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +37 -10
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +4 -1
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +15 -3
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudEvents.d.ts +32 -0
- package/dist/AssistantCloudEvents.d.ts.map +1 -0
- package/dist/AssistantCloudEvents.js +119 -0
- package/dist/AssistantCloudEvents.js.map +1 -0
- package/dist/AssistantCloudFiles.d.ts +12 -1
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudFiles.js +13 -1
- package/dist/AssistantCloudFiles.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +12 -0
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js +2 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/AssistantCloudScores.d.ts +27 -0
- package/dist/AssistantCloudScores.d.ts.map +1 -0
- package/dist/AssistantCloudScores.js +30 -0
- package/dist/AssistantCloudScores.js.map +1 -0
- package/dist/AssistantCloudThreadMessages.d.ts +11 -1
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +12 -1
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +8 -0
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/AssistantCloudThreads.js +9 -1
- package/dist/AssistantCloudThreads.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 +12 -9
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +54 -20
- 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/cloudResponse.d.ts +4 -1
- package/dist/cloudResponse.d.ts.map +1 -1
- package/dist/cloudResponse.js +14 -1
- package/dist/cloudResponse.js.map +1 -1
- package/dist/generateThreadTitle.js +1 -0
- package/dist/generateThreadTitle.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -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 +86 -7
- package/dist/runTelemetry.d.ts.map +1 -1
- package/dist/runTelemetry.js +144 -1
- package/dist/runTelemetry.js.map +1 -1
- package/dist/telemetry/index.d.ts +32 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +62 -0
- package/dist/telemetry/index.js.map +1 -0
- 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 +43 -6
- package/src/AssistantCloud.ts +23 -8
- package/src/AssistantCloudAPI.ts +72 -10
- package/src/AssistantCloudAuthStrategy.ts +17 -2
- package/src/AssistantCloudEvents.test.ts +149 -0
- package/src/AssistantCloudEvents.ts +190 -0
- package/src/AssistantCloudFiles.test.ts +59 -0
- package/src/AssistantCloudFiles.ts +39 -0
- package/src/AssistantCloudRuns.ts +13 -1
- package/src/AssistantCloudScores.test.ts +113 -0
- package/src/AssistantCloudScores.ts +61 -0
- package/src/AssistantCloudThreadMessages.test.ts +51 -0
- package/src/AssistantCloudThreadMessages.ts +33 -0
- package/src/AssistantCloudThreads.test.ts +21 -0
- package/src/AssistantCloudThreads.ts +21 -0
- package/src/CloudEngagementReporter.ts +231 -0
- package/src/CloudMessagePersistence.ts +70 -27
- 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/cloudResponse.test.ts +34 -1
- package/src/cloudResponse.ts +30 -0
- package/src/generateThreadTitle.test.ts +32 -0
- package/src/generateThreadTitle.ts +1 -0
- package/src/index.ts +35 -2
- package/src/instrumentMcpSampling.test.ts +102 -0
- package/src/instrumentMcpSampling.ts +16 -2
- package/src/runTelemetry.test.ts +325 -0
- package/src/runTelemetry.ts +274 -6
- package/src/telemetry/index.test.ts +252 -0
- package/src/telemetry/index.ts +98 -0
- package/src/tests/AssistantCloud.test.ts +55 -1
- package/src/tests/AssistantCloudAPI.test.ts +95 -4
- package/src/tests/AssistantCloudAuthStrategy.test.ts +93 -2
- package/src/tests/AssistantCloudProjects.test.ts +1 -5
- package/src/tests/CloudEngagementReporter.test.ts +184 -0
- package/src/tests/CloudMessagePersistence.test.ts +350 -0
- package/src/tests/CloudRunReporter.test.ts +100 -0
- package/src/version.ts +4 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { AssistantCloud } from "../AssistantCloud";
|
|
3
3
|
import type { AssistantCloudTelemetryConfig } from "../AssistantCloudAPI";
|
|
4
4
|
|
|
@@ -13,6 +13,10 @@ const createCloud = (
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
describe("AssistantCloud telemetry config", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllGlobals();
|
|
18
|
+
});
|
|
19
|
+
|
|
16
20
|
it("defaults to enabled", () => {
|
|
17
21
|
expect(createCloud().telemetry.enabled).toBe(true);
|
|
18
22
|
expect(createCloud(true).telemetry.enabled).toBe(true);
|
|
@@ -23,6 +27,13 @@ describe("AssistantCloud telemetry config", () => {
|
|
|
23
27
|
expect(createCloud({ enabled: false }).telemetry.enabled).toBe(false);
|
|
24
28
|
});
|
|
25
29
|
|
|
30
|
+
it("can disable engagement events without disabling run reports", () => {
|
|
31
|
+
expect(createCloud({ events: false }).telemetry).toEqual({
|
|
32
|
+
enabled: true,
|
|
33
|
+
events: false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
it("stays enabled when the config object carries an undefined enabled", () => {
|
|
27
38
|
const beforeReport: NonNullable<
|
|
28
39
|
AssistantCloudTelemetryConfig["beforeReport"]
|
|
@@ -36,4 +47,47 @@ describe("AssistantCloud telemetry config", () => {
|
|
|
36
47
|
expect(telemetry.enabled).toBe(true);
|
|
37
48
|
expect(telemetry.beforeReport).toBe(beforeReport);
|
|
38
49
|
});
|
|
50
|
+
|
|
51
|
+
it("preserves configured run report dimensions", () => {
|
|
52
|
+
expect(
|
|
53
|
+
createCloud({
|
|
54
|
+
release: "web-2026.09.08",
|
|
55
|
+
environment: "production",
|
|
56
|
+
tags: ["region:sg", "tier:paid"],
|
|
57
|
+
}).telemetry,
|
|
58
|
+
).toEqual({
|
|
59
|
+
enabled: true,
|
|
60
|
+
release: "web-2026.09.08",
|
|
61
|
+
environment: "production",
|
|
62
|
+
tags: ["region:sg", "tier:paid"],
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("forwards registered SDK identities to requests and stream options", async () => {
|
|
67
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
headers: new Headers(),
|
|
70
|
+
text: vi.fn().mockResolvedValue(JSON.stringify({ threads: [] })),
|
|
71
|
+
});
|
|
72
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
73
|
+
|
|
74
|
+
const cloud = createCloud();
|
|
75
|
+
cloud.registerSdk({ name: "@assistant-ui/core", version: "0.3.18" });
|
|
76
|
+
|
|
77
|
+
await cloud.threads.list();
|
|
78
|
+
|
|
79
|
+
const [, init] = fetchMock.mock.calls[0]!;
|
|
80
|
+
expect(init.headers).toMatchObject({
|
|
81
|
+
"Aui-Sdk": expect.stringMatching(
|
|
82
|
+
/^assistant-cloud\/.* @assistant-ui\/core\/0\.3\.18$/,
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
await expect(
|
|
86
|
+
cloud.runs.__internal_getAssistantOptions("assistant-id").headers(),
|
|
87
|
+
).resolves.toMatchObject({
|
|
88
|
+
"Aui-Sdk": expect.stringMatching(
|
|
89
|
+
/^assistant-cloud\/.* @assistant-ui\/core\/0\.3\.18$/,
|
|
90
|
+
),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
39
93
|
});
|
|
@@ -45,6 +45,7 @@ describe("AssistantCloudAPI", () => {
|
|
|
45
45
|
Authorization: "Bearer test-key",
|
|
46
46
|
"Aui-User-Id": "u-1",
|
|
47
47
|
"Aui-Workspace-Id": "w-1",
|
|
48
|
+
"Aui-Sdk": expect.stringMatching(/^assistant-cloud\//),
|
|
48
49
|
"Content-Type": "application/json",
|
|
49
50
|
"X-Test": "1",
|
|
50
51
|
});
|
|
@@ -53,6 +54,56 @@ describe("AssistantCloudAPI", () => {
|
|
|
53
54
|
expect(init.body).toBe(JSON.stringify({ hello: "world" }));
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
it("ignores identities that cannot travel in a header", () => {
|
|
58
|
+
const api = new AssistantCloudAPI({
|
|
59
|
+
apiKey: "test-key",
|
|
60
|
+
userId: "u-1",
|
|
61
|
+
workspaceId: "w-1",
|
|
62
|
+
});
|
|
63
|
+
api.registerSdk({ name: "bad name", version: "1.0.0" });
|
|
64
|
+
api.registerSdk({ name: "@scope/pkg", version: "1.0.0 ok" });
|
|
65
|
+
api.registerSdk({ name: "@scope/pkg\ttab", version: "1.0.0" });
|
|
66
|
+
api.registerSdk({ name: "@scope/ok", version: " 1.0.0 " });
|
|
67
|
+
|
|
68
|
+
expect(api.sdkHeader().split(" ")).toEqual([
|
|
69
|
+
expect.stringMatching(/^assistant-cloud\//),
|
|
70
|
+
"@scope/ok/1.0.0",
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("sends each registered SDK identity once in registration order", async () => {
|
|
75
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
headers: new Headers(),
|
|
78
|
+
json: vi.fn().mockResolvedValue({}),
|
|
79
|
+
});
|
|
80
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
81
|
+
|
|
82
|
+
const api = new AssistantCloudAPI({
|
|
83
|
+
apiKey: "test-key",
|
|
84
|
+
userId: "u-1",
|
|
85
|
+
workspaceId: "w-1",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
api.registerSdk({ name: " @assistant-ui/core ", version: " 0.3.18 " });
|
|
89
|
+
api.registerSdk({ name: "@assistant-ui/core", version: "0.3.18" });
|
|
90
|
+
api.registerSdk({ name: "@assistant-ui/ai-sdk", version: "0.0.5" });
|
|
91
|
+
api.registerSdk({ name: " ", version: "0.0.5" });
|
|
92
|
+
api.registerSdk({ name: "@assistant-ui/react-langgraph", version: " " });
|
|
93
|
+
|
|
94
|
+
await api.makeRawRequest("/threads", {
|
|
95
|
+
headers: { "Aui-Sdk": "overridden" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const [, init] = fetchMock.mock.calls[0]!;
|
|
99
|
+
expect(init.headers).toMatchObject({ "Aui-Sdk": api.sdkHeader() });
|
|
100
|
+
expect(api.sdkHeader().split(" ")).toEqual([
|
|
101
|
+
expect.stringMatching(/^assistant-cloud\//),
|
|
102
|
+
"@assistant-ui/core/0.3.18",
|
|
103
|
+
"@assistant-ui/ai-sdk/0.0.5",
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
56
107
|
it("uses custom baseUrl when provided with apiKey config", async () => {
|
|
57
108
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
58
109
|
ok: true,
|
|
@@ -143,7 +194,7 @@ describe("AssistantCloudAPI", () => {
|
|
|
143
194
|
);
|
|
144
195
|
});
|
|
145
196
|
|
|
146
|
-
it("
|
|
197
|
+
it("preserves the current error shape for JSON error responses without a code", async () => {
|
|
147
198
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
148
199
|
ok: false,
|
|
149
200
|
status: 400,
|
|
@@ -167,16 +218,54 @@ describe("AssistantCloudAPI", () => {
|
|
|
167
218
|
expect(error.name).toBe("CloudAPIError");
|
|
168
219
|
expect(error.message).toBe("invalid request payload");
|
|
169
220
|
expect(error.status).toBe(400);
|
|
221
|
+
expect(error.code).toBeUndefined();
|
|
222
|
+
expect(error.details).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("exposes the plan-limit error code and details", async () => {
|
|
226
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
227
|
+
ok: false,
|
|
228
|
+
status: 402,
|
|
229
|
+
headers: new Headers(),
|
|
230
|
+
text: vi.fn().mockResolvedValue(
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
error: "plan_limit_reached",
|
|
233
|
+
plan: "free",
|
|
234
|
+
period_end: "2026-10-01T00:00:00.000Z",
|
|
235
|
+
cap: 100,
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
});
|
|
239
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
240
|
+
|
|
241
|
+
const api = new AssistantCloudAPI({
|
|
242
|
+
apiKey: "test-key",
|
|
243
|
+
userId: "u-1",
|
|
244
|
+
workspaceId: "w-1",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const error = await api.makeRawRequest("/threads").catch((e) => e);
|
|
248
|
+
expect(error).toBeInstanceOf(CloudAPIError);
|
|
249
|
+
expect(error.message).toBe(
|
|
250
|
+
'Request failed with status 402, {"error":"plan_limit_reached","plan":"free","period_end":"2026-10-01T00:00:00.000Z","cap":100}',
|
|
251
|
+
);
|
|
252
|
+
expect(error.status).toBe(402);
|
|
253
|
+
expect(error.code).toBe("plan_limit_reached");
|
|
254
|
+
expect(error.details).toEqual({
|
|
255
|
+
plan: "free",
|
|
256
|
+
period_end: "2026-10-01T00:00:00.000Z",
|
|
257
|
+
cap: 100,
|
|
258
|
+
});
|
|
170
259
|
});
|
|
171
260
|
|
|
172
|
-
it("
|
|
261
|
+
it("exposes rate-limit error codes with empty details", async () => {
|
|
173
262
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
174
263
|
ok: false,
|
|
175
264
|
status: 429,
|
|
176
265
|
headers: new Headers(),
|
|
177
266
|
text: vi
|
|
178
267
|
.fn()
|
|
179
|
-
.mockResolvedValue(JSON.stringify({ error: "
|
|
268
|
+
.mockResolvedValue(JSON.stringify({ error: "rate_limited" })),
|
|
180
269
|
});
|
|
181
270
|
vi.stubGlobal("fetch", fetchMock);
|
|
182
271
|
|
|
@@ -189,9 +278,11 @@ describe("AssistantCloudAPI", () => {
|
|
|
189
278
|
const error = await api.makeRawRequest("/threads").catch((e) => e);
|
|
190
279
|
expect(error).toBeInstanceOf(CloudAPIError);
|
|
191
280
|
expect(error.message).toBe(
|
|
192
|
-
'Request failed with status 429, {"error":"
|
|
281
|
+
'Request failed with status 429, {"error":"rate_limited"}',
|
|
193
282
|
);
|
|
194
283
|
expect(error.status).toBe(429);
|
|
284
|
+
expect(error.code).toBe("rate_limited");
|
|
285
|
+
expect(error.details).toEqual({});
|
|
195
286
|
});
|
|
196
287
|
|
|
197
288
|
it("throws generic error with status for non-JSON error responses", async () => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
import {
|
|
3
4
|
AssistantCloudAnonymousAuthStrategy,
|
|
4
5
|
AssistantCloudJWTAuthStrategy,
|
|
6
|
+
readAnonymousRefreshToken,
|
|
5
7
|
} from "../AssistantCloudAuthStrategy";
|
|
6
8
|
import { CloudResponseError } from "../cloudResponse";
|
|
7
9
|
|
|
@@ -56,6 +58,73 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
it("reads the stored anonymous refresh token", () => {
|
|
62
|
+
const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]);
|
|
63
|
+
installLocalStorage({
|
|
64
|
+
getItem: (key) => values.get(key) ?? null,
|
|
65
|
+
setItem: (key, value) => {
|
|
66
|
+
values.set(key, value);
|
|
67
|
+
},
|
|
68
|
+
removeItem: (key) => {
|
|
69
|
+
values.delete(key);
|
|
70
|
+
},
|
|
71
|
+
} as Storage);
|
|
72
|
+
|
|
73
|
+
expect(readAnonymousRefreshToken(baseUrl)).toBe(refreshToken.token);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("reads the token stored for a base url given with a trailing slash", () => {
|
|
77
|
+
// The two-step cast below erases the contextual Storage typing the plain
|
|
78
|
+
// `as Storage` sites get, so this parameter needs its own annotation.
|
|
79
|
+
installLocalStorage({
|
|
80
|
+
getItem: (key: string) =>
|
|
81
|
+
key === refreshTokenKey ? JSON.stringify(refreshToken) : null,
|
|
82
|
+
setItem: vi.fn(),
|
|
83
|
+
removeItem: vi.fn(),
|
|
84
|
+
} as unknown as Storage);
|
|
85
|
+
|
|
86
|
+
expect(readAnonymousRefreshToken(`${baseUrl}/`)).toBe(refreshToken.token);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns null when no anonymous refresh token is stored", () => {
|
|
90
|
+
installLocalStorage({
|
|
91
|
+
getItem: () => null,
|
|
92
|
+
setItem: vi.fn(),
|
|
93
|
+
removeItem: vi.fn(),
|
|
94
|
+
} as unknown as Storage);
|
|
95
|
+
|
|
96
|
+
expect(readAnonymousRefreshToken(baseUrl)).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it.each([30_000, 0, -1])(
|
|
100
|
+
"returns null when the stored anonymous refresh token expires in %i ms",
|
|
101
|
+
(expiresIn) => {
|
|
102
|
+
vi.useFakeTimers();
|
|
103
|
+
const now = Date.UTC(2026, 8, 4, 12, 0, 0);
|
|
104
|
+
vi.setSystemTime(now);
|
|
105
|
+
const values = new Map([
|
|
106
|
+
[
|
|
107
|
+
refreshTokenKey,
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
token: refreshToken.token,
|
|
110
|
+
expires_at: new Date(now + expiresIn).toISOString(),
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
]);
|
|
114
|
+
installLocalStorage({
|
|
115
|
+
getItem: (key) => values.get(key) ?? null,
|
|
116
|
+
setItem: (key, value) => {
|
|
117
|
+
values.set(key, value);
|
|
118
|
+
},
|
|
119
|
+
removeItem: (key) => {
|
|
120
|
+
values.delete(key);
|
|
121
|
+
},
|
|
122
|
+
} as Storage);
|
|
123
|
+
|
|
124
|
+
expect(readAnonymousRefreshToken(baseUrl)).toBeNull();
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
59
128
|
it.each([
|
|
60
129
|
"2099-01-01",
|
|
61
130
|
"2099-01-01T00:00:00Z",
|
|
@@ -269,7 +338,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
269
338
|
{ once: true },
|
|
270
339
|
);
|
|
271
340
|
}),
|
|
272
|
-
} as Response),
|
|
341
|
+
} as unknown as Response),
|
|
273
342
|
)
|
|
274
343
|
.mockResolvedValueOnce({
|
|
275
344
|
ok: true,
|
|
@@ -598,7 +667,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
598
667
|
const removeItem = vi.fn(() => {
|
|
599
668
|
throw new DOMException("blocked", "SecurityError");
|
|
600
669
|
});
|
|
601
|
-
installLocalStorage({ getItem, setItem, removeItem } as Storage);
|
|
670
|
+
installLocalStorage({ getItem, setItem, removeItem } as unknown as Storage);
|
|
602
671
|
mockAnonymousTokenFetch();
|
|
603
672
|
|
|
604
673
|
await expect(
|
|
@@ -881,4 +950,26 @@ describe("AssistantCloudJWTAuthStrategy", () => {
|
|
|
881
950
|
});
|
|
882
951
|
expect(authToken).toHaveBeenCalledTimes(2);
|
|
883
952
|
});
|
|
953
|
+
|
|
954
|
+
it("does not cache a malformed rotated token", async () => {
|
|
955
|
+
const authToken = vi
|
|
956
|
+
.fn<() => Promise<string | null>>()
|
|
957
|
+
.mockResolvedValue(accessToken);
|
|
958
|
+
const strategy = new AssistantCloudJWTAuthStrategy(authToken);
|
|
959
|
+
|
|
960
|
+
await expect(strategy.getAuthHeaders()).resolves.toEqual({
|
|
961
|
+
Authorization: `Bearer ${accessToken}`,
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
expect(() =>
|
|
965
|
+
strategy.readAuthHeaders(
|
|
966
|
+
new Headers({ Authorization: "Bearer malformed" }),
|
|
967
|
+
),
|
|
968
|
+
).toThrow("Unable to determine the token expiry");
|
|
969
|
+
|
|
970
|
+
await expect(strategy.getAuthHeaders()).resolves.toEqual({
|
|
971
|
+
Authorization: `Bearer ${accessToken}`,
|
|
972
|
+
});
|
|
973
|
+
expect(authToken).toHaveBeenCalledTimes(1);
|
|
974
|
+
});
|
|
884
975
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach,
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
2
|
import { AssistantCloudProjects } from "../AssistantCloudProjects";
|
|
3
3
|
import type { AssistantCloudAPI } from "../AssistantCloudAPI";
|
|
4
4
|
|
|
@@ -19,10 +19,6 @@ describe("AssistantCloudProjects", () => {
|
|
|
19
19
|
projects = new AssistantCloudProjects(mockApi);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
vi.clearAllMocks();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
22
|
it("lists project threads with the query", async () => {
|
|
27
23
|
vi.mocked(mockApi.makeRequest).mockResolvedValue({ threads: [] });
|
|
28
24
|
|
|
@@ -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
|
+
});
|