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