This commit is contained in:
247
packages/agent/tests/e2e/connector-swarm-scenarios.test.ts
Normal file
247
packages/agent/tests/e2e/connector-swarm-scenarios.test.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* E2E Connector + Swarm Scenarios — verify connector tool chains and
|
||||
* multi-agent execution patterns with mocked dependencies.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ConnectorRegistry, type AuditLogger } from '../../src/connector-registry.js';
|
||||
import { BaseConnector, type ConnectorAction, type ConnectorResult } from '../../src/connector-sdk.js';
|
||||
import { executeParallel, type ExecutionDeps, type AgentMemberConfig, type AgentResult } from '../../../worker/src/execution/parallel.js';
|
||||
import { executeSequential } from '../../../worker/src/execution/sequential.js';
|
||||
import { executeCoordinator } from '../../../worker/src/execution/coordinator.js';
|
||||
import { AgentMessageBus } from '../../src/agent-message-bus.js';
|
||||
import { createAgentCommsTools } from '../../src/agent-comms-tools.js';
|
||||
import { WorkspaceSessionManager } from '../../../server/src/local/workspace-sessions.js';
|
||||
import type { MindDB, VaultStore } from '@waggle/core';
|
||||
import type { Orchestrator } from '../../src/orchestrator.js';
|
||||
import type { ConnectorHealth } from '@waggle/shared';
|
||||
|
||||
// ── Mock helpers ──────────────────────────────────────────────────────
|
||||
|
||||
class MockConnector extends BaseConnector {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly service: string;
|
||||
readonly authType = 'bearer' as const;
|
||||
readonly substrate = 'waggle' as const;
|
||||
readonly actions: ConnectorAction[];
|
||||
|
||||
constructor(id: string, name: string, service: string, actions: ConnectorAction[]) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = `Mock ${name}`;
|
||||
this.service = service;
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {}
|
||||
async healthCheck(): Promise<ConnectorHealth> {
|
||||
return { id: this.id, name: this.name, status: 'connected', lastChecked: new Date().toISOString() };
|
||||
}
|
||||
async execute(action: string, params: Record<string, unknown>): Promise<ConnectorResult> {
|
||||
return { success: true, data: { action, ...params } };
|
||||
}
|
||||
}
|
||||
|
||||
function createMockVault(connected: string[]): VaultStore {
|
||||
return {
|
||||
getConnectorCredential: vi.fn((id: string) => {
|
||||
if (connected.includes(id)) return { value: 'tok', type: 'bearer', isExpired: false };
|
||||
return null;
|
||||
}),
|
||||
setConnectorCredential: vi.fn(),
|
||||
set: vi.fn(), get: vi.fn(), delete: vi.fn(),
|
||||
list: vi.fn(() => []), has: vi.fn(() => false),
|
||||
migrateFromConfig: vi.fn(() => 0),
|
||||
} as unknown as VaultStore;
|
||||
}
|
||||
|
||||
function createMockDeps(): ExecutionDeps {
|
||||
return {
|
||||
runAgent: vi.fn(async (config) => ({
|
||||
content: `Agent output for: ${config.systemPrompt.slice(0, 40)}`,
|
||||
toolsUsed: ['mock'],
|
||||
usage: { inputTokens: 50, outputTokens: 25 },
|
||||
})),
|
||||
resolveTools: vi.fn((names) => names.map(n => ({
|
||||
name: n, description: `Mock ${n}`, parameters: { type: 'object', properties: {} },
|
||||
execute: async () => 'ok',
|
||||
}))),
|
||||
};
|
||||
}
|
||||
|
||||
const mockMembers: AgentMemberConfig[] = [
|
||||
{ member: { roleInGroup: 'lead', executionOrder: 0 }, agent: { id: 'a1', name: 'coordinator', model: 'claude-sonnet', systemPrompt: 'You are a coordinator.', tools: [] } },
|
||||
{ member: { roleInGroup: 'worker', executionOrder: 1 }, agent: { id: 'a2', name: 'researcher', model: 'claude-haiku', systemPrompt: 'You are a researcher.', tools: ['web_search'] } },
|
||||
{ member: { roleInGroup: 'worker', executionOrder: 2 }, agent: { id: 'a3', name: 'analyst', model: 'claude-haiku', systemPrompt: 'You are an analyst.', tools: ['search_memory'] } },
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Connector Scenarios
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('E2E Connector Scenarios', () => {
|
||||
describe('C1: GitHub integration — create issue from conversation', () => {
|
||||
it('connector generates tool, tool creates issue', async () => {
|
||||
const vault = createMockVault(['github']);
|
||||
const registry = new ConnectorRegistry(vault);
|
||||
registry.register(new MockConnector('github', 'GitHub', 'github.com', [
|
||||
{ name: 'create_issue', description: 'Create issue', inputSchema: { properties: { owner: { type: 'string' }, repo: { type: 'string' }, title: { type: 'string' } } }, riskLevel: 'medium' },
|
||||
]));
|
||||
|
||||
const tools = registry.generateTools();
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0].name).toBe('connector_github_create_issue');
|
||||
|
||||
const result = JSON.parse(await tools[0].execute({ owner: 'user', repo: 'waggle', title: 'Bug: login fails' }));
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.action).toBe('create_issue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C2: Email outreach — send with approval gate awareness', () => {
|
||||
it('email send tool is generated and marked high-risk', async () => {
|
||||
const vault = createMockVault(['email']);
|
||||
const audit = vi.fn();
|
||||
const registry = new ConnectorRegistry(vault, { log: audit });
|
||||
registry.register(new MockConnector('email', 'Email', 'sendgrid.com', [
|
||||
{ name: 'send_email', description: 'Send email', inputSchema: { properties: { to: { type: 'string' }, subject: { type: 'string' } } }, riskLevel: 'high' },
|
||||
]));
|
||||
|
||||
const tools = registry.generateTools();
|
||||
const sendTool = tools.find(t => t.name === 'connector_email_send_email')!;
|
||||
await sendTool.execute({ to: 'user@example.com', subject: 'Follow up' });
|
||||
|
||||
// Verify audit log was called
|
||||
expect(audit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
actionType: 'connector.email.send_email',
|
||||
requiresApproval: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('C3: Multi-connector workflow chain', () => {
|
||||
it('research → email → jira → slack in sequence', async () => {
|
||||
const vault = createMockVault(['github', 'email', 'jira', 'slack']);
|
||||
const registry = new ConnectorRegistry(vault);
|
||||
registry.register(new MockConnector('github', 'GitHub', 'github.com', [
|
||||
{ name: 'search_code', description: 'Search', inputSchema: { properties: {} }, riskLevel: 'low' },
|
||||
]));
|
||||
registry.register(new MockConnector('email', 'Email', 'sendgrid.com', [
|
||||
{ name: 'send_email', description: 'Send', inputSchema: { properties: {} }, riskLevel: 'high' },
|
||||
]));
|
||||
registry.register(new MockConnector('jira', 'Jira', 'atlassian.net', [
|
||||
{ name: 'create_issue', description: 'Create', inputSchema: { properties: {} }, riskLevel: 'medium' },
|
||||
]));
|
||||
registry.register(new MockConnector('slack', 'Slack', 'slack.com', [
|
||||
{ name: 'send_message', description: 'Send', inputSchema: { properties: {} }, riskLevel: 'medium' },
|
||||
]));
|
||||
|
||||
const tools = registry.generateTools();
|
||||
expect(tools).toHaveLength(4);
|
||||
|
||||
// Execute chain
|
||||
for (const tool of tools) {
|
||||
const result = JSON.parse(await tool.execute({}));
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Swarm Scenarios
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('E2E Swarm Scenarios', () => {
|
||||
describe('SW1: Parallel research — 3 agents investigate independently', () => {
|
||||
it('all agents execute in parallel and produce independent results', async () => {
|
||||
const deps = createMockDeps();
|
||||
const workers = mockMembers.filter(m => m.member.roleInGroup === 'worker');
|
||||
|
||||
const result = await executeParallel(workers, { task: 'Research market trends' }, deps);
|
||||
|
||||
expect(result.strategy).toBe('parallel');
|
||||
expect(result.agentCount).toBe(2);
|
||||
expect(deps.runAgent).toHaveBeenCalledTimes(2);
|
||||
expect((result.results as AgentResult[]).every((r) => r.output.length > 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SW2: Sequential pipeline — researcher → analyst', () => {
|
||||
it('second agent receives first agents output as context', async () => {
|
||||
const deps = createMockDeps();
|
||||
const workers = mockMembers.filter(m => m.member.roleInGroup === 'worker');
|
||||
|
||||
const result = await executeSequential(workers, { task: 'Research then analyze' }, deps);
|
||||
|
||||
expect(result.strategy).toBe('sequential');
|
||||
expect(deps.runAgent).toHaveBeenCalledTimes(2);
|
||||
// Second call should have "Previous Agent" in system prompt
|
||||
const calls = vi.mocked(deps.runAgent).mock.calls;
|
||||
expect(calls[1][0].systemPrompt).toContain('Previous Agent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SW3: Coordinator — plan → execute → synthesize', () => {
|
||||
it('3-phase coordinator pattern with 2 workers', async () => {
|
||||
const deps = createMockDeps();
|
||||
const result = await executeCoordinator(mockMembers, { task: 'Complex analysis' }, deps);
|
||||
|
||||
expect(result.strategy).toBe('coordinator');
|
||||
expect(result.leadAgent).toBe('coordinator');
|
||||
expect(result.workerCount).toBe(2);
|
||||
// Lead runs twice (plan + synthesize) + 2 workers = 4
|
||||
expect(deps.runAgent).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SW4: Cross-workspace communication via message bus', () => {
|
||||
it('agent in ws-1 sends message, agent in ws-2 receives it', async () => {
|
||||
const bus = new AgentMessageBus();
|
||||
const ws1Tools = createAgentCommsTools(bus, 'ws-1');
|
||||
const ws2Tools = createAgentCommsTools(bus, 'ws-2');
|
||||
|
||||
// Agent 1 sends
|
||||
const sendTool = ws1Tools.find(t => t.name === 'send_agent_message')!;
|
||||
const sendResult = JSON.parse(await sendTool.execute({ workspace: 'ws-2', message: 'Research findings attached' }));
|
||||
expect(sendResult.success).toBe(true);
|
||||
|
||||
// Agent 2 receives
|
||||
const checkTool = ws2Tools.find(t => t.name === 'check_agent_messages')!;
|
||||
const checkResult = JSON.parse(await checkTool.execute({}));
|
||||
expect(checkResult.count).toBe(1);
|
||||
expect(checkResult.messages[0].content).toBe('Research findings attached');
|
||||
expect(checkResult.messages[0].from).toBe('ws-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SW5: Workspace session lifecycle', () => {
|
||||
it('create, use, pause, resume, kill session', () => {
|
||||
const manager = new WorkspaceSessionManager(3);
|
||||
const mind = { close: vi.fn() } as unknown as MindDB;
|
||||
const orchestrator = { setWorkspaceMind: vi.fn() } as unknown as Orchestrator;
|
||||
const tools = [{ name: 't1', description: '', parameters: {}, execute: async () => '' }];
|
||||
|
||||
// Create
|
||||
const session = manager.create('ws-1', mind, orchestrator, tools, 'researcher');
|
||||
expect(session.status).toBe('active');
|
||||
expect(session.personaId).toBe('researcher');
|
||||
|
||||
// Pause
|
||||
manager.pause('ws-1');
|
||||
expect(session.status).toBe('paused');
|
||||
|
||||
// Resume
|
||||
manager.resume('ws-1');
|
||||
expect(session.status).toBe('active');
|
||||
|
||||
// Kill
|
||||
manager.close('ws-1');
|
||||
expect(manager.size).toBe(0);
|
||||
expect(mind.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
97
packages/agent/tests/e2e/scenario-framework.ts
Normal file
97
packages/agent/tests/e2e/scenario-framework.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* E2E Scenario Framework — setup/execute/verify pattern for testing
|
||||
* complete tool invocation chains with mocked LLM responses.
|
||||
*
|
||||
* Scenarios verify that the right tools are called in the right order
|
||||
* with the right parameters. They do NOT test LLM output quality.
|
||||
*/
|
||||
|
||||
import type { ToolDefinition } from '../../src/tools.js';
|
||||
|
||||
export interface ScenarioStep {
|
||||
/** What the user says */
|
||||
userMessage: string;
|
||||
/** Tools the agent should invoke (in order) */
|
||||
expectedToolCalls: string[];
|
||||
/** Pattern that should appear in the final result */
|
||||
expectedPattern?: RegExp;
|
||||
}
|
||||
|
||||
export interface ScenarioResult {
|
||||
toolsCalled: string[];
|
||||
toolArgs: Record<string, Record<string, unknown>>;
|
||||
outputs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a scenario by simulating tool calls.
|
||||
* Instead of running through LLM, we directly invoke tools in the expected order
|
||||
* and verify the tool chain works end-to-end.
|
||||
*/
|
||||
export async function executeScenario(
|
||||
tools: ToolDefinition[],
|
||||
steps: ScenarioStep[],
|
||||
toolArgs: Record<string, Record<string, unknown>>,
|
||||
): Promise<ScenarioResult> {
|
||||
const toolMap = new Map(tools.map(t => [t.name, t]));
|
||||
const called: string[] = [];
|
||||
const outputs: string[] = [];
|
||||
const capturedArgs: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for (const step of steps) {
|
||||
for (const toolName of step.expectedToolCalls) {
|
||||
const tool = toolMap.get(toolName);
|
||||
if (!tool) {
|
||||
outputs.push(`[ERROR] Tool not found: ${toolName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const args = toolArgs[toolName] ?? {};
|
||||
capturedArgs[toolName] = args;
|
||||
called.push(toolName);
|
||||
|
||||
try {
|
||||
const result = await tool.execute(args);
|
||||
outputs.push(result);
|
||||
} catch (err: unknown) {
|
||||
outputs.push(`[ERROR] ${toolName}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { toolsCalled: called, toolArgs: capturedArgs, outputs };
|
||||
}
|
||||
|
||||
/** Verify a scenario result matches expectations */
|
||||
export function verifyScenario(
|
||||
result: ScenarioResult,
|
||||
expectedTools: string[],
|
||||
expectedPatterns?: RegExp[],
|
||||
): { passed: boolean; failures: string[] } {
|
||||
const failures: string[] = [];
|
||||
|
||||
// Check all expected tools were called
|
||||
for (const tool of expectedTools) {
|
||||
if (!result.toolsCalled.includes(tool)) {
|
||||
failures.push(`Expected tool "${tool}" was not called`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check patterns in outputs
|
||||
if (expectedPatterns) {
|
||||
const allOutput = result.outputs.join('\n');
|
||||
for (const pattern of expectedPatterns) {
|
||||
if (!pattern.test(allOutput)) {
|
||||
failures.push(`Expected pattern ${pattern} not found in output`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check no errors
|
||||
const errors = result.outputs.filter(o => o.startsWith('[ERROR]'));
|
||||
if (errors.length > 0) {
|
||||
failures.push(...errors);
|
||||
}
|
||||
|
||||
return { passed: failures.length === 0, failures };
|
||||
}
|
||||
204
packages/agent/tests/e2e/solo-scenarios.test.ts
Normal file
204
packages/agent/tests/e2e/solo-scenarios.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* E2E Solo Scenarios — verify tool chain invocations for solo workflows.
|
||||
* All tests use direct tool execution (no LLM needed).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { executeScenario, verifyScenario } from './scenario-framework.js';
|
||||
import type { ToolDefinition } from '../../src/tools.js';
|
||||
import { ConnectorRegistry } from '../../src/connector-registry.js';
|
||||
import { CapabilityRouter } from '../../src/capability-router.js';
|
||||
import { composePersonaPrompt, getPersona, PERSONAS } from '../../src/personas.js';
|
||||
import { AgentMessageBus } from '../../src/agent-message-bus.js';
|
||||
import { needsConfirmation, getApprovalClass } from '../../src/confirmation.js';
|
||||
|
||||
// ── Mock tool factory ─────────────────────────────────────────────────
|
||||
|
||||
function mockTool(name: string, response: string = 'ok'): ToolDefinition {
|
||||
return {
|
||||
name,
|
||||
description: `Mock ${name}`,
|
||||
parameters: { type: 'object', properties: {} },
|
||||
execute: vi.fn(async () => response),
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Solo Scenarios
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('E2E Solo Scenarios', () => {
|
||||
describe('S1: Research report — web search → save → generate doc', () => {
|
||||
it('tool chain: web_search → web_fetch → save_memory → generate_docx', async () => {
|
||||
const tools = [
|
||||
mockTool('web_search', JSON.stringify({ results: [{ title: 'AI Trends', url: 'https://example.com' }] })),
|
||||
mockTool('web_fetch', '<html>AI is transforming industries...</html>'),
|
||||
mockTool('save_memory', 'Memory saved: AI research findings'),
|
||||
mockTool('generate_docx', 'Document generated: research-report.docx'),
|
||||
];
|
||||
|
||||
const result = await executeScenario(tools, [
|
||||
{ userMessage: 'Research AI trends and create a report', expectedToolCalls: ['web_search', 'web_fetch', 'save_memory', 'generate_docx'] },
|
||||
], {
|
||||
web_search: { query: 'AI trends 2026' },
|
||||
web_fetch: { url: 'https://example.com' },
|
||||
save_memory: { content: 'AI research findings', tags: ['research'] },
|
||||
generate_docx: { title: 'AI Trends Report', content: 'Analysis...' },
|
||||
});
|
||||
|
||||
const verification = verifyScenario(result, ['web_search', 'web_fetch', 'save_memory', 'generate_docx']);
|
||||
expect(verification.passed).toBe(true);
|
||||
expect(result.toolsCalled).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S2: Code review — read → search → edit', () => {
|
||||
it('tool chain: read_file → search_content → edit_file', async () => {
|
||||
const tools = [
|
||||
mockTool('read_file', 'function processData(data) { return data; }'),
|
||||
mockTool('search_content', JSON.stringify({ matches: [{ file: 'src/utils.ts', line: 42 }] })),
|
||||
mockTool('edit_file', 'File edited successfully'),
|
||||
];
|
||||
|
||||
const result = await executeScenario(tools, [
|
||||
{ userMessage: 'Review this code for issues', expectedToolCalls: ['read_file', 'search_content', 'edit_file'] },
|
||||
], {
|
||||
read_file: { path: 'src/utils.ts' },
|
||||
search_content: { pattern: 'processData', path: '.' },
|
||||
edit_file: { path: 'src/utils.ts', old_string: 'return data', new_string: 'return validateData(data)' },
|
||||
});
|
||||
|
||||
expect(result.toolsCalled).toEqual(['read_file', 'search_content', 'edit_file']);
|
||||
expect(result.outputs).toHaveLength(3);
|
||||
expect(result.outputs.every(o => !o.startsWith('[ERROR]'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S3: Project planning — create → add steps → execute', () => {
|
||||
it('tool chain: create_plan → add_plan_step → show_plan', async () => {
|
||||
const tools = [
|
||||
mockTool('create_plan', 'Plan created: Project Alpha'),
|
||||
mockTool('add_plan_step', 'Step added: Set up database'),
|
||||
mockTool('show_plan', 'Plan: 1. Set up database [pending]'),
|
||||
];
|
||||
|
||||
const result = await executeScenario(tools, [
|
||||
{ userMessage: 'Create a project plan', expectedToolCalls: ['create_plan', 'add_plan_step', 'show_plan'] },
|
||||
], {
|
||||
create_plan: { name: 'Project Alpha', description: 'New feature development' },
|
||||
add_plan_step: { title: 'Set up database', description: 'Create schema and migrations' },
|
||||
show_plan: {},
|
||||
});
|
||||
|
||||
expect(result.toolsCalled).toHaveLength(3);
|
||||
const verification = verifyScenario(result, ['create_plan', 'add_plan_step', 'show_plan']);
|
||||
expect(verification.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S4: Memory continuity — save → search finds it', () => {
|
||||
it('tool chain: save_memory → search_memory retrieves it', async () => {
|
||||
const savedContent = 'Project decision: use PostgreSQL for the database';
|
||||
const tools = [
|
||||
mockTool('save_memory', `Saved: ${savedContent}`),
|
||||
mockTool('search_memory', JSON.stringify({ results: [{ content: savedContent, score: 0.95 }] })),
|
||||
];
|
||||
|
||||
const result = await executeScenario(tools, [
|
||||
{ userMessage: 'Remember this decision', expectedToolCalls: ['save_memory'] },
|
||||
{ userMessage: 'What was our database decision?', expectedToolCalls: ['search_memory'] },
|
||||
], {
|
||||
save_memory: { content: savedContent },
|
||||
search_memory: { query: 'database decision' },
|
||||
});
|
||||
|
||||
expect(result.toolsCalled).toEqual(['save_memory', 'search_memory']);
|
||||
expect(result.outputs[1]).toContain('PostgreSQL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('S5: Capability discovery — acquire finds marketplace result', () => {
|
||||
it('acquire_capability returns marketplace candidates', async () => {
|
||||
const tools = [
|
||||
mockTool('acquire_capability', JSON.stringify({
|
||||
need: 'email sending',
|
||||
candidates: [{ name: 'sendgrid-skill', source: 'marketplace', availability: 'installable' }],
|
||||
})),
|
||||
];
|
||||
|
||||
const result = await executeScenario(tools, [
|
||||
{ userMessage: 'I need to send emails', expectedToolCalls: ['acquire_capability'] },
|
||||
], {
|
||||
acquire_capability: { need: 'email sending' },
|
||||
});
|
||||
|
||||
const output = JSON.parse(result.outputs[0]);
|
||||
expect(output.candidates).toHaveLength(1);
|
||||
expect(output.candidates[0].source).toBe('marketplace');
|
||||
});
|
||||
});
|
||||
|
||||
describe('S6: Persona composition works correctly', () => {
|
||||
it('all 8 personas compose valid prompts', () => {
|
||||
const corePrompt = 'You are Waggle, a workspace-native AI agent.';
|
||||
for (const persona of PERSONAS) {
|
||||
const composed = composePersonaPrompt(corePrompt, persona);
|
||||
expect(composed).toContain(corePrompt);
|
||||
expect(composed).toContain('Persona:');
|
||||
expect(composed.length).toBeLessThanOrEqual(32000);
|
||||
}
|
||||
});
|
||||
|
||||
it('getPersona returns correct persona by ID', () => {
|
||||
expect(getPersona('researcher')?.name).toBe('Researcher');
|
||||
expect(getPersona('coder')?.name).toBe('Coder');
|
||||
expect(getPersona('nonexistent')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('S7: Confirmation gates cover all Phase 8 tool types', () => {
|
||||
it('connector write tools require confirmation', () => {
|
||||
expect(needsConfirmation('connector_github_create_issue')).toBe(true);
|
||||
expect(needsConfirmation('connector_slack_send_message')).toBe(true);
|
||||
expect(needsConfirmation('connector_email_send_email')).toBe(true);
|
||||
expect(needsConfirmation('connector_jira_update_issue')).toBe(true);
|
||||
});
|
||||
|
||||
it('connector read tools do not require confirmation', () => {
|
||||
expect(needsConfirmation('connector_github_list_repos')).toBe(false);
|
||||
expect(needsConfirmation('connector_slack_list_channels')).toBe(false);
|
||||
});
|
||||
|
||||
it('email actions are critical approval class', () => {
|
||||
expect(getApprovalClass('connector_email_send_email')).toBe('critical');
|
||||
expect(getApprovalClass('connector_email_send_template')).toBe('critical');
|
||||
});
|
||||
|
||||
it('write actions are elevated approval class', () => {
|
||||
expect(getApprovalClass('connector_github_create_issue')).toBe('elevated');
|
||||
expect(getApprovalClass('connector_jira_create_issue')).toBe('elevated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('S8: Capability router resolves connectors', () => {
|
||||
it('routes to connected connectors with high confidence', () => {
|
||||
const router = new CapabilityRouter({
|
||||
toolNames: ['search_memory'],
|
||||
skills: [],
|
||||
plugins: [],
|
||||
mcpServers: [],
|
||||
subAgentRoles: [],
|
||||
connectors: [
|
||||
{ id: 'github', name: 'GitHub', service: 'github.com', connected: true, actions: ['create_issue'] },
|
||||
{ id: 'jira', name: 'Jira', service: 'atlassian.net', connected: false, actions: ['create_issue'] },
|
||||
],
|
||||
});
|
||||
|
||||
const routes = router.resolve('github');
|
||||
const connector = routes.find(r => r.source === 'connector');
|
||||
expect(connector).toBeDefined();
|
||||
expect(connector!.confidence).toBe(0.75);
|
||||
expect(connector!.available).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user