From 443ba981ada32580227d7246d5d4df76212c62bc Mon Sep 17 00:00:00 2001 From: gzkoala Date: Sat, 7 Mar 2026 14:21:51 +0800 Subject: [PATCH] =?UTF-8?q?docs(add):=E6=96=B0=E5=A2=9EProject=20Caffeine?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=B5=8B=E8=AF=95=E8=A7=84=E8=8C=83=E6=8C=87?= =?UTF-8?q?=E5=8D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: gzkoala --- ...ffeine-code-testing-specification-guide.md | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 docs/guide/project-caffeine-code-testing-specification-guide.md diff --git a/docs/guide/project-caffeine-code-testing-specification-guide.md b/docs/guide/project-caffeine-code-testing-specification-guide.md new file mode 100644 index 0000000..9483da5 --- /dev/null +++ b/docs/guide/project-caffeine-code-testing-specification-guide.md @@ -0,0 +1,254 @@ + +# Project Caffeine 项目代码测试规范指南 + +为保证 Project Caffeine 提示词策略 MCP Server 的高可用性、健壮性及代码质量,特制定本代码测试规范。本指南主要适用于开发阶段的自动化测试(单元测试与集成测试)及相关最佳实践。 +## 1. 测试工具栈建议 + +本项目推荐使用 **Jest** 作为核心测试框架,搭配 `ts-jest` 无缝支持 TypeScript 原生测试。 + +- **测试框架**: `jest`, `@types/jest` + +- **TypeScript 支持**: `ts-jest` + +- **Mock 工具**: Jest 内置 Mock 功能 (`jest.mock()`) + +- **手动/交互式测试**: MCP Inspector + + +_(如尚未安装,可通过 `npm install --save-dev jest ts-jest @types/jest` 安装,并使用 `npx ts-jest config:init` 初始化配置。)_ + +```bash +npm install --save-dev jest ts-jest @types/jest +npx ts-jest config:init +``` + +--- + +## 2. 测试分层策略 + +基于项目的经典三层架构(接入层 `app.ts` -> 控制层 `controllers` -> 服务层 `services`),测试策略分为以下三个层级: + +### 2.1 服务层单元测试 (Service Unit Tests) + +- **目标**: 验证纯业务逻辑的正确性,这是测试的重中之重。 + +- **重点对象 (以 v0.1.1 为例)**: `intentService.ts` (意图拆解算法), `resourceService.ts` (文件操作), `promptService.ts` (JSON 解析与组装)。 + +- **策略**: 对于纯函数(例如 v0.1.1 中的 `generateSearchQueries`),提供不同的输入(边界值、空值、正常值)验证输出;对于涉及文件系统的操作(例如 `readObsidianNote`),**必须使用 Mock** 拦截原生 `fs` 调用,严禁在单元测试中真实读写物理磁盘。 + +### 2.2 控制层单元测试 (Controller Unit Tests) + +- **目标**: 验证参数的 Zod 校验逻辑以及请求路由分发的正确性。 + +- **重点对象 (以 v0.1.1 为例)**: `promptsController.ts`, `toolsController.ts`。 + +- **策略**: Mock 掉底层的 Service 函数。重点测试:当传入非法参数时(如不带 `.md` 后缀的文件名),Zod Schema 是否能正确拦截并返回 `isError: true` 和标准的 MCP 错误响应格式。 + + +### 2.3 集成/E2E测试 (Integration Tests) + +- **目标**: 验证 MCP Server 与客户端之间的 STDIO 协议通信及功能全链路。 + +- **策略**: 自动化层面投入较少,主要依赖 **MCP Inspector** 进行人工或半自动化点检。 + + +--- + +## 3. 测试文件命名与目录结构 + +- **目录位置**: 测试文件应与被测试的源码文件同级,统一放在同级的 `__tests__` 文件夹中,或直接与源码文件同级。 + +- **命名规范**: 以 `.test.ts` 或 `.spec.ts` 结尾。 + + - 例如(以 v0.1.1 为例):被测文件 `src/services/intentService.ts`,测试文件应为 `src/services/__tests__/intentService.test.ts`。 + +--- + +## 4. 单元测试编写规范 (编写范例) + +### 4.1 纯粹逻辑的测试 (无副作用) + +针对 `intentService.ts` 中的 `generateSearchQueries` 函数,采用 **Given-When-Then** (假设-当-那么) 模式或清晰的用例描述: + +``` +// src/services/__tests__/intentService.test.ts +import { generateSearchQueries } from '../intentService'; + +describe('intentService -> generateSearchQueries', () => { + it('当输入常规查询时,应该正确分词并返回3-5个检索词', () => { + const result = generateSearchQueries('新能源汽车电池回收技术'); + expect(result.length).toBeGreaterThanOrEqual(3); + expect(result.length).toBeLessThanOrEqual(5); + expect(result).toContain('新能源汽车电池回收技术 相关研究'); // 验证补全逻辑 + }); + + it('当输入为空字符串时,应该返回默认的后备检索词', () => { + const result = generateSearchQueries(' '); + expect(result).toEqual(['通用研究主题']); + }); + + it('当输入带有大量标点符号时,应该正确清洗', () => { + const result = generateSearchQueries('AI芯片,市场趋势?2026;'); + // 验证标点符号是否被正确视为空格分割 + expect(result).toContain('AI芯片'); + expect(result).toContain('市场趋势'); + }); +}); +``` + +### 4.2 依赖外部系统(文件系统 IO)的 Mock 测试 + +针对 `resourceService.ts`,绝不允许污染真实的 `OBSIDIAN_VAULT_PATH`。 + +``` +// src/services/__tests__/resourceService.test.ts +import { readObsidianNote, saveNote } from '../resourceService'; +import fs from 'fs/promises'; +import path from 'path'; + +// 全局 Mock fs 模块 +jest.mock('fs/promises'); + +describe('resourceService', () => { + beforeEach(() => { + jest.clearAllMocks(); // 每个用例前清除 mock 状态 + }); + + describe('readObsidianNote', () => { + it('当发生路径遍历攻击时 (../),应该抛出安全警告', async () => { + await expect(readObsidianNote('../../etc/passwd')).rejects.toThrow('安全警告:越权访问拦截!'); + }); + + it('当读取合法路径时,应该返回文件内容', async () => { + // 模拟 fs.readFile 返回成功 + (fs.readFile as jest.Mock).mockResolvedValueOnce('# Mock Note Content'); + + const content = await readObsidianNote('test.md'); + expect(content).toBe('# Mock Note Content'); + expect(fs.readFile).toHaveBeenCalledTimes(1); + }); + }); +}); +``` + +### 4.3 控制层的数据校验 (Zod Schema) 测试 + +针对 `toolsController.ts`,验证 Zod 的拦截机制与 MCP 响应格式是否统一: + +``` +// src/controllers/__tests__/toolsController.test.ts +import { handleToolCall } from '../toolsController'; +import * as resourceService from '../../services/resourceService'; + +jest.mock('../../services/resourceService'); + +describe('toolsController -> handleToolCall', () => { + it('当调用 save_note 且文件名缺失 .md 后缀时,应该返回 Zod 拦截的错误响应', async () => { + const params = { filename: 'invalidName', content: 'test' }; + const result = await handleToolCall('save_note', params); + + expect(result.isError).toBe(true); + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('文件名必须以 .md 结尾'); // 验证 Zod 自定义错误消息 + }); + + it('当调用未知的工具名时,应该返回未知工具的错误响应', async () => { + const result = await handleToolCall('unknown_tool', {}); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('未知工具: unknown_tool'); + }); +}); +``` + +--- + +## 5. 测试覆盖率标准 + +为了保障核心功能的稳定性,项目 CI/CD 流程中应设置覆盖率门槛: + +- **Service 层**: 语句覆盖率 (Statements) 不低于 **85%**。 + +- **Controller 层**: 分支覆盖率 (Branches) 不低于 **80%**。 + +- 配置命令:`jest --coverage`。 + +--- + +## 6. 测试编写的红线规定 + +1. **禁止真实 I/O**: 单元测试中严禁发起真实的磁盘读写或网络请求。必须使用 Mock。 + +2. **独立性**: 每个 `it` 用例必须相互独立。禁止用例 A 的运行结果作为用例 B 的依赖。必须善用 `beforeEach` 和 `afterEach` 清理状态(例如 `jest.clearAllMocks()`)。 + +3. **断言明确**: 不要只断言 `expect(result).toBeDefined()`。必须断言具体的数据结构或内容,例如 MCP 要求的 `content: [{ type: "text", text: "..." }]` 结构。 + +4. **涵盖异常流**: 测试不仅要覆盖“Happy Path”(快乐路径,即正常执行的流程),**必须**编写针对 `throw Error` 和 Zod `isError: true` 的异常分支测试(Unhappy Path)。 + + +**附注**: 编写完测试后,建议将 `npm run test` 和 `npm run test:coverage` 配置入 `package.json` 的 `scripts` 中,以便日常开发与构建流集成。 + +--- + +## 7. 测试执行与查看指引 + +项目的 `package.json` 中已集成了标准化的测试脚本指令,开发者可以直接在终端使用以下命令执行测试: + +### 7.1 运行所有测试 + +```bash +npm run test +``` + +- **功能说明**:Jest 会自动全局扫描您的项目,找到所有符合命名规范的测试文件(如 `*.test.ts` 或 `*.spec.ts`),并串行/并行执行其中的所有用例。 + +- **输出查看**:终端会实时输出每个用例的测试结果,绿色的 `PASS` 表示通过,红色的 `FAIL` 表示失败及具体的报错堆栈。 + + +### 7.2 运行测试并生成代码覆盖率报告 + +```bash +npm run test:coverage +``` + +- **功能说明**:在跑完所有测试用例的同时,额外收集代码被测试用例“触碰”的情况,借此衡量测试的完备度。 + +- **输出查看**: + + - **终端报表**:在控制台底部会输出一张表格,直观展示当前项目的“语句 (Stmts)”、“分支 (Branch)”、“函数 (Funcs)”和“行 (Lines)”覆盖率比例。 + + - **网页视图**:命令执行完毕后,项目根目录会自动生成一个 `coverage/` 文件夹。您可以通过浏览器打开 `coverage/lcov-report/index.html`,以直观的界面逐行查看哪些代码片段处于“漏测”状态。 + + +### 7.3 运行指定文件的测试 + +如果您正在专注开发某个模块(如意图拆解服务),不需要每次都全量执行测试,可在命令后追加关键词或文件名进行过滤: + +```bash +npm run test -- intentService +``` + +- **功能说明**:Jest 将仅匹配文件名中包含 `intentService` 的测试文件并执行,从而大幅提高 TDD(测试驱动开发)环节下的反馈效率。 + +--- + +## 许可声明 + +本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.