uto_manager/test/domain/services/assignment_service_test.dart

209 lines
7.7 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:uto_manager/domain/entities/authorized_rank.dart';
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
import 'package:uto_manager/domain/entities/echelon.dart';
import 'package:uto_manager/domain/entities/personnel_record.dart';
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/services/assignment_service.dart';
import 'package:uto_manager/domain/services/unit_tree_service.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
void main() {
const treeService = UnitTreeService();
const service = AssignmentService(treeService: treeService);
PositionSlot makeSlot({
String slotId = 's1',
String unitId = 'u1',
String mosCode = '11B',
AuthorizedRank rank = AuthorizedRank.e5,
bool isKeyLeader = false,
String? assignedPersonnelId,
}) {
return PositionSlot(
slotId: slotId,
unitId: unitId,
positionTitle: 'Test Position',
dutyMos: DutyMos(mosCode),
authorizedRank: rank,
isKeyLeader: isKeyLeader,
assignedPersonnelId: assignedPersonnelId,
);
}
PersonnelRecord makeSoldier({
String id = 'p1',
String mosCode = '11B',
AuthorizedRank rank = AuthorizedRank.e5,
String? assignedSlotId,
ReadinessState readiness = ReadinessState.fullyMissionCapable,
}) {
return PersonnelRecord(
soldierId: id,
name: 'SGT Test',
primaryMos: DutyMos(mosCode),
payGrade: rank,
readinessState: readiness,
assignedSlotId: assignedSlotId,
);
}
// ---------------------------------------------------------------------------
// assign
// ---------------------------------------------------------------------------
group('assign', () {
test('successful assignment returns success outcome', () {
final slot = makeSlot();
final soldier = makeSoldier();
final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isTrue);
expect(outcome.updatedSlot?.assignedPersonnelId, 'p1');
expect(outcome.updatedSoldier?.assignedSlotId, 's1');
});
test('assignment to filled slot returns failure', () {
final slot = makeSlot(assignedPersonnelId: 'existing');
final soldier = makeSoldier();
final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isFalse);
expect(outcome.failureReason, contains('already filled'));
});
test('assignment of already-assigned soldier returns failure', () {
final slot = makeSlot();
final soldier = makeSoldier(assignedSlotId: 'other-slot');
final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isFalse);
expect(outcome.failureReason, contains('already assigned'));
});
test('MOS mismatch is flagged but does not block assignment', () {
final slot = makeSlot(mosCode: '68W');
final soldier = makeSoldier(mosCode: '11B');
final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isTrue);
expect(outcome.hasMosMismatch, isTrue);
});
test('rank mismatch is flagged when soldier rank is below minimum', () {
final slot = makeSlot(rank: AuthorizedRank.e7);
final soldier = makeSoldier(rank: AuthorizedRank.e4);
final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isTrue);
expect(outcome.hasRankMismatch, isTrue);
});
test('no rank mismatch when soldier meets minimum rank', () {
final slot = makeSlot(rank: AuthorizedRank.e5);
final soldier = makeSoldier(rank: AuthorizedRank.e6);
final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isTrue);
expect(outcome.hasRankMismatch, isFalse);
});
test('updated slot has correct personnel ID', () {
final slot = makeSlot(slotId: 'slot-abc');
final soldier = makeSoldier(id: 'soldier-xyz');
final outcome = service.assign(slot, soldier);
expect(outcome.updatedSlot?.assignedPersonnelId, 'soldier-xyz');
expect(outcome.updatedSoldier?.assignedSlotId, 'slot-abc');
});
});
// ---------------------------------------------------------------------------
// unassign
// ---------------------------------------------------------------------------
group('unassign', () {
test('unassigning from vacant slot returns unchanged slot', () {
final slot = makeSlot();
final outcome = service.unassign(slot, null);
expect(outcome.updatedSlot.isVacant, isTrue);
expect(outcome.updatedSoldier, isNull);
});
test('unassigning from filled slot clears assignment', () {
final slot = makeSlot(assignedPersonnelId: 'p1');
final soldier = makeSoldier(assignedSlotId: 's1');
final outcome = service.unassign(slot, soldier);
expect(outcome.updatedSlot.isVacant, isTrue);
expect(outcome.updatedSoldier?.isUnassigned, isTrue);
});
test('unassigning with null soldier still clears slot', () {
final slot = makeSlot(assignedPersonnelId: 'p1');
final outcome = service.unassign(slot, null);
expect(outcome.updatedSlot.isVacant, isTrue);
expect(outcome.updatedSoldier, isNull);
});
});
// ---------------------------------------------------------------------------
// aggregateReadiness
// ---------------------------------------------------------------------------
group('aggregateReadiness', () {
test('empty list returns grey', () {
expect(service.aggregateReadiness([]), ReadinessColor.grey);
});
test('all FMC returns green', () {
final soldiers = [
makeSoldier(id: 'p1', readiness: ReadinessState.fullyMissionCapable),
makeSoldier(id: 'p2', readiness: ReadinessState.fullyMissionCapable),
];
expect(service.aggregateReadiness(soldiers), ReadinessColor.green);
});
test('any NMC returns red', () {
final soldiers = [
makeSoldier(id: 'p1', readiness: ReadinessState.fullyMissionCapable),
makeSoldier(id: 'p2', readiness: ReadinessState.notMissionCapable),
];
expect(service.aggregateReadiness(soldiers), ReadinessColor.red);
});
test('mixed FMC and PMC returns amber', () {
final soldiers = [
makeSoldier(id: 'p1', readiness: ReadinessState.fullyMissionCapable),
makeSoldier(id: 'p2', readiness: ReadinessState.partiallyMissionCapable),
];
expect(service.aggregateReadiness(soldiers), ReadinessColor.amber);
});
test('single PMC soldier returns amber', () {
final soldiers = [makeSoldier(id: 'p1', readiness: ReadinessState.partiallyMissionCapable)];
expect(service.aggregateReadiness(soldiers), ReadinessColor.amber);
});
});
// ---------------------------------------------------------------------------
// UnitNode fixture (used to verify UnitTreeService integration)
// ---------------------------------------------------------------------------
group('UnitTreeService integration via AssignmentService', () {
test('validateReparent is accessible through treeService', () {
final bde = UnitNode(
unitId: 'bde',
echelon: Echelon.brigade,
branch: DoctrinalBranch.infantry,
name: 'Brigade',
isKeyLeader: false,
);
final bn = UnitNode(
unitId: 'bn',
echelon: Echelon.battalion,
branch: DoctrinalBranch.infantry,
name: 'Battalion',
isKeyLeader: false,
parentUnitId: 'bde',
);
final result = service.treeService.validateReparent(bn, bde, [bde, bn]);
// bn (battalion) under bde (brigade) is valid
expect(result.isValid, isTrue);
});
});
}