godcrm/backend/routes/v3/__tests__/chat-delegation.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

808 lines
33 KiB
JavaScript

/**
* ADR-117: Agent Delegation Syntax Tests
*
* Tests for the <<@agent>> delegation syntax that replaces
* plain @mention parsing in agent responses.
*
* Key behaviors:
* - parseDelegations() extracts <<@slug>> tokens from text
* - parseDelegations() does NOT match plain @mentions
* - delegation_depth prevents runaway chains (max 5)
* - User input @mentions remain unchanged
*/
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
// ─── Import parseDelegations directly from chat.js ──────────────────────────
// We need to test the function in isolation, so we'll import the module
// after setting up mocks.
const {
mockDbRun, mockDbGet, mockDbAll, mockIsPostgres,
mockExecuteAgentToolLoop, mockSaveStepMessage, mockResolveAllowedTools,
mockResolveAgentProvider, mockBuildAgentSystemPrompt,
mockLoadConversationHistory, mockFetchBoundRowContext,
mockDetectProvider, mockGetHistoryLimit,
mockLogAgentActivity, mockLogMessageSent, mockLogAgentMentioned,
mockLogToolUsed, mockLogAgentError, mockLogTaskCompleted,
mockIsMessageRelevantToAgent, mockResolveAgentUserService,
mockPaginateByBubbles,
} = vi.hoisted(() => {
const mockDbRun = vi.fn();
const mockDbGet = vi.fn();
const mockDbAll = vi.fn();
const mockIsPostgres = vi.fn(() => false);
const mockExecuteAgentToolLoop = vi.fn();
const mockSaveStepMessage = vi.fn();
const mockResolveAllowedTools = vi.fn();
const mockResolveAgentProvider = vi.fn();
const mockBuildAgentSystemPrompt = vi.fn();
const mockLoadConversationHistory = vi.fn();
const mockFetchBoundRowContext = vi.fn();
const mockDetectProvider = vi.fn();
const mockGetHistoryLimit = vi.fn();
const mockLogAgentActivity = vi.fn();
const mockLogMessageSent = vi.fn();
const mockLogAgentMentioned = vi.fn();
const mockLogToolUsed = vi.fn();
const mockLogAgentError = vi.fn();
const mockLogTaskCompleted = vi.fn();
const mockIsMessageRelevantToAgent = vi.fn();
const mockResolveAgentUserService = vi.fn();
const mockPaginateByBubbles = vi.fn();
return {
mockDbRun, mockDbGet, mockDbAll, mockIsPostgres,
mockExecuteAgentToolLoop, mockSaveStepMessage, mockResolveAllowedTools,
mockResolveAgentProvider, mockBuildAgentSystemPrompt,
mockLoadConversationHistory, mockFetchBoundRowContext,
mockDetectProvider, mockGetHistoryLimit,
mockLogAgentActivity, mockLogMessageSent, mockLogAgentMentioned,
mockLogToolUsed, mockLogAgentError, mockLogTaskCompleted,
mockIsMessageRelevantToAgent, mockResolveAgentUserService,
mockPaginateByBubbles,
};
});
// ─── Module mocks (same as chat-agent-loop.test.js) ─────────────────────────
vi.mock('../../../database/connection.js', () => ({
dbRun: (...args) => mockDbRun(...args),
dbGet: (...args) => mockDbGet(...args),
dbAll: (...args) => mockDbAll(...args),
isPostgres: () => mockIsPostgres(),
safeJsonParse: (str, fallback = null) => {
if (!str) return fallback;
if (typeof str !== 'string') return str;
try { return JSON.parse(str); } catch { return fallback; }
},
}));
vi.mock('../../../utils/logger.js', () => ({
apiLogger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
trace: vi.fn(),
},
}));
vi.mock('../../../utils/response.js', () => ({
success: (res, data, msg) => res.json({ success: true, data, message: msg }),
created: (res, data, msg) => res.status(201).json({ success: true, data, message: msg }),
error: (res, msg, code) => {
const statusCode = (typeof code === 'number' && code >= 100 && code < 600) ? code : 500;
return res.status(statusCode).json({ success: false, message: typeof msg === 'string' ? msg : 'Internal error' });
},
badRequest: (res, msg) => res.status(400).json({ success: false, message: msg }),
notFound: (res, msg) => res.status(404).json({ success: false, message: msg }),
forbidden: (res, msg) => res.status(403).json({ success: false, message: msg }),
unauthorized: (res, msg) => res.status(401).json({ success: false, message: msg }),
}));
vi.mock('../../../utils/bubblePagination.js', () => ({
paginateByBubbles: (...args) => mockPaginateByBubbles(...args),
BUBBLE_PAGE_SIZE: 20,
}));
vi.mock('../../../services/chat/response-mode.js', () => ({
isMessageRelevantToAgent: (...args) => mockIsMessageRelevantToAgent(...args),
}));
vi.mock('../../../services/agent-users.js', () => ({
resolveAgentUser: (...args) => mockResolveAgentUserService(...args),
}));
vi.mock('../../../services/chat/agent-execution-shared.js', () => ({
resolveAgentProvider: (...args) => mockResolveAgentProvider(...args),
buildAgentSystemPrompt: (...args) => mockBuildAgentSystemPrompt(...args),
loadConversationHistory: (...args) => mockLoadConversationHistory(...args),
fetchBoundRowContext: (...args) => mockFetchBoundRowContext(...args),
detectProvider: (...args) => mockDetectProvider(...args),
getHistoryLimit: (...args) => mockGetHistoryLimit(...args),
}));
vi.mock('../../../services/AgentLoopService.js', () => ({
agentLoop: (...args) => mockExecuteAgentToolLoop(...args),
saveStepMessage: (...args) => mockSaveStepMessage(...args),
resolveAllowedTools: (...args) => mockResolveAllowedTools(...args),
}));
vi.mock('../../../services/AgentActivityLogger.js', () => ({
logAgentActivity: (...args) => mockLogAgentActivity(...args),
logMessageSent: (...args) => mockLogMessageSent(...args),
logAgentMentioned: (...args) => mockLogAgentMentioned(...args),
logToolUsed: (...args) => mockLogToolUsed(...args),
logAgentError: (...args) => mockLogAgentError(...args),
logTaskCompleted: (...args) => mockLogTaskCompleted(...args),
}));
vi.mock('../../../services/AgentJobService.js', () => ({
createAndDispatchJob: vi.fn(),
}));
vi.mock('../../../services/ToolApprovalService.js', () => ({
approveToolExecution: vi.fn(),
rejectToolExecution: vi.fn(),
getPendingApprovals: vi.fn(),
getApprovalRules: vi.fn(),
updateApprovalRule: vi.fn(),
}));
vi.mock('../../../services/chatChunkingService.js', () => ({
triggerAutoSummaryIfNeeded: vi.fn(),
buildAIContext: vi.fn(),
parseAutoSummarySettings: vi.fn(),
generateSummaryPrompt: vi.fn(),
searchSimilarSummaries: vi.fn(),
parseVectorSearchSettings: vi.fn(),
}));
vi.mock('../../../services/labs/ai-execution-service.js', () => ({
executeSimpleAI: vi.fn(),
}));
vi.mock('../../../services/ChainHandoffService.js', () => ({
default: {
dispatchTask: vi.fn(),
getChainStatus: vi.fn(),
resolveAgent: vi.fn(),
},
}));
vi.mock('../tickets.js', () => ({
parseStatusDirective: vi.fn(),
STATE_MAP: {},
TRANSITIONS: {},
}));
vi.mock('express', () => ({
Router: vi.fn(() => ({
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
patch: vi.fn(),
use: vi.fn(),
})),
}));
// ═══════════════════════════════════════════════════════════════════════════════
// ADR-117: parseDelegations() Unit Tests
// ═══════════════════════════════════════════════════════════════════════════════
describe('ADR-117: Agent Delegation Syntax', () => {
let parseDelegations;
beforeAll(async () => {
// Import the module to get access to parseDelegations
const chatModule = await import('../chat.js');
parseDelegations = chatModule.parseDelegations;
});
describe('parseDelegations() — extraction', () => {
it('should extract <<@agent-name>> from text', () => {
const result = parseDelegations('Please review this <<@architect>>');
expect(result).toEqual(['architect']);
});
it('should extract multiple delegations', () => {
const result = parseDelegations('<<@architect>> and <<@test-runner>> please');
expect(result).toEqual(['architect', 'test-runner']);
});
it('should handle underscores in agent names', () => {
const result = parseDelegations('Delegating to <<@my_agent_v2>>');
expect(result).toEqual(['my_agent_v2']);
});
it('should be case-insensitive (normalize to lowercase)', () => {
const result = parseDelegations('<<@Architect>>');
expect(result).toEqual(['architect']);
});
it('should handle delegation at start of text', () => {
const result = parseDelegations('<<@developer>> please check this');
expect(result).toEqual(['developer']);
});
it('should handle delegation at end of text', () => {
const result = parseDelegations('Done, passing to <<@test-runner>>');
expect(result).toEqual(['test-runner']);
});
it('should handle delegation on its own line', () => {
const result = parseDelegations('I finished the implementation.\n<<@test-runner>>');
expect(result).toEqual(['test-runner']);
});
});
describe('parseDelegations() — NO false positives', () => {
it('should NOT match plain @mentions', () => {
const result = parseDelegations('Ask @architect about this');
expect(result).toEqual([]);
});
it('should NOT match partial syntax <<@name (missing closing)', () => {
const result = parseDelegations('<<@broken');
expect(result).toEqual([]);
});
it('should NOT match partial syntax @name>> (missing opening)', () => {
const result = parseDelegations('@broken>>');
expect(result).toEqual([]);
});
it('should NOT match single chevron <@name>', () => {
const result = parseDelegations('<@architect>');
expect(result).toEqual([]);
});
it('should handle mixed references and delegations correctly', () => {
const text = 'I discussed with @marketer and now delegating to <<@developer-ralph>>';
const result = parseDelegations(text);
expect(result).toEqual(['developer-ralph']);
});
it('should return empty array for text with only plain @mentions', () => {
const text = '@architect recommends using @developer for the implementation and @test-runner for tests';
const result = parseDelegations(text);
expect(result).toEqual([]);
});
});
describe('parseDelegations() — edge cases', () => {
it('should return empty array for null input', () => {
expect(parseDelegations(null)).toEqual([]);
});
it('should return empty array for undefined input', () => {
expect(parseDelegations(undefined)).toEqual([]);
});
it('should return empty array for empty string', () => {
expect(parseDelegations('')).toEqual([]);
});
it('should return empty array for non-string input', () => {
expect(parseDelegations(42)).toEqual([]);
expect(parseDelegations({})).toEqual([]);
});
it('should return empty array for text with no mentions at all', () => {
expect(parseDelegations('Just a regular message')).toEqual([]);
});
it('should deduplicate when same agent is mentioned twice', () => {
// parseDelegations returns all matches (including dupes) — dedup is caller's job
// But we document the behavior
const result = parseDelegations('<<@architect>> says ok, <<@architect>> confirmed');
expect(result).toEqual(['architect', 'architect']);
});
});
describe('Delegation depth protection', () => {
it('should block delegation when depth > 5 (MAX_DELEGATION_DEPTH)', () => {
// This tests the constant / guard logic
const MAX_DELEGATION_DEPTH = 5;
expect(6 > MAX_DELEGATION_DEPTH).toBe(true);
expect(5 > MAX_DELEGATION_DEPTH).toBe(false);
});
it('should allow delegation at depth 0 through 5', () => {
const MAX_DELEGATION_DEPTH = 5;
for (let depth = 0; depth <= 5; depth++) {
expect(depth > MAX_DELEGATION_DEPTH).toBe(false);
}
});
it('should block delegation at depth 6+', () => {
const MAX_DELEGATION_DEPTH = 5;
for (let depth = 6; depth <= 10; depth++) {
expect(depth > MAX_DELEGATION_DEPTH).toBe(true);
}
});
it('should increment depth on each delegation hop', () => {
const currentDepth = 2;
const nextDepth = currentDepth + 1;
expect(nextDepth).toBe(3);
});
it('should default delegation_depth to 0 when not provided', () => {
const options = {};
const { delegation_depth = 0 } = options;
expect(delegation_depth).toBe(0);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// ADR-077 Task 3: In-memory delegation chain tracking & loop prevention
// ═══════════════════════════════════════════════════════════════════════════
describe('ADR-077 Task 3: Delegation chain tracking', () => {
let getDelegationChain, clearDelegationChain, _activeDelegationChains;
let MAX_DELEGATION_DEPTH_ACTUAL, MAX_MENTIONS_PER_RESPONSE_ACTUAL;
beforeAll(async () => {
const chatModule = await import('../chat.js');
getDelegationChain = chatModule.getDelegationChain;
clearDelegationChain = chatModule.clearDelegationChain;
_activeDelegationChains = chatModule._activeDelegationChains;
MAX_DELEGATION_DEPTH_ACTUAL = chatModule.MAX_DELEGATION_DEPTH;
MAX_MENTIONS_PER_RESPONSE_ACTUAL = chatModule.MAX_MENTIONS_PER_RESPONSE;
});
beforeEach(() => {
// Clean up all chains before each test
for (const [key, val] of _activeDelegationChains.entries()) {
if (val._timer) clearTimeout(val._timer);
}
_activeDelegationChains.clear();
});
describe('Constants', () => {
it('MAX_DELEGATION_DEPTH should be 5', () => {
expect(MAX_DELEGATION_DEPTH_ACTUAL).toBe(5);
});
it('MAX_MENTIONS_PER_RESPONSE should be 3', () => {
expect(MAX_MENTIONS_PER_RESPONSE_ACTUAL).toBe(3);
});
});
describe('getDelegationChain()', () => {
it('should create a new chain for unknown conversationId', () => {
const chain = getDelegationChain(999);
expect(chain).toBeDefined();
expect(chain.depth).toBe(0);
expect(chain.agentIds).toBeInstanceOf(Set);
expect(chain.agentIds.size).toBe(0);
});
it('should return same chain for same conversationId', () => {
const chain1 = getDelegationChain(100);
chain1.depth = 3;
chain1.agentIds.add(42);
const chain2 = getDelegationChain(100);
expect(chain2.depth).toBe(3);
expect(chain2.agentIds.has(42)).toBe(true);
});
it('should return different chains for different conversationIds', () => {
const chain1 = getDelegationChain(100);
chain1.depth = 3;
const chain2 = getDelegationChain(200);
expect(chain2.depth).toBe(0);
});
});
describe('clearDelegationChain()', () => {
it('should remove chain from active map', () => {
getDelegationChain(100);
expect(_activeDelegationChains.has(100)).toBe(true);
clearDelegationChain(100);
expect(_activeDelegationChains.has(100)).toBe(false);
});
it('should be safe to call on non-existent chain', () => {
expect(() => clearDelegationChain(9999)).not.toThrow();
});
});
describe('Circular reference detection logic', () => {
it('should detect when agent A is already in chain (A→B→A)', () => {
const chain = getDelegationChain(100);
chain.agentIds.add(10); // Agent A
chain.agentIds.add(20); // Agent B
// Agent A tries to be delegated again
expect(chain.agentIds.has(10)).toBe(true);
});
it('should detect longer cycles (A→B→C→A)', () => {
const chain = getDelegationChain(100);
chain.agentIds.add(10); // Agent A
chain.agentIds.add(20); // Agent B
chain.agentIds.add(30); // Agent C
// Agent A tries to be delegated again
expect(chain.agentIds.has(10)).toBe(true);
});
it('should allow new agent not in chain', () => {
const chain = getDelegationChain(100);
chain.agentIds.add(10); // Agent A
chain.agentIds.add(20); // Agent B
// Agent C is not yet in chain
expect(chain.agentIds.has(30)).toBe(false);
});
});
describe('Depth guard logic', () => {
it('should allow delegation at depth <= MAX_DELEGATION_DEPTH', () => {
const chain = getDelegationChain(100);
chain.depth = MAX_DELEGATION_DEPTH_ACTUAL;
expect(chain.depth > MAX_DELEGATION_DEPTH_ACTUAL).toBe(false);
});
it('should block delegation when depth exceeds MAX_DELEGATION_DEPTH', () => {
const chain = getDelegationChain(100);
chain.depth = MAX_DELEGATION_DEPTH_ACTUAL + 1;
expect(chain.depth > MAX_DELEGATION_DEPTH_ACTUAL).toBe(true);
});
it('should track incrementing depth through simulated chain', () => {
const chain = getDelegationChain(100);
const agents = [10, 20, 30, 40, 50, 60]; // 6 agents
for (const agentId of agents) {
chain.depth += 1;
chain.agentIds.add(agentId);
if (chain.depth > MAX_DELEGATION_DEPTH_ACTUAL) {
// At depth 6 (agent 60), should be blocked
expect(agentId).toBe(60);
break;
}
}
expect(chain.depth).toBe(6); // Stopped at 6 > 5
expect(chain.agentIds.size).toBe(6);
});
});
describe('MAX_MENTIONS_PER_RESPONSE cap', () => {
it('should cap mentions array to MAX_MENTIONS_PER_RESPONSE', () => {
const mentions = ['agent-a', 'agent-b', 'agent-c', 'agent-d', 'agent-e'];
const capped = mentions.slice(0, MAX_MENTIONS_PER_RESPONSE_ACTUAL);
expect(capped).toHaveLength(3);
expect(capped).toEqual(['agent-a', 'agent-b', 'agent-c']);
});
it('should not cap when mentions are within limit', () => {
const mentions = ['agent-a', 'agent-b'];
const capped = mentions.slice(0, MAX_MENTIONS_PER_RESPONSE_ACTUAL);
expect(capped).toHaveLength(2);
});
});
});
describe('User input parsers — availability (parseMentions exported for display use)', () => {
it('parseMentions is still exported as a function (used for display, not delegation)', async () => {
// ADR-116 Task #4: parseMentions is no longer used for delegation triggers in user messages.
// It remains exported for display/reference use only.
const chatModule = await import('../chat.js');
expect(typeof chatModule.parseMentions).toBe('function');
});
it('parseMentions extracts plain @mentions correctly (display use)', async () => {
const chatModule = await import('../chat.js');
const result = chatModule.parseMentions('@architect help me');
expect(result).toEqual(['architect']);
});
it('parseMentions and parseDelegations are independent', async () => {
const chatModule = await import('../chat.js');
const text = 'Hey @marketer, please delegate to <<@architect>>';
const mentions = chatModule.parseMentions(text);
const delegations = chatModule.parseDelegations(text);
// parseMentions catches all @-prefixed words (display use, not delegation)
expect(mentions).toContain('marketer');
// parseDelegations only catches explicit delegation syntax
expect(delegations).toEqual(['architect']);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// ADR-116 Task #4: User message delegation uses invocation parsers only
// ═══════════════════════════════════════════════════════════════════════════════
describe('ADR-116 Task #4: User message delegation — invocation parsers only', () => {
let parseInvocationMentions, parseInvocationCommands;
let parseReferenceMentions, parseReferenceCommands;
beforeAll(async () => {
const chatModule = await import('../chat.js');
parseInvocationMentions = chatModule.parseInvocationMentions;
parseInvocationCommands = chatModule.parseInvocationCommands;
parseReferenceMentions = chatModule.parseReferenceMentions;
parseReferenceCommands = chatModule.parseReferenceCommands;
});
describe('<<@slug>> triggers delegation; plain @slug does not', () => {
it('<<@architect>> is detected as a delegation trigger', () => {
expect(parseInvocationMentions('<<@architect>> help me')).toEqual(['architect']);
});
it('plain @architect is NOT detected as a delegation trigger', () => {
// ADR-116 Task #4: only <<@slug>> triggers delegation — @slug is reference-only
expect(parseInvocationMentions('@architect help me')).toEqual([]);
});
it('mixed user message — only <<@slug>> triggers, @slug is reference', () => {
const content = '@marketer is mentioned but <<@architect>> is invoked';
expect(parseInvocationMentions(content)).toEqual(['architect']);
expect(parseReferenceMentions(content)).toEqual(['marketer']);
});
});
describe('<</slug>> triggers delegation; plain /slug does not', () => {
it('<</developer-ralph>> is detected as a delegation trigger', () => {
expect(parseInvocationCommands('<</developer-ralph>> fix bug')).toEqual([{ slug: 'developer-ralph', commandIndex: null }]);
});
it('plain /developer-ralph is NOT detected as a delegation trigger', () => {
// ADR-116 Task #4: only <</slug>> triggers delegation — /slug is reference-only
expect(parseInvocationCommands('/developer-ralph fix bug')).toEqual([]);
});
it('mixed user message — only <</slug>> triggers, /slug is reference', () => {
const content = 'Use /marketer for reference and <</developer-ralph>> for code';
expect(parseInvocationCommands(content)).toEqual([{ slug: 'developer-ralph', commandIndex: null }]);
expect(parseReferenceCommands(content)).toEqual(['marketer']);
});
});
describe('combined invocation tokens in a single user message', () => {
it('resolves both <<@slug>> and <</slug>> as delegation triggers', () => {
const content = '<<@architect>> review design, <</test-runner>> run tests';
const mentions = parseInvocationMentions(content);
const commands = parseInvocationCommands(content).map(c => c.slug);
const allDelegations = [...new Set([...mentions, ...commands])];
expect(allDelegations).toEqual(['architect', 'test-runner']);
});
it('plain @slug and /slug produce NO delegation triggers', () => {
const content = '@architect review and /test-runner run tests';
expect(parseInvocationMentions(content)).toEqual([]);
expect(parseInvocationCommands(content)).toEqual([]);
});
it('only structured tokens trigger delegation in a mixed message', () => {
const content = '@marketer and /frontend noted, but <<@architect>> and <</developer-ralph>> must act';
expect(parseInvocationMentions(content)).toEqual(['architect']);
expect(parseInvocationCommands(content)).toEqual([{ slug: 'developer-ralph', commandIndex: null }]);
});
it('deduplicates when same slug appears in both <<@>> and <</>> forms', () => {
const content = '<<@developer-ralph>> and <</developer-ralph>>';
const mentions = parseInvocationMentions(content);
const commands = parseInvocationCommands(content).map(c => c.slug);
const allDelegations = [...new Set([...mentions, ...commands])];
expect(allDelegations).toEqual(['developer-ralph']);
});
});
describe('edge cases', () => {
it('empty content produces no triggers', () => {
expect(parseInvocationMentions('')).toEqual([]);
expect(parseInvocationCommands('')).toEqual([]);
});
it('null content produces no triggers', () => {
expect(parseInvocationMentions(null)).toEqual([]);
expect(parseInvocationCommands(null)).toEqual([]);
});
it('URL paths are not mistaken for slash invocations', () => {
expect(parseInvocationCommands('See https://example.com/api/v3 for docs')).toEqual([]);
});
it('partial tokens do not match (missing closing >>)', () => {
expect(parseInvocationMentions('<<@architect')).toEqual([]);
expect(parseInvocationCommands('<</architect')).toEqual([]);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// ADR-116: Structured Invocation Tokens — Full Parser Suite Tests
// ═══════════════════════════════════════════════════════════════════════════════
describe('ADR-116: Structured Invocation Tokens', () => {
let parseInvocationMentions, parseInvocationCommands;
let parseReferenceMentions, parseReferenceCommands;
beforeAll(async () => {
const chatModule = await import('../chat.js');
parseInvocationMentions = chatModule.parseInvocationMentions;
parseInvocationCommands = chatModule.parseInvocationCommands;
parseReferenceMentions = chatModule.parseReferenceMentions;
parseReferenceCommands = chatModule.parseReferenceCommands;
});
// ── parseInvocationMentions (<<@slug>>) ──────────────────────────────────
describe('parseInvocationMentions()', () => {
it('should parse <<@architect>> as invocation', () => {
expect(parseInvocationMentions('<<@architect>> review this')).toEqual(['architect']);
});
it('should parse multiple <<@a>> <<@b>> tokens', () => {
expect(parseInvocationMentions('<<@architect>> and <<@test-runner>> please'))
.toEqual(['architect', 'test-runner']);
});
it('should NOT parse raw @architect as invocation', () => {
expect(parseInvocationMentions('Ask @architect about this')).toEqual([]);
});
it('should be case-insensitive', () => {
expect(parseInvocationMentions('<<@Architect>>')).toEqual(['architect']);
});
it('should return empty array for null/undefined input', () => {
expect(parseInvocationMentions(null)).toEqual([]);
expect(parseInvocationMentions(undefined)).toEqual([]);
expect(parseInvocationMentions('')).toEqual([]);
});
it('should handle mixed tokens: <<@a>> and @b — only return a', () => {
const text = 'I discussed with @marketer and now delegating to <<@developer-ralph>>';
expect(parseInvocationMentions(text)).toEqual(['developer-ralph']);
});
});
// ── parseInvocationCommands (<</slug>>) ──────────────────────────────────
describe('parseInvocationCommands()', () => {
it('should parse <</developer-ralph>> as invocation', () => {
expect(parseInvocationCommands('<</developer-ralph>> fix bug')).toEqual([{ slug: 'developer-ralph', commandIndex: null }]);
});
it('should parse multiple <</a>> <</b>> tokens', () => {
expect(parseInvocationCommands('<</architect>> and <</test-runner>>'))
.toEqual([{ slug: 'architect', commandIndex: null }, { slug: 'test-runner', commandIndex: null }]);
});
it('should NOT parse raw /developer-ralph as invocation', () => {
expect(parseInvocationCommands('Use /developer-ralph for bugs')).toEqual([]);
});
it('should NOT match URLs like https://example.com/path', () => {
expect(parseInvocationCommands('Visit https://example.com/api/v3')).toEqual([]);
});
it('should be case-insensitive', () => {
expect(parseInvocationCommands('<</Developer-Ralph>>')).toEqual([{ slug: 'developer-ralph', commandIndex: null }]);
});
it('should return empty array for null/undefined input', () => {
expect(parseInvocationCommands(null)).toEqual([]);
expect(parseInvocationCommands(undefined)).toEqual([]);
expect(parseInvocationCommands('')).toEqual([]);
});
it('should handle mixed: <</a>> and /b — only return a', () => {
const text = 'Use /marketer for marketing and <</developer-ralph>> for code';
expect(parseInvocationCommands(text)).toEqual([{ slug: 'developer-ralph', commandIndex: null }]);
});
it('should parse <</slug/N>> with command index', () => {
expect(parseInvocationCommands('<</sysadmin/0>>')).toEqual([{ slug: 'sysadmin', commandIndex: 0 }]);
expect(parseInvocationCommands('<</sysadmin/1>>')).toEqual([{ slug: 'sysadmin', commandIndex: 1 }]);
});
});
// ── parseReferenceMentions (@slug outside <<@...>>) ──────────────────────
describe('parseReferenceMentions()', () => {
it('should parse @architect as reference', () => {
expect(parseReferenceMentions('As @architect noted')).toEqual(['architect']);
});
it('should NOT include slugs from <<@architect>> tokens', () => {
expect(parseReferenceMentions('<<@architect>> reviewed it')).toEqual([]);
});
it('should handle text with both formats', () => {
const text = 'As @marketer said, delegating to <<@architect>>';
expect(parseReferenceMentions(text)).toEqual(['marketer']);
});
it('should return empty for null/undefined', () => {
expect(parseReferenceMentions(null)).toEqual([]);
expect(parseReferenceMentions(undefined)).toEqual([]);
});
it('should return multiple references', () => {
expect(parseReferenceMentions('@architect and @developer discussed')).toEqual(['architect', 'developer']);
});
});
// ── parseReferenceCommands (/slug outside <</...>>) ──────────────────────
describe('parseReferenceCommands()', () => {
it('should parse /developer-ralph as reference', () => {
expect(parseReferenceCommands('Use /developer-ralph for bugs')).toEqual(['developer-ralph']);
});
it('should NOT include slugs from <</developer-ralph>> tokens', () => {
expect(parseReferenceCommands('<</developer-ralph>> fix it')).toEqual([]);
});
it('should NOT match URLs or file paths', () => {
expect(parseReferenceCommands('Visit https://example.com/path')).toEqual([]);
});
it('should handle text with both formats', () => {
const text = 'Use /marketer for marketing and <</developer-ralph>> for code';
expect(parseReferenceCommands(text)).toEqual(['marketer']);
});
it('should return empty for null/undefined', () => {
expect(parseReferenceCommands(null)).toEqual([]);
expect(parseReferenceCommands(undefined)).toEqual([]);
});
});
// ── Integration: Agent response processing ──────────────────────────────
describe('Agent response processing (no false positives)', () => {
it('should NOT trigger delegation for plain @mention in agent response', () => {
const agentResponse = '@architect recommends using microservices';
expect(parseInvocationMentions(agentResponse)).toEqual([]);
expect(parseInvocationCommands(agentResponse)).toEqual([]);
});
it('should trigger delegation ONLY for <<@agent>> in agent response', () => {
const agentResponse = 'Done. <<@test-runner>> please run tests';
expect(parseInvocationMentions(agentResponse)).toEqual(['test-runner']);
});
it('should trigger delegation for <</agent>> in agent response', () => {
const agentResponse = 'Done. <</test-runner>> please run tests';
expect(parseInvocationCommands(agentResponse)).toEqual([{ slug: 'test-runner', commandIndex: null }]);
});
it('should handle mixed <<@a>> and <</b>> in same response', () => {
const agentResponse = '<<@architect>> review design, <</test-runner>> run tests';
const mentions = parseInvocationMentions(agentResponse);
const commands = parseInvocationCommands(agentResponse).map(c => c.slug);
const allDelegations = [...new Set([...mentions, ...commands])];
expect(allDelegations).toEqual(['architect', 'test-runner']);
});
it('should dedupe when same agent appears in both <<@>> and <</>> form', () => {
const agentResponse = '<<@developer-ralph>> and <</developer-ralph>>';
const mentions = parseInvocationMentions(agentResponse);
const commands = parseInvocationCommands(agentResponse).map(c => c.slug);
const allDelegations = [...new Set([...mentions, ...commands])];
expect(allDelegations).toEqual(['developer-ralph']);
});
});
});