uto_manager/test/domain/services/validation_isolate_test.dart

242 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/mtoe_validation_engine.dart';
import 'package:uto_manager/domain/services/validation_isolate.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
void main() {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
UnitNode makeUnit(String id, Echelon echelon, {String? parentId}) => UnitNode(
unitId: id,
echelon: echelon,
branch: DoctrinalBranch.infantry,
parentUnitId: parentId,
name: 'Unit $id',
isKeyLeader: false,
);
PositionSlot makeSlot(
String id,
String unitId, {
bool isKeyLeader = false,
String? assignedPersonnelId,
}) =>
PositionSlot(
slotId: id,
unitId: unitId,
positionTitle: isKeyLeader ? 'Squad Leader' : 'Rifleman',
dutyMos: DutyMos.unvalidated(isKeyLeader ? '11B' : '11B'),
authorizedRank: isKeyLeader ? AuthorizedRank.e6 : AuthorizedRank.e3,
isKeyLeader: isKeyLeader,
assignedPersonnelId: assignedPersonnelId,
);
// ---------------------------------------------------------------------------
// IsolateValidationRunner
// ---------------------------------------------------------------------------
group('IsolateValidationRunner', () {
late IsolateValidationRunner runner;
setUp(() {
runner = IsolateValidationRunner();
});
tearDown(() async {
await runner.dispose();
});
test('isStarted is false before start()', () {
expect(runner.isStarted, false);
});
test('isStarted is true after start()', () async {
await runner.start();
expect(runner.isStarted, true);
});
test('start() is idempotent — calling twice does not throw', () async {
await runner.start();
await runner.start(); // second call should be a no-op
expect(runner.isStarted, true);
});
test('validate() throws StateError if start() not called', () async {
expect(
() => runner.validate(units: const [], slots: const [], personnel: const []),
throwsStateError,
);
});
test('validate() returns empty findings for empty tree', () async {
await runner.start();
final response = await runner.validate(
units: const [],
slots: const [],
personnel: const [],
);
expect(response.hasError, false);
expect(response.findings, isEmpty);
});
test('validate() detects vacant key leader (V003)', () async {
await runner.start();
final unit = makeUnit('u1', Echelon.squad);
final slot = makeSlot('sl1', 'u1', isKeyLeader: true); // vacant
final response = await runner.validate(
units: [unit],
slots: [slot],
personnel: const [],
);
expect(response.hasError, false);
expect(response.findings.any((f) => f.findingCode == 'V003'), true);
});
test('validate() detects missing medic in platoon (V004)', () async {
await runner.start();
final unit = makeUnit('u1', Echelon.platoon);
final slot = makeSlot('sl1', 'u1'); // no 68W slot
final response = await runner.validate(
units: [unit],
slots: [slot],
personnel: const [],
);
expect(response.hasError, false);
expect(response.findings.any((f) => f.findingCode == 'V004'), true);
});
test('validate() returns clean result for fully-staffed valid unit', () async {
await runner.start();
// A squad with a filled key leader slot and no hierarchy violations.
final unit = makeUnit('u1', Echelon.squad);
final keyLeaderSlot = PositionSlot(
slotId: 'sl1',
unitId: 'u1',
positionTitle: 'Squad Leader',
dutyMos: DutyMos.unvalidated('11B'),
authorizedRank: AuthorizedRank.e6,
isKeyLeader: true,
assignedPersonnelId: 's1',
);
final soldier = PersonnelRecord(
soldierId: 's1',
name: 'SSG Smith',
primaryMos: DutyMos.unvalidated('11B'),
payGrade: AuthorizedRank.e6,
readinessState: ReadinessState.fullyMissionCapable,
);
final response = await runner.validate(
units: [unit],
slots: [keyLeaderSlot],
personnel: [soldier],
);
expect(response.hasError, false);
// V003 should not fire (key leader is filled).
expect(response.findings.any((f) => f.findingCode == 'V003'), false);
});
test('validate() detects structural hierarchy violation (V006)', () async {
await runner.start();
// Squad directly under Brigade — invalid.
final brigade = makeUnit('bde', Echelon.brigade);
final squad = makeUnit('sq1', Echelon.squad, parentId: 'bde');
final response = await runner.validate(
units: [brigade, squad],
slots: const [],
personnel: const [],
);
expect(response.hasError, false);
expect(response.findings.any((f) => f.findingCode == 'V006'), true);
expect(
response.findings.firstWhere((f) => f.findingCode == 'V006').severity,
FindingSeverity.critical,
);
});
test('validate() results match synchronous engine output', () async {
await runner.start();
final units = [
makeUnit('bde', Echelon.brigade),
makeUnit('bn', Echelon.battalion, parentId: 'bde'),
makeUnit('co', Echelon.company, parentId: 'bn'),
makeUnit('plt', Echelon.platoon, parentId: 'co'),
];
final slots = [
makeSlot('sl1', 'plt', isKeyLeader: true), // vacant key leader
];
final isolateResponse = await runner.validate(
units: units,
slots: slots,
personnel: const [],
);
// Compare with synchronous engine.
const syncEngine = MtoeValidationEngine();
final syncFindings = syncEngine.validate(
units: units,
slots: slots,
personnel: const [],
);
expect(isolateResponse.findings.length, syncFindings.length);
for (var i = 0; i < syncFindings.length; i++) {
expect(isolateResponse.findings[i].findingCode, syncFindings[i].findingCode);
expect(isolateResponse.findings[i].severity, syncFindings[i].severity);
}
});
test('dispose() sets isStarted to false', () async {
await runner.start();
await runner.dispose();
expect(runner.isStarted, false);
});
test('can restart after dispose()', () async {
await runner.start();
await runner.dispose();
await runner.start();
expect(runner.isStarted, true);
final response = await runner.validate(
units: const [],
slots: const [],
personnel: const [],
);
expect(response.hasError, false);
});
});
// ---------------------------------------------------------------------------
// ValidationResponse
// ---------------------------------------------------------------------------
group('ValidationResponse', () {
test('hasError is false when error is null', () {
const response = ValidationResponse(findings: []);
expect(response.hasError, false);
});
test('hasError is true when error is set', () {
const response = ValidationResponse(findings: [], error: 'Something went wrong');
expect(response.hasError, true);
});
});
}