@adcp/client 4.15.0 → 4.16.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.
Files changed (93) hide show
  1. package/dist/lib/adapters/si-session-manager.d.ts.map +1 -1
  2. package/dist/lib/adapters/si-session-manager.js +8 -3
  3. package/dist/lib/adapters/si-session-manager.js.map +1 -1
  4. package/dist/lib/agents/index.generated.d.ts +9 -1
  5. package/dist/lib/agents/index.generated.d.ts.map +1 -1
  6. package/dist/lib/agents/index.generated.js +12 -0
  7. package/dist/lib/agents/index.generated.js.map +1 -1
  8. package/dist/lib/core/ADCPMultiAgentClient.d.ts.map +1 -1
  9. package/dist/lib/core/ADCPMultiAgentClient.js +10 -3
  10. package/dist/lib/core/ADCPMultiAgentClient.js.map +1 -1
  11. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  12. package/dist/lib/core/AsyncHandler.d.ts.map +1 -1
  13. package/dist/lib/core/AsyncHandler.js +1 -1
  14. package/dist/lib/core/AsyncHandler.js.map +1 -1
  15. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  16. package/dist/lib/core/SingleAgentClient.js +1 -1
  17. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  18. package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
  19. package/dist/lib/core/TaskExecutor.js +18 -8
  20. package/dist/lib/core/TaskExecutor.js.map +1 -1
  21. package/dist/lib/registry/types.generated.d.ts +9 -9
  22. package/dist/lib/registry/types.generated.d.ts.map +1 -1
  23. package/dist/lib/registry/types.generated.js +1 -1
  24. package/dist/lib/testing/agent-tester.d.ts +1 -1
  25. package/dist/lib/testing/agent-tester.d.ts.map +1 -1
  26. package/dist/lib/testing/agent-tester.js +46 -2
  27. package/dist/lib/testing/agent-tester.js.map +1 -1
  28. package/dist/lib/testing/client.d.ts +5 -0
  29. package/dist/lib/testing/client.d.ts.map +1 -1
  30. package/dist/lib/testing/client.js +17 -1
  31. package/dist/lib/testing/client.js.map +1 -1
  32. package/dist/lib/testing/compliance/comply.d.ts +4 -0
  33. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  34. package/dist/lib/testing/compliance/comply.js +237 -172
  35. package/dist/lib/testing/compliance/comply.js.map +1 -1
  36. package/dist/lib/testing/compliance/types.d.ts +6 -0
  37. package/dist/lib/testing/compliance/types.d.ts.map +1 -1
  38. package/dist/lib/testing/index.d.ts +1 -1
  39. package/dist/lib/testing/index.d.ts.map +1 -1
  40. package/dist/lib/testing/index.js +10 -2
  41. package/dist/lib/testing/index.js.map +1 -1
  42. package/dist/lib/testing/orchestrator.d.ts +4 -0
  43. package/dist/lib/testing/orchestrator.d.ts.map +1 -1
  44. package/dist/lib/testing/orchestrator.js +12 -0
  45. package/dist/lib/testing/orchestrator.js.map +1 -1
  46. package/dist/lib/testing/scenarios/deterministic.d.ts +37 -0
  47. package/dist/lib/testing/scenarios/deterministic.d.ts.map +1 -0
  48. package/dist/lib/testing/scenarios/deterministic.js +705 -0
  49. package/dist/lib/testing/scenarios/deterministic.js.map +1 -0
  50. package/dist/lib/testing/scenarios/edge-cases.d.ts.map +1 -1
  51. package/dist/lib/testing/scenarios/edge-cases.js +7 -14
  52. package/dist/lib/testing/scenarios/edge-cases.js.map +1 -1
  53. package/dist/lib/testing/scenarios/error-compliance.d.ts.map +1 -1
  54. package/dist/lib/testing/scenarios/error-compliance.js +3 -6
  55. package/dist/lib/testing/scenarios/error-compliance.js.map +1 -1
  56. package/dist/lib/testing/scenarios/index.d.ts +1 -0
  57. package/dist/lib/testing/scenarios/index.d.ts.map +1 -1
  58. package/dist/lib/testing/scenarios/index.js +11 -1
  59. package/dist/lib/testing/scenarios/index.js.map +1 -1
  60. package/dist/lib/testing/scenarios/media-buy.d.ts.map +1 -1
  61. package/dist/lib/testing/scenarios/media-buy.js +0 -2
  62. package/dist/lib/testing/scenarios/media-buy.js.map +1 -1
  63. package/dist/lib/testing/scenarios/sponsored-intelligence.d.ts.map +1 -1
  64. package/dist/lib/testing/scenarios/sponsored-intelligence.js +2 -1
  65. package/dist/lib/testing/scenarios/sponsored-intelligence.js.map +1 -1
  66. package/dist/lib/testing/test-controller.d.ts +46 -0
  67. package/dist/lib/testing/test-controller.d.ts.map +1 -0
  68. package/dist/lib/testing/test-controller.js +143 -0
  69. package/dist/lib/testing/test-controller.js.map +1 -0
  70. package/dist/lib/testing/types.d.ts +4 -1
  71. package/dist/lib/testing/types.d.ts.map +1 -1
  72. package/dist/lib/types/core.generated.d.ts +280 -7613
  73. package/dist/lib/types/core.generated.d.ts.map +1 -1
  74. package/dist/lib/types/core.generated.js +1 -1
  75. package/dist/lib/types/schemas.generated.d.ts +1569 -9507
  76. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  77. package/dist/lib/types/schemas.generated.js +259 -210
  78. package/dist/lib/types/schemas.generated.js.map +1 -1
  79. package/dist/lib/types/tools.generated.d.ts +657 -7749
  80. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  81. package/dist/lib/utils/response-schemas.d.ts.map +1 -1
  82. package/dist/lib/utils/response-schemas.js +1 -0
  83. package/dist/lib/utils/response-schemas.js.map +1 -1
  84. package/dist/lib/utils/response-unwrapper.d.ts.map +1 -1
  85. package/dist/lib/utils/response-unwrapper.js +12 -0
  86. package/dist/lib/utils/response-unwrapper.js.map +1 -1
  87. package/dist/lib/utils/union-errors.d.ts +16 -0
  88. package/dist/lib/utils/union-errors.d.ts.map +1 -0
  89. package/dist/lib/utils/union-errors.js +34 -0
  90. package/dist/lib/utils/union-errors.js.map +1 -0
  91. package/dist/lib/version.d.ts +3 -3
  92. package/dist/lib/version.js +3 -3
  93. package/package.json +1 -1
@@ -0,0 +1,705 @@
1
+ "use strict";
2
+ /**
3
+ * Deterministic Compliance Scenarios
4
+ *
5
+ * These scenarios use the comply_test_controller to force seller-side
6
+ * state transitions, enabling full state machine verification.
7
+ * Only run when the controller is detected.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.testCreativeStateMachine = testCreativeStateMachine;
11
+ exports.testMediaBuyStateMachine = testMediaBuyStateMachine;
12
+ exports.testAccountStateMachine = testAccountStateMachine;
13
+ exports.testSessionStateMachine = testSessionStateMachine;
14
+ exports.testDeliverySimulation = testDeliverySimulation;
15
+ exports.testBudgetSimulation = testBudgetSimulation;
16
+ exports.testControllerValidation = testControllerValidation;
17
+ const client_1 = require("../client");
18
+ const test_controller_1 = require("../test-controller");
19
+ const media_buy_1 = require("./media-buy");
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+ function getController(options) {
24
+ return options._controllerCapabilities;
25
+ }
26
+ /** Extract an entity ID from step results (created_id, response_preview field, or plural array) */
27
+ function extractIdFromSteps(stepsToSearch, field) {
28
+ const pluralField = field.endsWith('_id') ? field + 's' : undefined; // creative_id → creative_ids
29
+ for (const step of stepsToSearch) {
30
+ if (step.created_id)
31
+ return step.created_id;
32
+ if (step.response_preview) {
33
+ try {
34
+ const preview = JSON.parse(step.response_preview);
35
+ if (preview[field])
36
+ return preview[field];
37
+ if (pluralField && Array.isArray(preview[pluralField]) && preview[pluralField][0]) {
38
+ return preview[pluralField][0];
39
+ }
40
+ }
41
+ catch {
42
+ /* skip */
43
+ }
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ /** Build a TestStepResult from a controller transition response */
49
+ function transitionStep(label, response, expectedSuccess, durationMs) {
50
+ const passed = response.success === expectedSuccess;
51
+ if (response.success) {
52
+ return {
53
+ step: label,
54
+ task: 'comply_test_controller',
55
+ passed,
56
+ duration_ms: durationMs,
57
+ details: `${response.previous_state} → ${response.current_state}`,
58
+ response_preview: JSON.stringify({ previous_state: response.previous_state, current_state: response.current_state }, null, 2),
59
+ ...(!passed && {
60
+ error: `Expected failure but got success: ${response.previous_state} → ${response.current_state}`,
61
+ }),
62
+ };
63
+ }
64
+ else {
65
+ return {
66
+ step: label,
67
+ task: 'comply_test_controller',
68
+ passed,
69
+ duration_ms: durationMs,
70
+ details: `Error: ${response.error} — ${response.error_detail || ''}`,
71
+ response_preview: JSON.stringify({ error: response.error, current_state: response.current_state }, null, 2),
72
+ ...(!passed && { error: `Expected success but got ${response.error}: ${response.error_detail || ''}` }),
73
+ };
74
+ }
75
+ }
76
+ /**
77
+ * Create timed wrappers bound to specific options.
78
+ * This ensures controller calls use the same account as test scenarios.
79
+ */
80
+ function createTimedHelpers(client, options) {
81
+ return {
82
+ async forceStatus(scenario, params) {
83
+ const start = Date.now();
84
+ const response = await (0, test_controller_1.forceStatus)(client, scenario, params, options);
85
+ return { response, durationMs: Date.now() - start };
86
+ },
87
+ async simulate(scenario, params) {
88
+ const start = Date.now();
89
+ const response = await (0, test_controller_1.simulate)(client, scenario, params, options);
90
+ return { response, durationMs: Date.now() - start };
91
+ },
92
+ };
93
+ }
94
+ /**
95
+ * Call a typed method on the client via executeTask.
96
+ * Uses the same pattern as existing scenarios but through the typed executeTask overload.
97
+ */
98
+ function callTask(client, taskName, params) {
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- AgentClient.executeTask exists but TestClient type doesn't expose it directly
100
+ return client.executeTask(taskName, params);
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Creative State Machine
104
+ // ---------------------------------------------------------------------------
105
+ async function testCreativeStateMachine(agentUrl, options) {
106
+ const steps = [];
107
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
108
+ const controller = getController(options);
109
+ if (!controller?.detected || !(0, test_controller_1.supportsScenario)(controller, 'force_creative_status')) {
110
+ steps.push({
111
+ step: 'Controller check',
112
+ task: 'comply_test_controller',
113
+ passed: true,
114
+ duration_ms: 0,
115
+ details: 'force_creative_status not supported — skipping',
116
+ });
117
+ return { steps };
118
+ }
119
+ const ctrl = createTimedHelpers(client, options);
120
+ // Step 1: Sync a creative to get an entity to work with
121
+ const { steps: syncSteps, profile } = await (0, media_buy_1.testCreativeSync)(agentUrl, options);
122
+ steps.push(...syncSteps);
123
+ const creativeId = extractIdFromSteps(syncSteps, 'creative_id');
124
+ if (!creativeId) {
125
+ steps.push({
126
+ step: 'Find creative for state machine test',
127
+ passed: false,
128
+ duration_ms: 0,
129
+ error: 'No creative_id found from sync_creatives — cannot test state machine',
130
+ });
131
+ return { steps, profile };
132
+ }
133
+ // Step 2: Force to approved
134
+ const { response: approveResult, durationMs: approveDur } = await ctrl.forceStatus('force_creative_status', {
135
+ creative_id: creativeId,
136
+ status: 'approved',
137
+ });
138
+ steps.push(transitionStep('Force creative → approved', approveResult, true, approveDur));
139
+ // Step 3: Verify via list_creatives
140
+ if (approveResult.success && profile?.tools.includes('list_creatives')) {
141
+ const { result: listResult, step: listStep } = await (0, client_1.runStep)('Verify creative status via list_creatives', 'list_creatives', async () => callTask(client, 'list_creatives', {}));
142
+ if (listResult?.success && listResult?.data) {
143
+ const creatives = listResult.data.creatives || [];
144
+ const found = creatives.find((c) => c.creative_id === creativeId);
145
+ if (found) {
146
+ listStep.details = `Creative ${creativeId} status: ${found.status}`;
147
+ if (found.status !== 'approved') {
148
+ listStep.passed = false;
149
+ listStep.error = `Expected status 'approved', got '${found.status}'`;
150
+ }
151
+ }
152
+ }
153
+ steps.push(listStep);
154
+ }
155
+ // Step 4: Force to archived
156
+ const { response: archiveResult, durationMs: archiveDur } = await ctrl.forceStatus('force_creative_status', {
157
+ creative_id: creativeId,
158
+ status: 'archived',
159
+ });
160
+ steps.push(transitionStep('Force creative → archived', archiveResult, true, archiveDur));
161
+ // Step 5: Invalid transition — archived is terminal, can't go back to processing
162
+ const { response: invalidResult, durationMs: invalidDur } = await ctrl.forceStatus('force_creative_status', {
163
+ creative_id: creativeId,
164
+ status: 'processing',
165
+ });
166
+ steps.push(transitionStep('Invalid: archived → processing (expect INVALID_TRANSITION)', invalidResult, false, invalidDur));
167
+ if (!invalidResult.success && invalidResult.error !== 'INVALID_TRANSITION') {
168
+ steps.push({
169
+ step: 'Validate error code for invalid transition',
170
+ task: 'comply_test_controller',
171
+ passed: false,
172
+ duration_ms: 0,
173
+ error: `Expected error code INVALID_TRANSITION, got ${invalidResult.error}`,
174
+ });
175
+ }
176
+ // Step 6: Force to rejected with reason on a fresh creative.
177
+ // Sellers may auto-approve on sync, so force to pending_review first (the only state
178
+ // that allows rejection in most state machines), then reject.
179
+ const { steps: resyncSteps } = await (0, media_buy_1.testCreativeSync)(agentUrl, options);
180
+ const freshCreativeId = extractIdFromSteps(resyncSteps, 'creative_id');
181
+ if (freshCreativeId) {
182
+ // Try to reach a state that allows rejection
183
+ const { response: toPendingReview } = await ctrl.forceStatus('force_creative_status', {
184
+ creative_id: freshCreativeId,
185
+ status: 'pending_review',
186
+ });
187
+ // Whether or not pending_review was reachable, try rejecting from wherever we are.
188
+ // Some sellers allow rejection from processing directly; others require pending_review first.
189
+ const { response: rejectResult, durationMs: rejectDur } = await ctrl.forceStatus('force_creative_status', {
190
+ creative_id: freshCreativeId,
191
+ status: 'rejected',
192
+ rejection_reason: 'Brand safety policy violation (comply test)',
193
+ });
194
+ steps.push(transitionStep('Force creative → rejected with reason', rejectResult, true, rejectDur));
195
+ }
196
+ return { steps, profile };
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // Media Buy State Machine
200
+ // ---------------------------------------------------------------------------
201
+ async function testMediaBuyStateMachine(agentUrl, options) {
202
+ const steps = [];
203
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
204
+ const controller = getController(options);
205
+ if (!controller?.detected || !(0, test_controller_1.supportsScenario)(controller, 'force_media_buy_status')) {
206
+ steps.push({
207
+ step: 'Controller check',
208
+ task: 'comply_test_controller',
209
+ passed: true,
210
+ duration_ms: 0,
211
+ details: 'force_media_buy_status not supported — skipping',
212
+ });
213
+ return { steps };
214
+ }
215
+ const ctrl = createTimedHelpers(client, options);
216
+ // Create a media buy
217
+ const { steps: createSteps, profile, mediaBuyId } = await (0, media_buy_1.testCreateMediaBuy)(agentUrl, options);
218
+ steps.push(...createSteps);
219
+ if (!mediaBuyId) {
220
+ steps.push({
221
+ step: 'Find media buy for state machine test',
222
+ passed: false,
223
+ duration_ms: 0,
224
+ error: 'No media_buy_id from create_media_buy — cannot test state machine',
225
+ });
226
+ return { steps, profile };
227
+ }
228
+ // Force to active
229
+ const { response: activateResult, durationMs: activateDur } = await ctrl.forceStatus('force_media_buy_status', {
230
+ media_buy_id: mediaBuyId,
231
+ status: 'active',
232
+ });
233
+ steps.push(transitionStep('Force media buy → active', activateResult, true, activateDur));
234
+ // Verify via get_media_buys
235
+ if (activateResult.success && profile?.tools.includes('get_media_buys')) {
236
+ const { result: getResult, step: getStep } = await (0, client_1.runStep)('Verify media buy status via get_media_buys', 'get_media_buys', async () => callTask(client, 'get_media_buys', { media_buy_id: mediaBuyId }));
237
+ if (getResult?.success && getResult?.data) {
238
+ const data = getResult.data;
239
+ const buys = data.media_buys || [data];
240
+ const found = buys.find((b) => b.media_buy_id === mediaBuyId);
241
+ if (found) {
242
+ getStep.details = `Media buy ${mediaBuyId} status: ${found.status}`;
243
+ if (found.status !== 'active') {
244
+ getStep.passed = false;
245
+ getStep.error = `Expected status 'active', got '${found.status}'`;
246
+ }
247
+ }
248
+ }
249
+ steps.push(getStep);
250
+ }
251
+ // Force to completed (terminal)
252
+ const { response: completeResult, durationMs: completeDur } = await ctrl.forceStatus('force_media_buy_status', {
253
+ media_buy_id: mediaBuyId,
254
+ status: 'completed',
255
+ });
256
+ steps.push(transitionStep('Force media buy → completed', completeResult, true, completeDur));
257
+ // Invalid: completed → active (terminal)
258
+ const { response: invalidResult, durationMs: invalidDur } = await ctrl.forceStatus('force_media_buy_status', {
259
+ media_buy_id: mediaBuyId,
260
+ status: 'active',
261
+ });
262
+ steps.push(transitionStep('Invalid: completed → active (expect INVALID_TRANSITION)', invalidResult, false, invalidDur));
263
+ // Test rejection: force to pending_activation first (some sellers create as active),
264
+ // then reject from there. If the seller doesn't support pending_activation, skip.
265
+ const { steps: create2Steps, mediaBuyId: mediaBuyId2 } = await (0, media_buy_1.testCreateMediaBuy)(agentUrl, options);
266
+ steps.push(...create2Steps);
267
+ if (mediaBuyId2) {
268
+ // Try to force to pending_activation first
269
+ const { response: pendingResult } = await ctrl.forceStatus('force_media_buy_status', {
270
+ media_buy_id: mediaBuyId2,
271
+ status: 'pending_activation',
272
+ });
273
+ if (pendingResult.success) {
274
+ // Now reject from pending_activation
275
+ const { response: rejectResult, durationMs: rejectDur } = await ctrl.forceStatus('force_media_buy_status', {
276
+ media_buy_id: mediaBuyId2,
277
+ status: 'rejected',
278
+ rejection_reason: 'Policy violation (comply test)',
279
+ });
280
+ steps.push(transitionStep('Force media buy → rejected from pending_activation', rejectResult, true, rejectDur));
281
+ }
282
+ else {
283
+ // Can't reach pending_activation — test cancellation instead (valid from active)
284
+ const { response: cancelResult, durationMs: cancelDur } = await ctrl.forceStatus('force_media_buy_status', {
285
+ media_buy_id: mediaBuyId2,
286
+ status: 'canceled',
287
+ });
288
+ steps.push(transitionStep('Force media buy → canceled from active', cancelResult, true, cancelDur));
289
+ }
290
+ }
291
+ return { steps, profile };
292
+ }
293
+ // ---------------------------------------------------------------------------
294
+ // Account State Machine
295
+ // ---------------------------------------------------------------------------
296
+ async function testAccountStateMachine(agentUrl, options) {
297
+ const steps = [];
298
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
299
+ const controller = getController(options);
300
+ if (!controller?.detected || !(0, test_controller_1.supportsScenario)(controller, 'force_account_status')) {
301
+ steps.push({
302
+ step: 'Controller check',
303
+ task: 'comply_test_controller',
304
+ passed: true,
305
+ duration_ms: 0,
306
+ details: 'force_account_status not supported — skipping',
307
+ });
308
+ return { steps };
309
+ }
310
+ const ctrl = createTimedHelpers(client, options);
311
+ // Discover an account to work with
312
+ const profile = options._profile;
313
+ let accountId;
314
+ if (profile?.tools.includes('list_accounts')) {
315
+ const { result: listResult, step: listStep } = await (0, client_1.runStep)('List accounts for state machine test', 'list_accounts', async () => callTask(client, 'list_accounts', {}));
316
+ if (listResult?.success && listResult?.data) {
317
+ const accounts = listResult.data.accounts || [];
318
+ const active = accounts.find((a) => a.status === 'active');
319
+ accountId = (active?.account_id || accounts[0]?.account_id);
320
+ listStep.details = `Found ${accounts.length} account(s), using ${accountId || 'none'}`;
321
+ }
322
+ steps.push(listStep);
323
+ }
324
+ if (!accountId) {
325
+ steps.push({
326
+ step: 'Find account for state machine test',
327
+ passed: true,
328
+ duration_ms: 0,
329
+ details: 'No account found — skipping account state machine test',
330
+ });
331
+ return { steps, profile };
332
+ }
333
+ // Force to suspended
334
+ const { response: suspendResult, durationMs: suspendDur } = await ctrl.forceStatus('force_account_status', {
335
+ account_id: accountId,
336
+ status: 'suspended',
337
+ });
338
+ steps.push(transitionStep('Force account → suspended', suspendResult, true, suspendDur));
339
+ // Verify operations are gated: create_media_buy should fail
340
+ if (suspendResult.success && profile?.tools.includes('create_media_buy') && profile?.tools.includes('get_products')) {
341
+ const { result: createResult, step: createStep } = await (0, client_1.runStep)('Verify create_media_buy blocked when suspended', 'create_media_buy', async () => callTask(client, 'create_media_buy', {
342
+ brief: 'comply test — should be blocked by suspension',
343
+ budget: { amount: 100, currency: 'USD' },
344
+ }));
345
+ // We expect this to fail
346
+ if (createResult?.success) {
347
+ createStep.passed = false;
348
+ createStep.error = 'create_media_buy succeeded but account is suspended — operation gate not enforced';
349
+ }
350
+ else {
351
+ createStep.passed = true;
352
+ createStep.details = 'create_media_buy correctly blocked when account is suspended';
353
+ }
354
+ steps.push(createStep);
355
+ }
356
+ // Reactivate
357
+ const { response: reactivateResult, durationMs: reactivateDur } = await ctrl.forceStatus('force_account_status', {
358
+ account_id: accountId,
359
+ status: 'active',
360
+ });
361
+ steps.push(transitionStep('Force account → active (reactivate)', reactivateResult, true, reactivateDur));
362
+ // Force to payment_required
363
+ const { response: paymentResult, durationMs: paymentDur } = await ctrl.forceStatus('force_account_status', {
364
+ account_id: accountId,
365
+ status: 'payment_required',
366
+ });
367
+ steps.push(transitionStep('Force account → payment_required', paymentResult, true, paymentDur));
368
+ // Restore to active
369
+ const { response: restoreResult, durationMs: restoreDur } = await ctrl.forceStatus('force_account_status', {
370
+ account_id: accountId,
371
+ status: 'active',
372
+ });
373
+ steps.push(transitionStep('Force account → active (restore)', restoreResult, true, restoreDur));
374
+ return { steps, profile };
375
+ }
376
+ // ---------------------------------------------------------------------------
377
+ // SI Session State Machine
378
+ // ---------------------------------------------------------------------------
379
+ async function testSessionStateMachine(agentUrl, options) {
380
+ const steps = [];
381
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
382
+ const controller = getController(options);
383
+ const profile = options._profile;
384
+ if (!controller?.detected || !(0, test_controller_1.supportsScenario)(controller, 'force_session_status')) {
385
+ steps.push({
386
+ step: 'Controller check',
387
+ task: 'comply_test_controller',
388
+ passed: true,
389
+ duration_ms: 0,
390
+ details: 'force_session_status not supported — skipping',
391
+ });
392
+ return { steps };
393
+ }
394
+ if (!profile?.tools.includes('si_initiate_session')) {
395
+ steps.push({
396
+ step: 'SI tools check',
397
+ passed: true,
398
+ duration_ms: 0,
399
+ details: 'si_initiate_session not available — skipping',
400
+ });
401
+ return { steps };
402
+ }
403
+ const ctrl = createTimedHelpers(client, options);
404
+ // Initiate a session
405
+ const { result: initResult, step: initStep } = await (0, client_1.runStep)('Initiate SI session for state machine test', 'si_initiate_session', async () => callTask(client, 'si_initiate_session', {
406
+ identity: { user_type: 'consumer' },
407
+ supported_capabilities: { response_formats: ['text'] },
408
+ }));
409
+ steps.push(initStep);
410
+ const sessionId = initResult?.data?.session_id;
411
+ if (!sessionId) {
412
+ steps.push({
413
+ step: 'Extract session ID',
414
+ passed: false,
415
+ duration_ms: 0,
416
+ error: 'No session_id in initiate response',
417
+ });
418
+ return { steps, profile };
419
+ }
420
+ // Force session timeout
421
+ const { response: timeoutResult, durationMs: timeoutDur } = await ctrl.forceStatus('force_session_status', {
422
+ session_id: sessionId,
423
+ status: 'terminated',
424
+ termination_reason: 'session_timeout',
425
+ });
426
+ steps.push(transitionStep('Force session → terminated (timeout)', timeoutResult, true, timeoutDur));
427
+ // Verify: si_send_message should fail with SESSION_NOT_FOUND or similar
428
+ if (timeoutResult.success && profile.tools.includes('si_send_message')) {
429
+ const { result: msgResult, step: msgStep } = await (0, client_1.runStep)('Verify si_send_message fails after forced termination', 'si_send_message', async () => callTask(client, 'si_send_message', {
430
+ session_id: sessionId,
431
+ message: 'comply test — session should be terminated',
432
+ }));
433
+ if (msgResult?.success) {
434
+ const data = msgResult.data;
435
+ const errors = data?.errors;
436
+ if ((errors && errors.length > 0) || data?.session_status === 'terminated') {
437
+ msgStep.passed = true;
438
+ msgStep.details = 'Agent correctly returned error/terminated status for expired session';
439
+ }
440
+ else {
441
+ msgStep.passed = false;
442
+ msgStep.error = 'si_send_message succeeded on terminated session — session state not enforced';
443
+ }
444
+ }
445
+ else {
446
+ msgStep.passed = true;
447
+ msgStep.details = 'si_send_message correctly failed on terminated session';
448
+ }
449
+ steps.push(msgStep);
450
+ }
451
+ return { steps, profile };
452
+ }
453
+ // ---------------------------------------------------------------------------
454
+ // Delivery Simulation
455
+ // ---------------------------------------------------------------------------
456
+ async function testDeliverySimulation(agentUrl, options) {
457
+ const steps = [];
458
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
459
+ const controller = getController(options);
460
+ const profile = options._profile;
461
+ if (!controller?.detected || !(0, test_controller_1.supportsScenario)(controller, 'simulate_delivery')) {
462
+ steps.push({
463
+ step: 'Controller check',
464
+ task: 'comply_test_controller',
465
+ passed: true,
466
+ duration_ms: 0,
467
+ details: 'simulate_delivery not supported — skipping',
468
+ });
469
+ return { steps };
470
+ }
471
+ const ctrl = createTimedHelpers(client, options);
472
+ // Create a media buy to simulate delivery on
473
+ const { steps: createSteps, mediaBuyId } = await (0, media_buy_1.testCreateMediaBuy)(agentUrl, options);
474
+ steps.push(...createSteps);
475
+ if (!mediaBuyId) {
476
+ steps.push({
477
+ step: 'Find media buy for delivery simulation',
478
+ passed: false,
479
+ duration_ms: 0,
480
+ error: 'No media_buy_id — cannot test delivery simulation',
481
+ });
482
+ return { steps, profile };
483
+ }
484
+ // Simulate delivery
485
+ const { response: simResult, durationMs: simDur } = await ctrl.simulate('simulate_delivery', {
486
+ media_buy_id: mediaBuyId,
487
+ impressions: 10000,
488
+ clicks: 150,
489
+ reported_spend: { amount: 150.0, currency: 'USD' },
490
+ });
491
+ if (simResult.success) {
492
+ steps.push({
493
+ step: 'Simulate delivery data',
494
+ task: 'comply_test_controller',
495
+ passed: true,
496
+ duration_ms: simDur,
497
+ details: simResult.message || 'Delivery simulated',
498
+ response_preview: JSON.stringify(simResult.simulated, null, 2),
499
+ });
500
+ }
501
+ else {
502
+ steps.push({
503
+ step: 'Simulate delivery data',
504
+ task: 'comply_test_controller',
505
+ passed: false,
506
+ duration_ms: simDur,
507
+ error: `${simResult.error}: ${simResult.error_detail || ''}`,
508
+ });
509
+ return { steps, profile };
510
+ }
511
+ // Verify via get_media_buy_delivery
512
+ if (profile?.tools.includes('get_media_buy_delivery')) {
513
+ const { result: deliveryResult, step: deliveryStep } = await (0, client_1.runStep)('Verify delivery data via get_media_buy_delivery', 'get_media_buy_delivery', async () => callTask(client, 'get_media_buy_delivery', { media_buy_id: mediaBuyId, account: (0, client_1.resolveAccount)(options) }));
514
+ if (deliveryResult?.success && deliveryResult?.data) {
515
+ const data = deliveryResult.data;
516
+ // Handle multiple response shapes:
517
+ // - { media_buy_deliveries: [{ totals: { impressions } }] } (training agent)
518
+ // - { summary: { impressions } } or { impressions } (other agents)
519
+ const deliveries = data.media_buy_deliveries;
520
+ const totals = deliveries?.[0]?.totals;
521
+ const summary = data.summary;
522
+ const impressions = (totals?.impressions ??
523
+ data.impressions ??
524
+ data.total_impressions ??
525
+ summary?.impressions);
526
+ if (impressions !== undefined && impressions >= 10000) {
527
+ deliveryStep.details = `Delivery reflects simulated data: ${impressions} impressions`;
528
+ }
529
+ else {
530
+ deliveryStep.passed = false;
531
+ deliveryStep.error = `Expected ≥10000 impressions in delivery report, got ${impressions}`;
532
+ }
533
+ deliveryStep.response_preview = JSON.stringify({ impressions, raw_keys: Object.keys(data) }, null, 2);
534
+ }
535
+ steps.push(deliveryStep);
536
+ }
537
+ return { steps, profile };
538
+ }
539
+ // ---------------------------------------------------------------------------
540
+ // Budget Simulation
541
+ // ---------------------------------------------------------------------------
542
+ async function testBudgetSimulation(agentUrl, options) {
543
+ const steps = [];
544
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
545
+ const controller = getController(options);
546
+ const profile = options._profile;
547
+ if (!controller?.detected || !(0, test_controller_1.supportsScenario)(controller, 'simulate_budget_spend')) {
548
+ steps.push({
549
+ step: 'Controller check',
550
+ task: 'comply_test_controller',
551
+ passed: true,
552
+ duration_ms: 0,
553
+ details: 'simulate_budget_spend not supported — skipping',
554
+ });
555
+ return { steps };
556
+ }
557
+ const ctrl = createTimedHelpers(client, options);
558
+ // Create a media buy with a known budget
559
+ const { steps: createSteps, mediaBuyId } = await (0, media_buy_1.testCreateMediaBuy)(agentUrl, options);
560
+ steps.push(...createSteps);
561
+ if (!mediaBuyId) {
562
+ steps.push({
563
+ step: 'Find media buy for budget simulation',
564
+ passed: false,
565
+ duration_ms: 0,
566
+ error: 'No media_buy_id — cannot test budget simulation',
567
+ });
568
+ return { steps, profile };
569
+ }
570
+ // Simulate 95% spend
571
+ const { response: simResult, durationMs: simDur } = await ctrl.simulate('simulate_budget_spend', {
572
+ media_buy_id: mediaBuyId,
573
+ spend_percentage: 95,
574
+ });
575
+ if (simResult.success) {
576
+ steps.push({
577
+ step: 'Simulate budget spend to 95%',
578
+ task: 'comply_test_controller',
579
+ passed: true,
580
+ duration_ms: simDur,
581
+ details: simResult.message || 'Budget spend simulated to 95%',
582
+ response_preview: JSON.stringify(simResult.simulated, null, 2),
583
+ });
584
+ }
585
+ else {
586
+ steps.push({
587
+ step: 'Simulate budget spend to 95%',
588
+ task: 'comply_test_controller',
589
+ passed: false,
590
+ duration_ms: simDur,
591
+ error: `${simResult.error}: ${simResult.error_detail || ''}`,
592
+ });
593
+ return { steps, profile };
594
+ }
595
+ // Simulate 100% spend
596
+ const { response: depletedResult, durationMs: depletedDur } = await ctrl.simulate('simulate_budget_spend', {
597
+ media_buy_id: mediaBuyId,
598
+ spend_percentage: 100,
599
+ });
600
+ if (depletedResult.success) {
601
+ steps.push({
602
+ step: 'Simulate budget fully depleted (100%)',
603
+ task: 'comply_test_controller',
604
+ passed: true,
605
+ duration_ms: depletedDur,
606
+ details: depletedResult.message || 'Budget fully depleted',
607
+ response_preview: JSON.stringify(depletedResult.simulated, null, 2),
608
+ });
609
+ }
610
+ else {
611
+ steps.push({
612
+ step: 'Simulate budget fully depleted (100%)',
613
+ task: 'comply_test_controller',
614
+ passed: false,
615
+ duration_ms: depletedDur,
616
+ error: `${depletedResult.error}: ${depletedResult.error_detail || ''}`,
617
+ });
618
+ }
619
+ return { steps, profile };
620
+ }
621
+ // ---------------------------------------------------------------------------
622
+ // Controller Self-Validation
623
+ // ---------------------------------------------------------------------------
624
+ async function testControllerValidation(agentUrl, options) {
625
+ const steps = [];
626
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
627
+ const controller = getController(options);
628
+ if (!controller?.detected) {
629
+ steps.push({
630
+ step: 'Controller check',
631
+ task: 'comply_test_controller',
632
+ passed: true,
633
+ duration_ms: 0,
634
+ details: 'No test controller — skipping validation',
635
+ });
636
+ return { steps };
637
+ }
638
+ // Test 1: Unknown scenario → expect UNKNOWN_SCENARIO
639
+ const { result: unknownResult, step: unknownStep } = await (0, client_1.runStep)('Unknown scenario returns UNKNOWN_SCENARIO', 'comply_test_controller', async () => (0, test_controller_1.callControllerRaw)(client, {
640
+ scenario: 'nonexistent_scenario',
641
+ params: {},
642
+ }, options));
643
+ const unknownData = unknownResult?.data;
644
+ if (unknownData && !unknownData.success && unknownData.error === 'UNKNOWN_SCENARIO') {
645
+ unknownStep.details = 'Correctly returned UNKNOWN_SCENARIO for nonexistent_scenario';
646
+ }
647
+ else {
648
+ unknownStep.passed = false;
649
+ unknownStep.error = `Expected UNKNOWN_SCENARIO error, got: ${JSON.stringify(unknownData)}`;
650
+ }
651
+ steps.push(unknownStep);
652
+ // Test 2: Missing required params → expect INVALID_PARAMS
653
+ if ((0, test_controller_1.supportsScenario)(controller, 'force_creative_status')) {
654
+ const start = Date.now();
655
+ const missingResult = await (0, test_controller_1.forceStatus)(client, 'force_creative_status', {}, options);
656
+ const dur = Date.now() - start;
657
+ if (!missingResult.success && missingResult.error === 'INVALID_PARAMS') {
658
+ steps.push({
659
+ step: 'Missing params returns INVALID_PARAMS',
660
+ task: 'comply_test_controller',
661
+ passed: true,
662
+ duration_ms: dur,
663
+ details: 'Correctly returned INVALID_PARAMS for missing creative_id/status',
664
+ });
665
+ }
666
+ else {
667
+ steps.push({
668
+ step: 'Missing params returns INVALID_PARAMS',
669
+ task: 'comply_test_controller',
670
+ passed: false,
671
+ duration_ms: dur,
672
+ error: `Expected INVALID_PARAMS, got: ${!missingResult.success ? missingResult.error : 'success'}`,
673
+ });
674
+ }
675
+ }
676
+ // Test 3: NOT_FOUND for nonexistent entity
677
+ if ((0, test_controller_1.supportsScenario)(controller, 'force_creative_status')) {
678
+ const start = Date.now();
679
+ const notFoundResult = await (0, test_controller_1.forceStatus)(client, 'force_creative_status', {
680
+ creative_id: 'comply-test-nonexistent-000000000000',
681
+ status: 'approved',
682
+ }, options);
683
+ const dur = Date.now() - start;
684
+ if (!notFoundResult.success && notFoundResult.error === 'NOT_FOUND') {
685
+ steps.push({
686
+ step: 'Nonexistent entity returns NOT_FOUND',
687
+ task: 'comply_test_controller',
688
+ passed: true,
689
+ duration_ms: dur,
690
+ details: 'Correctly returned NOT_FOUND for nonexistent creative',
691
+ });
692
+ }
693
+ else {
694
+ steps.push({
695
+ step: 'Nonexistent entity returns NOT_FOUND',
696
+ task: 'comply_test_controller',
697
+ passed: false,
698
+ duration_ms: dur,
699
+ error: `Expected NOT_FOUND, got: ${!notFoundResult.success ? notFoundResult.error : 'success'}`,
700
+ });
701
+ }
702
+ }
703
+ return { steps };
704
+ }
705
+ //# sourceMappingURL=deterministic.js.map