fix(vv): resolve all 6 V&V issues — field trial unblocked

All findings from the inaugural LeadValidator audit resolved and
confirmed. Release gate: PASS.

VV_ISSUE_002 (BLOCKER): 15 OpenAPI specs verified present covering
all 20 route groups (46 endpoints documented in docs/openapi/)

VV_ISSUE_003 (MAJOR): Remove any types from src/db/pool.ts —
replaced pool.query shim with unknown[] + Object.defineProperty,
zero any types, eslint-disable suppressions removed

VV_ISSUE_004 (MAJOR): Remove raw Pool from ScaffoldController and
HealthDetailedController — injected AgentRepository/CredentialRepository
and DbProbe interface respectively; added CredentialRepository.findActiveClientId()

VV_ISSUE_005 (MAJOR): Add unit tests for 5 untested services —
ComplianceStatusStore, EventPublisher, MarketplaceService,
OIDCTrustPolicyService, UsageService

VV_ISSUE_006 (MAJOR): Add integration tests for 7 missing route
groups — analytics, billing, tiers, webhooks, marketplace,
oidc-trust-policies, oidc-token-exchange

VV_ISSUE_001 (MINOR): Create missing design.md and tasks.md in 4
OpenSpec archives — all archives now complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-07 04:52:47 +00:00
parent d216096dfb
commit 7441c9f298
49 changed files with 8954 additions and 70 deletions

View File

@@ -0,0 +1,102 @@
/**
* Unit tests for src/services/ComplianceStatusStore.ts
*
* Uses jest.isolateModules to reset module-level state between test groups
* since the store is a module-level Map singleton.
*/
describe('ComplianceStatusStore', () => {
// Re-import the module fresh for each describe block to reset state
let updateControlStatus: (id: string, status: string) => void;
let getAllControlStatuses: () => unknown[];
let getControlStatus: (id: string) => unknown;
beforeEach(() => {
jest.resetModules();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const store = require('../../../src/services/ComplianceStatusStore');
updateControlStatus = store.updateControlStatus;
getAllControlStatuses = store.getAllControlStatuses;
getControlStatus = store.getControlStatus;
});
describe('getAllControlStatuses()', () => {
it('should return 5 controls on fresh module load', () => {
const statuses = getAllControlStatuses();
expect(statuses).toHaveLength(5);
});
it('should default all controls to unknown status', () => {
const statuses = getAllControlStatuses() as Array<{ status: string }>;
expect(statuses.every((s) => s.status === 'unknown')).toBe(true);
});
it('should return controls in canonical order', () => {
const statuses = getAllControlStatuses() as Array<{ id: string }>;
const ids = statuses.map((s) => s.id);
expect(ids).toEqual(['CC6.1', 'CC6.7', 'CC7.2', 'CC9.2', 'CC7.1']);
});
it('should include name and lastChecked fields on each control', () => {
const statuses = getAllControlStatuses() as Array<{ id: string; name: string; lastChecked: string }>;
for (const s of statuses) {
expect(typeof s.name).toBe('string');
expect(s.name.length).toBeGreaterThan(0);
expect(typeof s.lastChecked).toBe('string');
expect(() => new Date(s.lastChecked)).not.toThrow();
}
});
it('should map CC6.1 to Encryption at Rest', () => {
const statuses = getAllControlStatuses() as Array<{ id: string; name: string }>;
const cc61 = statuses.find((s) => s.id === 'CC6.1');
expect(cc61?.name).toBe('Encryption at Rest');
});
});
describe('updateControlStatus()', () => {
it('should update a control to passing', () => {
updateControlStatus('CC6.1', 'passing');
const status = getControlStatus('CC6.1') as { status: string };
expect(status.status).toBe('passing');
});
it('should update a control to failing', () => {
updateControlStatus('CC7.2', 'failing');
const status = getControlStatus('CC7.2') as { status: string };
expect(status.status).toBe('failing');
});
it('should overwrite a previous status', () => {
updateControlStatus('CC9.2', 'passing');
updateControlStatus('CC9.2', 'failing');
const status = getControlStatus('CC9.2') as { status: string };
expect(status.status).toBe('failing');
});
it('should update lastChecked timestamp on each update', async () => {
const before = Date.now();
updateControlStatus('CC7.1', 'passing');
const status = getControlStatus('CC7.1') as { lastChecked: string };
const after = new Date(status.lastChecked).getTime();
expect(after).toBeGreaterThanOrEqual(before);
});
it('should not affect other controls when one is updated', () => {
updateControlStatus('CC6.1', 'passing');
const all = getAllControlStatuses() as Array<{ id: string; status: string }>;
const others = all.filter((s) => s.id !== 'CC6.1');
expect(others.every((s) => s.status === 'unknown')).toBe(true);
});
});
describe('getControlStatus()', () => {
it('should return the correct control record', () => {
updateControlStatus('CC6.7', 'passing');
const status = getControlStatus('CC6.7') as { id: string; name: string; status: string };
expect(status.id).toBe('CC6.7');
expect(status.name).toBe('TLS Enforcement');
expect(status.status).toBe('passing');
});
});
});

View File

@@ -0,0 +1,163 @@
/**
* Unit tests for src/services/EventPublisher.ts
*/
import { EventPublisher } from '../../../src/services/EventPublisher';
import { WebhookDeliveryWorker } from '../../../src/workers/WebhookDeliveryWorker';
import { Pool } from 'pg';
jest.mock('../../../src/workers/WebhookDeliveryWorker');
jest.mock('pg');
const MockPool = Pool as jest.MockedClass<typeof Pool>;
const MockWorker = WebhookDeliveryWorker as jest.MockedClass<typeof WebhookDeliveryWorker>;
function makePool(queryImpl?: jest.Mock): jest.Mocked<Pool> {
const pool = new MockPool() as jest.Mocked<Pool>;
pool.query = queryImpl ?? jest.fn();
return pool;
}
function makeWorker(): jest.Mocked<WebhookDeliveryWorker> {
const worker = new MockWorker({} as never) as jest.Mocked<WebhookDeliveryWorker>;
worker.enqueue = jest.fn().mockResolvedValue(undefined);
return worker;
}
const ORG_ID = 'org-abc-123';
const EVENT_TYPE = 'agent.created' as const;
const DATA = { agentId: 'agent-001' };
describe('EventPublisher', () => {
let pool: jest.Mocked<Pool>;
let worker: jest.Mocked<WebhookDeliveryWorker>;
beforeEach(() => {
jest.clearAllMocks();
pool = makePool();
worker = makeWorker();
});
describe('publishEvent() — webhook fanout', () => {
it('should query for active subscriptions and create a delivery record', async () => {
const subscriptionRows = [{ id: 'sub-001', organization_id: ORG_ID }];
const deliveryRow = [{ id: 'del-001' }];
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: subscriptionRows, rowCount: 1 })
.mockResolvedValueOnce({ rows: deliveryRow, rowCount: 1 });
const publisher = new EventPublisher(worker, pool, null);
await publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA);
expect(pool.query).toHaveBeenCalledTimes(2);
expect(pool.query).toHaveBeenNthCalledWith(
1,
expect.stringContaining('webhook_subscriptions'),
[ORG_ID, JSON.stringify([EVENT_TYPE])],
);
expect(pool.query).toHaveBeenNthCalledWith(
2,
expect.stringContaining('webhook_deliveries'),
expect.arrayContaining(['sub-001', EVENT_TYPE]),
);
});
it('should enqueue a Bull delivery job for each matching subscription', async () => {
const subscriptionRows = [{ id: 'sub-001', organization_id: ORG_ID }];
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: subscriptionRows, rowCount: 1 })
.mockResolvedValueOnce({ rows: [{ id: 'del-001' }], rowCount: 1 });
const publisher = new EventPublisher(worker, pool, null);
await publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA);
expect(worker.enqueue).toHaveBeenCalledTimes(1);
expect(worker.enqueue).toHaveBeenCalledWith(
expect.objectContaining({
deliveryId: 'del-001',
subscriptionId: 'sub-001',
organizationId: ORG_ID,
}),
);
});
it('should fan out to multiple subscriptions', async () => {
const subscriptionRows = [
{ id: 'sub-001', organization_id: ORG_ID },
{ id: 'sub-002', organization_id: ORG_ID },
];
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: subscriptionRows, rowCount: 2 })
.mockResolvedValueOnce({ rows: [{ id: 'del-001' }], rowCount: 1 })
.mockResolvedValueOnce({ rows: [{ id: 'del-002' }], rowCount: 1 });
const publisher = new EventPublisher(worker, pool, null);
await publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA);
expect(worker.enqueue).toHaveBeenCalledTimes(2);
});
it('should not enqueue any jobs when no matching subscriptions exist', async () => {
pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });
const publisher = new EventPublisher(worker, pool, null);
await publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA);
expect(worker.enqueue).not.toHaveBeenCalled();
});
it('should not throw when subscription DB query fails', async () => {
pool.query = jest.fn().mockRejectedValueOnce(new Error('DB down'));
const publisher = new EventPublisher(worker, pool, null);
await expect(publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA)).resolves.toBeUndefined();
});
it('should not throw when delivery insert fails for a subscription', async () => {
const subscriptionRows = [{ id: 'sub-001', organization_id: ORG_ID }];
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: subscriptionRows, rowCount: 1 })
.mockRejectedValueOnce(new Error('Insert failed'));
const publisher = new EventPublisher(worker, pool, null);
await expect(publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA)).resolves.toBeUndefined();
});
});
describe('publishEvent() — Kafka fanout', () => {
it('should produce to Kafka when kafkaProducer is provided', async () => {
pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });
const kafkaProducer = { send: jest.fn().mockResolvedValue(undefined) };
const publisher = new EventPublisher(worker, pool, kafkaProducer as never);
await publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA);
expect(kafkaProducer.send).toHaveBeenCalledWith(
expect.objectContaining({
topic: 'agentidp-events',
messages: expect.arrayContaining([
expect.objectContaining({ key: ORG_ID }),
]),
}),
);
});
it('should not call Kafka when kafkaProducer is null', async () => {
pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });
const kafkaProducer = { send: jest.fn() };
const publisher = new EventPublisher(worker, pool, null);
await publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA);
expect(kafkaProducer.send).not.toHaveBeenCalled();
});
it('should not throw when Kafka produce fails', async () => {
pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });
const kafkaProducer = { send: jest.fn().mockRejectedValue(new Error('Kafka error')) };
const publisher = new EventPublisher(worker, pool, kafkaProducer as never);
await expect(publisher.publishEvent(ORG_ID, EVENT_TYPE, DATA)).resolves.toBeUndefined();
});
});
});

View File

@@ -0,0 +1,117 @@
/**
* Unit tests for src/services/MarketplaceService.ts
*/
import { MarketplaceService } from '../../../src/services/MarketplaceService';
import { AgentRepository } from '../../../src/repositories/AgentRepository';
import { AgentNotFoundError } from '../../../src/utils/errors';
import { IAgent, IMarketplaceFilters } from '../../../src/types/index';
jest.mock('../../../src/repositories/AgentRepository');
const MockAgentRepo = AgentRepository as jest.MockedClass<typeof AgentRepository>;
const BASE_FILTERS: IMarketplaceFilters = { page: 1, limit: 10 };
function makeAgent(overrides: Partial<IAgent> = {}): IAgent {
return {
agentId: 'agent-001',
organizationId: 'org-001',
email: 'agent@example.com',
agentType: 'screener',
version: 'v1.0.0',
capabilities: ['resume:read'],
owner: 'test-team',
deploymentEnv: 'production',
status: 'active',
isPublic: true,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-02'),
did: null,
didCreatedAt: null,
...overrides,
};
}
describe('MarketplaceService', () => {
let service: MarketplaceService;
let agentRepo: jest.Mocked<AgentRepository>;
beforeEach(() => {
jest.clearAllMocks();
agentRepo = new MockAgentRepo({} as never) as jest.Mocked<AgentRepository>;
service = new MarketplaceService(agentRepo);
});
describe('listPublicAgents()', () => {
it('should return mapped agent cards', async () => {
const agent = makeAgent();
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
const result = await service.listPublicAgents(BASE_FILTERS);
expect(result.data).toHaveLength(1);
expect(result.data[0].agentId).toBe('agent-001');
expect(result.total).toBe(1);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
});
it('should strip private fields (email, organizationId) from cards', async () => {
const agent = makeAgent();
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
const result = await service.listPublicAgents(BASE_FILTERS);
const card = result.data[0] as Record<string, unknown>;
expect(card['email']).toBeUndefined();
expect(card['organizationId']).toBeUndefined();
});
it('should include a minimal DID document when agent has a DID', async () => {
const agent = makeAgent({ did: 'did:web:sentryagent.ai:agents:agent-001' });
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
const result = await service.listPublicAgents(BASE_FILTERS);
expect(result.data[0].didDocument).not.toBeNull();
expect(result.data[0].didDocument?.id).toBe('did:web:sentryagent.ai:agents:agent-001');
});
it('should return null DID document when agent has no DID', async () => {
const agent = makeAgent({ did: null });
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
const result = await service.listPublicAgents(BASE_FILTERS);
expect(result.data[0].didDocument).toBeNull();
});
it('should return empty data array when no public agents exist', async () => {
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [], total: 0 });
const result = await service.listPublicAgents(BASE_FILTERS);
expect(result.data).toHaveLength(0);
expect(result.total).toBe(0);
});
});
describe('getPublicAgent()', () => {
it('should return a card for a public agent', async () => {
const agent = makeAgent();
agentRepo.findPublicById = jest.fn().mockResolvedValue(agent);
const card = await service.getPublicAgent('agent-001');
expect(card.agentId).toBe('agent-001');
expect(card.owner).toBe('test-team');
});
it('should throw AgentNotFoundError when agent is not found', async () => {
agentRepo.findPublicById = jest.fn().mockResolvedValue(null);
await expect(service.getPublicAgent('nonexistent')).rejects.toThrow(AgentNotFoundError);
});
});
});

View File

@@ -0,0 +1,200 @@
/**
* Unit tests for src/services/OIDCTrustPolicyService.ts
*/
import { Pool } from 'pg';
import {
OIDCTrustPolicyService,
TrustPolicyNotFoundError,
TrustPolicyViolationError,
} from '../../../src/services/OIDCTrustPolicyService';
import { ValidationError } from '../../../src/utils/errors';
jest.mock('pg');
const MockPool = Pool as jest.MockedClass<typeof Pool>;
function makePool(): jest.Mocked<Pool> {
const pool = new MockPool() as jest.Mocked<Pool>;
pool.query = jest.fn();
return pool;
}
function makePolicyRow(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
id: 'policy-001',
provider: 'github',
repository: 'acme/my-repo',
branch: null,
agent_id: 'agent-001',
created_at: new Date('2026-01-01'),
updated_at: new Date('2026-01-01'),
...overrides,
};
}
describe('OIDCTrustPolicyService', () => {
let service: OIDCTrustPolicyService;
let pool: jest.Mocked<Pool>;
beforeEach(() => {
jest.clearAllMocks();
pool = makePool();
service = new OIDCTrustPolicyService(pool);
});
describe('createTrustPolicy()', () => {
it('should create a trust policy successfully', async () => {
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: [{ agent_id: 'agent-001' }], rowCount: 1 })
.mockResolvedValueOnce({ rows: [makePolicyRow()], rowCount: 1 });
const result = await service.createTrustPolicy({
provider: 'github',
repository: 'acme/my-repo',
branch: null,
agentId: 'agent-001',
});
expect(result.provider).toBe('github');
expect(result.repository).toBe('acme/my-repo');
expect(result.branch).toBeNull();
});
it('should throw ValidationError for non-github provider', async () => {
await expect(
service.createTrustPolicy({
provider: 'gitlab' as never,
repository: 'acme/my-repo',
branch: null,
agentId: 'agent-001',
}),
).rejects.toThrow(ValidationError);
});
it('should throw ValidationError for malformed repository', async () => {
await expect(
service.createTrustPolicy({
provider: 'github',
repository: 'no-slash-here',
branch: null,
agentId: 'agent-001',
}),
).rejects.toThrow(ValidationError);
});
it('should throw ValidationError when agentId is empty', async () => {
await expect(
service.createTrustPolicy({
provider: 'github',
repository: 'acme/my-repo',
branch: null,
agentId: '',
}),
).rejects.toThrow(ValidationError);
});
it('should throw ValidationError when agent not found', async () => {
pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });
await expect(
service.createTrustPolicy({
provider: 'github',
repository: 'acme/my-repo',
branch: null,
agentId: 'nonexistent',
}),
).rejects.toThrow(ValidationError);
});
});
describe('listTrustPoliciesForAgent()', () => {
it('should return mapped policies', async () => {
pool.query = jest.fn().mockResolvedValue({
rows: [makePolicyRow(), makePolicyRow({ id: 'policy-002' })],
rowCount: 2,
});
const policies = await service.listTrustPoliciesForAgent('agent-001');
expect(policies).toHaveLength(2);
expect(policies[0].id).toBe('policy-001');
});
it('should return an empty array when no policies exist', async () => {
pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });
const policies = await service.listTrustPoliciesForAgent('agent-001');
expect(policies).toHaveLength(0);
});
});
describe('deleteTrustPolicy()', () => {
it('should delete a policy successfully', async () => {
pool.query = jest.fn().mockResolvedValue({ rowCount: 1 });
await expect(service.deleteTrustPolicy('policy-001')).resolves.toBeUndefined();
});
it('should throw TrustPolicyNotFoundError when policy does not exist', async () => {
pool.query = jest.fn().mockResolvedValue({ rowCount: 0 });
await expect(service.deleteTrustPolicy('nonexistent')).rejects.toThrow(TrustPolicyNotFoundError);
});
});
describe('enforceTrustPolicy()', () => {
it('should pass when a wildcard branch policy exists (branch: null)', async () => {
pool.query = jest.fn().mockResolvedValue({
rows: [makePolicyRow({ branch: null })],
rowCount: 1,
});
await expect(
service.enforceTrustPolicy('github', 'acme/my-repo', 'refs/heads/main', 'agent-001'),
).resolves.toBeUndefined();
});
it('should pass when branch matches exactly', async () => {
pool.query = jest.fn().mockResolvedValue({
rows: [makePolicyRow({ branch: 'main' })],
rowCount: 1,
});
await expect(
service.enforceTrustPolicy('github', 'acme/my-repo', 'main', 'agent-001'),
).resolves.toBeUndefined();
});
it('should normalize refs/heads/ prefix and match', async () => {
pool.query = jest.fn().mockResolvedValue({
rows: [makePolicyRow({ branch: 'main' })],
rowCount: 1,
});
await expect(
service.enforceTrustPolicy('github', 'acme/my-repo', 'refs/heads/main', 'agent-001'),
).resolves.toBeUndefined();
});
it('should throw TrustPolicyViolationError when no policies exist', async () => {
pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });
await expect(
service.enforceTrustPolicy('github', 'acme/my-repo', 'main', 'agent-001'),
).rejects.toThrow(TrustPolicyViolationError);
});
it('should throw TrustPolicyViolationError when branch does not match constrained policy', async () => {
pool.query = jest.fn().mockResolvedValue({
rows: [makePolicyRow({ branch: 'main' })],
rowCount: 1,
});
await expect(
service.enforceTrustPolicy('github', 'acme/my-repo', 'feature/evil', 'agent-001'),
).rejects.toThrow(TrustPolicyViolationError);
});
});
});

View File

@@ -0,0 +1,116 @@
/**
* Unit tests for src/services/UsageService.ts
*/
import { Pool } from 'pg';
import { UsageService } from '../../../src/services/UsageService';
jest.mock('pg');
const MockPool = Pool as jest.MockedClass<typeof Pool>;
function makePool(): jest.Mocked<Pool> {
const pool = new MockPool() as jest.Mocked<Pool>;
pool.query = jest.fn();
return pool;
}
const TENANT_ID = 'org-abc-123';
const DATE = '2026-04-07';
describe('UsageService', () => {
let service: UsageService;
let pool: jest.Mocked<Pool>;
beforeEach(() => {
jest.clearAllMocks();
pool = makePool();
service = new UsageService(pool);
});
describe('getDailyUsage()', () => {
it('should return usage summary with real api call count', async () => {
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: [{ count: '42' }], rowCount: 1 })
.mockResolvedValueOnce({ rows: [{ count: '5' }], rowCount: 1 });
const result = await service.getDailyUsage(TENANT_ID, DATE);
expect(result.tenantId).toBe(TENANT_ID);
expect(result.date).toBe(DATE);
expect(result.apiCalls).toBe(42);
expect(result.agentCount).toBe(5);
});
it('should default apiCalls to 0 when no usage row exists', async () => {
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: [{ count: '0' }], rowCount: 1 })
.mockResolvedValueOnce({ rows: [{ count: '3' }], rowCount: 1 });
const result = await service.getDailyUsage(TENANT_ID, DATE);
expect(result.apiCalls).toBe(0);
});
it('should handle missing count row gracefully (defaults to 0)', async () => {
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: [], rowCount: 0 })
.mockResolvedValueOnce({ rows: [{ count: '2' }], rowCount: 1 });
const result = await service.getDailyUsage(TENANT_ID, DATE);
expect(result.apiCalls).toBe(0);
});
it('should query usage_events with correct tenant and date params', async () => {
pool.query = jest.fn()
.mockResolvedValueOnce({ rows: [{ count: '10' }], rowCount: 1 })
.mockResolvedValueOnce({ rows: [{ count: '0' }], rowCount: 1 });
await service.getDailyUsage(TENANT_ID, DATE);
expect(pool.query).toHaveBeenNthCalledWith(
1,
expect.stringContaining('usage_events'),
[TENANT_ID, DATE],
);
});
});
describe('getActiveAgentCount()', () => {
it('should return the count of non-decommissioned agents', async () => {
pool.query = jest.fn().mockResolvedValue({ rows: [{ count: '7' }], rowCount: 1 });
const count = await service.getActiveAgentCount(TENANT_ID);
expect(count).toBe(7);
});
it('should return 0 when no agents exist for tenant', async () => {
pool.query = jest.fn().mockResolvedValue({ rows: [{ count: '0' }], rowCount: 1 });
const count = await service.getActiveAgentCount(TENANT_ID);
expect(count).toBe(0);
});
it('should exclude decommissioned agents (query contains status check)', async () => {
pool.query = jest.fn().mockResolvedValue({ rows: [{ count: '3' }], rowCount: 1 });
await service.getActiveAgentCount(TENANT_ID);
expect(pool.query).toHaveBeenCalledWith(
expect.stringContaining('decommissioned'),
[TENANT_ID],
);
});
it('should handle missing count row gracefully', async () => {
pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });
const count = await service.getActiveAgentCount(TENANT_ID);
expect(count).toBe(0);
});
});
});