Files
gongxue-base/apps/server/src/agent-tools/tools/business-tools.spec.ts

80 lines
4.4 KiB
TypeScript

import { CaslAbilityFactory } from '../../authorization/casl-ability.factory';
import type { AuthenticatedUser } from '../../authorization';
import { AgentBusinessScopeFactory } from '../agent-business-scope.factory';
import { AgentToolContextFactory } from '../agent-tool.types';
import { SearchClassesTool } from './search-classes.tool';
import { GetAttendanceSummaryTool } from './get-attendance-summary.tool';
import { SearchRoomsTool } from './search-rooms.tool';
import { GetRoomOccupancySummaryTool } from './get-room-occupancy-summary.tool';
import { SearchBillsTool } from './search-bills.tool';
import { GetDashboardStatsTool } from './get-dashboard-stats.tool';
function context(permissions: string[] = [], isSuperAdmin = false) {
const user: AuthenticatedUser = { id: 7, username: 'teacher', permissions, isSuperAdmin, roles: [] };
return AgentToolContextFactory.fromAuthenticatedUser(user);
}
const scopes = new AgentBusinessScopeFactory(new CaslAbilityFactory());
describe('agent business tools', () => {
it('search_classes rejects unknown fields and enforces teacher scope', async () => {
const service = { agentSearchClasses: jest.fn().mockResolvedValue([]) };
const tool = new SearchClassesTool(service as never, scopes);
expect(tool.validate({ userId: 1 }).ok).toBe(false);
expect(tool.validate({ limit: 51 }).ok).toBe(false);
await tool.execute({ keyword: '一班' }, context(['class:view']));
expect(service.agentSearchClasses).toHaveBeenCalledWith(7, false, { keyword: '一班' });
});
it('class:edit grants full class and attendance scope', async () => {
const classService = { agentSearchClasses: jest.fn().mockResolvedValue([]) };
const attendanceService = { agentGetAttendanceSummary: jest.fn().mockResolvedValue([]) };
const ctx = context(['class:view', 'class:edit', 'attendance:view']);
await new SearchClassesTool(classService as never, scopes).execute({}, ctx);
await new GetAttendanceSummaryTool(attendanceService as never, scopes).execute({}, ctx);
expect(classService.agentSearchClasses).toHaveBeenCalledWith(7, true, {});
expect(attendanceService.agentGetAttendanceSummary).toHaveBeenCalledWith(7, true, {});
});
it('attendance validates date range and limit', () => {
const tool = new GetAttendanceSummaryTool({} as never, scopes);
expect(tool.validate({ dateFrom: '2026-07-23', dateTo: '2026-07-22' }).ok).toBe(false);
expect(tool.validate({ dateFrom: '2026-02-30' }).ok).toBe(false);
expect(tool.validate({ limit: 50 }).ok).toBe(true);
});
it('room tools reject sensitive/unknown fields and forward safe input', async () => {
const service = {
agentSearchRooms: jest.fn().mockResolvedValue([]),
agentGetRoomOccupancySummary: jest.fn().mockResolvedValue([]),
};
const search = new SearchRoomsTool(service as never);
const summary = new GetRoomOccupancySummaryTool(service as never);
expect(search.validate({ studentName: '张三' }).ok).toBe(false);
expect(summary.validate({ permissions: ['room:view'] }).ok).toBe(false);
await search.execute({ building: '1号楼', limit: 10 }, context(['room:view']));
await summary.execute({ date: '2026-07-23' }, context(['room:view']));
expect(service.agentSearchRooms).toHaveBeenCalledWith({ building: '1号楼', limit: 10 });
expect(service.agentGetRoomOccupancySummary).toHaveBeenCalledWith({ date: '2026-07-23' });
});
it('bill tool exposes read permission and validates ranges', async () => {
const service = { agentSearchBills: jest.fn().mockResolvedValue([]) };
const tool = new SearchBillsTool(service as never);
expect(tool.requiredPermission).toBe('bill:view');
expect(tool.validate({ periodStart: '2026-07-31', periodEnd: '2026-07-01' }).ok).toBe(false);
await tool.execute({ status: 'unpaid', limit: 20 }, context(['bill:view']));
expect(service.agentSearchBills).toHaveBeenCalledWith({ status: 'unpaid', limit: 20 });
});
it('dashboard uses teacher scope unless super admin', async () => {
const service = { agentGetDashboardStats: jest.fn().mockResolvedValue({}) };
const tool = new GetDashboardStatsTool(service as never, scopes);
expect(tool.validate({ debug: true }).ok).toBe(false);
await tool.execute({}, context(['dashboard:view']));
await tool.execute({}, context([], true));
expect(service.agentGetDashboardStats).toHaveBeenNthCalledWith(1, 7, false);
expect(service.agentGetDashboardStats).toHaveBeenNthCalledWith(2, 7, true);
});
});