Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
150 lines
5.8 KiB
JavaScript
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]);
|
|
});
|
|
});
|