@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,252 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ cacheData,
4
+ getCachedData,
5
+ getOrCompute,
6
+ getStoredData,
7
+ invalidateCache,
8
+ putWithMetadata,
9
+ shouldSendAlert,
10
+ type KVNamespaceLike,
11
+ } from '../../storage/kv/cache';
12
+ import {
13
+ getCircuitBreakerState,
14
+ incrementHopCount,
15
+ isServiceAllowed,
16
+ recordFailure,
17
+ recordSuccess,
18
+ resetCircuitBreaker,
19
+ updateCircuitBreakerState,
20
+ } from '../../storage/kv/circuit-breaker';
21
+
22
+ function createMockKV(): KVNamespaceLike & {
23
+ get: ReturnType<typeof vi.fn>;
24
+ put: ReturnType<typeof vi.fn>;
25
+ delete: ReturnType<typeof vi.fn>;
26
+ } {
27
+ return {
28
+ get: vi.fn(),
29
+ put: vi.fn(),
30
+ delete: vi.fn(),
31
+ } as unknown as KVNamespaceLike & {
32
+ get: ReturnType<typeof vi.fn>;
33
+ put: ReturnType<typeof vi.fn>;
34
+ delete: ReturnType<typeof vi.fn>;
35
+ };
36
+ }
37
+
38
+ describe('KV Cache helpers', () => {
39
+ let mockKV: ReturnType<typeof createMockKV>;
40
+
41
+ beforeEach(() => {
42
+ mockKV = createMockKV();
43
+ });
44
+
45
+ describe('Alert deduplication', () => {
46
+ it('allows first alert and stores marker', async () => {
47
+ mockKV.get.mockResolvedValue(null);
48
+
49
+ const result = await shouldSendAlert('test-alert', mockKV);
50
+
51
+ expect(result).toBe(true);
52
+ expect(mockKV.put).toHaveBeenCalledWith(
53
+ 'alert:test-alert',
54
+ expect.any(String),
55
+ expect.objectContaining({ expirationTtl: 3600 })
56
+ );
57
+ });
58
+
59
+ it('suppresses duplicate alerts within TTL window', async () => {
60
+ mockKV.get.mockResolvedValue('already-sent');
61
+
62
+ const result = await shouldSendAlert('duplicate-alert', mockKV);
63
+
64
+ expect(result).toBe(false);
65
+ expect(mockKV.put).not.toHaveBeenCalled();
66
+ });
67
+ });
68
+
69
+ describe('Cache primitives', () => {
70
+ it('stores hot data with default TTL', async () => {
71
+ await cacheData('mrr', { value: 100 }, mockKV);
72
+ expect(mockKV.put).toHaveBeenCalledWith(
73
+ 'cache:mrr',
74
+ JSON.stringify({ value: 100 }),
75
+ expect.objectContaining({ expirationTtl: 900 })
76
+ );
77
+ });
78
+
79
+ it('retrieves cached data and parses JSON', async () => {
80
+ mockKV.get.mockResolvedValue(JSON.stringify({ foo: 'bar' }));
81
+
82
+ const cached = await getCachedData<{ foo: string }>('demo', mockKV);
83
+
84
+ expect(cached).toEqual({ foo: 'bar' });
85
+ expect(mockKV.get).toHaveBeenCalledWith('cache:demo');
86
+ });
87
+
88
+ it('supports getOrCompute pattern with cache miss', async () => {
89
+ mockKV.get.mockResolvedValueOnce(null);
90
+ const compute = vi.fn().mockResolvedValue({ total: 42 });
91
+
92
+ const result = await getOrCompute('report', compute, mockKV, 120);
93
+
94
+ expect(compute).toHaveBeenCalledTimes(1);
95
+ expect(mockKV.put).toHaveBeenCalledWith(
96
+ 'cache:report',
97
+ JSON.stringify({ total: 42 }),
98
+ expect.objectContaining({ expirationTtl: 120 })
99
+ );
100
+ expect(result).toEqual({ total: 42 });
101
+ });
102
+
103
+ it('returns cached value when available without recomputing', async () => {
104
+ mockKV.get.mockResolvedValue(JSON.stringify({ total: 99 }));
105
+ const compute = vi.fn();
106
+
107
+ const result = await getOrCompute('report', compute, mockKV);
108
+
109
+ expect(compute).not.toHaveBeenCalled();
110
+ expect(result).toEqual({ total: 99 });
111
+ });
112
+
113
+ it('invalidates cached entries', async () => {
114
+ await invalidateCache('stale', mockKV);
115
+ expect(mockKV.delete).toHaveBeenCalledWith('cache:stale');
116
+ });
117
+
118
+ it('stores and retrieves arbitrary data payloads', async () => {
119
+ mockKV.get.mockResolvedValue(JSON.stringify({ state: 'ok' }));
120
+
121
+ await putWithMetadata('status', { state: 'ok' }, mockKV, {
122
+ ttl: 30,
123
+ metadata: { scope: 'demo' },
124
+ });
125
+ expect(mockKV.put).toHaveBeenCalledWith(
126
+ 'data:status',
127
+ JSON.stringify({ state: 'ok' }),
128
+ expect.objectContaining({ expirationTtl: 30, metadata: { scope: 'demo' } })
129
+ );
130
+
131
+ const retrieved = await getStoredData<{ state: string }>('status', mockKV);
132
+ expect(retrieved).toEqual({ state: 'ok' });
133
+ });
134
+ });
135
+
136
+ describe('Circuit breaker integration', () => {
137
+ it('returns default state when none persisted', async () => {
138
+ mockKV.get.mockResolvedValue(null);
139
+
140
+ const state = await getCircuitBreakerState('cloakpipe', mockKV);
141
+
142
+ expect(state).toMatchObject({
143
+ hop_count: 0,
144
+ cooldown: false,
145
+ kill_switch: false,
146
+ consecutive_failures: 0,
147
+ });
148
+ });
149
+
150
+ it('updates stored state when incrementing hop count', async () => {
151
+ mockKV.get.mockResolvedValue(
152
+ JSON.stringify({
153
+ hop_count: 1,
154
+ cooldown: false,
155
+ kill_switch: false,
156
+ last_updated: '2025-10-25T10:00:00Z',
157
+ consecutive_failures: 0,
158
+ })
159
+ );
160
+
161
+ const state = await incrementHopCount('cloakpipe', mockKV, 3);
162
+
163
+ expect(state.hop_count).toBe(2);
164
+ expect(mockKV.put).toHaveBeenCalled();
165
+ });
166
+
167
+ it('trips circuit breaker when hop count exceeds limit', async () => {
168
+ mockKV.get.mockResolvedValue(
169
+ JSON.stringify({
170
+ hop_count: 2,
171
+ cooldown: false,
172
+ kill_switch: false,
173
+ last_updated: '2025-10-25T10:00:00Z',
174
+ consecutive_failures: 0,
175
+ })
176
+ );
177
+
178
+ const state = await incrementHopCount('homeostat', mockKV, 3);
179
+
180
+ expect(state.cooldown).toBe(true);
181
+ });
182
+
183
+ it('blocks service when kill switch active', async () => {
184
+ mockKV.get.mockResolvedValue(
185
+ JSON.stringify({
186
+ hop_count: 0,
187
+ cooldown: false,
188
+ kill_switch: true,
189
+ consecutive_failures: 0,
190
+ })
191
+ );
192
+
193
+ const allowed = await isServiceAllowed('gatekeeper', mockKV);
194
+ expect(allowed).toBe(false);
195
+ });
196
+
197
+ it('records failures and trips when threshold reached', async () => {
198
+ mockKV.get.mockResolvedValue(
199
+ JSON.stringify({
200
+ hop_count: 0,
201
+ cooldown: false,
202
+ kill_switch: false,
203
+ consecutive_failures: 4,
204
+ })
205
+ );
206
+
207
+ const state = await recordFailure('cloakpipe', mockKV, 5);
208
+ expect(state.consecutive_failures).toBe(5);
209
+ expect(state.cooldown).toBe(true);
210
+ });
211
+
212
+ it('resets failure counter on success and clears cooldown', async () => {
213
+ mockKV.get.mockResolvedValue(
214
+ JSON.stringify({
215
+ hop_count: 0,
216
+ cooldown: true,
217
+ kill_switch: false,
218
+ consecutive_failures: 2,
219
+ })
220
+ );
221
+
222
+ const state = await recordSuccess('homeostat', mockKV);
223
+ expect(state.consecutive_failures).toBe(0);
224
+ expect(state.cooldown).toBe(false);
225
+ });
226
+
227
+ it('allows manual state updates and resets', async () => {
228
+ const updatedState = {
229
+ hop_count: 1,
230
+ cooldown: true,
231
+ kill_switch: false,
232
+ last_updated: '2025-10-25T10:00:00Z',
233
+ consecutive_failures: 3,
234
+ };
235
+
236
+ await updateCircuitBreakerState('gatekeeper', updatedState, mockKV);
237
+ expect(mockKV.put).toHaveBeenCalledWith(
238
+ 'circuit:gatekeeper',
239
+ expect.any(String),
240
+ expect.objectContaining({ expirationTtl: 86400 })
241
+ );
242
+
243
+ mockKV.put.mockClear();
244
+ await resetCircuitBreaker('gatekeeper', mockKV);
245
+ expect(mockKV.put).toHaveBeenCalledWith(
246
+ 'circuit:gatekeeper',
247
+ expect.any(String),
248
+ expect.objectContaining({ expirationTtl: 86400 })
249
+ );
250
+ });
251
+ });
252
+ });