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
660 lines
22 KiB
TypeScript
660 lines
22 KiB
TypeScript
/**
|
|
* Integration Docker Validation Tests
|
|
*
|
|
* Validates the RuFlo Docker-based deployment stack without running Docker.
|
|
* Checks docker-compose.yml, nginx.conf, MCP bridge source, CLI Dockerfile,
|
|
* and CLI build/init/doctor commands for correctness.
|
|
*
|
|
* 40+ test cases covering:
|
|
* - Docker Compose config parsing and service definitions
|
|
* - Nginx reverse proxy configuration
|
|
* - MCP bridge endpoint and CORS validation
|
|
* - CLI Dockerfile multi-stage build
|
|
* - CLI build, init, and doctor commands
|
|
* - Generated file validity
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
import { execSync } from 'child_process';
|
|
import { resolve, join } from 'path';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Paths
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const ROOT = resolve(__dirname, '..', '..', '..', '..'); // /workspaces/claude-flow
|
|
const CLI_DIR = resolve(__dirname, '..'); // v3/@claude-flow/cli
|
|
const RUFLO_DIR = join(ROOT, 'ruflo');
|
|
const COMPOSE_PATH = join(RUFLO_DIR, 'docker-compose.yml');
|
|
const NGINX_CONF_PATH = join(RUFLO_DIR, 'src', 'nginx', 'nginx.conf');
|
|
const NGINX_DOCKERFILE = join(RUFLO_DIR, 'src', 'nginx', 'Dockerfile');
|
|
const MCP_BRIDGE_INDEX = join(RUFLO_DIR, 'src', 'mcp-bridge', 'index.js');
|
|
const MCP_BRIDGE_DOCKERFILE = join(RUFLO_DIR, 'src', 'mcp-bridge', 'Dockerfile');
|
|
const CLI_DOCKERFILE = join(CLI_DIR, 'docker', 'Dockerfile');
|
|
const ENV_EXAMPLE = join(RUFLO_DIR, '.env.example');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function readFile(p: string): string {
|
|
return readFileSync(p, 'utf-8');
|
|
}
|
|
|
|
/** Minimal YAML parser: extracts top-level keys under `services:` */
|
|
function parseComposeServices(yaml: string): string[] {
|
|
const lines = yaml.split('\n');
|
|
const services: string[] = [];
|
|
let inServices = false;
|
|
for (const line of lines) {
|
|
if (/^services:/.test(line)) { inServices = true; continue; }
|
|
if (inServices && /^[a-z]/.test(line)) break; // next top-level key
|
|
if (inServices && /^ [a-z][\w-]*:/.test(line)) {
|
|
services.push(line.trim().replace(':', ''));
|
|
}
|
|
}
|
|
return services;
|
|
}
|
|
|
|
/** Extract all EXPOSE directives from a Dockerfile */
|
|
function extractExposePort(content: string): number[] {
|
|
return [...content.matchAll(/^EXPOSE\s+(\d+)/gm)].map(m => parseInt(m[1], 10));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Docker Compose Config Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Docker Compose Configuration', () => {
|
|
let composeContent: string;
|
|
|
|
beforeAll(() => {
|
|
composeContent = readFile(COMPOSE_PATH);
|
|
});
|
|
|
|
it('docker-compose.yml exists', () => {
|
|
expect(existsSync(COMPOSE_PATH)).toBe(true);
|
|
});
|
|
|
|
it('defines exactly 4 services (mongodb, mcp-bridge, nginx, chat-ui)', () => {
|
|
const services = parseComposeServices(composeContent);
|
|
expect(services).toEqual(
|
|
expect.arrayContaining(['mongodb', 'mcp-bridge', 'nginx', 'chat-ui']),
|
|
);
|
|
expect(services).toHaveLength(4);
|
|
});
|
|
|
|
it('mongodb uses mongo:7 image', () => {
|
|
expect(composeContent).toMatch(/image:\s*mongo:7/);
|
|
});
|
|
|
|
it('mongodb exposes port 27017', () => {
|
|
expect(composeContent).toMatch(/"27017:27017"/);
|
|
});
|
|
|
|
it('mongodb has a named volume for data persistence', () => {
|
|
expect(composeContent).toMatch(/mongo-data:\/data\/db/);
|
|
expect(composeContent).toMatch(/^volumes:\s*\n\s+mongo-data:/m);
|
|
});
|
|
|
|
it('mcp-bridge builds from correct context', () => {
|
|
expect(composeContent).toMatch(/context:\s*\.\/src\/ruvocal\/mcp-bridge/);
|
|
});
|
|
|
|
it('mcp-bridge publishes port 3001', () => {
|
|
expect(composeContent).toMatch(/"3001:3001"/);
|
|
});
|
|
|
|
it('mcp-bridge has a healthcheck', () => {
|
|
// The healthcheck should reference /health
|
|
const mcpSection = composeContent.split('mcp-bridge:')[1]?.split(/\n [a-z]/)[0] ?? '';
|
|
expect(composeContent).toContain('healthcheck');
|
|
expect(composeContent).toContain('/health');
|
|
});
|
|
|
|
it('nginx depends on chat-ui and mcp-bridge', () => {
|
|
// Extract nginx depends_on block
|
|
const nginxBlock = composeContent.split('nginx:')[1]?.split(/\n [a-z]/)[0] ?? '';
|
|
expect(nginxBlock).toContain('chat-ui');
|
|
expect(nginxBlock).toContain('mcp-bridge');
|
|
});
|
|
|
|
it('nginx publishes port 3000', () => {
|
|
expect(composeContent).toMatch(/"3000:3000"/);
|
|
});
|
|
|
|
it('nginx has a healthcheck', () => {
|
|
// The nginx healthcheck should use wget
|
|
expect(composeContent).toContain('wget');
|
|
expect(composeContent).toContain('http://localhost:3000');
|
|
});
|
|
|
|
it('chat-ui depends on mongodb and mcp-bridge', () => {
|
|
const chatBlock = composeContent.split('chat-ui:')[1] ?? '';
|
|
const depsBlock = chatBlock.split('depends_on:')[1]?.split(/\n [a-z]/)[0] ?? '';
|
|
expect(depsBlock).toContain('mongodb');
|
|
expect(depsBlock).toContain('mcp-bridge');
|
|
});
|
|
|
|
it('chat-ui exposes port 3000 internally only', () => {
|
|
const chatBlock = composeContent.split('chat-ui:')[1]?.split(/\n [a-z]/)[0] ?? '';
|
|
// Should use "expose" not "ports" since nginx fronts it
|
|
expect(chatBlock).toContain('expose');
|
|
});
|
|
|
|
it('chat-ui injects DOTENV_LOCAL with required env vars', () => {
|
|
expect(composeContent).toContain('DOTENV_LOCAL');
|
|
expect(composeContent).toContain('MONGODB_URL=mongodb://mongodb:27017');
|
|
expect(composeContent).toContain('PUBLIC_APP_NAME');
|
|
expect(composeContent).toContain('OPENAI_BASE_URL=http://mcp-bridge:3001');
|
|
});
|
|
|
|
it('all services use restart: unless-stopped', () => {
|
|
const matches = composeContent.match(/restart:\s*unless-stopped/g);
|
|
expect(matches).not.toBeNull();
|
|
expect(matches!.length).toBeGreaterThanOrEqual(4);
|
|
});
|
|
|
|
it('MCP tool group env vars are defined for mcp-bridge', () => {
|
|
const groups = [
|
|
'MCP_GROUP_INTELLIGENCE', 'MCP_GROUP_AGENTS',
|
|
'MCP_GROUP_MEMORY', 'MCP_GROUP_DEVTOOLS',
|
|
'MCP_GROUP_SECURITY', 'MCP_GROUP_BROWSER',
|
|
'MCP_GROUP_NEURAL', 'MCP_GROUP_AGENTIC_FLOW',
|
|
'MCP_GROUP_CLAUDE_CODE', 'MCP_GROUP_GEMINI',
|
|
'MCP_GROUP_CODEX',
|
|
];
|
|
for (const g of groups) {
|
|
expect(composeContent).toContain(g);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Nginx Config Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Nginx Configuration', () => {
|
|
let nginxContent: string;
|
|
|
|
beforeAll(() => {
|
|
nginxContent = readFile(NGINX_CONF_PATH);
|
|
});
|
|
|
|
it('nginx.conf exists', () => {
|
|
expect(existsSync(NGINX_CONF_PATH)).toBe(true);
|
|
});
|
|
|
|
it('listens on port 3000', () => {
|
|
expect(nginxContent).toMatch(/listen\s+3000/);
|
|
});
|
|
|
|
it('gzip is disabled (required for sub_filter)', () => {
|
|
expect(nginxContent).toMatch(/gzip\s+off/);
|
|
});
|
|
|
|
it('sets Access-Control-Allow-Origin header', () => {
|
|
expect(nginxContent).toContain('Access-Control-Allow-Origin');
|
|
});
|
|
|
|
it('sets Access-Control-Allow-Methods header', () => {
|
|
expect(nginxContent).toContain('Access-Control-Allow-Methods');
|
|
expect(nginxContent).toContain('GET');
|
|
expect(nginxContent).toContain('POST');
|
|
expect(nginxContent).toContain('OPTIONS');
|
|
});
|
|
|
|
it('sets Access-Control-Allow-Headers header', () => {
|
|
expect(nginxContent).toContain('Access-Control-Allow-Headers');
|
|
expect(nginxContent).toContain('Content-Type');
|
|
expect(nginxContent).toContain('Authorization');
|
|
});
|
|
|
|
it('handles OPTIONS preflight requests with 204', () => {
|
|
expect(nginxContent).toMatch(/if\s*\(\$request_method\s*=\s*OPTIONS\)/);
|
|
expect(nginxContent).toContain('return 204');
|
|
});
|
|
|
|
it('proxies root location to chat-ui:3000', () => {
|
|
expect(nginxContent).toMatch(/proxy_pass\s+http:\/\/chat-ui:3000/);
|
|
});
|
|
|
|
it('sets WebSocket upgrade headers', () => {
|
|
expect(nginxContent).toContain('Upgrade');
|
|
expect(nginxContent).toContain('"upgrade"');
|
|
expect(nginxContent).toContain('$http_upgrade');
|
|
});
|
|
|
|
it('disables upstream gzip via Accept-Encoding header', () => {
|
|
expect(nginxContent).toContain('proxy_set_header Accept-Encoding ""');
|
|
});
|
|
|
|
it('injects RuFlo welcome.js script via sub_filter', () => {
|
|
expect(nginxContent).toMatch(/sub_filter\s+'<\/head>'\s+'<script src="\/ruflo\/welcome\.js"/);
|
|
});
|
|
|
|
it('has sub_filter_once off for multiple replacements', () => {
|
|
expect(nginxContent).toMatch(/sub_filter_once\s+off/);
|
|
});
|
|
|
|
it('applies sub_filter to text/html and application/json', () => {
|
|
expect(nginxContent).toContain('sub_filter_types');
|
|
expect(nginxContent).toContain('text/html');
|
|
expect(nginxContent).toContain('application/json');
|
|
});
|
|
|
|
it('serves /chatui/ static assets from /etc/nginx/static/', () => {
|
|
expect(nginxContent).toMatch(/location\s+\/chatui\//);
|
|
expect(nginxContent).toContain('alias /etc/nginx/static/');
|
|
});
|
|
|
|
it('serves /ruflo/ static assets from /etc/nginx/static/', () => {
|
|
expect(nginxContent).toMatch(/location\s+\/ruflo\//);
|
|
});
|
|
|
|
it('rewrites localhost:3000 URLs to relative paths via sub_filter', () => {
|
|
expect(nginxContent).toContain("sub_filter 'http://localhost:3000' ''");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Nginx Dockerfile Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Nginx Dockerfile', () => {
|
|
let dockerContent: string;
|
|
|
|
beforeAll(() => {
|
|
dockerContent = readFile(NGINX_DOCKERFILE);
|
|
});
|
|
|
|
it('Nginx Dockerfile exists', () => {
|
|
expect(existsSync(NGINX_DOCKERFILE)).toBe(true);
|
|
});
|
|
|
|
it('uses nginx alpine base image', () => {
|
|
expect(dockerContent).toMatch(/FROM\s+nginx:\d+[\w.-]*-alpine/);
|
|
});
|
|
|
|
it('copies nginx.conf into container', () => {
|
|
expect(dockerContent).toContain('COPY nginx.conf /etc/nginx/nginx.conf');
|
|
});
|
|
|
|
it('copies static assets into container', () => {
|
|
expect(dockerContent).toContain('COPY static/ /etc/nginx/static/');
|
|
});
|
|
|
|
it('exposes port 3000', () => {
|
|
const ports = extractExposePort(dockerContent);
|
|
expect(ports).toContain(3000);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. MCP Bridge Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MCP Bridge (index.js)', () => {
|
|
let bridgeContent: string;
|
|
|
|
beforeAll(() => {
|
|
bridgeContent = readFile(MCP_BRIDGE_INDEX);
|
|
});
|
|
|
|
it('MCP bridge index.js exists', () => {
|
|
expect(existsSync(MCP_BRIDGE_INDEX)).toBe(true);
|
|
});
|
|
|
|
it('sets up CORS middleware with Access-Control-Allow-Origin', () => {
|
|
expect(bridgeContent).toContain('Access-Control-Allow-Origin');
|
|
expect(bridgeContent).toContain('"*"');
|
|
});
|
|
|
|
it('sets CORS Access-Control-Allow-Methods', () => {
|
|
expect(bridgeContent).toContain('Access-Control-Allow-Methods');
|
|
});
|
|
|
|
it('sets CORS Access-Control-Allow-Headers', () => {
|
|
expect(bridgeContent).toContain('Access-Control-Allow-Headers');
|
|
});
|
|
|
|
it('handles OPTIONS preflight with 204', () => {
|
|
expect(bridgeContent).toContain('OPTIONS');
|
|
expect(bridgeContent).toContain('sendStatus(204)');
|
|
});
|
|
|
|
it('defines /health endpoint', () => {
|
|
expect(bridgeContent).toMatch(/app\.get\(["']\/health["']/);
|
|
});
|
|
|
|
it('/health endpoint returns status and tool counts', () => {
|
|
// The health handler builds a JSON response with status, tools, groups, backends
|
|
expect(bridgeContent).toContain('"ok"');
|
|
expect(bridgeContent).toContain('"mcp-bridge"');
|
|
});
|
|
|
|
it('defines /models endpoint', () => {
|
|
expect(bridgeContent).toMatch(/app\.get\(["']\/models["']/);
|
|
});
|
|
|
|
it('defines /groups endpoint', () => {
|
|
expect(bridgeContent).toMatch(/app\.get\(["']\/groups["']/);
|
|
});
|
|
|
|
it('defines /chat/completions endpoint', () => {
|
|
expect(bridgeContent).toMatch(/app\.post\(["']\/chat\/completions["']/);
|
|
});
|
|
|
|
it('defines per-group MCP endpoints', () => {
|
|
expect(bridgeContent).toMatch(/app\.post\(`\/mcp\/\$\{groupName\}`/);
|
|
expect(bridgeContent).toMatch(/app\.get\(`\/mcp\/\$\{groupName\}`/);
|
|
});
|
|
|
|
it('defines tool group configuration with at least 8 groups', () => {
|
|
const groupMatches = bridgeContent.match(/["']?[\w-]+["']?\s*:\s*\{[^}]*enabled\s*:/g);
|
|
expect(groupMatches).not.toBeNull();
|
|
expect(groupMatches!.length).toBeGreaterThanOrEqual(8);
|
|
});
|
|
|
|
it('uses express with 10mb JSON body limit', () => {
|
|
expect(bridgeContent).toContain('express.json({ limit: "10mb" })');
|
|
});
|
|
|
|
it('defaults to port 3001', () => {
|
|
expect(bridgeContent).toContain('"3001"');
|
|
});
|
|
|
|
it('listens on the configured PORT', () => {
|
|
expect(bridgeContent).toMatch(/app\.listen\(PORT/);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. MCP Bridge Dockerfile Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MCP Bridge Dockerfile', () => {
|
|
let dockerContent: string;
|
|
|
|
beforeAll(() => {
|
|
dockerContent = readFile(MCP_BRIDGE_DOCKERFILE);
|
|
});
|
|
|
|
it('MCP Bridge Dockerfile exists', () => {
|
|
expect(existsSync(MCP_BRIDGE_DOCKERFILE)).toBe(true);
|
|
});
|
|
|
|
it('uses Node.js 20 slim base image', () => {
|
|
expect(dockerContent).toMatch(/FROM\s+node:20-slim/);
|
|
});
|
|
|
|
it('installs production dependencies', () => {
|
|
expect(dockerContent).toContain('npm install --production');
|
|
});
|
|
|
|
it('copies index.js and mcp-stdio-kernel.js', () => {
|
|
expect(dockerContent).toContain('COPY index.js ./');
|
|
expect(dockerContent).toContain('COPY mcp-stdio-kernel.js ./');
|
|
});
|
|
|
|
it('creates writable .claude-flow directories', () => {
|
|
expect(dockerContent).toContain('/app/.claude-flow/tasks');
|
|
expect(dockerContent).toContain('/app/.claude-flow/memory');
|
|
expect(dockerContent).toContain('/app/.claude-flow/sessions');
|
|
});
|
|
|
|
it('runs as non-root user', () => {
|
|
expect(dockerContent).toContain('USER node');
|
|
});
|
|
|
|
it('exposes port 3001', () => {
|
|
const ports = extractExposePort(dockerContent);
|
|
expect(ports).toContain(3001);
|
|
});
|
|
|
|
it('sets default tool group environment variables', () => {
|
|
expect(dockerContent).toContain('MCP_GROUP_INTELLIGENCE=true');
|
|
expect(dockerContent).toContain('MCP_GROUP_AGENTS=true');
|
|
expect(dockerContent).toContain('MCP_GROUP_SECURITY=false');
|
|
});
|
|
|
|
it('CMD runs node index.js', () => {
|
|
expect(dockerContent).toMatch(/CMD\s+\["node",\s*"index\.js"\]/);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. CLI Dockerfile Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('CLI Dockerfile (ruflo:lite)', () => {
|
|
let dockerContent: string;
|
|
|
|
beforeAll(() => {
|
|
dockerContent = readFile(CLI_DOCKERFILE);
|
|
});
|
|
|
|
it('CLI Dockerfile exists', () => {
|
|
expect(existsSync(CLI_DOCKERFILE)).toBe(true);
|
|
});
|
|
|
|
it('uses multi-stage build with build and production stages', () => {
|
|
const fromStatements = dockerContent.match(/^FROM\s+/gm);
|
|
expect(fromStatements).not.toBeNull();
|
|
expect(fromStatements!.length).toBeGreaterThanOrEqual(2);
|
|
expect(dockerContent).toContain('AS build');
|
|
expect(dockerContent).toContain('AS production');
|
|
});
|
|
|
|
it('build stage uses Node 22 alpine', () => {
|
|
expect(dockerContent).toMatch(/FROM\s+node:22-alpine\s+AS\s+build/);
|
|
});
|
|
|
|
it('production stage uses Node 22 alpine', () => {
|
|
expect(dockerContent).toMatch(/FROM\s+node:22-alpine\s+AS\s+production/);
|
|
});
|
|
|
|
it('installs ruflo globally in the build stage', () => {
|
|
expect(dockerContent).toContain('npm install -g ruflo@latest');
|
|
});
|
|
|
|
it('prunes heavy optional dependencies to reduce image size', () => {
|
|
const prunedPackages = [
|
|
'agentic-flow', '@opentelemetry', 'onnxruntime-node',
|
|
'@anthropic-ai', 'agentdb',
|
|
];
|
|
for (const pkg of prunedPackages) {
|
|
expect(dockerContent).toContain(pkg);
|
|
}
|
|
});
|
|
|
|
it('creates a non-root user (ruflo)', () => {
|
|
expect(dockerContent).toContain('adduser');
|
|
expect(dockerContent).toContain('ruflo');
|
|
expect(dockerContent).toContain('USER ruflo');
|
|
});
|
|
|
|
it('installs dumb-init for PID 1 signal handling', () => {
|
|
expect(dockerContent).toContain('dumb-init');
|
|
expect(dockerContent).toContain('ENTRYPOINT ["/usr/bin/dumb-init"');
|
|
});
|
|
|
|
it('sets NODE_ENV=production', () => {
|
|
expect(dockerContent).toContain('NODE_ENV=production');
|
|
});
|
|
|
|
it('has a HEALTHCHECK using ruflo doctor', () => {
|
|
expect(dockerContent).toContain('HEALTHCHECK');
|
|
expect(dockerContent).toContain('ruflo doctor');
|
|
});
|
|
|
|
it('default CMD starts MCP server', () => {
|
|
expect(dockerContent).toContain('CMD ["ruflo", "mcp", "start"]');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. Environment Example Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Environment Example (.env.example)', () => {
|
|
let envContent: string;
|
|
|
|
beforeAll(() => {
|
|
envContent = readFile(ENV_EXAMPLE);
|
|
});
|
|
|
|
it('.env.example exists', () => {
|
|
expect(existsSync(ENV_EXAMPLE)).toBe(true);
|
|
});
|
|
|
|
it('defines BRAND_NAME and BRAND_DESCRIPTION', () => {
|
|
expect(envContent).toContain('BRAND_NAME=');
|
|
expect(envContent).toContain('BRAND_DESCRIPTION=');
|
|
});
|
|
|
|
it('defines at least one AI provider key placeholder', () => {
|
|
expect(envContent).toContain('OPENAI_API_KEY=');
|
|
});
|
|
|
|
it('defines MONGODB_DB_NAME', () => {
|
|
expect(envContent).toContain('MONGODB_DB_NAME=');
|
|
});
|
|
|
|
it('defines all MCP tool group toggles', () => {
|
|
const groups = [
|
|
'MCP_GROUP_INTELLIGENCE', 'MCP_GROUP_AGENTS',
|
|
'MCP_GROUP_MEMORY', 'MCP_GROUP_DEVTOOLS',
|
|
'MCP_GROUP_SECURITY', 'MCP_GROUP_BROWSER',
|
|
'MCP_GROUP_NEURAL', 'MCP_GROUP_AGENTIC_FLOW',
|
|
'MCP_GROUP_CLAUDE_CODE', 'MCP_GROUP_GEMINI',
|
|
'MCP_GROUP_CODEX',
|
|
];
|
|
for (const g of groups) {
|
|
expect(envContent).toContain(g);
|
|
}
|
|
});
|
|
|
|
it('contains security warning about never committing real keys', () => {
|
|
expect(envContent.toLowerCase()).toContain('never commit');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. CLI Build Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('CLI Build', () => {
|
|
it('package.json exists and has correct name', () => {
|
|
const pkg = JSON.parse(readFile(join(CLI_DIR, 'package.json')));
|
|
expect(pkg.name).toBe('@claude-flow/cli');
|
|
});
|
|
|
|
it('package.json defines build script as tsc', () => {
|
|
const pkg = JSON.parse(readFile(join(CLI_DIR, 'package.json')));
|
|
expect(pkg.scripts.build).toBe('tsc');
|
|
});
|
|
|
|
it('package.json defines test script using vitest', () => {
|
|
const pkg = JSON.parse(readFile(join(CLI_DIR, 'package.json')));
|
|
expect(pkg.scripts.test).toContain('vitest');
|
|
});
|
|
|
|
it('tsconfig.json exists', () => {
|
|
expect(existsSync(join(CLI_DIR, 'tsconfig.json'))).toBe(true);
|
|
});
|
|
|
|
it('vitest.config.ts exists and includes __tests__', () => {
|
|
const config = readFile(join(CLI_DIR, 'vitest.config.ts'));
|
|
expect(config).toContain('__tests__');
|
|
});
|
|
|
|
it('npm run build succeeds', () => {
|
|
const result = execSync('npm run build 2>&1', {
|
|
cwd: CLI_DIR,
|
|
encoding: 'utf-8',
|
|
timeout: 60_000,
|
|
});
|
|
// tsc exits 0 on success; if it fails, execSync throws
|
|
expect(result).toBeDefined();
|
|
}, 65_000);
|
|
|
|
it('dist directory exists after build', () => {
|
|
expect(existsSync(join(CLI_DIR, 'dist'))).toBe(true);
|
|
});
|
|
|
|
it('bin/cli.js entry point exists', () => {
|
|
expect(existsSync(join(CLI_DIR, 'bin', 'cli.js'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 9. Cross-Service Port Consistency
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cross-Service Port Consistency', () => {
|
|
it('nginx proxies to chat-ui:3000 matching chat-ui expose port', () => {
|
|
const nginx = readFile(NGINX_CONF_PATH);
|
|
const compose = readFile(COMPOSE_PATH);
|
|
expect(nginx).toContain('proxy_pass http://chat-ui:3000');
|
|
// chat-ui exposes 3000
|
|
const chatBlock = compose.split('chat-ui:')[1] ?? '';
|
|
expect(chatBlock).toContain('"3000"');
|
|
});
|
|
|
|
it('chat-ui OPENAI_BASE_URL points to mcp-bridge:3001', () => {
|
|
const compose = readFile(COMPOSE_PATH);
|
|
expect(compose).toContain('OPENAI_BASE_URL=http://mcp-bridge:3001');
|
|
});
|
|
|
|
it('MCP bridge Dockerfile EXPOSE matches compose published port', () => {
|
|
const dockerfile = readFile(MCP_BRIDGE_DOCKERFILE);
|
|
const compose = readFile(COMPOSE_PATH);
|
|
expect(dockerfile).toContain('EXPOSE 3001');
|
|
expect(compose).toContain('"3001:3001"');
|
|
});
|
|
|
|
it('nginx Dockerfile EXPOSE matches compose published port', () => {
|
|
const dockerfile = readFile(NGINX_DOCKERFILE);
|
|
const compose = readFile(COMPOSE_PATH);
|
|
expect(dockerfile).toContain('EXPOSE 3000');
|
|
expect(compose).toContain('"3000:3000"');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 10. Security Validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Security Checks', () => {
|
|
it('docker-compose.yml does not contain hardcoded API keys', () => {
|
|
const compose = readFile(COMPOSE_PATH);
|
|
// Should use ${VAR:-default} syntax, not hardcoded keys
|
|
expect(compose).not.toMatch(/sk-[a-zA-Z0-9]{20,}/);
|
|
expect(compose).not.toMatch(/OPENAI_API_KEY:\s*sk-/);
|
|
});
|
|
|
|
it('.env.example does not contain actual API key values', () => {
|
|
const env = readFile(ENV_EXAMPLE);
|
|
// Key lines should be empty or placeholder
|
|
const keyLine = env.split('\n').find(l => l.startsWith('OPENAI_API_KEY='));
|
|
expect(keyLine).toBe('OPENAI_API_KEY=');
|
|
});
|
|
|
|
it('MCP bridge Dockerfile runs as non-root', () => {
|
|
const dockerfile = readFile(MCP_BRIDGE_DOCKERFILE);
|
|
expect(dockerfile).toContain('USER node');
|
|
});
|
|
|
|
it('CLI Dockerfile runs as non-root', () => {
|
|
const dockerfile = readFile(CLI_DOCKERFILE);
|
|
expect(dockerfile).toContain('USER ruflo');
|
|
});
|
|
|
|
it('nginx CORS allows all origins (expected for development)', () => {
|
|
const nginx = readFile(NGINX_CONF_PATH);
|
|
expect(nginx).toContain('"*"');
|
|
});
|
|
});
|