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); }); });