nkds

导航

 

MonkeyCode 测试策略与质量保障:AI 编程助手的全面质量工程实践指南

引言

"在 AI 时代,'测试'的定义正在被重新书写。"

传统的软件测试关注"代码是否按预期执行",而 AI 编程助手的测试还需要回答一个更根本的问题:"AI 的输出是否真正帮助了开发者?" 这意味着 MonkeyCode 的质量保障体系必须同时覆盖传统软件工程质量维度和 AI 特有的质量维度。

作为完全开源的 AI 编程助手(Apache License 2.0),MonkeyCode 的测试策略不仅服务于内部质量保证,更是一份公开的质量承诺——让每一位用户、贡献者和企业客户都能了解我们如何确保产品的可靠性、安全性和有效性。

本文将系统性地分享 MonkeyCode 从单元测试到 AI 效果评估的完整测试体系,包括测试金字塔设计、AI 输出质量评估方法、自动化测试基础设施,以及开源社区如何参与质量共建。

🎯 核心信息


一、AI 编程助手的质量挑战

1.1 传统测试 vs AI 测试的本质差异

┌─────────────────────────────────────────────────────────────┐
│         传统软件 vs AI 系统的质量维度对比                     │
│                                                             │
│  ══════════════════════════════════════════════════════    │
│                                                             │
│  传统软件测试                          AI 系统测试            │
│  ──────────                          ─────────             │
│  ✅ 确定性输出                       ⚠️ 概率性输出          │
│     (相同输入 → 相同输出)              (相同输入 → 可能不同) │
│                                                             │
│  ✅ 明确的正确答案                  ⚠️ "好"是主观的         │
│     (pass/fail)                      (质量评分而非二元判断)  │
│                                                             │
│  ✅ 完整的覆盖率可度量               ⚠️ 输出空间无限大       │
│     (行/分支/路径覆盖)                (无法穷举所有可能输出) │
│                                                             │
│  ✅ 回归测试稳定                     ⚠️ 模型更新导致行为变化  │
│     (测试用例长期有效)                (需要持续重新评估)      │
│                                                             │
│  ✅ 性能基准明确                    ⚠️ 质量与成本的权衡       │
│     (响应时间/吞吐量)                 (更好的质量 = 更多计算) │
│                                                             │
│  ══════════════════════════════════════════════════════    │
│                                                             │
│  💡 关键洞察: AI 测试不是要替代传统测试,                   │
│     而是在其基础上增加新的评估维度                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 MonkeyCode 质量矩阵

质量维度 子维度 度量指标 目标值 测试方法
功能性 补全准确率 Accept@1, Accept@5 >65%, >85% 基准测试集
代码正确性 通过编译比例 >95% 自动编译验证
功能完整性 需求覆盖率 100% E2E 测试
可靠性 崩溃率 CRASH/1000次调用 <0.1% 错误监控
超时率 Timeout/1000次调用 <1% 性能测试
数据丢失率 数据损坏事件 0 数据完整性检查
性能 首次补全延迟 P50/P99 <200ms/<800ms 性能基准
启动时间 冷启动 <3s 启动性能测试
内存占用 RSS <300MB 内存分析
安全性 注入攻击防护 SQLi/XSS 阻断率 100% 安全扫描
数据隐私 本地处理比例 >99% 审计日志
权限控制 越权访问事件 0 权限测试
可用性 NPS 分数 用户满意度 >60 用户调研
易上手时间 Time-to-first-value <5分钟 可用性测试
错误恢复能力 自愈成功率 >90% 混沌测试

二、MonkeyCode 测试金字塔

2.1 四层测试架构

┌─────────────────────────────────────────────────────────────────────────┐
│                    MonkeyCode 测试金字塔                                  │
│                                                                         │
│                              ▲                                          │
│                             /|\                                         │
│                            / | \                                        │
│                           /  |  \                                       │
│                          /   |   \                                      │
│                         / AI-E2E \                                     │
│                        /   Tests   \        ← 少量但高价值              │
│                       /    (端到端)   \          ~50 个场景             │
│                      /                \       执行时间: ~30min           │
│                     /──────────────────\                                │
│                    /    Integration      \                              │
│                   /       Tests           \    ← 中等数量               │
│                  /      (集成测试)          \      ~500 个用例            │
│                 /                            \  执行时间: ~10min         │
│                /──────────────────────────────\                         │
│               /         Unit Tests            \                         │
│              /           (单元测试)             \ ← 大量快速             │
│             /                                    \  ~5000+ 个用例        │
│            /                                      \ 执行时间: ~3min      │
│           /────────────────────────────────────────\                      │
│          /            Static Analysis               \                    │
│         /              (静态分析)                    \ ← 最多最快         │
│        /                                          \ Lint + Type Check   │
│       └────────────────────────────────────────────┘ 执行时间: ~30s      │
│                                                                         │
│  ══════════════════════════════════════════════════════════════════    │
│                                                                         │
│  数量趋势:  静态分析 >>> 单元测试 >> 集成测试 > AI-E2E 测试              │
│  执行速度:  静态分析 < 单元测试 < 集成测试 << AI-E2E 测试               │
│  反馈速度:  静态分析(秒级) → 单元(分钟) → 集成(分钟) → E2E(小时)       │
│                                                                         │
╚═══════════════════════════════════════════════════════════════════════╝

2.2 各层详细说明

第一层:静态分析 (Static Analysis)

# monkeycode/.eslintrc.yaml (部分配置)
# MonkeyCode 静态分析规则集

rules:
  # === 类型安全 ===
  "@typescript-eslint/no-explicit-any": "error"
  "@typescript-eslint/explicit-function-return-type": "warn"
  "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
  
  # === 代码质量 ===
  "complexity": ["warn", { "max": 15 }]
  "max-lines-per-function": ["warn", { "max": 80, "skipBlankLines": true, "skipComments": true }]
  "max-depth": ["error", { "max": 4 }]
  "max-nested-callbacks": ["error", { "max": 3 }]
  
  # === 安全相关 ===
  "no-eval": "error"
  "no-implied-eval": "error"
  "no-new-func": "error"
  "no-script-url": "error"
  
  # === AI 相关特殊规则 ===
  "monkeyCode/no-hardcoded-api-key": "error"  # 自定义:禁止硬编码 API Key
  "monkeyCode/no-sync-ai-call-in-main-thread": "error"  # 自定义:主线程禁止同步 AI 调用
  "monkeyCode/require-error-handling-for-ai": "error"  # 自定义:AI 调用必须有错误处理
  
  # === 测试相关 ===
  "jest/no-disabled-tests": "warn"
  "jest/no-focused-tests": "error"
  "jest/valid-expect": "error"
// ===== monkeycode/src/core/__tests__/unit/completion-engine.test.ts =====
/**
 * MonkeyCode 补全引擎 — 单元测试示例
 * 
 * 展示如何对 AI 编程助手的核心组件进行单元测试,
 * 包括 mock AI 服务、状态管理、缓存逻辑等。
 */

import { CompletionEngine } from '../completion-engine';
import { MockAIService } from './mocks/mock-ai-service';
import { InMemoryCache } from './mocks/in-memory-cache';

describe('CompletionEngine', () => {
  let engine: CompletionEngine;
  let mockAI: MockAIService;
  let cache: InMemoryCache;

  beforeEach(() => {
    mockAI = new MockAIService();
    cache = new InMemoryCache();
    engine = new CompletionEngine({
      aiService: mockAI,
      cache,
      maxCacheSize: 1000,
      cacheTTL: 3600000, // 1 hour
    });
  });

  afterEach(() => {
    jest.clearAllMocks();
    engine.dispose();
  });

  // ========== 基本功能测试 ==========

  describe('basic completion', () => {
    it('should return completion for valid request', async () => {
      const request = {
        filePath: '/test.ts',
        language: 'typescript',
        position: { line: 0, column: 6 },
        content: 'function add(a, b) {\n  return a + ',
      };

      mockAI.setMockResponse({
        choices: [{ text: 'b;\n}' }],
        model: 'test-model',
        usage: { promptTokens: 10, completionTokens: 5 },
      });

      const result = await engine.complete(request);

      expect(result).toBeDefined();
      expect(result.completions).toHaveLength(1);
      expect(result.completions[0].text).toBe('b;\n}');
      expect(result.model).toBe('test-model');
    });

    it('should handle empty content gracefully', async () => {
      const request = {
        filePath: '/empty.js',
        language: 'javascript',
        position: { line: 0, column: 0 },
        content: '',
      };

      const result = await engine.complete(request);

      // 空文件应该返回空结果而不是报错
      expect(result).toBeDefined();
      expect(result.completions).toEqual([]);
    });
  });

  // ========== 缓存行为测试 ==========

  describe('caching behavior', () => {
    it('should return cached result for identical request', async () => {
      const request = {
        filePath: '/cached.ts',
        language: 'typescript',
        position: { line: 0, column: 10 },
        content: 'const hello = "hello"',
      };

      mockAI.setMockResponse({
        choices: [{ text: 'world";' }],
        model: 'test',
        usage: { promptTokens: 5, completionTokens: 2 },
      });

      // 第一次调用
      const result1 = await engine.complete(request);
      expect(mockAI.callCount).toBe(1);

      // 第二次调用应该命中缓存
      const result2 = await engine.complete(request);
      expect(mockAI.callCount).toBe(1); // 不应该再次调用 AI
      expect(result2.completions[0].text).toBe(result1.completions[0].text);
    });

    it('should invalidate cache when content changes', async () => {
      const baseRequest = {
        filePath: '/cache-test.ts',
        language: 'typescript',
        position: { line: 0, column: 15 },
        content: 'let x = "original"',
      };

      mockAI.setMockResponse({
        choices: [{ text: '"' }],
        model: 'test',
        usage: { promptTokens: 5, completionTokens: 1 },
      });

      await engine.complete(baseRequest);

      // 修改内容后应该产生新的请求
      const modifiedRequest = {
        ...baseRequest,
        content: 'let x = "modified"',
      };

      mockAI.setMockResponse({
        choices: [{ text = '";' }],
        model: 'test',
        usage: { promptTokens: 5, completionTokens: 1 },
      });

      await engine.complete(modifiedRequest);
      expect(mockAI.callCount).toBe(2); // 应该有两次 AI 调用
    });
  });

  // ========== 错误处理测试 ==========

  describe('error handling', () => {
    it('should handle AI service timeout gracefully', async () => {
      const request = {
        filePath: '/timeout.ts',
        language: 'typescript',
        position: { line: 0, column: 0 },
        content: 'some code',
      };

      mockAI.setMockError(new Error('Request timeout'));

      const result = await engine.complete(request);

      expect(result.error).toBeDefined();
      expect(result.error.code).toBe('TIMEOUT');
      expect(result.completions).toEqual([]);
      // 不应该抛出异常
    });

    it('should handle rate limiting with retry', async () => {
      const request = {
        filePath: '/rate-limit.ts',
        language: 'typescript',
        position: { line: 0, column: 0 },
        content: 'code here',
      };

      // 前两次返回 rate limit 错误
      mockAI.setMockSequence([
        new Error('Rate limit exceeded'),
        new Error('Rate limit exceeded'),
        {
          choices: [{ text: 'result' }],
          model: 'test',
          usage: { promptTokens: 5, completionTokens: 1 },
        },
      ]);

      const result = await engine.complete(request);

      expect(result.completions).toHaveLength(1);
      expect(mockAI.callCount).toBe(3); // 2 次失败 + 1 次成功
    });

    it('should handle malformed AI response', async () => {
      const request = {
        filePath: '/malformed.ts',
        language: 'typescript',
        position: { line: 0, column: 0 },
        content: 'code',
      };

      // 返回格式错误的响应
      mockAI.setMalformedResponse({ invalid: 'data' });

      const result = await engine.complete(request);

      expect(result.error).toBeDefined();
      expect(result.error.code).toBe('INVALID_RESPONSE');
    });
  });

  // ========== 并发安全测试 ==========

  describe('concurrency safety', () => {
    it('should handle multiple concurrent requests', async () => {
      const requests = Array.from({ length: 20 }, (_, i) => ({
        filePath: `/concurrent-${i}.ts`,
        language: 'typescript' as const,
        position: { line: 0, column: i },
        content: `const v${i} = `,
      }));

      mockAI.setMockResponse({
        choices: [{ text: `${Math.random()};` }],
        model: 'test',
        usage: { promptTokens: 5, completionTokens: 1 },
      });

      // 并发发送 20 个请求
      const results = await Promise.all(
        requests.map(r => engine.complete(r))
      );

      // 所有请求都应该成功完成
      expect(results.every(r => r.completions.length > 0)).toBe(true);
      // AI 服务应该被调用了 20 次
      expect(mockAI.callCount).toBe(20);
    });

    it('should deduplicate identical concurrent requests', async () => {
      const sameRequest = {
        filePath: '/dedup.ts',
        language: 'typescript' as const,
        position: { line: 0, column: 5 },
        content: 'const x = ',
      };

      mockAI.setMockResponse({
        choices: [{ text: '1;' }],
        model: 'test',
        usage: { promptTokens: 5, completionTokens: 1 },
      });

      // 同时发送 5 个相同的请求
      const promises = Array.from({ length: 5 }, () =>
        engine.complete(sameRequest)
      );
      const results = await Promise.all(promises);

      // 由于去重机制,AI 只应该被调用一次
      expect(mockAI.callCount).toBeLessThanOrEqual(2);
      // 所有结果都应该一致
      const texts = results.map(r => r.completions[0]?.text);
      expect(new Set(texts).size).toBe(1);
    });
  });
});

第二层:集成测试 (Integration Tests)

# ===== monkeycode/tests/integration/test_editor_integration.py =====
"""
MonkeyCode 编辑器集成测试

测试 MonkeyCode 与 VSCode/JetBrains 编辑器的集成行为,
包括扩展激活、命令注册、事件处理等。
"""

import pytest
import asyncio
from typing import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock


@pytest.fixture
async def vscode_extension() -> AsyncGenerator:
    """创建 VSCode 扩展模拟实例"""
    extension = MagicMock()
    extension.activate = AsyncMock()
    extension.deactivate = AsyncMock()
    
    # 模拟 VSCode API
    extension.vscode_api = MagicMock()
    extension.vscode_api.registerCommand = MagicMock(return_value=MagicMock())
    extension.vscode_api.workspace = MagicMock()
    extension.vscode_api.window = MagicMock()
    
    yield extension
    await extension.deactivate()


class TestVSCodeExtensionActivation:
    """VSCode 扩展激活测试"""
    
    @pytest.mark.asyncio
    async def test_extension_activates_successfully(self, vscode_extension):
        """扩展应成功激活"""
        from monkeycode.vscode.extension import activate
        
        context = MagicMock()
        result = await activate(vscode_extension, context)
        
        assert result.status == 'ok'
        assert vscode_extension.activate.called
    
    @pytest.mark.asyncio
    async def test_commands_registered_on_activation(self, vscode_extension):
        """激活时应注册所有预期命令"""
        from monkeycode.vscode.extension import EXPECTED_COMMANDS
        
        context = MagicMock()
        await activate(vscode_extension, context)
        
        registered_calls = [
            call.args[0] for call in 
            vscode_extension.vscode_api.registerCommand.call_args_list
        ]
        
        for cmd in EXPECTED_COMMANDS:
            assert cmd in registered_calls, f"Command '{cmd}' not registered"


class TestEditorIntegration:
    """编辑器交互集成测试"""
    
    @pytest.mark.asyncio
    async def test_completion_triggered_on_typing(self, vscode_extension):
        """输入时应触发补全"""
        from monkeycode.core import CompletionTrigger
        
        trigger = CompletionTrigger(vscode_extension)
        
        # 模拟打字事件
        event = {
            'type': 'textChange',
            'document': {'languageId': 'typescript'},
            'position': {'line': 0, 'column': 10},
            'text': 'func',
        }
        
        should_trigger = await trigger.should_trigger(event)
        assert should_trigger is True
    
    @pytest.mark.asyncio
    async def test_file_save_triggers_analysis(self, vscode_extension):
        """保存文件应触发分析"""
        from monkeycode.core import FileAnalyzer
        
        analyzer = FileAnalyzer(vscode_extension)
        
        save_event = {
            'uri': 'file:///project/src/main.ts',
            'content': 'export function hello() {}',
        }
        
        analysis_result = await analyzer.analyze_on_save(save_event)
        
        assert analysis_result is not None
        assert analysis_result.language == 'typescript'
        assert analysis_result.functions_count >= 1


class TestMultiFileContext:
    """多文件上下文测试"""
    
    @pytest.mark.asyncio
    async def test_cross_file_reference_resolution(self, vscode_extension):
        """跨文件引用解析"""
        from monkeycode.context import ContextBuilder
        
        builder = ContextBuilder(vscode_extension)
        
        # 模拟打开多个文件
        files = {
            'types.ts': 'interface User { name: string; age: number; }',
            'utils.ts': 'function createUser(): User { return { name: "", age: 0 }; }',
            'main.ts': 'const user = createUser();',
        }
        
        context = await builder.build_context(files, active_file='main.ts')
        
        # 应该包含跨文件的类型信息
        assert 'User' in context.type_definitions
        assert 'createUser' in context.function_signatures
    
    @pytest.mark.asyncio
    async def test_context_size_limit(self, vscode_extension):
        """上下文大小限制"""
        from monkeycode.context import ContextBuilder
        
        builder = ContextBuilder(
            vscode_extension, 
            max_context_tokens=4000
        )
        
        # 创建一个大文件
        large_content = '\n'.join([f'// line {i}' for i in range(10000)])
        files = {'large.ts': large_content}
        
        context = await builder.build_context(files, active_file='large.ts')
        
        # 上下文应在限制范围内
        assert context.token_count <= 4500  # 允许一定余量


# ===== monkeycode/tests/integration/test_ai_provider_integration.py =====
"""
MonkeyCode AI Provider 集成测试

测试各种 AI 后端的集成:
- OpenAI GPT 系列
- Anthropic Claude 系列
- 开源模型 (Ollama/vLLM)
- Azure OpenAI
"""

import pytest
import httpx
from unittest.mock import patch, AsyncMock


class TestOpenAIIntegration:
    """OpenAI API 集成测试"""
    
    @pytest.fixture
    def openai_config(self):
        return {
            'provider': 'openai',
            'api_key': 'test-key',
            'model': 'gpt-4',
            'base_url': 'https://api.openai.com/v1',
            'max_tokens': 256,
            'temperature': 0.2,
        }
    
    @pytest.mark.asyncio
    @patch('httpx.AsyncClient.post')
    async def test_successful_completion(self, mock_post, openai_config):
        """成功的补全请求"""
        from monkeycode.ai.providers.openai import OpenAIProvider
        
        mock_response = AsyncMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            'id': 'cmpl-test',
            'choices': [{
                'index': 0,
                'text': 'return a + b;',
                'finish_reason': 'stop',
            }],
            'usage': {'prompt_tokens': 50, 'completion_tokens': 8},
        }
        mock_post.return_value = mock_response
        
        provider = OpenAIProvider(openai_config)
        result = await provider.complete(
            prompt='function sum(a, b) {',
            language='javascript'
        )
        
        assert result.text == 'return a + b;'
        assert result.finish_reason == 'stop'
        assert result.usage.prompt_tokens == 50
    
    @pytest.mark.asyncio
    @patch('httpx.AsyncClient.post')
    async def test_error_handling_429(self, mock_post, openai_config):
        """Rate Limit 错误处理"""
        from monkeycode.ai.providers.openai import OpenAIProvider
        from monkeycode.ai.errors import RateLimitError
        
        mock_response = AsyncMock()
        mock_response.status_code = 429
        mock_response.headers = {'Retry-After': '5'}
        mock_post.return_value = mock_response
        
        provider = OpenAIProvider(openai_config)
        
        with pytest.raises(RateLimitError) as exc_info:
            await provider.complete(prompt='test')
        
        assert exc_info.value.retry_after == 5


class TestOllamaIntegration:
    """本地 Ollama 模型集成测试"""
    
    @pytest.fixture
    def ollama_config(self):
        return {
            'provider': 'ollama',
            'base_url': 'http://localhost:11434',
            'model': 'codellama:13b',
            'max_tokens': 512,
        }
    
    @pytest.mark.asyncio
    @patch('httpx.AsyncClient.post')
    async def test_local_model_connection(self, mock_post, ollama_config):
        """本地模型连接测试"""
        from monkeycode.ai.providers.ollama import OllamaProvider
        
        mock_response = AsyncMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            'response': 'console.log("Hello");',
            'done': True,
        }
        mock_post.return_value = mock_response
        
        provider = OllamaProvider(ollama_config)
        
        # 测试连接
        is_connected = await provider.health_check()
        assert is_connected is True
        
        # 测试补全
        result = await provider.complete(prompt='// Log hello')
        assert 'console.log' in result.text

第三层:AI 效果评估 (AI Effectiveness Evaluation)

# ===== monkeycode/tests/evaluation/benchmark_runner.py =====
"""
MonkeyCode AI 效果评估框架

使用标准化的 benchmark 数据集评估 AI 补全质量。
支持多种评估指标:
- Exact Match: 完全匹配率
- Edit Similarity: 编辑相似度 (Levenshtein)
- BLEU/ROUGE: N-gram 重叠度
- CodeBLEU: 代码专用 BLEU 变体
- Executable: 可执行通过率
- Human Preference: 人工偏好评分 (抽样)
"""

import json
import asyncio
import statistics
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from pathlib import Path
from datetime import datetime


@dataclass
class EvaluationSample:
    """单个评估样本"""
    sample_id: str
    file_path: str
    language: str
    prefix: str       # 光标前的代码
    suffix: str       # 光标后的代码
    expected: List[str]  # 一个或多个期望的补全结果
    metadata: Dict = field(default_factory=dict)


@dataclass
class PredictionResult:
    """预测结果"""
    sample_id: str
    predictions: List[str]  # 模型生成的多个候选
    latency_ms: float
    tokens_used: int
    model_name: str
    timestamp: float


@dataclass
class MetricResult:
    """指标结果"""
    metric_name: str
    value: float
    confidence_interval: Tuple[float, float]
    sample_count: int
    details: Dict = field(default_factory=dict)


class BenchmarkRunner:
    """Benchmark 运行器"""
    
    def __init__(
        self,
        dataset_path: Path,
        output_dir: Path,
        num_predictions: int = 5,  # 生成多少个候选
        max_samples: Optional[int] = None,
    ):
        self.dataset_path = dataset_path
        self.output_dir = output_dir
        self.num_predictions = num_predictions
        self.max_samples = max_samples
        self.results: List[PredictionResult] = []
        
        self.output_dir.mkdir(parents=True, exist_ok=True)
    
    def load_dataset(self) -> List[EvaluationSample]:
        """加载 benchmark 数据集"""
        samples = []
        
        # 支持多种数据集格式
        if self.dataset_path.suffix == '.jsonl':
            with open(self.dataset_path) as f:
                for line in f:
                    data = json.loads(line.strip())
                    samples.append(EvaluationSample(**data))
        elif self.dataset_path.is_dir():
            # 目录模式:每个 .json 文件是一个样本
            for json_file in self.dataset_path.glob('*.json'):
                with open(json_file) as f:
                    data = json.load(f)
                    samples.append(EvaluationSample(**data))
        
        if self.max_samples:
            samples = samples[:self.max_samples]
        
        print(f"📊 Loaded {len(samples)} evaluation samples")
        return samples
    
    async def run_evaluation(self, engine) -> List[PredictionResult]:
        """运行评估"""
        samples = self.load_dataset()
        results = []
        
        total = len(samples)
        for i, sample in enumerate(samples):
            try:
                start_time = asyncio.get_event_loop().time()
                
                # 调用引擎生成补全
                response = await engine.complete({
                    'filePath': sample.file_path,
                    'language': sample.language,
                    'position': self._find_position(sample.prefix),
                    'content': sample.prefix + sample.suffix,
                    'numPredictions': self.num_predictions,
                })
                
                latency = (asyncio.get_event_loop().time() - start_time) * 1000
                
                result = PredictionResult(
                    sample_id=sample.sample_id,
                    predictions=[c.text for c in response.completions],
                    latency_ms=latency,
                    tokens_used=response.usage.total_tokens if response.usage else 0,
                    model_name=response.model or 'unknown',
                    timestamp=datetime.now().timestamp(),
                )
                results.append(result)
                
                # 进度显示
                if (i + 1) % 10 == 0 or i == total - 1:
                    print(f"  Progress: {i+1}/{total} ({(i+1)/total*100:.1f}%)")
                    
            except Exception as e:
                print(f"  ⚠️ Error on sample {sample.sample_id}: {e}")
                # 记录空结果
                results.append(PredictionResult(
                    sample_id=sample.sample_id,
                    predictions=[],
                    latency_ms=-1,
                    tokens_used=0,
                    model_name='error',
                    timestamp=datetime.now().timestamp(),
                ))
        
        self.results = results
        return results
    
    def compute_metrics(self) -> List[MetricResult]:
        """计算所有评估指标"""
        metrics = []
        
        # 1. Exact Match Rate (完全匹配率)
        em_rate = self._compute_exact_match()
        metrics.append(em_rate)
        
        # 2. Edit Similarity (编辑相似度)
        es_metric = self._compute_edit_similarity()
        metrics.append(es_metric)
        
        # 3. Accept@k (前 k 个候选中至少有一个被接受)
        accept_metrics = self._compute_accept_at_k(k_values=[1, 3, 5])
        metrics.extend(accept_metrics)
        
        # 4. Latency Metrics (延迟指标)
        latency_metrics = self._compute_latency_metrics()
        metrics.extend(latency_metrics)
        
        # 5. Per-Language Breakdown (分语言统计)
        lang_metrics = self._compute_per_language_metrics()
        metrics.extend(lang_metrics)
        
        return metrics
    
    def _compute_exact_match(self) -> MetricResult:
        """计算完全匹配率"""
        matches = 0
        total = len(self.results)
        
        for result in self.results:
            # 找到对应的样本
            sample = next(
                (s for s in self.load_dataset() if s.sample_id == result.sample_id),
                None
            )
            if not sample or not result.predictions:
                continue
            
            # 检查是否有预测完全匹配任何期望结果
            for pred in result.predictions:
                if pred.strip() in [e.strip() for e in sample.expected]:
                    matches += 1
                    break
        
        rate = matches / total if total > 0 else 0
        
        return MetricResult(
            metric_name='ExactMatch',
            value=round(rate * 100, 2),
            confidence_interval=self._bootstrap_ci(
                lambda r: any(p.strip() in s.expected for p in r.predictions 
                             for s in [next(x for x in self.load_dataset() 
                                           if x.sample_id == r.sample_id)] 
                             if r.predictions),
                n_bootstrap=1000
            ),
            sample_count=total,
        )
    
    def _compute_edit_similarity(self) -> MetricResult:
        """计算编辑相似度 (归一化 Levenshtein)"""
        import difflib
        
        similarities = []
        
        for result in self.results:
            if not result.predictions:
                continue
            
            sample = next(
                (s for s in self.load_dataset() if s.sample_id == result.sample_id),
                None
            )
            if not sample:
                continue
            
            best_sim = 0
            for pred in result.predictions[:1]:  # 取最佳预测
                for expected in sample.expected:
                    sim = difflib.SequenceMatcher(None, pred.strip(), expected.strip()).ratio()
                    best_sim = max(best_sim, sim)
            
            similarities.append(best_sim)
        
        avg_sim = statistics.mean(similarities) if similarities else 0
        
        return MetricResult(
            metric_name='EditSimilarity',
            value=round(avg_sim * 100, 2),
            confidence_interval=(
                round(avg_sim * 100 - 2, 2),
                round(avg_sim * 100 + 2, 2),
            ),
            sample_count=len(similarities),
        )
    
    def _compute_accept_at_k(self, k_values=[1, 3, 5]) -> List[MetricResult]:
        """计算 Accept@k 指标"""
        metrics = []
        
        for k in k_values:
            accepts = 0
            total = len(self.results)
            
            for result in self.results:
                if not result.predictions:
                    continue
                
                sample = next(
                    (s for s in self.load_dataset() if s.sample_id == result.sample_id),
                    None
                )
                if not sample:
                    continue
                
                top_k = result.predictions[:k]
                for pred in top_k:
                    if pred.strip() in [e.strip() for e in sample.expected]:
                        accepts += 1
                        break
            
            rate = accepts / total if total > 0 else 0
            
            metrics.append(MetricResult(
                metric_name=f'Accept@{k}',
                value=round(rate * 100, 2),
                confidence_interval=(round(rate*100-1, 2), round(rate*100+1, 2)),
                sample_count=total,
            ))
        
        return metrics
    
    def _compute_latency_metrics(self) -> List[MetricResult]:
        """计算延迟指标"""
        latencies = [r.latency_ms for r in self.results if r.latency_ms > 0]
        
        if not latencies:
            return [
                MetricResult('Latency_P50', 0, (0, 0), 0),
                MetricResult('Latency_P99', 0, (0, 0), 0),
            ]
        
        sorted_lat = sorted(latencies)
        p50 = sorted_lat[int(len(sorted_lat) * 0.5)]
        p99 = sorted_lat[min(int(len(sorted_lat) * 0.99), len(sorted_lat)-1)]
        mean_lat = statistics.mean(latencies)
        
        return [
            MetricResult('Latency_P50', round(p50, 1), (p50-20, p50+20), len(latencies)),
            MetricResult('Latency_P99', round(p99, 1), (p99-50, p99+50), len(latencies)),
            MetricResult('Latency_Mean', round(mean_lat, 1), (mean_lat-10, mean_lat+10), len(latencies)),
        ]
    
    def generate_report(self) -> str:
        """生成评估报告"""
        metrics = self.compute_metrics()
        
        report_lines = [
            '# MonkeyCode AI 效果评估报告',
            f'\n📅 评估时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
            f'📊 样本总数: {len(self.results)}',
            '',
            '## 📈 核心指标',
            '',
            '| 指标 | 数值 | 置信区间 | 样本数 |',
            '|------|------|---------|--------|',
        ]
        
        for m in metrics:
            ci_str = f"[{m.confidence_interval[0]}, {m.confidence_interval[1]}]"
            report_lines.append(
                f"| {m.metric_name} | **{m.value}** | {ci_str} | {m.sample_count} |"
            )
        
        report_lines.extend([
            '',
            '## 📋 详细分析',
            '',
            '### 各语言表现',
            '',
        ])
        
        # 按语言分组统计
        lang_stats = {}
        for m in metrics:
            if m.metric_name.startswith('Lang_'):
                lang = m.metric_name.replace('Lang_', '')
                lang_stats[lang] = m.value
        
        for lang, score in sorted(lang_stats.items(), key=lambda x: -x[1]):
            bar_len = int(score / 5)
            bar = '█' * bar_len + '░' * (20 - bar_len)
            report_lines.append(f"| {lang:<12} | {bar} | {score:.1f}% |")
        
        report_content = '\n'.join(report_lines)
        
        # 保存报告
        report_path = self.output_dir / f'report_{datetime.now().strftime("%Y%m%d_%H%M%S")}.md'
        report_path.write_text(report_content)
        
        print(f"\n✅ Report saved to: {report_path}")
        return report_content
    
    @staticmethod
    def _find_position(prefix: str) -> dict:
        """从 prefix 中推断光标位置"""
        lines = prefix.split('\n')
        return {
            'line': len(lines) - 1,
            'column': len(lines[-1]),
        }
    
    def _bootstrap_ci(self, scoring_fn, n_bootstrap=1000) -> tuple:
        """Bootstrap 置信区间"""
        import random
        scores = [scoring_fn(r) for r in self.results if r.predictions]
        
        if not scores:
            return (0, 0)
        
        bootstrapped = []
        for _ in range(n_bootstrap):
            sample = [scores[random.randint(0, len(scores)-1)] for _ in scores]
            bootstrapped.append(statistics.mean(sample))
        
        bootstrapped.sort()
        lower = bootstrapped[int(n_bootstrap * 0.025)]
        upper = bootstrapped[int(n_bootstrap * 0.975)]
        
        return (round(lower, 2), round(upper, 2))
    
    def _compute_per_language_metrics(self) -> List[MetricResult]:
        """分语言计算指标"""
        # 收集所有样本的语言信息
        samples_by_lang = {}
        for sample in self.load_dataset():
            lang = sample.language
            if lang not in samples_by_lang:
                samples_by_lang[lang] = []
            samples_by_lang[lang].append(sample.sample_id)
        
        metrics = []
        for lang, sample_ids in samples_by_lang.items():
            lang_results = [r for r in self.results if r.sample_id in sample_ids]
            if not lang_results:
                continue
            
            matches = sum(
                1 for r in lang_results if r.predictions and any(
                    p.strip() in s.expected
                    for p in r.predictions[:1]
                    for s in [next(x for x in self.load_dataset() 
                                 if x.sample_id == r.sample_id)]
                )
            )
            
            rate = matches / len(lang_results) * 100
            metrics.append(MetricResult(
                metric_name=f'Lang_{lang}',
                value=round(rate, 2),
                confidence_interval=(round(rate-3, 2), round(rate+3, 2)),
                sample_count=len(lang_results),
            ))
        
        return metrics


# 使用示例
if __name__ == '__main__':
    runner = BenchmarkRunner(
        dataset_path=Path('./benchmark_datasets/human_eval'),
        output_dir=Path('./eval_results'),
        num_predictions=5,
        max_samples=None,  # 使用全部样本
    )
    
    import asyncio
    from monkeycode.core.engine import CompletionEngine
    
    engine = CompletionEngine.from_config('./config/default.yaml')
    
    # 运行评估
    asyncio.run(runner.run_evaluation(engine))
    
    # 生成报告
    report = runner.generate_report()
    print(report)

三、自动化 CI/CD 测试流水线

3.1 GitHub Actions 工作流

# ===== .github/workflows/ci.yml =====
name: MonkeyCode CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    # 每天 UTC 2:00 (北京时间 10:00) 运行 nightly 测试
    - cron: '0 2 * * *'

env:
  NODE_VERSION: '20'
  PYTHON_VERSION: '3.11'

jobs:
  # === Job 1: Lint & 类型检查 ===
  lint:
    name: 🔍 Lint & Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: ESLint
        run: npm run lint
      
      - name: TypeScript type check
        run: npm run typecheck
      
      - name: Format check (Prettier)
        run: npm run format:check

  # === Job 2: 单元测试 ===
  unit-tests:
    name: 🧪 Unit Tests
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: npm run test:unit -- --coverage --ci
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          fail_ci_if_error: false

  # === Job 3: 集成测试 ===
  integration-tests:
    name: 🔗 Integration Tests
    runs-on: ubuntu-latest
    needs: [lint, unit-tests]
    services:
      ollama:
        image: ollama/ollama:latest
        ports:
          - 11434:11434
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      
      - name: Install dependencies
        run: |
          npm ci
          pip install -r tests/requirements.txt
      
      - name: Pull Ollama models
        run: docker exec ollama ollama pull codellama:7b
      
      - name: Run integration tests
        run: npm run test:integration
        env:
          AI_PROVIDER: ollama
          OLLAMA_BASE_URL: http://localhost:11434
          REDIS_URL: redis://localhost:6379

  # === Job 4: AI 效果评估 (Nightly) ===
  ai-evaluation:
    name: 🤖 AI Evaluation
    runs-on: ubuntu-latest
    needs: [integration-tests]
    if: github.event_name == 'schedule' || github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      
      - name: Install dependencies
        run: pip install -r tests/eval_requirements.txt
      
      - name: Download benchmark datasets
        run: |
          mkdir -p benchmark_datasets
          curl -L -o benchmark_datasets/human_eval.zip \
            https://storage.googleapis.com/monkeyCode-benchmarks/latest.zip
          unzip benchmark_datasets/human_eval.zip -d benchmark_datasets/
      
      - name: Run AI evaluation
        run: |
          python tests/evaluation/run_benchmark.py \
            --dataset benchmark_datasets/human_eval \
            --output eval_results \
            --model gpt-4 \
            --num-samples 500
        env:
          OPENAI_API_KEY: ${{ secrets.EVAL_OPENAI_KEY }}
      
      - name: Upload evaluation results
        uses: actions/upload-artifact@v4
        with:
          name: ai-evaluation-results
          path: eval_results/
          retention-days: 90
      
      - name: Check quality gates
        run: |
          python tests/eval/check_quality_gates.py \
            --results eval_results/report_latest.md \
            --threshold-accept1 60 \
            --threshold-latency-p99 1000

  # === Job 5: 安全扫描 ===
  security:
    name: 🔒 Security Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'
      
      - name: Run npm audit
        run: npm audit --audit-level=high
      
      - name: CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          languages: javascript, typescript, python

  # === Job 6: E2E 测试 ===
  e2e-tests:
    name: 🌐 E2E Tests
    runs-on: ubuntu-latest
    needs: [lint, unit-tests]
    strategy:
      matrix:
        browser: [chromium, firefox]
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}
      
      - name: Run E2E tests
        run: npm run test:e2e
        env:
          PLAYWRIGHT_BROWSER: ${{ matrix.browser }}
      
      - name: Upload test artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: e2e-results-${{ matrix.browser }}
          path: test-results/
          retention-days: 7

四、质量门禁 (Quality Gates)

4.1 分级质量标准

# ===== monkeycode/quality-gates.yaml =====
quality_gates:

  # === 必须通过的门槛 (Blocking) ===
  
  must_pass:
    - name: "单元测试通过率"
      metric: unit_test_pass_rate
      operator: ">="
      threshold: 100
      message: "所有单元测试必须通过"
      
    - name: "无新增编译错误"
      metric: compile_errors
      operator: "=="
      threshold: 0
      message: "不允许引入新的编译错误"
      
    - name: "无 CRITICAL 安全漏洞"
      metric: critical_vulnerabilities
      operator: "=="
      threshold: 0
      message: "必须修复所有 CRITICAL 级别安全漏洞"
      
    - name: "类型检查通过"
      metric: typescript_errors
      operator: "=="
      threshold: 0
      message: "TypeScript 类型检查必须通过"

  # === 应该达到的标准 (Warning) ===
  
  should_pass:
    - name: "代码覆盖率"
      metric: code_coverage
      operator: ">="
      threshold: 80
      message: "代码覆盖率建议 ≥ 80%"
      
    - name: "Accept@1 准确率"
      metric: accept_at_1
      operator: ">="
      threshold: 60
      message: "AI 补全首选项接受率建议 ≥ 60%"
      
    - name: "P99 延迟"
      metric: latency_p99_ms
      operator: "<="
      threshold: 1000
      message: "P99 延迟建议 ≤ 1000ms"
      
    - name: "崩溃率"
      metric: crash_rate_per_1000
      operator: "<="
      threshold: 1
      message: "每千次调用崩溃率建议 ≤ 0.1%"
      
    - name: "ESLint 错误数"
      metric: eslint_errors
      operator: "=="
      threshold: 0
      message: "不应有 ESLint error 级别问题"

  # === 优秀标准 (Stretch Goals) ===
  
  excellent:
    - name: "Accept@5 准确率"
      metric: accept_at_5
      operator: ">="
      threshold: 88
      message: "优秀: 前5项接受率 ≥ 88%"
      
    - name: "P50 延迟"
      metric: latency_p50_ms
      operator: "<="
      threshold: 200
      message: "优秀: 中位延迟 ≤ 200ms"
      
    - name: "代码覆盖率"
      metric: code_coverage
      operator: ">="
      threshold: 90
      message: "优秀: 覆盖率 ≥ 90%"

4.2 质量仪表盘关键指标

┌─────────────────────────────────────────────────────────────┐
│         📊 MonkeyCode 质量仪表盘 (实时)                      │
│                                                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │  ✅ 单元测试  │  │  🟢 覆盖率   │  │  🟢 ESLint   │     │
│  │  通过率: 100% │  │  84.2%      │  │  0 errors    │     │
│  │  5234/5234   │  │  目标: 80%+  │  │  3 warnings │     │
│  └──────────────┘  └──────────────┘  └──────────────┘     │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  🤖 AI 效果指标 (最近 24h)                             │  │
│  │                                                       │  │
│  │  Accept@1:  ████████████████░░░░  67.3%  🟢 目标≥60% │  │
│  │  Accept@5:  ████████████████████░  89.1%  🟢 目标≥85% │  │
│  │  ExactMatch: ██████████░░░░░░░░░░  42.8%             │  │
│  │  EditSim:   ████████████████████░  87.5%             │  │
│  │                                                       │  │
│  │  P50 延迟:   ████░░░░░░░░░░░░░░░░░  185ms 🟢 ≤200ms  │  │
│  │  P99 延迟:   ████████████████████░  782ms 🟢 ≤1000ms │  │
│  │  错误率:     ██░░░░░░░░░░░░░░░░░░░░  0.03% 🟢 ≤0.1% │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  🔒 安全状态                                           │  │
│  │  CRITICAL: 0  HIGH: 2  MEDIUM: 12  LOW: 28           │  │
│  │  上次扫描: 2 小时前                                    │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
╚═══════════════════════════════════════════════════════════╝

五、开源社区的参与方式

5.1 如何贡献测试用例

## 为 MonkeyCode 贡献测试用例指南

### 为什么需要你的贡献?

MonkeyCode 的 AI 能力在不断进化,我们需要更多真实场景的测试样本来:
- 发现在特定语言/框架下的边界情况
- 提升对罕见编程模式的识别能力
- 确保 AI 输出的多样性和准确性

### 如何提交测试用例?

#### 方式一:提交 Benchmark 样本

1. Fork [monkeycode-ai/benchmark-datasets](https://github.com/monkeycode-ai/benchmark-datasets) 仓库
2. 在 `contributions/` 目录下创建 JSON 文件:

```json
{
  "sample_id": "contrib-yourname-001",
  "file_path": "src/components/UserForm.tsx",
  "language": "typescript",
  "prefix": "interface UserFormProps {\n  onSubmit: (data: UserData) => void;\n  initialData?: ",
  "suffix": ";\n}\n\nexport function UserForm({ onSubmit, initialData }: UserFormProps) {\n  return <form>",
  "expected": [
    "Partial<UserData>;",
    "UserData | undefined;"
  ],
  "difficulty": "medium",
  "tags": ["react", "typescript", "interfaces"],
  "contributor": "your-github-username",
  "notes": "Testing React component prop inference with optional generic type"
}
  1. 提交 PR,我们会审核并合入到下一个版本

方式二:报告 AI 输出问题

如果你发现 MonkeyCode 的输出有问题,请通过以下方式反馈:

  1. GitHub Issue (推荐):

    • 前往 Issues
    • 选择模板 AI Output Issue
    • 填写触发条件、期望输出、实际输出
    • 如果方便的话,附上截图或录屏
  2. 内置反馈按钮:

    • 在 IDE 中点击 AI 补全结果旁的 👎 按钮
    • 选择问题类型并提交
    • 反馈会自动包含上下文信息

贡献者激励

  • 🏆 Top Contributor 月榜: 每月评选最多高质量样本的贡献者
  • 📛 荣誉墙: 在 README 和官网展示活跃贡献者
  • 🎁 限量周边: MonkeyCode 定制 T恤、贴纸等
  • 💼 优先体验: 新功能内测资格

---

## 六、经验总结与未来方向

### 6.1 核心经验总结

┌─────────────────────────────────────────────────────────────┐
│ MonkeyCode 测试策略核心经验 │
│ │
│ 1. 测试金字塔依然适用,但顶层需要重新定义 │
│ AI 的"端到端测试"不是验证功能流程,而是验证输出质量 │
│ │
│ 2. 自动化评估 + 人工抽检相结合 │
│ 自动化 benchmark 保证基线,人工评审发现微妙质量问题 │
│ │
│ 3. 回归测试是 AI 产品最大的挑战 │
│ 模型更新可能导致已有测试全部失效,需要建立鲁棒的评估集 │
│ │
│ 4. 性能和质量是不可分割的 │
│ 更好的模型通常意味着更高的延迟,需要找到最优平衡点 │
│ │
│ 5. 社区参与是提升测试覆盖率的最佳途径 │
│ 真实用户的场景比任何合成数据都有价值 │
│ │
│ 6. 安全测试不能事后补救 │
│ AI 输入/输出的安全校验必须在设计阶段就纳入 │
│ │
└─────────────────────────────────────────────────────────────┘


### 6.2 未来规划

```yaml
future_testing_plans:

  2026_q3:
    - "引入 Agent 评估: 用更强模型自动评估弱模型的输出质量"
    - "扩展多语言 benchmark: 覆盖 Rust/Go/Swift/Kotlin 等"
    - "实时 A/B 测试框架: 在生产环境对比不同 Prompt 策略"

  2026_q4:
    - "用户行为驱动的测试: 从真实使用数据自动生成回归测试"
    - "对抗性测试: 自动构造 edge case 攻击 AI 输出"
    - "可解释性评估: 评估 AI 输出的可理解程度"

  2027_h1:
    - "联邦学习下的质量评估: 保护隐私的分布式效果评估"
    - "跨 IDE 一致性测试: 确保不同编辑器下行为一致"
    - "自动化质量回滚: 当指标下降时自动阻止发布"

结语

"质量不是测试出来的,而是设计和构建出来的。但对于 AI 产品来说,持续的测试和评估是保持质量的唯一途径。"

MonkeyCode 的测试体系仍在不断演进中。我们相信,通过开放透明的质量标准和社区协作的力量,我们可以构建出一个既智能又可靠的 AI 编程助手。

如果你对 MonkeyCode 的测试策略感兴趣,或者想为我们的 benchmark 数据集贡献力量,欢迎通过 GitHub 与我们交流!

💡 相关资源:

MonkeyCode — 用严格的质量标准,守护每一次智能补全的可靠性。 🐵🧪✨

posted on 2026-06-30 12:51  MonkeyCode  阅读(6)  评论(0)    收藏  举报