@littlebearapps/platform-admin-sdk 2.0.0 → 2.2.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 (185) hide show
  1. package/README.md +4 -7
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +206 -4
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  7. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  8. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  9. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  10. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  11. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  12. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  13. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  14. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  15. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  18. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  19. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  20. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  21. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  22. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  23. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  24. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  25. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  26. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  27. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  28. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  30. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  31. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  32. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  34. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  35. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  36. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  37. package/templates/full/dashboard/src/pages/map.astro +561 -0
  38. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  39. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  40. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  41. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  42. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  43. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  44. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  45. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  46. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  47. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  48. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  49. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  50. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  51. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  52. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  53. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  54. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  55. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  56. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  57. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  58. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  59. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  60. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  61. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  62. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  63. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  64. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  65. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  66. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  67. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  68. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  69. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  70. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  71. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  72. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  73. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  74. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  75. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  76. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  77. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  78. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  79. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  80. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  81. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  82. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  83. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  84. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  85. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  86. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  87. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  88. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  89. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  90. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  91. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  92. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  93. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  94. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  95. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  96. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  97. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  98. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  99. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  100. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  101. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  102. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  103. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  104. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  105. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  106. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  107. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  108. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  109. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  110. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  111. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  112. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  113. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  114. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  115. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  116. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  117. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  118. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  119. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  120. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  121. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  122. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  123. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  124. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  125. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  126. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  127. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  128. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  129. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  130. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  131. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  132. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  133. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  134. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  135. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  136. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  137. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  138. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  139. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  140. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  141. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  142. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  143. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  144. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  145. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  146. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  147. package/templates/shared/tests/unit/billing.test.ts +331 -0
  148. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  149. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  150. package/templates/shared/tests/unit/control.test.ts +226 -0
  151. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  152. package/templates/shared/tests/unit/economics.test.ts +365 -0
  153. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  154. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  155. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  156. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  157. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  158. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  159. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  160. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  161. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  162. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  163. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  164. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  165. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  166. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  167. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  168. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  169. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  170. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  171. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  172. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  173. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  174. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  175. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  176. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  177. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  178. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  179. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  180. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  181. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
  182. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  183. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  184. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  185. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Unit Tests for Cost Calculator
3
+ *
4
+ * Tests CF resource cost calculation from telemetry metrics.
5
+ *
6
+ * @module tests/unit/cost-calculator
7
+ * @created 2026-01-27
8
+ * @task Real-Time Cost Tracking for Platform SDK
9
+ */
10
+
11
+ import { describe, expect, it } from 'vitest';
12
+ import { calculateCFCostFromMetrics } from '../../workers/lib/usage/queue/cost-calculator';
13
+ import type { FeatureMetrics } from '@littlebearapps/platform-consumer-sdk';
14
+
15
+ describe('Cost Calculator', () => {
16
+ describe('calculateCFCostFromMetrics', () => {
17
+ it('returns zero for empty metrics', () => {
18
+ const metrics: FeatureMetrics = {};
19
+ const cost = calculateCFCostFromMetrics(metrics);
20
+
21
+ expect(cost).toBe(0);
22
+ });
23
+
24
+ it('calculates D1 costs correctly', () => {
25
+ const metrics: FeatureMetrics = {
26
+ d1RowsRead: 1_000_000_000, // 1 billion rows
27
+ d1RowsWritten: 1_000_000, // 1 million rows
28
+ };
29
+ const cost = calculateCFCostFromMetrics(metrics);
30
+
31
+ // 1B reads at $0.001/B = $0.001
32
+ // 1M writes at $1.00/M = $1.00
33
+ expect(cost).toBeCloseTo(1.001, 4);
34
+ });
35
+
36
+ it('calculates KV costs correctly', () => {
37
+ const metrics: FeatureMetrics = {
38
+ kvReads: 1_000_000, // 1 million reads
39
+ kvWrites: 100_000, // 100K writes
40
+ kvDeletes: 50_000, // 50K deletes
41
+ kvLists: 10_000, // 10K lists
42
+ };
43
+ const cost = calculateCFCostFromMetrics(metrics);
44
+
45
+ // 1M reads at $0.50/M = $0.50
46
+ // 100K writes at $5.00/M = $0.50
47
+ // 50K deletes at $5.00/M = $0.25
48
+ // 10K lists at $5.00/M = $0.05
49
+ expect(cost).toBeCloseTo(1.3, 4);
50
+ });
51
+
52
+ it('calculates R2 costs correctly', () => {
53
+ const metrics: FeatureMetrics = {
54
+ r2ClassA: 1_000_000, // 1 million Class A ops
55
+ r2ClassB: 1_000_000, // 1 million Class B ops
56
+ };
57
+ const cost = calculateCFCostFromMetrics(metrics);
58
+
59
+ // 1M Class A at $4.50/M = $4.50
60
+ // 1M Class B at $0.36/M = $0.36
61
+ expect(cost).toBeCloseTo(4.86, 4);
62
+ });
63
+
64
+ it('calculates Workers AI costs correctly', () => {
65
+ const metrics: FeatureMetrics = {
66
+ aiNeurons: 10_000, // 10K neurons
67
+ };
68
+ const cost = calculateCFCostFromMetrics(metrics);
69
+
70
+ // 10K neurons at $0.011/1000 = $0.11
71
+ expect(cost).toBeCloseTo(0.11, 4);
72
+ });
73
+
74
+ it('calculates DO costs correctly', () => {
75
+ const metrics: FeatureMetrics = {
76
+ doRequests: 1_000_000, // 1M requests
77
+ doGbSeconds: 1_000_000, // 1M GB-seconds
78
+ };
79
+ const cost = calculateCFCostFromMetrics(metrics);
80
+
81
+ // 1M requests at $0.15/M = $0.15
82
+ // 1M GB-seconds at $12.50/M = $12.50
83
+ expect(cost).toBeCloseTo(12.65, 4);
84
+ });
85
+
86
+ it('calculates Vectorize costs correctly', () => {
87
+ const metrics: FeatureMetrics = {
88
+ vectorizeQueries: 1_000_000, // 1M queries
89
+ };
90
+ const cost = calculateCFCostFromMetrics(metrics);
91
+
92
+ // 1M queries at $0.01/M = $0.01
93
+ expect(cost).toBeCloseTo(0.01, 4);
94
+ });
95
+
96
+ it('calculates Queues costs correctly', () => {
97
+ const metrics: FeatureMetrics = {
98
+ queueMessages: 1_000_000, // 1M messages
99
+ };
100
+ const cost = calculateCFCostFromMetrics(metrics);
101
+
102
+ // 1M messages at $0.40/M = $0.40
103
+ expect(cost).toBeCloseTo(0.4, 4);
104
+ });
105
+
106
+ it('calculates combined costs for realistic workload', () => {
107
+ // Simulate a Scout scanner run
108
+ const metrics: FeatureMetrics = {
109
+ d1RowsRead: 50_000, // 50K reads
110
+ d1RowsWritten: 500, // 500 writes
111
+ kvReads: 1_000, // 1K KV reads
112
+ kvWrites: 100, // 100 KV writes
113
+ aiNeurons: 5_000, // 5K neurons for AI
114
+ queueMessages: 50, // 50 queue messages
115
+ };
116
+ const cost = calculateCFCostFromMetrics(metrics);
117
+
118
+ // All these are small fractions of a cent
119
+ // D1: 50K/1B * $0.001 + 500/1M * $1 ≈ $0.0005
120
+ // KV: 1K/1M * $0.50 + 100/1M * $5 ≈ $0.001
121
+ // AI: 5K/1000 * $0.011 = $0.055
122
+ // Queue: 50/1M * $0.40 ≈ $0.00002
123
+ // Total ≈ $0.057
124
+ expect(cost).toBeGreaterThan(0);
125
+ expect(cost).toBeLessThan(0.1); // Sanity check
126
+ });
127
+
128
+ it('handles undefined and zero values', () => {
129
+ const metrics: FeatureMetrics = {
130
+ d1RowsRead: 0,
131
+ d1RowsWritten: undefined,
132
+ kvReads: 1_000_000,
133
+ // Other fields not present
134
+ };
135
+ const cost = calculateCFCostFromMetrics(metrics);
136
+
137
+ // Only KV reads should contribute
138
+ expect(cost).toBeCloseTo(0.5, 4);
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Unit Tests for BCU Cost Allocator
3
+ *
4
+ * Tests scarcity-weighted quota enforcement via Budget Consumption Units.
5
+ *
6
+ * @module tests/unit/economics
7
+ * @created 2026-01-23
8
+ * @task Intelligent Degradation for Platform Usage
9
+ */
10
+
11
+ import { describe, expect, it } from 'vitest';
12
+ import {
13
+ calculateBCU,
14
+ calculateBCUForResource,
15
+ checkBCUBudget,
16
+ usdToBCU,
17
+ bcuToUSD,
18
+ describeDominantResource,
19
+ formatBCUResult,
20
+ getTopContributors,
21
+ combineBCUResults,
22
+ DEFAULT_BCU_WEIGHTS,
23
+ type BCUResult,
24
+ } from '../../workers/lib/economics';
25
+ import type { FeatureMetrics } from '@littlebearapps/platform-consumer-sdk';
26
+
27
+ describe('BCU Cost Allocator', () => {
28
+ describe('calculateBCU', () => {
29
+ it('returns zero for empty metrics', () => {
30
+ const metrics: FeatureMetrics = {};
31
+ const result = calculateBCU(metrics);
32
+
33
+ expect(result.total).toBe(0);
34
+ expect(result.dominantResource).toBeNull();
35
+ expect(result.dominantPercentage).toBe(0);
36
+ });
37
+
38
+ it('calculates BCU for single resource', () => {
39
+ const metrics: FeatureMetrics = { aiNeurons: 100 };
40
+ const result = calculateBCU(metrics);
41
+
42
+ // aiNeurons weight is 100, so 100 neurons = 10000 BCU
43
+ expect(result.total).toBe(10000);
44
+ expect(result.dominantResource).toBe('aiNeurons');
45
+ expect(result.dominantPercentage).toBe(100);
46
+ expect(result.breakdown.aiNeurons).toBe(10000);
47
+ });
48
+
49
+ it('calculates BCU for multiple resources', () => {
50
+ const metrics: FeatureMetrics = {
51
+ aiNeurons: 10, // 10 * 100 = 1000 BCU
52
+ d1RowsWritten: 100, // 100 * 10 = 1000 BCU
53
+ kvWrites: 1000, // 1000 * 1 = 1000 BCU
54
+ requests: 100000, // 100000 * 0.001 = 100 BCU
55
+ };
56
+ const result = calculateBCU(metrics);
57
+
58
+ expect(result.total).toBe(3100);
59
+ expect(result.breakdown.aiNeurons).toBe(1000);
60
+ expect(result.breakdown.d1RowsWritten).toBe(1000);
61
+ expect(result.breakdown.kvWrites).toBe(1000);
62
+ expect(result.breakdown.requests).toBe(100);
63
+ });
64
+
65
+ it('identifies dominant resource correctly', () => {
66
+ const metrics: FeatureMetrics = {
67
+ aiNeurons: 100, // 10000 BCU (dominant)
68
+ kvWrites: 100, // 100 BCU
69
+ requests: 1000, // 1 BCU
70
+ };
71
+ const result = calculateBCU(metrics);
72
+
73
+ expect(result.dominantResource).toBe('aiNeurons');
74
+ // 10000 / 10101 * 100 ≈ 99%
75
+ expect(result.dominantPercentage).toBeGreaterThan(98);
76
+ });
77
+
78
+ it('handles zero values in metrics', () => {
79
+ const metrics: FeatureMetrics = {
80
+ aiNeurons: 0,
81
+ kvWrites: 0,
82
+ requests: 100,
83
+ };
84
+ const result = calculateBCU(metrics);
85
+
86
+ expect(result.total).toBe(0.1); // 100 * 0.001
87
+ expect(result.breakdown.aiNeurons).toBeUndefined();
88
+ expect(result.breakdown.kvWrites).toBeUndefined();
89
+ });
90
+
91
+ it('uses custom weights when provided', () => {
92
+ const metrics: FeatureMetrics = { aiNeurons: 10 };
93
+ const customWeights = { ...DEFAULT_BCU_WEIGHTS, aiNeurons: 1000 };
94
+
95
+ const defaultResult = calculateBCU(metrics);
96
+ const customResult = calculateBCU(metrics, customWeights);
97
+
98
+ expect(defaultResult.total).toBe(1000); // 10 * 100
99
+ expect(customResult.total).toBe(10000); // 10 * 1000
100
+ });
101
+ });
102
+
103
+ describe('calculateBCUForResource', () => {
104
+ it('calculates BCU for specific resource type', () => {
105
+ expect(calculateBCUForResource('aiNeurons', 10)).toBe(1000);
106
+ expect(calculateBCUForResource('d1RowsWritten', 100)).toBe(1000);
107
+ expect(calculateBCUForResource('kvWrites', 100)).toBe(100);
108
+ expect(calculateBCUForResource('requests', 1000)).toBe(1);
109
+ });
110
+ });
111
+
112
+ describe('checkBCUBudget', () => {
113
+ it('calculates utilisation correctly', () => {
114
+ const state = checkBCUBudget(7000, 10000);
115
+
116
+ expect(state.currentBCU).toBe(7000);
117
+ expect(state.limitBCU).toBe(10000);
118
+ expect(state.utilisation).toBe(0.7);
119
+ expect(state.exceeded).toBe(false);
120
+ });
121
+
122
+ it('detects exceeded budget', () => {
123
+ const state = checkBCUBudget(12000, 10000);
124
+
125
+ expect(state.utilisation).toBe(1.2);
126
+ expect(state.exceeded).toBe(true);
127
+ });
128
+
129
+ it('handles zero budget limit', () => {
130
+ const state = checkBCUBudget(100, 0);
131
+
132
+ // When limit is 0, utilisation is 0 (can't divide by zero)
133
+ // and exceeded is true because any usage exceeds a zero limit
134
+ expect(state.utilisation).toBe(0);
135
+ expect(state.exceeded).toBe(true);
136
+ });
137
+
138
+ it('handles exact budget match', () => {
139
+ const state = checkBCUBudget(10000, 10000);
140
+
141
+ expect(state.utilisation).toBe(1);
142
+ expect(state.exceeded).toBe(false); // Exact match is not exceeded
143
+ });
144
+ });
145
+
146
+ describe('usdToBCU / bcuToUSD', () => {
147
+ it('converts USD to BCU', () => {
148
+ expect(usdToBCU(1)).toBe(10000);
149
+ expect(usdToBCU(5)).toBe(50000);
150
+ expect(usdToBCU(0.5)).toBe(5000);
151
+ });
152
+
153
+ it('converts BCU to USD', () => {
154
+ expect(bcuToUSD(10000)).toBe(1);
155
+ expect(bcuToUSD(50000)).toBe(5);
156
+ expect(bcuToUSD(5000)).toBe(0.5);
157
+ });
158
+
159
+ it('round-trips correctly', () => {
160
+ const original = 3.5;
161
+ const converted = bcuToUSD(usdToBCU(original));
162
+ expect(converted).toBe(original);
163
+ });
164
+ });
165
+
166
+ describe('describeDominantResource', () => {
167
+ it('returns human-readable descriptions', () => {
168
+ expect(describeDominantResource('aiNeurons')).toBe('AI compute (neurons)');
169
+ expect(describeDominantResource('d1RowsWritten')).toBe('D1 rows written');
170
+ expect(describeDominantResource('kvWrites')).toBe('KV writes');
171
+ expect(describeDominantResource('requests')).toBe('HTTP requests');
172
+ });
173
+
174
+ it('returns "none" for null', () => {
175
+ expect(describeDominantResource(null)).toBe('none');
176
+ });
177
+ });
178
+
179
+ describe('formatBCUResult', () => {
180
+ it('formats result as readable string', () => {
181
+ const result: BCUResult = {
182
+ total: 5000,
183
+ breakdown: { aiNeurons: 4000, kvWrites: 1000 },
184
+ dominantResource: 'aiNeurons',
185
+ dominantPercentage: 80,
186
+ };
187
+
188
+ const formatted = formatBCUResult(result);
189
+
190
+ expect(formatted).toContain('BCU: 5000.00');
191
+ expect(formatted).toContain('dominant: AI compute (neurons) (80.0%)');
192
+ });
193
+
194
+ it('handles null dominant resource', () => {
195
+ const result: BCUResult = {
196
+ total: 0,
197
+ breakdown: {},
198
+ dominantResource: null,
199
+ dominantPercentage: 0,
200
+ };
201
+
202
+ const formatted = formatBCUResult(result);
203
+ expect(formatted).toContain('dominant: none');
204
+ });
205
+ });
206
+
207
+ describe('getTopContributors', () => {
208
+ it('returns top N contributors sorted by BCU', () => {
209
+ const result: BCUResult = {
210
+ total: 6000,
211
+ breakdown: {
212
+ aiNeurons: 3000,
213
+ d1RowsWritten: 2000,
214
+ kvWrites: 800,
215
+ requests: 200,
216
+ },
217
+ dominantResource: 'aiNeurons',
218
+ dominantPercentage: 50,
219
+ };
220
+
221
+ const top = getTopContributors(result, 3);
222
+
223
+ expect(top.length).toBe(3);
224
+ expect(top[0].resource).toBe('aiNeurons');
225
+ expect(top[0].bcu).toBe(3000);
226
+ expect(top[0].percentage).toBe(50);
227
+ expect(top[1].resource).toBe('d1RowsWritten');
228
+ expect(top[2].resource).toBe('kvWrites');
229
+ });
230
+
231
+ it('handles fewer contributors than requested', () => {
232
+ const result: BCUResult = {
233
+ total: 1000,
234
+ breakdown: { aiNeurons: 1000 },
235
+ dominantResource: 'aiNeurons',
236
+ dominantPercentage: 100,
237
+ };
238
+
239
+ const top = getTopContributors(result, 5);
240
+ expect(top.length).toBe(1);
241
+ });
242
+ });
243
+
244
+ describe('combineBCUResults', () => {
245
+ it('combines multiple results correctly', () => {
246
+ const results: BCUResult[] = [
247
+ {
248
+ total: 1000,
249
+ breakdown: { aiNeurons: 1000 },
250
+ dominantResource: 'aiNeurons',
251
+ dominantPercentage: 100,
252
+ },
253
+ {
254
+ total: 500,
255
+ breakdown: { kvWrites: 500 },
256
+ dominantResource: 'kvWrites',
257
+ dominantPercentage: 100,
258
+ },
259
+ {
260
+ total: 200,
261
+ breakdown: { aiNeurons: 200 },
262
+ dominantResource: 'aiNeurons',
263
+ dominantPercentage: 100,
264
+ },
265
+ ];
266
+
267
+ const combined = combineBCUResults(results);
268
+
269
+ expect(combined.total).toBe(1700);
270
+ expect(combined.breakdown.aiNeurons).toBe(1200);
271
+ expect(combined.breakdown.kvWrites).toBe(500);
272
+ expect(combined.dominantResource).toBe('aiNeurons');
273
+ });
274
+
275
+ it('handles empty results array', () => {
276
+ const combined = combineBCUResults([]);
277
+
278
+ expect(combined.total).toBe(0);
279
+ expect(combined.dominantResource).toBeNull();
280
+ });
281
+ });
282
+
283
+ describe('Weight Ratios', () => {
284
+ it('AI is weighted much higher than D1 writes', () => {
285
+ // 1 AI neuron should cost more than 1 D1 write
286
+ const aiWeight = DEFAULT_BCU_WEIGHTS.aiNeurons;
287
+ const d1Weight = DEFAULT_BCU_WEIGHTS.d1RowsWritten;
288
+
289
+ expect(aiWeight).toBeGreaterThan(d1Weight);
290
+ expect(aiWeight / d1Weight).toBe(10); // AI is 10x D1 writes
291
+ });
292
+
293
+ it('writes are weighted higher than reads', () => {
294
+ expect(DEFAULT_BCU_WEIGHTS.d1RowsWritten).toBeGreaterThan(DEFAULT_BCU_WEIGHTS.d1RowsRead);
295
+ expect(DEFAULT_BCU_WEIGHTS.kvWrites).toBeGreaterThan(DEFAULT_BCU_WEIGHTS.kvReads);
296
+ });
297
+
298
+ it('requests have very low weight', () => {
299
+ expect(DEFAULT_BCU_WEIGHTS.requests).toBeLessThan(0.01);
300
+ });
301
+ });
302
+
303
+ describe('Real-World Scenarios', () => {
304
+ it('calculates typical Scout OCR invocation', () => {
305
+ // A typical Scout OCR invocation might:
306
+ // - 50 AI neurons (OCR processing)
307
+ // - 10 D1 writes (store results)
308
+ // - 5 KV writes (cache)
309
+ // - 1 request
310
+ const metrics: FeatureMetrics = {
311
+ aiNeurons: 50,
312
+ d1RowsWritten: 10,
313
+ kvWrites: 5,
314
+ requests: 1,
315
+ };
316
+
317
+ const result = calculateBCU(metrics);
318
+
319
+ // 50*100 + 10*10 + 5*1 + 1*0.001 = 5000 + 100 + 5 + 0.001 = 5105.001
320
+ expect(result.total).toBeCloseTo(5105, 0);
321
+ expect(result.dominantResource).toBe('aiNeurons');
322
+ });
323
+
324
+ it('calculates typical Brand Copilot API call', () => {
325
+ // A typical Brand Copilot call might:
326
+ // - 200 AI neurons (LLM processing)
327
+ // - 3 D1 writes
328
+ // - 10 KV reads
329
+ // - 2 KV writes
330
+ const metrics: FeatureMetrics = {
331
+ aiNeurons: 200,
332
+ d1RowsWritten: 3,
333
+ kvReads: 10,
334
+ kvWrites: 2,
335
+ requests: 1,
336
+ };
337
+
338
+ const result = calculateBCU(metrics);
339
+
340
+ // 200*100 + 3*10 + 10*0.1 + 2*1 + 1*0.001 = 20000 + 30 + 1 + 2 + 0.001 ≈ 20033
341
+ expect(result.total).toBeCloseTo(20033, 0);
342
+ expect(result.dominantResource).toBe('aiNeurons');
343
+ expect(result.dominantPercentage).toBeGreaterThan(99);
344
+ });
345
+
346
+ it('calculates read-heavy workload', () => {
347
+ // A read-heavy analytics query:
348
+ // - 0 AI neurons
349
+ // - 1000 D1 reads
350
+ // - 50 KV reads
351
+ // - 0 writes
352
+ const metrics: FeatureMetrics = {
353
+ d1RowsRead: 1000,
354
+ kvReads: 50,
355
+ requests: 1,
356
+ };
357
+
358
+ const result = calculateBCU(metrics);
359
+
360
+ // 1000*0.01 + 50*0.1 + 1*0.001 = 10 + 5 + 0.001 = 15.001
361
+ expect(result.total).toBeCloseTo(15, 0);
362
+ expect(result.dominantResource).toBe('d1RowsRead');
363
+ });
364
+ });
365
+ });