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,177 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { validatePluginManifest } from '../src/plugin-manifest.js';
import { PluginManager } from '../src/plugin-manager.js';
function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-plugin-test-'));
}
function writePluginJson(dir: string, manifest: Record<string, unknown>): void {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'plugin.json'), JSON.stringify(manifest), 'utf-8');
}
const VALID_MANIFEST = {
name: 'test-plugin',
version: '1.0.0',
description: 'A test plugin',
skills: ['summarize'],
};
describe('validatePluginManifest', () => {
it('validates a correct manifest', () => {
const result = validatePluginManifest(VALID_MANIFEST);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('rejects manifest with missing name', () => {
const result = validatePluginManifest({
version: '1.0.0',
description: 'No name',
});
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes('name'))).toBe(true);
});
it('rejects manifest with missing version', () => {
const result = validatePluginManifest({
name: 'test',
description: 'No version',
});
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes('version'))).toBe(true);
});
it('rejects manifest with missing description', () => {
const result = validatePluginManifest({
name: 'test',
version: '1.0.0',
});
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes('description'))).toBe(true);
});
it('rejects path traversal and separator characters in plugin names', () => {
for (const name of ['../escape', 'nested/plugin', 'C:\\escape', '..']) {
const result = validatePluginManifest({
name,
version: '1.0.0',
description: 'Unsafe name',
});
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes('filesystem-safe'))).toBe(true);
}
});
it('validates manifest with mcpServers', () => {
const result = validatePluginManifest({
name: 'mcp-plugin',
version: '1.0.0',
description: 'Has MCP servers',
mcpServers: [{ name: 'server1', command: 'npx', args: ['serve'] }],
});
expect(result.valid).toBe(true);
});
it('rejects invalid mcpServers entries', () => {
const result = validatePluginManifest({
name: 'bad-mcp',
version: '1.0.0',
description: 'Bad MCP',
mcpServers: [{ name: '', command: '' }],
});
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
});
describe('PluginManager', () => {
let pluginsDir: string;
let manager: PluginManager;
const tempDirs: string[] = [];
beforeEach(() => {
pluginsDir = makeTempDir();
tempDirs.push(pluginsDir);
manager = new PluginManager(pluginsDir);
});
afterEach(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.length = 0;
});
it('starts with no plugins', () => {
expect(manager.list()).toEqual([]);
});
it('installs a local plugin', () => {
const sourceDir = makeTempDir();
tempDirs.push(sourceDir);
writePluginJson(sourceDir, VALID_MANIFEST);
manager.installLocal(sourceDir);
const plugins = manager.list();
expect(plugins).toHaveLength(1);
expect(plugins[0].name).toBe('test-plugin');
expect(plugins[0].version).toBe('1.0.0');
// Verify plugin files were copied
const copiedManifest = path.join(pluginsDir, 'test-plugin', 'plugin.json');
expect(fs.existsSync(copiedManifest)).toBe(true);
});
it('rejects installing a plugin with invalid manifest', () => {
const sourceDir = makeTempDir();
tempDirs.push(sourceDir);
writePluginJson(sourceDir, { name: '', version: '1.0.0', description: '' });
expect(() => manager.installLocal(sourceDir)).toThrow('Invalid plugin manifest');
});
it('rejects installing from directory without plugin.json', () => {
const sourceDir = makeTempDir();
tempDirs.push(sourceDir);
expect(() => manager.installLocal(sourceDir)).toThrow('No plugin.json found');
});
it('uninstalls a plugin', () => {
const sourceDir = makeTempDir();
tempDirs.push(sourceDir);
writePluginJson(sourceDir, VALID_MANIFEST);
manager.installLocal(sourceDir);
expect(manager.list()).toHaveLength(1);
manager.uninstall('test-plugin');
expect(manager.list()).toHaveLength(0);
// Verify plugin directory was removed
expect(fs.existsSync(path.join(pluginsDir, 'test-plugin'))).toBe(false);
});
it('throws when uninstalling a plugin that is not installed', () => {
expect(() => manager.uninstall('nonexistent')).toThrow('not installed');
});
it('persists registry across instances', () => {
const sourceDir = makeTempDir();
tempDirs.push(sourceDir);
writePluginJson(sourceDir, VALID_MANIFEST);
manager.installLocal(sourceDir);
// Create a new manager instance pointing at the same directory
const manager2 = new PluginManager(pluginsDir);
expect(manager2.list()).toHaveLength(1);
expect(manager2.list()[0].name).toBe('test-plugin');
});
});

View File

@@ -0,0 +1,440 @@
import { describe, it, expect, vi } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import {
PluginRuntime,
PluginRuntimeManager,
webResearchPluginManifest,
type PluginManifestWithTools,
type PluginLifecycleState,
} from '../src/plugin-runtime.js';
import { PluginManager } from '../src/plugin-manager.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function simpleManifest(overrides: Partial<PluginManifestWithTools> = {}): PluginManifestWithTools {
return {
name: 'test-plugin',
version: '1.0.0',
description: 'A test plugin',
skills: ['test-skill'],
tools: [
{
name: 'test_tool',
description: 'A test tool',
parameters: { type: 'object', properties: {} },
},
],
...overrides,
};
}
// ---------------------------------------------------------------------------
// PluginRuntime — single plugin
// ---------------------------------------------------------------------------
describe('PluginRuntime', () => {
it('starts in installed state after construction', () => {
const runtime = new PluginRuntime(simpleManifest());
expect(runtime.getState()).toBe('installed');
});
it('enable() transitions to enabled then active', async () => {
const runtime = new PluginRuntime(simpleManifest());
const states: PluginLifecycleState[] = [];
runtime.on('stateChange', (e: { to: PluginLifecycleState }) => states.push(e.to));
await runtime.enable();
expect(runtime.getState()).toBe('active');
expect(states).toEqual(['enabled', 'active']);
});
it('active plugin exposes contributed tools', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
const tools = runtime.getContributedTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('test_tool');
expect(tools[0].description).toBe('A test tool');
expect(typeof tools[0].execute).toBe('function');
});
it('active plugin exposes contributed skills', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
const skills = runtime.getContributedSkills();
expect(skills).toEqual(['test-skill']);
});
it('disable() removes tools and skills, transitions to disabled', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
runtime.disable();
expect(runtime.getState()).toBe('disabled');
expect(runtime.getContributedTools()).toEqual([]);
expect(runtime.getContributedSkills()).toEqual([]);
});
it('re-enable after disable works', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
runtime.disable();
expect(runtime.getState()).toBe('disabled');
await runtime.enable();
expect(runtime.getState()).toBe('active');
expect(runtime.getContributedTools()).toHaveLength(1);
expect(runtime.getContributedSkills()).toEqual(['test-skill']);
});
it('enters error state on activation failure (missing capability)', async () => {
const runtime = new PluginRuntime(simpleManifest(), {
requiredCapabilities: ['network'],
availableCapabilities: [],
});
await expect(runtime.enable()).rejects.toThrow('Missing required capability: network');
expect(runtime.getState()).toBe('error');
});
it('can re-enable from error state', async () => {
// Use a counter-based executor that throws on first activation, succeeds on second
let callCount = 0;
const flakyExecutor = (def: { name: string; description: string; parameters: Record<string, unknown> }) => {
callCount++;
if (callCount <= 1) {
throw new Error('Transient activation failure');
}
return async () => 'ok';
};
const runtime = new PluginRuntime(simpleManifest(), {
toolExecutor: flakyExecutor,
});
// First enable() fails — toolExecutor throws during activation
await expect(runtime.enable()).rejects.toThrow('Transient activation failure');
expect(runtime.getState()).toBe('error');
// Second enable() succeeds — same runtime, error → enabled → active
await runtime.enable();
expect(runtime.getState()).toBe('active');
expect(runtime.getContributedTools()).toHaveLength(1);
});
it('enable() on already-active plugin is a no-op', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
expect(runtime.getState()).toBe('active');
const stateChanges: string[] = [];
runtime.on('stateChange', (e: { to: string }) => stateChanges.push(e.to));
await runtime.enable(); // no-op
expect(runtime.getState()).toBe('active');
expect(stateChanges).toEqual([]); // no transitions fired
});
it('disable() on already-disabled plugin is a no-op', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
runtime.disable();
expect(runtime.getState()).toBe('disabled');
const stateChanges: string[] = [];
runtime.on('stateChange', (e: { to: string }) => stateChanges.push(e.to));
runtime.disable(); // no-op
expect(runtime.getState()).toBe('disabled');
expect(stateChanges).toEqual([]);
});
it('emits stateChange events with from/to', async () => {
const runtime = new PluginRuntime(simpleManifest());
const events: Array<{ plugin: string; from: string; to: string }> = [];
runtime.on('stateChange', (e) => events.push(e));
await runtime.enable();
expect(events).toEqual([
{ plugin: 'test-plugin', from: 'installed', to: 'enabled' },
{ plugin: 'test-plugin', from: 'enabled', to: 'active' },
]);
});
it('emits error event on activation failure', async () => {
const runtime = new PluginRuntime(simpleManifest(), {
requiredCapabilities: ['gpu'],
availableCapabilities: [],
});
const errors: Array<{ plugin: string; error: Error }> = [];
runtime.on('error', (e) => errors.push(e));
await runtime.enable().catch(() => {}); // swallow throw
expect(errors).toHaveLength(1);
expect(errors[0].plugin).toBe('test-plugin');
expect(errors[0].error.message).toContain('gpu');
});
it('uses custom tool executor when provided', async () => {
const customExecutor = vi.fn(() => async () => 'custom-result');
const runtime = new PluginRuntime(simpleManifest(), { toolExecutor: customExecutor });
await runtime.enable();
const tools = runtime.getContributedTools();
expect(customExecutor).toHaveBeenCalledTimes(1);
const result = await tools[0].execute({});
expect(result).toBe('custom-result');
});
it('default executor returns JSON with tool name and args', async () => {
const runtime = new PluginRuntime(simpleManifest());
await runtime.enable();
const result = await runtime.getContributedTools()[0].execute({ foo: 'bar' });
const parsed = JSON.parse(result);
expect(parsed).toEqual({ tool: 'test_tool', args: { foo: 'bar' }, status: 'executed' });
});
});
// ---------------------------------------------------------------------------
// PluginRuntimeManager — multi-plugin management
// ---------------------------------------------------------------------------
describe('PluginRuntimeManager', () => {
it('registers a plugin in installed state', () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest());
const states = mgr.getPluginStates();
expect(states['test-plugin']).toBe('installed');
});
it('registers multiple plugins', () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest({ name: 'plugin-a' }));
mgr.register(simpleManifest({ name: 'plugin-b' }));
const states = mgr.getPluginStates();
expect(Object.keys(states)).toHaveLength(2);
expect(states['plugin-a']).toBe('installed');
expect(states['plugin-b']).toBe('installed');
});
it('throws on duplicate registration', () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest());
expect(() => mgr.register(simpleManifest())).toThrow('already registered');
});
it('enables and activates a plugin', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest());
await mgr.enable('test-plugin');
expect(mgr.getPluginStates()['test-plugin']).toBe('active');
});
it('disables a plugin', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest());
await mgr.enable('test-plugin');
mgr.disable('test-plugin');
expect(mgr.getPluginStates()['test-plugin']).toBe('disabled');
});
it('getActive() returns only active plugins', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest({ name: 'active-one' }));
mgr.register(simpleManifest({ name: 'inactive-one' }));
await mgr.enable('active-one');
const active = mgr.getActive();
expect(active).toHaveLength(1);
expect(active[0].getManifest().name).toBe('active-one');
});
it('getAllTools() aggregates tools from all active plugins', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(
simpleManifest({
name: 'plugin-a',
tools: [{ name: 'tool_a', description: 'Tool A', parameters: {} }],
})
);
mgr.register(
simpleManifest({
name: 'plugin-b',
tools: [{ name: 'tool_b', description: 'Tool B', parameters: {} }],
})
);
await mgr.enable('plugin-a');
await mgr.enable('plugin-b');
const tools = mgr.getAllTools();
expect(tools).toHaveLength(2);
expect(tools.map((t) => t.name).sort()).toEqual(['tool_a', 'tool_b']);
});
it('getAllSkills() aggregates skills from all active plugins', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest({ name: 'p1', skills: ['skill-a'] }));
mgr.register(simpleManifest({ name: 'p2', skills: ['skill-b', 'skill-c'] }));
await mgr.enable('p1');
await mgr.enable('p2');
const skills = mgr.getAllSkills();
expect(skills.sort()).toEqual(['skill-a', 'skill-b', 'skill-c']);
});
it('getPluginStates() returns all plugin states', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest({ name: 'a' }));
mgr.register(simpleManifest({ name: 'b' }));
mgr.register(simpleManifest({ name: 'c' }));
await mgr.enable('a');
await mgr.enable('b');
mgr.disable('b');
expect(mgr.getPluginStates()).toEqual({
a: 'active',
b: 'disabled',
c: 'installed',
});
});
it('forwards stateChange events from plugins', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(simpleManifest());
const events: Array<{ plugin: string; to: string }> = [];
mgr.on('stateChange', (e) => events.push(e));
await mgr.enable('test-plugin');
expect(events).toHaveLength(2); // enabled, active
expect(events[0].to).toBe('enabled');
expect(events[1].to).toBe('active');
});
it('throws when enabling unregistered plugin', async () => {
const mgr = new PluginRuntimeManager();
await expect(mgr.enable('nonexistent')).rejects.toThrow('not registered');
});
});
// ---------------------------------------------------------------------------
// Flagship plugin fixture — web-research
// ---------------------------------------------------------------------------
describe('webResearchPluginManifest', () => {
it('has correct name and tools', () => {
expect(webResearchPluginManifest.name).toBe('web-research');
expect(webResearchPluginManifest.tools).toHaveLength(2);
expect(webResearchPluginManifest.tools!.map((t) => t.name)).toEqual(['web_scrape', 'web_summarize']);
});
it('can be registered and activated via PluginRuntimeManager', async () => {
const mgr = new PluginRuntimeManager();
mgr.register(webResearchPluginManifest);
await mgr.enable('web-research');
const tools = mgr.getAllTools();
expect(tools).toHaveLength(2);
expect(mgr.getAllSkills()).toEqual(['web-research']);
});
});
// ---------------------------------------------------------------------------
// PluginManager.toRuntimeManager — bridge from filesystem to runtime
// ---------------------------------------------------------------------------
describe('PluginManager.toRuntimeManager', () => {
let tmpDir: string;
function createTestPlugin(pluginsDir: string, name: string): void {
const pluginDir = path.join(pluginsDir, name);
fs.mkdirSync(pluginDir, { recursive: true });
const manifest = {
name,
version: '1.0.0',
description: `Test plugin ${name}`,
skills: [`${name}-skill`],
tools: [
{ name: `${name}_tool`, description: `Tool from ${name}`, parameters: { type: 'object', properties: {} } },
],
};
fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest));
}
it('creates a PluginRuntimeManager with all installed plugins', () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-pm-bridge-'));
const pm = new PluginManager(tmpDir);
// Install two test plugins
const srcA = path.join(tmpDir, '_src_a');
const srcB = path.join(tmpDir, '_src_b');
createTestPlugin(tmpDir, '_src_a');
createTestPlugin(tmpDir, '_src_b');
// installLocal copies and registers
pm.installLocal(srcA);
pm.installLocal(srcB);
const rtm = pm.toRuntimeManager();
const states = rtm.getPluginStates();
expect(Object.keys(states)).toHaveLength(2);
expect(states['_src_a']).toBe('installed');
expect(states['_src_b']).toBe('installed');
// Clean up
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('runtime manager plugins can be enabled and contribute tools', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-pm-bridge2-'));
const pm = new PluginManager(tmpDir);
const srcDir = path.join(tmpDir, '_src_plug');
createTestPlugin(tmpDir, '_src_plug');
pm.installLocal(srcDir);
const rtm = pm.toRuntimeManager();
await rtm.enable('_src_plug');
const tools = rtm.getAllTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('_src_plug_tool');
// Clean up
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('returns empty manager when no plugins installed', () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-pm-empty-'));
const pm = new PluginManager(tmpDir);
const rtm = pm.toRuntimeManager();
expect(Object.keys(rtm.getPluginStates())).toHaveLength(0);
expect(rtm.getAllTools()).toEqual([]);
// Clean up
fs.rmSync(tmpDir, { recursive: true, force: true });
});
});

View File

@@ -0,0 +1,110 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
listStarterSkills,
installStarterSkills,
getStarterSkillsDir,
} from '../src/starter-skills/index.js';
describe('starter-skills', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-starter-skills-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('getStarterSkillsDir() returns a directory that exists', () => {
const dir = getStarterSkillsDir();
expect(fs.existsSync(dir)).toBe(true);
});
it('listStarterSkills() returns 18 skill names', () => {
const skills = listStarterSkills();
expect(skills).toHaveLength(18);
});
it('all expected skill names are present', () => {
const skills = listStarterSkills();
const expected = [
'brainstorm',
'catch-up',
'code-review',
'compare-docs',
'daily-plan',
'decision-matrix',
'draft-memo',
'explain-concept',
'extract-actions',
'meeting-prep',
'plan-execute',
'research-synthesis',
'research-team',
'retrospective',
'review-pair',
'risk-assessment',
'status-update',
'task-breakdown',
];
expect(skills).toEqual(expected);
});
it('each starter skill file exists and has content (> 50 chars)', () => {
const dir = getStarterSkillsDir();
const skills = listStarterSkills();
for (const name of skills) {
const filePath = path.join(dir, `${name}.md`);
expect(fs.existsSync(filePath), `${name}.md should exist`).toBe(true);
const content = fs.readFileSync(filePath, 'utf-8');
expect(content.length, `${name}.md should have > 50 chars`).toBeGreaterThan(50);
}
});
it('all skill names are valid (alphanumeric + hyphens only)', () => {
const skills = listStarterSkills();
for (const name of skills) {
expect(name, `${name} should match [a-z0-9-]+`).toMatch(/^[a-z0-9-]+$/);
}
});
it('installStarterSkills() copies files to target directory', () => {
const installed = installStarterSkills(tmpDir);
expect(installed).toHaveLength(18);
// Verify files exist in target
for (const name of installed) {
const filePath = path.join(tmpDir, `${name}.md`);
expect(fs.existsSync(filePath), `${name}.md should be installed`).toBe(true);
}
});
it('installStarterSkills() does NOT overwrite existing skills', () => {
// Pre-create a skill with custom content
const customContent = '# Custom catch-up skill\nMy custom version.';
fs.writeFileSync(path.join(tmpDir, 'catch-up.md'), customContent, 'utf-8');
const installed = installStarterSkills(tmpDir);
// catch-up should NOT be in the installed list (was skipped)
expect(installed).not.toContain('catch-up');
expect(installed).toHaveLength(17);
// Verify custom content was preserved
const content = fs.readFileSync(path.join(tmpDir, 'catch-up.md'), 'utf-8');
expect(content).toBe(customContent);
});
it('installStarterSkills() creates target directory if missing', () => {
const nestedDir = path.join(tmpDir, 'nested', 'skills');
expect(fs.existsSync(nestedDir)).toBe(false);
const installed = installStarterSkills(nestedDir);
expect(installed).toHaveLength(18);
expect(fs.existsSync(nestedDir)).toBe(true);
});
});

View File

@@ -0,0 +1,284 @@
import { describe, it, expect } from 'vitest';
import {
validateSkillMd,
isValidSemver,
compareSemver,
checkSkillDependencies,
checkVersionDowngrade,
} from '../src/validate-skill.js';
describe('validateSkillMd', () => {
it('parses valid SKILL.md', () => {
const content = `---
name: summarizer
description: Summarizes long documents
version: 1.0.0
author: waggle-team
---
You are an expert summarizer. Given a document, produce a concise summary.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.warnings).toHaveLength(0);
expect(result.metadata).toBeDefined();
expect(result.metadata!.name).toBe('summarizer');
expect(result.metadata!.description).toBe('Summarizes long documents');
expect(result.metadata!.version).toBe('1.0.0');
expect(result.metadata!.author).toBe('waggle-team');
expect(result.metadata!.systemPrompt).toBe(
'You are an expert summarizer. Given a document, produce a concise summary.',
);
});
it('rejects missing name', () => {
const content = `---
description: A skill without a name
---
Some prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: name');
});
it('rejects missing description', () => {
const content = `---
name: no-desc
---
Some prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: description');
});
it('rejects missing frontmatter', () => {
const content = `Just a plain markdown file with no frontmatter.`;
const result = validateSkillMd(content);
expect(result.valid).toBe(false);
expect(result.errors).toContain(
'Missing YAML frontmatter (must be wrapped in --- delimiters)',
);
});
it('extracts system prompt from body', () => {
const content = `---
name: coder
description: Writes code
---
You are a coding assistant.
Always use TypeScript.
Write clean, tested code.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true);
expect(result.metadata!.systemPrompt).toBe(
'You are a coding assistant.\n\nAlways use TypeScript.\nWrite clean, tested code.',
);
});
// F18: Version validation tests
it('warns on invalid semver version', () => {
const content = `---
name: bad-version
description: Has invalid version
version: abc
---
Prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true); // still valid, just warns
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0]).toContain('Invalid semver version');
expect(result.warnings[0]).toContain('abc');
});
it('accepts valid semver with pre-release', () => {
const content = `---
name: prerelease
description: Has pre-release version
version: 2.0.0-beta.1
---
Prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true);
expect(result.warnings).toHaveLength(0);
expect(result.metadata!.version).toBe('2.0.0-beta.1');
});
// F18: Dependency parsing tests
it('parses dependencies from bracket syntax', () => {
const content = `---
name: researcher
description: Research skill
dependencies: [search_memory, save_memory, query_knowledge]
---
Research prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true);
expect(result.metadata!.dependencies).toEqual(['search_memory', 'save_memory', 'query_knowledge']);
});
it('parses dependencies from comma-separated syntax', () => {
const content = `---
name: researcher
description: Research skill
dependencies: search_memory, save_memory
---
Research prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true);
expect(result.metadata!.dependencies).toEqual(['search_memory', 'save_memory']);
});
it('returns undefined dependencies when field is absent', () => {
const content = `---
name: no-deps
description: No dependencies
---
Prompt.
`;
const result = validateSkillMd(content);
expect(result.valid).toBe(true);
expect(result.metadata!.dependencies).toBeUndefined();
});
});
describe('isValidSemver', () => {
it('accepts standard semver', () => {
expect(isValidSemver('1.0.0')).toBe(true);
expect(isValidSemver('0.1.0')).toBe(true);
expect(isValidSemver('12.34.56')).toBe(true);
});
it('accepts semver with pre-release', () => {
expect(isValidSemver('1.0.0-alpha')).toBe(true);
expect(isValidSemver('1.0.0-beta.1')).toBe(true);
});
it('accepts semver with build metadata', () => {
expect(isValidSemver('1.0.0+build.123')).toBe(true);
});
it('rejects invalid semver', () => {
expect(isValidSemver('1.0')).toBe(false);
expect(isValidSemver('abc')).toBe(false);
expect(isValidSemver('v1.0.0')).toBe(false);
expect(isValidSemver('1')).toBe(false);
expect(isValidSemver('')).toBe(false);
});
});
describe('compareSemver', () => {
it('compares equal versions', () => {
expect(compareSemver('1.0.0', '1.0.0')).toBe(0);
});
it('compares different major versions', () => {
expect(compareSemver('2.0.0', '1.0.0')).toBe(1);
expect(compareSemver('1.0.0', '2.0.0')).toBe(-1);
});
it('compares different minor versions', () => {
expect(compareSemver('1.2.0', '1.1.0')).toBe(1);
expect(compareSemver('1.0.0', '1.1.0')).toBe(-1);
});
it('compares different patch versions', () => {
expect(compareSemver('1.0.2', '1.0.1')).toBe(1);
expect(compareSemver('1.0.0', '1.0.1')).toBe(-1);
});
it('ignores pre-release when comparing', () => {
expect(compareSemver('1.0.0-alpha', '1.0.0-beta')).toBe(0);
});
});
describe('checkSkillDependencies', () => {
it('returns no warnings when all dependencies are available', () => {
const metadata = {
name: 'test-skill',
description: 'test',
dependencies: ['search_memory', 'save_memory'],
systemPrompt: '',
};
const warnings = checkSkillDependencies(metadata, ['search_memory', 'save_memory', 'get_identity']);
expect(warnings).toHaveLength(0);
});
it('warns about missing dependency tools', () => {
const metadata = {
name: 'test-skill',
description: 'test',
dependencies: ['search_memory', 'nonexistent_tool'],
systemPrompt: '',
};
const warnings = checkSkillDependencies(metadata, ['search_memory', 'save_memory']);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('nonexistent_tool');
expect(warnings[0]).toContain('not available');
});
it('returns no warnings when no dependencies declared', () => {
const metadata = {
name: 'test-skill',
description: 'test',
systemPrompt: '',
};
const warnings = checkSkillDependencies(metadata, ['search_memory']);
expect(warnings).toHaveLength(0);
});
});
describe('checkVersionDowngrade', () => {
it('detects downgrade', () => {
const warning = checkVersionDowngrade('my-skill', '2.0.0', '1.0.0');
expect(warning).toContain('Downgrade detected');
expect(warning).toContain('1.0.0 < 2.0.0');
});
it('detects same version re-install', () => {
const warning = checkVersionDowngrade('my-skill', '1.0.0', '1.0.0');
expect(warning).toContain('Same version re-install');
});
it('returns null for upgrade', () => {
const warning = checkVersionDowngrade('my-skill', '1.0.0', '2.0.0');
expect(warning).toBeNull();
});
it('returns null when versions are missing', () => {
expect(checkVersionDowngrade('s', undefined, '1.0.0')).toBeNull();
expect(checkVersionDowngrade('s', '1.0.0', undefined)).toBeNull();
expect(checkVersionDowngrade('s', undefined, undefined)).toBeNull();
});
it('returns null for invalid semver', () => {
expect(checkVersionDowngrade('s', 'abc', '1.0.0')).toBeNull();
expect(checkVersionDowngrade('s', '1.0.0', 'xyz')).toBeNull();
});
});

View File

@@ -0,0 +1,124 @@
/**
* Wave G — Capability Surface Productization tests.
*
* Verifies:
* 1. Pack catalog structure and completeness
* 2. Pack-to-skill relationships are valid
* 3. Product language consistency (no internal categories leaked)
* 4. Capability summary data shape
*/
import { describe, it, expect } from 'vitest';
import { listCapabilityPacks, getPackManifest } from '../src/capability-packs/index.js';
describe('Wave G: Capability Surface', () => {
describe('Pack Catalog', () => {
it('has exactly 5 curated packs', () => {
const packs = listCapabilityPacks();
expect(packs).toHaveLength(5);
});
it('all packs have required fields', () => {
const packs = listCapabilityPacks();
for (const pack of packs) {
expect(pack.id).toBeTruthy();
expect(pack.name).toBeTruthy();
expect(pack.description).toBeTruthy();
expect(pack.skills.length).toBeGreaterThan(0);
}
});
it('includes the 5 core pack IDs', () => {
const packs = listCapabilityPacks();
const ids = packs.map(p => p.id);
expect(ids).toContain('writing-suite');
expect(ids).toContain('research-workflow');
expect(ids).toContain('planning-master');
expect(ids).toContain('decision-framework');
expect(ids).toContain('team-collaboration');
});
it('each pack has user-facing name (not slug)', () => {
const packs = listCapabilityPacks();
for (const pack of packs) {
// Names should have spaces and be title-cased, not slugs
expect(pack.name).toMatch(/[A-Z]/); // Contains uppercase
expect(pack.name).not.toMatch(/^[a-z-]+$/); // Not a slug
}
});
});
describe('Pack-to-Skill Relationships', () => {
it('each pack references valid skill IDs', () => {
const packs = listCapabilityPacks();
for (const pack of packs) {
for (const skillId of pack.skills) {
expect(typeof skillId).toBe('string');
expect(skillId.length).toBeGreaterThan(0);
// Skill IDs should be kebab-case
expect(skillId).toMatch(/^[a-z][a-z0-9-]*$/);
}
}
});
it('getPackManifest returns correct pack by ID', () => {
const pack = getPackManifest('writing-suite');
expect(pack).not.toBeNull();
expect(pack!.name).toBe('Writing Suite');
expect(pack!.skills).toContain('draft-memo');
});
it('getPackManifest returns null for unknown pack', () => {
const pack = getPackManifest('nonexistent-pack');
expect(pack).toBeNull();
});
});
describe('Product Language', () => {
it('pack descriptions use user-facing language (no internal terms)', () => {
const packs = listCapabilityPacks();
const internalTerms = ['plugin', 'MCP', 'hook', 'mcp_server'];
for (const pack of packs) {
for (const term of internalTerms) {
expect(pack.description.toLowerCase()).not.toContain(term.toLowerCase());
}
}
});
it('pack names do not reference implementation details', () => {
const packs = listCapabilityPacks();
for (const pack of packs) {
expect(pack.name.toLowerCase()).not.toContain('plugin');
expect(pack.name.toLowerCase()).not.toContain('mcp');
expect(pack.name.toLowerCase()).not.toContain('hook');
}
});
});
describe('Capability Summary Shape', () => {
it('can construct a unified capability count from component data', () => {
// Simulates what the UI does: merge implementation counts into product counts
const mockCapabilities = {
tools: { count: 25, native: 20, plugin: 3, mcp: 2 },
skills: [{ name: 'draft-memo' }, { name: 'research-synthesis' }],
workflows: [{ name: 'research-team' }],
plugins: [{ name: 'p1', state: 'active' }],
mcpServers: [{ name: 'm1', healthy: true }],
commands: [{ name: '/help' }, { name: '/cost' }],
};
// Product-level counts (what user sees)
const toolCount = mockCapabilities.tools.count;
const skillCount = mockCapabilities.skills.length;
const workflowCount = mockCapabilities.workflows.length;
const extensionCount = mockCapabilities.plugins.length + mockCapabilities.mcpServers.length;
const commandCount = mockCapabilities.commands.length;
expect(toolCount).toBe(25);
expect(skillCount).toBe(2);
expect(workflowCount).toBe(1);
expect(extensionCount).toBe(2); // unified plugins + MCP
expect(commandCount).toBe(2);
});
});
});