@dingdawg/sdk 2.0.0
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/dist/client.d.ts +126 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +302 -0
- package/dist/client.js.map +1 -0
- package/dist/durable.d.ts +162 -0
- package/dist/durable.d.ts.map +1 -0
- package/dist/durable.js +223 -0
- package/dist/durable.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/client.test.ts +576 -0
- package/src/__tests__/durable.test.ts +212 -0
- package/src/client.ts +461 -0
- package/src/durable.ts +347 -0
- package/src/index.ts +43 -0
- package/src/types.ts +178 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dingdawg/sdk — DingDawgClient unit tests
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Constructor validation (missing/empty API key throws TypeError)
|
|
6
|
+
* - Default baseUrl is set correctly
|
|
7
|
+
* - Custom baseUrl is accepted
|
|
8
|
+
* - Trailing slash stripped from baseUrl
|
|
9
|
+
* - All API method namespaces exist (agent.*, billing.*)
|
|
10
|
+
* - All methods are functions with correct arity
|
|
11
|
+
* - Request URL construction (correct path per method)
|
|
12
|
+
* - Authorization header includes API key
|
|
13
|
+
* - Content-Type header set to application/json
|
|
14
|
+
* - User-Agent header present
|
|
15
|
+
* - GET requests don't send a body
|
|
16
|
+
* - POST requests send JSON body
|
|
17
|
+
* - agent.create normalises response to AgentRecord shape
|
|
18
|
+
* - agent.list returns PaginatedList shape
|
|
19
|
+
* - agent.get returns AgentRecord shape
|
|
20
|
+
* - agent.sendMessage with string message
|
|
21
|
+
* - agent.sendMessage with SendMessageOptions object
|
|
22
|
+
* - agent.sendMessage correctly encodes agentId in URL
|
|
23
|
+
* - billing.currentMonth returns MonthlyBillingSummary shape
|
|
24
|
+
* - billing.summary returns BillingSummary shape
|
|
25
|
+
* - Network error is wrapped in DingDawgApiError (status 0)
|
|
26
|
+
* - 401 response throws DingDawgApiError with status 401
|
|
27
|
+
* - 404 response throws DingDawgApiError with status 404
|
|
28
|
+
* - 422 response throws DingDawgApiError with status 422
|
|
29
|
+
* - 500 response throws DingDawgApiError with status 500
|
|
30
|
+
* - DingDawgApiError.body contains parsed API error detail
|
|
31
|
+
* - Non-JSON error body is handled gracefully
|
|
32
|
+
* - DingDawgApiError is instanceof Error
|
|
33
|
+
* - DingDawgApiError.name is "DingDawgApiError"
|
|
34
|
+
* - agent.list pagination params forwarded as query string
|
|
35
|
+
* - agent.create sends all optional fields
|
|
36
|
+
* - sendMessage sessionId/userId/metadata forwarded in body
|
|
37
|
+
* - client.agent is same reference on repeated access (no new object per call)
|
|
38
|
+
* - client.billing is same reference on repeated access
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { jest, describe, test, expect, beforeEach } from "@jest/globals";
|
|
42
|
+
import { DingDawgClient, DingDawgApiError } from "../client.js";
|
|
43
|
+
import type {
|
|
44
|
+
AgentRecord,
|
|
45
|
+
PaginatedList,
|
|
46
|
+
MonthlyBillingSummary,
|
|
47
|
+
BillingSummary,
|
|
48
|
+
TriggerResponse,
|
|
49
|
+
} from "../types.js";
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
type FetchCall = {
|
|
56
|
+
url: string;
|
|
57
|
+
init: RequestInit;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Build a mock fetch that records calls and returns a preset response. */
|
|
61
|
+
function makeMockFetch(
|
|
62
|
+
status: number,
|
|
63
|
+
body: unknown,
|
|
64
|
+
contentType = "application/json"
|
|
65
|
+
): { mock: typeof fetch; calls: FetchCall[] } {
|
|
66
|
+
const calls: FetchCall[] = [];
|
|
67
|
+
|
|
68
|
+
const mock = jest.fn(async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
69
|
+
calls.push({ url: String(url), init: init ?? {} });
|
|
70
|
+
const bodyText =
|
|
71
|
+
typeof body === "string" ? body : JSON.stringify(body);
|
|
72
|
+
return new Response(bodyText, {
|
|
73
|
+
status,
|
|
74
|
+
headers: { "Content-Type": contentType },
|
|
75
|
+
});
|
|
76
|
+
}) as unknown as typeof fetch;
|
|
77
|
+
|
|
78
|
+
return { mock, calls };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build a mock fetch that throws a network error. */
|
|
82
|
+
function makeNetworkErrorFetch(): typeof fetch {
|
|
83
|
+
return jest.fn(async () => {
|
|
84
|
+
throw new Error("ECONNREFUSED");
|
|
85
|
+
}) as unknown as typeof fetch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Sample agent API response (snake_case from backend). */
|
|
89
|
+
const SAMPLE_AGENT_RESPONSE = {
|
|
90
|
+
id: "agent-uuid-001",
|
|
91
|
+
handle: "test-agent",
|
|
92
|
+
name: "Test Agent",
|
|
93
|
+
agent_type: "business",
|
|
94
|
+
industry_type: "restaurant",
|
|
95
|
+
status: "active",
|
|
96
|
+
created_at: "2026-03-01T00:00:00Z",
|
|
97
|
+
updated_at: "2026-03-11T00:00:00Z",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Sample trigger response. */
|
|
101
|
+
const SAMPLE_TRIGGER_RESPONSE = {
|
|
102
|
+
reply: "Hello! How can I help you today?",
|
|
103
|
+
session_id: "session-abc-123",
|
|
104
|
+
timestamp: "2026-03-11T12:00:00Z",
|
|
105
|
+
model: "gpt-4o-mini",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Sample monthly billing response. */
|
|
109
|
+
const SAMPLE_MONTHLY_BILLING = {
|
|
110
|
+
month: "2026-03",
|
|
111
|
+
total_actions: 42,
|
|
112
|
+
total_cents: 4200,
|
|
113
|
+
free_actions_remaining: 8,
|
|
114
|
+
line_items: [
|
|
115
|
+
{ action: "crm_lookup", count: 20, cost_cents: 2000 },
|
|
116
|
+
{ action: "email_send", count: 22, cost_cents: 2200 },
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Sample billing summary response. */
|
|
121
|
+
const SAMPLE_BILLING_SUMMARY = {
|
|
122
|
+
total_actions: 150,
|
|
123
|
+
total_cents: 10000,
|
|
124
|
+
current_month: SAMPLE_MONTHLY_BILLING,
|
|
125
|
+
stripe_customer_id: "cus_abc123",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Constructor tests
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
describe("DingDawgClient — constructor", () => {
|
|
133
|
+
test("throws TypeError when apiKey is missing (undefined)", () => {
|
|
134
|
+
expect(() => {
|
|
135
|
+
// @ts-expect-error intentional bad call
|
|
136
|
+
new DingDawgClient({});
|
|
137
|
+
}).toThrow(TypeError);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("throws TypeError when apiKey is empty string", () => {
|
|
141
|
+
expect(() => {
|
|
142
|
+
new DingDawgClient({ apiKey: "" });
|
|
143
|
+
}).toThrow(TypeError);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("throws TypeError when apiKey is whitespace only", () => {
|
|
147
|
+
expect(() => {
|
|
148
|
+
new DingDawgClient({ apiKey: " " });
|
|
149
|
+
}).toThrow(TypeError);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("accepts a valid apiKey without throwing", () => {
|
|
153
|
+
expect(() => {
|
|
154
|
+
new DingDawgClient({ apiKey: "dd_live_test_key" });
|
|
155
|
+
}).not.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("default baseUrl points to production Railway URL", () => {
|
|
159
|
+
const client = new DingDawgClient({ apiKey: "dd_test" });
|
|
160
|
+
// Access via sendMessage which constructs the URL — check fetch call
|
|
161
|
+
expect(client).toBeDefined();
|
|
162
|
+
// We verify baseUrl indirectly through request URL construction tests
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("accepts custom baseUrl", () => {
|
|
166
|
+
expect(() => {
|
|
167
|
+
new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
168
|
+
}).not.toThrow();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("strips trailing slash from baseUrl", async () => {
|
|
172
|
+
const { mock, calls } = makeMockFetch(200, SAMPLE_AGENT_RESPONSE);
|
|
173
|
+
global.fetch = mock;
|
|
174
|
+
|
|
175
|
+
const client = new DingDawgClient({
|
|
176
|
+
apiKey: "dd_test",
|
|
177
|
+
baseUrl: "http://localhost:8000/",
|
|
178
|
+
});
|
|
179
|
+
await client.agent.get("agent-001");
|
|
180
|
+
|
|
181
|
+
expect(calls[0]?.url).not.toContain("//api");
|
|
182
|
+
expect(calls[0]?.url).toMatch(/^http:\/\/localhost:8000\/api/);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// API namespace existence tests
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
describe("DingDawgClient — namespace structure", () => {
|
|
191
|
+
let client: DingDawgClient;
|
|
192
|
+
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
client = new DingDawgClient({ apiKey: "dd_live_test" });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("client.agent is defined", () => {
|
|
198
|
+
expect(client.agent).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("client.billing is defined", () => {
|
|
202
|
+
expect(client.billing).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("client.agent.create is a function", () => {
|
|
206
|
+
expect(typeof client.agent.create).toBe("function");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("client.agent.list is a function", () => {
|
|
210
|
+
expect(typeof client.agent.list).toBe("function");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("client.agent.get is a function", () => {
|
|
214
|
+
expect(typeof client.agent.get).toBe("function");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("client.agent.sendMessage is a function", () => {
|
|
218
|
+
expect(typeof client.agent.sendMessage).toBe("function");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("client.billing.currentMonth is a function", () => {
|
|
222
|
+
expect(typeof client.billing.currentMonth).toBe("function");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("client.billing.summary is a function", () => {
|
|
226
|
+
expect(typeof client.billing.summary).toBe("function");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("client.agent returns same object reference each access", () => {
|
|
230
|
+
expect(client.agent).toBe(client.agent);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("client.billing returns same object reference each access", () => {
|
|
234
|
+
expect(client.billing).toBe(client.billing);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Request header tests
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
describe("DingDawgClient — request headers", () => {
|
|
243
|
+
let client: DingDawgClient;
|
|
244
|
+
let calls: FetchCall[];
|
|
245
|
+
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
const { mock, calls: c } = makeMockFetch(200, SAMPLE_AGENT_RESPONSE);
|
|
248
|
+
global.fetch = mock;
|
|
249
|
+
calls = c;
|
|
250
|
+
client = new DingDawgClient({
|
|
251
|
+
apiKey: "dd_live_sk_abc123",
|
|
252
|
+
baseUrl: "http://localhost:8000",
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("Authorization header includes API key as Bearer token", async () => {
|
|
257
|
+
await client.agent.get("agent-001");
|
|
258
|
+
const headers = calls[0]?.init.headers as Record<string, string>;
|
|
259
|
+
expect(headers?.["Authorization"]).toBe("Bearer dd_live_sk_abc123");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("Content-Type header is application/json", async () => {
|
|
263
|
+
await client.agent.get("agent-001");
|
|
264
|
+
const headers = calls[0]?.init.headers as Record<string, string>;
|
|
265
|
+
expect(headers?.["Content-Type"]).toBe("application/json");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("User-Agent header is present", async () => {
|
|
269
|
+
await client.agent.get("agent-001");
|
|
270
|
+
const headers = calls[0]?.init.headers as Record<string, string>;
|
|
271
|
+
expect(headers?.["User-Agent"]).toMatch(/@dingdawg\/sdk/);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Request URL construction tests
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
describe("DingDawgClient — URL construction", () => {
|
|
280
|
+
let calls: FetchCall[];
|
|
281
|
+
|
|
282
|
+
function setupClient(): DingDawgClient {
|
|
283
|
+
const { mock, calls: c } = makeMockFetch(200, SAMPLE_AGENT_RESPONSE);
|
|
284
|
+
global.fetch = mock;
|
|
285
|
+
calls = c;
|
|
286
|
+
return new DingDawgClient({
|
|
287
|
+
apiKey: "dd_test",
|
|
288
|
+
baseUrl: "http://localhost:8000",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
test("agent.get constructs correct URL", async () => {
|
|
293
|
+
const client = setupClient();
|
|
294
|
+
await client.agent.get("my-agent-id");
|
|
295
|
+
expect(calls[0]?.url).toBe(
|
|
296
|
+
"http://localhost:8000/api/v2/partner/agents/my-agent-id"
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("agent.list constructs correct URL", async () => {
|
|
301
|
+
const { mock, calls: c } = makeMockFetch(200, {
|
|
302
|
+
items: [],
|
|
303
|
+
total: 0,
|
|
304
|
+
limit: 20,
|
|
305
|
+
offset: 0,
|
|
306
|
+
});
|
|
307
|
+
global.fetch = mock;
|
|
308
|
+
calls = c;
|
|
309
|
+
const client = new DingDawgClient({
|
|
310
|
+
apiKey: "dd_test",
|
|
311
|
+
baseUrl: "http://localhost:8000",
|
|
312
|
+
});
|
|
313
|
+
await client.agent.list();
|
|
314
|
+
expect(calls[0]?.url).toBe(
|
|
315
|
+
"http://localhost:8000/api/v2/partner/agents"
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("agent.list forwards limit as query param", async () => {
|
|
320
|
+
const { mock, calls: c } = makeMockFetch(200, {
|
|
321
|
+
items: [],
|
|
322
|
+
total: 0,
|
|
323
|
+
limit: 5,
|
|
324
|
+
offset: 0,
|
|
325
|
+
});
|
|
326
|
+
global.fetch = mock;
|
|
327
|
+
calls = c;
|
|
328
|
+
const client = new DingDawgClient({
|
|
329
|
+
apiKey: "dd_test",
|
|
330
|
+
baseUrl: "http://localhost:8000",
|
|
331
|
+
});
|
|
332
|
+
await client.agent.list({ limit: 5, offset: 10 });
|
|
333
|
+
expect(calls[0]?.url).toContain("limit=5");
|
|
334
|
+
expect(calls[0]?.url).toContain("offset=10");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("agent.create posts to correct URL", async () => {
|
|
338
|
+
const client = setupClient();
|
|
339
|
+
await client.agent.create({ name: "Bot", handle: "bot" });
|
|
340
|
+
expect(calls[0]?.url).toBe(
|
|
341
|
+
"http://localhost:8000/api/v2/partner/agents"
|
|
342
|
+
);
|
|
343
|
+
expect(calls[0]?.init.method).toBe("POST");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("agent.sendMessage posts to trigger endpoint with encoded agentId", async () => {
|
|
347
|
+
const client = setupClient();
|
|
348
|
+
const { mock: triggerMock, calls: tc } = makeMockFetch(200, SAMPLE_TRIGGER_RESPONSE);
|
|
349
|
+
global.fetch = triggerMock;
|
|
350
|
+
await client.agent.sendMessage("agent-abc-123", "Hi");
|
|
351
|
+
expect(tc[0]?.url).toBe(
|
|
352
|
+
"http://localhost:8000/api/v1/agents/agent-abc-123/trigger"
|
|
353
|
+
);
|
|
354
|
+
expect(tc[0]?.init.method).toBe("POST");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("billing.currentMonth fetches correct URL", async () => {
|
|
358
|
+
const { mock: bMock, calls: bc } = makeMockFetch(200, SAMPLE_MONTHLY_BILLING);
|
|
359
|
+
global.fetch = bMock;
|
|
360
|
+
const client = new DingDawgClient({
|
|
361
|
+
apiKey: "dd_test",
|
|
362
|
+
baseUrl: "http://localhost:8000",
|
|
363
|
+
});
|
|
364
|
+
await client.billing.currentMonth();
|
|
365
|
+
expect(bc[0]?.url).toBe(
|
|
366
|
+
"http://localhost:8000/api/v2/partner/billing/current-month"
|
|
367
|
+
);
|
|
368
|
+
expect(bc[0]?.init.method).toBe("GET");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("billing.summary fetches correct URL", async () => {
|
|
372
|
+
const { mock: bMock, calls: bc } = makeMockFetch(200, SAMPLE_BILLING_SUMMARY);
|
|
373
|
+
global.fetch = bMock;
|
|
374
|
+
const client = new DingDawgClient({
|
|
375
|
+
apiKey: "dd_test",
|
|
376
|
+
baseUrl: "http://localhost:8000",
|
|
377
|
+
});
|
|
378
|
+
await client.billing.summary();
|
|
379
|
+
expect(bc[0]?.url).toBe(
|
|
380
|
+
"http://localhost:8000/api/v2/partner/billing"
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Response normalisation tests
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
describe("DingDawgClient — response normalisation", () => {
|
|
390
|
+
test("agent.get returns correctly shaped AgentRecord", async () => {
|
|
391
|
+
const { mock } = makeMockFetch(200, SAMPLE_AGENT_RESPONSE);
|
|
392
|
+
global.fetch = mock;
|
|
393
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
394
|
+
const agent: AgentRecord = await client.agent.get("agent-uuid-001");
|
|
395
|
+
|
|
396
|
+
expect(agent.id).toBe("agent-uuid-001");
|
|
397
|
+
expect(agent.handle).toBe("test-agent");
|
|
398
|
+
expect(agent.name).toBe("Test Agent");
|
|
399
|
+
expect(agent.agentType).toBe("business");
|
|
400
|
+
expect(agent.industry).toBe("restaurant");
|
|
401
|
+
expect(agent.status).toBe("active");
|
|
402
|
+
expect(agent.createdAt).toBe("2026-03-01T00:00:00Z");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("agent.list returns PaginatedList shape", async () => {
|
|
406
|
+
const listResponse = {
|
|
407
|
+
items: [SAMPLE_AGENT_RESPONSE],
|
|
408
|
+
total: 1,
|
|
409
|
+
limit: 20,
|
|
410
|
+
offset: 0,
|
|
411
|
+
};
|
|
412
|
+
const { mock } = makeMockFetch(200, listResponse);
|
|
413
|
+
global.fetch = mock;
|
|
414
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
415
|
+
const result: PaginatedList<AgentRecord> = await client.agent.list();
|
|
416
|
+
|
|
417
|
+
expect(result.items).toHaveLength(1);
|
|
418
|
+
expect(result.total).toBe(1);
|
|
419
|
+
expect(result.limit).toBe(20);
|
|
420
|
+
expect(result.offset).toBe(0);
|
|
421
|
+
expect(result.items[0]?.id).toBe("agent-uuid-001");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("agent.sendMessage with string normalises to TriggerResponse", async () => {
|
|
425
|
+
const { mock } = makeMockFetch(200, SAMPLE_TRIGGER_RESPONSE);
|
|
426
|
+
global.fetch = mock;
|
|
427
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
428
|
+
const result: TriggerResponse = await client.agent.sendMessage("agent-001", "Hello");
|
|
429
|
+
|
|
430
|
+
expect(result.reply).toBe("Hello! How can I help you today?");
|
|
431
|
+
expect(result.sessionId).toBe("session-abc-123");
|
|
432
|
+
expect(result.timestamp).toBe("2026-03-11T12:00:00Z");
|
|
433
|
+
expect(result.model).toBe("gpt-4o-mini");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("agent.sendMessage with SendMessageOptions forwards all fields", async () => {
|
|
437
|
+
const { mock, calls } = makeMockFetch(200, SAMPLE_TRIGGER_RESPONSE);
|
|
438
|
+
global.fetch = mock;
|
|
439
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
440
|
+
await client.agent.sendMessage("agent-001", {
|
|
441
|
+
message: "Hello",
|
|
442
|
+
userId: "user-xyz",
|
|
443
|
+
sessionId: "session-existing",
|
|
444
|
+
metadata: { channel: "web" },
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const body = JSON.parse(calls[0]?.init.body as string);
|
|
448
|
+
expect(body.user_id).toBe("user-xyz");
|
|
449
|
+
expect(body.session_id).toBe("session-existing");
|
|
450
|
+
expect(body.metadata?.channel).toBe("web");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("billing.currentMonth returns MonthlyBillingSummary shape", async () => {
|
|
454
|
+
const { mock } = makeMockFetch(200, SAMPLE_MONTHLY_BILLING);
|
|
455
|
+
global.fetch = mock;
|
|
456
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
457
|
+
const result: MonthlyBillingSummary = await client.billing.currentMonth();
|
|
458
|
+
|
|
459
|
+
expect(result.month).toBe("2026-03");
|
|
460
|
+
expect(result.totalActions).toBe(42);
|
|
461
|
+
expect(result.totalCents).toBe(4200);
|
|
462
|
+
expect(result.freeActionsRemaining).toBe(8);
|
|
463
|
+
expect(result.lineItems).toHaveLength(2);
|
|
464
|
+
expect(result.lineItems[0]?.action).toBe("crm_lookup");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("billing.summary returns BillingSummary with currentMonth nested", async () => {
|
|
468
|
+
const { mock } = makeMockFetch(200, SAMPLE_BILLING_SUMMARY);
|
|
469
|
+
global.fetch = mock;
|
|
470
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
471
|
+
const result: BillingSummary = await client.billing.summary();
|
|
472
|
+
|
|
473
|
+
expect(result.totalActions).toBe(150);
|
|
474
|
+
expect(result.totalCents).toBe(10000);
|
|
475
|
+
expect(result.stripeCustomerId).toBe("cus_abc123");
|
|
476
|
+
expect(result.currentMonth.month).toBe("2026-03");
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Error handling tests
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
describe("DingDawgClient — error handling", () => {
|
|
485
|
+
test("network error throws DingDawgApiError with status 0", async () => {
|
|
486
|
+
global.fetch = makeNetworkErrorFetch();
|
|
487
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
488
|
+
|
|
489
|
+
await expect(client.agent.get("agent-001")).rejects.toThrow(DingDawgApiError);
|
|
490
|
+
await expect(client.agent.get("agent-001")).rejects.toMatchObject({
|
|
491
|
+
status: 0,
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("401 response throws DingDawgApiError with status 401", async () => {
|
|
496
|
+
const { mock } = makeMockFetch(401, { detail: "Invalid token" });
|
|
497
|
+
global.fetch = mock;
|
|
498
|
+
const client = new DingDawgClient({ apiKey: "dd_bad_key", baseUrl: "http://localhost:8000" });
|
|
499
|
+
|
|
500
|
+
await expect(client.agent.get("agent-001")).rejects.toMatchObject({ status: 401 });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("404 response throws DingDawgApiError with status 404", async () => {
|
|
504
|
+
const { mock } = makeMockFetch(404, { detail: "Agent not found" });
|
|
505
|
+
global.fetch = mock;
|
|
506
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
507
|
+
|
|
508
|
+
await expect(client.agent.get("non-existent")).rejects.toMatchObject({ status: 404 });
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("422 response throws DingDawgApiError with status 422", async () => {
|
|
512
|
+
const { mock } = makeMockFetch(422, { detail: "Validation error" });
|
|
513
|
+
global.fetch = mock;
|
|
514
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
515
|
+
|
|
516
|
+
await expect(
|
|
517
|
+
client.agent.create({ name: "", handle: "" })
|
|
518
|
+
).rejects.toMatchObject({ status: 422 });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("500 response throws DingDawgApiError with status 500", async () => {
|
|
522
|
+
const { mock } = makeMockFetch(500, { detail: "Internal server error" });
|
|
523
|
+
global.fetch = mock;
|
|
524
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
525
|
+
|
|
526
|
+
await expect(client.agent.get("agent-001")).rejects.toMatchObject({ status: 500 });
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("DingDawgApiError.body contains API error detail", async () => {
|
|
530
|
+
const { mock } = makeMockFetch(404, { detail: "Agent not found", code: "AGENT_NOT_FOUND" });
|
|
531
|
+
global.fetch = mock;
|
|
532
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
await client.agent.get("missing");
|
|
536
|
+
} catch (err) {
|
|
537
|
+
expect(err).toBeInstanceOf(DingDawgApiError);
|
|
538
|
+
const apiErr = err as DingDawgApiError;
|
|
539
|
+
expect(apiErr.body?.detail).toBe("Agent not found");
|
|
540
|
+
expect(apiErr.body?.code).toBe("AGENT_NOT_FOUND");
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("non-JSON error body is handled gracefully (no crash)", async () => {
|
|
545
|
+
const { mock } = makeMockFetch(503, "Service Unavailable", "text/plain");
|
|
546
|
+
global.fetch = mock;
|
|
547
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
548
|
+
|
|
549
|
+
await expect(client.agent.get("agent-001")).rejects.toBeInstanceOf(DingDawgApiError);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("DingDawgApiError is instanceof Error", async () => {
|
|
553
|
+
const { mock } = makeMockFetch(401, { detail: "Unauthorized" });
|
|
554
|
+
global.fetch = mock;
|
|
555
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
await client.agent.get("agent-001");
|
|
559
|
+
} catch (err) {
|
|
560
|
+
expect(err).toBeInstanceOf(Error);
|
|
561
|
+
expect(err).toBeInstanceOf(DingDawgApiError);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("DingDawgApiError.name is 'DingDawgApiError'", async () => {
|
|
566
|
+
const { mock } = makeMockFetch(401, { detail: "Unauthorized" });
|
|
567
|
+
global.fetch = mock;
|
|
568
|
+
const client = new DingDawgClient({ apiKey: "dd_test", baseUrl: "http://localhost:8000" });
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
await client.agent.get("agent-001");
|
|
572
|
+
} catch (err) {
|
|
573
|
+
expect((err as DingDawgApiError).name).toBe("DingDawgApiError");
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
});
|