@littlebearapps/platform-admin-sdk 2.0.0 → 2.1.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.
- package/README.md +2 -2
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +86 -2
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- 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
|
+
});
|