godcrm/backend/services/__tests__/agent-table-resolver.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

150 lines
5.8 KiB
JavaScript

// ADR-164 Phase 3a — characterization ("golden-set") test for the agent-table resolver.
//
// Proves the shadow-read contract that lets the 11 hot-path swaps be behaviour-preserving:
// 1. 'name' mode returns exactly the name-matched table ids (the legacy literal).
// 2. 'marker' mode returns exactly the table_type='ai_agents' ids.
// 3. 'dual' mode (default) RETURNS THE NAME-SET — byte-identical to the old behaviour —
// regardless of what the marker set contains.
// 4. 'dual' logs divergence separately:
// - name \ marker -> WARN kind=name_minus_marker (SAFETY gate; must be empty in prod)
// - marker \ name -> INFO kind=marker_minus_name (feature signal)
// and when the two sets are identical, it logs NOTHING.
// 5. spaceId scopes the query; empty result yields [].
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
const mockDbAll = vi.fn();
const mockIsPostgres = vi.fn(() => true);
const mockWarn = vi.fn();
const mockInfo = vi.fn();
vi.mock('../../database/connection.js', () => ({
dbAll: (...a) => mockDbAll(...a),
isPostgres: (...a) => mockIsPostgres(...a),
}));
vi.mock('../../utils/logger.js', () => ({
apiLogger: { debug: vi.fn(), info: (...a) => mockInfo(...a), warn: (...a) => mockWarn(...a), error: vi.fn() },
}));
const { getAgentTableIds } = await import('../agentTableResolver.js');
// Route each dbAll call to a name- or marker-set based on the SQL it runs.
function wireSets({ nameIds = [], markerIds = [] }) {
mockDbAll.mockImplementation((sql) => {
if (/ut\.table_type\s*=/i.test(sql)) return Promise.resolve(markerIds.map(id => ({ id })));
if (/ut\.name\s*=/i.test(sql)) return Promise.resolve(nameIds.map(id => ({ id })));
return Promise.resolve([]);
});
}
const ORIG_MODE = process.env.AGENT_TABLE_RESOLVE_MODE;
beforeEach(() => {
mockDbAll.mockReset();
mockIsPostgres.mockReturnValue(true);
mockWarn.mockReset();
mockInfo.mockReset();
});
afterEach(() => {
if (ORIG_MODE === undefined) delete process.env.AGENT_TABLE_RESOLVE_MODE;
else process.env.AGENT_TABLE_RESOLVE_MODE = ORIG_MODE;
});
describe('getAgentTableIds — modes', () => {
it("'name' mode returns the name-matched ids (legacy literal) and never queries the marker", async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'name';
wireSets({ nameIds: [1784, 278], markerIds: [1784] });
const ids = await getAgentTableIds();
expect(ids.sort((a, b) => a - b)).toEqual([278, 1784]);
expect(mockDbAll).toHaveBeenCalledTimes(1); // name only
expect(mockWarn).not.toHaveBeenCalled();
expect(mockInfo).not.toHaveBeenCalled();
});
it("'marker' mode returns the marker ids and never queries by name", async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'marker';
wireSets({ nameIds: [1784, 278], markerIds: [1784, 999] });
const ids = await getAgentTableIds();
expect(ids.sort((a, b) => a - b)).toEqual([999, 1784]);
expect(mockDbAll).toHaveBeenCalledTimes(1); // marker only
});
});
describe('getAgentTableIds — dual (default) shadow-read', () => {
it('returns the NAME-set even when the marker set differs (behaviour-preserving)', async () => {
delete process.env.AGENT_TABLE_RESOLVE_MODE; // default => dual
wireSets({ nameIds: [1784, 278], markerIds: [1784, 999] });
const ids = await getAgentTableIds();
expect(ids.sort((a, b) => a - b)).toEqual([278, 1784]); // == name-set, NOT the marker-set
expect(mockDbAll).toHaveBeenCalledTimes(2); // both queried
});
it('logs name_minus_marker as a WARN (safety gate) when a served table lacks the marker', async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'dual';
wireSets({ nameIds: [1784, 278], markerIds: [1784] }); // 278 has no marker
await getAgentTableIds();
const warned = mockWarn.mock.calls.some(
([meta]) => meta?.event === 'agent_table_resolve_divergence'
&& meta?.kind === 'name_minus_marker'
&& Array.isArray(meta.tableIds) && meta.tableIds.includes(278),
);
expect(warned).toBe(true);
});
it('logs marker_minus_name as INFO (feature signal) for renamed/copy_space/passport-less tables', async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'dual';
wireSets({ nameIds: [1784], markerIds: [1784, 999] }); // 999 marker-only
await getAgentTableIds();
const infoed = mockInfo.mock.calls.some(
([meta]) => meta?.event === 'agent_table_resolve_divergence'
&& meta?.kind === 'marker_minus_name'
&& Array.isArray(meta.tableIds) && meta.tableIds.includes(999),
);
expect(infoed).toBe(true);
});
it('logs NOTHING when name-set and marker-set are identical (the post-backfill steady state)', async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'dual';
wireSets({ nameIds: [1784, 278], markerIds: [278, 1784] }); // same set, any order
const ids = await getAgentTableIds();
expect(ids.sort((a, b) => a - b)).toEqual([278, 1784]);
expect(mockWarn).not.toHaveBeenCalled();
expect(mockInfo).not.toHaveBeenCalled();
});
});
describe('getAgentTableIds — scoping & edge cases', () => {
it('passes spaceId into the query and returns [] when nothing matches', async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'name';
wireSets({ nameIds: [] });
const ids = await getAgentTableIds({ spaceId: 11 });
expect(ids).toEqual([]);
const call = mockDbAll.mock.calls[0];
expect(call[0]).toMatch(/p\.space_id/i); // space-scoped SQL
expect(call[1]).toEqual(['AI Agents', 11]); // name + spaceId params
});
it('dedupes and coerces ids to integers', async () => {
process.env.AGENT_TABLE_RESOLVE_MODE = 'name';
mockDbAll.mockResolvedValue([{ id: 1784 }, { id: '1784' }, { id: 278 }, { id: null }]);
const ids = await getAgentTableIds();
expect(ids.sort((a, b) => a - b)).toEqual([278, 1784]);
});
});