|
1 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | +import { eq } from "drizzle-orm"; |
2 | 3 | import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; |
3 | 4 | import { |
4 | 5 | agents, |
@@ -637,4 +638,39 @@ describeEmbeddedPostgres("budgetService release gate enforcement", () => { |
637 | 638 | }); |
638 | 639 | expect(overviewAfterResume.activeIncidents).toHaveLength(0); |
639 | 640 | }); |
| 641 | + |
| 642 | + it("keeps the overview readable after the agent a policy points at is deleted", async () => { |
| 643 | + const { companyId, agentId } = await createBudgetFixture(); |
| 644 | + const service = budgetService(db, { cancelWorkForScope: vi.fn().mockResolvedValue(undefined) }); |
| 645 | + await db.insert(budgetPolicies).values({ |
| 646 | + companyId, |
| 647 | + scopeType: "agent", |
| 648 | + scopeId: agentId, |
| 649 | + metric: "billed_cents", |
| 650 | + windowKind: "lifetime", |
| 651 | + amount: 500, |
| 652 | + warnPercent: 80, |
| 653 | + hardStopEnabled: true, |
| 654 | + notifyEnabled: false, |
| 655 | + isActive: true, |
| 656 | + }); |
| 657 | + |
| 658 | + // scopeId is a soft reference with no foreign key, so deleting the agent |
| 659 | + // leaves the policy pointing at a row that is gone. |
| 660 | + await db.delete(agents).where(eq(agents.id, agentId)); |
| 661 | + |
| 662 | + // Previously this threw notFound. The overview resolves every scope in one |
| 663 | + // pass, so one deleted agent took out the whole view instead of one row. |
| 664 | + const overview = await service.overview(companyId); |
| 665 | + |
| 666 | + expect(overview.policies).toHaveLength(1); |
| 667 | + expect(overview.policies[0]).toMatchObject({ |
| 668 | + scopeType: "agent", |
| 669 | + scopeId: agentId, |
| 670 | + amount: 500, |
| 671 | + paused: false, |
| 672 | + }); |
| 673 | + expect(overview.policies[0]?.scopeName).toContain("deleted"); |
| 674 | + }); |
| 675 | + |
640 | 676 | }); |
0 commit comments