@wopr-network/platform-core 1.73.0 → 1.74.1

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.
@@ -5,6 +5,7 @@ import { DrizzleBotBilling } from "./bot-billing.js";
5
5
  describe("bot-billing storage tier", () => {
6
6
  let pool;
7
7
  let db;
8
+ let repo;
8
9
  let billing;
9
10
  beforeAll(async () => {
10
11
  ({ db, pool } = await createTestDb());
@@ -16,14 +17,17 @@ describe("bot-billing storage tier", () => {
16
17
  });
17
18
  beforeEach(async () => {
18
19
  await rollbackTestTransaction(pool);
19
- billing = new DrizzleBotBilling(new DrizzleBotInstanceRepository(db));
20
+ repo = new DrizzleBotInstanceRepository(db);
21
+ billing = new DrizzleBotBilling(repo);
20
22
  });
21
23
  it("new bot defaults to standard storage tier", async () => {
22
24
  await billing.registerBot("bot-1", "tenant-1", "TestBot");
25
+ await repo.startBilling("bot-1");
23
26
  expect(await billing.getStorageTier("bot-1")).toBe("standard");
24
27
  });
25
28
  it("setStorageTier updates tier", async () => {
26
29
  await billing.registerBot("bot-1", "tenant-1", "TestBot");
30
+ await repo.startBilling("bot-1");
27
31
  await billing.setStorageTier("bot-1", "pro");
28
32
  expect(await billing.getStorageTier("bot-1")).toBe("pro");
29
33
  });
@@ -32,8 +36,11 @@ describe("bot-billing storage tier", () => {
32
36
  });
33
37
  it("getStorageTierCostsForTenant sums active bot storage costs", async () => {
34
38
  await billing.registerBot("bot-1", "tenant-1", "Bot1");
39
+ await repo.startBilling("bot-1");
35
40
  await billing.registerBot("bot-2", "tenant-1", "Bot2");
41
+ await repo.startBilling("bot-2");
36
42
  await billing.registerBot("bot-3", "tenant-1", "Bot3");
43
+ await repo.startBilling("bot-3");
37
44
  await billing.setStorageTier("bot-1", "plus"); // 3 credits/day
38
45
  await billing.setStorageTier("bot-2", "max"); // 15 credits/day
39
46
  // bot-3 stays standard // 0 credits/day
@@ -41,6 +48,7 @@ describe("bot-billing storage tier", () => {
41
48
  });
42
49
  it("getStorageTierCostsForTenant excludes suspended bots", async () => {
43
50
  await billing.registerBot("bot-1", "tenant-1", "Bot1");
51
+ await repo.startBilling("bot-1");
44
52
  await billing.setStorageTier("bot-1", "pro"); // 8 credits/day
45
53
  await billing.suspendBot("bot-1");
46
54
  expect((await billing.getStorageTierCostsForTenant("tenant-1")).toCents()).toBe(0);
@@ -1,9 +1,10 @@
1
1
  import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
4
- import { runRuntimeDeductions } from "./runtime-cron.js";
4
+ import { dailyBotCost, runRuntimeDeductions } from "./runtime-cron.js";
5
5
  describe("runtime cron with storage tiers", () => {
6
6
  const TODAY = "2025-01-01";
7
+ const BASE_COST_CREDIT = dailyBotCost(TODAY);
7
8
  let pool;
8
9
  let db;
9
10
  let ledger;
@@ -28,8 +29,9 @@ describe("runtime cron with storage tiers", () => {
28
29
  });
29
30
  expect(result.processed).toBe(1);
30
31
  const balance = await ledger.balance("t1");
31
- // 1000 - 17 (base) - 8 (pro storage surcharge) = 975
32
- expect(balance.toCents()).toBe(975);
32
+ // 1000 - dailyBotCost (base) - 8 (pro storage surcharge)
33
+ const expected = Credit.fromCents(1000).subtract(BASE_COST_CREDIT).subtract(Credit.fromCents(8));
34
+ expect(balance.toCents()).toBe(expected.toCents());
33
35
  });
34
36
  it("debits only base cost for standard storage tier (zero surcharge)", async () => {
35
37
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
@@ -40,7 +42,8 @@ describe("runtime cron with storage tiers", () => {
40
42
  getStorageTierCosts: async () => Credit.ZERO,
41
43
  });
42
44
  expect(result.processed).toBe(1);
43
- expect((await ledger.balance("t1")).toCents()).toBe(983); // 1000 - 17
45
+ const expectedStd = Credit.fromCents(1000).subtract(BASE_COST_CREDIT);
46
+ expect((await ledger.balance("t1")).toCents()).toBe(expectedStd.toCents());
44
47
  });
45
48
  it("skips storage surcharge when callback not provided (backward compat)", async () => {
46
49
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
@@ -50,10 +53,13 @@ describe("runtime cron with storage tiers", () => {
50
53
  getActiveBotCount: async () => 1,
51
54
  });
52
55
  expect(result.processed).toBe(1);
53
- expect((await ledger.balance("t1")).toCents()).toBe(983); // 1000 - 17
56
+ const expectedBackcompat = Credit.fromCents(1000).subtract(BASE_COST_CREDIT);
57
+ expect((await ledger.balance("t1")).toCents()).toBe(expectedBackcompat.toCents());
54
58
  });
55
59
  it("suspends tenant when storage surcharge exhausts remaining balance", async () => {
56
- await ledger.credit("t1", Credit.fromCents(20), "purchase"); // Only 20 cents
60
+ // Seed just enough for base cost + 3 cents, so storage surcharge (8) exceeds remainder
61
+ const seed = BASE_COST_CREDIT.add(Credit.fromCents(3));
62
+ await ledger.credit("t1", seed, "purchase");
57
63
  const suspended = [];
58
64
  const result = await runRuntimeDeductions({
59
65
  ledger,
@@ -64,7 +70,7 @@ describe("runtime cron with storage tiers", () => {
64
70
  suspended.push(tenantId);
65
71
  },
66
72
  });
67
- // 20 - 17 = 3 remaining, then 8 surcharge > 3, so partial debit + suspend
73
+ // seed - BASE_COST = 3 remaining, then 8 surcharge > 3, so partial debit + suspend
68
74
  expect(result.processed).toBe(1);
69
75
  expect(result.suspended).toContain("t1");
70
76
  expect((await ledger.balance("t1")).toCents()).toBe(0);
@@ -80,9 +80,12 @@ export async function buildContainer(bootConfig) {
80
80
  const profileStore = new ProfileStore(fleetDataDir);
81
81
  const proxy = new ProxyManager();
82
82
  const serviceKeyRepo = new DrizzleServiceKeyRepository(db);
83
+ const { DrizzleBotInstanceRepository } = await import("../fleet/drizzle-bot-instance-repository.js");
84
+ const botInstanceRepo = new DrizzleBotInstanceRepository(db);
83
85
  const manager = new FleetManagerClass(docker, profileStore, undefined, // platformDiscovery
84
86
  undefined, // networkPolicy
85
- proxy);
87
+ proxy, undefined, // commandBus
88
+ botInstanceRepo);
86
89
  fleet = { manager, docker, proxy, profileStore, serviceKeyRepo };
87
90
  }
88
91
  // 9. Crypto services (when enabled)
@@ -27,6 +27,33 @@ export async function startBackgroundServices(container) {
27
27
  // Non-fatal — proxy sync will retry on next health tick
28
28
  }
29
29
  }
30
+ // Backfill bot_instances from YAML profiles (one-time sync on startup)
31
+ if (container.fleet) {
32
+ try {
33
+ const { DrizzleBotInstanceRepository } = await import("../fleet/drizzle-bot-instance-repository.js");
34
+ const botInstanceRepo = new DrizzleBotInstanceRepository(container.db);
35
+ const profiles = await container.fleet.profileStore.list();
36
+ let synced = 0;
37
+ for (const profile of profiles) {
38
+ const existing = await botInstanceRepo.getById(profile.id);
39
+ if (!existing) {
40
+ try {
41
+ await botInstanceRepo.register(profile.id, profile.tenantId, profile.name);
42
+ synced++;
43
+ }
44
+ catch {
45
+ // Ignore duplicates / constraint violations
46
+ }
47
+ }
48
+ }
49
+ if (synced > 0) {
50
+ logger.info(`Backfilled ${synced} bot instances from profiles into DB`);
51
+ }
52
+ }
53
+ catch (err) {
54
+ logger.warn("Failed to backfill bot_instances (non-fatal)", { error: String(err) });
55
+ }
56
+ }
30
57
  // Hot pool manager (if enabled)
31
58
  if (container.hotPool) {
32
59
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.73.0",
3
+ "version": "1.74.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -218,15 +218,28 @@ export class DrizzleBotInstanceRepository implements IBotInstanceRepository {
218
218
  .where(eq(botInstances.id, botId));
219
219
  }
220
220
 
221
- async register(botId: string, tenantId: string, name: string): Promise<void> {
221
+ async register(
222
+ botId: string,
223
+ tenantId: string,
224
+ name: string,
225
+ billingState: BillingState = "inactive",
226
+ ): Promise<void> {
222
227
  await this.db.insert(botInstances).values({
223
228
  id: botId,
224
229
  tenantId,
225
230
  name,
226
- billingState: "active",
231
+ billingState,
227
232
  });
228
233
  }
229
234
 
235
+ async startBilling(botId: string): Promise<void> {
236
+ await this.db.update(botInstances).set({ billingState: "active" }).where(eq(botInstances.id, botId));
237
+ }
238
+
239
+ async stopBilling(botId: string): Promise<void> {
240
+ await this.db.update(botInstances).set({ billingState: "inactive" }).where(eq(botInstances.id, botId));
241
+ }
242
+
230
243
  async getStorageTier(botId: string): Promise<string | null> {
231
244
  const row = (
232
245
  await this.db
@@ -152,6 +152,16 @@ export class FleetManager {
152
152
 
153
153
  const instance = await this.buildInstance(profile);
154
154
  instance.emitCreated();
155
+
156
+ // Register in bot_instances DB table for billing
157
+ if (this.instanceRepo) {
158
+ try {
159
+ await this.instanceRepo.register(id, profile.tenantId, profile.name);
160
+ } catch (err) {
161
+ logger.warn("Failed to register bot instance in DB (non-fatal)", { id, err });
162
+ }
163
+ }
164
+
155
165
  return instance;
156
166
  };
157
167
 
@@ -239,6 +249,15 @@ export class FleetManager {
239
249
  await this.networkPolicy.cleanupAfterRemoval(profile.tenantId);
240
250
  }
241
251
 
252
+ // Remove from bot_instances DB table
253
+ if (this.instanceRepo) {
254
+ try {
255
+ await this.instanceRepo.deleteById(id);
256
+ } catch (err) {
257
+ logger.warn("Failed to delete bot instance from DB (non-fatal)", { id, err });
258
+ }
259
+ }
260
+
242
261
  await this.store.delete(id);
243
262
  logger.info(`Removed bot ${id}`);
244
263
  this.emitEvent("bot.removed", id, profile.tenantId);
@@ -109,6 +109,27 @@ export class Instance {
109
109
  this.emit("bot.created");
110
110
  }
111
111
 
112
+ /**
113
+ * Start billing for this instance ($5/month prorated daily).
114
+ * Call after creation for persistent, billable instances (e.g., Paperclip).
115
+ * Ephemeral instances (e.g., Holy Ship) skip this — they bill per-token at the gateway.
116
+ */
117
+ async startBilling(): Promise<void> {
118
+ if (!this.instanceRepo) {
119
+ logger.warn("startBilling() called but no instanceRepo available", { id: this.id });
120
+ return;
121
+ }
122
+ await this.instanceRepo.setBillingState(this.id, "active");
123
+ logger.info("Billing started for instance", { id: this.id, name: this.profile.name });
124
+ }
125
+
126
+ /** Stop billing for this instance (e.g., on suspend or downgrade). */
127
+ async stopBilling(): Promise<void> {
128
+ if (!this.instanceRepo) return;
129
+ await this.instanceRepo.setBillingState(this.id, "suspended");
130
+ logger.info("Billing stopped for instance", { id: this.id, name: this.profile.name });
131
+ }
132
+
112
133
  async start(): Promise<void> {
113
134
  this.assertLocal("start()");
114
135
  return this.withLock(async () => {
@@ -78,7 +78,7 @@ export interface SelfHostedNodeRegistration extends NodeRegistration {
78
78
  // ---------------------------------------------------------------------------
79
79
 
80
80
  /** Billing lifecycle states for a bot instance. */
81
- export type BillingState = "active" | "suspended" | "destroyed";
81
+ export type BillingState = "active" | "inactive" | "suspended" | "destroyed";
82
82
 
83
83
  /** Plain domain object for a bot instance — mirrors `bot_instances` table. */
84
84
  export interface BotInstance {
@@ -56,6 +56,7 @@ function createMockDeps(nodeId: string | null = "node-1") {
56
56
  describe("BotBilling", () => {
57
57
  let pool: PGlite;
58
58
  let db: DrizzleDb;
59
+ let repo: DrizzleBotInstanceRepository;
59
60
  let billing: BotBilling;
60
61
  let ledger: DrizzleLedger;
61
62
 
@@ -69,7 +70,8 @@ describe("BotBilling", () => {
69
70
 
70
71
  beforeEach(async () => {
71
72
  await truncateAllTables(pool);
72
- billing = new BotBilling(new DrizzleBotInstanceRepository(db));
73
+ repo = new DrizzleBotInstanceRepository(db);
74
+ billing = new BotBilling(repo);
73
75
  ledger = new DrizzleLedger(db);
74
76
 
75
77
  await ledger.seedSystemAccounts();
@@ -78,6 +80,7 @@ describe("BotBilling", () => {
78
80
  describe("registerBot", () => {
79
81
  it("registers a bot in active billing state", async () => {
80
82
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
83
+ await repo.startBilling("bot-1");
81
84
  const info = await billing.getBotBilling("bot-1");
82
85
  expect(info).not.toBeNull();
83
86
  // biome-ignore lint/suspicious/noExplicitAny: intentional test cast
@@ -100,8 +103,11 @@ describe("BotBilling", () => {
100
103
 
101
104
  it("counts only active bots for the tenant", async () => {
102
105
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
106
+ await repo.startBilling("bot-1");
103
107
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
108
+ await repo.startBilling("bot-2");
104
109
  await billing.registerBot("bot-3", "tenant-2", "bot-c");
110
+ await repo.startBilling("bot-3");
105
111
 
106
112
  expect(await billing.getActiveBotCount("tenant-1")).toBe(2);
107
113
  expect(await billing.getActiveBotCount("tenant-2")).toBe(1);
@@ -109,7 +115,9 @@ describe("BotBilling", () => {
109
115
 
110
116
  it("does not count suspended bots", async () => {
111
117
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
118
+ await repo.startBilling("bot-1");
112
119
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
120
+ await repo.startBilling("bot-2");
113
121
  await billing.suspendBot("bot-1");
114
122
 
115
123
  expect(await billing.getActiveBotCount("tenant-1")).toBe(1);
@@ -117,6 +125,7 @@ describe("BotBilling", () => {
117
125
 
118
126
  it("does not count destroyed bots", async () => {
119
127
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
128
+ await repo.startBilling("bot-1");
120
129
  await billing.destroyBot("bot-1");
121
130
 
122
131
  expect(await billing.getActiveBotCount("tenant-1")).toBe(0);
@@ -126,6 +135,7 @@ describe("BotBilling", () => {
126
135
  describe("suspendBot", () => {
127
136
  it("transitions bot from active to suspended", async () => {
128
137
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
138
+ await repo.startBilling("bot-1");
129
139
  await billing.suspendBot("bot-1");
130
140
 
131
141
  const info = await billing.getBotBilling("bot-1");
@@ -139,6 +149,7 @@ describe("BotBilling", () => {
139
149
 
140
150
  it("sets destroyAfter to 30 days after suspension", async () => {
141
151
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
152
+ await repo.startBilling("bot-1");
142
153
  await billing.suspendBot("bot-1");
143
154
 
144
155
  const info = await billing.getBotBilling("bot-1");
@@ -155,8 +166,11 @@ describe("BotBilling", () => {
155
166
  describe("suspendAllForTenant", () => {
156
167
  it("suspends all active bots for a tenant", async () => {
157
168
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
169
+ await repo.startBilling("bot-1");
158
170
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
171
+ await repo.startBilling("bot-2");
159
172
  await billing.registerBot("bot-3", "tenant-2", "bot-c");
173
+ await repo.startBilling("bot-3");
160
174
 
161
175
  const suspended = await billing.suspendAllForTenant("tenant-1");
162
176
 
@@ -174,6 +188,7 @@ describe("BotBilling", () => {
174
188
  describe("reactivateBot", () => {
175
189
  it("transitions bot from suspended to active", async () => {
176
190
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
191
+ await repo.startBilling("bot-1");
177
192
  await billing.suspendBot("bot-1");
178
193
  await billing.reactivateBot("bot-1");
179
194
 
@@ -188,6 +203,7 @@ describe("BotBilling", () => {
188
203
 
189
204
  it("does not reactivate a destroyed bot", async () => {
190
205
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
206
+ await repo.startBilling("bot-1");
191
207
  await billing.destroyBot("bot-1");
192
208
  await billing.reactivateBot("bot-1");
193
209
 
@@ -198,6 +214,7 @@ describe("BotBilling", () => {
198
214
 
199
215
  it("does not affect already-active bots", async () => {
200
216
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
217
+ await repo.startBilling("bot-1");
201
218
  await billing.reactivateBot("bot-1");
202
219
 
203
220
  const info = await billing.getBotBilling("bot-1");
@@ -209,7 +226,9 @@ describe("BotBilling", () => {
209
226
  describe("checkReactivation", () => {
210
227
  it("reactivates suspended bots when balance is positive", async () => {
211
228
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
229
+ await repo.startBilling("bot-1");
212
230
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
231
+ await repo.startBilling("bot-2");
213
232
  await billing.suspendBot("bot-1");
214
233
  await billing.suspendBot("bot-2");
215
234
 
@@ -226,6 +245,7 @@ describe("BotBilling", () => {
226
245
 
227
246
  it("does not reactivate when balance is zero", async () => {
228
247
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
248
+ await repo.startBilling("bot-1");
229
249
  await billing.suspendBot("bot-1");
230
250
 
231
251
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
@@ -235,6 +255,7 @@ describe("BotBilling", () => {
235
255
 
236
256
  it("does not reactivate destroyed bots", async () => {
237
257
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
258
+ await repo.startBilling("bot-1");
238
259
  await billing.destroyBot("bot-1");
239
260
 
240
261
  await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
@@ -261,6 +282,7 @@ describe("BotBilling", () => {
261
282
  describe("destroyBot", () => {
262
283
  it("marks bot as destroyed", async () => {
263
284
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
285
+ await repo.startBilling("bot-1");
264
286
  await billing.destroyBot("bot-1");
265
287
 
266
288
  const info = await billing.getBotBilling("bot-1");
@@ -272,6 +294,7 @@ describe("BotBilling", () => {
272
294
  describe("destroyExpiredBots", () => {
273
295
  it("destroys bots past their grace period", async () => {
274
296
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
297
+ await repo.startBilling("bot-1");
275
298
 
276
299
  // Set destroyAfter to the past using drizzle sql
277
300
  await db
@@ -293,6 +316,7 @@ describe("BotBilling", () => {
293
316
 
294
317
  it("does not destroy bots still within grace period", async () => {
295
318
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
319
+ await repo.startBilling("bot-1");
296
320
  await billing.suspendBot("bot-1");
297
321
 
298
322
  const destroyed = await billing.destroyExpiredBots();
@@ -305,6 +329,7 @@ describe("BotBilling", () => {
305
329
 
306
330
  it("does not touch active bots", async () => {
307
331
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
332
+ await repo.startBilling("bot-1");
308
333
 
309
334
  const destroyed = await billing.destroyExpiredBots();
310
335
  expect(destroyed).toEqual([]);
@@ -386,8 +411,10 @@ describe("BotBilling", () => {
386
411
 
387
412
  it("returns correct daily cost for known storage tiers", async () => {
388
413
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
414
+ await repo.startBilling("bot-1");
389
415
  await billing.setStorageTier("bot-1", "pro");
390
416
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
417
+ await repo.startBilling("bot-2");
391
418
  await billing.setStorageTier("bot-2", "plus");
392
419
 
393
420
  expect((await billing.getStorageTierCostsForTenant("tenant-1")).toCents()).toBe(11);
@@ -395,6 +422,7 @@ describe("BotBilling", () => {
395
422
 
396
423
  it("returns 0 for unknown storage tier (fallback branch)", async () => {
397
424
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
425
+ await repo.startBilling("bot-1");
398
426
  // Bypass setStorageTier to insert an unrecognized tier value directly
399
427
  await pool.query(`UPDATE bot_instances SET storage_tier = 'unknown_tier' WHERE id = 'bot-1'`);
400
428
 
@@ -404,6 +432,7 @@ describe("BotBilling", () => {
404
432
 
405
433
  it("does not include suspended bots in storage tier cost", async () => {
406
434
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
435
+ await repo.startBilling("bot-1");
407
436
  await billing.setStorageTier("bot-1", "pro");
408
437
  await billing.suspendBot("bot-1");
409
438
 
@@ -414,8 +443,11 @@ describe("BotBilling", () => {
414
443
  describe("listForTenant", () => {
415
444
  it("lists all bots regardless of billing state", async () => {
416
445
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
446
+ await repo.startBilling("bot-1");
417
447
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
448
+ await repo.startBilling("bot-2");
418
449
  await billing.registerBot("bot-3", "tenant-2", "bot-c");
450
+ await repo.startBilling("bot-3");
419
451
  await billing.suspendBot("bot-2");
420
452
 
421
453
  const bots = await billing.listForTenant("tenant-1");
@@ -427,6 +459,7 @@ describe("BotBilling", () => {
427
459
  describe("full lifecycle", () => {
428
460
  it("active -> suspended -> reactivated -> active", async () => {
429
461
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
462
+ await repo.startBilling("bot-1");
430
463
  // biome-ignore lint/suspicious/noExplicitAny: intentional test cast
431
464
  expect(((await billing.getBotBilling("bot-1")) as any)?.billingState).toBe("active");
432
465
 
@@ -446,6 +479,7 @@ describe("BotBilling", () => {
446
479
 
447
480
  it("active -> suspended -> destroyed (after grace period)", async () => {
448
481
  await billing.registerBot("bot-1", "tenant-1", "my-bot");
482
+ await repo.startBilling("bot-1");
449
483
  await billing.suspendBot("bot-1");
450
484
 
451
485
  await db.update(botInstances).set({ destroyAfter: sql`now() - interval '1 day'` }).where(sql`id = 'bot-1'`);