80 lines
4.4 KiB
TypeScript
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);
|
|
});
|
|
});
|