@adcp/client 4.14.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 (161) hide show
  1. package/bin/adcp.js +2 -0
  2. package/dist/lib/adapters/governance-adapter.d.ts +2 -2
  3. package/dist/lib/adapters/governance-adapter.d.ts.map +1 -1
  4. package/dist/lib/adapters/governance-adapter.js +1 -3
  5. package/dist/lib/adapters/governance-adapter.js.map +1 -1
  6. package/dist/lib/adapters/si-session-manager.d.ts.map +1 -1
  7. package/dist/lib/adapters/si-session-manager.js +8 -3
  8. package/dist/lib/adapters/si-session-manager.js.map +1 -1
  9. package/dist/lib/agents/index.generated.d.ts +9 -1
  10. package/dist/lib/agents/index.generated.d.ts.map +1 -1
  11. package/dist/lib/agents/index.generated.js +12 -0
  12. package/dist/lib/agents/index.generated.js.map +1 -1
  13. package/dist/lib/core/ADCPMultiAgentClient.d.ts.map +1 -1
  14. package/dist/lib/core/ADCPMultiAgentClient.js +10 -3
  15. package/dist/lib/core/ADCPMultiAgentClient.js.map +1 -1
  16. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  17. package/dist/lib/core/AsyncHandler.d.ts.map +1 -1
  18. package/dist/lib/core/AsyncHandler.js +1 -1
  19. package/dist/lib/core/AsyncHandler.js.map +1 -1
  20. package/dist/lib/core/GovernanceMiddleware.d.ts +2 -10
  21. package/dist/lib/core/GovernanceMiddleware.d.ts.map +1 -1
  22. package/dist/lib/core/GovernanceMiddleware.js +8 -51
  23. package/dist/lib/core/GovernanceMiddleware.js.map +1 -1
  24. package/dist/lib/core/GovernanceTypes.d.ts +4 -4
  25. package/dist/lib/core/GovernanceTypes.d.ts.map +1 -1
  26. package/dist/lib/core/GovernanceTypes.js +1 -0
  27. package/dist/lib/core/GovernanceTypes.js.map +1 -1
  28. package/dist/lib/core/SingleAgentClient.d.ts +1 -1
  29. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  30. package/dist/lib/core/SingleAgentClient.js +7 -4
  31. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  32. package/dist/lib/core/TaskExecutor.d.ts +4 -0
  33. package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
  34. package/dist/lib/core/TaskExecutor.js +61 -18
  35. package/dist/lib/core/TaskExecutor.js.map +1 -1
  36. package/dist/lib/index.d.ts +5 -3
  37. package/dist/lib/index.d.ts.map +1 -1
  38. package/dist/lib/index.js +12 -6
  39. package/dist/lib/index.js.map +1 -1
  40. package/dist/lib/protocols/index.d.ts +1 -0
  41. package/dist/lib/protocols/index.d.ts.map +1 -1
  42. package/dist/lib/protocols/index.js +14 -4
  43. package/dist/lib/protocols/index.js.map +1 -1
  44. package/dist/lib/protocols/mcp-tasks.d.ts +55 -0
  45. package/dist/lib/protocols/mcp-tasks.d.ts.map +1 -0
  46. package/dist/lib/protocols/mcp-tasks.js +334 -0
  47. package/dist/lib/protocols/mcp-tasks.js.map +1 -0
  48. package/dist/lib/protocols/mcp.d.ts +9 -0
  49. package/dist/lib/protocols/mcp.d.ts.map +1 -1
  50. package/dist/lib/protocols/mcp.js +4 -0
  51. package/dist/lib/protocols/mcp.js.map +1 -1
  52. package/dist/lib/registry/types.generated.d.ts +9 -9
  53. package/dist/lib/registry/types.generated.d.ts.map +1 -1
  54. package/dist/lib/registry/types.generated.js +1 -1
  55. package/dist/lib/server/index.d.ts +2 -0
  56. package/dist/lib/server/index.d.ts.map +1 -1
  57. package/dist/lib/server/index.js +7 -1
  58. package/dist/lib/server/index.js.map +1 -1
  59. package/dist/lib/server/tasks.d.ts +86 -0
  60. package/dist/lib/server/tasks.d.ts.map +1 -0
  61. package/dist/lib/server/tasks.js +110 -0
  62. package/dist/lib/server/tasks.js.map +1 -0
  63. package/dist/lib/testing/agent-tester.d.ts +1 -1
  64. package/dist/lib/testing/agent-tester.d.ts.map +1 -1
  65. package/dist/lib/testing/agent-tester.js +52 -2
  66. package/dist/lib/testing/agent-tester.js.map +1 -1
  67. package/dist/lib/testing/client.d.ts +18 -0
  68. package/dist/lib/testing/client.d.ts.map +1 -1
  69. package/dist/lib/testing/client.js +39 -1
  70. package/dist/lib/testing/client.js.map +1 -1
  71. package/dist/lib/testing/compliance/comply.d.ts +4 -0
  72. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  73. package/dist/lib/testing/compliance/comply.js +406 -173
  74. package/dist/lib/testing/compliance/comply.js.map +1 -1
  75. package/dist/lib/testing/compliance/types.d.ts +7 -1
  76. package/dist/lib/testing/compliance/types.d.ts.map +1 -1
  77. package/dist/lib/testing/index.d.ts +3 -1
  78. package/dist/lib/testing/index.d.ts.map +1 -1
  79. package/dist/lib/testing/index.js +13 -2
  80. package/dist/lib/testing/index.js.map +1 -1
  81. package/dist/lib/testing/orchestrator.d.ts +4 -0
  82. package/dist/lib/testing/orchestrator.d.ts.map +1 -1
  83. package/dist/lib/testing/orchestrator.js +19 -2
  84. package/dist/lib/testing/orchestrator.js.map +1 -1
  85. package/dist/lib/testing/scenarios/capabilities.js +2 -2
  86. package/dist/lib/testing/scenarios/capabilities.js.map +1 -1
  87. package/dist/lib/testing/scenarios/creative.js +4 -4
  88. package/dist/lib/testing/scenarios/creative.js.map +1 -1
  89. package/dist/lib/testing/scenarios/deterministic.d.ts +37 -0
  90. package/dist/lib/testing/scenarios/deterministic.d.ts.map +1 -0
  91. package/dist/lib/testing/scenarios/deterministic.js +705 -0
  92. package/dist/lib/testing/scenarios/deterministic.js.map +1 -0
  93. package/dist/lib/testing/scenarios/discovery.js +2 -2
  94. package/dist/lib/testing/scenarios/discovery.js.map +1 -1
  95. package/dist/lib/testing/scenarios/edge-cases.d.ts.map +1 -1
  96. package/dist/lib/testing/scenarios/edge-cases.js +18 -25
  97. package/dist/lib/testing/scenarios/edge-cases.js.map +1 -1
  98. package/dist/lib/testing/scenarios/error-compliance.d.ts.map +1 -1
  99. package/dist/lib/testing/scenarios/error-compliance.js +9 -13
  100. package/dist/lib/testing/scenarios/error-compliance.js.map +1 -1
  101. package/dist/lib/testing/scenarios/governance.d.ts +15 -0
  102. package/dist/lib/testing/scenarios/governance.d.ts.map +1 -1
  103. package/dist/lib/testing/scenarios/governance.js +386 -49
  104. package/dist/lib/testing/scenarios/governance.js.map +1 -1
  105. package/dist/lib/testing/scenarios/health.js +2 -2
  106. package/dist/lib/testing/scenarios/health.js.map +1 -1
  107. package/dist/lib/testing/scenarios/index.d.ts +2 -1
  108. package/dist/lib/testing/scenarios/index.d.ts.map +1 -1
  109. package/dist/lib/testing/scenarios/index.js +12 -1
  110. package/dist/lib/testing/scenarios/index.js.map +1 -1
  111. package/dist/lib/testing/scenarios/media-buy.d.ts.map +1 -1
  112. package/dist/lib/testing/scenarios/media-buy.js +258 -31
  113. package/dist/lib/testing/scenarios/media-buy.js.map +1 -1
  114. package/dist/lib/testing/scenarios/schema-compliance.js +2 -2
  115. package/dist/lib/testing/scenarios/schema-compliance.js.map +1 -1
  116. package/dist/lib/testing/scenarios/signals.d.ts.map +1 -1
  117. package/dist/lib/testing/scenarios/signals.js +35 -2
  118. package/dist/lib/testing/scenarios/signals.js.map +1 -1
  119. package/dist/lib/testing/scenarios/sponsored-intelligence.d.ts.map +1 -1
  120. package/dist/lib/testing/scenarios/sponsored-intelligence.js +8 -7
  121. package/dist/lib/testing/scenarios/sponsored-intelligence.js.map +1 -1
  122. package/dist/lib/testing/stubs/governance-agent-stub.d.ts +72 -0
  123. package/dist/lib/testing/stubs/governance-agent-stub.d.ts.map +1 -0
  124. package/dist/lib/testing/stubs/governance-agent-stub.js +295 -0
  125. package/dist/lib/testing/stubs/governance-agent-stub.js.map +1 -0
  126. package/dist/lib/testing/stubs/index.d.ts +3 -0
  127. package/dist/lib/testing/stubs/index.d.ts.map +1 -0
  128. package/dist/lib/testing/stubs/index.js +6 -0
  129. package/dist/lib/testing/stubs/index.js.map +1 -0
  130. package/dist/lib/testing/test-controller.d.ts +46 -0
  131. package/dist/lib/testing/test-controller.d.ts.map +1 -0
  132. package/dist/lib/testing/test-controller.js +143 -0
  133. package/dist/lib/testing/test-controller.js.map +1 -0
  134. package/dist/lib/testing/types.d.ts +8 -1
  135. package/dist/lib/testing/types.d.ts.map +1 -1
  136. package/dist/lib/types/core.generated.d.ts +562 -97
  137. package/dist/lib/types/core.generated.d.ts.map +1 -1
  138. package/dist/lib/types/core.generated.js +1 -1
  139. package/dist/lib/types/error-codes.d.ts +4 -4
  140. package/dist/lib/types/error-codes.d.ts.map +1 -1
  141. package/dist/lib/types/error-codes.js +26 -2
  142. package/dist/lib/types/error-codes.js.map +1 -1
  143. package/dist/lib/types/schemas.generated.d.ts +4625 -8682
  144. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  145. package/dist/lib/types/schemas.generated.js +711 -403
  146. package/dist/lib/types/schemas.generated.js.map +1 -1
  147. package/dist/lib/types/tools.generated.d.ts +1188 -405
  148. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  149. package/dist/lib/utils/response-schemas.d.ts.map +1 -1
  150. package/dist/lib/utils/response-schemas.js +2 -0
  151. package/dist/lib/utils/response-schemas.js.map +1 -1
  152. package/dist/lib/utils/response-unwrapper.d.ts.map +1 -1
  153. package/dist/lib/utils/response-unwrapper.js +12 -0
  154. package/dist/lib/utils/response-unwrapper.js.map +1 -1
  155. package/dist/lib/utils/union-errors.d.ts +16 -0
  156. package/dist/lib/utils/union-errors.d.ts.map +1 -0
  157. package/dist/lib/utils/union-errors.js +34 -0
  158. package/dist/lib/utils/union-errors.js.map +1 -0
  159. package/dist/lib/version.d.ts +3 -3
  160. package/dist/lib/version.js +3 -3
  161. package/package.json +1 -1
@@ -47,13 +47,21 @@ const client_1 = require("../client");
47
47
  const orchestrator_1 = require("../orchestrator");
48
48
  const profiles_1 = require("./profiles");
49
49
  const mcp_1 = require("../../protocols/mcp");
50
+ const test_controller_1 = require("../test-controller");
50
51
  /**
51
52
  * Maps each track to its constituent scenarios and a human-readable label.
52
53
  */
53
54
  const TRACK_DEFINITIONS = {
54
55
  core: {
55
56
  label: 'Core Protocol',
56
- scenarios: ['health_check', 'discovery', 'capability_discovery', 'schema_compliance'],
57
+ scenarios: [
58
+ 'health_check',
59
+ 'discovery',
60
+ 'capability_discovery',
61
+ 'schema_compliance',
62
+ 'controller_validation',
63
+ 'deterministic_account',
64
+ ],
57
65
  },
58
66
  products: {
59
67
  label: 'Product Discovery',
@@ -69,29 +77,41 @@ const TRACK_DEFINITIONS = {
69
77
  'media_buy_lifecycle',
70
78
  'terminal_state_enforcement',
71
79
  'package_lifecycle',
80
+ 'seller_governance_context',
81
+ 'deterministic_media_buy',
82
+ 'deterministic_budget',
72
83
  ],
73
84
  },
74
85
  creative: {
75
86
  label: 'Creative Management',
76
- scenarios: ['creative_sync', 'creative_flow'],
87
+ scenarios: ['creative_sync', 'creative_flow', 'deterministic_creative'],
77
88
  },
78
89
  reporting: {
79
90
  label: 'Reporting',
80
91
  // full_sales_flow covers get_media_buy_delivery — but we assess it as a
81
92
  // separate track concern by checking if the agent has the tool
82
- scenarios: ['full_sales_flow'],
93
+ scenarios: ['full_sales_flow', 'deterministic_delivery'],
83
94
  },
84
95
  governance: {
85
96
  label: 'Governance',
86
97
  scenarios: ['governance_property_lists', 'governance_content_standards', 'property_list_filters'],
87
98
  },
99
+ campaign_governance: {
100
+ label: 'Campaign Governance',
101
+ scenarios: [
102
+ 'campaign_governance',
103
+ 'campaign_governance_denied',
104
+ 'campaign_governance_conditions',
105
+ 'campaign_governance_delivery',
106
+ ],
107
+ },
88
108
  signals: {
89
109
  label: 'Signals',
90
110
  scenarios: ['signals_flow'],
91
111
  },
92
112
  si: {
93
113
  label: 'Sponsored Intelligence',
94
- scenarios: ['si_session_lifecycle', 'si_availability', 'si_handoff'],
114
+ scenarios: ['si_session_lifecycle', 'si_availability', 'si_handoff', 'deterministic_session'],
95
115
  },
96
116
  audiences: {
97
117
  label: 'Audience Management',
@@ -113,6 +133,7 @@ const TRACK_RELEVANCE = {
113
133
  creative: ['sync_creatives', 'build_creative', 'list_creative_formats'],
114
134
  reporting: ['get_media_buy_delivery'],
115
135
  governance: ['create_property_list', 'list_content_standards'],
136
+ campaign_governance: ['sync_plans', 'check_governance'],
116
137
  signals: ['get_signals'],
117
138
  si: ['si_initiate_session'],
118
139
  audiences: ['sync_audiences'],
@@ -125,6 +146,7 @@ const TRACK_ORDER = [
125
146
  'creative',
126
147
  'reporting',
127
148
  'governance',
149
+ 'campaign_governance',
128
150
  'signals',
129
151
  'si',
130
152
  'audiences',
@@ -248,8 +270,11 @@ function collectObservations(track, results, profile) {
248
270
  }
249
271
  // Media buy track observations
250
272
  if (track === 'media_buy') {
251
- // Check for valid_actions support
273
+ // Check for valid_actions support (first match only)
274
+ let checkedValidActions = false;
252
275
  for (const result of results) {
276
+ if (checkedValidActions)
277
+ break;
253
278
  for (const step of result.steps ?? []) {
254
279
  if (step.task === 'get_media_buys' && step.response_preview) {
255
280
  try {
@@ -257,12 +282,135 @@ function collectObservations(track, results, profile) {
257
282
  if (preview.valid_actions === undefined || preview.valid_actions === null) {
258
283
  observations.push({
259
284
  category: 'best_practice',
260
- severity: 'suggestion',
285
+ severity: 'warning',
261
286
  track,
262
287
  message: 'Agent does not return valid_actions in get_media_buys response. ' +
263
- 'valid_actions eliminates the need for buyers to internalize the state machine.',
288
+ 'Without valid_actions, buyer agents must hardcode the state machine to know what operations are permitted.',
289
+ });
290
+ }
291
+ // Check creative_deadline support
292
+ if (preview.has_creative_deadline === false) {
293
+ observations.push({
294
+ category: 'best_practice',
295
+ severity: 'suggestion',
296
+ track,
297
+ message: 'Agent does not return creative_deadline on media buys or packages. ' +
298
+ 'Buyers need to know when creative uploads must be finalized to avoid rejected submissions.',
299
+ });
300
+ }
301
+ // Check history entry shape when present
302
+ if (preview.history_entries && preview.history_entries > 0 && preview.history_valid === false) {
303
+ observations.push({
304
+ category: 'best_practice',
305
+ severity: 'warning',
306
+ track,
307
+ message: 'Agent returns history entries but some lack required fields (timestamp, action). ' +
308
+ 'History entries must include at least timestamp and action to be useful for audit.',
309
+ });
310
+ }
311
+ // Check dry_run/sandbox confirmation
312
+ if (preview.sandbox === undefined || preview.sandbox === null) {
313
+ observations.push({
314
+ category: 'best_practice',
315
+ severity: 'suggestion',
316
+ track,
317
+ message: 'Agent does not confirm sandbox mode in get_media_buys response. ' +
318
+ 'Include sandbox: true so buyers can verify the agent honored dry_run mode.',
319
+ });
320
+ }
321
+ checkedValidActions = true;
322
+ }
323
+ catch {
324
+ // not always JSON
325
+ }
326
+ break;
327
+ }
328
+ }
329
+ }
330
+ // Check for confirmed_at and revision in create_media_buy responses (first match only)
331
+ let checkedCreateLifecycle = false;
332
+ for (const result of results) {
333
+ if (checkedCreateLifecycle)
334
+ break;
335
+ for (const step of result.steps ?? []) {
336
+ if (step.task === 'create_media_buy' && step.response_preview) {
337
+ try {
338
+ const preview = JSON.parse(step.response_preview);
339
+ if (preview.confirmed_at === undefined || preview.confirmed_at === null) {
340
+ observations.push({
341
+ category: 'best_practice',
342
+ severity: 'warning',
343
+ track,
344
+ message: 'Agent does not return confirmed_at in create_media_buy response. ' +
345
+ 'A successful response constitutes order confirmation — confirmed_at provides an auditable timestamp for dispute resolution.',
346
+ });
347
+ }
348
+ if (preview.revision === undefined || preview.revision === null) {
349
+ observations.push({
350
+ category: 'best_practice',
351
+ severity: 'suggestion',
352
+ track,
353
+ message: 'Agent does not return revision in create_media_buy response. ' +
354
+ 'Revision numbers enable optimistic concurrency for safe concurrent updates.',
264
355
  });
265
356
  }
357
+ checkedCreateLifecycle = true;
358
+ }
359
+ catch {
360
+ // not always JSON
361
+ }
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ // Check for history support in get_media_buys responses (first match only)
367
+ let checkedHistory = false;
368
+ for (const result of results) {
369
+ if (checkedHistory)
370
+ break;
371
+ for (const step of result.steps ?? []) {
372
+ if (step.task === 'get_media_buys' && step.response_preview) {
373
+ try {
374
+ const preview = JSON.parse(step.response_preview);
375
+ if (preview.history_entries !== undefined && preview.history_entries === 0) {
376
+ observations.push({
377
+ category: 'best_practice',
378
+ severity: 'suggestion',
379
+ track,
380
+ message: 'Agent does not return revision history when include_history is requested. ' +
381
+ 'History enables audit trails and helps buyers understand what changed.',
382
+ });
383
+ }
384
+ checkedHistory = true;
385
+ }
386
+ catch {
387
+ // not always JSON
388
+ }
389
+ break;
390
+ }
391
+ }
392
+ }
393
+ // Check canceled_by validation on canceled media buys (first match only)
394
+ let checkedCancellation = false;
395
+ for (const result of results) {
396
+ if (checkedCancellation)
397
+ break;
398
+ for (const step of result.steps ?? []) {
399
+ if (step.task === 'update_media_buy' && step.response_preview) {
400
+ try {
401
+ const preview = JSON.parse(step.response_preview);
402
+ if (preview.status === 'canceled') {
403
+ if (!preview.canceled_by) {
404
+ observations.push({
405
+ category: 'completeness',
406
+ severity: 'warning',
407
+ track,
408
+ message: 'Agent transitions to canceled status but does not include canceled_by field. ' +
409
+ 'Buyers need to distinguish buyer-initiated from seller-initiated cancellations.',
410
+ });
411
+ }
412
+ checkedCancellation = true;
413
+ }
266
414
  }
267
415
  catch {
268
416
  // not always JSON
@@ -322,6 +470,34 @@ function collectObservations(track, results, profile) {
322
470
  }
323
471
  }
324
472
  }
473
+ // Campaign governance track observations
474
+ if (track === 'campaign_governance') {
475
+ let anyCheckMissingContext = false;
476
+ for (const result of results) {
477
+ for (const step of result.steps ?? []) {
478
+ if (step.task === 'check_governance' && step.passed && step.response_preview) {
479
+ try {
480
+ const preview = JSON.parse(step.response_preview);
481
+ if (!preview.governance_context || preview.governance_context === '(absent)') {
482
+ anyCheckMissingContext = true;
483
+ }
484
+ }
485
+ catch {
486
+ // not always JSON
487
+ }
488
+ }
489
+ }
490
+ }
491
+ if (anyCheckMissingContext) {
492
+ observations.push({
493
+ category: 'best_practice',
494
+ severity: 'warning',
495
+ track,
496
+ message: 'Governance agent did not return governance_context on check_governance response. ' +
497
+ 'Without it, sellers cannot maintain governance continuity across the media buy lifecycle.',
498
+ });
499
+ }
500
+ }
325
501
  // Check for slow responses
326
502
  for (const result of results) {
327
503
  for (const step of result.steps ?? []) {
@@ -352,196 +528,253 @@ async function comply(agentUrl, options = {}) {
352
528
  }
353
529
  async function complyImpl(agentUrl, options) {
354
530
  const start = Date.now();
355
- const { tracks: trackFilter, platform_type, ...testOptions } = options;
531
+ const { tracks: trackFilter, platform_type, timeout_ms, signal: externalSignal, ...testOptions } = options;
356
532
  const platformProfile = platform_type ? (0, profiles_1.getPlatformProfile)(platform_type) : undefined;
357
- const effectiveOptions = {
358
- ...testOptions,
359
- dry_run: testOptions.dry_run !== false,
360
- test_session_id: testOptions.test_session_id || `comply-${Date.now()}`,
361
- };
362
- // Discover agent capabilities first
363
- const client = (0, client_1.createTestClient)(agentUrl, effectiveOptions.protocol ?? 'mcp', effectiveOptions);
364
- const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
365
- if (!profileStep.passed) {
366
- const errorMsg = profileStep.error || 'Unknown error';
367
- const observations = [];
368
- // Check for auth errors either explicit 401/Unauthorized or MCP SDK's generic
369
- // "Failed to discover" which often wraps a 401
370
- const isExplicitAuthError = errorMsg.includes('401') ||
371
- errorMsg.includes('Unauthorized') ||
372
- errorMsg.includes('unauthorized') ||
373
- errorMsg.includes('authentication') ||
374
- errorMsg.includes('JWS') ||
375
- errorMsg.includes('JWT') ||
376
- errorMsg.includes('signature verification');
377
- // When MCP SDK wraps the error, probe the endpoint directly
378
- let isAuthError = isExplicitAuthError;
379
- if (!isAuthError && errorMsg.includes('Failed to discover')) {
380
- try {
381
- const probe = await fetch(agentUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
382
- if (probe.status === 401 || probe.status === 403) {
383
- isAuthError = true;
533
+ // Validate timeout_ms
534
+ if (timeout_ms !== undefined) {
535
+ if (typeof timeout_ms !== 'number' || !Number.isFinite(timeout_ms) || timeout_ms <= 0) {
536
+ throw new TypeError(`timeout_ms must be a positive finite number, got: ${timeout_ms}`);
537
+ }
538
+ }
539
+ // Build a combined AbortSignal from timeout_ms and/or external signal
540
+ const needsAbort = timeout_ms !== undefined || externalSignal !== undefined;
541
+ const abortController = needsAbort ? new AbortController() : undefined;
542
+ let timeoutId;
543
+ const onExternalAbort = externalSignal ? () => abortController.abort(externalSignal.reason) : undefined;
544
+ if (timeout_ms !== undefined && abortController) {
545
+ timeoutId = setTimeout(() => abortController.abort(new Error(`comply() timed out after ${timeout_ms}ms`)), timeout_ms);
546
+ }
547
+ if (externalSignal && abortController) {
548
+ if (externalSignal.aborted) {
549
+ abortController.abort(externalSignal.reason);
550
+ }
551
+ else {
552
+ externalSignal.addEventListener('abort', onExternalAbort, { once: true });
553
+ }
554
+ }
555
+ const signal = abortController?.signal;
556
+ try {
557
+ const effectiveOptions = {
558
+ ...testOptions,
559
+ dry_run: testOptions.dry_run !== false,
560
+ test_session_id: testOptions.test_session_id || `comply-${Date.now()}`,
561
+ };
562
+ // Check for abort before starting
563
+ signal?.throwIfAborted();
564
+ // Discover agent capabilities once and share across all scenarios
565
+ const client = (0, client_1.createTestClient)(agentUrl, effectiveOptions.protocol ?? 'mcp', effectiveOptions);
566
+ const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
567
+ effectiveOptions._client = client;
568
+ effectiveOptions._profile = profile;
569
+ // Detect test controller for deterministic mode
570
+ let controllerDetection = { detected: false };
571
+ if (profileStep.passed && (0, test_controller_1.hasTestController)(profile)) {
572
+ controllerDetection = await (0, test_controller_1.detectController)(client, profile, effectiveOptions);
573
+ if (controllerDetection.detected) {
574
+ effectiveOptions._controllerCapabilities = controllerDetection;
575
+ }
576
+ }
577
+ if (!profileStep.passed) {
578
+ const errorMsg = profileStep.error || 'Unknown error';
579
+ const observations = [];
580
+ // Check for auth errors — either explicit 401/Unauthorized or MCP SDK's generic
581
+ // "Failed to discover" which often wraps a 401
582
+ const isExplicitAuthError = errorMsg.includes('401') ||
583
+ errorMsg.includes('Unauthorized') ||
584
+ errorMsg.includes('unauthorized') ||
585
+ errorMsg.includes('authentication') ||
586
+ errorMsg.includes('JWS') ||
587
+ errorMsg.includes('JWT') ||
588
+ errorMsg.includes('signature verification');
589
+ // When MCP SDK wraps the error, probe the endpoint directly
590
+ let isAuthError = isExplicitAuthError;
591
+ if (!isAuthError && errorMsg.includes('Failed to discover')) {
592
+ try {
593
+ const probe = await fetch(agentUrl, {
594
+ method: 'POST',
595
+ headers: { 'Content-Type': 'application/json' },
596
+ signal,
597
+ });
598
+ if (probe.status === 401 || probe.status === 403) {
599
+ isAuthError = true;
600
+ }
601
+ }
602
+ catch {
603
+ // Network error — not an auth issue
384
604
  }
385
605
  }
386
- catch {
387
- // Network error — not an auth issue
606
+ const headline = isAuthError ? `Authentication required` : `Agent unreachable — ${errorMsg}`;
607
+ if (isAuthError) {
608
+ // Check if agent supports OAuth
609
+ const { discoverOAuthMetadata } = await Promise.resolve().then(() => __importStar(require('../../auth/oauth/discovery')));
610
+ const oauthMeta = await discoverOAuthMetadata(agentUrl);
611
+ if (oauthMeta) {
612
+ observations.push({
613
+ category: 'auth',
614
+ severity: 'error',
615
+ message: `Agent requires OAuth (issuer: ${oauthMeta.issuer || 'unknown'}). Save credentials: adcp --save-auth <alias> ${agentUrl} --oauth`,
616
+ });
617
+ }
618
+ else {
619
+ observations.push({
620
+ category: 'auth',
621
+ severity: 'error',
622
+ message: 'Agent returned 401. Check your --auth token.',
623
+ });
624
+ }
388
625
  }
626
+ return {
627
+ agent_url: agentUrl,
628
+ agent_profile: profile,
629
+ tracks: [],
630
+ summary: {
631
+ tracks_passed: 0,
632
+ tracks_failed: 0,
633
+ tracks_skipped: 0,
634
+ tracks_partial: 0,
635
+ tracks_expected: 0,
636
+ headline,
637
+ },
638
+ observations,
639
+ tested_at: new Date().toISOString(),
640
+ total_duration_ms: Date.now() - start,
641
+ dry_run: effectiveOptions.dry_run !== false,
642
+ };
389
643
  }
390
- const headline = isAuthError ? `Authentication required` : `Agent unreachable — ${errorMsg}`;
391
- if (isAuthError) {
392
- // Check if agent supports OAuth
393
- const { discoverOAuthMetadata } = await Promise.resolve().then(() => __importStar(require('../../auth/oauth/discovery')));
394
- const oauthMeta = await discoverOAuthMetadata(agentUrl);
395
- if (oauthMeta) {
396
- observations.push({
397
- category: 'auth',
398
- severity: 'error',
399
- message: `Agent requires OAuth (issuer: ${oauthMeta.issuer || 'unknown'}). Save credentials: adcp --save-auth <alias> ${agentUrl} --oauth`,
644
+ const tracksToRun = trackFilter ?? TRACK_ORDER;
645
+ const trackResults = [];
646
+ const allObservations = [];
647
+ for (const track of tracksToRun) {
648
+ // Check for abort between tracks
649
+ signal?.throwIfAborted();
650
+ const def = TRACK_DEFINITIONS[track];
651
+ if (!def)
652
+ continue;
653
+ if (!isTrackApplicable(track, profile.tools)) {
654
+ const isExpected = track !== 'core' && (platformProfile?.expected_tracks.includes(track) ?? false);
655
+ trackResults.push({
656
+ track,
657
+ status: isExpected ? 'expected' : 'skip',
658
+ label: def.label,
659
+ scenarios: [],
660
+ skipped_scenarios: def.scenarios,
661
+ observations: [],
662
+ duration_ms: 0,
400
663
  });
664
+ continue;
401
665
  }
402
- else {
403
- observations.push({
404
- category: 'auth',
405
- severity: 'error',
406
- message: 'Agent returned 401. Check your --auth token.',
666
+ const trackStart = Date.now();
667
+ const applicable = (0, orchestrator_1.getApplicableScenarios)(profile.tools, def.scenarios);
668
+ const skipped = def.scenarios.filter(s => !applicable.includes(s));
669
+ // Track is relevant (agent has some related tools) but no scenarios match
670
+ // the specific tool combinations. Report as pass with an observation.
671
+ if (applicable.length === 0) {
672
+ const relevantTools = TRACK_RELEVANCE[track].filter(t => profile.tools.includes(t));
673
+ const observations = [
674
+ {
675
+ category: 'completeness',
676
+ severity: 'info',
677
+ track,
678
+ message: `Agent has ${relevantTools.join(', ')} but no test scenarios cover this tool combination. ` +
679
+ `Compliance tests exist for: ${def.scenarios.join(', ')}.`,
680
+ evidence: { tools_present: relevantTools, scenarios_available: def.scenarios },
681
+ },
682
+ ];
683
+ allObservations.push(...observations);
684
+ trackResults.push({
685
+ track,
686
+ status: 'pass',
687
+ label: def.label,
688
+ scenarios: [],
689
+ skipped_scenarios: skipped,
690
+ observations,
691
+ duration_ms: Date.now() - trackStart,
407
692
  });
693
+ continue;
408
694
  }
409
- }
410
- return {
411
- agent_url: agentUrl,
412
- agent_profile: profile,
413
- tracks: [],
414
- summary: {
415
- tracks_passed: 0,
416
- tracks_failed: 0,
417
- tracks_skipped: 0,
418
- tracks_partial: 0,
419
- tracks_expected: 0,
420
- headline,
421
- },
422
- observations,
423
- tested_at: new Date().toISOString(),
424
- total_duration_ms: Date.now() - start,
425
- dry_run: effectiveOptions.dry_run !== false,
426
- };
427
- }
428
- const tracksToRun = trackFilter ?? TRACK_ORDER;
429
- const trackResults = [];
430
- const allObservations = [];
431
- for (const track of tracksToRun) {
432
- const def = TRACK_DEFINITIONS[track];
433
- if (!def)
434
- continue;
435
- if (!isTrackApplicable(track, profile.tools)) {
436
- const isExpected = track !== 'core' && (platformProfile?.expected_tracks.includes(track) ?? false);
437
- trackResults.push({
438
- track,
439
- status: isExpected ? 'expected' : 'skip',
440
- label: def.label,
441
- scenarios: [],
442
- skipped_scenarios: def.scenarios,
443
- observations: [],
444
- duration_ms: 0,
445
- });
446
- continue;
447
- }
448
- const trackStart = Date.now();
449
- const applicable = (0, orchestrator_1.getApplicableScenarios)(profile.tools, def.scenarios);
450
- const skipped = def.scenarios.filter(s => !applicable.includes(s));
451
- // Track is relevant (agent has some related tools) but no scenarios match
452
- // the specific tool combinations. Report as pass with an observation.
453
- if (applicable.length === 0) {
454
- const relevantTools = TRACK_RELEVANCE[track].filter(t => profile.tools.includes(t));
455
- const observations = [
456
- {
457
- category: 'completeness',
695
+ // Run each applicable scenario for this track
696
+ const results = [];
697
+ for (const scenario of applicable) {
698
+ // Check for abort between scenarios
699
+ signal?.throwIfAborted();
700
+ const result = await (0, agent_tester_1.testAgent)(agentUrl, scenario, effectiveOptions);
701
+ results.push(result);
702
+ }
703
+ const observations = collectObservations(track, results, profile);
704
+ // Detect auth-only failures when running without auth
705
+ const hasAuth = !!effectiveOptions.auth;
706
+ const authSkippedScenarios = !hasAuth ? results.filter(r => isAuthOnlyFailure(r)).map(r => r.scenario) : [];
707
+ if (authSkippedScenarios.length > 0) {
708
+ observations.push({
709
+ category: 'auth',
458
710
  severity: 'info',
459
711
  track,
460
- message: `Agent has ${relevantTools.join(', ')} but no test scenarios cover this tool combination. ` +
461
- `Compliance tests exist for: ${def.scenarios.join(', ')}.`,
462
- evidence: { tools_present: relevantTools, scenarios_available: def.scenarios },
463
- },
464
- ];
712
+ message: `${authSkippedScenarios.length} scenario(s) require authentication: ${authSkippedScenarios.join(', ')}. ` +
713
+ `Re-run with --auth to test.`,
714
+ evidence: { scenarios: authSkippedScenarios },
715
+ });
716
+ }
465
717
  allObservations.push(...observations);
718
+ const status = computeTrackStatus(results, skipped.length, hasAuth);
719
+ const hasDeterministicScenario = applicable.some(s => s.startsWith('deterministic_') || s === 'controller_validation');
720
+ const mode = hasDeterministicScenario ? 'deterministic' : 'observational';
466
721
  trackResults.push({
467
722
  track,
468
- status: 'pass',
723
+ status,
469
724
  label: def.label,
470
- scenarios: [],
725
+ scenarios: results,
471
726
  skipped_scenarios: skipped,
472
727
  observations,
473
728
  duration_ms: Date.now() - trackStart,
729
+ mode,
474
730
  });
475
- continue;
476
- }
477
- // Run each applicable scenario for this track
478
- const results = [];
479
- for (const scenario of applicable) {
480
- const result = await (0, agent_tester_1.testAgent)(agentUrl, scenario, effectiveOptions);
481
- results.push(result);
482
731
  }
483
- const observations = collectObservations(track, results, profile);
484
- // Detect auth-only failures when running without auth
485
- const hasAuth = !!effectiveOptions.auth;
486
- const authSkippedScenarios = !hasAuth ? results.filter(r => isAuthOnlyFailure(r)).map(r => r.scenario) : [];
487
- if (authSkippedScenarios.length > 0) {
488
- observations.push({
489
- category: 'auth',
490
- severity: 'info',
491
- track,
492
- message: `${authSkippedScenarios.length} scenario(s) require authentication: ${authSkippedScenarios.join(', ')}. ` +
493
- `Re-run with --auth to test.`,
494
- evidence: { scenarios: authSkippedScenarios },
495
- });
732
+ // Build platform coherence result if platform type was declared
733
+ let platformCoherence;
734
+ if (platformProfile) {
735
+ const findings = platformProfile.checkCoherence(profile);
736
+ const missingTracks = platformProfile.expected_tracks.filter(t => !isTrackApplicable(t, profile.tools) && t !== 'core');
737
+ // Add coherence findings as observations
738
+ for (const finding of findings) {
739
+ allObservations.push({
740
+ category: 'coherence',
741
+ severity: finding.severity,
742
+ message: `${finding.expected} ${finding.actual}. ${finding.guidance}`,
743
+ evidence: { platform_type: platformProfile.type },
744
+ });
745
+ }
746
+ platformCoherence = {
747
+ platform_type: platformProfile.type,
748
+ label: platformProfile.label,
749
+ expected_tracks: platformProfile.expected_tracks,
750
+ missing_tracks: missingTracks,
751
+ findings,
752
+ coherent: findings.filter(f => f.severity === 'error' || f.severity === 'warning').length === 0 &&
753
+ missingTracks.length === 0,
754
+ };
496
755
  }
497
- allObservations.push(...observations);
498
- const status = computeTrackStatus(results, skipped.length, hasAuth);
499
- trackResults.push({
500
- track,
501
- status,
502
- label: def.label,
503
- scenarios: results,
504
- skipped_scenarios: skipped,
505
- observations,
506
- duration_ms: Date.now() - trackStart,
507
- });
756
+ const summary = buildSummary(trackResults);
757
+ return {
758
+ agent_url: agentUrl,
759
+ agent_profile: profile,
760
+ tracks: trackResults,
761
+ summary,
762
+ observations: allObservations,
763
+ platform_coherence: platformCoherence,
764
+ controller_detected: controllerDetection.detected,
765
+ controller_scenarios: controllerDetection.detected ? controllerDetection.scenarios : undefined,
766
+ tested_at: new Date().toISOString(),
767
+ total_duration_ms: Date.now() - start,
768
+ dry_run: effectiveOptions.dry_run !== false,
769
+ };
508
770
  }
509
- // Build platform coherence result if platform type was declared
510
- let platformCoherence;
511
- if (platformProfile) {
512
- const findings = platformProfile.checkCoherence(profile);
513
- const missingTracks = platformProfile.expected_tracks.filter(t => !isTrackApplicable(t, profile.tools) && t !== 'core');
514
- // Add coherence findings as observations
515
- for (const finding of findings) {
516
- allObservations.push({
517
- category: 'coherence',
518
- severity: finding.severity,
519
- message: `${finding.expected} — ${finding.actual}. ${finding.guidance}`,
520
- evidence: { platform_type: platformProfile.type },
521
- });
771
+ finally {
772
+ if (timeoutId !== undefined)
773
+ clearTimeout(timeoutId);
774
+ if (onExternalAbort && externalSignal) {
775
+ externalSignal.removeEventListener('abort', onExternalAbort);
522
776
  }
523
- platformCoherence = {
524
- platform_type: platformProfile.type,
525
- label: platformProfile.label,
526
- expected_tracks: platformProfile.expected_tracks,
527
- missing_tracks: missingTracks,
528
- findings,
529
- coherent: findings.filter(f => f.severity === 'error' || f.severity === 'warning').length === 0 &&
530
- missingTracks.length === 0,
531
- };
532
777
  }
533
- const summary = buildSummary(trackResults);
534
- return {
535
- agent_url: agentUrl,
536
- agent_profile: profile,
537
- tracks: trackResults,
538
- summary,
539
- observations: allObservations,
540
- platform_coherence: platformCoherence,
541
- tested_at: new Date().toISOString(),
542
- total_duration_ms: Date.now() - start,
543
- dry_run: effectiveOptions.dry_run !== false,
544
- };
545
778
  }
546
779
  function buildSummary(tracks) {
547
780
  const passed = tracks.filter(t => t.status === 'pass').length;