mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
4e513b431d
- Delete dead MCP server.ts (911 lines, never used in production — entry.ts imports MCPProtocolServer). Add legacy aliases in index.ts for backwards compat. - Fix ESLint: rename .eslintrc.js to .eslintrc.cjs (CommonJS in ESM project) - Fix hardcoded version '3.0.0' in protocol-server.ts and http-server.ts — now reads dynamically from package.json - Fix vitest process hang: add worker-level afterAll force-exit (setup-worker.ts) and global teardown safety net. Native modules (better-sqlite3, hnswlib-node) held open handles preventing fork worker exit. - Add test gate to npm-publish.yml — 194 unit test files must pass before publish - Disable benchmark.yml release trigger (benchmarks/suite.ts doesn't exist) - Remove continue-on-error from test steps in optimized-ci.yml (was masking failures) - Add getToolDefinitions() to MCPProtocolServer for test introspection - Update e2e and integration tests to use production server API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
177 lines
6.5 KiB
TypeScript
177 lines
6.5 KiB
TypeScript
/**
|
|
* E2E Smoke Test: OpenCode-AQE Integration
|
|
*
|
|
* Validates end-to-end compatibility between AQE MCP server and OpenCode:
|
|
* - MCP tool listing and schema validation
|
|
* - Tool registration completeness
|
|
* - Bridge initialization
|
|
* - OpenCode config validation (agents + skills YAML)
|
|
*
|
|
* Note: Tool invocation tests (fleet_health, test_generate, etc.) were removed
|
|
* when server.ts (legacy MCPServer with .invoke() API) was deleted. The production
|
|
* MCPProtocolServer uses JSON-RPC over stdio — invocation tests require a full
|
|
* transport integration test, not a direct method call.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { createMCPProtocolServer } from '../../src/mcp/protocol-server';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import YAML from 'yaml';
|
|
|
|
const OPENCODE_DIR = path.resolve(__dirname, '../../../../.opencode');
|
|
const AGENTS_DIR = path.join(OPENCODE_DIR, 'agents');
|
|
const SKILLS_DIR = path.join(OPENCODE_DIR, 'skills');
|
|
|
|
describe('OpenCode-AQE E2E Smoke Test', () => {
|
|
// -------------------------------------------------------------------------
|
|
// 1. MCP tool listing
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('should list 30+ tools with OpenCode-compatible schemas', async () => {
|
|
const server = createMCPProtocolServer();
|
|
const tools = server.getToolDefinitions();
|
|
expect(tools.length).toBeGreaterThanOrEqual(30);
|
|
|
|
for (const tool of tools) {
|
|
expect(tool.name).toBeDefined();
|
|
expect(typeof tool.name).toBe('string');
|
|
expect(tool.description).toBeDefined();
|
|
expect(typeof tool.description).toBe('string');
|
|
expect(tool.parameters).toBeDefined();
|
|
expect(Array.isArray(tool.parameters)).toBe(true);
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 2. Tool descriptions under 200 chars
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('should have tool descriptions under 200 characters for OpenCode display', async () => {
|
|
const server = createMCPProtocolServer();
|
|
const tools = server.getToolDefinitions();
|
|
const tooLong: string[] = [];
|
|
|
|
for (const tool of tools) {
|
|
if (tool.description && tool.description.length > 200) {
|
|
tooLong.push(`${tool.name} (${tool.description.length} chars)`);
|
|
}
|
|
}
|
|
|
|
if (tooLong.length > 0) {
|
|
console.warn(`Tools with descriptions > 200 chars:\n ${tooLong.join('\n ')}`);
|
|
}
|
|
const complianceRate = (tools.length - tooLong.length) / tools.length;
|
|
expect(complianceRate).toBeGreaterThanOrEqual(0.8);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 3. Bridge initBridge
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('should initialize bridge and return valid status', async () => {
|
|
try {
|
|
const { initBridge } = await import(
|
|
'../../../packages/aqe-opencode-bridge/src/index'
|
|
);
|
|
const status = await initBridge();
|
|
expect(status).toBeDefined();
|
|
expect(status.initialized).toBe(true);
|
|
expect(status.skillCount).toBeGreaterThan(0);
|
|
expect(status.tierBreakdown).toBeDefined();
|
|
expect(status.tierBreakdown.tier1).toBeGreaterThan(0);
|
|
expect(status.tierBreakdown.tier2).toBeGreaterThan(0);
|
|
expect(status.tierBreakdown.tier3).toBeGreaterThan(0);
|
|
} catch (err) {
|
|
// Bridge may have import issues in test env — structured error is OK
|
|
expect(err).toBeInstanceOf(Error);
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 4. Agent YAML configs are parseable
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('should have all .opencode/agents/ YAML configs parseable', () => {
|
|
if (!fs.existsSync(AGENTS_DIR)) {
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.yaml'));
|
|
expect(files.length).toBeGreaterThanOrEqual(10);
|
|
|
|
for (const file of files) {
|
|
const content = fs.readFileSync(path.join(AGENTS_DIR, file), 'utf-8');
|
|
let parsed: unknown;
|
|
|
|
try {
|
|
parsed = YAML.parse(content);
|
|
} catch (err) {
|
|
throw new Error(`Invalid YAML in agents/${file}: ${(err as Error).message}`);
|
|
}
|
|
|
|
expect(parsed).toBeDefined();
|
|
expect(typeof parsed).toBe('object');
|
|
|
|
const agent = parsed as Record<string, unknown>;
|
|
expect(agent.name, `${file}: missing name`).toBeDefined();
|
|
expect(agent.description, `${file}: missing description`).toBeDefined();
|
|
expect(agent.systemPrompt, `${file}: missing systemPrompt`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 5. Skill YAML configs are parseable
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('should have all .opencode/skills/ YAML configs parseable', () => {
|
|
if (!fs.existsSync(SKILLS_DIR)) {
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.yaml'));
|
|
expect(files.length).toBeGreaterThanOrEqual(15);
|
|
|
|
for (const file of files) {
|
|
const content = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf-8');
|
|
let parsed: unknown;
|
|
|
|
try {
|
|
parsed = YAML.parse(content);
|
|
} catch (err) {
|
|
throw new Error(`Invalid YAML in skills/${file}: ${(err as Error).message}`);
|
|
}
|
|
|
|
expect(parsed).toBeDefined();
|
|
expect(typeof parsed).toBe('object');
|
|
|
|
const skill = parsed as Record<string, unknown>;
|
|
expect(skill.name, `${file}: missing name`).toBeDefined();
|
|
expect(skill.description, `${file}: missing description`).toBeDefined();
|
|
expect(skill.steps, `${file}: missing steps`).toBeDefined();
|
|
expect(Array.isArray(skill.steps), `${file}: steps must be array`).toBe(true);
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 6. Permissions file exists and is valid
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('should have valid permissions.yaml', () => {
|
|
const permPath = path.join(OPENCODE_DIR, 'permissions.yaml');
|
|
if (!fs.existsSync(permPath)) {
|
|
return;
|
|
}
|
|
|
|
const content = fs.readFileSync(permPath, 'utf-8');
|
|
const parsed = YAML.parse(content) as Record<string, unknown>;
|
|
|
|
expect(parsed.defaults).toBeDefined();
|
|
expect(typeof parsed.defaults).toBe('object');
|
|
|
|
if (parsed.agents) {
|
|
expect(typeof parsed.agents).toBe('object');
|
|
}
|
|
});
|
|
});
|