@adcp/client 4.13.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 (133) hide show
  1. package/bin/adcp.js +25 -23
  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 +28 -1
  50. package/dist/lib/testing/agent-tester.js.map +1 -1
  51. package/dist/lib/testing/client.d.ts +19 -0
  52. package/dist/lib/testing/client.d.ts.map +1 -1
  53. package/dist/lib/testing/client.js +64 -3
  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 +218 -3
  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 +3 -1
  61. package/dist/lib/testing/index.d.ts.map +1 -1
  62. package/dist/lib/testing/index.js +9 -2
  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 +14 -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 +2 -2
  85. package/dist/lib/testing/scenarios/index.d.ts.map +1 -1
  86. package/dist/lib/testing/scenarios/index.js +5 -1
  87. package/dist/lib/testing/scenarios/index.js.map +1 -1
  88. package/dist/lib/testing/scenarios/media-buy.d.ts +24 -0
  89. package/dist/lib/testing/scenarios/media-buy.d.ts.map +1 -1
  90. package/dist/lib/testing/scenarios/media-buy.js +555 -8
  91. package/dist/lib/testing/scenarios/media-buy.js.map +1 -1
  92. package/dist/lib/testing/scenarios/schema-compliance.d.ts.map +1 -1
  93. package/dist/lib/testing/scenarios/schema-compliance.js +5 -2
  94. package/dist/lib/testing/scenarios/schema-compliance.js.map +1 -1
  95. package/dist/lib/testing/scenarios/signals.d.ts.map +1 -1
  96. package/dist/lib/testing/scenarios/signals.js +39 -3
  97. package/dist/lib/testing/scenarios/signals.js.map +1 -1
  98. package/dist/lib/testing/scenarios/sponsored-intelligence.js +6 -6
  99. package/dist/lib/testing/scenarios/sponsored-intelligence.js.map +1 -1
  100. package/dist/lib/testing/stubs/governance-agent-stub.d.ts +72 -0
  101. package/dist/lib/testing/stubs/governance-agent-stub.d.ts.map +1 -0
  102. package/dist/lib/testing/stubs/governance-agent-stub.js +295 -0
  103. package/dist/lib/testing/stubs/governance-agent-stub.js.map +1 -0
  104. package/dist/lib/testing/stubs/index.d.ts +3 -0
  105. package/dist/lib/testing/stubs/index.d.ts.map +1 -0
  106. package/dist/lib/testing/stubs/index.js +6 -0
  107. package/dist/lib/testing/stubs/index.js.map +1 -0
  108. package/dist/lib/testing/types.d.ts +5 -1
  109. package/dist/lib/testing/types.d.ts.map +1 -1
  110. package/dist/lib/types/core.generated.d.ts +7890 -92
  111. package/dist/lib/types/core.generated.d.ts.map +1 -1
  112. package/dist/lib/types/core.generated.js +1 -1
  113. package/dist/lib/types/error-codes.d.ts +4 -4
  114. package/dist/lib/types/error-codes.d.ts.map +1 -1
  115. package/dist/lib/types/error-codes.js +26 -2
  116. package/dist/lib/types/error-codes.js.map +1 -1
  117. package/dist/lib/types/schemas.generated.d.ts +7649 -3768
  118. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  119. package/dist/lib/types/schemas.generated.js +677 -418
  120. package/dist/lib/types/schemas.generated.js.map +1 -1
  121. package/dist/lib/types/tools.generated.d.ts +8325 -450
  122. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  123. package/dist/lib/utils/response-schemas.d.ts +9 -0
  124. package/dist/lib/utils/response-schemas.d.ts.map +1 -0
  125. package/dist/lib/utils/response-schemas.js +60 -0
  126. package/dist/lib/utils/response-schemas.js.map +1 -0
  127. package/dist/lib/utils/response-unwrapper.d.ts +2 -2
  128. package/dist/lib/utils/response-unwrapper.d.ts.map +1 -1
  129. package/dist/lib/utils/response-unwrapper.js +3 -91
  130. package/dist/lib/utils/response-unwrapper.js.map +1 -1
  131. package/dist/lib/version.d.ts +3 -3
  132. package/dist/lib/version.js +3 -3
  133. package/package.json +1 -1
@@ -19,6 +19,9 @@ exports.testCreativeInline = testCreativeInline;
19
19
  exports.testCreativeReference = testCreativeReference;
20
20
  exports.resolveAccountForAudiences = resolveAccountForAudiences;
21
21
  exports.testSyncAudiences = testSyncAudiences;
22
+ exports.testMediaBuyLifecycle = testMediaBuyLifecycle;
23
+ exports.testTerminalStateEnforcement = testTerminalStateEnforcement;
24
+ exports.testPackageLifecycle = testPackageLifecycle;
22
25
  const client_1 = require("../client");
23
26
  const discovery_1 = require("./discovery");
24
27
  /**
@@ -145,7 +148,7 @@ function buildSyncCreativeFromManifest(manifest, fallbackFormatId) {
145
148
  */
146
149
  async function testCreateMediaBuy(agentUrl, options) {
147
150
  const steps = [];
148
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
151
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
149
152
  // First run discovery
150
153
  const { steps: discoverySteps, profile } = await (0, discovery_1.testDiscovery)(agentUrl, options);
151
154
  steps.push(...discoverySteps);
@@ -207,6 +210,7 @@ async function testCreateMediaBuy(agentUrl, options) {
207
210
  async () => client.createMediaBuy(createRequest));
208
211
  let mediaBuyId;
209
212
  if (createResult?.success && createResult?.data) {
213
+ steps.push((0, client_1.validateResponseSchema)('create_media_buy', createResult.data));
210
214
  const mediaBuy = createResult.data;
211
215
  const nested = mediaBuy.media_buy;
212
216
  mediaBuyId = (mediaBuy.media_buy_id || nested?.media_buy_id);
@@ -214,9 +218,15 @@ async function testCreateMediaBuy(agentUrl, options) {
214
218
  const packages = (mediaBuy.packages || nested?.packages);
215
219
  createStep.details = `Created media buy: ${mediaBuyId}, status: ${status}`;
216
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);
217
224
  createStep.response_preview = JSON.stringify({
218
225
  media_buy_id: mediaBuyId,
219
226
  status,
227
+ confirmed_at: confirmedAt,
228
+ revision,
229
+ valid_actions: validActions,
220
230
  packages_count: packages?.length,
221
231
  pricing_model: pricingOption.pricing_model,
222
232
  product_name: product.name,
@@ -235,7 +245,7 @@ async function testCreateMediaBuy(agentUrl, options) {
235
245
  */
236
246
  async function testFullSalesFlow(agentUrl, options) {
237
247
  const steps = [];
238
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
248
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
239
249
  // Run create media buy flow first
240
250
  const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
241
251
  steps.push(...createSteps);
@@ -276,6 +286,7 @@ async function testFullSalesFlow(agentUrl, options) {
276
286
  include_snapshot: true,
277
287
  }));
278
288
  if (snapshotResult?.success && snapshotResult?.data) {
289
+ steps.push((0, client_1.validateResponseSchema)('get_media_buys', snapshotResult.data));
279
290
  const mediaBuys = snapshotResult.data.media_buys || [];
280
291
  const mediaBuy = mediaBuys.find((item) => item.media_buy_id === mediaBuyId) || mediaBuys[0];
281
292
  const packages = mediaBuy?.packages || [];
@@ -341,9 +352,9 @@ async function testFullSalesFlow(agentUrl, options) {
341
352
  */
342
353
  async function testCreativeSync(agentUrl, options) {
343
354
  const steps = [];
344
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
355
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
345
356
  // Discover profile
346
- const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
357
+ const { profile, step: profileStep } = await (0, client_1.getOrDiscoverProfile)(client, options);
347
358
  steps.push(profileStep);
348
359
  if (!profile.tools.includes('sync_creatives')) {
349
360
  steps.push({
@@ -457,7 +468,7 @@ async function testCreativeSync(agentUrl, options) {
457
468
  */
458
469
  async function testCreativeInline(agentUrl, options) {
459
470
  const steps = [];
460
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
471
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
461
472
  // Discovery first
462
473
  const { steps: discoverySteps, profile } = await (0, discovery_1.testDiscovery)(agentUrl, options);
463
474
  steps.push(...discoverySteps);
@@ -569,6 +580,7 @@ async function testCreativeInline(agentUrl, options) {
569
580
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
570
581
  async () => client.createMediaBuy(createRequest));
571
582
  if (createResult?.success && createResult?.data) {
583
+ steps.push((0, client_1.validateResponseSchema)('create_media_buy', createResult.data));
572
584
  const mediaBuy = createResult.data;
573
585
  const nested = mediaBuy.media_buy;
574
586
  const mediaBuyId = (mediaBuy.media_buy_id || nested?.media_buy_id);
@@ -601,7 +613,7 @@ async function testCreativeInline(agentUrl, options) {
601
613
  */
602
614
  async function testCreativeReference(agentUrl, options) {
603
615
  const steps = [];
604
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
616
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
605
617
  const { steps: discoverySteps, profile } = await (0, discovery_1.testDiscovery)(agentUrl, options);
606
618
  steps.push(...discoverySteps);
607
619
  if (!profile?.tools.includes('build_creative') || !profile.tools.includes('sync_creatives')) {
@@ -701,6 +713,7 @@ async function testCreativeReference(agentUrl, options) {
701
713
  });
702
714
  const { result: createResult, step: createStep } = await (0, client_1.runStep)('Create media buy with referenced creative', 'create_media_buy', async () => client.executeTask('create_media_buy', createRequest));
703
715
  if (createResult?.success && createResult?.data) {
716
+ steps.push((0, client_1.validateResponseSchema)('create_media_buy', createResult.data));
704
717
  const mediaBuy = createResult.data;
705
718
  const mediaBuyId = mediaBuy.media_buy_id || mediaBuy.media_buy?.media_buy_id;
706
719
  const packages = mediaBuy.packages || mediaBuy.media_buy?.packages;
@@ -790,9 +803,9 @@ async function resolveAccountForAudiences(options, tools, listAccounts) {
790
803
  */
791
804
  async function testSyncAudiences(agentUrl, options) {
792
805
  const steps = [];
793
- const client = (0, client_1.createTestClient)(agentUrl, options.protocol || 'mcp', options);
806
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
794
807
  // Discover agent profile
795
- const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
808
+ const { profile, step: profileStep } = await (0, client_1.getOrDiscoverProfile)(client, options);
796
809
  steps.push(profileStep);
797
810
  if (!profileStep.passed) {
798
811
  return { steps, profile };
@@ -867,7 +880,19 @@ async function testSyncAudiences(agentUrl, options) {
867
880
  action: testAudience?.action,
868
881
  status: testAudience?.status,
869
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,
870
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
+ }
871
896
  }
872
897
  else if (createResult && !createResult.success) {
873
898
  createStep.passed = false;
@@ -905,4 +930,526 @@ async function testSyncAudiences(agentUrl, options) {
905
930
  steps.push(deleteStep);
906
931
  return { steps, profile };
907
932
  }
933
+ // ---------------------------------------------------------------------------
934
+ // State Machine Compliance Scenarios
935
+ // ---------------------------------------------------------------------------
936
+ /**
937
+ * Extract media buy status from a response, handling nested shapes.
938
+ */
939
+ function extractStatus(data) {
940
+ const nested = data.media_buy;
941
+ return (data.status ?? nested?.status);
942
+ }
943
+ /**
944
+ * Test: Media Buy Lifecycle
945
+ * Exercises the full state machine: create -> pause -> resume -> get status -> cancel
946
+ */
947
+ async function testMediaBuyLifecycle(agentUrl, options) {
948
+ const steps = [];
949
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
950
+ // Create a media buy to work with
951
+ const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
952
+ steps.push(...createSteps);
953
+ if (!mediaBuyId || !profile?.tools.includes('update_media_buy')) {
954
+ return { steps, profile };
955
+ }
956
+ // Track revisions across steps for monotonicity check
957
+ const revisions = [];
958
+ // Step 1: Pause the media buy
959
+ const { result: pauseResult, step: pauseStep } = await (0, client_1.runStep)('Pause media buy', 'update_media_buy', async () => client.updateMediaBuy({
960
+ media_buy_id: mediaBuyId,
961
+ paused: true,
962
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
963
+ }));
964
+ if (pauseResult?.success && pauseResult?.data) {
965
+ const data = pauseResult.data;
966
+ const status = extractStatus(data);
967
+ const pauseRevision = data.revision;
968
+ if (pauseRevision !== undefined)
969
+ revisions.push({ step: 'pause', revision: pauseRevision });
970
+ pauseStep.details = `Paused media buy, status: ${status}`;
971
+ pauseStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status, revision: pauseRevision }, null, 2);
972
+ if (status && status !== 'paused') {
973
+ pauseStep.warnings = [`Expected status 'paused', got '${status}'`];
974
+ }
975
+ }
976
+ else if (pauseResult && !pauseResult.success) {
977
+ pauseStep.passed = false;
978
+ pauseStep.error = pauseResult.error || 'Pause operation failed';
979
+ }
980
+ steps.push(pauseStep);
981
+ // Step 2: Resume the media buy
982
+ const { result: resumeResult, step: resumeStep } = await (0, client_1.runStep)('Resume media buy', 'update_media_buy', async () => client.updateMediaBuy({
983
+ media_buy_id: mediaBuyId,
984
+ paused: false,
985
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
986
+ }));
987
+ if (resumeResult?.success && resumeResult?.data) {
988
+ const data = resumeResult.data;
989
+ const status = extractStatus(data);
990
+ const resumeRevision = data.revision;
991
+ if (resumeRevision !== undefined)
992
+ revisions.push({ step: 'resume', revision: resumeRevision });
993
+ resumeStep.details = `Resumed media buy, status: ${status}`;
994
+ resumeStep.response_preview = JSON.stringify({ media_buy_id: mediaBuyId, status, revision: resumeRevision }, null, 2);
995
+ if (status && status !== 'active' && status !== 'pending_activation') {
996
+ resumeStep.warnings = [`Expected status 'active' or 'pending_activation', got '${status}'`];
997
+ }
998
+ }
999
+ else if (resumeResult && !resumeResult.success) {
1000
+ resumeStep.passed = false;
1001
+ resumeStep.error = resumeResult.error || 'Resume operation failed';
1002
+ }
1003
+ steps.push(resumeStep);
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)
1049
+ if (profile.tools.includes('get_media_buys')) {
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', {
1051
+ media_buy_ids: [mediaBuyId],
1052
+ include_history: 10,
1053
+ }));
1054
+ if (statusResult?.success && statusResult?.data) {
1055
+ const mediaBuys = (statusResult.data.media_buys || []);
1056
+ const mediaBuy = mediaBuys.find((item) => item.media_buy_id === mediaBuyId) || mediaBuys[0];
1057
+ if (!mediaBuy) {
1058
+ statusStep.passed = false;
1059
+ statusStep.error = 'get_media_buys did not return the created media buy';
1060
+ }
1061
+ else {
1062
+ const validActions = mediaBuy.valid_actions;
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'}`;
1077
+ statusStep.response_preview = JSON.stringify({
1078
+ media_buy_id: mediaBuy.media_buy_id,
1079
+ status: mediaBuy.status,
1080
+ confirmed_at: mbConfirmedAt,
1081
+ revision: mbRevision,
1082
+ valid_actions: validActions,
1083
+ history_entries: history?.length ?? 0,
1084
+ history_valid: historyValid,
1085
+ has_creative_deadline: hasCreativeDeadline,
1086
+ sandbox: mediaBuy.sandbox,
1087
+ }, null, 2);
1088
+ }
1089
+ }
1090
+ else if (statusResult && !statusResult.success) {
1091
+ statusStep.passed = false;
1092
+ statusStep.error = statusResult.error || 'get_media_buys failed';
1093
+ }
1094
+ steps.push(statusStep);
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
+ }
1152
+ // Step 4: Cancel the media buy
1153
+ const { result: cancelResult, step: cancelStep } = await (0, client_1.runStep)('Cancel media buy', 'update_media_buy', async () => client.updateMediaBuy({
1154
+ media_buy_id: mediaBuyId,
1155
+ canceled: true,
1156
+ cancellation_reason: 'AdCP compliance test — lifecycle scenario',
1157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1158
+ }));
1159
+ if (cancelResult?.success && cancelResult?.data) {
1160
+ const data = cancelResult.data;
1161
+ const status = extractStatus(data);
1162
+ const cancelRevision = data.revision;
1163
+ cancelStep.details = `Canceled media buy, status: ${status}`;
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);
1169
+ if (status && status !== 'canceled') {
1170
+ cancelStep.warnings = [`Expected status 'canceled', got '${status}'`];
1171
+ }
1172
+ }
1173
+ else if (cancelResult && !cancelResult.success) {
1174
+ // NOT_CANCELLABLE is a valid response — agent may not support cancellation
1175
+ const error = cancelResult.error || '';
1176
+ if (error.includes('NOT_CANCELLABLE') || error.includes('not_cancellable')) {
1177
+ cancelStep.passed = true;
1178
+ cancelStep.details = 'Agent does not support cancellation (NOT_CANCELLABLE)';
1179
+ }
1180
+ else {
1181
+ cancelStep.passed = false;
1182
+ cancelStep.error = cancelResult.error || 'Cancel operation failed';
1183
+ }
1184
+ }
1185
+ steps.push(cancelStep);
1186
+ return { steps, profile };
1187
+ }
1188
+ /**
1189
+ * Test: Terminal State Enforcement
1190
+ * Verifies agents reject updates to media buys in terminal states.
1191
+ */
1192
+ async function testTerminalStateEnforcement(agentUrl, options) {
1193
+ const steps = [];
1194
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
1195
+ // Create and cancel a media buy
1196
+ const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
1197
+ steps.push(...createSteps);
1198
+ if (!mediaBuyId || !profile?.tools.includes('update_media_buy')) {
1199
+ return { steps, profile };
1200
+ }
1201
+ // Cancel the media buy to put it in a terminal state
1202
+ const { result: cancelResult, step: cancelStep } = await (0, client_1.runStep)('Cancel media buy (setup)', 'update_media_buy', async () => client.updateMediaBuy({
1203
+ media_buy_id: mediaBuyId,
1204
+ canceled: true,
1205
+ cancellation_reason: 'AdCP compliance test — terminal state enforcement',
1206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1207
+ }));
1208
+ if (cancelResult?.success && cancelResult?.data) {
1209
+ const data = cancelResult.data;
1210
+ cancelStep.details = `Canceled media buy, status: ${extractStatus(data)}`;
1211
+ }
1212
+ else if (cancelResult && !cancelResult.success) {
1213
+ const error = cancelResult.error || '';
1214
+ if (error.includes('NOT_CANCELLABLE') || error.includes('not_cancellable')) {
1215
+ // Agent doesn't support cancellation — try to find a completed buy instead
1216
+ cancelStep.passed = true;
1217
+ cancelStep.details = 'Agent does not support cancellation — will check for completed media buys instead';
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
+ }
1266
+ return { steps, profile };
1267
+ }
1268
+ cancelStep.passed = false;
1269
+ cancelStep.error = cancelResult.error || 'Cancel setup failed';
1270
+ }
1271
+ steps.push(cancelStep);
1272
+ // Try to pause the canceled media buy — should be rejected
1273
+ const { result: pauseTerminalResult, step: pauseTerminalStep } = await (0, client_1.runStep)('Update canceled media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1274
+ media_buy_id: mediaBuyId,
1275
+ paused: true,
1276
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1277
+ }));
1278
+ if (pauseTerminalResult?.success) {
1279
+ pauseTerminalStep.passed = false;
1280
+ pauseTerminalStep.error = 'Agent accepted update to canceled media buy — should reject with INVALID_STATE';
1281
+ }
1282
+ else if (pauseTerminalResult) {
1283
+ // Agent returned { success: false } — correct behavior
1284
+ pauseTerminalStep.passed = true;
1285
+ const error = pauseTerminalResult.error || '';
1286
+ const hasExpectedCode = error.includes('INVALID_STATE') || error.includes('invalid_state');
1287
+ pauseTerminalStep.details = hasExpectedCode
1288
+ ? 'Correctly rejected with INVALID_STATE'
1289
+ : `Correctly rejected update to canceled media buy: ${error}`;
1290
+ if (!hasExpectedCode && error) {
1291
+ pauseTerminalStep.warnings = ['Agent rejected the update but did not use INVALID_STATE error code'];
1292
+ }
1293
+ }
1294
+ // else: result is undefined (exception thrown) — runStep already set passed=false and error
1295
+ steps.push(pauseTerminalStep);
1296
+ // Try to cancel again — should also be rejected (or idempotent)
1297
+ const { result: reCancelResult, step: reCancelStep } = await (0, client_1.runStep)('Cancel already-canceled media buy (expect rejection)', 'update_media_buy', async () => client.updateMediaBuy({
1298
+ media_buy_id: mediaBuyId,
1299
+ canceled: true,
1300
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1301
+ }));
1302
+ if (reCancelResult?.success) {
1303
+ // Idempotent cancellation is acceptable
1304
+ reCancelStep.passed = true;
1305
+ reCancelStep.details = 'Agent accepted re-cancellation (idempotent) — acceptable behavior';
1306
+ }
1307
+ else if (reCancelResult) {
1308
+ reCancelStep.passed = true;
1309
+ const error = reCancelResult.error || '';
1310
+ reCancelStep.details = `Correctly rejected re-cancellation: ${error}`;
1311
+ }
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
+ }
1344
+ return { steps, profile };
1345
+ }
1346
+ /**
1347
+ * Test: Package Lifecycle
1348
+ * Tests package-level pause/resume independent of media buy status.
1349
+ */
1350
+ async function testPackageLifecycle(agentUrl, options) {
1351
+ const steps = [];
1352
+ const client = (0, client_1.getOrCreateClient)(agentUrl, options);
1353
+ // Create a media buy
1354
+ const { steps: createSteps, profile, mediaBuyId } = await testCreateMediaBuy(agentUrl, options);
1355
+ steps.push(...createSteps);
1356
+ if (!mediaBuyId || !profile?.tools.includes('update_media_buy')) {
1357
+ return { steps, profile };
1358
+ }
1359
+ // Find a package ID — try get_media_buys first, fall back to convention
1360
+ let packageId = 'pkg-0';
1361
+ if (profile.tools.includes('get_media_buys')) {
1362
+ const { result: fetchResult, step: fetchStep } = await (0, client_1.runStep)('Fetch package IDs', 'get_media_buys', async () => client.executeTask('get_media_buys', {
1363
+ media_buy_ids: [mediaBuyId],
1364
+ }));
1365
+ if (fetchResult?.success && fetchResult?.data) {
1366
+ const mediaBuys = (fetchResult.data.media_buys || []);
1367
+ const mediaBuy = mediaBuys.find((item) => item.media_buy_id === mediaBuyId) || mediaBuys[0];
1368
+ const packages = (mediaBuy?.packages || []);
1369
+ if (packages[0]?.package_id) {
1370
+ packageId = packages[0].package_id;
1371
+ fetchStep.details = `Found package ${packageId}`;
1372
+ }
1373
+ else {
1374
+ fetchStep.details = `No packages found, falling back to '${packageId}'`;
1375
+ }
1376
+ }
1377
+ else if (fetchResult && !fetchResult.success) {
1378
+ fetchStep.passed = false;
1379
+ fetchStep.error = fetchResult.error || 'get_media_buys failed during package ID discovery';
1380
+ }
1381
+ steps.push(fetchStep);
1382
+ }
1383
+ // Step 1: Pause a package
1384
+ const { result: pauseResult, step: pauseStep } = await (0, client_1.runStep)('Pause package', 'update_media_buy', async () => client.updateMediaBuy({
1385
+ media_buy_id: mediaBuyId,
1386
+ packages: [{ package_id: packageId, paused: true }],
1387
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1388
+ }));
1389
+ if (pauseResult?.success && pauseResult?.data) {
1390
+ const data = pauseResult.data;
1391
+ const affectedPackages = (data.affected_packages || []);
1392
+ const pkg = affectedPackages.find(p => p.package_id === packageId) || affectedPackages[0];
1393
+ pauseStep.details = `Paused package ${packageId}, paused: ${pkg?.paused}`;
1394
+ pauseStep.response_preview = JSON.stringify({
1395
+ media_buy_id: mediaBuyId,
1396
+ media_buy_status: extractStatus(data),
1397
+ package_id: pkg?.package_id,
1398
+ package_paused: pkg?.paused,
1399
+ }, null, 2);
1400
+ }
1401
+ else if (pauseResult && !pauseResult.success) {
1402
+ pauseStep.passed = false;
1403
+ pauseStep.error = pauseResult.error || 'Package pause failed';
1404
+ }
1405
+ steps.push(pauseStep);
1406
+ // Step 2: Resume the package
1407
+ const { result: resumeResult, step: resumeStep } = await (0, client_1.runStep)('Resume package', 'update_media_buy', async () => client.updateMediaBuy({
1408
+ media_buy_id: mediaBuyId,
1409
+ packages: [{ package_id: packageId, paused: false }],
1410
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- intentional: test request bypasses strict typing
1411
+ }));
1412
+ if (resumeResult?.success && resumeResult?.data) {
1413
+ const data = resumeResult.data;
1414
+ const affectedPackages = (data.affected_packages || []);
1415
+ const pkg = affectedPackages.find(p => p.package_id === packageId) || affectedPackages[0];
1416
+ resumeStep.details = `Resumed package ${packageId}, paused: ${pkg?.paused}`;
1417
+ resumeStep.response_preview = JSON.stringify({
1418
+ media_buy_id: mediaBuyId,
1419
+ media_buy_status: extractStatus(data),
1420
+ package_id: pkg?.package_id,
1421
+ package_paused: pkg?.paused,
1422
+ }, null, 2);
1423
+ }
1424
+ else if (resumeResult && !resumeResult.success) {
1425
+ resumeStep.passed = false;
1426
+ resumeStep.error = resumeResult.error || 'Package resume failed';
1427
+ }
1428
+ steps.push(resumeStep);
1429
+ // Step 3: Verify media buy is still active
1430
+ if (profile.tools.includes('get_media_buys')) {
1431
+ const { result: verifyResult, step: verifyStep } = await (0, client_1.runStep)('Verify media buy still active after package operations', 'get_media_buys', async () => client.executeTask('get_media_buys', {
1432
+ media_buy_ids: [mediaBuyId],
1433
+ }));
1434
+ if (verifyResult?.success && verifyResult?.data) {
1435
+ const mediaBuys = (verifyResult.data.media_buys || []);
1436
+ const mediaBuy = mediaBuys.find((item) => item.media_buy_id === mediaBuyId) || mediaBuys[0];
1437
+ const status = mediaBuy?.status;
1438
+ if (status === 'active' || status === 'pending_activation') {
1439
+ verifyStep.details = `Media buy still ${status} after package-level operations`;
1440
+ }
1441
+ else {
1442
+ verifyStep.details = `Media buy status is '${status}' — expected 'active' or 'pending_activation'`;
1443
+ verifyStep.warnings = [`Package-level pause/resume changed media buy status to '${status}'`];
1444
+ }
1445
+ verifyStep.response_preview = JSON.stringify({ media_buy_id: mediaBuy?.media_buy_id, status }, null, 2);
1446
+ }
1447
+ else if (verifyResult && !verifyResult.success) {
1448
+ verifyStep.passed = false;
1449
+ verifyStep.error = verifyResult.error || 'get_media_buys verification failed';
1450
+ }
1451
+ steps.push(verifyStep);
1452
+ }
1453
+ return { steps, profile };
1454
+ }
908
1455
  //# sourceMappingURL=media-buy.js.map