uto_manager/test/domain/services/mtoe_validation_engine_test...

500 lines
15 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/mtoe_validation_engine.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
void main() {
const engine = MtoeValidationEngine();
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
UnitNode makeUnit({
required String id,
required Echelon echelon,
DoctrinalBranch branch = DoctrinalBranch.infantry,
String? parentId,
String name = 'Test Unit',
}) =>
UnitNode(
unitId: id,
echelon: echelon,
branch: branch,
parentUnitId: parentId,
name: name,
isKeyLeader: false,
);
PositionSlot makeSlot({
required String slotId,
required String unitId,
String mos = '11B',
AuthorizedRank rank = AuthorizedRank.e5,
bool isKeyLeader = false,
String? assignedPersonnelId,
}) =>
PositionSlot(
slotId: slotId,
unitId: unitId,
positionTitle: 'Test Position',
dutyMos: DutyMos.unvalidated(mos),
authorizedRank: rank,
isKeyLeader: isKeyLeader,
assignedPersonnelId: assignedPersonnelId,
);
PersonnelRecord makeSoldier({
required String id,
String mos = '11B',
AuthorizedRank rank = AuthorizedRank.e5,
String? assignedSlotId,
ReadinessState readiness = ReadinessState.fullyMissionCapable,
}) =>
PersonnelRecord(
soldierId: id,
name: 'SGT Test',
primaryMos: DutyMos.unvalidated(mos),
payGrade: rank,
readinessState: readiness,
assignedSlotId: assignedSlotId,
);
// ---------------------------------------------------------------------------
// Empty input
// ---------------------------------------------------------------------------
group('empty input', () {
test('returns no findings for empty tree', () {
final findings = engine.validate(units: [], slots: [], personnel: []);
expect(findings, isEmpty);
});
});
// ---------------------------------------------------------------------------
// V001 — MOS Mismatch
// ---------------------------------------------------------------------------
group('V001 — MOS mismatch', () {
test('flags slot where assigned soldier MOS does not match', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
mos: '68W',
assignedPersonnelId: 'p1',
);
final soldier = makeSoldier(id: 'p1', mos: '11B', assignedSlotId: 's1');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [soldier],
);
expect(findings.any((f) => f.findingCode == 'V001'), isTrue);
expect(
findings.firstWhere((f) => f.findingCode == 'V001').severity,
FindingSeverity.warning,
);
});
test('no V001 when MOS matches', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
mos: '11B',
assignedPersonnelId: 'p1',
);
final soldier = makeSoldier(id: 'p1', mos: '11B', assignedSlotId: 's1');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [soldier],
);
expect(findings.any((f) => f.findingCode == 'V001'), isFalse);
});
test('no V001 for vacant slots', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V001'), isFalse);
});
});
// ---------------------------------------------------------------------------
// V002 — Rank Mismatch
// ---------------------------------------------------------------------------
group('V002 — rank mismatch', () {
test('flags slot where soldier rank is below minimum', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
rank: AuthorizedRank.e6,
assignedPersonnelId: 'p1',
);
final soldier = makeSoldier(
id: 'p1',
rank: AuthorizedRank.e4,
assignedSlotId: 's1',
);
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [soldier],
);
expect(findings.any((f) => f.findingCode == 'V002'), isTrue);
expect(
findings.firstWhere((f) => f.findingCode == 'V002').severity,
FindingSeverity.warning,
);
});
test('no V002 when rank meets minimum', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
rank: AuthorizedRank.e5,
assignedPersonnelId: 'p1',
);
final soldier = makeSoldier(
id: 'p1',
rank: AuthorizedRank.e6,
assignedSlotId: 's1',
);
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [soldier],
);
expect(findings.any((f) => f.findingCode == 'V002'), isFalse);
});
});
// ---------------------------------------------------------------------------
// V003 — Vacant Key Leader
// ---------------------------------------------------------------------------
group('V003 — vacant key leader', () {
test('flags vacant key leader slot as critical', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
isKeyLeader: true,
);
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V003'), isTrue);
expect(
findings.firstWhere((f) => f.findingCode == 'V003').severity,
FindingSeverity.critical,
);
});
test('no V003 when key leader slot is filled', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
isKeyLeader: true,
assignedPersonnelId: 'p1',
);
final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [soldier],
);
expect(findings.any((f) => f.findingCode == 'V003'), isFalse);
});
test('no V003 for non-key-leader vacant slots', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(slotId: 's1', unitId: 'u1', isKeyLeader: false);
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V003'), isFalse);
});
});
// ---------------------------------------------------------------------------
// V004 — Missing Combat Medic
// ---------------------------------------------------------------------------
group('V004 — missing combat medic', () {
test('flags platoon with no 68W slot as advisory', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '11B');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V004'), isTrue);
expect(
findings.firstWhere((f) => f.findingCode == 'V004').severity,
FindingSeverity.advisory,
);
});
test('no V004 when platoon has 68W slot', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V004'), isFalse);
});
test('no V004 for non-platoon echelons', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '11B');
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V004'), isFalse);
});
});
// ---------------------------------------------------------------------------
// V005 — Missing Fire Support Specialist
// ---------------------------------------------------------------------------
group('V005 — missing fire support specialist', () {
test('flags platoon with no 13F slot as advisory', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot68w = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W');
final findings = engine.validate(
units: [unit],
slots: [slot68w],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V005'), isTrue);
});
test('no V005 when platoon has 13F slot', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot68w = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W');
final slot13f = makeSlot(slotId: 's2', unitId: 'u1', mos: '13F');
final findings = engine.validate(
units: [unit],
slots: [slot68w, slot13f],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V005'), isFalse);
});
});
// ---------------------------------------------------------------------------
// V006 — Structural Hierarchy Violation
// ---------------------------------------------------------------------------
group('V006 — structural hierarchy violation', () {
test('flags squad directly under brigade as critical', () {
final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade);
final squad = makeUnit(
id: 'sq',
echelon: Echelon.squad,
parentId: 'bde',
);
final findings = engine.validate(
units: [brigade, squad],
slots: [],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V006'), isTrue);
expect(
findings.firstWhere((f) => f.findingCode == 'V006').severity,
FindingSeverity.critical,
);
});
test('no V006 for valid battalion under brigade', () {
final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade);
final battalion = makeUnit(
id: 'bn',
echelon: Echelon.battalion,
parentId: 'bde',
);
final findings = engine.validate(
units: [brigade, battalion],
slots: [],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V006'), isFalse);
});
test('no V006 for root units (no parent)', () {
final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade);
final findings = engine.validate(
units: [brigade],
slots: [],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V006'), isFalse);
});
});
// ---------------------------------------------------------------------------
// V007 — Manning Below 70%
// ---------------------------------------------------------------------------
group('V007 — manning below 70%', () {
test('flags unit with fill rate below 70% as warning', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
// 1 filled out of 5 = 20%
final slots = [
makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'),
makeSlot(slotId: 's2', unitId: 'u1'),
makeSlot(slotId: 's3', unitId: 'u1'),
makeSlot(slotId: 's4', unitId: 'u1'),
makeSlot(slotId: 's5', unitId: 'u1'),
];
final findings = engine.validate(
units: [unit],
slots: slots,
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V007'), isTrue);
expect(
findings.firstWhere((f) => f.findingCode == 'V007').severity,
FindingSeverity.warning,
);
});
test('no V007 when fill rate is exactly 70%', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
// 7 filled out of 10 = 70%
final slots = List.generate(
10,
(i) => makeSlot(
slotId: 's$i',
unitId: 'u1',
assignedPersonnelId: i < 7 ? 'p$i' : null,
),
);
final findings = engine.validate(
units: [unit],
slots: slots,
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V007'), isFalse);
});
test('no V007 for units with no slots', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final findings = engine.validate(
units: [unit],
slots: [],
personnel: [],
);
expect(findings.any((f) => f.findingCode == 'V007'), isFalse);
});
});
// ---------------------------------------------------------------------------
// Multiple findings
// ---------------------------------------------------------------------------
group('multiple findings', () {
test('returns multiple findings for a poorly configured unit', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
// Vacant key leader + no 68W + no 13F + low manning
final slot = makeSlot(
slotId: 's1',
unitId: 'u1',
isKeyLeader: true,
);
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
// V003 (vacant key leader), V004 (no medic), V005 (no FSO), V007 (manning)
final codes = findings.map((f) => f.findingCode).toSet();
expect(codes, containsAll(['V003', 'V004', 'V005', 'V007']));
});
test('finding unitId is set correctly', () {
final unit = makeUnit(id: 'my-unit', echelon: Echelon.platoon);
final slot = makeSlot(
slotId: 's1',
unitId: 'my-unit',
isKeyLeader: true,
);
final findings = engine.validate(
units: [unit],
slots: [slot],
personnel: [],
);
final v003 = findings.firstWhere((f) => f.findingCode == 'V003');
expect(v003.unitId, 'my-unit');
});
});
}