270 lines
9.4 KiB
TypeScript
270 lines
9.4 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import {
|
|
searchCapabilities,
|
|
type SearchCapabilitiesInput,
|
|
type MarketplaceCandidate,
|
|
} from '../src/capability-acquisition.js';
|
|
import { createSkillTools } from '../src/skill-tools.js';
|
|
|
|
describe('capability-marketplace', () => {
|
|
let tmpDir: string;
|
|
let starterDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-mkt-'));
|
|
starterDir = path.join(tmpDir, 'starter-skills');
|
|
fs.mkdirSync(starterDir, { recursive: true });
|
|
|
|
// Create a sample starter skill (for ranking comparison)
|
|
fs.writeFileSync(
|
|
path.join(starterDir, 'research-synthesis.md'),
|
|
'# Research Synthesis\n\nConduct structured research and provide synthesis.',
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// ── Marketplace as acquisition source ─────────────────────────────
|
|
|
|
it('includes marketplace candidates in search results', () => {
|
|
const marketplaceCandidates: MarketplaceCandidate[] = [
|
|
{
|
|
name: 'email-composer',
|
|
description: 'Compose and send emails with templates and personalization',
|
|
packageType: 'skill',
|
|
source: 'marketplace',
|
|
},
|
|
{
|
|
name: 'smtp-sender',
|
|
description: 'Send emails via SMTP with attachment support',
|
|
packageType: 'plugin',
|
|
source: 'marketplace',
|
|
},
|
|
];
|
|
|
|
const result = searchCapabilities({
|
|
need: 'email sending with templates',
|
|
installedSkills: [],
|
|
starterSkillsDir: starterDir,
|
|
marketplaceCandidates,
|
|
});
|
|
|
|
const emailCandidates = result.candidates.filter(c => c.source === 'marketplace');
|
|
expect(emailCandidates.length).toBeGreaterThan(0);
|
|
|
|
const emailComposer = result.candidates.find(c => c.name === 'email-composer');
|
|
expect(emailComposer).toBeDefined();
|
|
expect(emailComposer!.type).toBe('marketplace');
|
|
expect(emailComposer!.availability).toBe('installable');
|
|
expect(emailComposer!.source).toBe('marketplace');
|
|
expect(emailComposer!.installAction).toBe('install_capability');
|
|
});
|
|
|
|
it('gracefully handles empty marketplace results', () => {
|
|
const result = searchCapabilities({
|
|
need: 'research synthesis structured investigation',
|
|
installedSkills: [],
|
|
starterSkillsDir: starterDir,
|
|
marketplaceCandidates: [],
|
|
});
|
|
|
|
// Should still find starter skills (research-synthesis matches)
|
|
expect(result.candidates.length).toBeGreaterThan(0);
|
|
expect(result.candidates.some(c => c.source === 'marketplace')).toBe(false);
|
|
expect(result.candidates.some(c => c.source === 'starter-pack')).toBe(true);
|
|
});
|
|
|
|
it('ranks installed > starter > marketplace (by availability preference)', () => {
|
|
const result = searchCapabilities({
|
|
need: 'research synthesis',
|
|
installedSkills: [
|
|
{ name: 'my-research', content: '# Research Guide\n\nResearch and synthesize findings' },
|
|
],
|
|
starterSkillsDir: starterDir,
|
|
marketplaceCandidates: [
|
|
{
|
|
name: 'research-pro',
|
|
description: 'Professional research synthesis and analysis tools',
|
|
packageType: 'skill',
|
|
source: 'marketplace',
|
|
},
|
|
],
|
|
});
|
|
|
|
// All three sources should appear
|
|
const installed = result.candidates.find(c => c.name === 'my-research');
|
|
const starter = result.candidates.find(c => c.name === 'research-synthesis' && c.source === 'starter-pack');
|
|
const marketplace = result.candidates.find(c => c.name === 'research-pro');
|
|
|
|
expect(installed).toBeDefined();
|
|
expect(starter).toBeDefined();
|
|
expect(marketplace).toBeDefined();
|
|
|
|
// Active skills should rank before installable ones (when scores are close)
|
|
expect(installed!.availability).toBe('active');
|
|
expect(starter!.availability).toBe('installable');
|
|
expect(marketplace!.availability).toBe('installable');
|
|
});
|
|
|
|
it('skips marketplace candidates that are already installed', () => {
|
|
const result = searchCapabilities({
|
|
need: 'research synthesis',
|
|
installedSkills: [
|
|
{ name: 'research-pro', content: '# Research Pro\n\nAdvanced research' },
|
|
],
|
|
starterSkillsDir: starterDir,
|
|
marketplaceCandidates: [
|
|
{
|
|
name: 'research-pro',
|
|
description: 'Professional research synthesis',
|
|
packageType: 'skill',
|
|
source: 'marketplace',
|
|
},
|
|
],
|
|
});
|
|
|
|
// Should not have a duplicate marketplace candidate
|
|
const marketplaceCandidates = result.candidates.filter(c => c.name === 'research-pro' && c.source === 'marketplace');
|
|
expect(marketplaceCandidates.length).toBe(0);
|
|
});
|
|
|
|
it('skips marketplace candidates that duplicate starter pack entries', () => {
|
|
const result = searchCapabilities({
|
|
need: 'research synthesis',
|
|
installedSkills: [],
|
|
starterSkillsDir: starterDir,
|
|
marketplaceCandidates: [
|
|
{
|
|
name: 'research-synthesis',
|
|
description: 'Research synthesis skill',
|
|
packageType: 'skill',
|
|
source: 'marketplace',
|
|
},
|
|
],
|
|
});
|
|
|
|
// Should have the starter-pack version, not the marketplace duplicate
|
|
const fromStarter = result.candidates.filter(c => c.name === 'research-synthesis' && c.source === 'starter-pack');
|
|
const fromMarketplace = result.candidates.filter(c => c.name === 'research-synthesis' && c.source === 'marketplace');
|
|
expect(fromStarter.length).toBe(1);
|
|
expect(fromMarketplace.length).toBe(0);
|
|
});
|
|
|
|
it('marketplace candidates include trust assessment', () => {
|
|
const result = searchCapabilities({
|
|
need: 'email sending',
|
|
installedSkills: [],
|
|
starterSkillsDir: starterDir,
|
|
marketplaceCandidates: [
|
|
{
|
|
name: 'email-sender',
|
|
description: 'Send emails with SMTP support',
|
|
packageType: 'skill',
|
|
source: 'marketplace',
|
|
},
|
|
],
|
|
});
|
|
|
|
const mktCandidate = result.candidates.find(c => c.source === 'marketplace');
|
|
expect(mktCandidate).toBeDefined();
|
|
expect(mktCandidate!.trust).toBeDefined();
|
|
expect(mktCandidate!.trust!.riskLevel).toBeDefined();
|
|
});
|
|
|
|
// ── Marketplace search callback in skill tools ──────────────────
|
|
|
|
describe('acquire_capability with marketplace callback', () => {
|
|
it('calls searchMarketplace and includes results', async () => {
|
|
let searchCalled = false;
|
|
const tools = createSkillTools({
|
|
waggleHome: tmpDir,
|
|
starterSkillsDir: starterDir,
|
|
nativeToolNames: [],
|
|
searchMarketplace: async (query: string) => {
|
|
searchCalled = true;
|
|
return [
|
|
{
|
|
packageId: 73,
|
|
name: 'email-pro',
|
|
description: `Professional email tools matching: ${query}`,
|
|
packageType: 'skill',
|
|
installType: 'plugin',
|
|
source: 'marketplace',
|
|
},
|
|
];
|
|
},
|
|
});
|
|
|
|
const acquireTool = tools.find(t => t.name === 'acquire_capability');
|
|
expect(acquireTool).toBeDefined();
|
|
|
|
const result = await acquireTool!.execute({ need: 'email automation' });
|
|
expect(searchCalled).toBe(true);
|
|
expect(result).toContain('email-pro');
|
|
expect(result).toContain('"kind":"marketplace"');
|
|
expect(result).toContain('"packageId":73');
|
|
expect(result).toContain('"installType":"plugin"');
|
|
});
|
|
|
|
it('does not emit an actionable marketplace marker without canonical identity', async () => {
|
|
const tools = createSkillTools({
|
|
waggleHome: tmpDir,
|
|
starterSkillsDir: starterDir,
|
|
nativeToolNames: [],
|
|
searchMarketplace: async () => [
|
|
{
|
|
name: 'email-pro',
|
|
description: 'Professional email automation tools',
|
|
packageType: 'skill',
|
|
source: 'marketplace',
|
|
},
|
|
],
|
|
});
|
|
|
|
const acquireTool = tools.find(t => t.name === 'acquire_capability');
|
|
const result = await acquireTool!.execute({ need: 'email automation' });
|
|
|
|
expect(result).toContain('email-pro');
|
|
expect(result).not.toContain('waggle:capability_request');
|
|
});
|
|
|
|
it('degrades gracefully when marketplace callback throws', async () => {
|
|
const tools = createSkillTools({
|
|
waggleHome: tmpDir,
|
|
starterSkillsDir: starterDir,
|
|
nativeToolNames: [],
|
|
searchMarketplace: async () => {
|
|
throw new Error('Marketplace DB offline');
|
|
},
|
|
});
|
|
|
|
const acquireTool = tools.find(t => t.name === 'acquire_capability');
|
|
const result = await acquireTool!.execute({ need: 'research synthesis' });
|
|
|
|
// Should still return results from starter pack (not crash)
|
|
expect(result).toBeTruthy();
|
|
expect(result).toContain('research-synthesis');
|
|
});
|
|
|
|
it('works without marketplace callback (no crash)', async () => {
|
|
const tools = createSkillTools({
|
|
waggleHome: tmpDir,
|
|
starterSkillsDir: starterDir,
|
|
nativeToolNames: ['web_search'],
|
|
});
|
|
|
|
const acquireTool = tools.find(t => t.name === 'acquire_capability');
|
|
const result = await acquireTool!.execute({ need: 'search the web' });
|
|
|
|
// Should find native tool
|
|
expect(result).toBeTruthy();
|
|
expect(result).toContain('web_search');
|
|
});
|
|
});
|
|
});
|