Files
opsxshell/TDD_PLAN.md
T

403 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TDD_PLAN.md - 测试驱动开发计划
本文档规划 OpsXShell 的测试策略,覆盖从单元测试到 E2E 测试的完整体系。
---
## 当前状态
| 指标 | 当前值 | 目标值 |
|------|--------|--------|
| 测试文件数 | 3 | 15+ |
| 测试用例数 | 34 | 150+ |
| 估算覆盖率 | ~10% | ≥50% |
| 核心模块覆盖 | 0% | ≥80% |
### 已有测试
| 测试文件 | 用例数 | 覆盖模块 | 质量 |
|----------|--------|----------|------|
| `test/stores/tabStore.test.ts` | 15 | tabStore | ✅ 完整 |
| `test/stores/connectionPool.test.ts` | 7 | poolStore | 🟡 仅接口验证 |
| `test/components/sftp/utils.test.ts` | 12 | sftp/utils | ✅ 完整 |
---
## 测试策略
```
v1.3.0 测试计划
├── P0: 核心安全模块(crypto, ssh-service)
├── P1: 数据层(database, error-handler, stores)
├── P1: IPC 通信层(8 个 IPC Handler)
├── P2: UI 层(TerminalView, useSFTP hooks)
└── P3: E2E 集成(Playwright)
```
### 原则
1. **安全先行**: 加密/解密模块必须 100% 测试覆盖
2. **核心优先**: SSH 连接逻辑是应用核心,优先覆盖
3. **可 mock 性**: 所有外部依赖(ssh2, better-sqlite3, electron)必须可 mock
4. **独立性**: 每个测试独立运行,不依赖外部状态
5. **快速反馈**: 单元测试 < 5s,集成测试 < 30s
---
## 测试环境配置
### 已有配置
```typescript
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: {
statements: 50, // 当前远未达到
branches: 40,
functions: 50,
lines: 50,
},
},
},
})
```
### Mock 策略
```typescript
// test/setup.ts 已有:
// - window.electronAPI 完整 mock
// - localStorage mock
// 需要新增:
// - ssh2 Client mock
// - better-sqlite3 mock
// - zmodem-ts mock
```
---
## P0: 核心模块测试(第一批)
### 1. crypto.ts 测试 — 加密/解密正确性
**文件**: `test/services/crypto.test.ts`
**用例目标**: 15+
```typescript
describe('Crypto Service', () => {
describe('encrypt / decrypt', () => {
it('应正确加密并解密明文字符串')
it('应正确加密并解密空字符串')
it('应正确加密并解密特殊字符和 Unicode')
it('每次加密应生成不同的密文(随机 IV)')
it('密文篡改后解密应失败(Auth Tag 验证)')
it('错误密钥解密应失败')
})
describe('deriveKey', () => {
it('相同密码和 salt 应派生相同密钥')
it('不同 salt 应派生不同密钥')
it('应使用 100,000 次 PBKDF2 迭代')
})
describe('generateSalt / generateIV', () => {
it('salt 应为 32 字节随机值')
it('IV 应为 12 字节随机值(GCM 推荐)')
it('每次生成的值应不同')
})
})
```
### 2. ssh-service.ts 测试 — SSH 连接逻辑
**文件**: `test/services/ssh-service.test.ts`
**用例目标**: 20+
**Mock**: ssh2 Client
```typescript
// Mock ssh2
vi.mock('ssh2', () => ({
Client: vi.fn().mockImplementation(() => ({
connect: vi.fn(),
on: vi.fn(),
end: vi.fn(),
shell: vi.fn(),
forwardOut: vi.fn(),
forwardIn: vi.fn(),
sftp: vi.fn(),
})),
}))
describe('SSH Service', () => {
describe('connect', () => {
it('应使用密码认证连接')
it('应使用私钥认证连接')
it('应使用 SSH Agent 认证连接')
it('连接失败应返回错误')
it('连接超时应返回超时错误')
it('重复连接同一主机应复用')
})
describe('createShell', () => {
it('应创建交互式 Shell')
it('应传入终端尺寸 cols/rows')
it('Shell 创建失败应返回错误')
})
describe('disconnect', () => {
it('应正确关闭连接')
it('应清理所有 Shell 会话')
it('应清理端口转发')
})
describe('portForward', () => {
it('应创建本地端口转发')
it('应创建远程端口转发')
it('应创建 SOCKS5 动态代理')
it('端口被占用应返回错误')
})
describe('connectionPool', () => {
it('不应超过最大连接数限制')
it('空闲连接应被自动回收')
it('LRU 淘汰策略应正确工作')
})
})
```
---
## P1: 数据层 + Store 测试(第二批)
### 3. database-service.ts 测试
**文件**: `test/services/database.test.ts`
**用例目标**: 15+
```typescript
describe('Database Service', () => {
describe('Session CRUD', () => {
it('应创建新会话并返回 ID')
it('应获取所有会话列表')
it('应根据 ID 获取单个会话')
it('应更新会话字段')
it('应删除会话')
it('密码字段应加密存储')
it('读取时密码字段应解密')
})
describe('Settings', () => {
it('应保存和读取设置')
it('未设置的键应返回默认值')
it('应支持复杂对象类型设置')
})
describe('数据完整性', () => {
it('数据库文件不存在应自动创建')
it('Schema 版本升级应自动迁移')
})
})
```
### 4. error-handler.ts 测试
**文件**: `test/utils/error-handler.test.ts`
**用例目标**: 10+
```typescript
describe('Error Handler', () => {
describe('inferErrorCode', () => {
it('应从 ECONNREFUSED 推断连接错误')
it('应从 ETIMEDOUT 推断超时错误')
it('应从 auth 推断认证错误')
it('应从 ENOENT 推断文件不存在错误')
it('未知错误应返回通用错误码')
})
describe('withErrorHandling', () => {
it('应包装异步函数并捕获错误')
it('应返回统一错误格式')
})
})
```
### 5. poolStore 测试增强
**文件**: `test/stores/connectionPool.test.ts`
**用例目标**: 15+ (新增 8)
```typescript
describe('Connection Pool Store', () => {
// 已有: 基本接口验证
// 新增:
it('应拒绝超过最大连接数的新连接')
it('LRU 淘汰应移除最久未使用的连接')
it('后台连接超过最大数应断开最旧')
it('活跃连接不应被 LRU 淘汰')
it('连接状态机转换应完整覆盖')
it('挂起超时 30 分钟应彻底断开')
it('保活心跳应重置空闲计时器')
})
```
### 6. lifecycleStore 测试
**文件**: `test/stores/lifecycleStore.test.ts`
**用例目标**: 10+
---
## P1: IPC Handler 测试(第三批)
### 7. SSH IPC 测试
**文件**: `test/ipc/ssh.ipc.test.ts`
**用例目标**: 8+
```typescript
describe('SSH IPC Handlers', () => {
it('ssh:connect 应调用 sshService.connect')
it('ssh:disconnect 应调用 sshService.disconnect')
it('ssh:create-shell 应调用 sshService.createShell')
it('ssh:shell-write 应调用 sshService.shellWrite')
it('ssh:shell-resize 应调用 sshService.shellResize')
it('ssh:forward-local 应调用 sshService.forwardLocal')
it('ssh:forward-remote 应调用 sshService.forwardRemote')
it('无效参数应返回错误')
})
```
### 8. SFTP IPC 测试
**文件**: `test/ipc/sftp.ipc.test.ts`
**用例目标**: 8+
### 9. Session IPC 测试
**文件**: `test/ipc/session.ipc.test.ts`
**用例目标**: 6+
### 10. Settings IPC 测试
**文件**: `test/ipc/settings.ipc.test.ts`
**用例目标**: 4+
### 11. 其他 IPC 测试
**文件**: `test/ipc/{dialog,file,zmodem,app}.ipc.test.ts`
**用例目标**: 每个文件 3-5+
---
## P2: UI 层测试(第四批)
### 12. TerminalView 组件测试
**文件**: `test/components/TerminalView.test.tsx`
**用例目标**: 10+
```typescript
// Mock xterm
vi.mock('@xterm/xterm', () => ({
Terminal: vi.fn().mockImplementation(() => ({
onData: vi.fn(), onResize: vi.fn(), open: vi.fn(),
write: vi.fn(), dispose: vi.fn(), cols: 80, rows: 24,
})),
}))
describe('TerminalView', () => {
it('应渲染终端容器')
it('应显示连接中状态')
it('连接成功应初始化 XTerm')
it('连接失败应显示错误信息')
it('Shell 数据应写入终端')
it('终端输入应发送到 Shell')
it('窗口调整应 resize 终端')
it('组件卸载应释放资源')
})
```
### 13. useSFTP Hooks 测试
**文件**: `test/hooks/useSFTP.test.ts`
**用例目标**: 15+
### 14. 其他组件测试
- `test/components/Sidebar.test.tsx` — 5+
- `test/components/SessionDialog.test.tsx` — 5+
- `test/components/SFTPView.test.tsx` — 5+
---
## P3: E2E 集成测试(远期)
### Playwright 配置
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './test/e2e',
timeout: 30000,
use: { baseURL: 'http://localhost:5173' },
})
```
### E2E 用例
- 快速连接 → SSH 终端出现
- 侧边栏会话 → 点击 → 终端连接
- SFTP → 选择服务器 → 浏览目录
- 标签页 → 创建 → 切换 → 关闭
---
## 测试执行
### 命令
```bash
npm test # 运行所有测试
npm test -- --coverage # 带覆盖率报告
npm test -- test/services/ # 只跑 services 测试
npm test -- --reporter=verbose # 详细输出
npm test -- --watch # 监听模式
```
### CI 集成(规划)
```yaml
# .github/workflows/test.yml
- name: Test
run: npm test -- --coverage
- name: Coverage Check
run: |
if [ $(npm test -- --coverage --reporter=json 2>/dev/null | jq '.total.lines.pct') -lt 50 ]; then
echo "Coverage below 50%"
exit 1
fi
```
---
## 覆盖率追踪
| 版本 | 总覆盖率 | 核心模块 | 用例数 | 日期 |
|------|----------|----------|--------|------|
| v1.0.0 | ~10% | 0% | 34 | 2026-04-06 |
| v1.1.0 | ~12% | 0% | 34 | — |
| v1.2.0 | ~15% | 0% | 40 | — |
| v1.3.0-batch1 | ~25% | 60% | 69 | — |
| v1.3.0-batch2 | ~35% | 60% | 104 | — |
| v1.3.0-batch3 | ~45% | 70% | 143 | — |
| v1.3.0-final | **≥50%** | **≥80%** | **150+** | — |