Files
nanoclaw/src/modules/approvals/picks.test.ts
gavrielc 0f6a1ba1ed style: apply prettier formatting to touched files
Pre-commit hook reflowed imports on files changed in the previous commit.
Unrelated format drift on other files intentionally left unstaged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:31:42 +03:00

147 lines
4.5 KiB
TypeScript

/**
* Tests for pickApprover + pickApprovalDelivery — the approver-selection
* half of what used to live in src/access.ts. Moved here in PR #7 alongside
* the approvals re-tier.
*/
import { beforeEach, afterEach, describe, expect, it } from 'vitest';
import type { ChannelAdapter, OutboundMessage } from '../../channels/adapter.js';
import {
initChannelAdapters,
registerChannelAdapter,
teardownChannelAdapters,
} from '../../channels/channel-registry.js';
import { closeDb, createAgentGroup, initTestDb, runMigrations } from '../../db/index.js';
import { createUser } from '../permissions/db/users.js';
import { grantRole } from '../permissions/db/user-roles.js';
import { pickApprovalDelivery, pickApprover } from './primitive.js';
function now(): string {
return new Date().toISOString();
}
beforeEach(() => {
const db = initTestDb();
runMigrations(db);
});
afterEach(async () => {
await teardownChannelAdapters();
closeDb();
});
async function mountMockAdapter(
channelType: string,
openDM?: (handle: string) => Promise<string>,
): Promise<{ delivered: OutboundMessage[]; openDMCalls: string[] }> {
const delivered: OutboundMessage[] = [];
const openDMCalls: string[] = [];
const adapter: ChannelAdapter = {
name: channelType,
channelType,
supportsThreads: false,
async setup() {},
async teardown() {},
isConnected() {
return true;
},
async deliver(_platformId, _threadId, message) {
delivered.push(message);
return undefined;
},
async setTyping() {},
};
if (openDM) {
adapter.openDM = async (handle: string) => {
openDMCalls.push(handle);
return openDM(handle);
};
}
registerChannelAdapter(channelType, { factory: () => adapter });
await initChannelAdapters(() => ({
conversations: [],
onInbound: () => {},
onInboundEvent: () => {},
onMetadata: () => {},
onAction: () => {},
}));
return { delivered, openDMCalls };
}
function seedAgentGroup(id: string): void {
createAgentGroup({
id,
name: id.toUpperCase(),
folder: id,
agent_provider: null,
created_at: now(),
});
}
function seedUser(id: string, kind: string): void {
createUser({ id, kind, display_name: null, created_at: now() });
}
describe('pickApprover', () => {
beforeEach(() => {
seedAgentGroup('ag-1');
seedAgentGroup('ag-2');
});
it('prefers scoped admins, then globals, then owners — deduplicated', () => {
seedUser('u-owner', 'telegram');
seedUser('u-ga', 'telegram');
seedUser('u-sa', 'telegram');
grantRole({ user_id: 'u-owner', role: 'owner', agent_group_id: null, granted_by: null, granted_at: now() });
grantRole({ user_id: 'u-ga', role: 'admin', agent_group_id: null, granted_by: null, granted_at: now() });
grantRole({ user_id: 'u-sa', role: 'admin', agent_group_id: 'ag-1', granted_by: null, granted_at: now() });
expect(pickApprover('ag-1')).toEqual(['u-sa', 'u-ga', 'u-owner']);
expect(pickApprover('ag-2')).toEqual(['u-ga', 'u-owner']);
expect(pickApprover(null)).toEqual(['u-ga', 'u-owner']);
});
it('returns empty list when nobody is privileged', () => {
expect(pickApprover('ag-1')).toEqual([]);
});
});
describe('pickApprovalDelivery', () => {
beforeEach(() => {
seedAgentGroup('ag-1');
});
it('returns the first reachable approver', async () => {
await mountMockAdapter('telegram');
seedUser('telegram:111', 'telegram');
seedUser('telegram:222', 'telegram');
const result = await pickApprovalDelivery(['telegram:111', 'telegram:222'], 'telegram');
expect(result?.userId).toBe('telegram:111');
expect(result?.messagingGroup.platform_id).toBe('111');
});
it('prefers same-channel-kind approver on tie-break', async () => {
await mountMockAdapter('telegram');
await mountMockAdapter('discord', async (h) => `dm-${h}`);
seedUser('telegram:111', 'telegram');
seedUser('discord:222', 'discord');
const result = await pickApprovalDelivery(['telegram:111', 'discord:222'], 'discord');
expect(result?.userId).toBe('discord:222');
});
it('falls through to any reachable approver when none match origin', async () => {
await mountMockAdapter('telegram');
seedUser('telegram:111', 'telegram');
const result = await pickApprovalDelivery(['telegram:111'], 'discord');
expect(result?.userId).toBe('telegram:111');
});
it('returns null when nobody is reachable', async () => {
seedUser('telegram:111', 'telegram');
expect(await pickApprovalDelivery(['telegram:111'], 'telegram')).toBeNull();
});
});