moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View 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();
});
});
});

View 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 };
}

View 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);
});
});
});