712 lines
22 KiB
TypeScript
712 lines
22 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
// Mock the logger so log.warn/error route through console (tests spy on console)
|
|
vi.mock('../logger.js', () => ({
|
|
createLogger: () => ({
|
|
fatal: (...args: unknown[]) => console.error(...args),
|
|
error: (...args: unknown[]) => console.error(...args),
|
|
warn: (...args: unknown[]) => console.warn(...args),
|
|
info: (...args: unknown[]) => console.log(...args),
|
|
debug: (...args: unknown[]) => console.log(...args),
|
|
trace: (...args: unknown[]) => console.log(...args),
|
|
pino: {},
|
|
}),
|
|
}));
|
|
|
|
import {
|
|
normalizeAgents,
|
|
canonicalizeServerMode,
|
|
isApiServerMode,
|
|
isDockerServerMode,
|
|
type LettaBotConfig,
|
|
type AgentConfig,
|
|
} from './types.js';
|
|
|
|
describe('normalizeAgents', () => {
|
|
const envVars = [
|
|
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_DM_POLICY', 'TELEGRAM_ALLOWED_USERS',
|
|
'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN', 'SLACK_DM_POLICY', 'SLACK_ALLOWED_USERS',
|
|
'WHATSAPP_ENABLED', 'WHATSAPP_SELF_CHAT_MODE', 'WHATSAPP_DM_POLICY', 'WHATSAPP_ALLOWED_USERS',
|
|
'SIGNAL_PHONE_NUMBER', 'SIGNAL_SELF_CHAT_MODE', 'SIGNAL_DM_POLICY', 'SIGNAL_ALLOWED_USERS',
|
|
'DISCORD_BOT_TOKEN', 'DISCORD_DM_POLICY', 'DISCORD_ALLOWED_USERS',
|
|
'BLUESKY_WANTED_DIDS', 'BLUESKY_WANTED_COLLECTIONS', 'BLUESKY_JETSTREAM_URL', 'BLUESKY_CURSOR',
|
|
'BLUESKY_HANDLE', 'BLUESKY_APP_PASSWORD', 'BLUESKY_SERVICE_URL', 'BLUESKY_APPVIEW_URL',
|
|
'BLUESKY_NOTIFICATIONS_ENABLED', 'BLUESKY_NOTIFICATIONS_INTERVAL_SEC', 'BLUESKY_NOTIFICATIONS_LIMIT',
|
|
'BLUESKY_NOTIFICATIONS_PRIORITY', 'BLUESKY_NOTIFICATIONS_REASONS',
|
|
'HEARTBEAT_ENABLED', 'HEARTBEAT_INTERVAL_MIN', 'HEARTBEAT_SKIP_RECENT_USER_MIN',
|
|
'CRON_ENABLED',
|
|
];
|
|
const savedEnv: Record<string, string | undefined> = {};
|
|
|
|
beforeEach(() => {
|
|
for (const key of envVars) {
|
|
savedEnv[key] = process.env[key];
|
|
delete process.env[key];
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const key of envVars) {
|
|
if (savedEnv[key] !== undefined) {
|
|
process.env[key] = savedEnv[key];
|
|
} else {
|
|
delete process.env[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
it('canonicalizes legacy server mode aliases', () => {
|
|
expect(canonicalizeServerMode('cloud')).toBe('api');
|
|
expect(canonicalizeServerMode('api')).toBe('api');
|
|
expect(canonicalizeServerMode('selfhosted')).toBe('docker');
|
|
expect(canonicalizeServerMode('docker')).toBe('docker');
|
|
expect(isApiServerMode('cloud')).toBe(true);
|
|
expect(isDockerServerMode('selfhosted')).toBe(true);
|
|
});
|
|
|
|
it('should normalize legacy single-agent config to one-entry array', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: {
|
|
name: 'TestBot',
|
|
model: 'anthropic/claude-sonnet-4',
|
|
},
|
|
channels: {
|
|
telegram: {
|
|
enabled: true,
|
|
token: 'test-token',
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents).toHaveLength(1);
|
|
expect(agents[0].name).toBe('TestBot');
|
|
expect(agents[0].model).toBe('anthropic/claude-sonnet-4');
|
|
expect(agents[0].channels.telegram?.token).toBe('test-token');
|
|
});
|
|
|
|
it('should drop channels with enabled: false', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {
|
|
telegram: {
|
|
enabled: true,
|
|
token: 'test-token',
|
|
},
|
|
slack: {
|
|
enabled: false,
|
|
botToken: 'should-be-dropped',
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels.telegram).toBeDefined();
|
|
expect(agents[0].channels.slack).toBeUndefined();
|
|
});
|
|
|
|
it('should normalize multi-agent config channels', () => {
|
|
const agentsArray: AgentConfig[] = [
|
|
{
|
|
name: 'Bot1',
|
|
channels: {
|
|
telegram: { enabled: true, token: 'token1' },
|
|
slack: { enabled: true, botToken: 'missing-app-token' },
|
|
},
|
|
},
|
|
{
|
|
name: 'Bot2',
|
|
channels: {
|
|
slack: { enabled: true, botToken: 'token2', appToken: 'app2' },
|
|
discord: { enabled: false, token: 'disabled' },
|
|
},
|
|
},
|
|
];
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agents: agentsArray,
|
|
// Legacy fields (ignored when agents[] is present)
|
|
agent: { name: 'Unused', model: 'unused' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents).toHaveLength(2);
|
|
expect(agents[0].channels.telegram?.token).toBe('token1');
|
|
expect(agents[0].channels.slack).toBeUndefined();
|
|
expect(agents[1].channels.slack?.botToken).toBe('token2');
|
|
expect(agents[1].channels.discord).toBeUndefined();
|
|
});
|
|
|
|
it('should produce empty channels object when no channels configured', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels).toEqual({});
|
|
});
|
|
|
|
it('should default agent name to "LettaBot" when not provided', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: '', model: '' }, // Empty name should fall back to 'LettaBot'
|
|
channels: {},
|
|
};
|
|
|
|
// Override with empty name to test default
|
|
const agents = normalizeAgents({
|
|
...config,
|
|
agent: undefined as any, // Test fallback when agent is missing
|
|
});
|
|
|
|
expect(agents[0].name).toBe('LettaBot');
|
|
});
|
|
|
|
it('should drop channels without required credentials', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {
|
|
telegram: {
|
|
enabled: true,
|
|
// Missing token
|
|
},
|
|
slack: {
|
|
enabled: true,
|
|
botToken: 'has-bot-token-only',
|
|
// Missing appToken
|
|
},
|
|
signal: {
|
|
enabled: true,
|
|
// Missing phone
|
|
},
|
|
discord: {
|
|
enabled: true,
|
|
// Missing token
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels).toEqual({});
|
|
});
|
|
|
|
it('should preserve agent id when provided', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: {
|
|
id: 'agent-123',
|
|
name: 'TestBot',
|
|
model: 'test',
|
|
},
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].id).toBe('agent-123');
|
|
});
|
|
|
|
it('should normalize legacy listeningGroups + requireMention to groups.mode and warn', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot' },
|
|
channels: {
|
|
telegram: {
|
|
enabled: true,
|
|
token: 'test-token',
|
|
listeningGroups: ['-100123', '-100456'],
|
|
groups: {
|
|
'*': { requireMention: true },
|
|
'-100456': { requireMention: false },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
const groups = agents[0].channels.telegram?.groups;
|
|
|
|
expect(groups?.['*']?.mode).toBe('mention-only');
|
|
expect(groups?.['-100123']?.mode).toBe('listen');
|
|
expect(groups?.['-100456']?.mode).toBe('listen');
|
|
expect((agents[0].channels.telegram as any).listeningGroups).toBeUndefined();
|
|
expect(
|
|
warnSpy.mock.calls.some((args) => String(args[0]).includes('listeningGroups'))
|
|
).toBe(true);
|
|
expect(
|
|
warnSpy.mock.calls.some((args) => String(args[0]).includes('requireMention'))
|
|
).toBe(true);
|
|
|
|
warnSpy.mockRestore();
|
|
});
|
|
|
|
it('should preserve legacy listeningGroups semantics by adding wildcard open', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot' },
|
|
channels: {
|
|
discord: {
|
|
enabled: true,
|
|
token: 'discord-token',
|
|
listeningGroups: ['1234567890'],
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
const groups = agents[0].channels.discord?.groups;
|
|
|
|
expect(groups?.['*']?.mode).toBe('open');
|
|
expect(groups?.['1234567890']?.mode).toBe('listen');
|
|
});
|
|
|
|
describe('env var fallback (container deploys)', () => {
|
|
it('should pick up channels from env vars when YAML has none', () => {
|
|
process.env.TELEGRAM_BOT_TOKEN = 'env-telegram-token';
|
|
process.env.DISCORD_BOT_TOKEN = 'env-discord-token';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels.telegram?.token).toBe('env-telegram-token');
|
|
expect(agents[0].channels.discord?.token).toBe('env-discord-token');
|
|
});
|
|
|
|
it('should not override YAML channels with env vars', () => {
|
|
process.env.TELEGRAM_BOT_TOKEN = 'env-token';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {
|
|
telegram: { enabled: true, token: 'yaml-token' },
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels.telegram?.token).toBe('yaml-token');
|
|
});
|
|
|
|
it('should merge env var credential into YAML block missing it', () => {
|
|
process.env.SIGNAL_PHONE_NUMBER = '+15551234567';
|
|
process.env.DISCORD_BOT_TOKEN = 'env-discord-token';
|
|
process.env.TELEGRAM_BOT_TOKEN = 'env-tg-token';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {
|
|
signal: { enabled: true, selfChat: true, dmPolicy: 'pairing' },
|
|
discord: { enabled: true, dmPolicy: 'open' },
|
|
telegram: { enabled: true, dmPolicy: 'pairing' },
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
// Env var should fill in the missing credential
|
|
expect(agents[0].channels.signal?.phone).toBe('+15551234567');
|
|
expect(agents[0].channels.signal?.dmPolicy).toBe('pairing');
|
|
expect(agents[0].channels.discord?.token).toBe('env-discord-token');
|
|
expect(agents[0].channels.discord?.dmPolicy).toBe('open');
|
|
expect(agents[0].channels.telegram?.token).toBe('env-tg-token');
|
|
});
|
|
|
|
it('should merge env var credential into YAML block missing it', () => {
|
|
process.env.SIGNAL_PHONE_NUMBER = '+15551234567';
|
|
process.env.DISCORD_BOT_TOKEN = 'env-discord-token';
|
|
process.env.TELEGRAM_BOT_TOKEN = 'env-tg-token';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {
|
|
signal: { enabled: true, selfChat: true, dmPolicy: 'pairing' },
|
|
discord: { enabled: true, dmPolicy: 'open' },
|
|
telegram: { enabled: true, dmPolicy: 'pairing' },
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
// Env var should fill in the missing credential
|
|
expect(agents[0].channels.signal?.phone).toBe('+15551234567');
|
|
expect(agents[0].channels.signal?.dmPolicy).toBe('pairing');
|
|
expect(agents[0].channels.discord?.token).toBe('env-discord-token');
|
|
expect(agents[0].channels.discord?.dmPolicy).toBe('open');
|
|
expect(agents[0].channels.telegram?.token).toBe('env-tg-token');
|
|
});
|
|
|
|
it('should not apply env vars in multi-agent mode', () => {
|
|
process.env.TELEGRAM_BOT_TOKEN = 'env-token';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agents: [{ name: 'Bot1', channels: {} }],
|
|
agent: { name: 'Unused', model: 'unused' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels.telegram).toBeUndefined();
|
|
});
|
|
|
|
it('should pick up heartbeat from env vars when YAML features is empty', () => {
|
|
process.env.HEARTBEAT_ENABLED = 'true';
|
|
process.env.HEARTBEAT_INTERVAL_MIN = '15';
|
|
process.env.HEARTBEAT_SKIP_RECENT_USER_MIN = '5';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].features?.heartbeat).toEqual({
|
|
enabled: true,
|
|
intervalMin: 15,
|
|
skipRecentUserMin: 5,
|
|
});
|
|
});
|
|
|
|
it('should pick up cron from env vars when YAML features is empty', () => {
|
|
process.env.CRON_ENABLED = 'true';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].features?.cron).toBe(true);
|
|
});
|
|
|
|
it('should merge env var heartbeat into existing YAML features', () => {
|
|
process.env.HEARTBEAT_ENABLED = 'true';
|
|
process.env.HEARTBEAT_INTERVAL_MIN = '20';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
features: {
|
|
cron: true,
|
|
maxToolCalls: 50,
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
// Env var heartbeat should merge in
|
|
expect(agents[0].features?.heartbeat).toEqual({
|
|
enabled: true,
|
|
intervalMin: 20,
|
|
});
|
|
// Existing YAML features should be preserved
|
|
expect(agents[0].features?.cron).toBe(true);
|
|
expect(agents[0].features?.maxToolCalls).toBe(50);
|
|
});
|
|
|
|
it('should not override YAML heartbeat with env vars', () => {
|
|
process.env.HEARTBEAT_ENABLED = 'true';
|
|
process.env.HEARTBEAT_INTERVAL_MIN = '99';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
features: {
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalMin: 10,
|
|
skipRecentUserMin: 3,
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
// YAML values should win
|
|
expect(agents[0].features?.heartbeat?.intervalMin).toBe(10);
|
|
expect(agents[0].features?.heartbeat?.skipRecentUserMin).toBe(3);
|
|
});
|
|
|
|
it('should handle heartbeat env var with defaults when interval not set', () => {
|
|
process.env.HEARTBEAT_ENABLED = 'true';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].features?.heartbeat).toEqual({ enabled: true });
|
|
});
|
|
|
|
it('should not override YAML cron: false with env var', () => {
|
|
process.env.CRON_ENABLED = 'true';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
features: {
|
|
cron: false,
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].features?.cron).toBe(false);
|
|
});
|
|
|
|
it('should not enable heartbeat when env var is not true', () => {
|
|
process.env.HEARTBEAT_ENABLED = 'false';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].features?.heartbeat).toBeUndefined();
|
|
});
|
|
|
|
it('should pick up all channel types from env vars', () => {
|
|
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
|
|
process.env.SLACK_BOT_TOKEN = 'slack-bot';
|
|
process.env.SLACK_APP_TOKEN = 'slack-app';
|
|
process.env.WHATSAPP_ENABLED = 'true';
|
|
process.env.SIGNAL_PHONE_NUMBER = '+1234567890';
|
|
process.env.DISCORD_BOT_TOKEN = 'discord-token';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels.telegram?.token).toBe('tg-token');
|
|
expect(agents[0].channels.slack?.botToken).toBe('slack-bot');
|
|
expect(agents[0].channels.slack?.appToken).toBe('slack-app');
|
|
expect(agents[0].channels.whatsapp?.enabled).toBe(true);
|
|
expect(agents[0].channels.signal?.phone).toBe('+1234567890');
|
|
expect(agents[0].channels.discord?.token).toBe('discord-token');
|
|
});
|
|
|
|
it('should pick up allowedUsers from env vars for all channels', () => {
|
|
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
|
|
process.env.TELEGRAM_DM_POLICY = 'allowlist';
|
|
process.env.TELEGRAM_ALLOWED_USERS = '515978553, 123456';
|
|
|
|
process.env.SLACK_BOT_TOKEN = 'slack-bot';
|
|
process.env.SLACK_APP_TOKEN = 'slack-app';
|
|
process.env.SLACK_DM_POLICY = 'allowlist';
|
|
process.env.SLACK_ALLOWED_USERS = 'U123,U456';
|
|
|
|
process.env.DISCORD_BOT_TOKEN = 'discord-token';
|
|
process.env.DISCORD_DM_POLICY = 'allowlist';
|
|
process.env.DISCORD_ALLOWED_USERS = '999888777';
|
|
|
|
process.env.WHATSAPP_ENABLED = 'true';
|
|
process.env.WHATSAPP_DM_POLICY = 'allowlist';
|
|
process.env.WHATSAPP_ALLOWED_USERS = '+1234567890,+0987654321';
|
|
|
|
process.env.SIGNAL_PHONE_NUMBER = '+1555000000';
|
|
process.env.SIGNAL_DM_POLICY = 'allowlist';
|
|
process.env.SIGNAL_ALLOWED_USERS = '+1555111111';
|
|
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].channels.telegram?.dmPolicy).toBe('allowlist');
|
|
expect(agents[0].channels.telegram?.allowedUsers).toEqual(['515978553', '123456']);
|
|
|
|
expect(agents[0].channels.slack?.dmPolicy).toBe('allowlist');
|
|
expect(agents[0].channels.slack?.allowedUsers).toEqual(['U123', 'U456']);
|
|
|
|
expect(agents[0].channels.discord?.dmPolicy).toBe('allowlist');
|
|
expect(agents[0].channels.discord?.allowedUsers).toEqual(['999888777']);
|
|
|
|
expect(agents[0].channels.whatsapp?.dmPolicy).toBe('allowlist');
|
|
expect(agents[0].channels.whatsapp?.allowedUsers).toEqual(['+1234567890', '+0987654321']);
|
|
|
|
expect(agents[0].channels.signal?.dmPolicy).toBe('allowlist');
|
|
expect(agents[0].channels.signal?.allowedUsers).toEqual(['+1555111111']);
|
|
});
|
|
});
|
|
|
|
it('should preserve features, polling, and integrations', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot', model: 'test' },
|
|
channels: {},
|
|
features: {
|
|
cron: true,
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalMin: 10,
|
|
skipRecentUserMin: 3,
|
|
},
|
|
maxToolCalls: 50,
|
|
},
|
|
polling: {
|
|
enabled: true,
|
|
intervalMs: 30000,
|
|
},
|
|
integrations: {
|
|
google: {
|
|
enabled: true,
|
|
account: 'test@example.com',
|
|
},
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].features).toEqual(config.features);
|
|
expect(agents[0].polling).toEqual(config.polling);
|
|
expect(agents[0].integrations).toEqual(config.integrations);
|
|
});
|
|
|
|
it('should pass through displayName', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: {
|
|
name: 'Signo',
|
|
displayName: '💜 Signo',
|
|
},
|
|
channels: {
|
|
telegram: { enabled: true, token: 'test-token' },
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].displayName).toBe('💜 Signo');
|
|
});
|
|
|
|
it('should pass through displayName in multi-agent config', () => {
|
|
const agentsArray: AgentConfig[] = [
|
|
{
|
|
name: 'Signo',
|
|
displayName: '💜 Signo',
|
|
channels: { telegram: { enabled: true, token: 't1' } },
|
|
},
|
|
{
|
|
name: 'DevOps',
|
|
displayName: '👾 DevOps',
|
|
channels: { discord: { enabled: true, token: 'd1' } },
|
|
},
|
|
];
|
|
|
|
const config = {
|
|
server: { mode: 'cloud' as const },
|
|
agents: agentsArray,
|
|
} as LettaBotConfig;
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].displayName).toBe('💜 Signo');
|
|
expect(agents[1].displayName).toBe('👾 DevOps');
|
|
});
|
|
|
|
it('should pass through conversations config in legacy mode', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot' },
|
|
channels: {},
|
|
conversations: {
|
|
mode: 'per-channel',
|
|
heartbeat: 'dedicated',
|
|
},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].conversations?.mode).toBe('per-channel');
|
|
expect(agents[0].conversations?.heartbeat).toBe('dedicated');
|
|
});
|
|
|
|
it('should pass through conversations as undefined when not set', () => {
|
|
const config: LettaBotConfig = {
|
|
server: { mode: 'cloud' },
|
|
agent: { name: 'TestBot' },
|
|
channels: {},
|
|
};
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents[0].conversations).toBeUndefined();
|
|
});
|
|
|
|
it('should normalize onboarding-generated agents[] config (no legacy agent/channels)', () => {
|
|
// This matches the shape that onboarding now writes: agents[] at top level,
|
|
// with no legacy agent/channels/features fields.
|
|
const config = {
|
|
server: { mode: 'cloud' as const },
|
|
agents: [{
|
|
name: 'LettaBot',
|
|
id: 'agent-abc123',
|
|
channels: {
|
|
telegram: { enabled: true, token: 'tg-token', dmPolicy: 'pairing' as const },
|
|
whatsapp: { enabled: true, selfChat: true },
|
|
},
|
|
features: {
|
|
cron: true,
|
|
heartbeat: { enabled: true, intervalMin: 30 },
|
|
},
|
|
}],
|
|
// loadConfig() merges defaults for agent/channels, so they'll exist at runtime
|
|
agent: { name: 'LettaBot' },
|
|
channels: {},
|
|
} as LettaBotConfig;
|
|
|
|
const agents = normalizeAgents(config);
|
|
|
|
expect(agents).toHaveLength(1);
|
|
expect(agents[0].name).toBe('LettaBot');
|
|
expect(agents[0].id).toBe('agent-abc123');
|
|
expect(agents[0].channels.telegram?.token).toBe('tg-token');
|
|
expect(agents[0].channels.whatsapp?.enabled).toBe(true);
|
|
expect(agents[0].features?.cron).toBe(true);
|
|
expect(agents[0].features?.heartbeat?.intervalMin).toBe(30);
|
|
});
|
|
});
|