209 lines
7.7 KiB
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);
|
|
});
|
|
});
|
|
}
|