1b7d55848d
- 删除 Server 中 60+ 个与 Core 重复的类型定义 - 将动态导入 (await import) 改为静态类型导入 (import type) - 保留必要的运行时静态导入 - 修复测试文件中的 mock 初始化问题 - 净删除约 960 行重复代码 重构文件: - routes/checkpoints.ts: 删除 155 行重复类型 - routes/agents.ts: 删除 93 行重复类型 - routes/commands.ts: 删除 83 行重复类型 - routes/mcp.ts: 修复类型窄化 - routes/hooks.ts: 已使用静态导入 - routes/providers.ts: 删除 63 行重复类型 - session/manager.ts: 删除 41 行重复类型 - routes/sessions.ts: 添加类型导入 - permission/handler.ts: 添加类型导入
315 lines
9.3 KiB
TypeScript
315 lines
9.3 KiB
TypeScript
/**
|
|
* 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(),
|
|
}));
|
|
|
|
vi.mock('@ai-assistant/core', () => 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();
|
|
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);
|
|
});
|
|
});
|
|
});
|