23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
2037 lines
67 KiB
TypeScript
2037 lines
67 KiB
TypeScript
/**
|
|
* Deep Tests for Plugins, Transfer, Production, Runtime, Update, Config, and Appliance modules.
|
|
*
|
|
* Covers:
|
|
* - Plugin discovery, search, install/uninstall lifecycle
|
|
* - Transfer serialization round-trips (CFP format)
|
|
* - Anonymization (PII detection and redaction)
|
|
* - IPFS client helpers (CID/IPNS validation, gateway URLs, hash)
|
|
* - IPFS upload helpers (gateway URL, IPNS URL, service status)
|
|
* - Plugin Store high-level API
|
|
* - Transfer Store search, registry, and helpers
|
|
* - Config adapter (system <-> v3)
|
|
* - Production utilities (circuit breaker, rate limiter, retry, error handler, monitoring)
|
|
* - Update system (validator, rate-limiter)
|
|
* - Benchmark infrastructure
|
|
* - Type exports completeness
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import * as crypto from 'crypto';
|
|
|
|
// ============================================================================
|
|
// 1. Plugin Manager
|
|
// ============================================================================
|
|
|
|
import {
|
|
PluginManager,
|
|
getPluginManager,
|
|
resetPluginManager,
|
|
} from '../src/plugins/manager.js';
|
|
|
|
describe('PluginManager', () => {
|
|
let manager: PluginManager;
|
|
const testDir = `/tmp/plugin-test-${Date.now()}`;
|
|
|
|
beforeEach(() => {
|
|
resetPluginManager();
|
|
manager = new PluginManager(testDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
resetPluginManager();
|
|
});
|
|
|
|
it('should initialize and create plugins directory', async () => {
|
|
await manager.initialize();
|
|
const installed = await manager.getInstalled();
|
|
expect(installed).toEqual([]);
|
|
});
|
|
|
|
it('should return empty list when no plugins installed', async () => {
|
|
await manager.initialize();
|
|
const plugins = await manager.getInstalled();
|
|
expect(plugins).toHaveLength(0);
|
|
});
|
|
|
|
it('should report not installed for unknown plugin', async () => {
|
|
await manager.initialize();
|
|
const isInstalled = await manager.isInstalled('nonexistent');
|
|
expect(isInstalled).toBe(false);
|
|
});
|
|
|
|
it('should return undefined for unknown plugin get', async () => {
|
|
await manager.initialize();
|
|
const plugin = await manager.getPlugin('nonexistent');
|
|
expect(plugin).toBeUndefined();
|
|
});
|
|
|
|
it('should fail enable on non-installed plugin', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.enable('nonexistent');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('not installed');
|
|
});
|
|
|
|
it('should fail disable on non-installed plugin', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.disable('nonexistent');
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should fail toggle on non-installed plugin', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.toggle('nonexistent');
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should fail uninstall on non-installed plugin', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.uninstall('nonexistent');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('not installed');
|
|
});
|
|
|
|
it('should fail upgrade on non-installed plugin', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.upgrade('nonexistent');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('not installed');
|
|
});
|
|
|
|
it('should fail setConfig on non-installed plugin', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.setConfig('nonexistent', { key: 'value' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should fail installFromLocal with nonexistent path', async () => {
|
|
await manager.initialize();
|
|
const result = await manager.installFromLocal('/nonexistent/path');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('does not exist');
|
|
});
|
|
|
|
it('should return correct plugins dir and manifest path', () => {
|
|
expect(manager.getPluginsDir()).toContain('.claude-flow/plugins');
|
|
expect(manager.getManifestPath()).toContain('installed.json');
|
|
});
|
|
|
|
it('getPluginManager returns singleton', () => {
|
|
resetPluginManager();
|
|
const mgr1 = getPluginManager('/tmp/test-singleton');
|
|
const mgr2 = getPluginManager('/tmp/test-other');
|
|
expect(mgr1).toBe(mgr2); // Singleton, ignores second base dir
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 2. Plugin Store Types (completeness)
|
|
// ============================================================================
|
|
|
|
import type {
|
|
PluginEntry,
|
|
PluginRegistry,
|
|
PluginSearchOptions,
|
|
PluginSearchResult,
|
|
PluginStoreConfig,
|
|
PluginType,
|
|
PluginPermission,
|
|
PluginAuthor,
|
|
PluginCategory,
|
|
SecurityAudit,
|
|
SecurityIssue,
|
|
PluginDependency,
|
|
CompatibilityEntry,
|
|
PluginPublishOptions,
|
|
PluginPublishResult,
|
|
PluginDownloadOptions,
|
|
PluginDownloadResult,
|
|
KnownPluginRegistry,
|
|
PluginManifest,
|
|
InstalledPlugins,
|
|
} from '../src/plugins/store/types.js';
|
|
|
|
describe('Plugin Store Types', () => {
|
|
it('should allow creating a valid PluginEntry', () => {
|
|
const entry: PluginEntry = {
|
|
id: 'test-plugin',
|
|
name: '@test/plugin',
|
|
displayName: 'Test Plugin',
|
|
description: 'A test plugin',
|
|
version: '1.0.0',
|
|
cid: 'QmTest',
|
|
size: 1000,
|
|
checksum: 'sha256:abc',
|
|
author: {
|
|
id: 'author-1',
|
|
verified: true,
|
|
plugins: 1,
|
|
totalDownloads: 100,
|
|
reputation: 5,
|
|
},
|
|
license: 'MIT',
|
|
categories: ['official'],
|
|
tags: ['test'],
|
|
keywords: ['testing'],
|
|
downloads: 100,
|
|
rating: 4.5,
|
|
ratingCount: 10,
|
|
lastUpdated: new Date().toISOString(),
|
|
createdAt: new Date().toISOString(),
|
|
minClaudeFlowVersion: '3.0.0',
|
|
dependencies: [],
|
|
type: 'integration',
|
|
hooks: [],
|
|
commands: [],
|
|
permissions: ['memory'],
|
|
exports: ['TestExport'],
|
|
verified: true,
|
|
trustLevel: 'official',
|
|
};
|
|
expect(entry.id).toBe('test-plugin');
|
|
expect(entry.type).toBe('integration');
|
|
});
|
|
|
|
it('should accept all PluginType values', () => {
|
|
const types: PluginType[] = ['agent', 'hook', 'command', 'provider', 'integration', 'theme', 'core', 'hybrid'];
|
|
expect(types).toHaveLength(8);
|
|
});
|
|
|
|
it('should accept all PluginPermission values', () => {
|
|
const perms: PluginPermission[] = ['network', 'filesystem', 'execute', 'memory', 'agents', 'credentials', 'config', 'hooks', 'privileged'];
|
|
expect(perms).toHaveLength(9);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 3. Plugin Search
|
|
// ============================================================================
|
|
|
|
import {
|
|
searchPlugins,
|
|
getPluginSearchSuggestions,
|
|
getPluginTagCloud,
|
|
getPluginCategoryStats,
|
|
findSimilarPlugins,
|
|
getFeaturedPlugins,
|
|
getTrendingPlugins,
|
|
getNewestPlugins,
|
|
getOfficialPlugins,
|
|
getPluginsByPermission,
|
|
} from '../src/plugins/store/search.js';
|
|
|
|
function createMockPluginRegistry(): PluginRegistry {
|
|
const author: PluginAuthor = {
|
|
id: 'author-1',
|
|
displayName: 'Test Author',
|
|
verified: true,
|
|
plugins: 2,
|
|
totalDownloads: 500,
|
|
reputation: 10,
|
|
};
|
|
const makePlugin = (id: string, overrides: Partial<PluginEntry> = {}): PluginEntry => ({
|
|
id,
|
|
name: `@test/${id}`,
|
|
displayName: `Plugin ${id}`,
|
|
description: `Description for ${id}`,
|
|
version: '1.0.0',
|
|
cid: `Qm${id}`,
|
|
size: 1000,
|
|
checksum: `sha256:${id}`,
|
|
author,
|
|
license: 'MIT',
|
|
categories: ['official'],
|
|
tags: ['test', 'core'],
|
|
keywords: ['testing'],
|
|
downloads: 100,
|
|
rating: 4.5,
|
|
ratingCount: 10,
|
|
lastUpdated: '2026-01-01T00:00:00Z',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
minClaudeFlowVersion: '3.0.0',
|
|
dependencies: [],
|
|
type: 'integration',
|
|
hooks: [],
|
|
commands: [],
|
|
permissions: ['memory'],
|
|
exports: [],
|
|
verified: true,
|
|
trustLevel: 'official',
|
|
...overrides,
|
|
});
|
|
|
|
return {
|
|
version: '1.0.0',
|
|
type: 'plugins',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
ipnsName: 'test-ipns',
|
|
plugins: [
|
|
makePlugin('plugin-a', { downloads: 200, rating: 5, tags: ['security', 'auth'] }),
|
|
makePlugin('plugin-b', { downloads: 50, rating: 3, type: 'agent', categories: ['community'], permissions: ['network'] }),
|
|
makePlugin('plugin-c', { downloads: 300, rating: 4, tags: ['perf'], verified: false, trustLevel: 'community' }),
|
|
],
|
|
categories: [{ id: 'official', name: 'Official', description: 'Official plugins', pluginCount: 2 }],
|
|
authors: [author],
|
|
totalPlugins: 3,
|
|
totalDownloads: 550,
|
|
totalAuthors: 1,
|
|
featured: ['plugin-a'],
|
|
trending: ['plugin-c'],
|
|
newest: ['plugin-b'],
|
|
official: ['plugin-a'],
|
|
compatibilityMatrix: [],
|
|
};
|
|
}
|
|
|
|
describe('Plugin Search', () => {
|
|
const registry = createMockPluginRegistry();
|
|
|
|
it('should return all plugins with no options', () => {
|
|
const result = searchPlugins(registry);
|
|
expect(result.total).toBe(3);
|
|
});
|
|
|
|
it('should filter by text query', () => {
|
|
const result = searchPlugins(registry, { query: 'plugin-a' });
|
|
expect(result.plugins).toHaveLength(1);
|
|
expect(result.plugins[0].id).toBe('plugin-a');
|
|
});
|
|
|
|
it('should filter by category', () => {
|
|
const result = searchPlugins(registry, { category: 'community' });
|
|
expect(result.plugins).toHaveLength(1);
|
|
expect(result.plugins[0].id).toBe('plugin-b');
|
|
});
|
|
|
|
it('should filter by type', () => {
|
|
const result = searchPlugins(registry, { type: 'agent' });
|
|
expect(result.plugins).toHaveLength(1);
|
|
expect(result.plugins[0].id).toBe('plugin-b');
|
|
});
|
|
|
|
it('should filter by tags', () => {
|
|
const result = searchPlugins(registry, { tags: ['security'] });
|
|
expect(result.plugins).toHaveLength(1);
|
|
expect(result.plugins[0].id).toBe('plugin-a');
|
|
});
|
|
|
|
it('should filter by minRating', () => {
|
|
const result = searchPlugins(registry, { minRating: 4 });
|
|
expect(result.plugins.every(p => p.rating >= 4)).toBe(true);
|
|
});
|
|
|
|
it('should filter by minDownloads', () => {
|
|
const result = searchPlugins(registry, { minDownloads: 100 });
|
|
expect(result.plugins.every(p => p.downloads >= 100)).toBe(true);
|
|
});
|
|
|
|
it('should filter by verified', () => {
|
|
const result = searchPlugins(registry, { verified: true });
|
|
expect(result.plugins.every(p => p.verified)).toBe(true);
|
|
});
|
|
|
|
it('should filter by trustLevel', () => {
|
|
const result = searchPlugins(registry, { trustLevel: 'official' });
|
|
expect(result.plugins.every(p => p.trustLevel === 'official')).toBe(true);
|
|
});
|
|
|
|
it('should filter by permissions', () => {
|
|
const result = searchPlugins(registry, { permissions: ['network'] });
|
|
expect(result.plugins).toHaveLength(1);
|
|
});
|
|
|
|
it('should sort by name ascending', () => {
|
|
const result = searchPlugins(registry, { sortBy: 'name', sortOrder: 'asc' });
|
|
expect(result.plugins[0].id).toBe('plugin-a');
|
|
});
|
|
|
|
it('should sort by rating descending', () => {
|
|
const result = searchPlugins(registry, { sortBy: 'rating', sortOrder: 'desc' });
|
|
expect(result.plugins[0].rating).toBeGreaterThanOrEqual(result.plugins[1].rating);
|
|
});
|
|
|
|
it('should paginate results', () => {
|
|
const result = searchPlugins(registry, { limit: 2, offset: 0 });
|
|
expect(result.plugins).toHaveLength(2);
|
|
expect(result.hasMore).toBe(true);
|
|
});
|
|
|
|
it('getPluginSearchSuggestions returns suggestions', () => {
|
|
const suggestions = getPluginSearchSuggestions(registry, 'sec');
|
|
expect(suggestions.length).toBeGreaterThan(0);
|
|
expect(suggestions.some(s => s.includes('sec'))).toBe(true);
|
|
});
|
|
|
|
it('getPluginTagCloud returns tag counts', () => {
|
|
const cloud = getPluginTagCloud(registry);
|
|
expect(cloud instanceof Map).toBe(true);
|
|
expect(cloud.get('test')).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('getPluginCategoryStats returns category counts', () => {
|
|
const stats = getPluginCategoryStats(registry);
|
|
expect(stats.get('official')).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('findSimilarPlugins finds related plugins', () => {
|
|
const similar = findSimilarPlugins(registry, 'plugin-a');
|
|
// All have overlapping tags with plugin-a
|
|
expect(similar.length).toBeGreaterThan(0);
|
|
expect(similar.every(p => p.id !== 'plugin-a')).toBe(true);
|
|
});
|
|
|
|
it('findSimilarPlugins returns empty for unknown plugin', () => {
|
|
const similar = findSimilarPlugins(registry, 'nonexistent');
|
|
expect(similar).toHaveLength(0);
|
|
});
|
|
|
|
it('getFeaturedPlugins returns featured', () => {
|
|
const featured = getFeaturedPlugins(registry);
|
|
expect(featured).toHaveLength(1);
|
|
expect(featured[0].id).toBe('plugin-a');
|
|
});
|
|
|
|
it('getTrendingPlugins returns trending', () => {
|
|
const trending = getTrendingPlugins(registry);
|
|
expect(trending).toHaveLength(1);
|
|
});
|
|
|
|
it('getNewestPlugins returns newest', () => {
|
|
const newest = getNewestPlugins(registry);
|
|
expect(newest).toHaveLength(1);
|
|
});
|
|
|
|
it('getOfficialPlugins returns official', () => {
|
|
const official = getOfficialPlugins(registry);
|
|
expect(official).toHaveLength(1);
|
|
});
|
|
|
|
it('getPluginsByPermission filters by permission', () => {
|
|
const plugins = getPluginsByPermission(registry, 'memory');
|
|
expect(plugins.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 4. Plugin Store (high-level API)
|
|
// ============================================================================
|
|
|
|
import { PluginStore, createPluginStore } from '../src/plugins/store/index.js';
|
|
|
|
describe('PluginStore High-Level API', () => {
|
|
it('should not be initialized by default', () => {
|
|
const store = createPluginStore();
|
|
expect(store.isInitialized()).toBe(false);
|
|
});
|
|
|
|
it('should return empty results when not initialized', () => {
|
|
const store = new PluginStore();
|
|
const result = store.search();
|
|
expect(result.total).toBe(0);
|
|
expect(result.plugins).toHaveLength(0);
|
|
});
|
|
|
|
it('should return empty featured when not initialized', () => {
|
|
const store = new PluginStore();
|
|
expect(store.getFeatured()).toEqual([]);
|
|
expect(store.getOfficial()).toEqual([]);
|
|
expect(store.getTrending()).toEqual([]);
|
|
expect(store.getNewest()).toEqual([]);
|
|
});
|
|
|
|
it('should return undefined for getPlugin when not initialized', () => {
|
|
const store = new PluginStore();
|
|
expect(store.getPlugin('any')).toBeUndefined();
|
|
});
|
|
|
|
it('should return empty similar when not initialized', () => {
|
|
const store = new PluginStore();
|
|
expect(store.getSimilarPlugins('any')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 5. CFP Serialization
|
|
// ============================================================================
|
|
|
|
import {
|
|
createCFP,
|
|
serializeToJson,
|
|
serializeToBuffer,
|
|
deserializeCFP,
|
|
validateCFP,
|
|
getFileExtension,
|
|
detectFormat,
|
|
} from '../src/transfer/serialization/cfp.js';
|
|
import type { CFPFormat, PatternCollection, SerializationFormat } from '../src/transfer/types.js';
|
|
|
|
function createMockPatterns(): PatternCollection {
|
|
return {
|
|
routing: [
|
|
{ id: 'r1', trigger: 'auth', action: 'route-to-auth', confidence: 0.9, usageCount: 100, successRate: 0.95 },
|
|
{ id: 'r2', trigger: 'test', action: 'route-to-test', confidence: 0.8, usageCount: 50, successRate: 0.85 },
|
|
],
|
|
complexity: [
|
|
{ id: 'c1', pattern: 'nested-if', complexity: 5, tokens: 100, frequency: 20 },
|
|
],
|
|
coverage: [
|
|
{ id: 'cov1', domain: 'auth', coverage: 0.85, gaps: ['2fa'] },
|
|
],
|
|
trajectory: [
|
|
{ id: 't1', steps: ['analyze', 'plan', 'code'], outcome: 'success', duration: 5000, learnings: ['use TDD'] },
|
|
],
|
|
custom: [
|
|
{ id: 'cu1', type: 'framework-pattern', data: { name: 'react' }, metadata: { version: '18' } },
|
|
],
|
|
};
|
|
}
|
|
|
|
describe('CFP Serialization', () => {
|
|
it('should create a valid CFP document', () => {
|
|
const cfp = createCFP({
|
|
name: 'test-patterns',
|
|
description: 'Test patterns',
|
|
patterns: createMockPatterns(),
|
|
});
|
|
expect(cfp.magic).toBe('CFP1');
|
|
expect(cfp.metadata.name).toBe('test-patterns');
|
|
expect(cfp.statistics.totalPatterns).toBe(6);
|
|
});
|
|
|
|
it('should calculate average confidence from routing patterns', () => {
|
|
const cfp = createCFP({
|
|
name: 'test',
|
|
description: 'test',
|
|
patterns: createMockPatterns(),
|
|
});
|
|
expect(cfp.statistics.avgConfidence).toBeCloseTo(0.85, 1);
|
|
});
|
|
|
|
it('should round-trip JSON serialize/deserialize', () => {
|
|
const cfp = createCFP({
|
|
name: 'roundtrip',
|
|
description: 'Roundtrip test',
|
|
patterns: createMockPatterns(),
|
|
tags: ['test'],
|
|
license: 'Apache-2.0',
|
|
});
|
|
const json = serializeToJson(cfp);
|
|
const deserialized = deserializeCFP(json);
|
|
expect(deserialized.magic).toBe('CFP1');
|
|
expect(deserialized.metadata.name).toBe('roundtrip');
|
|
expect(deserialized.patterns.routing).toHaveLength(2);
|
|
});
|
|
|
|
it('should set checksum on serialization', () => {
|
|
const cfp = createCFP({
|
|
name: 'checksum-test',
|
|
description: 'test',
|
|
patterns: createMockPatterns(),
|
|
});
|
|
expect(cfp.anonymization.checksum).toBe('');
|
|
serializeToJson(cfp);
|
|
expect(cfp.anonymization.checksum).not.toBe('');
|
|
expect(cfp.anonymization.checksum.length).toBe(64); // SHA256 hex
|
|
});
|
|
|
|
it('should serialize to Buffer for JSON format', () => {
|
|
const cfp = createCFP({ name: 'buf', description: 'test', patterns: createMockPatterns() });
|
|
const buf = serializeToBuffer(cfp, 'json');
|
|
expect(Buffer.isBuffer(buf)).toBe(true);
|
|
expect(buf.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should throw error for unsupported formats (fixed C-5)', () => {
|
|
const cfp = createCFP({ name: 'buf', description: 'test', patterns: createMockPatterns() });
|
|
// Unsupported formats now throw instead of silently falling back (ADR-061 C-5 fix)
|
|
expect(() => serializeToBuffer(cfp, 'cbor')).toThrow("not implemented");
|
|
});
|
|
|
|
it('should deserialize from Buffer', () => {
|
|
const cfp = createCFP({ name: 'buf-deser', description: 'test', patterns: createMockPatterns() });
|
|
const buf = serializeToBuffer(cfp, 'json');
|
|
const deserialized = deserializeCFP(buf);
|
|
expect(deserialized.metadata.name).toBe('buf-deser');
|
|
});
|
|
|
|
it('should reject invalid magic bytes', () => {
|
|
expect(() => deserializeCFP('{"magic": "INVALID"}')).toThrow('Invalid CFP format');
|
|
});
|
|
|
|
it('should validate a valid CFP', () => {
|
|
const cfp = createCFP({ name: 'valid', description: 'test', patterns: createMockPatterns() });
|
|
const result = validateCFP(cfp);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should invalidate CFP with wrong magic', () => {
|
|
const cfp = createCFP({ name: 'bad', description: 'test', patterns: createMockPatterns() });
|
|
(cfp as any).magic = 'BAD1';
|
|
const result = validateCFP(cfp);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Invalid magic bytes: BAD1');
|
|
});
|
|
|
|
it('should invalidate CFP with missing version', () => {
|
|
const cfp = createCFP({ name: 'no-ver', description: 'test', patterns: createMockPatterns() });
|
|
(cfp as any).version = '';
|
|
const result = validateCFP(cfp);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should invalidate CFP with missing metadata.id', () => {
|
|
const cfp = createCFP({ name: 'no-id', description: 'test', patterns: createMockPatterns() });
|
|
(cfp.metadata as any).id = '';
|
|
const result = validateCFP(cfp);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('getFileExtension returns correct extensions', () => {
|
|
expect(getFileExtension('json')).toBe('.cfp.json');
|
|
expect(getFileExtension('cbor')).toBe('.cfp');
|
|
expect(getFileExtension('cbor.gz')).toBe('.cfp.gz');
|
|
expect(getFileExtension('cbor.zstd')).toBe('.cfp.zst');
|
|
expect(getFileExtension('msgpack')).toBe('.cfp.mp');
|
|
});
|
|
|
|
it('detectFormat detects format from file path', () => {
|
|
expect(detectFormat('patterns.cfp.json')).toBe('json');
|
|
expect(detectFormat('patterns.cfp.gz')).toBe('cbor.gz');
|
|
expect(detectFormat('patterns.cfp.zst')).toBe('cbor.zstd');
|
|
expect(detectFormat('patterns.cfp.mp')).toBe('msgpack');
|
|
expect(detectFormat('patterns.cfp')).toBe('cbor');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 6. Anonymization
|
|
// ============================================================================
|
|
|
|
import {
|
|
detectPII,
|
|
redactPII,
|
|
anonymizeCFP,
|
|
scanCFPForPII,
|
|
} from '../src/transfer/anonymization/index.js';
|
|
|
|
describe('Anonymization', () => {
|
|
it('should detect email PII', () => {
|
|
const result = detectPII('Contact user@example.com for support');
|
|
expect(result.found).toBe(true);
|
|
expect(result.types.email).toBe(1);
|
|
});
|
|
|
|
it('should detect phone PII', () => {
|
|
const result = detectPII('Call 555-123-4567 for help');
|
|
expect(result.found).toBe(true);
|
|
expect(result.types.phone).toBe(1);
|
|
});
|
|
|
|
it('should detect IPv4 PII', () => {
|
|
const result = detectPII('Server at 192.168.1.100');
|
|
expect(result.found).toBe(true);
|
|
expect(result.types.ipv4).toBe(1);
|
|
});
|
|
|
|
it('should detect API key PII', () => {
|
|
const result = detectPII('Use sk-abcdefghijklmnopqrstuvwxyz123456');
|
|
expect(result.found).toBe(true);
|
|
expect(result.types.apiKey).toBe(1);
|
|
});
|
|
|
|
it('should detect JWT PII', () => {
|
|
const result = detectPII('Token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc_def-ghi');
|
|
expect(result.found).toBe(true);
|
|
expect(result.types.jwt).toBe(1);
|
|
});
|
|
|
|
it('should detect home path PII', () => {
|
|
const result = detectPII('File at /Users/johndoe/Documents');
|
|
expect(result.found).toBe(true);
|
|
expect(result.types.homePath).toBe(1);
|
|
});
|
|
|
|
it('should not find PII in clean content', () => {
|
|
const result = detectPII('Just a normal description with no personal data');
|
|
expect(result.found).toBe(false);
|
|
expect(result.count).toBe(0);
|
|
});
|
|
|
|
it('should redact email', () => {
|
|
const redacted = redactPII('Contact user@example.com');
|
|
expect(redacted).not.toContain('user@example.com');
|
|
expect(redacted).toContain('@example.com');
|
|
});
|
|
|
|
it('should redact IP addresses', () => {
|
|
const redacted = redactPII('Server: 192.168.1.1');
|
|
expect(redacted).toContain('0.0.0.0');
|
|
});
|
|
|
|
it('should redact API keys', () => {
|
|
const redacted = redactPII('Key: sk-abcdefghijklmnopqrstuvwxyz123456');
|
|
expect(redacted).toContain('[REDACTED_API_KEY]');
|
|
});
|
|
|
|
it('should apply minimal anonymization (remove author name)', () => {
|
|
const cfp = createCFP({
|
|
name: 'anon-test',
|
|
description: 'test',
|
|
patterns: createMockPatterns(),
|
|
author: { id: 'auth1', displayName: 'John Doe' },
|
|
});
|
|
const { cfp: anonymized, transforms } = anonymizeCFP(cfp, 'minimal');
|
|
expect(anonymized.metadata.author?.displayName).toBeUndefined();
|
|
expect(transforms).toContain('author-name-removed');
|
|
});
|
|
|
|
it('should apply standard anonymization (PII redacted)', () => {
|
|
const patterns = createMockPatterns();
|
|
patterns.routing[0].context = { note: 'Contact user@example.com' };
|
|
const cfp = createCFP({ name: 'std', description: 'test', patterns });
|
|
const { cfp: anonymized, transforms } = anonymizeCFP(cfp, 'standard');
|
|
expect(transforms).toContain('pii-redacted');
|
|
expect(transforms).toContain('timestamps-generalized');
|
|
expect(anonymized.anonymization.piiRedacted).toBe(true);
|
|
});
|
|
|
|
it('should apply strict anonymization (hash IDs, remove context)', () => {
|
|
const cfp = createCFP({ name: 'strict', description: 'test', patterns: createMockPatterns() });
|
|
const { cfp: anonymized, transforms } = anonymizeCFP(cfp, 'strict');
|
|
expect(transforms).toContain('ids-hashed');
|
|
expect(transforms).toContain('context-removed');
|
|
expect(transforms).toContain('paths-stripped');
|
|
// IDs should be hashed
|
|
expect(anonymized.patterns.routing[0].id).toMatch(/^pattern_/);
|
|
});
|
|
|
|
it('should apply paranoid anonymization (differential privacy, remove learnings)', () => {
|
|
const cfp = createCFP({ name: 'paranoid', description: 'test', patterns: createMockPatterns() });
|
|
const { cfp: anonymized, transforms } = anonymizeCFP(cfp, 'paranoid');
|
|
expect(transforms).toContain('differential-privacy-noise');
|
|
expect(transforms).toContain('learnings-removed');
|
|
// Trajectory learnings should be empty
|
|
expect(anonymized.patterns.trajectory[0].learnings).toHaveLength(0);
|
|
});
|
|
|
|
it('scanCFPForPII detects PII in patterns', () => {
|
|
const patterns = createMockPatterns();
|
|
patterns.routing[0].context = { email: 'test@pii.com' };
|
|
const cfp = createCFP({ name: 'scan', description: 'test', patterns });
|
|
const result = scanCFPForPII(cfp);
|
|
expect(result.found).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 7. IPFS Client
|
|
// ============================================================================
|
|
|
|
import {
|
|
isValidCID,
|
|
isValidIPNS,
|
|
getGatewayUrl,
|
|
getGatewayUrls,
|
|
hashContent,
|
|
parseCID,
|
|
formatBytes,
|
|
IPFS_GATEWAYS,
|
|
IPNS_RESOLVERS,
|
|
} from '../src/transfer/ipfs/client.js';
|
|
|
|
describe('IPFS Client', () => {
|
|
it('should validate CIDv0', () => {
|
|
expect(isValidCID('QmXbfEAaR7D2Ujm4GAkbwcGZQMHqAMpwDoje4583uNP834')).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid CID', () => {
|
|
expect(isValidCID('invalid')).toBe(false);
|
|
expect(isValidCID('')).toBe(false);
|
|
});
|
|
|
|
it('should validate IPNS domain', () => {
|
|
expect(isValidIPNS('example.com')).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid IPNS', () => {
|
|
expect(isValidIPNS('')).toBe(false);
|
|
});
|
|
|
|
it('should generate gateway URL', () => {
|
|
const url = getGatewayUrl('QmTest', 'https://ipfs.io');
|
|
expect(url).toBe('https://ipfs.io/ipfs/QmTest');
|
|
});
|
|
|
|
it('should generate multiple gateway URLs', () => {
|
|
const urls = getGatewayUrls('QmTest');
|
|
expect(urls.length).toBe(IPFS_GATEWAYS.length);
|
|
expect(urls[0]).toContain('/ipfs/QmTest');
|
|
});
|
|
|
|
it('should hash content consistently', () => {
|
|
const hash1 = hashContent('hello world');
|
|
const hash2 = hashContent('hello world');
|
|
expect(hash1).toBe(hash2);
|
|
expect(hash1.length).toBe(64); // SHA256 hex
|
|
});
|
|
|
|
it('should hash Buffer content', () => {
|
|
const hash = hashContent(Buffer.from('test'));
|
|
expect(hash.length).toBe(64);
|
|
});
|
|
|
|
it('should parse CIDv0', () => {
|
|
const parsed = parseCID('QmXbfEAaR7D2Ujm4GAkbwcGZQMHqAMpwDoje4583uNP834');
|
|
expect(parsed).not.toBeNull();
|
|
expect(parsed!.version).toBe(0);
|
|
expect(parsed!.codec).toBe('dag-pb');
|
|
});
|
|
|
|
it('should return null for invalid CID parse', () => {
|
|
expect(parseCID('invalid')).toBeNull();
|
|
});
|
|
|
|
it('should format bytes correctly', () => {
|
|
expect(formatBytes(0)).toBe('0 B');
|
|
expect(formatBytes(1024)).toBe('1 KB');
|
|
expect(formatBytes(1048576)).toBe('1 MB');
|
|
});
|
|
|
|
it('IPFS_GATEWAYS should include known gateways', () => {
|
|
expect(IPFS_GATEWAYS.length).toBeGreaterThanOrEqual(3);
|
|
expect(IPFS_GATEWAYS).toContain('https://ipfs.io');
|
|
});
|
|
|
|
it('IPNS_RESOLVERS should have resolvers', () => {
|
|
expect(IPNS_RESOLVERS.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 8. IPFS Upload
|
|
// ============================================================================
|
|
|
|
import {
|
|
getGatewayURL,
|
|
getIPNSURL,
|
|
hasIPFSCredentials,
|
|
getIPFSServiceStatus,
|
|
} from '../src/transfer/ipfs/upload.js';
|
|
|
|
describe('IPFS Upload Helpers', () => {
|
|
it('getGatewayURL creates correct URL', () => {
|
|
expect(getGatewayURL('QmTest')).toBe('https://w3s.link/ipfs/QmTest');
|
|
expect(getGatewayURL('QmTest', 'https://ipfs.io')).toBe('https://ipfs.io/ipfs/QmTest');
|
|
});
|
|
|
|
it('getIPNSURL creates correct URL', () => {
|
|
expect(getIPNSURL('example.com')).toBe('https://w3s.link/ipns/example.com');
|
|
});
|
|
|
|
it('hasIPFSCredentials returns false without env vars', () => {
|
|
const origWeb3 = process.env.WEB3_STORAGE_TOKEN;
|
|
const origPinata = process.env.PINATA_API_KEY;
|
|
const origIpfs = process.env.IPFS_API_URL;
|
|
delete process.env.WEB3_STORAGE_TOKEN;
|
|
delete process.env.W3_TOKEN;
|
|
delete process.env.IPFS_TOKEN;
|
|
delete process.env.PINATA_API_KEY;
|
|
delete process.env.IPFS_API_URL;
|
|
expect(hasIPFSCredentials()).toBe(false);
|
|
// Restore
|
|
if (origWeb3) process.env.WEB3_STORAGE_TOKEN = origWeb3;
|
|
if (origPinata) process.env.PINATA_API_KEY = origPinata;
|
|
if (origIpfs) process.env.IPFS_API_URL = origIpfs;
|
|
});
|
|
|
|
it('getIPFSServiceStatus returns demo when no credentials', () => {
|
|
const origWeb3 = process.env.WEB3_STORAGE_TOKEN;
|
|
const origPinata = process.env.PINATA_API_KEY;
|
|
const origIpfs = process.env.IPFS_API_URL;
|
|
delete process.env.WEB3_STORAGE_TOKEN;
|
|
delete process.env.W3_TOKEN;
|
|
delete process.env.IPFS_TOKEN;
|
|
delete process.env.PINATA_API_KEY;
|
|
delete process.env.IPFS_API_URL;
|
|
const status = getIPFSServiceStatus();
|
|
expect(status.service).toBe('demo');
|
|
expect(status.configured).toBe(false);
|
|
// Restore
|
|
if (origWeb3) process.env.WEB3_STORAGE_TOKEN = origWeb3;
|
|
if (origPinata) process.env.PINATA_API_KEY = origPinata;
|
|
if (origIpfs) process.env.IPFS_API_URL = origIpfs;
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 9. Config Adapter
|
|
// ============================================================================
|
|
|
|
import { systemConfigToV3Config, v3ConfigToSystemConfig } from '../src/config-adapter.js';
|
|
|
|
describe('Config Adapter', () => {
|
|
it('should convert minimal SystemConfig to V3Config', () => {
|
|
const v3 = systemConfigToV3Config({} as any);
|
|
expect(v3.version).toBe('3.0.0');
|
|
expect(v3.swarm.topology).toBe('hierarchical');
|
|
expect(v3.memory.backend).toBe('hybrid');
|
|
});
|
|
|
|
it('should normalize topology adaptive -> hybrid', () => {
|
|
const v3 = systemConfigToV3Config({ swarm: { topology: 'adaptive' } } as any);
|
|
expect(v3.swarm.topology).toBe('hybrid');
|
|
});
|
|
|
|
it('should handle hierarchical-mesh topology', () => {
|
|
const v3 = systemConfigToV3Config({ swarm: { topology: 'hierarchical-mesh' } } as any);
|
|
expect(v3.swarm.topology).toBe('hierarchical-mesh');
|
|
});
|
|
|
|
it('should normalize memory backend redis -> memory', () => {
|
|
const v3 = systemConfigToV3Config({ memory: { type: 'redis' } } as any);
|
|
expect(v3.memory.backend).toBe('memory');
|
|
});
|
|
|
|
it('should convert V3Config back to SystemConfig', () => {
|
|
const v3 = systemConfigToV3Config({} as any);
|
|
const sys = v3ConfigToSystemConfig(v3);
|
|
expect(sys.swarm?.topology).toBe('hierarchical');
|
|
expect(sys.mcp?.name).toBe('ruflo');
|
|
});
|
|
|
|
it('should denormalize hybrid topology to hierarchical-mesh', () => {
|
|
const v3 = systemConfigToV3Config({ swarm: { topology: 'adaptive' } } as any);
|
|
expect(v3.swarm.topology).toBe('hybrid');
|
|
const sys = v3ConfigToSystemConfig(v3);
|
|
expect(sys.swarm?.topology).toBe('hierarchical-mesh');
|
|
});
|
|
|
|
it('should preserve maxAgents in round trip', () => {
|
|
const v3 = systemConfigToV3Config({ swarm: { maxAgents: 12 } } as any);
|
|
expect(v3.swarm.maxAgents).toBe(12);
|
|
const sys = v3ConfigToSystemConfig(v3);
|
|
expect(sys.swarm?.maxAgents).toBe(12);
|
|
});
|
|
|
|
it('should set HNSW from agentdb config', () => {
|
|
const v3 = systemConfigToV3Config({
|
|
memory: { agentdb: { indexType: 'hnsw', dimensions: 384 } },
|
|
} as any);
|
|
expect(v3.memory.enableHNSW).toBe(true);
|
|
expect(v3.memory.vectorDimension).toBe(384);
|
|
});
|
|
|
|
it('should preserve MCP port in round trip', () => {
|
|
const v3 = systemConfigToV3Config({
|
|
mcp: { transport: { port: 4000 } },
|
|
} as any);
|
|
expect(v3.mcp.serverPort).toBe(4000);
|
|
const sys = v3ConfigToSystemConfig(v3);
|
|
expect(sys.mcp?.transport?.port).toBe(4000);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 10. Production - Circuit Breaker
|
|
// ============================================================================
|
|
|
|
import { CircuitBreaker, getCircuitBreaker, resetAllCircuits } from '../src/production/circuit-breaker.js';
|
|
|
|
describe('Circuit Breaker', () => {
|
|
let breaker: CircuitBreaker;
|
|
|
|
beforeEach(() => {
|
|
breaker = new CircuitBreaker({ failureThreshold: 3, resetTimeoutMs: 100, successThreshold: 2 });
|
|
});
|
|
|
|
it('should start in closed state', () => {
|
|
expect(breaker.getState()).toBe('closed');
|
|
});
|
|
|
|
it('should allow requests in closed state', () => {
|
|
expect(breaker.isAllowed()).toBe(true);
|
|
});
|
|
|
|
it('should open after reaching failure threshold', () => {
|
|
breaker.recordFailure();
|
|
breaker.recordFailure();
|
|
breaker.recordFailure();
|
|
expect(breaker.getState()).toBe('open');
|
|
});
|
|
|
|
it('should reject requests in open state', () => {
|
|
for (let i = 0; i < 3; i++) breaker.recordFailure();
|
|
expect(breaker.isAllowed()).toBe(false);
|
|
});
|
|
|
|
it('should transition to half-open after timeout', async () => {
|
|
for (let i = 0; i < 3; i++) breaker.recordFailure();
|
|
expect(breaker.getState()).toBe('open');
|
|
await new Promise(r => setTimeout(r, 150));
|
|
expect(breaker.getState()).toBe('half-open');
|
|
});
|
|
|
|
it('should close after enough successes in half-open', async () => {
|
|
for (let i = 0; i < 3; i++) breaker.recordFailure();
|
|
await new Promise(r => setTimeout(r, 150));
|
|
expect(breaker.getState()).toBe('half-open');
|
|
breaker.recordSuccess();
|
|
breaker.recordSuccess();
|
|
expect(breaker.getState()).toBe('closed');
|
|
});
|
|
|
|
it('should go back to open on failure in half-open', async () => {
|
|
for (let i = 0; i < 3; i++) breaker.recordFailure();
|
|
await new Promise(r => setTimeout(r, 150));
|
|
// Must trigger the state transition check first
|
|
expect(breaker.getState()).toBe('half-open');
|
|
breaker.recordFailure();
|
|
expect(breaker.getState()).toBe('open');
|
|
});
|
|
|
|
it('execute should run function in closed state', async () => {
|
|
const result = await breaker.execute(async () => 42);
|
|
expect(result).toBe(42);
|
|
});
|
|
|
|
it('execute should throw in open state', async () => {
|
|
for (let i = 0; i < 3; i++) breaker.recordFailure();
|
|
await expect(breaker.execute(async () => 42)).rejects.toThrow('Circuit breaker is open');
|
|
});
|
|
|
|
it('should track stats correctly', () => {
|
|
breaker.recordSuccess();
|
|
breaker.recordFailure();
|
|
const stats = breaker.getStats();
|
|
expect(stats.totalSuccesses).toBe(1);
|
|
expect(stats.totalFailures).toBe(1);
|
|
});
|
|
|
|
it('getFailureRate returns correct rate', () => {
|
|
breaker.recordSuccess();
|
|
breaker.recordFailure();
|
|
// Execute increments totalRequests, record does not
|
|
// But getFailureRate uses totalFailures / totalRequests
|
|
expect(breaker.getFailureRate()).toBe(0); // totalRequests is 0 when not using execute()
|
|
});
|
|
|
|
it('reset clears all state', () => {
|
|
breaker.recordFailure();
|
|
breaker.recordFailure();
|
|
breaker.reset();
|
|
expect(breaker.getState()).toBe('closed');
|
|
expect(breaker.getStats().totalFailures).toBe(0);
|
|
});
|
|
|
|
it('manual open/close works', () => {
|
|
breaker.open();
|
|
expect(breaker.getState()).toBe('open');
|
|
breaker.close();
|
|
expect(breaker.getState()).toBe('closed');
|
|
});
|
|
|
|
it('getCircuitBreaker returns named breakers', () => {
|
|
resetAllCircuits();
|
|
const b1 = getCircuitBreaker('test-service');
|
|
const b2 = getCircuitBreaker('test-service');
|
|
expect(b1).toBe(b2);
|
|
resetAllCircuits();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 11. Production - Rate Limiter
|
|
// ============================================================================
|
|
|
|
import { RateLimiter, createRateLimiter } from '../src/production/rate-limiter.js';
|
|
|
|
describe('Rate Limiter', () => {
|
|
it('should allow requests below limit', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 5, windowMs: 1000 });
|
|
const result = limiter.check('test-op');
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.remaining).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should block when limit exceeded', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 2, windowMs: 60000, burstMultiplier: 1 });
|
|
limiter.check('op');
|
|
limiter.check('op');
|
|
const result = limiter.check('op');
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.retryAfterMs).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should allow burst above limit', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 2, windowMs: 60000, burstMultiplier: 2 });
|
|
limiter.check('op');
|
|
limiter.check('op');
|
|
const result = limiter.check('op');
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it('should skip whitelisted operations', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 1, windowMs: 60000, whitelist: ['admin'] });
|
|
limiter.check('admin');
|
|
limiter.check('admin');
|
|
const result = limiter.check('admin');
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.remaining).toBe(Infinity);
|
|
});
|
|
|
|
it('should apply per-operation limits', () => {
|
|
const limiter = new RateLimiter({
|
|
maxRequests: 100,
|
|
windowMs: 60000,
|
|
operationLimits: { 'heavy-op': { maxRequests: 1, windowMs: 60000 } },
|
|
burstMultiplier: 1,
|
|
});
|
|
limiter.check('heavy-op');
|
|
const result = limiter.check('heavy-op');
|
|
expect(result.allowed).toBe(false);
|
|
});
|
|
|
|
it('getStatus reports current usage', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 10, windowMs: 60000 });
|
|
limiter.check('op');
|
|
const status = limiter.getStatus('op');
|
|
expect(status.current).toBe(1);
|
|
expect(status.limit).toBe(10);
|
|
});
|
|
|
|
it('reset clears specific key', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 10, windowMs: 60000 });
|
|
limiter.check('op');
|
|
limiter.reset('op');
|
|
const status = limiter.getStatus('op');
|
|
expect(status.current).toBe(0);
|
|
});
|
|
|
|
it('resetAll clears all', () => {
|
|
const limiter = new RateLimiter({ maxRequests: 10, windowMs: 60000 });
|
|
limiter.check('op1');
|
|
limiter.check('op2');
|
|
limiter.resetAll();
|
|
expect(limiter.getStats().totalBuckets).toBe(0);
|
|
});
|
|
|
|
it('createRateLimiter factory works', () => {
|
|
const limiter = createRateLimiter({ maxRequests: 5 });
|
|
expect(limiter).toBeInstanceOf(RateLimiter);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 12. Production - Retry
|
|
// ============================================================================
|
|
|
|
import { withRetry } from '../src/production/retry.js';
|
|
import type { RetryConfig } from '../src/production/retry.js';
|
|
|
|
describe('Retry', () => {
|
|
it('should succeed on first attempt', async () => {
|
|
const result = await withRetry(async () => 'ok', { maxAttempts: 3 });
|
|
expect(result.success).toBe(true);
|
|
expect(result.result).toBe('ok');
|
|
expect(result.attempts).toBe(1);
|
|
});
|
|
|
|
it('should retry on failure', async () => {
|
|
let attempt = 0;
|
|
const result = await withRetry(
|
|
async () => {
|
|
attempt++;
|
|
if (attempt < 3) throw new Error('fail');
|
|
return 'done';
|
|
},
|
|
{ maxAttempts: 5, initialDelayMs: 1, maxDelayMs: 10, jitter: 0 }
|
|
);
|
|
expect(result.success).toBe(true);
|
|
expect(result.attempts).toBe(3);
|
|
});
|
|
|
|
it('should fail after max attempts', async () => {
|
|
const result = await withRetry(
|
|
async () => { throw new Error('always fail'); },
|
|
{ maxAttempts: 2, initialDelayMs: 1, maxDelayMs: 10, jitter: 0 }
|
|
);
|
|
expect(result.success).toBe(false);
|
|
expect(result.attempts).toBe(2);
|
|
expect(result.error?.message).toBe('always fail');
|
|
});
|
|
|
|
it('should not retry non-retryable errors', async () => {
|
|
const result = await withRetry(
|
|
async () => { throw new Error('validation error'); },
|
|
{ maxAttempts: 5, initialDelayMs: 1, nonRetryableErrors: ['validation'] }
|
|
);
|
|
expect(result.success).toBe(false);
|
|
expect(result.attempts).toBe(1);
|
|
});
|
|
|
|
it('should call onRetry callback', async () => {
|
|
const retries: number[] = [];
|
|
let attempt = 0;
|
|
await withRetry(
|
|
async () => {
|
|
attempt++;
|
|
if (attempt < 3) throw new Error('fail');
|
|
return 'ok';
|
|
},
|
|
{
|
|
maxAttempts: 5,
|
|
initialDelayMs: 1,
|
|
jitter: 0,
|
|
onRetry: (_err, attempt) => retries.push(attempt),
|
|
}
|
|
);
|
|
expect(retries).toEqual([1, 2]);
|
|
});
|
|
|
|
it('should record retry history', async () => {
|
|
let attempt = 0;
|
|
const result = await withRetry(
|
|
async () => {
|
|
attempt++;
|
|
if (attempt < 2) throw new Error('retry me');
|
|
return 'ok';
|
|
},
|
|
{ maxAttempts: 3, initialDelayMs: 1, jitter: 0 }
|
|
);
|
|
expect(result.retryHistory).toHaveLength(1);
|
|
expect(result.retryHistory[0].error).toBe('retry me');
|
|
});
|
|
|
|
it('should use custom shouldRetry function', async () => {
|
|
const result = await withRetry(
|
|
async () => { throw new Error('custom'); },
|
|
{
|
|
maxAttempts: 5,
|
|
initialDelayMs: 1,
|
|
shouldRetry: (_err, attempt) => attempt < 2, // Only retry once
|
|
}
|
|
);
|
|
expect(result.attempts).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 13. Production - Error Handler
|
|
// ============================================================================
|
|
|
|
import { ErrorHandler } from '../src/production/error-handler.js';
|
|
|
|
describe('Error Handler', () => {
|
|
let handler: ErrorHandler;
|
|
|
|
beforeEach(() => {
|
|
handler = new ErrorHandler({ includeStack: false, sanitize: true });
|
|
});
|
|
|
|
it('should classify validation errors', () => {
|
|
expect(handler.classifyError(new Error('Invalid input'))).toBe('validation');
|
|
});
|
|
|
|
it('should classify timeout errors', () => {
|
|
expect(handler.classifyError(new Error('Request timed out'))).toBe('timeout');
|
|
});
|
|
|
|
it('should classify authentication errors', () => {
|
|
expect(handler.classifyError(new Error('Unauthorized access'))).toBe('authentication');
|
|
});
|
|
|
|
it('should classify rate limit errors', () => {
|
|
expect(handler.classifyError('Too many requests')).toBe('rate_limit');
|
|
});
|
|
|
|
it('should classify unknown errors', () => {
|
|
expect(handler.classifyError(new Error('something went wrong 12345'))).toBe('unknown');
|
|
});
|
|
|
|
it('should identify retryable categories', () => {
|
|
expect(handler.isRetryable('timeout')).toBe(true);
|
|
expect(handler.isRetryable('external_service')).toBe(true);
|
|
expect(handler.isRetryable('validation')).toBe(false);
|
|
expect(handler.isRetryable('authentication')).toBe(false);
|
|
});
|
|
|
|
it('should sanitize sensitive data', () => {
|
|
const sanitized = handler.sanitize({ password: 'secret123', name: 'test' });
|
|
expect(sanitized.password).toBe('[REDACTED]');
|
|
expect(sanitized.name).toBe('test');
|
|
});
|
|
|
|
it('should sanitize nested objects with sensitive keys', () => {
|
|
// Note: ErrorHandler.sanitize uses case-sensitive includes() on SENSITIVE_KEYS.
|
|
// 'password' matches lowercase, but 'apiKey' lowered to 'apikey' does not match
|
|
// 'apiKey' because includes() is case-sensitive. Using 'password' to test nesting.
|
|
const sanitized = handler.sanitize({
|
|
config: { password: 'secret123', host: 'localhost' },
|
|
});
|
|
expect((sanitized.config as any).password).toBe('[REDACTED]');
|
|
expect((sanitized.config as any).host).toBe('localhost');
|
|
});
|
|
|
|
it('should handle errors and return structured response', () => {
|
|
const result = handler.handle(new Error('Connection refused'));
|
|
expect(result.success).toBe(false);
|
|
expect(result.error.category).toBe('external_service');
|
|
expect(result.error.retryable).toBe(true);
|
|
});
|
|
|
|
it('should track error statistics', () => {
|
|
handler.handle(new Error('timeout'));
|
|
handler.handle(new Error('invalid'));
|
|
const stats = handler.getStats();
|
|
expect(stats.totalErrors).toBe(2);
|
|
expect(stats.byCategory).toHaveProperty('timeout');
|
|
});
|
|
|
|
it('should clear error log', () => {
|
|
handler.handle(new Error('test'));
|
|
handler.clearLog();
|
|
const stats = handler.getStats();
|
|
expect(stats.totalErrors).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 14. Production - Monitoring
|
|
// ============================================================================
|
|
|
|
import { MonitoringHooks, createMonitor } from '../src/production/monitoring.js';
|
|
|
|
describe('Monitoring', () => {
|
|
let monitor: MonitoringHooks;
|
|
|
|
beforeEach(() => {
|
|
monitor = new MonitoringHooks({ samplingRate: 1.0 });
|
|
});
|
|
|
|
it('should record counter metrics', () => {
|
|
monitor.counter('test_count', 1);
|
|
const metrics = monitor.getMetrics('test_count');
|
|
expect(metrics).toHaveLength(1);
|
|
expect(metrics[0].value).toBe(1);
|
|
});
|
|
|
|
it('should record gauge metrics', () => {
|
|
monitor.gauge('cpu_usage', 0.75);
|
|
const metrics = monitor.getMetrics('cpu_usage');
|
|
expect(metrics).toHaveLength(1);
|
|
expect(metrics[0].value).toBe(0.75);
|
|
});
|
|
|
|
it('should track requests', () => {
|
|
const end = monitor.startRequest('req-1');
|
|
end();
|
|
const perf = monitor.getPerformanceMetrics();
|
|
expect(perf.requestCount).toBe(1);
|
|
expect(perf.activeRequests).toBe(0);
|
|
});
|
|
|
|
it('should track errors', () => {
|
|
monitor.recordError(new Error('test'));
|
|
const perf = monitor.getPerformanceMetrics();
|
|
expect(perf.errorCount).toBe(1);
|
|
});
|
|
|
|
it('should calculate performance percentiles', () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
const end = monitor.startRequest(`req-${i}`);
|
|
end();
|
|
}
|
|
const perf = monitor.getPerformanceMetrics();
|
|
expect(perf.p50ResponseTimeMs).toBeGreaterThanOrEqual(0);
|
|
expect(perf.p95ResponseTimeMs).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should register and run health checks', async () => {
|
|
monitor.registerHealthCheck('db', async () => ({ healthy: true }));
|
|
monitor.registerHealthCheck('cache', async () => ({ healthy: false, message: 'down' }));
|
|
const status = await monitor.runHealthChecks();
|
|
expect(status.healthy).toBe(false);
|
|
expect(status.checks.db.status).toBe('healthy');
|
|
expect(status.checks.cache.status).toBe('unhealthy');
|
|
});
|
|
|
|
it('should generate alerts when threshold exceeded', () => {
|
|
const mon = new MonitoringHooks({
|
|
samplingRate: 1.0,
|
|
alertThresholds: { 'error_rate': { warning: 0.05, critical: 0.1 } },
|
|
});
|
|
mon.gauge('error_rate', 0.15);
|
|
const alerts = mon.getAlerts('critical');
|
|
expect(alerts).toHaveLength(1);
|
|
});
|
|
|
|
it('should acknowledge alerts', () => {
|
|
monitor = new MonitoringHooks({
|
|
samplingRate: 1.0,
|
|
alertThresholds: { 'high_val': { warning: 5, critical: 10 } },
|
|
});
|
|
monitor.gauge('high_val', 15);
|
|
const alerts = monitor.getAlerts();
|
|
expect(alerts.length).toBeGreaterThan(0);
|
|
const acknowledged = monitor.acknowledgeAlert(alerts[0].id);
|
|
expect(acknowledged).toBe(true);
|
|
expect(monitor.getAlerts()).toHaveLength(0);
|
|
});
|
|
|
|
it('should get metrics summary', () => {
|
|
monitor.counter('op_a', 1);
|
|
monitor.counter('op_a', 2);
|
|
monitor.counter('op_b', 5);
|
|
const summary = monitor.getMetricsSummary();
|
|
expect(summary.op_a.count).toBe(2);
|
|
expect(summary.op_a.avgValue).toBe(1.5);
|
|
expect(summary.op_b.lastValue).toBe(5);
|
|
});
|
|
|
|
it('reset clears all data', () => {
|
|
monitor.counter('x', 1);
|
|
monitor.recordError(new Error('test'));
|
|
monitor.reset();
|
|
expect(monitor.getPerformanceMetrics().requestCount).toBe(0);
|
|
expect(monitor.getPerformanceMetrics().errorCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 15. Update - Validator
|
|
// ============================================================================
|
|
|
|
import { validateUpdate, validateBulkUpdate } from '../src/update/validator.js';
|
|
|
|
describe('Update Validator', () => {
|
|
it('should validate compatible update', () => {
|
|
const result = validateUpdate(
|
|
'@claude-flow/cli', '3.0.0-alpha.50', '3.0.0-alpha.55', {}
|
|
);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should warn about major version bumps', () => {
|
|
const result = validateUpdate(
|
|
'@claude-flow/cli', '2.0.0', '3.0.0', {}
|
|
);
|
|
expect(result.warnings.length).toBeGreaterThan(0);
|
|
expect(result.warnings.some(w => w.includes('Major version'))).toBe(true);
|
|
});
|
|
|
|
it('should detect incompatible peer dependency', () => {
|
|
const result = validateUpdate(
|
|
'@claude-flow/cli', '3.0.0-alpha.50', '3.0.0-alpha.55',
|
|
{ '@claude-flow/embeddings': '2.0.0' }
|
|
);
|
|
// CLI requires embeddings >= 3.0.0-alpha.1
|
|
expect(result.valid).toBe(false);
|
|
expect(result.incompatibilities.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should handle unknown packages gracefully', () => {
|
|
const result = validateUpdate('unknown-package', '1.0.0', '2.0.0', {});
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('validateBulkUpdate checks all updates', () => {
|
|
const result = validateBulkUpdate(
|
|
[
|
|
{ package: '@claude-flow/cli', from: '3.0.0-alpha.50', to: '3.0.0-alpha.55' },
|
|
{ package: '@claude-flow/embeddings', from: '3.0.0-alpha.1', to: '3.0.0-alpha.5' },
|
|
],
|
|
{ '@claude-flow/cli': '3.0.0-alpha.50', '@claude-flow/embeddings': '3.0.0-alpha.1' }
|
|
);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 16. Update - Rate Limiter
|
|
// ============================================================================
|
|
|
|
import { shouldCheckForUpdates } from '../src/update/rate-limiter.js';
|
|
|
|
describe('Update Rate Limiter', () => {
|
|
it('should block in CI environment', () => {
|
|
const origCI = process.env.CI;
|
|
process.env.CI = 'true';
|
|
const result = shouldCheckForUpdates();
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain('CI');
|
|
if (origCI) process.env.CI = origCI; else delete process.env.CI;
|
|
});
|
|
|
|
it('should block when auto-update disabled', () => {
|
|
const origCI = process.env.CI;
|
|
const origAutoUpdate = process.env.CLAUDE_FLOW_AUTO_UPDATE;
|
|
delete process.env.CI;
|
|
delete process.env.CONTINUOUS_INTEGRATION;
|
|
process.env.CLAUDE_FLOW_AUTO_UPDATE = 'false';
|
|
const result = shouldCheckForUpdates();
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain('disabled');
|
|
if (origCI) process.env.CI = origCI;
|
|
if (origAutoUpdate) process.env.CLAUDE_FLOW_AUTO_UPDATE = origAutoUpdate;
|
|
else delete process.env.CLAUDE_FLOW_AUTO_UPDATE;
|
|
});
|
|
|
|
it('should allow when force update requested', () => {
|
|
const origCI = process.env.CI;
|
|
const origForce = process.env.CLAUDE_FLOW_FORCE_UPDATE;
|
|
delete process.env.CI;
|
|
delete process.env.CONTINUOUS_INTEGRATION;
|
|
delete process.env.CLAUDE_FLOW_AUTO_UPDATE;
|
|
process.env.CLAUDE_FLOW_FORCE_UPDATE = 'true';
|
|
const result = shouldCheckForUpdates();
|
|
expect(result.allowed).toBe(true);
|
|
if (origCI) process.env.CI = origCI;
|
|
if (origForce) process.env.CLAUDE_FLOW_FORCE_UPDATE = origForce;
|
|
else delete process.env.CLAUDE_FLOW_FORCE_UPDATE;
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 17. Benchmark Infrastructure
|
|
// ============================================================================
|
|
|
|
import { runBenchmark, formatBenchmarkResult } from '../src/benchmarks/pretrain/index.js';
|
|
import type { BenchmarkResult, BenchmarkConfig } from '../src/benchmarks/pretrain/index.js';
|
|
|
|
describe('Benchmark Infrastructure', () => {
|
|
it('should run a benchmark and return results', async () => {
|
|
const result = await runBenchmark(
|
|
'test-bench',
|
|
() => { /* no-op */ },
|
|
{ iterations: 5, warmupIterations: 1 }
|
|
);
|
|
expect(result.name).toBe('test-bench');
|
|
expect(result.iterations).toBe(5);
|
|
expect(result.meanMs).toBeGreaterThanOrEqual(0);
|
|
expect(result.opsPerSecond).toBeGreaterThan(0);
|
|
expect(result.targetMet).toBe(true); // No target = always met
|
|
});
|
|
|
|
it('should detect when target is not met', async () => {
|
|
const result = await runBenchmark(
|
|
'slow-bench',
|
|
async () => { await new Promise(r => setTimeout(r, 10)); },
|
|
{ iterations: 3, warmupIterations: 1, targetMs: 0.001 }
|
|
);
|
|
expect(result.targetMet).toBe(false);
|
|
});
|
|
|
|
it('should format benchmark result as string', () => {
|
|
const result: BenchmarkResult = {
|
|
name: 'test',
|
|
iterations: 100,
|
|
meanMs: 0.05,
|
|
medianMs: 0.04,
|
|
p95Ms: 0.08,
|
|
p99Ms: 0.1,
|
|
minMs: 0.01,
|
|
maxMs: 0.15,
|
|
stdDev: 0.02,
|
|
opsPerSecond: 20000,
|
|
targetMet: true,
|
|
targetMs: 0.1,
|
|
};
|
|
const formatted = formatBenchmarkResult(result);
|
|
expect(formatted).toContain('test');
|
|
expect(formatted).toContain('Mean:');
|
|
expect(formatted).toContain('Ops/s:');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 18. Transfer Types (completeness)
|
|
// ============================================================================
|
|
|
|
import type {
|
|
AnonymizationLevel,
|
|
SerializationFormat as SFormat,
|
|
PatternType,
|
|
ImportStrategy,
|
|
ConflictResolution,
|
|
TrustLevel,
|
|
PinningService,
|
|
PatternMetadata,
|
|
AnonymizedAuthor,
|
|
AnonymizationRecord,
|
|
RoutingPattern,
|
|
ComplexityPattern,
|
|
CoveragePattern,
|
|
TrajectoryPattern,
|
|
CustomPattern,
|
|
PatternStatistics,
|
|
PatternSignature,
|
|
IPFSMetadata,
|
|
ExportOptions,
|
|
ImportOptions,
|
|
ExportResult,
|
|
ImportResult,
|
|
IPFSConfig,
|
|
PIIDetectionResult,
|
|
VerificationResult,
|
|
} from '../src/transfer/types.js';
|
|
|
|
describe('Transfer Types Completeness', () => {
|
|
it('should have all AnonymizationLevel values', () => {
|
|
const levels: AnonymizationLevel[] = ['minimal', 'standard', 'strict', 'paranoid'];
|
|
expect(levels).toHaveLength(4);
|
|
});
|
|
|
|
it('should have all SerializationFormat values', () => {
|
|
const formats: SFormat[] = ['cbor', 'json', 'msgpack', 'cbor.gz', 'cbor.zstd'];
|
|
expect(formats).toHaveLength(5);
|
|
});
|
|
|
|
it('should have all PatternType values', () => {
|
|
const types: PatternType[] = ['routing', 'complexity', 'coverage', 'trajectory', 'custom'];
|
|
expect(types).toHaveLength(5);
|
|
});
|
|
|
|
it('should have all ImportStrategy values', () => {
|
|
const strats: ImportStrategy[] = ['replace', 'merge', 'append'];
|
|
expect(strats).toHaveLength(3);
|
|
});
|
|
|
|
it('should have all TrustLevel values', () => {
|
|
const levels: TrustLevel[] = ['official', 'verified', 'community', 'unverified', 'untrusted'];
|
|
expect(levels).toHaveLength(5);
|
|
});
|
|
|
|
it('should have all PinningService values', () => {
|
|
const services: PinningService[] = ['local', 'pinata', 'web3storage', 'infura', 'custom'];
|
|
expect(services).toHaveLength(5);
|
|
});
|
|
|
|
it('should construct valid CFPFormat', () => {
|
|
const cfp: CFPFormat = {
|
|
magic: 'CFP1',
|
|
version: '1.0.0',
|
|
createdAt: new Date().toISOString(),
|
|
generatedBy: 'test',
|
|
metadata: {
|
|
id: 'test-id',
|
|
tags: [],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
anonymization: {
|
|
level: 'minimal',
|
|
appliedTransforms: [],
|
|
piiRedacted: false,
|
|
pathsStripped: false,
|
|
timestampsGeneralized: false,
|
|
checksum: '',
|
|
},
|
|
patterns: { routing: [], complexity: [], coverage: [], trajectory: [], custom: [] },
|
|
statistics: { totalPatterns: 0, avgConfidence: 0, patternTypes: {}, timeRange: { start: '', end: '' } },
|
|
};
|
|
expect(cfp.magic).toBe('CFP1');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 19. Transfer Store Search
|
|
// ============================================================================
|
|
|
|
import {
|
|
searchPatterns,
|
|
getPatternById,
|
|
getPatternByName,
|
|
getPatternsByCategory,
|
|
getSearchSuggestions,
|
|
getTagCloud,
|
|
getCategoryStats,
|
|
} from '../src/transfer/store/search.js';
|
|
import type { PatternRegistry as TPatternRegistry, PatternEntry as TPatternEntry, PatternAuthor as TPatternAuthor } from '../src/transfer/store/types.js';
|
|
|
|
function createMockPatternRegistry(): TPatternRegistry {
|
|
const author: TPatternAuthor = {
|
|
id: 'author-1',
|
|
displayName: 'Test Author',
|
|
verified: true,
|
|
patterns: 2,
|
|
totalDownloads: 100,
|
|
};
|
|
const makePattern = (id: string, overrides: Partial<TPatternEntry> = {}): TPatternEntry => ({
|
|
id,
|
|
name: `pattern-${id}`,
|
|
displayName: `Pattern ${id}`,
|
|
description: `Description ${id}`,
|
|
version: '1.0.0',
|
|
cid: `Qm${id}`,
|
|
size: 500,
|
|
checksum: `sha256:${id}`,
|
|
author,
|
|
license: 'MIT',
|
|
categories: ['routing'],
|
|
tags: ['test'],
|
|
downloads: 50,
|
|
rating: 4,
|
|
ratingCount: 5,
|
|
lastUpdated: '2026-01-01T00:00:00Z',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
minClaudeFlowVersion: '3.0.0',
|
|
verified: true,
|
|
trustLevel: 'official',
|
|
...overrides,
|
|
});
|
|
|
|
return {
|
|
version: '1.0.0',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
ipnsName: 'test-patterns',
|
|
patterns: [
|
|
makePattern('p1', { downloads: 100, tags: ['auth', 'routing'] }),
|
|
makePattern('p2', { downloads: 200, categories: ['complexity'], tags: ['perf'] }),
|
|
],
|
|
categories: [{ id: 'routing', name: 'Routing', description: 'Routing patterns', patternCount: 1 }],
|
|
authors: [author],
|
|
totalPatterns: 2,
|
|
totalDownloads: 300,
|
|
totalAuthors: 1,
|
|
featured: ['p1'],
|
|
trending: ['p2'],
|
|
newest: ['p1'],
|
|
};
|
|
}
|
|
|
|
describe('Transfer Store Search', () => {
|
|
const registry = createMockPatternRegistry();
|
|
|
|
it('should search all patterns', () => {
|
|
const result = searchPatterns(registry);
|
|
expect(result.total).toBe(2);
|
|
});
|
|
|
|
it('should search by text query', () => {
|
|
const result = searchPatterns(registry, { query: 'p1' });
|
|
expect(result.patterns).toHaveLength(1);
|
|
});
|
|
|
|
it('should search by category', () => {
|
|
const result = searchPatterns(registry, { category: 'complexity' });
|
|
expect(result.patterns).toHaveLength(1);
|
|
expect(result.patterns[0].id).toBe('p2');
|
|
});
|
|
|
|
it('should search by tags', () => {
|
|
const result = searchPatterns(registry, { tags: ['auth'] });
|
|
expect(result.patterns).toHaveLength(1);
|
|
});
|
|
|
|
it('getPatternById finds pattern', () => {
|
|
const pattern = getPatternById(registry, 'p1');
|
|
expect(pattern).not.toBeUndefined();
|
|
expect(pattern!.id).toBe('p1');
|
|
});
|
|
|
|
it('getPatternById returns undefined for missing', () => {
|
|
expect(getPatternById(registry, 'nonexistent')).toBeUndefined();
|
|
});
|
|
|
|
it('getPatternByName finds pattern', () => {
|
|
const pattern = getPatternByName(registry, 'pattern-p1');
|
|
expect(pattern).not.toBeUndefined();
|
|
});
|
|
|
|
it('getPatternsByCategory returns matching', () => {
|
|
const patterns = getPatternsByCategory(registry, 'routing');
|
|
expect(patterns.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('getSearchSuggestions returns suggestions', () => {
|
|
const suggestions = getSearchSuggestions(registry, 'pat');
|
|
expect(suggestions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('getTagCloud returns tag counts', () => {
|
|
const cloud = getTagCloud(registry);
|
|
expect(cloud instanceof Map).toBe(true);
|
|
// p1 has tags ['auth', 'routing'], p2 has tags ['perf']
|
|
expect(cloud.get('auth')).toBe(1);
|
|
expect(cloud.get('perf')).toBe(1);
|
|
});
|
|
|
|
it('getCategoryStats returns stats', () => {
|
|
const stats = getCategoryStats(registry);
|
|
expect(stats.get('routing')).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 20. Transfer Store Registry
|
|
// ============================================================================
|
|
|
|
import {
|
|
createRegistry,
|
|
addPatternToRegistry,
|
|
removePatternFromRegistry,
|
|
serializeRegistry,
|
|
deserializeRegistry,
|
|
generatePatternId,
|
|
mergeRegistries,
|
|
} from '../src/transfer/store/registry.js';
|
|
|
|
describe('Transfer Store Registry', () => {
|
|
it('should create a new registry', () => {
|
|
const registry = createRegistry('test-ipns');
|
|
expect(registry.version).toBeTruthy();
|
|
expect(registry.ipnsName).toBe('test-ipns');
|
|
expect(registry.totalPatterns).toBe(0);
|
|
});
|
|
|
|
it('should add pattern to registry', () => {
|
|
const registry = createRegistry('test');
|
|
const pattern: TPatternEntry = {
|
|
id: 'new-pattern',
|
|
name: 'New Pattern',
|
|
displayName: 'New Pattern',
|
|
description: 'A new pattern',
|
|
version: '1.0.0',
|
|
cid: 'QmNew',
|
|
size: 100,
|
|
checksum: 'sha256:new',
|
|
author: { id: 'a1', verified: true, patterns: 1, totalDownloads: 0 },
|
|
license: 'MIT',
|
|
categories: ['routing'],
|
|
tags: ['test'],
|
|
downloads: 0,
|
|
rating: 0,
|
|
ratingCount: 0,
|
|
lastUpdated: new Date().toISOString(),
|
|
createdAt: new Date().toISOString(),
|
|
minClaudeFlowVersion: '3.0.0',
|
|
verified: false,
|
|
trustLevel: 'community',
|
|
};
|
|
const updated = addPatternToRegistry(registry, pattern);
|
|
expect(updated.totalPatterns).toBe(1);
|
|
expect(updated.patterns).toHaveLength(1);
|
|
});
|
|
|
|
it('should remove pattern from registry', () => {
|
|
const registry = createRegistry('test');
|
|
const pattern: TPatternEntry = {
|
|
id: 'to-remove',
|
|
name: 'Remove Me',
|
|
displayName: 'Remove Me',
|
|
description: 'test',
|
|
version: '1.0.0',
|
|
cid: 'QmRemove',
|
|
size: 100,
|
|
checksum: 'sha256:rm',
|
|
author: { id: 'a1', verified: true, patterns: 1, totalDownloads: 0 },
|
|
license: 'MIT',
|
|
categories: [],
|
|
tags: [],
|
|
downloads: 0,
|
|
rating: 0,
|
|
ratingCount: 0,
|
|
lastUpdated: new Date().toISOString(),
|
|
createdAt: new Date().toISOString(),
|
|
minClaudeFlowVersion: '3.0.0',
|
|
verified: false,
|
|
trustLevel: 'community',
|
|
};
|
|
const withPattern = addPatternToRegistry(registry, pattern);
|
|
const removed = removePatternFromRegistry(withPattern, 'to-remove');
|
|
expect(removed.totalPatterns).toBe(0);
|
|
expect(removed.patterns).toHaveLength(0);
|
|
});
|
|
|
|
it('should serialize and deserialize registry', () => {
|
|
const registry = createRegistry('test');
|
|
const serialized = serializeRegistry(registry);
|
|
expect(typeof serialized).toBe('string');
|
|
const deserialized = deserializeRegistry(serialized);
|
|
expect(deserialized.ipnsName).toBe('test');
|
|
});
|
|
|
|
it('should generate unique pattern IDs', () => {
|
|
const id1 = generatePatternId('test', 'a1');
|
|
const id2 = generatePatternId('test2', 'a1');
|
|
expect(id1).not.toBe(id2);
|
|
});
|
|
|
|
it('should merge two registries', () => {
|
|
const r1 = createRegistry('r1');
|
|
const r2 = createRegistry('r2');
|
|
const pattern: TPatternEntry = {
|
|
id: 'merged',
|
|
name: 'Merged',
|
|
displayName: 'Merged',
|
|
description: 'test',
|
|
version: '1.0.0',
|
|
cid: 'QmMerged',
|
|
size: 100,
|
|
checksum: 'sha256:m',
|
|
author: { id: 'a1', verified: true, patterns: 1, totalDownloads: 0 },
|
|
license: 'MIT',
|
|
categories: [],
|
|
tags: [],
|
|
downloads: 0,
|
|
rating: 0,
|
|
ratingCount: 0,
|
|
lastUpdated: new Date().toISOString(),
|
|
createdAt: new Date().toISOString(),
|
|
minClaudeFlowVersion: '3.0.0',
|
|
verified: false,
|
|
trustLevel: 'community',
|
|
};
|
|
const r2WithPattern = addPatternToRegistry(r2, pattern);
|
|
const merged = mergeRegistries(r1, r2WithPattern);
|
|
expect(merged.totalPatterns).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 21. GCS Storage
|
|
// ============================================================================
|
|
|
|
import { getGCSConfig, getGCSStatus, hasGCSCredentials } from '../src/transfer/storage/gcs.js';
|
|
|
|
describe('GCS Storage', () => {
|
|
it('getGCSConfig returns null without env vars', () => {
|
|
const origBucket = process.env.GCS_BUCKET;
|
|
const origGoogle = process.env.GOOGLE_CLOUD_BUCKET;
|
|
delete process.env.GCS_BUCKET;
|
|
delete process.env.GOOGLE_CLOUD_BUCKET;
|
|
expect(getGCSConfig()).toBeNull();
|
|
if (origBucket) process.env.GCS_BUCKET = origBucket;
|
|
if (origGoogle) process.env.GOOGLE_CLOUD_BUCKET = origGoogle;
|
|
});
|
|
|
|
it('getGCSStatus reflects configuration state', () => {
|
|
const status = getGCSStatus();
|
|
// In test environment, gcloud is likely not available
|
|
expect(status).toHaveProperty('available');
|
|
expect(status).toHaveProperty('message');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 22. Production Exports (module completeness)
|
|
// ============================================================================
|
|
|
|
import * as production from '../src/production/index.js';
|
|
|
|
describe('Production Module Exports', () => {
|
|
it('should export ErrorHandler', () => {
|
|
expect(production.ErrorHandler).toBeDefined();
|
|
});
|
|
|
|
it('should export withErrorHandling', () => {
|
|
expect(production.withErrorHandling).toBeDefined();
|
|
});
|
|
|
|
it('should export RateLimiter', () => {
|
|
expect(production.RateLimiter).toBeDefined();
|
|
});
|
|
|
|
it('should export createRateLimiter', () => {
|
|
expect(production.createRateLimiter).toBeDefined();
|
|
});
|
|
|
|
it('should export withRetry', () => {
|
|
expect(production.withRetry).toBeDefined();
|
|
});
|
|
|
|
it('should export CircuitBreaker', () => {
|
|
expect(production.CircuitBreaker).toBeDefined();
|
|
});
|
|
|
|
it('should export MonitoringHooks', () => {
|
|
expect(production.MonitoringHooks).toBeDefined();
|
|
});
|
|
|
|
it('should export createMonitor', () => {
|
|
expect(production.createMonitor).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 23. Transfer Module Exports
|
|
// ============================================================================
|
|
|
|
import * as transfer from '../src/transfer/index.js';
|
|
|
|
describe('Transfer Module Exports', () => {
|
|
it('should export CFP serialization functions', () => {
|
|
expect(transfer.createCFP).toBeDefined();
|
|
expect(transfer.serializeToJson).toBeDefined();
|
|
expect(transfer.serializeToBuffer).toBeDefined();
|
|
expect(transfer.deserializeCFP).toBeDefined();
|
|
expect(transfer.validateCFP).toBeDefined();
|
|
expect(transfer.getFileExtension).toBeDefined();
|
|
expect(transfer.detectFormat).toBeDefined();
|
|
});
|
|
|
|
it('should export anonymization functions', () => {
|
|
expect(transfer.detectPII).toBeDefined();
|
|
expect(transfer.redactPII).toBeDefined();
|
|
expect(transfer.anonymizeCFP).toBeDefined();
|
|
expect(transfer.scanCFPForPII).toBeDefined();
|
|
});
|
|
|
|
it('should export IPFS upload functions', () => {
|
|
expect(transfer.uploadToIPFS).toBeDefined();
|
|
expect(transfer.pinContent).toBeDefined();
|
|
expect(transfer.unpinContent).toBeDefined();
|
|
expect(transfer.checkContent).toBeDefined();
|
|
expect(transfer.getGatewayURL).toBeDefined();
|
|
expect(transfer.getIPNSURL).toBeDefined();
|
|
});
|
|
|
|
it('should export Seraphine model functions', () => {
|
|
expect(transfer.SERAPHINE_VERSION).toBeDefined();
|
|
expect(transfer.createSeraphinePatterns).toBeDefined();
|
|
expect(transfer.getSeraphineInfo).toBeDefined();
|
|
});
|
|
|
|
it('should export Store classes and functions', () => {
|
|
expect(transfer.PatternStore).toBeDefined();
|
|
expect(transfer.createPatternStore).toBeDefined();
|
|
expect(transfer.PatternDiscovery).toBeDefined();
|
|
expect(transfer.searchPatterns).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// 24. Plugin Store Module Exports
|
|
// ============================================================================
|
|
|
|
import * as pluginStoreModule from '../src/plugins/store/index.js';
|
|
|
|
describe('Plugin Store Module Exports', () => {
|
|
it('should export PluginDiscoveryService', () => {
|
|
expect(pluginStoreModule.PluginDiscoveryService).toBeDefined();
|
|
});
|
|
|
|
it('should export createPluginDiscoveryService', () => {
|
|
expect(pluginStoreModule.createPluginDiscoveryService).toBeDefined();
|
|
});
|
|
|
|
it('should export search functions', () => {
|
|
expect(pluginStoreModule.searchPlugins).toBeDefined();
|
|
expect(pluginStoreModule.getPluginSearchSuggestions).toBeDefined();
|
|
expect(pluginStoreModule.getPluginTagCloud).toBeDefined();
|
|
expect(pluginStoreModule.findSimilarPlugins).toBeDefined();
|
|
expect(pluginStoreModule.getFeaturedPlugins).toBeDefined();
|
|
expect(pluginStoreModule.getTrendingPlugins).toBeDefined();
|
|
expect(pluginStoreModule.getNewestPlugins).toBeDefined();
|
|
expect(pluginStoreModule.getOfficialPlugins).toBeDefined();
|
|
expect(pluginStoreModule.getPluginsByPermission).toBeDefined();
|
|
});
|
|
|
|
it('should export PluginStore class', () => {
|
|
expect(pluginStoreModule.PluginStore).toBeDefined();
|
|
expect(pluginStoreModule.createPluginStore).toBeDefined();
|
|
});
|
|
});
|