import { NotFoundException } from '@nestjs/common'; import { UpdateStudentsTool } from './update-students.tool'; function createTool(overrides: Record = {}) { const studentsService = { update: jest.fn(async (id: number, dto: Record) => ({ id, name: dto.name ?? '学生', })), ...(overrides.studentsService ?? {}), }; const tool = new UpdateStudentsTool(studentsService as never); return { tool, studentsService }; } const validInput = { updates: [ { id: 201, name: '於嘉丽' }, { id: 172, name: '徐玚' }, ], }; describe('UpdateStudentsTool', () => { it('exposes student:edit permission and student skill', () => { const { tool } = createTool(); expect(tool.name).toBe('update_students'); expect(tool.skillKey).toBe('student'); expect(tool.requiredPermission).toBe('student:edit'); }); it('rejects forbidden, unknown, and empty input', () => { const { tool } = createTool(); expect(tool.validate({ userId: 1 }).ok).toBe(false); expect(tool.validate({ updates: [], admin: true }).ok).toBe(false); expect(tool.validate({}).ok).toBe(false); expect(tool.validate({ updates: [] }).ok).toBe(false); expect(tool.validate({ updates: [{ id: 201 }] }).ok).toBe(false); }); it('rejects invalid ids, duplicate ids, invalid fields, and oversized batches', () => { const { tool } = createTool(); expect(tool.validate({ updates: [{ id: 0, name: 'A' }] }).ok).toBe(false); expect(tool.validate({ updates: [{ id: 'x', name: 'A' }] }).ok).toBe(false); expect( tool.validate({ updates: [ { id: 201, name: 'A' }, { id: 201, name: 'B' }, ], }).ok, ).toBe(false); expect(tool.validate({ updates: [{ id: 201, status: 'archived' }] }).ok).toBe(false); expect(tool.validate({ updates: [{ id: 201, name: 'A'.repeat(51) }] }).ok).toBe(false); expect( tool.validate({ updates: Array.from({ length: 13 }, (_, index) => ({ id: index + 1, name: 'A' })), }).ok, ).toBe(false); }); it('accepts all supported editable fields', () => { const { tool } = createTool(); const result = tool.validate({ updates: [ { id: 201, name: '於嘉丽', studentNo: 'S201', phone: '13800138000', idNumber: 'ID201', gender: '女', ethnicity: '汉族', emergencyContact: '家长', emergencyPhone: '13900139000', organizationId: 2, supervisor: '王老师', status: 'active', }, ], }); expect(result.ok).toBe(true); if (result.ok) { expect(result.value.updates[0]).toMatchObject({ id: 201, name: '於嘉丽', organizationId: 2, status: 'active', }); } }); it('updates every student and reports a safe summary', async () => { const { tool, studentsService } = createTool(); const result = await tool.execute(validInput, {} as never); expect(studentsService.update).toHaveBeenCalledTimes(2); expect(studentsService.update).toHaveBeenCalledWith(201, { name: '於嘉丽' }); expect(result).toEqual({ message: '成功更新 2 名学生,失败 0 条', updated: [ { id: 201, name: '於嘉丽' }, { id: 172, name: '徐玚' }, ], failed: [], }); expect(JSON.stringify(result)).not.toContain('13800138000'); }); it('continues when one student cannot be updated', async () => { const { tool } = createTool({ studentsService: { update: jest .fn() .mockRejectedValueOnce(new NotFoundException('not found')) .mockResolvedValueOnce({ id: 172, name: '徐玚' }), }, }); const result = await tool.execute(validInput, {} as never); expect(result).toEqual({ message: '成功更新 1 名学生,失败 1 条', updated: [{ id: 172, name: '徐玚' }], failed: [{ id: 201, error: '学生不存在' }], }); }); });