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,205 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { FastifyInstance } from 'fastify';
import { detectInstalledTools, runExternalTool } from '@waggle/agent';
import type { ToolManifest } from '@waggle/shared';
import { buildLocalServer } from '../../packages/server/src/local/index.js';
import { injectWithAuth } from '../../packages/server/tests/test-utils.js';
const LIVE = process.env.WAGGLE_LIVE_EXTERNAL_AGENTS === '1';
const describeLive = LIVE ? describe : describe.skip;
async function waitFor(
predicate: () => boolean,
timeoutMs: number,
message: string,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return;
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(message);
}
describeLive('live external-agent collaboration', () => {
let server: FastifyInstance;
let dataDir: string;
let sourceWorkspaceDir: string;
let sourceWorkspaceId: string;
let synthesisWorkspaceDir: string;
let synthesisWorkspaceId: string;
let openClawBinary: string | undefined;
beforeAll(async () => {
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-live-collab-'));
sourceWorkspaceDir = path.join(dataDir, 'source-workspace');
synthesisWorkspaceDir = path.join(dataDir, 'synthesis-workspace');
fs.mkdirSync(sourceWorkspaceDir, { recursive: true });
fs.mkdirSync(synthesisWorkspaceDir, { recursive: true });
fs.writeFileSync(
path.join(sourceWorkspaceDir, 'FACTS.md'),
'# Collaboration fixture\n\nAlpha code: HONEY-17\nBeta code: WAGGLE-42\n',
'utf8',
);
server = await buildLocalServer({ dataDir });
const sourceWorkspace = server.workspaceManager.create({
name: 'Live Collaboration Source',
group: 'live-test',
directory: sourceWorkspaceDir,
});
const synthesisWorkspace = server.workspaceManager.create({
name: 'Live Collaboration Synthesis',
group: 'live-test',
directory: synthesisWorkspaceDir,
});
sourceWorkspaceId = sourceWorkspace.id;
synthesisWorkspaceId = synthesisWorkspace.id;
await server.listen({ host: '127.0.0.1', port: 0 });
}, 120_000);
afterAll(async () => {
let cleanupError: unknown;
if (openClawBinary && synthesisWorkspaceDir && synthesisWorkspaceId && fs.existsSync(synthesisWorkspaceDir)) {
try {
const agentId = openClawAgentId(synthesisWorkspaceId, fs.realpathSync(synthesisWorkspaceDir));
const cleanupManifest: ToolManifest = {
id: 'openclaw-cleanup', displayName: 'OpenClaw cleanup', launchable: false,
hookCapable: false, hookPointer: '.openclaw/test-cleanup',
detect: { kind: 'path', binaryName: 'openclaw' },
capabilities: { interactiveLaunch: false, headlessTask: true, structuredProgress: true, resumable: false, liveWaggleDance: false },
task: {
argvTemplate: ['agents', 'delete', agentId, '--force', '--json'],
accessArgs: { native: [] }, promptTransport: 'stdin', outputDialect: 'json',
workspaceBinding: 'cwd', permissionModes: ['native'], resumable: false,
},
};
const cleanup = await runExternalTool({
manifest: cleanupManifest, binary: openClawBinary,
workspaceId: synthesisWorkspaceId, workspacePath: synthesisWorkspaceDir,
runId: 'live-cleanup', roomId: 'live-cleanup', prompt: '', access: 'native', timeoutMs: 30_000,
});
if (cleanup.status !== 'completed') {
cleanupError = new Error(`OpenClaw live-test cleanup failed: ${cleanup.stderrTail || cleanup.summary}`);
}
} catch (err) {
cleanupError = err;
}
}
if (server) await server.close();
if (dataDir) {
let lastError: unknown;
let removed = false;
for (let attempt = 0; attempt < 10; attempt++) {
try {
fs.rmSync(dataDir, { recursive: true, force: true });
removed = true;
break;
} catch (err) {
lastError = err;
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
if (!removed) throw lastError;
}
if (cleanupError) throw cleanupError;
}, 30_000);
it('delivers an asymmetric Hermes finding to OpenClaw through WaggleDance', async (context) => {
const detection = await detectInstalledTools();
const required = ['hermes', 'openclaw'];
const unavailable = required.filter((id) => !detection.tools.some(
(tool) => tool.id === id && tool.installed && tool.installedPath,
));
if (unavailable.length > 0) {
context.skip(`Missing live tools: ${unavailable.join(', ')}`);
return;
}
openClawBinary = detection.tools.find((tool) => tool.id === 'openclaw')?.installedPath ?? undefined;
expect(server.workspaceManager.get(sourceWorkspaceId)?.directory).toBe(sourceWorkspaceDir);
expect(server.workspaceManager.get(synthesisWorkspaceId)?.directory).toBe(synthesisWorkspaceDir);
const response = await injectWithAuth(server, {
method: 'POST',
url: '/api/tools/run',
payload: {
prompt: [
'This is a read-only acceptance test. Do not modify any file.',
'Look only in the assigned workspace for FACTS.md.',
'If it exists, return one line: ALPHA=<alpha code> BETA=<beta code>.',
'If it does not exist, return exactly NO_LOCAL_FACTS and never guess the codes.',
'If peer findings are supplied in a later collaboration round, use those findings as the source of truth.',
].join(' '),
participants: [
{ toolId: 'hermes', workspaceIds: [sourceWorkspaceId], access: 'native' },
{ toolId: 'openclaw', workspaceIds: [synthesisWorkspaceId], access: 'native' },
],
timeoutMs: 180_000,
},
});
expect(response.statusCode).toBe(202);
const body = response.json() as {
roomId: string;
runs: Array<{ runId: string; toolId: string; workspaceId: string }>;
};
expect(body.runs).toHaveLength(3);
await waitFor(
() => ['completed', 'failed', 'cancelled', 'interrupted'].includes(
server.agentRunRegistry.get(body.roomId)?.status ?? '',
),
210_000,
'live external-agent Room did not settle',
);
const room = server.agentRunRegistry.get(body.roomId);
const runs = body.runs.map(({ runId }) => server.agentRunRegistry.get(runId)!);
const diagnostic = JSON.stringify({
room: { status: room?.status, result: room?.result },
runs: runs.map((run) => ({
id: run.id, tool: run.executor.toolId, status: run.status,
result: run.result, progress: run.progress, memoryRefs: run.memoryRefs,
})),
});
expect(room?.status, diagnostic).toBe('completed');
for (const run of runs) {
expect(run.status, diagnostic).toBe('completed');
expect(run.memoryRefs.status, diagnostic).toBe('complete');
expect(run.memoryRefs.personalFrameIds?.length).toBeGreaterThan(0);
expect(run.kind).toBe('worker');
if (run.kind === 'worker') {
expect(run.memoryRefs.workspaceFrameIds?.[run.workspaceId]?.length).toBeGreaterThan(0);
}
}
const hermes = runs.find((run) => run.executor.toolId === 'hermes');
const openClawFirstWave = runs.find((run) => run.executor.toolId === 'openclaw'
&& !run.title.startsWith('WaggleDance synthesis'));
const synthesis = runs.find((run) => run.title.startsWith('WaggleDance synthesis'));
expect(hermes?.result?.summary, diagnostic).toContain('ALPHA=HONEY-17 BETA=WAGGLE-42');
expect(openClawFirstWave?.result?.summary, diagnostic).toContain('NO_LOCAL_FACTS');
expect(openClawFirstWave?.result?.summary, diagnostic).not.toContain('HONEY-17');
expect(synthesis?.kind).toBe('worker');
if (synthesis?.kind === 'worker') expect(synthesis.workspaceId).toBe(synthesisWorkspaceId);
expect(synthesis?.result?.summary, diagnostic).toContain('ALPHA=HONEY-17 BETA=WAGGLE-42');
const roomSignals = server.signalBus?.query({ teamId: `room::${body.roomId}`, limit: 1_000 }) ?? [];
const subtypes = roomSignals.map((message) => message.subtype);
expect(subtypes).toEqual(expect.arrayContaining(['task_delegation', 'task_claim', 'routed_share', 'knowledge_match']));
const peerDelivery = roomSignals.find((message) => message.subtype === 'knowledge_match');
expect(peerDelivery?.content.peerFindings).toEqual(expect.arrayContaining([
expect.stringContaining('ALPHA=HONEY-17 BETA=WAGGLE-42'),
]));
expect(new Set(roomSignals.map((message) => message.content.runId))).toEqual(
new Set(body.runs.map((run) => run.runId)),
);
}, 360_000);
});
function openClawAgentId(workspaceId: string, cwd: string): string {
const digest = createHash('sha256').update(`${workspaceId}\0${cwd}`).digest('hex').slice(0, 8);
const base = workspaceId.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-|-$/g, '').slice(0, 32) || 'workspace';
return `waggle-${base}-${digest}`;
}

View File

@@ -0,0 +1,256 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { buildServer } from '../../packages/server/src/index.js';
import {
users, teams, teamMembers, tasks, messages,
teamEntities, teamResources, agentAuditLog,
} from '../../packages/server/src/db/schema.js';
import { sql } from 'drizzle-orm';
describe('M3 Full Stack Integration', () => {
let server: Awaited<ReturnType<typeof buildServer>>;
let ownerId: string;
let memberId: string;
let teamId: string;
const teamSlug = 'integ-team';
beforeAll(async () => {
server = await buildServer();
// Clean up leftover test data from previous runs (reverse dependency order)
await server.db.execute(sql`DELETE FROM agent_audit_log WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'integ_%')`);
await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_relations WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_entities WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM messages WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM tasks WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM agent_jobs WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM cron_schedules WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM teams WHERE slug = 'integ-team'`);
await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'integ_%'`);
// Override auth handler for testing
server._authHandler.fn = async function (request: FastifyRequest, reply: FastifyReply) {
const testUserId = request.headers['x-test-user-id'] as string;
if (!testUserId) {
return reply.code(401).send({ error: 'Missing x-test-user-id header' });
}
request.userId = testUserId;
request.clerkId = 'test';
};
});
afterAll(async () => {
// Clean up all test data
await server.db.execute(sql`DELETE FROM agent_audit_log WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'integ_%')`);
await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_relations WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_entities WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM messages WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM tasks WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM agent_jobs WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM cron_schedules WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`);
await server.db.execute(sql`DELETE FROM teams WHERE slug = 'integ-team'`);
await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'integ_%'`);
await server.close();
});
it('Step 1: Creates users via webhook', async () => {
// Create owner
let res = await server.inject({
method: 'POST',
url: '/api/webhooks/clerk',
payload: {
type: 'user.created',
data: {
id: 'integ_owner',
first_name: 'Owner',
last_name: 'User',
email_addresses: [{ email_address: 'integ_owner@test.com' }],
},
},
});
expect(res.statusCode).toBe(200);
// Create member
res = await server.inject({
method: 'POST',
url: '/api/webhooks/clerk',
payload: {
type: 'user.created',
data: {
id: 'integ_member',
first_name: 'Member',
last_name: 'User',
email_addresses: [{ email_address: 'integ_member@test.com' }],
},
},
});
expect(res.statusCode).toBe(200);
// Get user IDs
const [owner] = await server.db.select().from(users).where(sql`clerk_id = 'integ_owner'`);
const [member] = await server.db.select().from(users).where(sql`clerk_id = 'integ_member'`);
expect(owner).toBeDefined();
expect(member).toBeDefined();
ownerId = owner.id;
memberId = member.id;
});
it('Step 2: Creates team and invites member', async () => {
let res = await server.inject({
method: 'POST',
url: '/api/teams',
headers: { 'x-test-user-id': ownerId },
payload: { name: 'Integration Team', slug: teamSlug },
});
expect(res.statusCode).toBe(201);
teamId = JSON.parse(res.body).id;
// Add member
res = await server.inject({
method: 'POST',
url: `/api/teams/${teamSlug}/members`,
headers: { 'x-test-user-id': ownerId },
payload: { email: 'integ_member@test.com', role: 'member' },
});
expect(res.statusCode).toBe(201);
});
it('Step 3: Creates task on team board', async () => {
const res = await server.inject({
method: 'POST',
url: `/api/teams/${teamSlug}/tasks`,
headers: { 'x-test-user-id': ownerId },
payload: { title: 'Research competitor pricing', priority: 'high' },
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.title).toBe('Research competitor pricing');
expect(body.priority).toBe('high');
expect(body.status).toBe('open');
});
it('Step 4: Sends Waggle Dance message', async () => {
const res = await server.inject({
method: 'POST',
url: `/api/teams/${teamSlug}/messages`,
headers: { 'x-test-user-id': ownerId },
payload: {
type: 'request',
subtype: 'knowledge_check',
content: { topic: 'competitor pricing' },
},
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.type).toBe('request');
expect(body.subtype).toBe('knowledge_check');
});
it('Step 5: Shares knowledge entity to team graph', async () => {
const res = await server.inject({
method: 'POST',
url: `/api/teams/${teamSlug}/entities`,
headers: { 'x-test-user-id': ownerId },
payload: { entityType: 'competitor', name: 'Acme Corp', properties: { pricing: '$99/mo' } },
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.entityType).toBe('competitor');
expect(body.name).toBe('Acme Corp');
});
it('Step 6: Shares team resource', async () => {
const res = await server.inject({
method: 'POST',
url: `/api/teams/${teamSlug}/resources`,
headers: { 'x-test-user-id': ownerId },
payload: {
resourceType: 'model_recipe',
name: 'Fast Research Config',
config: { model: 'claude-haiku-4-5', temperature: 0.3 },
},
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.resourceType).toBe('model_recipe');
expect(body.name).toBe('Fast Research Config');
});
it('Step 7: Creates audit trail entry', async () => {
const { AuditService } = await import('../../packages/server/src/services/audit-service.js');
const auditService = new AuditService(server.db);
const entry = await auditService.log({
userId: ownerId,
teamId,
agentName: 'memory-weaver',
actionType: 'consolidation',
description: 'Consolidated meeting notes',
});
expect(entry.agentName).toBe('memory-weaver');
// Verify via admin API
// First promote owner to admin role (owner already has admin+ perms)
const res = await server.inject({
method: 'GET',
url: `/api/admin/teams/${teamSlug}/audit`,
headers: { 'x-test-user-id': ownerId },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.length).toBeGreaterThanOrEqual(1);
});
it('Step 8: Member cannot access admin audit endpoint', async () => {
const res = await server.inject({
method: 'GET',
url: `/api/admin/teams/${teamSlug}/audit`,
headers: { 'x-test-user-id': memberId },
});
expect(res.statusCode).toBe(403);
});
it('Step 9: Lists tasks, messages, entities for the team', async () => {
// Tasks
const tasksRes = await server.inject({
method: 'GET',
url: `/api/teams/${teamSlug}/tasks`,
headers: { 'x-test-user-id': memberId },
});
expect(tasksRes.statusCode).toBe(200);
expect(JSON.parse(tasksRes.body).length).toBeGreaterThanOrEqual(1);
// Messages
const msgsRes = await server.inject({
method: 'GET',
url: `/api/teams/${teamSlug}/messages`,
headers: { 'x-test-user-id': memberId },
});
expect(msgsRes.statusCode).toBe(200);
expect(JSON.parse(msgsRes.body).length).toBeGreaterThanOrEqual(1);
// Entities
const entRes = await server.inject({
method: 'GET',
url: `/api/teams/${teamSlug}/entities`,
headers: { 'x-test-user-id': memberId },
});
expect(entRes.statusCode).toBe(200);
expect(JSON.parse(entRes.body).length).toBeGreaterThanOrEqual(1);
});
it('Step 10: Health check passes', async () => {
const res = await server.inject({ method: 'GET', url: '/health' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body).status).toBe('ok');
});
});