/** * Hooks Route 测试 * * 测试 Hooks 配置管理 REST API 端点 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; // Create mock hooks module const mockHooksModule = vi.hoisted(() => ({ loadProjectConfig: vi.fn(), loadHookConfig: vi.fn(), getConfigFilePath: vi.fn(), createDefaultConfig: vi.fn(), })); // Track if module should be available let moduleAvailable = true; vi.mock('@ai-assistant/core', () => { if (!moduleAvailable) { throw new Error('Module not found'); } return mockHooksModule; }); vi.mock('../../../src/routes/config.js', () => ({ getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), })); // Mock fs/promises vi.mock('fs/promises', () => ({ readFile: vi.fn().mockResolvedValue('{}'), writeFile: vi.fn().mockResolvedValue(undefined), })); import { hooksRouter } from '../../../src/routes/hooks.js'; // Create test app const app = new Hono(); app.route('/hooks', hooksRouter); describe('Hooks Route', () => { beforeEach(() => { vi.clearAllMocks(); moduleAvailable = true; mockHooksModule.loadHookConfig.mockResolvedValue(null); mockHooksModule.getConfigFilePath.mockResolvedValue('/test/workdir/.ai-assistant.json'); }); describe('GET /hooks/config - 获取完整钩子配置', () => { it('返回钩子配置', async () => { mockHooksModule.loadHookConfig.mockResolvedValue({ file_edited: { '*.ts': [{ command: ['npx', 'prettier', '--write'] }] }, session_completed: [], }); const res = await app.request('/hooks/config'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.file_edited).toBeDefined(); }); it('无配置时返回默认空对象', async () => { mockHooksModule.loadHookConfig.mockResolvedValue(null); const res = await app.request('/hooks/config'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data.file_edited).toEqual({}); expect(json.data.session_completed).toEqual([]); }); }); describe('PUT /hooks/config - 更新完整钩子配置', () => { it('更新成功', async () => { const res = await app.request('/hooks/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_edited: { '*.ts': [{ command: ['echo', 'edited'] }] }, }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); }); describe('GET /hooks/file-edited - 获取 file_edited 钩子', () => { it('返回 file_edited 配置', async () => { mockHooksModule.loadHookConfig.mockResolvedValue({ file_edited: { '*.ts': [{ command: ['npx', 'prettier', '--write'] }] }, }); const res = await app.request('/hooks/file-edited'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data['*.ts']).toBeDefined(); }); it('无配置时返回空对象', async () => { const res = await app.request('/hooks/file-edited'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual({}); }); }); describe('PUT /hooks/file-edited - 更新 file_edited 钩子', () => { it('更新成功', async () => { const res = await app.request('/hooks/file-edited', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ '*.ts': [{ command: ['npx', 'eslint', '--fix'] }], }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); }); describe('GET /hooks/file-created - 获取 file_created 钩子', () => { it('返回 file_created 配置', async () => { mockHooksModule.loadHookConfig.mockResolvedValue({ file_created: { '*.tsx': [{ command: ['echo', 'new file'] }] }, }); const res = await app.request('/hooks/file-created'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('无配置时返回空对象', async () => { const res = await app.request('/hooks/file-created'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual({}); }); }); describe('PUT /hooks/file-created - 更新 file_created 钩子', () => { it('更新成功', async () => { const res = await app.request('/hooks/file-created', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ '*.tsx': [{ command: ['echo', 'created'] }], }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); }); describe('GET /hooks/file-deleted - 获取 file_deleted 钩子', () => { it('返回 file_deleted 配置', async () => { mockHooksModule.loadHookConfig.mockResolvedValue({ file_deleted: { '*.log': [{ command: ['echo', 'deleted'] }] }, }); const res = await app.request('/hooks/file-deleted'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('无配置时返回空对象', async () => { const res = await app.request('/hooks/file-deleted'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual({}); }); }); describe('PUT /hooks/file-deleted - 更新 file_deleted 钩子', () => { it('更新成功', async () => { const res = await app.request('/hooks/file-deleted', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ '*.tmp': [{ command: ['echo', 'cleaned'] }], }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); }); describe('GET /hooks/session-completed - 获取 session_completed 钩子', () => { it('返回 session_completed 配置', async () => { mockHooksModule.loadHookConfig.mockResolvedValue({ session_completed: [{ command: ['echo', 'session done'] }], }); const res = await app.request('/hooks/session-completed'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toHaveLength(1); }); it('无配置时返回空数组', async () => { const res = await app.request('/hooks/session-completed'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual([]); }); }); describe('PUT /hooks/session-completed - 更新 session_completed 钩子', () => { it('更新成功', async () => { const res = await app.request('/hooks/session-completed', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([{ command: ['echo', 'completed'] }]), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); }); describe('POST /hooks/test - 测试执行钩子命令', () => { it('执行简单命令', async () => { const res = await app.request('/hooks/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: ['echo', 'test'], }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toHaveProperty('exitCode'); expect(json.data).toHaveProperty('duration'); }); it('无效命令配置返回 400', async () => { const res = await app.request('/hooks/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: [], }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); expect(json.error).toContain('Invalid command configuration'); }); it('命令非数组返回 400', async () => { const res = await app.request('/hooks/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'echo test', }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); it('支持超时配置', async () => { const res = await app.request('/hooks/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: ['echo', 'quick'], timeout: 5000, }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('支持环境变量', async () => { const res = await app.request('/hooks/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: ['echo', '$TEST_VAR'], environment: { TEST_VAR: 'hello' }, }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); }); });