@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
@@ -64,7 +64,6 @@ function buildCreateMediaBuyRequest(product, pricingOption, options, extras = {}
64
64
  const isAuction = !('fixed_price' in pricingOption) &&
65
65
  (pricingOption.floor_price !== undefined || pricingOption.price_guidance !== undefined);
66
66
  const packageRequest = {
67
- buyer_ref: `pkg-test-${Date.now()}`,
68
67
  product_id: product.product_id,
69
68
  budget,
70
69
  pricing_option_id: pricingOption.pricing_option_id,
@@ -82,7 +81,6 @@ function buildCreateMediaBuyRequest(product, pricingOption, options, extras = {}
82
81
  packageRequest.creative_ids = extras.creative_ids;
83
82
  }
84
83
  return {
85
- buyer_ref: `e2e-test-${Date.now()}`,
86
84
  account: (0, client_1.resolveAccount)(options),
87
85
  brand: (0, client_1.resolveBrand)(options),
88
86
  start_time: startTime.toISOString(),
@@ -148,7 +146,7 @@ function buildSyncCreativeFromManifest(manifest, fallbackFormatId) {
148
146
  */
149
147
  async function testCreateMediaBuy(agentUrl, options) {
150
148
  const steps = [];
151
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
149
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
152
150
  // First run discovery
153
151
  const { steps: discoverySteps, profile } = await (0, discovery_1.testDiscovery)(agentUrl, options);
154
152
  steps.push(...discoverySteps);
@@ -218,9 +216,15 @@ async function testCreateMediaBuy(agentUrl, options) {
218
216
  const packages = (mediaBuy.packages || nested?.packages);
219
217
  createStep.details = `Created media buy: ${mediaBuyId}, status: ${status}`;
220
218
  createStep.created_id = mediaBuyId;
219
+ const confirmedAt = (mediaBuy.confirmed_at ?? nested?.confirmed_at);
220
+ const revision = (mediaBuy.revision ?? nested?.revision);
221
+ const validActions = (mediaBuy.valid_actions ?? nested?.valid_actions);
221
222
  createStep.response_preview = JSON.stringify({
222
223
  media_buy_id: mediaBuyId,
223
224
  status,
225
+ confirmed_at: confirmedAt,
226
+ revision,
227
+ valid_actions: validActions,
224
228
  packages_count: packages?.length,
225
229
  pricing_model: pricingOption.pricing_model,
226
230
  product_name: product.name,
@@ -239,7 +243,7 @@ async function testCreateMediaBuy(agentUrl, options) {
239
243
  */
240
244
  async function testFullSalesFlow(agentUrl, options) {
241
245
  const steps = [];
242
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
246
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
243
247
  // Run create media buy flow first
244
248
  const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
245
249
  steps.push(...createSteps);
@@ -346,9 +350,9 @@ async function testFullSalesFlow(agentUrl, options) {
346
350
  */
347
351
  async function testCreativeSync(agentUrl, options) {
348
352
  const steps = [];
349
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
353
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
350
354
  // Discover profile
351
- const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
355
+ const { profile, step: profileStep } = await (0, client_1.getOrDiscoverProfile)(client, options);
352
356
  steps.push(profileStep);
353
357
  if (!profile.tools.includes('sync_creatives')) {
354
358
  steps.push({
@@ -462,7 +466,7 @@ async function testCreativeSync(agentUrl, options) {
462
466
  */
463
467
  async function testCreativeInline(agentUrl, options) {
464
468
  const steps = [];
465
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
469
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
466
470
  // Discovery first
467
471
  const { steps: discoverySteps, profile } = await (0, discovery_1.testDiscovery)(agentUrl, options);
468
472
  steps.push(...discoverySteps);
@@ -607,7 +611,7 @@ async function testCreativeInline(agentUrl, options) {
607
611
  */
608
612
  async function testCreativeReference(agentUrl, options) {
609
613
  const steps = [];
610
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
614
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
611
615
  const { steps: discoverySteps, profile } = await (0, discovery_1.testDiscovery)(agentUrl, options);
612
616
  steps.push(...discoverySteps);
613
617
  if (!profile?.tools.includes('build_creative') || !profile.tools.includes('sync_creatives')) {
@@ -797,9 +801,9 @@ async function resolveAccountForAudiences(options, tools, listAccounts) {
797
801
  */
798
802
  async function testSyncAudiences(agentUrl, options) {
799
803
  const steps = [];
800
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
804
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
801
805
  // Discover agent profile
802
- const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
806
+ const { profile, step: profileStep } = await (0, client_1.getOrDiscoverProfile)(client, options);
803
807
  steps.push(profileStep);
804
808
  if (!profileStep.passed) {
805
809
  return { steps, profile };
@@ -874,7 +878,19 @@ async function testSyncAudiences(agentUrl, options) {
874
878
  action: testAudience?.action,
875
879
  status: testAudience?.status,
876
880
  uploaded_count: testAudience?.uploaded_count,
881
+ matched_count: testAudience?.matched_count,
882
+ effective_match_rate: testAudience?.effective_match_rate,
883
+ match_breakdown: testAudience?.match_breakdown,
877
884
  }, null, 2);
885
+ // Advisory: report match breakdown availability
886
+ if (testAudience?.status === 'ready') {
887
+ if (testAudience.match_breakdown) {
888
+ createStep.details += `, match_breakdown: ${testAudience.match_breakdown.length} ID type(s)`;
889
+ }
890
+ if (testAudience.effective_match_rate != null) {
891
+ createStep.details += `, effective_match_rate: ${(testAudience.effective_match_rate * 100).toFixed(1)}%`;
892
+ }
893
+ }
878
894
  }
879
895
  else if (createResult && !createResult.success) {
880
896
  createStep.passed = false;
@@ -928,13 +944,15 @@ function extractStatus(data) {
928
944
  */
929
945
  async function testMediaBuyLifecycle(agentUrl, options) {
930
946
  const steps = [];
931
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
947
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
932
948
  // Create a media buy to work with
933
949
  const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
934
950
  steps.push(...createSteps);
935
951
  if (!mediaBuyId || !profile?.tools.includes('update_media_buy')) {
936
952
  return { steps, profile };
937
953
  }
954
+ // Track revisions across steps for monotonicity check
955
+ const revisions = [];
938
956
  // Step 1: Pause the media buy
939
957
  const { result: pauseResult, step: pauseStep } = await (0, client_1.runStep)('Pause media buy', 'update_media_buy', async () => client.updateMediaBuy({
940
958
  media_buy_id: mediaBuyId,
@@ -944,8 +962,11 @@ async function testMediaBuyLifecycle(agentUrl, options) {
944
962
  if (pauseResult?.success && pauseResult?.data) {
945
963
  const data = pauseResult.data;
946
964
  const status = extractStatus(data);
965
+ const pauseRevision = data.revision;
966
+ if (pauseRevision !== undefined)
967
+ revisions.push({ step: 'pause', revision: pauseRevision });
947
968
  pauseStep.details = `Paused media buy, status: ${status}`;
948
- pauseStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status }, null, 2);
969
+ pauseStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status, revision: pauseRevision }, null, 2);
949
970
  if (status && status !== 'paused') {
950
971
  pauseStep.warnings = [`Expected status 'paused', got '${status}'`];
951
972
  }
@@ -964,8 +985,11 @@ async function testMediaBuyLifecycle(agentUrl, options) {
964
985
  if (resumeResult?.success && resumeResult?.data) {
965
986
  const data = resumeResult.data;
966
987
  const status = extractStatus(data);
988
+ const resumeRevision = data.revision;
989
+ if (resumeRevision !== undefined)
990
+ revisions.push({ step: 'resume', revision: resumeRevision });
967
991
  resumeStep.details = `Resumed media buy, status: ${status}`;
968
- resumeStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status }, null, 2);
992
+ resumeStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status, revision: resumeRevision }, null, 2);
969
993
  if (status && status !== 'active' && status !== 'pending_activation') {
970
994
  resumeStep.warnings = [`Expected status 'active' or 'pending_activation', got '${status}'`];
971
995
  }
@@ -975,10 +999,55 @@ async function testMediaBuyLifecycle(agentUrl, options) {
975
999
  resumeStep.error = resumeResult.error || 'Resume operation failed';
976
1000
  }
977
1001
  steps.push(resumeStep);
978
- // Step 3: Get status and check valid_actions (if get_media_buys available)
1002
+ // Step 2b: Budget update verify substantive field mutation
1003
+ // Find a package to update budget on
1004
+ let budgetPackageId;
1005
+ let originalBudget;
1006
+ if (profile.tools.includes('get_media_buys')) {
1007
+ const { result: fetchResult } = await (0, client_1.runStep)('Fetch packages for budget test', 'get_media_buys', async () => client.executeTask('get_media_buys', { media_buy_ids: [mediaBuyId] }));
1008
+ if (fetchResult?.success && fetchResult?.data) {
1009
+ const mbs = (fetchResult.data.media_buys || []);
1010
+ const mb = mbs.find((item) => item.media_buy_id === mediaBuyId) || mbs[0];
1011
+ const pkgs = (mb?.packages || []);
1012
+ if (pkgs[0]) {
1013
+ budgetPackageId = pkgs[0].package_id;
1014
+ originalBudget = pkgs[0].budget;
1015
+ }
1016
+ }
1017
+ }
1018
+ if (budgetPackageId && originalBudget !== undefined) {
1019
+ const newBudget = Math.round(originalBudget * 1.2 * 100) / 100; // 20% increase
1020
+ const { result: budgetResult, step: budgetStep } = await (0, client_1.runStep)('Update package budget', 'update_media_buy', async () => client.updateMediaBuy({
1021
+ media_buy_id: mediaBuyId,
1022
+ packages: [{ package_id: budgetPackageId, budget: newBudget }],
1023
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1024
+ }));
1025
+ if (budgetResult?.success && budgetResult?.data) {
1026
+ const data = budgetResult.data;
1027
+ const budgetRevision = data.revision;
1028
+ if (budgetRevision !== undefined)
1029
+ revisions.push({ step: 'budget_update', revision: budgetRevision });
1030
+ budgetStep.details = `Updated budget from $${originalBudget} to $${newBudget}`;
1031
+ budgetStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, package_id: budgetPackageId, new_budget: newBudget, revision: budgetRevision }, null, 2);
1032
+ }
1033
+ else if (budgetResult && !budgetResult.success) {
1034
+ const error = budgetResult.error || '';
1035
+ if (error.includes('BUDGET_EXCEEDED') || error.includes('budget_exceeded')) {
1036
+ budgetStep.passed = true;
1037
+ budgetStep.details = `Agent rejected budget increase with BUDGET_EXCEEDED — acceptable`;
1038
+ }
1039
+ else {
1040
+ budgetStep.passed = false;
1041
+ budgetStep.error = budgetResult.error || 'Budget update failed';
1042
+ }
1043
+ }
1044
+ steps.push(budgetStep);
1045
+ }
1046
+ // Step 3: Get status and check valid_actions, confirmed_at, revision, history (if get_media_buys available)
979
1047
  if (profile.tools.includes('get_media_buys')) {
980
1048
  const { result: statusResult, step: statusStep } = await (0, client_1.runStep)('Get media buy status and valid_actions', 'get_media_buys', async () => client.executeTask('get_media_buys', {
981
1049
  media_buy_ids: [mediaBuyId],
1050
+ include_history: 10,
982
1051
  }));
983
1052
  if (statusResult?.success && statusResult?.data) {
984
1053
  const mediaBuys = (statusResult.data.media_buys || []);
@@ -989,11 +1058,30 @@ async function testMediaBuyLifecycle(agentUrl, options) {
989
1058
  }
990
1059
  else {
991
1060
  const validActions = mediaBuy.valid_actions;
992
- statusStep.details = `Status: ${mediaBuy.status}, valid_actions: ${validActions ? validActions.join(', ') : 'not provided'}`;
1061
+ const mbRevision = mediaBuy.revision;
1062
+ const mbConfirmedAt = mediaBuy.confirmed_at;
1063
+ const history = mediaBuy.history;
1064
+ const packages = (mediaBuy.packages || []);
1065
+ const hasCreativeDeadline = packages.some(p => p.creative_deadline) || !!mediaBuy.creative_deadline;
1066
+ // Validate history entry shape if present
1067
+ let historyValid = true;
1068
+ if (history?.length) {
1069
+ const missingTimestamp = history.some(h => !h.timestamp);
1070
+ const missingAction = history.some(h => !h.action);
1071
+ if (missingTimestamp || missingAction)
1072
+ historyValid = false;
1073
+ }
1074
+ statusStep.details = `Status: ${mediaBuy.status}, valid_actions: ${validActions ? validActions.join(', ') : 'not provided'}, revision: ${mbRevision ?? 'not provided'}`;
993
1075
  statusStep.response_preview = JSON.stringify({
994
1076
  media_buy_id: mediaBuy.media_buy_id,
995
1077
  status: mediaBuy.status,
1078
+ confirmed_at: mbConfirmedAt,
1079
+ revision: mbRevision,
996
1080
  valid_actions: validActions,
1081
+ history_entries: history?.length ?? 0,
1082
+ history_valid: historyValid,
1083
+ has_creative_deadline: hasCreativeDeadline,
1084
+ sandbox: mediaBuy.sandbox,
997
1085
  }, null, 2);
998
1086
  }
999
1087
  }
@@ -1003,6 +1091,62 @@ async function testMediaBuyLifecycle(agentUrl, options) {
1003
1091
  }
1004
1092
  steps.push(statusStep);
1005
1093
  }
1094
+ // Step 3b: Revision concurrency check — use actual stale revision from an earlier step
1095
+ const lastRevision = revisions.length > 0 ? revisions[revisions.length - 1].revision : undefined;
1096
+ const staleRevision = revisions.length >= 2 ? revisions[0].revision : -1;
1097
+ const { result: conflictResult, step: conflictStep } = await (0, client_1.runStep)('Update with stale revision (expect CONFLICT)', 'update_media_buy', async () => client.updateMediaBuy({
1098
+ media_buy_id: mediaBuyId,
1099
+ revision: staleRevision,
1100
+ end_time: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
1101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1102
+ }));
1103
+ if (conflictResult && !conflictResult.success) {
1104
+ const error = conflictResult.error || '';
1105
+ if (error.includes('CONFLICT') || error.includes('conflict') || error.includes('revision')) {
1106
+ conflictStep.passed = true;
1107
+ conflictStep.details = `Correctly rejected stale revision ${staleRevision} with CONFLICT (current: ${lastRevision ?? 'unknown'})`;
1108
+ }
1109
+ else {
1110
+ // Agent rejected for another reason — still acceptable, revision may not be supported
1111
+ conflictStep.passed = true;
1112
+ conflictStep.details = `Agent rejected update: ${error}`;
1113
+ conflictStep.warnings = [
1114
+ 'Agent did not return CONFLICT for stale revision — revision concurrency may not be supported',
1115
+ ];
1116
+ }
1117
+ }
1118
+ else if (conflictResult?.success) {
1119
+ // Agent accepted stale revision — revision concurrency not enforced
1120
+ conflictStep.passed = true;
1121
+ conflictStep.details = `Agent accepted stale revision ${staleRevision} — optimistic concurrency not enforced`;
1122
+ conflictStep.warnings = ['Agent does not enforce optimistic concurrency via revision numbers'];
1123
+ }
1124
+ else if (!conflictResult) {
1125
+ conflictStep.passed = false;
1126
+ conflictStep.error = 'No response from update_media_buy';
1127
+ }
1128
+ steps.push(conflictStep);
1129
+ // Step 3c: Check revision monotonicity
1130
+ if (revisions.length >= 2) {
1131
+ const monotonicStep = {
1132
+ step: 'Revision monotonicity check',
1133
+ task: 'update_media_buy',
1134
+ passed: true,
1135
+ duration_ms: 0,
1136
+ };
1137
+ const isMonotonic = revisions.every((r, i) => i === 0 || r.revision > revisions[i - 1].revision);
1138
+ if (isMonotonic) {
1139
+ monotonicStep.details = `Revisions are monotonically increasing: ${revisions.map(r => `${r.step}=${r.revision}`).join(' → ')}`;
1140
+ }
1141
+ else {
1142
+ monotonicStep.passed = true; // advisory, not a hard fail
1143
+ monotonicStep.details = `Revisions: ${revisions.map(r => `${r.step}=${r.revision}`).join(' → ')}`;
1144
+ monotonicStep.warnings = [
1145
+ `Revision numbers are not monotonically increasing — buyers depend on this for concurrency safety`,
1146
+ ];
1147
+ }
1148
+ steps.push(monotonicStep);
1149
+ }
1006
1150
  // Step 4: Cancel the media buy
1007
1151
  const { result: cancelResult, step: cancelStep } = await (0, client_1.runStep)('Cancel media buy', 'update_media_buy', async () => client.updateMediaBuy({
1008
1152
  media_buy_id: mediaBuyId,
@@ -1013,8 +1157,13 @@ async function testMediaBuyLifecycle(agentUrl, options) {
1013
1157
  if (cancelResult?.success && cancelResult?.data) {
1014
1158
  const data = cancelResult.data;
1015
1159
  const status = extractStatus(data);
1160
+ const cancelRevision = data.revision;
1016
1161
  cancelStep.details = `Canceled media buy, status: ${status}`;
1017
- cancelStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status }, null, 2);
1162
+ const canceledBy = data.canceled_by;
1163
+ const canceledAt = data.canceled_at;
1164
+ if (cancelRevision !== undefined)
1165
+ revisions.push({ step: 'cancel', revision: cancelRevision });
1166
+ cancelStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status, revision: cancelRevision, canceled_by: canceledBy, canceled_at: canceledAt }, null, 2);
1018
1167
  if (status && status !== 'canceled') {
1019
1168
  cancelStep.warnings = [`Expected status 'canceled', got '${status}'`];
1020
1169
  }
@@ -1040,7 +1189,7 @@ async function testMediaBuyLifecycle(agentUrl, options) {
1040
1189
  */
1041
1190
  async function testTerminalStateEnforcement(agentUrl, options) {
1042
1191
  const steps = [];
1043
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
1192
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
1044
1193
  // Create and cancel a media buy
1045
1194
  const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
1046
1195
  steps.push(...createSteps);
@@ -1061,10 +1210,57 @@ async function testTerminalStateEnforcement(agentUrl, options) {
1061
1210
  else if (cancelResult && !cancelResult.success) {
1062
1211
  const error = cancelResult.error || '';
1063
1212
  if (error.includes('NOT_CANCELLABLE') || error.includes('not_cancellable')) {
1064
- // Agent doesn't support cancellation — can't test terminal state enforcement
1213
+ // Agent doesn't support cancellation — try to find a completed buy instead
1065
1214
  cancelStep.passed = true;
1066
- cancelStep.details = 'Agent does not support cancellation — skipping terminal state tests';
1215
+ cancelStep.details = 'Agent does not support cancellation — will check for completed media buys instead';
1067
1216
  steps.push(cancelStep);
1217
+ // Look for a completed media buy to test terminal state enforcement against
1218
+ if (profile.tools.includes('get_media_buys')) {
1219
+ const { result: completedResult, step: completedStep } = await (0, client_1.runStep)('Find completed media buy for terminal state test', 'get_media_buys', async () => client.executeTask('get_media_buys', {
1220
+ status_filter: ['completed'],
1221
+ pagination: { max_results: 1 },
1222
+ }));
1223
+ if (completedResult?.success && completedResult?.data) {
1224
+ const completedBuys = (completedResult.data.media_buys || []);
1225
+ if (completedBuys.length > 0) {
1226
+ const completedId = completedBuys[0].media_buy_id;
1227
+ completedStep.details = `Found completed media buy: ${completedId}`;
1228
+ steps.push(completedStep);
1229
+ // Try to update the completed media buy
1230
+ const { result: updateCompletedResult, step: updateCompletedStep } = await (0, client_1.runStep)('Update completed media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1231
+ media_buy_id: completedId,
1232
+ paused: true,
1233
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1234
+ }));
1235
+ if (updateCompletedResult?.success) {
1236
+ updateCompletedStep.passed = false;
1237
+ updateCompletedStep.error =
1238
+ 'Agent accepted update to completed media buy — should reject with INVALID_STATE';
1239
+ }
1240
+ else if (updateCompletedResult) {
1241
+ updateCompletedStep.passed = true;
1242
+ const err = updateCompletedResult.error || '';
1243
+ const hasCode = err.includes('INVALID_STATE') || err.includes('invalid_state');
1244
+ updateCompletedStep.details = hasCode
1245
+ ? 'Correctly rejected update to completed media buy with INVALID_STATE'
1246
+ : `Correctly rejected update to completed media buy: ${err}`;
1247
+ if (!hasCode && err) {
1248
+ updateCompletedStep.warnings = ['Agent rejected the update but did not use INVALID_STATE error code'];
1249
+ }
1250
+ }
1251
+ steps.push(updateCompletedStep);
1252
+ }
1253
+ else {
1254
+ completedStep.details = 'No completed media buys found — cannot test terminal state enforcement';
1255
+ steps.push(completedStep);
1256
+ }
1257
+ }
1258
+ else {
1259
+ completedStep.passed = false;
1260
+ completedStep.error = completedResult?.error || 'Failed to query for completed media buys';
1261
+ steps.push(completedStep);
1262
+ }
1263
+ }
1068
1264
  return { steps, profile };
1069
1265
  }
1070
1266
  cancelStep.passed = false;
@@ -1072,29 +1268,29 @@ async function testTerminalStateEnforcement(agentUrl, options) {
1072
1268
  }
1073
1269
  steps.push(cancelStep);
1074
1270
  // Try to pause the canceled media buy — should be rejected
1075
- const { result: pauseResult, step: pauseStep } = await (0, client_1.runStep)('Update canceled media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1271
+ const { result: pauseTerminalResult, step: pauseTerminalStep } = await (0, client_1.runStep)('Update canceled media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1076
1272
  media_buy_id: mediaBuyId,
1077
1273
  paused: true,
1078
1274
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1079
1275
  }));
1080
- if (pauseResult?.success) {
1081
- pauseStep.passed = false;
1082
- pauseStep.error = 'Agent accepted update to canceled media buy — should reject with INVALID_STATE';
1276
+ if (pauseTerminalResult?.success) {
1277
+ pauseTerminalStep.passed = false;
1278
+ pauseTerminalStep.error = 'Agent accepted update to canceled media buy — should reject with INVALID_STATE';
1083
1279
  }
1084
- else if (pauseResult) {
1280
+ else if (pauseTerminalResult) {
1085
1281
  // Agent returned { success: false } — correct behavior
1086
- pauseStep.passed = true;
1087
- const error = pauseResult.error || '';
1282
+ pauseTerminalStep.passed = true;
1283
+ const error = pauseTerminalResult.error || '';
1088
1284
  const hasExpectedCode = error.includes('INVALID_STATE') || error.includes('invalid_state');
1089
- pauseStep.details = hasExpectedCode
1285
+ pauseTerminalStep.details = hasExpectedCode
1090
1286
  ? 'Correctly rejected with INVALID_STATE'
1091
1287
  : `Correctly rejected update to canceled media buy: ${error}`;
1092
1288
  if (!hasExpectedCode && error) {
1093
- pauseStep.warnings = ['Agent rejected the update but did not use INVALID_STATE error code'];
1289
+ pauseTerminalStep.warnings = ['Agent rejected the update but did not use INVALID_STATE error code'];
1094
1290
  }
1095
1291
  }
1096
- // else: pauseResult is undefined (exception thrown) — runStep already set passed=false and error
1097
- steps.push(pauseStep);
1292
+ // else: result is undefined (exception thrown) — runStep already set passed=false and error
1293
+ steps.push(pauseTerminalStep);
1098
1294
  // Try to cancel again — should also be rejected (or idempotent)
1099
1295
  const { result: reCancelResult, step: reCancelStep } = await (0, client_1.runStep)('Cancel already-canceled media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1100
1296
  media_buy_id: mediaBuyId,
@@ -1112,6 +1308,37 @@ async function testTerminalStateEnforcement(agentUrl, options) {
1112
1308
  reCancelStep.details = `Correctly rejected re-cancellation: ${error}`;
1113
1309
  }
1114
1310
  steps.push(reCancelStep);
1311
+ // Also check completed terminal state if get_media_buys is available
1312
+ if (profile.tools.includes('get_media_buys')) {
1313
+ const { result: completedResult } = await (0, client_1.runStep)('Find completed media buy', 'get_media_buys', async () => client.executeTask('get_media_buys', {
1314
+ status_filter: ['completed'],
1315
+ pagination: { max_results: 1 },
1316
+ }));
1317
+ if (completedResult?.success && completedResult?.data) {
1318
+ const completedBuys = (completedResult.data.media_buys || []);
1319
+ if (completedBuys.length > 0) {
1320
+ const completedId = completedBuys[0].media_buy_id;
1321
+ const { result: updateCompletedResult, step: updateCompletedStep } = await (0, client_1.runStep)('Update completed media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1322
+ media_buy_id: completedId,
1323
+ paused: true,
1324
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1325
+ }));
1326
+ if (updateCompletedResult?.success) {
1327
+ updateCompletedStep.passed = false;
1328
+ updateCompletedStep.error = 'Agent accepted update to completed media buy — should reject with INVALID_STATE';
1329
+ }
1330
+ else if (updateCompletedResult) {
1331
+ updateCompletedStep.passed = true;
1332
+ const err = updateCompletedResult.error || '';
1333
+ const hasCode = err.includes('INVALID_STATE') || err.includes('invalid_state');
1334
+ updateCompletedStep.details = hasCode
1335
+ ? 'Correctly rejected update to completed media buy with INVALID_STATE'
1336
+ : `Correctly rejected update to completed media buy: ${err}`;
1337
+ }
1338
+ steps.push(updateCompletedStep);
1339
+ }
1340
+ }
1341
+ }
1115
1342
  return { steps, profile };
1116
1343
  }
1117
1344
  /**
@@ -1120,7 +1347,7 @@ async function testTerminalStateEnforcement(agentUrl, options) {
1120
1347
  */
1121
1348
  async function testPackageLifecycle(agentUrl, options) {
1122
1349
  const steps = [];
1123
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
1350
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
1124
1351
  // Create a media buy
1125
1352
  const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
1126
1353
  steps.push(...createSteps);