@yoryoboy/bi-mcp 1.16.0 → 1.19.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 (158) hide show
  1. package/README.md +12 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/index.js +179 -6
  4. package/dist/index.js.map +2 -2
  5. package/dist/mcp-use.json +3 -6
  6. package/dist/src/config/__tests__/google-store.test.js +30 -0
  7. package/dist/src/config/__tests__/google-store.test.js.map +7 -0
  8. package/dist/src/config/__tests__/mcp-tool-deadline.test.js +64 -0
  9. package/dist/src/config/__tests__/mcp-tool-deadline.test.js.map +7 -0
  10. package/dist/src/config/google-store.js +18 -0
  11. package/dist/src/config/google-store.js.map +2 -2
  12. package/dist/src/config/mcp-tool-deadline-context.js +34 -0
  13. package/dist/src/config/mcp-tool-deadline-context.js.map +7 -0
  14. package/dist/src/config/mcp-tool-deadline.js +67 -0
  15. package/dist/src/config/mcp-tool-deadline.js.map +7 -0
  16. package/dist/src/services/mercadolibre/__tests__/mercadolibre-api.test.js +38 -3
  17. package/dist/src/services/mercadolibre/__tests__/mercadolibre-api.test.js.map +2 -2
  18. package/dist/src/services/mercadolibre/__tests__/mercadolibre-buyer-reconciliation.test.js +226 -0
  19. package/dist/src/services/mercadolibre/__tests__/mercadolibre-buyer-reconciliation.test.js.map +7 -0
  20. package/dist/src/services/mercadolibre/__tests__/mercadolibre-claim-reconciliation.test.js +127 -0
  21. package/dist/src/services/mercadolibre/__tests__/mercadolibre-claim-reconciliation.test.js.map +7 -0
  22. package/dist/src/services/mercadolibre/__tests__/mercadolibre-kits.test.js +103 -0
  23. package/dist/src/services/mercadolibre/__tests__/mercadolibre-kits.test.js.map +7 -0
  24. package/dist/src/services/mercadolibre/__tests__/mercadolibre-order-billing-info.test.js +582 -0
  25. package/dist/src/services/mercadolibre/__tests__/mercadolibre-order-billing-info.test.js.map +7 -0
  26. package/dist/src/services/mercadolibre/__tests__/mercadolibre-orders.test.js +10 -0
  27. package/dist/src/services/mercadolibre/__tests__/mercadolibre-orders.test.js.map +2 -2
  28. package/dist/src/services/mercadolibre/__tests__/mercadolibre-packs.test.js +36 -0
  29. package/dist/src/services/mercadolibre/__tests__/mercadolibre-packs.test.js.map +7 -0
  30. package/dist/src/services/mercadolibre/__tests__/mercadolibre-payments.test.js +137 -0
  31. package/dist/src/services/mercadolibre/__tests__/mercadolibre-payments.test.js.map +7 -0
  32. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-group-ads.test.js +105 -0
  33. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-group-ads.test.js.map +7 -0
  34. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.js +143 -0
  35. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.js.map +7 -0
  36. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.js +100 -0
  37. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.js.map +7 -0
  38. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads.test.js +350 -0
  39. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads.test.js.map +7 -0
  40. package/dist/src/services/mercadolibre/__tests__/mercadolibre-return-reconciliation.test.js +228 -0
  41. package/dist/src/services/mercadolibre/__tests__/mercadolibre-return-reconciliation.test.js.map +7 -0
  42. package/dist/src/services/mercadolibre/__tests__/mercadolibre-shipping.test.js +58 -0
  43. package/dist/src/services/mercadolibre/__tests__/mercadolibre-shipping.test.js.map +7 -0
  44. package/dist/src/services/mercadolibre/mercadolibre-api.js +15 -3
  45. package/dist/src/services/mercadolibre/mercadolibre-api.js.map +2 -2
  46. package/dist/src/services/mercadolibre/mercadolibre-billing.js +110 -3
  47. package/dist/src/services/mercadolibre/mercadolibre-billing.js.map +2 -2
  48. package/dist/src/services/mercadolibre/mercadolibre-buyer-reconciliation.js +368 -0
  49. package/dist/src/services/mercadolibre/mercadolibre-buyer-reconciliation.js.map +7 -0
  50. package/dist/src/services/mercadolibre/mercadolibre-claim-reconciliation.js +399 -0
  51. package/dist/src/services/mercadolibre/mercadolibre-claim-reconciliation.js.map +7 -0
  52. package/dist/src/services/mercadolibre/mercadolibre-kits.js +55 -0
  53. package/dist/src/services/mercadolibre/mercadolibre-kits.js.map +7 -0
  54. package/dist/src/services/mercadolibre/mercadolibre-order-billing-info.js +392 -0
  55. package/dist/src/services/mercadolibre/mercadolibre-order-billing-info.js.map +7 -0
  56. package/dist/src/services/mercadolibre/mercadolibre-orders.js +8 -8
  57. package/dist/src/services/mercadolibre/mercadolibre-orders.js.map +2 -2
  58. package/dist/src/services/mercadolibre/mercadolibre-packs.js +21 -0
  59. package/dist/src/services/mercadolibre/mercadolibre-packs.js.map +7 -0
  60. package/dist/src/services/mercadolibre/mercadolibre-payments.js +60 -0
  61. package/dist/src/services/mercadolibre/mercadolibre-payments.js.map +7 -0
  62. package/dist/src/services/mercadolibre/mercadolibre-product-ads.js +429 -0
  63. package/dist/src/services/mercadolibre/mercadolibre-product-ads.js.map +7 -0
  64. package/dist/src/services/mercadolibre/mercadolibre-return-reconciliation.js +436 -0
  65. package/dist/src/services/mercadolibre/mercadolibre-return-reconciliation.js.map +7 -0
  66. package/dist/src/services/mercadolibre/mercadolibre-shipping.js +35 -0
  67. package/dist/src/services/mercadolibre/mercadolibre-shipping.js.map +7 -0
  68. package/dist/src/services/vtex/__tests__/vtex-api.test.js +67 -0
  69. package/dist/src/services/vtex/__tests__/vtex-api.test.js.map +7 -0
  70. package/dist/src/services/vtex/vtex-api.js +6 -0
  71. package/dist/src/services/vtex/vtex-api.js.map +2 -2
  72. package/dist/src/tools/config/__tests__/list-profiles.test.js +154 -0
  73. package/dist/src/tools/config/__tests__/list-profiles.test.js.map +7 -0
  74. package/dist/src/tools/config/list-profiles.js +39 -3
  75. package/dist/src/tools/config/list-profiles.js.map +2 -2
  76. package/dist/src/tools/mercadolibre/__tests__/get-order-billing-info.test.js +339 -0
  77. package/dist/src/tools/mercadolibre/__tests__/get-order-billing-info.test.js.map +7 -0
  78. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-group-ads.test.js +225 -0
  79. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-group-ads.test.js.map +7 -0
  80. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-groups.test.js +148 -0
  81. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-groups.test.js.map +7 -0
  82. package/dist/src/tools/mercadolibre/__tests__/product-ads-campaigns.test.js +60 -0
  83. package/dist/src/tools/mercadolibre/__tests__/product-ads-campaigns.test.js.map +7 -0
  84. package/dist/src/tools/mercadolibre/__tests__/product-ads-helpers.test.js +128 -0
  85. package/dist/src/tools/mercadolibre/__tests__/product-ads-helpers.test.js.map +7 -0
  86. package/dist/src/tools/mercadolibre/__tests__/product-ads-tools.test.js +435 -0
  87. package/dist/src/tools/mercadolibre/__tests__/product-ads-tools.test.js.map +7 -0
  88. package/dist/src/tools/mercadolibre/__tests__/reconcile-buyer-data.test.js +31 -0
  89. package/dist/src/tools/mercadolibre/__tests__/reconcile-buyer-data.test.js.map +7 -0
  90. package/dist/src/tools/mercadolibre/__tests__/reconcile-claim-data.test.js +36 -0
  91. package/dist/src/tools/mercadolibre/__tests__/reconcile-claim-data.test.js.map +7 -0
  92. package/dist/src/tools/mercadolibre/__tests__/validate-connection.test.js +205 -0
  93. package/dist/src/tools/mercadolibre/__tests__/validate-connection.test.js.map +7 -0
  94. package/dist/src/tools/mercadolibre/get-billing-provisions-by-order.js +313 -0
  95. package/dist/src/tools/mercadolibre/get-billing-provisions-by-order.js.map +7 -0
  96. package/dist/src/tools/mercadolibre/get-bundle-context-by-item.js +176 -0
  97. package/dist/src/tools/mercadolibre/get-bundle-context-by-item.js.map +7 -0
  98. package/dist/src/tools/mercadolibre/get-full-storage-charges.js +183 -0
  99. package/dist/src/tools/mercadolibre/get-full-storage-charges.js.map +7 -0
  100. package/dist/src/tools/mercadolibre/get-order-billing-info.js +189 -0
  101. package/dist/src/tools/mercadolibre/get-order-billing-info.js.map +7 -0
  102. package/dist/src/tools/mercadolibre/get-pack-context-by-order.js +150 -0
  103. package/dist/src/tools/mercadolibre/get-pack-context-by-order.js.map +7 -0
  104. package/dist/src/tools/mercadolibre/get-payment-details.js +322 -0
  105. package/dist/src/tools/mercadolibre/get-payment-details.js.map +7 -0
  106. package/dist/src/tools/mercadolibre/get-product-ads-ad-group-ads.js +144 -0
  107. package/dist/src/tools/mercadolibre/get-product-ads-ad-group-ads.js.map +7 -0
  108. package/dist/src/tools/mercadolibre/get-product-ads-advertisers.js +59 -0
  109. package/dist/src/tools/mercadolibre/get-product-ads-advertisers.js.map +7 -0
  110. package/dist/src/tools/mercadolibre/get-shipping-quote-by-declared-dimensions.js +104 -0
  111. package/dist/src/tools/mercadolibre/get-shipping-quote-by-declared-dimensions.js.map +7 -0
  112. package/dist/src/tools/mercadolibre/index.js +32 -0
  113. package/dist/src/tools/mercadolibre/index.js.map +2 -2
  114. package/dist/src/tools/mercadolibre/product-ads-helpers.js +130 -0
  115. package/dist/src/tools/mercadolibre/product-ads-helpers.js.map +7 -0
  116. package/dist/src/tools/mercadolibre/reconcile-buyer-data.js +27 -0
  117. package/dist/src/tools/mercadolibre/reconcile-buyer-data.js.map +7 -0
  118. package/dist/src/tools/mercadolibre/reconcile-claim-data.js +46 -0
  119. package/dist/src/tools/mercadolibre/reconcile-claim-data.js.map +7 -0
  120. package/dist/src/tools/mercadolibre/reconcile-return-data.js +25 -0
  121. package/dist/src/tools/mercadolibre/reconcile-return-data.js.map +7 -0
  122. package/dist/src/tools/mercadolibre/search-product-ads-ad-groups.js +162 -0
  123. package/dist/src/tools/mercadolibre/search-product-ads-ad-groups.js.map +7 -0
  124. package/dist/src/tools/mercadolibre/search-product-ads-ads.js +216 -0
  125. package/dist/src/tools/mercadolibre/search-product-ads-ads.js.map +7 -0
  126. package/dist/src/tools/mercadolibre/search-product-ads-campaigns.js +116 -0
  127. package/dist/src/tools/mercadolibre/search-product-ads-campaigns.js.map +7 -0
  128. package/dist/src/tools/mercadolibre/validate-connection.js +40 -12
  129. package/dist/src/tools/mercadolibre/validate-connection.js.map +2 -2
  130. package/dist/tests/meli/mercadolibre-billing-provisions.test.js +378 -0
  131. package/dist/tests/meli/mercadolibre-billing-provisions.test.js.map +2 -2
  132. package/dist/tests/meli/mercadolibre-order-billing-info-surface.test.js +123 -0
  133. package/dist/tests/meli/mercadolibre-order-billing-info-surface.test.js.map +7 -0
  134. package/dist/tests/meli/mercadolibre-payment-details.test.js +356 -0
  135. package/dist/tests/meli/mercadolibre-payment-details.test.js.map +7 -0
  136. package/dist/tests/meli/mercadolibre-payments.test.js +62 -0
  137. package/dist/tests/meli/mercadolibre-payments.test.js.map +7 -0
  138. package/dist/tests/meli/mercadolibre-product-ads-ad-groups-surface.test.js +90 -0
  139. package/dist/tests/meli/mercadolibre-product-ads-ad-groups-surface.test.js.map +7 -0
  140. package/dist/tests/meli/mercadolibre-product-ads-campaigns-surface.test.js +72 -0
  141. package/dist/tests/meli/mercadolibre-product-ads-campaigns-surface.test.js.map +7 -0
  142. package/dist/tests/meli/mercadolibre-tool-handlers-pack-kit-shipping.test.js +400 -0
  143. package/dist/tests/meli/mercadolibre-tool-handlers-pack-kit-shipping.test.js.map +7 -0
  144. package/dist/tests/meli/mercadolibre-tool-handlers-small.test.js +31 -12
  145. package/dist/tests/meli/mercadolibre-tool-handlers-small.test.js.map +2 -2
  146. package/dist/tests/vtex/vtex-commercial-conditions-docs.test.js +1 -1
  147. package/dist/tests/vtex/vtex-commercial-conditions-docs.test.js.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/src/google-ads/report-utils.js +0 -78
  150. package/dist/src/google-ads/report-utils.js.map +0 -7
  151. package/dist/src/search-console/search-console-utils.js +0 -275
  152. package/dist/src/search-console/search-console-utils.js.map +0 -7
  153. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js +0 -165
  154. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js.map +0 -7
  155. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js +0 -233
  156. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js.map +0 -7
  157. package/dist/src/tools/vtex/catalog-admin-batch.js +0 -298
  158. package/dist/src/tools/vtex/catalog-admin-batch.js.map +0 -7
@@ -87,10 +87,28 @@ async function listProfileGoogleServiceMappings() {
87
87
  throw error;
88
88
  }
89
89
  }
90
+ async function listActiveProfileGtmMappingSummaries() {
91
+ try {
92
+ const result = await getDb().query(
93
+ `select profile_id, count(*) as active_mapping_count
94
+ from profile_gtm_mappings
95
+ where status = 'active'
96
+ group by profile_id`
97
+ );
98
+ return result.rows.map((row) => ({
99
+ profileId: row.profile_id,
100
+ activeMappingCount: Number(row.active_mapping_count)
101
+ }));
102
+ } catch (error) {
103
+ if (isMissingRelationError(error)) return [];
104
+ throw error;
105
+ }
106
+ }
90
107
  export {
91
108
  getActiveGoogleRefreshTokenFromStore,
92
109
  getGoogleOAuthConnection,
93
110
  getProfileGoogleServiceMapping,
111
+ listActiveProfileGtmMappingSummaries,
94
112
  listProfileGoogleServiceMappings
95
113
  };
96
114
  //# sourceMappingURL=google-store.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/config/google-store.ts"],
4
- "sourcesContent": ["import { getDb } from '../db/client.js';\nimport { decryptSecret } from './vtex-crypto.js';\n\nexport type GoogleServiceStatus = 'active' | 'disabled' | 'expired' | 'error' | 'pending';\n\nexport interface GoogleOAuthConnectionRecord {\n id: string;\n encryptedRefreshToken: string | null;\n scopes: string[];\n status: GoogleServiceStatus;\n googleAccountEmail: string | null;\n lastValidatedAt: Date | null;\n lastError: string | null;\n}\n\nexport interface ProfileGoogleServiceMappingRecord {\n profileId: string;\n googleAdsCustomerId: string | null;\n googleAdsLabel: string | null;\n googleAdsStatus: GoogleServiceStatus;\n googleAdsLastValidatedAt: Date | null;\n googleAdsLastError: string | null;\n ga4PropertyId: string | null;\n ga4PropertyLabel: string | null;\n ga4Status: GoogleServiceStatus;\n ga4LastValidatedAt: Date | null;\n ga4LastError: string | null;\n searchConsoleSiteUrl: string | null;\n searchConsoleLabel: string | null;\n searchConsoleStatus: GoogleServiceStatus;\n searchConsoleLastValidatedAt: Date | null;\n searchConsoleLastError: string | null;\n updatedAt: Date | null;\n}\n\ninterface GoogleOAuthConnectionRow {\n id: string;\n encrypted_refresh_token: string | null;\n scopes: string[] | null;\n status: GoogleServiceStatus;\n google_account_email: string | null;\n last_validated_at: Date | null;\n last_error: string | null;\n}\n\ninterface ProfileGoogleServiceMappingRow {\n profile_id: string;\n google_ads_customer_id: string | null;\n google_ads_label: string | null;\n google_ads_status: GoogleServiceStatus;\n google_ads_last_validated_at: Date | null;\n google_ads_last_error: string | null;\n ga4_property_id: string | null;\n ga4_property_label: string | null;\n ga4_status: GoogleServiceStatus;\n ga4_last_validated_at: Date | null;\n ga4_last_error: string | null;\n search_console_site_url: string | null;\n search_console_label: string | null;\n search_console_status: GoogleServiceStatus;\n search_console_last_validated_at: Date | null;\n search_console_last_error: string | null;\n updated_at: Date | null;\n}\n\nfunction isMissingRelationError(error: unknown): boolean {\n return error instanceof Error && /does not exist/i.test(error.message);\n}\n\nfunction mapConnection(row: GoogleOAuthConnectionRow): GoogleOAuthConnectionRecord {\n return {\n id: row.id,\n encryptedRefreshToken: row.encrypted_refresh_token,\n scopes: Array.isArray(row.scopes) ? row.scopes : [],\n status: row.status,\n googleAccountEmail: row.google_account_email,\n lastValidatedAt: row.last_validated_at,\n lastError: row.last_error,\n };\n}\n\nfunction mapMapping(row: ProfileGoogleServiceMappingRow): ProfileGoogleServiceMappingRecord {\n return {\n profileId: row.profile_id,\n googleAdsCustomerId: row.google_ads_customer_id,\n googleAdsLabel: row.google_ads_label,\n googleAdsStatus: row.google_ads_status,\n googleAdsLastValidatedAt: row.google_ads_last_validated_at,\n googleAdsLastError: row.google_ads_last_error,\n ga4PropertyId: row.ga4_property_id,\n ga4PropertyLabel: row.ga4_property_label,\n ga4Status: row.ga4_status,\n ga4LastValidatedAt: row.ga4_last_validated_at,\n ga4LastError: row.ga4_last_error,\n searchConsoleSiteUrl: row.search_console_site_url,\n searchConsoleLabel: row.search_console_label,\n searchConsoleStatus: row.search_console_status,\n searchConsoleLastValidatedAt: row.search_console_last_validated_at,\n searchConsoleLastError: row.search_console_last_error,\n updatedAt: row.updated_at,\n };\n}\n\nexport async function getGoogleOAuthConnection(): Promise<GoogleOAuthConnectionRecord | null> {\n try {\n const result = await getDb().query<GoogleOAuthConnectionRow>(\n `select id, encrypted_refresh_token, scopes, status, google_account_email, last_validated_at, last_error\n from google_oauth_connections\n where id = 'global'`\n );\n const row = result.rows[0];\n return row ? mapConnection(row) : null;\n } catch (error) {\n if (isMissingRelationError(error)) return null;\n throw error;\n }\n}\n\nexport async function getActiveGoogleRefreshTokenFromStore(): Promise<string | null> {\n const connection = await getGoogleOAuthConnection();\n if (!connection || connection.status !== 'active' || !connection.encryptedRefreshToken) return null;\n return decryptSecret(connection.encryptedRefreshToken);\n}\n\nexport async function getProfileGoogleServiceMapping(profileId: string): Promise<ProfileGoogleServiceMappingRecord | null> {\n try {\n const result = await getDb().query<ProfileGoogleServiceMappingRow>(\n `select profile_id, google_ads_customer_id, google_ads_label, google_ads_status, google_ads_last_validated_at, google_ads_last_error,\n ga4_property_id, ga4_property_label, ga4_status, ga4_last_validated_at, ga4_last_error,\n search_console_site_url, search_console_label, search_console_status, search_console_last_validated_at, search_console_last_error,\n updated_at\n from profile_google_service_mappings\n where profile_id = $1`,\n [profileId]\n );\n const row = result.rows[0];\n return row ? mapMapping(row) : null;\n } catch (error) {\n if (isMissingRelationError(error)) return null;\n throw error;\n }\n}\n\nexport async function listProfileGoogleServiceMappings(): Promise<ProfileGoogleServiceMappingRecord[]> {\n try {\n const result = await getDb().query<ProfileGoogleServiceMappingRow>(\n `select profile_id, google_ads_customer_id, google_ads_label, google_ads_status, google_ads_last_validated_at, google_ads_last_error,\n ga4_property_id, ga4_property_label, ga4_status, ga4_last_validated_at, ga4_last_error,\n search_console_site_url, search_console_label, search_console_status, search_console_last_validated_at, search_console_last_error,\n updated_at\n from profile_google_service_mappings`\n );\n return result.rows.map(mapMapping);\n } catch (error) {\n if (isMissingRelationError(error)) return [];\n throw error;\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAgE9B,SAAS,uBAAuB,OAAyB;AACvD,SAAO,iBAAiB,SAAS,kBAAkB,KAAK,MAAM,OAAO;AACvE;AAEA,SAAS,cAAc,KAA4D;AACjF,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,uBAAuB,IAAI;AAAA,IAC3B,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,CAAC;AAAA,IAClD,QAAQ,IAAI;AAAA,IACZ,oBAAoB,IAAI;AAAA,IACxB,iBAAiB,IAAI;AAAA,IACrB,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,SAAS,WAAW,KAAwE;AAC1F,SAAO;AAAA,IACL,WAAW,IAAI;AAAA,IACf,qBAAqB,IAAI;AAAA,IACzB,gBAAgB,IAAI;AAAA,IACpB,iBAAiB,IAAI;AAAA,IACrB,0BAA0B,IAAI;AAAA,IAC9B,oBAAoB,IAAI;AAAA,IACxB,eAAe,IAAI;AAAA,IACnB,kBAAkB,IAAI;AAAA,IACtB,WAAW,IAAI;AAAA,IACf,oBAAoB,IAAI;AAAA,IACxB,cAAc,IAAI;AAAA,IAClB,sBAAsB,IAAI;AAAA,IAC1B,oBAAoB,IAAI;AAAA,IACxB,qBAAqB,IAAI;AAAA,IACzB,8BAA8B,IAAI;AAAA,IAClC,wBAAwB,IAAI;AAAA,IAC5B,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,eAAsB,2BAAwE;AAC5F,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA,IAGF;AACA,UAAM,MAAM,OAAO,KAAK,CAAC;AACzB,WAAO,MAAM,cAAc,GAAG,IAAI;AAAA,EACpC,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO;AAC1C,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,uCAA+D;AACnF,QAAM,aAAa,MAAM,yBAAyB;AAClD,MAAI,CAAC,cAAc,WAAW,WAAW,YAAY,CAAC,WAAW,sBAAuB,QAAO;AAC/F,SAAO,cAAc,WAAW,qBAAqB;AACvD;AAEA,eAAsB,+BAA+B,WAAsE;AACzH,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,CAAC,SAAS;AAAA,IACZ;AACA,UAAM,MAAM,OAAO,KAAK,CAAC;AACzB,WAAO,MAAM,WAAW,GAAG,IAAI;AAAA,EACjC,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO;AAC1C,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,mCAAiF;AACrG,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKF;AACA,WAAO,OAAO,KAAK,IAAI,UAAU;AAAA,EACnC,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO,CAAC;AAC3C,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import { getDb } from '../db/client.js';\nimport { decryptSecret } from './vtex-crypto.js';\n\nexport type GoogleServiceStatus = 'active' | 'disabled' | 'expired' | 'error' | 'pending';\n\nexport interface GoogleOAuthConnectionRecord {\n id: string;\n encryptedRefreshToken: string | null;\n scopes: string[];\n status: GoogleServiceStatus;\n googleAccountEmail: string | null;\n lastValidatedAt: Date | null;\n lastError: string | null;\n}\n\nexport interface ProfileGoogleServiceMappingRecord {\n profileId: string;\n googleAdsCustomerId: string | null;\n googleAdsLabel: string | null;\n googleAdsStatus: GoogleServiceStatus;\n googleAdsLastValidatedAt: Date | null;\n googleAdsLastError: string | null;\n ga4PropertyId: string | null;\n ga4PropertyLabel: string | null;\n ga4Status: GoogleServiceStatus;\n ga4LastValidatedAt: Date | null;\n ga4LastError: string | null;\n searchConsoleSiteUrl: string | null;\n searchConsoleLabel: string | null;\n searchConsoleStatus: GoogleServiceStatus;\n searchConsoleLastValidatedAt: Date | null;\n searchConsoleLastError: string | null;\n updatedAt: Date | null;\n}\n\nexport interface ProfileGtmMappingSummaryRecord {\n profileId: string;\n activeMappingCount: number;\n}\n\ninterface GoogleOAuthConnectionRow {\n id: string;\n encrypted_refresh_token: string | null;\n scopes: string[] | null;\n status: GoogleServiceStatus;\n google_account_email: string | null;\n last_validated_at: Date | null;\n last_error: string | null;\n}\n\ninterface ProfileGoogleServiceMappingRow {\n profile_id: string;\n google_ads_customer_id: string | null;\n google_ads_label: string | null;\n google_ads_status: GoogleServiceStatus;\n google_ads_last_validated_at: Date | null;\n google_ads_last_error: string | null;\n ga4_property_id: string | null;\n ga4_property_label: string | null;\n ga4_status: GoogleServiceStatus;\n ga4_last_validated_at: Date | null;\n ga4_last_error: string | null;\n search_console_site_url: string | null;\n search_console_label: string | null;\n search_console_status: GoogleServiceStatus;\n search_console_last_validated_at: Date | null;\n search_console_last_error: string | null;\n updated_at: Date | null;\n}\n\ninterface ProfileGtmMappingSummaryRow {\n profile_id: string;\n active_mapping_count: string | number;\n}\n\nfunction isMissingRelationError(error: unknown): boolean {\n return error instanceof Error && /does not exist/i.test(error.message);\n}\n\nfunction mapConnection(row: GoogleOAuthConnectionRow): GoogleOAuthConnectionRecord {\n return {\n id: row.id,\n encryptedRefreshToken: row.encrypted_refresh_token,\n scopes: Array.isArray(row.scopes) ? row.scopes : [],\n status: row.status,\n googleAccountEmail: row.google_account_email,\n lastValidatedAt: row.last_validated_at,\n lastError: row.last_error,\n };\n}\n\nfunction mapMapping(row: ProfileGoogleServiceMappingRow): ProfileGoogleServiceMappingRecord {\n return {\n profileId: row.profile_id,\n googleAdsCustomerId: row.google_ads_customer_id,\n googleAdsLabel: row.google_ads_label,\n googleAdsStatus: row.google_ads_status,\n googleAdsLastValidatedAt: row.google_ads_last_validated_at,\n googleAdsLastError: row.google_ads_last_error,\n ga4PropertyId: row.ga4_property_id,\n ga4PropertyLabel: row.ga4_property_label,\n ga4Status: row.ga4_status,\n ga4LastValidatedAt: row.ga4_last_validated_at,\n ga4LastError: row.ga4_last_error,\n searchConsoleSiteUrl: row.search_console_site_url,\n searchConsoleLabel: row.search_console_label,\n searchConsoleStatus: row.search_console_status,\n searchConsoleLastValidatedAt: row.search_console_last_validated_at,\n searchConsoleLastError: row.search_console_last_error,\n updatedAt: row.updated_at,\n };\n}\n\nexport async function getGoogleOAuthConnection(): Promise<GoogleOAuthConnectionRecord | null> {\n try {\n const result = await getDb().query<GoogleOAuthConnectionRow>(\n `select id, encrypted_refresh_token, scopes, status, google_account_email, last_validated_at, last_error\n from google_oauth_connections\n where id = 'global'`\n );\n const row = result.rows[0];\n return row ? mapConnection(row) : null;\n } catch (error) {\n if (isMissingRelationError(error)) return null;\n throw error;\n }\n}\n\nexport async function getActiveGoogleRefreshTokenFromStore(): Promise<string | null> {\n const connection = await getGoogleOAuthConnection();\n if (!connection || connection.status !== 'active' || !connection.encryptedRefreshToken) return null;\n return decryptSecret(connection.encryptedRefreshToken);\n}\n\nexport async function getProfileGoogleServiceMapping(profileId: string): Promise<ProfileGoogleServiceMappingRecord | null> {\n try {\n const result = await getDb().query<ProfileGoogleServiceMappingRow>(\n `select profile_id, google_ads_customer_id, google_ads_label, google_ads_status, google_ads_last_validated_at, google_ads_last_error,\n ga4_property_id, ga4_property_label, ga4_status, ga4_last_validated_at, ga4_last_error,\n search_console_site_url, search_console_label, search_console_status, search_console_last_validated_at, search_console_last_error,\n updated_at\n from profile_google_service_mappings\n where profile_id = $1`,\n [profileId]\n );\n const row = result.rows[0];\n return row ? mapMapping(row) : null;\n } catch (error) {\n if (isMissingRelationError(error)) return null;\n throw error;\n }\n}\n\nexport async function listProfileGoogleServiceMappings(): Promise<ProfileGoogleServiceMappingRecord[]> {\n try {\n const result = await getDb().query<ProfileGoogleServiceMappingRow>(\n `select profile_id, google_ads_customer_id, google_ads_label, google_ads_status, google_ads_last_validated_at, google_ads_last_error,\n ga4_property_id, ga4_property_label, ga4_status, ga4_last_validated_at, ga4_last_error,\n search_console_site_url, search_console_label, search_console_status, search_console_last_validated_at, search_console_last_error,\n updated_at\n from profile_google_service_mappings`\n );\n return result.rows.map(mapMapping);\n } catch (error) {\n if (isMissingRelationError(error)) return [];\n throw error;\n }\n}\n\nexport async function listActiveProfileGtmMappingSummaries(): Promise<\n ProfileGtmMappingSummaryRecord[]\n> {\n try {\n const result = await getDb().query<ProfileGtmMappingSummaryRow>(\n `select profile_id, count(*) as active_mapping_count\n from profile_gtm_mappings\n where status = 'active'\n group by profile_id`,\n );\n return result.rows.map((row) => ({\n profileId: row.profile_id,\n activeMappingCount: Number(row.active_mapping_count),\n }));\n } catch (error) {\n if (isMissingRelationError(error)) return [];\n throw error;\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AA0E9B,SAAS,uBAAuB,OAAyB;AACvD,SAAO,iBAAiB,SAAS,kBAAkB,KAAK,MAAM,OAAO;AACvE;AAEA,SAAS,cAAc,KAA4D;AACjF,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,uBAAuB,IAAI;AAAA,IAC3B,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,CAAC;AAAA,IAClD,QAAQ,IAAI;AAAA,IACZ,oBAAoB,IAAI;AAAA,IACxB,iBAAiB,IAAI;AAAA,IACrB,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,SAAS,WAAW,KAAwE;AAC1F,SAAO;AAAA,IACL,WAAW,IAAI;AAAA,IACf,qBAAqB,IAAI;AAAA,IACzB,gBAAgB,IAAI;AAAA,IACpB,iBAAiB,IAAI;AAAA,IACrB,0BAA0B,IAAI;AAAA,IAC9B,oBAAoB,IAAI;AAAA,IACxB,eAAe,IAAI;AAAA,IACnB,kBAAkB,IAAI;AAAA,IACtB,WAAW,IAAI;AAAA,IACf,oBAAoB,IAAI;AAAA,IACxB,cAAc,IAAI;AAAA,IAClB,sBAAsB,IAAI;AAAA,IAC1B,oBAAoB,IAAI;AAAA,IACxB,qBAAqB,IAAI;AAAA,IACzB,8BAA8B,IAAI;AAAA,IAClC,wBAAwB,IAAI;AAAA,IAC5B,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,eAAsB,2BAAwE;AAC5F,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA,IAGF;AACA,UAAM,MAAM,OAAO,KAAK,CAAC;AACzB,WAAO,MAAM,cAAc,GAAG,IAAI;AAAA,EACpC,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO;AAC1C,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,uCAA+D;AACnF,QAAM,aAAa,MAAM,yBAAyB;AAClD,MAAI,CAAC,cAAc,WAAW,WAAW,YAAY,CAAC,WAAW,sBAAuB,QAAO;AAC/F,SAAO,cAAc,WAAW,qBAAqB;AACvD;AAEA,eAAsB,+BAA+B,WAAsE;AACzH,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,CAAC,SAAS;AAAA,IACZ;AACA,UAAM,MAAM,OAAO,KAAK,CAAC;AACzB,WAAO,MAAM,WAAW,GAAG,IAAI;AAAA,EACjC,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO;AAC1C,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,mCAAiF;AACrG,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKF;AACA,WAAO,OAAO,KAAK,IAAI,UAAU;AAAA,EACnC,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO,CAAC;AAC3C,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,uCAEpB;AACA,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA;AAAA,IAIF;AACA,WAAO,OAAO,KAAK,IAAI,CAAC,SAAS;AAAA,MAC/B,WAAW,IAAI;AAAA,MACf,oBAAoB,OAAO,IAAI,oBAAoB;AAAA,IACrD,EAAE;AAAA,EACJ,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO,CAAC;AAC3C,UAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,34 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const deadlineContext = new AsyncLocalStorage();
3
+ function runWithMcpToolDeadlineContext(signal, callback, deadlineAt) {
4
+ return deadlineContext.run({ signal, deadlineAt }, callback);
5
+ }
6
+ function getMcpToolDeadlineSignal() {
7
+ return deadlineContext.getStore()?.signal;
8
+ }
9
+ function getMcpToolDeadlineRemainingMs(now = Date.now()) {
10
+ const deadlineAt = deadlineContext.getStore()?.deadlineAt;
11
+ return deadlineAt === void 0 ? void 0 : Math.max(0, deadlineAt - now);
12
+ }
13
+ function composeMcpToolDeadlineSignal(signal) {
14
+ const deadlineSignal = getMcpToolDeadlineSignal();
15
+ if (!deadlineSignal) return signal;
16
+ if (!signal || signal === deadlineSignal) return deadlineSignal;
17
+ if (!signal.addEventListener) return deadlineSignal;
18
+ const controller = new AbortController();
19
+ const abort = () => controller.abort();
20
+ if (deadlineSignal.aborted || signal.aborted) {
21
+ controller.abort();
22
+ } else {
23
+ deadlineSignal.addEventListener("abort", abort, { once: true });
24
+ signal.addEventListener("abort", abort, { once: true });
25
+ }
26
+ return controller.signal;
27
+ }
28
+ export {
29
+ composeMcpToolDeadlineSignal,
30
+ getMcpToolDeadlineRemainingMs,
31
+ getMcpToolDeadlineSignal,
32
+ runWithMcpToolDeadlineContext
33
+ };
34
+ //# sourceMappingURL=mcp-tool-deadline-context.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/config/mcp-tool-deadline-context.ts"],
4
+ "sourcesContent": ["import { AsyncLocalStorage } from \"node:async_hooks\";\n\ntype DeadlineContext = { signal: AbortSignal; deadlineAt?: number };\ntype CompatibleAbortSignal = {\n readonly aborted: boolean;\n addEventListener?: (...args: any[]) => any;\n};\n\nconst deadlineContext = new AsyncLocalStorage<DeadlineContext>();\n\nexport function runWithMcpToolDeadlineContext<T>(\n signal: AbortSignal,\n callback: () => Promise<T>,\n deadlineAt?: number,\n): Promise<T> {\n return deadlineContext.run({ signal, deadlineAt }, callback);\n}\n\nexport function getMcpToolDeadlineSignal(): AbortSignal | undefined {\n return deadlineContext.getStore()?.signal;\n}\n\nexport function getMcpToolDeadlineRemainingMs(now = Date.now()): number | undefined {\n const deadlineAt = deadlineContext.getStore()?.deadlineAt;\n return deadlineAt === undefined ? undefined : Math.max(0, deadlineAt - now);\n}\n\nexport function composeMcpToolDeadlineSignal(signal?: CompatibleAbortSignal): CompatibleAbortSignal | undefined {\n const deadlineSignal = getMcpToolDeadlineSignal();\n if (!deadlineSignal) return signal;\n if (!signal || signal === deadlineSignal) return deadlineSignal;\n\n if (!signal.addEventListener) return deadlineSignal;\n\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (deadlineSignal.aborted || signal.aborted) {\n controller.abort();\n } else {\n deadlineSignal.addEventListener(\"abort\", abort, { once: true });\n signal.addEventListener(\"abort\", abort, { once: true });\n }\n return controller.signal;\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAyB;AAQlC,MAAM,kBAAkB,IAAI,kBAAmC;AAExD,SAAS,8BACd,QACA,UACA,YACY;AACZ,SAAO,gBAAgB,IAAI,EAAE,QAAQ,WAAW,GAAG,QAAQ;AAC7D;AAEO,SAAS,2BAAoD;AAClE,SAAO,gBAAgB,SAAS,GAAG;AACrC;AAEO,SAAS,8BAA8B,MAAM,KAAK,IAAI,GAAuB;AAClF,QAAM,aAAa,gBAAgB,SAAS,GAAG;AAC/C,SAAO,eAAe,SAAY,SAAY,KAAK,IAAI,GAAG,aAAa,GAAG;AAC5E;AAEO,SAAS,6BAA6B,QAAmE;AAC9G,QAAM,iBAAiB,yBAAyB;AAChD,MAAI,CAAC,eAAgB,QAAO;AAC5B,MAAI,CAAC,UAAU,WAAW,eAAgB,QAAO;AAEjD,MAAI,CAAC,OAAO,iBAAkB,QAAO;AAErC,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,MAAM,WAAW,MAAM;AACrC,MAAI,eAAe,WAAW,OAAO,SAAS;AAC5C,eAAW,MAAM;AAAA,EACnB,OAAO;AACL,mBAAe,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9D,WAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AAAA,EACxD;AACA,SAAO,WAAW;AACpB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,67 @@
1
+ import { error } from "mcp-use/server";
2
+ import { runWithMcpToolDeadlineContext } from "./mcp-tool-deadline-context.js";
3
+ const DEFAULT_MCP_TOOL_TOTAL_DEADLINE_MS = 3e5;
4
+ const MCP_TOOL_TOTAL_DEADLINE_ENV = "MCP_TOOL_TOTAL_DEADLINE_MS";
5
+ function parsePositiveInteger(value, fallback) {
6
+ if (value === void 0 || value === "") return fallback;
7
+ if (!/^\d+$/.test(value)) {
8
+ throw new Error(`${MCP_TOOL_TOTAL_DEADLINE_ENV} must be a positive integer`);
9
+ }
10
+ const parsed = Number(value);
11
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
12
+ throw new Error(`${MCP_TOOL_TOTAL_DEADLINE_ENV} must be a positive integer`);
13
+ }
14
+ return parsed;
15
+ }
16
+ const MCP_TOOL_TOTAL_DEADLINE_MS = parsePositiveInteger(
17
+ process.env[MCP_TOOL_TOTAL_DEADLINE_ENV],
18
+ DEFAULT_MCP_TOOL_TOTAL_DEADLINE_MS
19
+ );
20
+ function createMcpToolDeadlineMiddleware(deadlineMs = MCP_TOOL_TOTAL_DEADLINE_MS, options = {}) {
21
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0) {
22
+ throw new Error("MCP tool deadline must be a positive integer");
23
+ }
24
+ return async (ctx, next) => {
25
+ const startedAt = Date.now();
26
+ const controller = new AbortController();
27
+ let timer;
28
+ let timedOut = false;
29
+ const operation = runWithMcpToolDeadlineContext(controller.signal, next, startedAt + deadlineMs);
30
+ void operation.catch(() => void 0);
31
+ try {
32
+ return await Promise.race([
33
+ operation,
34
+ new Promise((resolve) => {
35
+ timer = setTimeout(
36
+ () => {
37
+ timedOut = true;
38
+ controller.abort();
39
+ const event = {
40
+ tool_name: typeof ctx.params?.name === "string" ? ctx.params.name : "unknown",
41
+ elapsed_ms: Math.max(0, Date.now() - startedAt),
42
+ deadline_ms: deadlineMs,
43
+ outcome: "deadline_exceeded"
44
+ };
45
+ (options.onDeadlineExceeded ?? ((deadlineEvent) => {
46
+ console.warn("[MCP TOOL DEADLINE]", JSON.stringify(deadlineEvent));
47
+ }))(event);
48
+ resolve(error(`MCP tool deadline exceeded (deadline_ms=${deadlineMs}; cancellation=cooperative; non_cooperative_boundaries=present)`));
49
+ },
50
+ deadlineMs
51
+ );
52
+ })
53
+ ]);
54
+ } finally {
55
+ if (timer) clearTimeout(timer);
56
+ if (!timedOut) controller.abort();
57
+ }
58
+ };
59
+ }
60
+ export {
61
+ DEFAULT_MCP_TOOL_TOTAL_DEADLINE_MS,
62
+ MCP_TOOL_TOTAL_DEADLINE_ENV,
63
+ MCP_TOOL_TOTAL_DEADLINE_MS,
64
+ createMcpToolDeadlineMiddleware,
65
+ parsePositiveInteger
66
+ };
67
+ //# sourceMappingURL=mcp-tool-deadline.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/config/mcp-tool-deadline.ts"],
4
+ "sourcesContent": ["import { error } from \"mcp-use/server\";\n\nimport { runWithMcpToolDeadlineContext } from \"./mcp-tool-deadline-context.js\";\n\nexport const DEFAULT_MCP_TOOL_TOTAL_DEADLINE_MS = 300_000;\nexport const MCP_TOOL_TOTAL_DEADLINE_ENV = \"MCP_TOOL_TOTAL_DEADLINE_MS\";\n\nexport function parsePositiveInteger(value: string | undefined, fallback: number): number {\n if (value === undefined || value === \"\") return fallback;\n if (!/^\\d+$/.test(value)) {\n throw new Error(`${MCP_TOOL_TOTAL_DEADLINE_ENV} must be a positive integer`);\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed <= 0) {\n throw new Error(`${MCP_TOOL_TOTAL_DEADLINE_ENV} must be a positive integer`);\n }\n\n return parsed;\n}\n\nexport const MCP_TOOL_TOTAL_DEADLINE_MS = parsePositiveInteger(\n process.env[MCP_TOOL_TOTAL_DEADLINE_ENV],\n DEFAULT_MCP_TOOL_TOTAL_DEADLINE_MS,\n);\n\nexport interface McpToolDeadlineEvent {\n tool_name: string;\n elapsed_ms: number;\n deadline_ms: number;\n outcome: \"deadline_exceeded\";\n}\n\nexport interface McpToolDeadlineMiddlewareOptions {\n onDeadlineExceeded?: (event: McpToolDeadlineEvent) => void;\n}\n\nexport function createMcpToolDeadlineMiddleware(\n deadlineMs = MCP_TOOL_TOTAL_DEADLINE_MS,\n options: McpToolDeadlineMiddlewareOptions = {},\n) {\n if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0) {\n throw new Error(\"MCP tool deadline must be a positive integer\");\n }\n\n return async (ctx: { params?: { name?: unknown } }, next: () => Promise<unknown>) => {\n const startedAt = Date.now();\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const operation = runWithMcpToolDeadlineContext(controller.signal, next, startedAt + deadlineMs);\n // A late rejection must be observed after the caller-visible deadline response.\n void operation.catch(() => undefined);\n try {\n return await Promise.race([\n operation,\n new Promise<ReturnType<typeof error>>((resolve) => {\n timer = setTimeout(\n () => {\n timedOut = true;\n controller.abort();\n const event: McpToolDeadlineEvent = {\n tool_name: typeof ctx.params?.name === \"string\" ? ctx.params.name : \"unknown\",\n elapsed_ms: Math.max(0, Date.now() - startedAt),\n deadline_ms: deadlineMs,\n outcome: \"deadline_exceeded\",\n };\n (options.onDeadlineExceeded ?? ((deadlineEvent: McpToolDeadlineEvent) => {\n console.warn(\"[MCP TOOL DEADLINE]\", JSON.stringify(deadlineEvent));\n }))(event);\n resolve(error(`MCP tool deadline exceeded (deadline_ms=${deadlineMs}; cancellation=cooperative; non_cooperative_boundaries=present)`));\n },\n deadlineMs,\n );\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n if (!timedOut) controller.abort();\n }\n };\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AAEtB,SAAS,qCAAqC;AAEvC,MAAM,qCAAqC;AAC3C,MAAM,8BAA8B;AAEpC,SAAS,qBAAqB,OAA2B,UAA0B;AACxF,MAAI,UAAU,UAAa,UAAU,GAAI,QAAO;AAChD,MAAI,CAAC,QAAQ,KAAK,KAAK,GAAG;AACxB,UAAM,IAAI,MAAM,GAAG,2BAA2B,6BAA6B;AAAA,EAC7E;AAEA,QAAM,SAAS,OAAO,KAAK;AAC3B,MAAI,CAAC,OAAO,cAAc,MAAM,KAAK,UAAU,GAAG;AAChD,UAAM,IAAI,MAAM,GAAG,2BAA2B,6BAA6B;AAAA,EAC7E;AAEA,SAAO;AACT;AAEO,MAAM,6BAA6B;AAAA,EACxC,QAAQ,IAAI,2BAA2B;AAAA,EACvC;AACF;AAaO,SAAS,gCACd,aAAa,4BACb,UAA4C,CAAC,GAC7C;AACA,MAAI,CAAC,OAAO,cAAc,UAAU,KAAK,cAAc,GAAG;AACxD,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AAEA,SAAO,OAAO,KAAsC,SAAiC;AACnF,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI;AACJ,QAAI,WAAW;AACf,UAAM,YAAY,8BAA8B,WAAW,QAAQ,MAAM,YAAY,UAAU;AAE/F,SAAK,UAAU,MAAM,MAAM,MAAS;AACpC,QAAI;AACF,aAAO,MAAM,QAAQ,KAAK;AAAA,QACxB;AAAA,QACA,IAAI,QAAkC,CAAC,YAAY;AACjD,kBAAQ;AAAA,YACN,MAAM;AACJ,yBAAW;AACX,yBAAW,MAAM;AACjB,oBAAM,QAA8B;AAAA,gBAClC,WAAW,OAAO,IAAI,QAAQ,SAAS,WAAW,IAAI,OAAO,OAAO;AAAA,gBACpE,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,SAAS;AAAA,gBAC9C,aAAa;AAAA,gBACb,SAAS;AAAA,cACX;AACA,eAAC,QAAQ,uBAAuB,CAAC,kBAAwC;AACvE,wBAAQ,KAAK,uBAAuB,KAAK,UAAU,aAAa,CAAC;AAAA,cACnE,IAAI,KAAK;AACT,sBAAQ,MAAM,2CAA2C,UAAU,iEAAiE,CAAC;AAAA,YACvI;AAAA,YACA;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH,UAAE;AACA,UAAI,MAAO,cAAa,KAAK;AAC7B,UAAI,CAAC,SAAU,YAAW,MAAM;AAAA,IAClC;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -1,11 +1,13 @@
1
1
  import axios from "axios";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { getMercadoLibreAccessForProfile } from "../../../config/mercadolibre.js";
4
+ import { runWithMcpToolDeadlineContext } from "../../../config/mcp-tool-deadline-context.js";
4
5
  import {
5
6
  createMercadoLibreClient,
6
7
  formatMercadoLibreError,
7
8
  getMercadoLibreApiContext,
8
9
  getMercadoLibreUsersMe,
10
+ getMercadoLibreUsersMeWithAccessToken,
9
11
  isRetryableMercadoLibreError,
10
12
  MercadoLibreApiError,
11
13
  buildMercadoLibrePaginationMetadata,
@@ -193,16 +195,17 @@ describe("mercadolibre-api foundations", () => {
193
195
  );
194
196
  });
195
197
  it("creates a client with auth headers and converts axios failures into MercadoLibreApiError", async () => {
198
+ const requestUse = vi.fn();
196
199
  const responseUse = vi.fn();
197
200
  const client = {
198
- interceptors: { response: { use: responseUse } }
201
+ interceptors: { request: { use: requestUse }, response: { use: responseUse } }
199
202
  };
200
203
  vi.spyOn(axios, "create").mockReturnValue(client);
201
204
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => void 0);
202
205
  createMercadoLibreClient("access-token");
203
206
  expect(axios.create).toHaveBeenCalledWith({
204
207
  baseURL: "https://api.mercadolibre.com",
205
- timeout: 3e4,
208
+ timeout: 9e4,
206
209
  headers: {
207
210
  Accept: "application/json",
208
211
  "Content-Type": "application/json",
@@ -233,11 +236,22 @@ describe("mercadolibre-api foundations", () => {
233
236
  "[MERCADOLIBRE API ERROR] GET /orders/search -> 429"
234
237
  );
235
238
  });
239
+ it("composes the MCP deadline signal into the shared Axios client", async () => {
240
+ const requestUse = vi.fn();
241
+ vi.spyOn(axios, "create").mockReturnValue({
242
+ interceptors: { request: { use: requestUse }, response: { use: vi.fn() } }
243
+ });
244
+ createMercadoLibreClient("access-token");
245
+ const requestInterceptor = requestUse.mock.calls[0]?.[0];
246
+ const controller = new AbortController();
247
+ const request = await runWithMcpToolDeadlineContext(controller.signal, async () => requestInterceptor({}));
248
+ expect(request.signal).toBe(controller.signal);
249
+ });
236
250
  it("builds api context from the persisted profile access and exposes /users/me", async () => {
237
251
  const getMock = vi.fn().mockResolvedValue({ data: { id: 123, nickname: "seller" } });
238
252
  const client = {
239
253
  get: getMock,
240
- interceptors: { response: { use: vi.fn() } }
254
+ interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } }
241
255
  };
242
256
  vi.mocked(getMercadoLibreAccessForProfile).mockResolvedValue({
243
257
  sellerId: "seller-1",
@@ -256,6 +270,27 @@ describe("mercadolibre-api foundations", () => {
256
270
  expect(getMock).toHaveBeenCalledWith("/users/me");
257
271
  expect(me).toEqual({ id: 123, nickname: "seller" });
258
272
  });
273
+ it("probes /users/me with the explicitly supplied access token", async () => {
274
+ const getMock = vi.fn().mockResolvedValue({ data: { id: 456, nickname: "explicit-seller" } });
275
+ const client = {
276
+ get: getMock,
277
+ interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } }
278
+ };
279
+ vi.spyOn(axios, "create").mockReturnValue(client);
280
+ const me = await getMercadoLibreUsersMeWithAccessToken("explicit-access-token");
281
+ expect(axios.create).toHaveBeenCalledWith({
282
+ baseURL: "https://api.mercadolibre.com",
283
+ timeout: 9e4,
284
+ headers: {
285
+ Accept: "application/json",
286
+ "Content-Type": "application/json",
287
+ Authorization: "Bearer explicit-access-token"
288
+ }
289
+ });
290
+ expect(getMock).toHaveBeenCalledWith("/users/me");
291
+ expect(getMercadoLibreAccessForProfile).not.toHaveBeenCalled();
292
+ expect(me).toEqual({ id: 456, nickname: "explicit-seller" });
293
+ });
259
294
  it("detects retryable MercadoLibre errors including missing status codes", () => {
260
295
  expect(isRetryableMercadoLibreError(new MercadoLibreApiError("network"))).toBe(true);
261
296
  expect(isRetryableMercadoLibreError(new MercadoLibreApiError("forbidden", 403))).toBe(false);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/services/mercadolibre/__tests__/mercadolibre-api.test.ts"],
4
- "sourcesContent": ["import axios, { AxiosError } from \"axios\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { getMercadoLibreAccessForProfile } from \"../../../config/mercadolibre.js\";\nimport {\n createMercadoLibreClient,\n formatMercadoLibreError,\n getMercadoLibreApiContext,\n getMercadoLibreUsersMe,\n isRetryableMercadoLibreError,\n MercadoLibreApiError,\n buildMercadoLibrePaginationMetadata,\n normalizeMercadoLibrePaging,\n parseMercadoLibreMultiget,\n runMercadoLibreBatch,\n sanitizeMercadoLibreText,\n} from \"../mercadolibre-api.js\";\n\nvi.mock(\"../../../config/mercadolibre.js\", () => ({\n getMercadoLibreAccessForProfile: vi.fn(),\n}));\n\ndescribe(\"mercadolibre-api foundations\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n vi.restoreAllMocks();\n });\n\n it(\"redacts sensitive tokens from MercadoLibre errors\", () => {\n const sanitized = sanitizeMercadoLibreText(\n \"Bearer abc.def access_token=secret refresh_token=refresh client_secret=topsecret code=oauth\"\n );\n\n expect(sanitized).toContain(\"Bearer [redacted]\");\n expect(sanitized).toContain(\"access_token=[redacted]\");\n expect(sanitized).toContain(\"refresh_token=[redacted]\");\n expect(sanitized).toContain(\"client_secret=[redacted]\");\n expect(sanitized).toContain(\"code=[redacted]\");\n });\n\n it(\"normalizes paging values and builds continuation metadata\", () => {\n const paging = normalizeMercadoLibrePaging({ total: 25, limit: \"10\", offset: -4 });\n const metadata = buildMercadoLibrePaginationMetadata({\n total: paging.total,\n limit: paging.limit,\n offset: paging.offset,\n returned: 10,\n nextField: \"offset\",\n });\n\n expect(paging).toEqual({ total: 25, limit: 10, offset: 0 });\n expect(metadata).toEqual({\n total: 25,\n limit: 10,\n offset: 0,\n returned: 10,\n has_more: true,\n continuation: \"Reenviar la misma tool con offset=10 para continuar.\",\n });\n });\n\n it(\"normalizes invalid paging values and reports finished pagination when no more pages exist\", () => {\n const paging = normalizeMercadoLibrePaging({ total: \"oops\", limit: undefined, offset: Number.NaN });\n const metadata = buildMercadoLibrePaginationMetadata({\n total: 2,\n limit: 0,\n offset: 0,\n returned: 2,\n nextField: \"scroll_id\",\n });\n\n expect(paging).toEqual({ total: 0, limit: 0, offset: 0 });\n expect(metadata).toEqual({\n total: 2,\n limit: 2,\n offset: 0,\n returned: 2,\n has_more: false,\n continuation: \"No more pages for this query.\",\n });\n });\n\n it(\"filters multiget responses to successful bodies only\", async () => {\n const result = await parseMercadoLibreMultiget({\n data: [\n { code: 200, body: { id: \"MLA1\" } },\n { code: 404, body: { id: \"MLA2\" } },\n { code: 204 },\n { code: 201, body: { id: \"MLA3\" } },\n ],\n } as never);\n\n expect(result).toEqual([{ id: \"MLA1\" }, { id: \"MLA3\" }]);\n });\n\n it(\"returns an empty multiget payload when no successful bodies are present\", async () => {\n const result = await parseMercadoLibreMultiget({\n data: [{ code: 404, body: { id: \"missing\" } }, { code: 204 }, { code: 200 }],\n } as never);\n\n expect(result).toEqual([]);\n });\n\n it(\"retries retryable batch failures and keeps ordering stable\", async () => {\n vi.useFakeTimers();\n vi.spyOn(Math, \"random\").mockReturnValue(0);\n\n const fetcher = vi\n .fn<(id: string) => Promise<{ id: string; recovered?: boolean }>>()\n .mockImplementationOnce(async () => {\n throw new MercadoLibreApiError(\"rate limited\", 429);\n })\n .mockResolvedValueOnce({ id: \"one\", recovered: true })\n .mockResolvedValueOnce({ id: \"two\" });\n\n const promise = runMercadoLibreBatch([\"one\", \"two\"], fetcher, {\n maxConcurrency: 1,\n maxRetries: 1,\n baseRetryDelayMs: 50,\n });\n\n await vi.advanceTimersByTimeAsync(50);\n\n await expect(promise).resolves.toEqual({\n successful: [\n { id: \"one\", document: { id: \"one\", recovered: true } },\n { id: \"two\", document: { id: \"two\" } },\n ],\n failed: [],\n });\n expect(fetcher).toHaveBeenCalledTimes(3);\n });\n\n it(\"honors retry-after headers for retryable failures\", async () => {\n vi.useFakeTimers();\n\n const fetcher = vi\n .fn<(id: string) => Promise<{ id: string }>>()\n .mockImplementationOnce(async () => {\n throw new MercadoLibreApiError(\"wait\", 429, undefined, 120);\n })\n .mockResolvedValueOnce({ id: \"one\" });\n\n const promise = runMercadoLibreBatch([\"one\"], fetcher, {\n maxRetries: 1,\n baseRetryDelayMs: 50,\n });\n\n await vi.advanceTimersByTimeAsync(119);\n expect(fetcher).toHaveBeenCalledTimes(1);\n\n await vi.advanceTimersByTimeAsync(1);\n\n await expect(promise).resolves.toEqual({\n successful: [{ id: \"one\", document: { id: \"one\" } }],\n failed: [],\n });\n });\n\n it(\"classifies non-retryable batch failures with a safe message\", async () => {\n const result = await runMercadoLibreBatch(\n [\"broken\"],\n async () => {\n throw new Error(\"refresh_token=secret exploded\");\n },\n { maxRetries: 2 }\n );\n\n expect(result).toEqual({\n successful: [],\n failed: [\n {\n id: \"broken\",\n message: \"refresh_token=[redacted] exploded\",\n statusCode: undefined,\n attempts: 1,\n retryable: false,\n },\n ],\n });\n });\n\n it(\"treats unknown rejected batch reasons as a single failed attempt\", async () => {\n const fetcher = vi.fn(async () => {\n throw \"Bearer abc123\";\n });\n\n const result = await runMercadoLibreBatch([\"broken\"], fetcher, { maxRetries: 0 });\n\n expect(result).toEqual({\n successful: [],\n failed: [\n {\n id: \"broken\",\n message: \"Failed to fetch MercadoLibre resource broken\",\n statusCode: undefined,\n attempts: 1,\n retryable: false,\n },\n ],\n });\n });\n\n it(\"returns an empty batch result when no ids are provided\", async () => {\n const fetcher = vi.fn();\n\n await expect(runMercadoLibreBatch([], fetcher)).resolves.toEqual({\n successful: [],\n failed: [],\n });\n expect(fetcher).not.toHaveBeenCalled();\n });\n\n it(\"formats axios errors with sanitized response details\", () => {\n const error = {\n isAxiosError: true,\n message: \"Request failed with status code 400\",\n response: {\n status: 400,\n data: {\n error: \"bad_request\",\n cause: [{ message: \"refresh_token=top-secret\" }],\n },\n headers: {},\n },\n } as AxiosError;\n\n expect(formatMercadoLibreError(error)).toBe(\n \"MercadoLibre API request failed (400): refresh_token=[redacted]\"\n );\n });\n\n it(\"creates a client with auth headers and converts axios failures into MercadoLibreApiError\", async () => {\n const responseUse = vi.fn();\n const client = {\n interceptors: { response: { use: responseUse } },\n };\n vi.spyOn(axios, \"create\").mockReturnValue(client as never);\n const consoleSpy = vi.spyOn(console, \"error\").mockImplementation(() => undefined);\n\n createMercadoLibreClient(\"access-token\");\n\n expect(axios.create).toHaveBeenCalledWith({\n baseURL: \"https://api.mercadolibre.com\",\n timeout: 30000,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer access-token\",\n },\n });\n\n const rejectHandler = responseUse.mock.calls[0]?.[1] as (error: AxiosError) => Promise<never>;\n const retryAt = new Date(Date.now() + 5000).toUTCString();\n\n await expect(\n rejectHandler({\n isAxiosError: true,\n message: \"boom\",\n config: { method: \"get\", url: \"/orders/search\" },\n response: {\n status: 429,\n headers: { \"retry-after\": retryAt },\n data: { error: \"rate_limit\", message: \"access_token=secret\" },\n },\n } as unknown as AxiosError)\n ).rejects.toMatchObject({\n name: \"MercadoLibreApiError\",\n status: 429,\n code: \"rate_limit\",\n message: \"MercadoLibre API request failed (429): access_token=[redacted]\",\n retryAfterMs: expect.any(Number),\n });\n\n expect(consoleSpy).toHaveBeenCalledWith(\n \"[MERCADOLIBRE API ERROR] GET /orders/search -> 429\"\n );\n });\n\n it(\"builds api context from the persisted profile access and exposes /users/me\", async () => {\n const getMock = vi.fn().mockResolvedValue({ data: { id: 123, nickname: \"seller\" } });\n const client = {\n get: getMock,\n interceptors: { response: { use: vi.fn() } },\n };\n\n vi.mocked(getMercadoLibreAccessForProfile).mockResolvedValue({\n sellerId: \"seller-1\",\n accessToken: \"stored-token\",\n refreshToken: \"refresh\",\n } as never);\n vi.spyOn(axios, \"create\").mockReturnValue(client as never);\n\n const context = await getMercadoLibreApiContext(\"profile-1\");\n const me = await getMercadoLibreUsersMe(\"profile-1\");\n\n expect(context).toMatchObject({\n sellerId: \"seller-1\",\n accessToken: \"stored-token\",\n refreshToken: \"refresh\",\n api: client,\n });\n expect(getMock).toHaveBeenCalledWith(\"/users/me\");\n expect(me).toEqual({ id: 123, nickname: \"seller\" });\n });\n\n it(\"detects retryable MercadoLibre errors including missing status codes\", () => {\n expect(isRetryableMercadoLibreError(new MercadoLibreApiError(\"network\"))).toBe(true);\n expect(isRetryableMercadoLibreError(new MercadoLibreApiError(\"forbidden\", 403))).toBe(false);\n expect(isRetryableMercadoLibreError(new Error(\"plain\"))).toBe(false);\n });\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import axios, { AxiosError } from \"axios\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { getMercadoLibreAccessForProfile } from \"../../../config/mercadolibre.js\";\nimport { runWithMcpToolDeadlineContext } from \"../../../config/mcp-tool-deadline-context.js\";\nimport {\n createMercadoLibreClient,\n formatMercadoLibreError,\n getMercadoLibreApiContext,\n getMercadoLibreUsersMe,\n getMercadoLibreUsersMeWithAccessToken,\n isRetryableMercadoLibreError,\n MercadoLibreApiError,\n buildMercadoLibrePaginationMetadata,\n normalizeMercadoLibrePaging,\n parseMercadoLibreMultiget,\n runMercadoLibreBatch,\n sanitizeMercadoLibreText,\n} from \"../mercadolibre-api.js\";\n\nvi.mock(\"../../../config/mercadolibre.js\", () => ({\n getMercadoLibreAccessForProfile: vi.fn(),\n}));\n\ndescribe(\"mercadolibre-api foundations\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n vi.restoreAllMocks();\n });\n\n it(\"redacts sensitive tokens from MercadoLibre errors\", () => {\n const sanitized = sanitizeMercadoLibreText(\n \"Bearer abc.def access_token=secret refresh_token=refresh client_secret=topsecret code=oauth\"\n );\n\n expect(sanitized).toContain(\"Bearer [redacted]\");\n expect(sanitized).toContain(\"access_token=[redacted]\");\n expect(sanitized).toContain(\"refresh_token=[redacted]\");\n expect(sanitized).toContain(\"client_secret=[redacted]\");\n expect(sanitized).toContain(\"code=[redacted]\");\n });\n\n it(\"normalizes paging values and builds continuation metadata\", () => {\n const paging = normalizeMercadoLibrePaging({ total: 25, limit: \"10\", offset: -4 });\n const metadata = buildMercadoLibrePaginationMetadata({\n total: paging.total,\n limit: paging.limit,\n offset: paging.offset,\n returned: 10,\n nextField: \"offset\",\n });\n\n expect(paging).toEqual({ total: 25, limit: 10, offset: 0 });\n expect(metadata).toEqual({\n total: 25,\n limit: 10,\n offset: 0,\n returned: 10,\n has_more: true,\n continuation: \"Reenviar la misma tool con offset=10 para continuar.\",\n });\n });\n\n it(\"normalizes invalid paging values and reports finished pagination when no more pages exist\", () => {\n const paging = normalizeMercadoLibrePaging({ total: \"oops\", limit: undefined, offset: Number.NaN });\n const metadata = buildMercadoLibrePaginationMetadata({\n total: 2,\n limit: 0,\n offset: 0,\n returned: 2,\n nextField: \"scroll_id\",\n });\n\n expect(paging).toEqual({ total: 0, limit: 0, offset: 0 });\n expect(metadata).toEqual({\n total: 2,\n limit: 2,\n offset: 0,\n returned: 2,\n has_more: false,\n continuation: \"No more pages for this query.\",\n });\n });\n\n it(\"filters multiget responses to successful bodies only\", async () => {\n const result = await parseMercadoLibreMultiget({\n data: [\n { code: 200, body: { id: \"MLA1\" } },\n { code: 404, body: { id: \"MLA2\" } },\n { code: 204 },\n { code: 201, body: { id: \"MLA3\" } },\n ],\n } as never);\n\n expect(result).toEqual([{ id: \"MLA1\" }, { id: \"MLA3\" }]);\n });\n\n it(\"returns an empty multiget payload when no successful bodies are present\", async () => {\n const result = await parseMercadoLibreMultiget({\n data: [{ code: 404, body: { id: \"missing\" } }, { code: 204 }, { code: 200 }],\n } as never);\n\n expect(result).toEqual([]);\n });\n\n it(\"retries retryable batch failures and keeps ordering stable\", async () => {\n vi.useFakeTimers();\n vi.spyOn(Math, \"random\").mockReturnValue(0);\n\n const fetcher = vi\n .fn<(id: string) => Promise<{ id: string; recovered?: boolean }>>()\n .mockImplementationOnce(async () => {\n throw new MercadoLibreApiError(\"rate limited\", 429);\n })\n .mockResolvedValueOnce({ id: \"one\", recovered: true })\n .mockResolvedValueOnce({ id: \"two\" });\n\n const promise = runMercadoLibreBatch([\"one\", \"two\"], fetcher, {\n maxConcurrency: 1,\n maxRetries: 1,\n baseRetryDelayMs: 50,\n });\n\n await vi.advanceTimersByTimeAsync(50);\n\n await expect(promise).resolves.toEqual({\n successful: [\n { id: \"one\", document: { id: \"one\", recovered: true } },\n { id: \"two\", document: { id: \"two\" } },\n ],\n failed: [],\n });\n expect(fetcher).toHaveBeenCalledTimes(3);\n });\n\n it(\"honors retry-after headers for retryable failures\", async () => {\n vi.useFakeTimers();\n\n const fetcher = vi\n .fn<(id: string) => Promise<{ id: string }>>()\n .mockImplementationOnce(async () => {\n throw new MercadoLibreApiError(\"wait\", 429, undefined, 120);\n })\n .mockResolvedValueOnce({ id: \"one\" });\n\n const promise = runMercadoLibreBatch([\"one\"], fetcher, {\n maxRetries: 1,\n baseRetryDelayMs: 50,\n });\n\n await vi.advanceTimersByTimeAsync(119);\n expect(fetcher).toHaveBeenCalledTimes(1);\n\n await vi.advanceTimersByTimeAsync(1);\n\n await expect(promise).resolves.toEqual({\n successful: [{ id: \"one\", document: { id: \"one\" } }],\n failed: [],\n });\n });\n\n it(\"classifies non-retryable batch failures with a safe message\", async () => {\n const result = await runMercadoLibreBatch(\n [\"broken\"],\n async () => {\n throw new Error(\"refresh_token=secret exploded\");\n },\n { maxRetries: 2 }\n );\n\n expect(result).toEqual({\n successful: [],\n failed: [\n {\n id: \"broken\",\n message: \"refresh_token=[redacted] exploded\",\n statusCode: undefined,\n attempts: 1,\n retryable: false,\n },\n ],\n });\n });\n\n it(\"treats unknown rejected batch reasons as a single failed attempt\", async () => {\n const fetcher = vi.fn(async () => {\n throw \"Bearer abc123\";\n });\n\n const result = await runMercadoLibreBatch([\"broken\"], fetcher, { maxRetries: 0 });\n\n expect(result).toEqual({\n successful: [],\n failed: [\n {\n id: \"broken\",\n message: \"Failed to fetch MercadoLibre resource broken\",\n statusCode: undefined,\n attempts: 1,\n retryable: false,\n },\n ],\n });\n });\n\n it(\"returns an empty batch result when no ids are provided\", async () => {\n const fetcher = vi.fn();\n\n await expect(runMercadoLibreBatch([], fetcher)).resolves.toEqual({\n successful: [],\n failed: [],\n });\n expect(fetcher).not.toHaveBeenCalled();\n });\n\n it(\"formats axios errors with sanitized response details\", () => {\n const error = {\n isAxiosError: true,\n message: \"Request failed with status code 400\",\n response: {\n status: 400,\n data: {\n error: \"bad_request\",\n cause: [{ message: \"refresh_token=top-secret\" }],\n },\n headers: {},\n },\n } as AxiosError;\n\n expect(formatMercadoLibreError(error)).toBe(\n \"MercadoLibre API request failed (400): refresh_token=[redacted]\"\n );\n });\n\n it(\"creates a client with auth headers and converts axios failures into MercadoLibreApiError\", async () => {\n const requestUse = vi.fn();\n const responseUse = vi.fn();\n const client = {\n interceptors: { request: { use: requestUse }, response: { use: responseUse } },\n };\n vi.spyOn(axios, \"create\").mockReturnValue(client as never);\n const consoleSpy = vi.spyOn(console, \"error\").mockImplementation(() => undefined);\n\n createMercadoLibreClient(\"access-token\");\n\n expect(axios.create).toHaveBeenCalledWith({\n baseURL: \"https://api.mercadolibre.com\",\n timeout: 90000,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer access-token\",\n },\n });\n\n const rejectHandler = responseUse.mock.calls[0]?.[1] as (error: AxiosError) => Promise<never>;\n const retryAt = new Date(Date.now() + 5000).toUTCString();\n\n await expect(\n rejectHandler({\n isAxiosError: true,\n message: \"boom\",\n config: { method: \"get\", url: \"/orders/search\" },\n response: {\n status: 429,\n headers: { \"retry-after\": retryAt },\n data: { error: \"rate_limit\", message: \"access_token=secret\" },\n },\n } as unknown as AxiosError)\n ).rejects.toMatchObject({\n name: \"MercadoLibreApiError\",\n status: 429,\n code: \"rate_limit\",\n message: \"MercadoLibre API request failed (429): access_token=[redacted]\",\n retryAfterMs: expect.any(Number),\n });\n\n expect(consoleSpy).toHaveBeenCalledWith(\n \"[MERCADOLIBRE API ERROR] GET /orders/search -> 429\"\n );\n });\n\n it(\"composes the MCP deadline signal into the shared Axios client\", async () => {\n const requestUse = vi.fn();\n vi.spyOn(axios, \"create\").mockReturnValue({\n interceptors: { request: { use: requestUse }, response: { use: vi.fn() } },\n } as never);\n createMercadoLibreClient(\"access-token\");\n const requestInterceptor = requestUse.mock.calls[0]?.[0] as (config: { signal?: AbortSignal }) => { signal?: AbortSignal };\n const controller = new AbortController();\n const request = await runWithMcpToolDeadlineContext(controller.signal, async () => requestInterceptor({}));\n expect(request.signal).toBe(controller.signal);\n });\n\n it(\"builds api context from the persisted profile access and exposes /users/me\", async () => {\n const getMock = vi.fn().mockResolvedValue({ data: { id: 123, nickname: \"seller\" } });\n const client = {\n get: getMock,\n interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },\n };\n\n vi.mocked(getMercadoLibreAccessForProfile).mockResolvedValue({\n sellerId: \"seller-1\",\n accessToken: \"stored-token\",\n refreshToken: \"refresh\",\n } as never);\n vi.spyOn(axios, \"create\").mockReturnValue(client as never);\n\n const context = await getMercadoLibreApiContext(\"profile-1\");\n const me = await getMercadoLibreUsersMe(\"profile-1\");\n\n expect(context).toMatchObject({\n sellerId: \"seller-1\",\n accessToken: \"stored-token\",\n refreshToken: \"refresh\",\n api: client,\n });\n expect(getMock).toHaveBeenCalledWith(\"/users/me\");\n expect(me).toEqual({ id: 123, nickname: \"seller\" });\n });\n\n it(\"probes /users/me with the explicitly supplied access token\", async () => {\n const getMock = vi.fn().mockResolvedValue({ data: { id: 456, nickname: \"explicit-seller\" } });\n const client = {\n get: getMock,\n interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },\n };\n vi.spyOn(axios, \"create\").mockReturnValue(client as never);\n\n const me = await getMercadoLibreUsersMeWithAccessToken(\"explicit-access-token\");\n\n expect(axios.create).toHaveBeenCalledWith({\n baseURL: \"https://api.mercadolibre.com\",\n timeout: 90000,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer explicit-access-token\",\n },\n });\n expect(getMock).toHaveBeenCalledWith(\"/users/me\");\n expect(getMercadoLibreAccessForProfile).not.toHaveBeenCalled();\n expect(me).toEqual({ id: 456, nickname: \"explicit-seller\" });\n });\n\n it(\"detects retryable MercadoLibre errors including missing status codes\", () => {\n expect(isRetryableMercadoLibreError(new MercadoLibreApiError(\"network\"))).toBe(true);\n expect(isRetryableMercadoLibreError(new MercadoLibreApiError(\"forbidden\", 403))).toBe(false);\n expect(isRetryableMercadoLibreError(new Error(\"plain\"))).toBe(false);\n });\n});\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,226 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { billingMock, orderMock, shipmentMock, ownershipMock } = vi.hoisted(() => ({
3
+ billingMock: vi.fn(),
4
+ orderMock: vi.fn(),
5
+ shipmentMock: vi.fn(),
6
+ ownershipMock: vi.fn()
7
+ }));
8
+ vi.mock("../mercadolibre-order-billing-info.js", () => ({ getMercadoLibreSaleBillingInfo: billingMock, verifyMercadoLibreOrderOwnership: ownershipMock }));
9
+ vi.mock("../mercadolibre-orders.js", () => ({ getMercadoLibreOrder: orderMock, getMercadoLibreShipment: shipmentMock }));
10
+ import { MercadoLibreApiError } from "../mercadolibre-api.js";
11
+ import { getBuyerReconciliationSafetyMarginMs, reconcileMercadoLibreBuyerData } from "../mercadolibre-buyer-reconciliation.js";
12
+ import { createMcpToolDeadlineMiddleware } from "../../../config/mcp-tool-deadline.js";
13
+ const order = (id, shipment2 = "1001") => ({ id, seller: { id: "seller-1" }, buyer: { nickname: "buyer-nick" }, shipping: { id: shipment2 } });
14
+ const available = (id, type = "DNI", number = "123", shipment2 = "1001") => ({
15
+ available: true,
16
+ order: order(id, shipment2),
17
+ billingInfo: { buyer: { billing_info: {
18
+ name: "Ada",
19
+ last_name: "Lovelace",
20
+ identification: { type, number },
21
+ address: { street_name: "Main", street_number: 10, city_name: "City", state: { code: "S", name: "State" }, zip_code: "1000", country_id: "AR" },
22
+ taxes: { taxpayer_type: { id: "CONSUMIDOR_FINAL", description: "Consumidor Final" } }
23
+ } } }
24
+ });
25
+ const missingFiscal = { available: false, reason: "billing_info_fiscal_fields_missing" };
26
+ const shipment = { destination: { shipping_address: { street_name: "Shipping", street_number: "20", city_name: "Port", state: { name: "Province" }, zip_code: "2000", country_id: "AR" } } };
27
+ describe("reconcileMercadoLibreBuyerData", () => {
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ ownershipMock.mockResolvedValue("owned");
31
+ shipmentMock.mockResolvedValue(shipment);
32
+ });
33
+ it("derives a bounded safety margin from the global timeout", () => {
34
+ expect(getBuyerReconciliationSafetyMarginMs(3e5)).toBe(1e4);
35
+ expect(getBuyerReconciliationSafetyMarginMs(2e3)).toBe(100);
36
+ expect(getBuyerReconciliationSafetyMarginMs(1e6)).toBe(1e4);
37
+ });
38
+ it("returns a complete direct order with buyer, document, fiscal and shipping addresses", async () => {
39
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-1", result: available("o-1", "DNI", "111") });
40
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"]);
41
+ const item = result.results[0];
42
+ expect(item).toMatchObject({ sale_reference: "sale-1", resolution: { kind: "order", status: "resolved" }, data: { completeness: "complete" } });
43
+ expect(item.data.buyer).toMatchObject({ full_name: "Ada Lovelace", nickname: "buyer-nick", document: { type: "DNI", number: "111" }, fiscal_address: { state: "State" }, shipping_address: { street: "Shipping 20" } });
44
+ expect(item.data).not.toHaveProperty("sources");
45
+ expect(item.data).not.toHaveProperty("missing_fields");
46
+ expect(item.data).not.toHaveProperty("reasons");
47
+ expect(item).not.toHaveProperty("metadata");
48
+ expect(result).not.toHaveProperty("metadata");
49
+ expect(result.summary).toEqual({ requested: 1, complete: 1, partial: 0, unavailable: 0, orders: 1, packs: 0, unknown: 0 });
50
+ });
51
+ it("keeps useful order buyer data when fiscal fields are partial", async () => {
52
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-1", result: missingFiscal });
53
+ orderMock.mockResolvedValue(order("o-1"));
54
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"]);
55
+ expect(result.results[0].data).toMatchObject({ completeness: "partial", buyer: { nickname: "buyer-nick" } });
56
+ expect(result.results[0].data.issues).toContainEqual(expect.objectContaining({ field: "fiscal_address", code: "billing_info_fiscal_fields_missing" }));
57
+ });
58
+ it.each([["DNI", "DNI"], ["CUIT", "CUIT"]])("normalizes %s documents without changing the number", async (source, expected) => {
59
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-1", result: available("o-1", source, "999") });
60
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"]);
61
+ expect(result.results[0].data.buyer?.document).toEqual({ type: expected, number: "999" });
62
+ });
63
+ it("consolidates a unanimous pack at top-level data and hides member identifiers", async () => {
64
+ billingMock.mockResolvedValue({ resolutionKind: "pack", packId: "pack-secret", metadata: { memberCount: 2 }, members: [
65
+ { orderId: "m-1", accessible: true, result: available("m-1", "DNI", "123", "1001") },
66
+ { orderId: "m-2", accessible: true, result: available("m-2", "DNI", "123", "1002") }
67
+ ] });
68
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["pack-sale"]);
69
+ expect(result.results[0]).toMatchObject({ pack: { agreement: "unanimous" }, data: { completeness: "complete" } });
70
+ expect(result.results[0].data).not.toHaveProperty("issues");
71
+ expect(result.results[0]).not.toHaveProperty("metadata");
72
+ expect(result.results[0].pack).not.toHaveProperty("consolidated");
73
+ expect(result.results[0].pack).not.toHaveProperty("members");
74
+ expect(JSON.stringify(result)).not.toMatch(/pack-secret|m-1|m-2/);
75
+ });
76
+ it("does not consolidate a pack when shipping destinations differ", async () => {
77
+ const secondShipment = { destination: { shipping_address: { street_name: "Other", street_number: "20", city_name: "Port", state: { name: "Province" }, zip_code: "2000", country_id: "AR" } } };
78
+ billingMock.mockResolvedValue({ resolutionKind: "pack", packId: "pack-secret", metadata: { memberCount: 2 }, members: [
79
+ { orderId: "m-1", accessible: true, result: available("m-1", "DNI", "123", "1001") },
80
+ { orderId: "m-2", accessible: true, result: available("m-2", "DNI", "123", "1002") }
81
+ ] });
82
+ shipmentMock.mockImplementation(async (_profile, shipmentId) => shipmentId === "1002" ? secondShipment : shipment);
83
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["pack-sale"]);
84
+ expect(result.results[0].pack?.agreement).toBe("disagreement");
85
+ expect(result.results[0].data.completeness).toBe("partial");
86
+ expect(result.results[0].pack).toHaveProperty("members");
87
+ expect(result.summary).toMatchObject({ complete: 0, partial: 1, unavailable: 0 });
88
+ });
89
+ it("returns opaque indexed members for disagreement and partial packs", async () => {
90
+ billingMock.mockResolvedValue({ resolutionKind: "pack", packId: "pack-secret", metadata: { memberCount: 2 }, members: [
91
+ { orderId: "m-1", accessible: true, result: available("m-1", "DNI", "1") },
92
+ { orderId: "m-2", accessible: true, result: available("m-2", "CUIT", "2") }
93
+ ] });
94
+ let result = await reconcileMercadoLibreBuyerData("triplim", ["pack-sale"]);
95
+ expect(result.results[0].pack).toMatchObject({ agreement: "disagreement", members: [{ member_index: 0 }, { member_index: 1 }] });
96
+ billingMock.mockResolvedValue({ resolutionKind: "pack", packId: "pack-secret", metadata: { memberCount: 2 }, members: [{ orderId: "m-1", accessible: true, result: missingFiscal }, { orderId: "m-2", accessible: false, failureReason: "access_denied" }] });
97
+ result = await reconcileMercadoLibreBuyerData("triplim", ["pack-sale"]);
98
+ expect(result.results[0].pack?.agreement).toBe("partial");
99
+ expect(result.results[0].pack?.members?.[1]).toMatchObject({ member_index: 1, data: { completeness: "unavailable" } });
100
+ expect(result.results[0].data.issues).toContainEqual(expect.objectContaining({ field: "pack_members", code: "pack_partial_coverage" }));
101
+ expect(result.summary).toMatchObject({ complete: 0, partial: 1, unavailable: 0 });
102
+ });
103
+ it("exposes a safe shipping issue without operational metadata", async () => {
104
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-1", result: available("o-1") });
105
+ shipmentMock.mockRejectedValue(new MercadoLibreApiError("temporary", 503));
106
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"], { maxRetries: 1, sleep: async () => void 0 });
107
+ expect(result.results[0].data.issues).toContainEqual(expect.objectContaining({ field: "shipping_address", code: "shipping_source_unavailable" }));
108
+ expect(result.results[0]).not.toHaveProperty("metadata");
109
+ expect(result).not.toHaveProperty("metadata");
110
+ });
111
+ it("counts only actual shipping retry exhaustion in the degraded event", async () => {
112
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-1", result: available("o-1") });
113
+ shipmentMock.mockRejectedValue(new MercadoLibreApiError("temporary", 503));
114
+ const withoutRetry = vi.fn();
115
+ await reconcileMercadoLibreBuyerData("triplim", ["sale-1"], { maxRetries: 0, onDegradedOutcome: withoutRetry });
116
+ expect(withoutRetry).toHaveBeenCalledWith(expect.objectContaining({ retry: 0 }));
117
+ const exhausted = vi.fn();
118
+ await reconcileMercadoLibreBuyerData("triplim", ["sale-1"], { maxRetries: 1, sleep: async () => void 0, onDegradedOutcome: exhausted });
119
+ expect(exhausted).toHaveBeenCalledWith(expect.objectContaining({ retry: 1 }));
120
+ expect(shipmentMock).toHaveBeenCalledTimes(3);
121
+ });
122
+ it("reports pack member issues without operational metadata or upstream details", async () => {
123
+ billingMock.mockResolvedValue({ resolutionKind: "pack", packId: "pack-secret", metadata: { memberCount: 2 }, members: [
124
+ { orderId: "m-1", accessible: false, failureReason: "operation_deadline_exceeded" },
125
+ { orderId: "m-2", accessible: false, failureReason: "upstream_unavailable", attempts: 3, retryable: true }
126
+ ] });
127
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["pack-sale"]);
128
+ expect(result.results[0].pack?.members?.[0].data.issues).toContainEqual(expect.objectContaining({ code: "operation_deadline_exceeded" }));
129
+ expect(result.results[0].pack?.members?.[1].data.issues).toContainEqual(expect.objectContaining({ code: "upstream_unavailable" }));
130
+ expect(result.results[0]).not.toHaveProperty("metadata");
131
+ expect(result).not.toHaveProperty("metadata");
132
+ expect(JSON.stringify(result)).not.toContain("temporary");
133
+ });
134
+ it("propagates an unavailable pack deadline as a safe issue", async () => {
135
+ billingMock.mockResolvedValue({
136
+ resolutionKind: "pack",
137
+ packId: "pack-secret",
138
+ members: [],
139
+ unavailableReason: "pack_operation_deadline_exceeded",
140
+ metadata: { memberCount: 0 }
141
+ });
142
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["pack-sale"]);
143
+ expect(result.results[0]).toMatchObject({ resolution: { kind: "pack", status: "unavailable" }, data: { issues: [expect.objectContaining({ code: "operation_deadline_exceeded" })] } });
144
+ expect(result).not.toHaveProperty("metadata");
145
+ expect(result.summary).toMatchObject({ requested: 1, unavailable: 1, partial: 0 });
146
+ });
147
+ it.each(["mismatch", "unverifiable"])("returns no PII when ownership is %s", async (ownership) => {
148
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "foreign-order", result: missingFiscal });
149
+ orderMock.mockResolvedValue({ ...order("foreign-order"), buyer: { nickname: "secret-buyer" } });
150
+ ownershipMock.mockResolvedValue(ownership);
151
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"]);
152
+ expect(result.results[0].data.buyer).toBeUndefined();
153
+ expect(JSON.stringify(result)).not.toContain("secret-buyer");
154
+ expect(shipmentMock).not.toHaveBeenCalled();
155
+ });
156
+ it("does not turn a deadline during order lookup into pack fallback", async () => {
157
+ billingMock.mockRejectedValue(new MercadoLibreApiError("timeout", void 0));
158
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"], { operationDeadlineMs: 1, now: () => 10 });
159
+ expect(result.results[0].resolution.kind).toBe("unknown");
160
+ expect(billingMock).toHaveBeenCalledTimes(1);
161
+ expect(orderMock).not.toHaveBeenCalled();
162
+ });
163
+ it("marks already-expired references as partial processing", async () => {
164
+ const now = vi.fn().mockReturnValueOnce(10).mockReturnValue(11);
165
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"], { operationDeadlineMs: 1, now });
166
+ expect(result.results[0]).toMatchObject({ data: { issues: [expect.objectContaining({ code: "operation_deadline_exceeded" })] } });
167
+ expect(result).not.toHaveProperty("metadata");
168
+ expect(result.summary).toMatchObject({ requested: 1, unavailable: 1, partial: 0 });
169
+ });
170
+ it("returns a structured reference issue before the outer middleware deadline", async () => {
171
+ vi.useFakeTimers();
172
+ billingMock.mockImplementation(() => new Promise((_, reject) => {
173
+ setTimeout(() => reject(new Error("billing source unavailable")), 10);
174
+ }));
175
+ const middleware = createMcpToolDeadlineMiddleware(25);
176
+ const resultPromise = middleware({}, () => reconcileMercadoLibreBuyerData("triplim", ["sale-1"]));
177
+ await vi.advanceTimersByTimeAsync(10);
178
+ await expect(resultPromise).resolves.toMatchObject({
179
+ results: [{ data: { issues: [expect.objectContaining({ code: "operation_deadline_exceeded" })] } }]
180
+ });
181
+ vi.useRealTimers();
182
+ });
183
+ it.each([
184
+ ["AbortError", Object.assign(new Error("cancelled"), { name: "AbortError" })],
185
+ ["timeout sentinel", Object.assign(new Error("request timed out"), { code: "ETIMEDOUT" })]
186
+ ])("classifies %s as an operation deadline", async (_label, failure) => {
187
+ billingMock.mockRejectedValue(failure);
188
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"]);
189
+ expect(result.results[0]).toMatchObject({ data: { issues: [expect.objectContaining({ code: "operation_deadline_exceeded" })] } });
190
+ expect(result).not.toHaveProperty("metadata");
191
+ });
192
+ it("keeps nondeadline resolution failures as reference_unavailable", async () => {
193
+ billingMock.mockRejectedValue(new Error("billing source unavailable"));
194
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-1"]);
195
+ expect(result.results[0]).toMatchObject({ resolution: { kind: "unknown", status: "unavailable" }, data: { issues: [expect.objectContaining({ code: "reference_unavailable" })] } });
196
+ expect(result).not.toHaveProperty("metadata");
197
+ expect(result.summary).toMatchObject({ requested: 1, unavailable: 1, partial: 0, unknown: 1 });
198
+ });
199
+ it("preserves duplicate occurrences and input order", async () => {
200
+ billingMock.mockImplementation(async (_profile, reference) => ({ resolutionKind: "order", orderId: reference, result: available(reference) }));
201
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["second", "first", "second"]);
202
+ expect(result.results.map((item) => item.sale_reference)).toEqual(["second", "first", "second"]);
203
+ expect(result.summary.requested).toBe(3);
204
+ });
205
+ it("emits only aggregate degraded events and keeps the public response unchanged", async () => {
206
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
207
+ const error = vi.spyOn(console, "error").mockImplementation(() => void 0);
208
+ try {
209
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-secret", result: available("o-secret", "CUIT", "secret-doc") });
210
+ const result = await reconcileMercadoLibreBuyerData("triplim", ["sale-secret"]);
211
+ expect(warn).not.toHaveBeenCalled();
212
+ expect(error).not.toHaveBeenCalled();
213
+ expect(result.results[0].data.buyer?.document?.number).toBe("secret-doc");
214
+ billingMock.mockResolvedValue({ resolutionKind: "order", orderId: "o-secret", result: missingFiscal });
215
+ const degraded = await reconcileMercadoLibreBuyerData("triplim", ["sale-secret"], { onDegradedOutcome: (event) => warn(JSON.stringify(event)) });
216
+ expect(warn).toHaveBeenCalledOnce();
217
+ expect(warn.mock.calls[0]?.[0]).not.toContain("secret-doc");
218
+ expect(degraded).not.toHaveProperty("operational_event");
219
+ expect(degraded).not.toHaveProperty("telemetry");
220
+ } finally {
221
+ warn.mockRestore();
222
+ error.mockRestore();
223
+ }
224
+ });
225
+ });
226
+ //# sourceMappingURL=mercadolibre-buyer-reconciliation.test.js.map