aero-agent-roles 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +18 -0
- package/README.md +49 -0
- package/bin/aero-agent-roles.js +127 -0
- package/lib/catalog.js +97 -0
- package/lib/install.js +48 -0
- package/lib/mcp.js +199 -0
- package/manifest.json +516 -0
- package/package.json +44 -0
- package/roles/aerodynamics-engineer/ROLE.md +153 -0
- package/roles/aerodynamics-engineer/SOURCES.md +12 -0
- package/roles/aerodynamics-engineer/cli.py +143 -0
- package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
- package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
- package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
- package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
- package/roles/aircraft-design-engineer/ROLE.md +138 -0
- package/roles/aircraft-design-engineer/SOURCES.md +12 -0
- package/roles/aircraft-design-engineer/cli.py +152 -0
- package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
- package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
- package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
- package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
- package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
- package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
- package/roles/airworthiness-compliance-engineer/cli.py +146 -0
- package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
- package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
- package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
- package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
- package/roles/as9100-quality-auditor/ROLE.md +128 -0
- package/roles/as9100-quality-auditor/SOURCES.md +16 -0
- package/roles/as9100-quality-auditor/cli.py +136 -0
- package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
- package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
- package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
- package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
- package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
- package/roles/data-bus-avionics-engineer/cli.py +311 -0
- package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
- package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
- package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
- package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
- package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
- package/roles/do160-environmental-engineer/ROLE.md +136 -0
- package/roles/do160-environmental-engineer/SOURCES.md +30 -0
- package/roles/do160-environmental-engineer/cli.py +147 -0
- package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
- package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
- package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
- package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
- package/roles/do178c-cert-engineer/ROLE.md +120 -0
- package/roles/do178c-cert-engineer/SOURCES.md +16 -0
- package/roles/do178c-cert-engineer/cli.py +129 -0
- package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
- package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
- package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
- package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
- package/roles/do254-hardware-engineer/ROLE.md +127 -0
- package/roles/do254-hardware-engineer/SOURCES.md +19 -0
- package/roles/do254-hardware-engineer/cli.py +308 -0
- package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
- package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
- package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
- package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
- package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
- package/roles/engineering-analysis-engineer/ROLE.md +154 -0
- package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
- package/roles/engineering-analysis-engineer/cli.py +151 -0
- package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
- package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
- package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
- package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
- package/roles/flight-management-engineer/ROLE.md +160 -0
- package/roles/flight-management-engineer/SOURCES.md +30 -0
- package/roles/flight-management-engineer/cli.py +258 -0
- package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
- package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
- package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
- package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
- package/roles/flight-mechanics-engineer/ROLE.md +162 -0
- package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
- package/roles/flight-mechanics-engineer/cli.py +149 -0
- package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
- package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
- package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
- package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
- package/roles/flight-test-engineer/ROLE.md +137 -0
- package/roles/flight-test-engineer/SOURCES.md +24 -0
- package/roles/flight-test-engineer/cli.py +135 -0
- package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
- package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
- package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
- package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
- package/roles/flight-test-performance-engineer/ROLE.md +165 -0
- package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
- package/roles/flight-test-performance-engineer/cli.py +319 -0
- package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
- package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
- package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
- package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
- package/roles/gnc-engineer/ROLE.md +141 -0
- package/roles/gnc-engineer/SOURCES.md +35 -0
- package/roles/gnc-engineer/cli.py +141 -0
- package/roles/gnc-engineer/core/gnc_core.py +1074 -0
- package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
- package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
- package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
- package/roles/guidance-engineer/ROLE.md +156 -0
- package/roles/guidance-engineer/SOURCES.md +43 -0
- package/roles/guidance-engineer/cli.py +280 -0
- package/roles/guidance-engineer/core/guidance_core.py +820 -0
- package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
- package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
- package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
- package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
- package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
- package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
- package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
- package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
- package/roles/ndt-engineer/ROLE.md +157 -0
- package/roles/ndt-engineer/SOURCES.md +18 -0
- package/roles/ndt-engineer/cli.py +316 -0
- package/roles/ndt-engineer/core/ndt_core.py +1590 -0
- package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
- package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
- package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
- package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
- package/roles/numerical-analysis-engineer/ROLE.md +158 -0
- package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
- package/roles/numerical-analysis-engineer/cli.py +414 -0
- package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
- package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
- package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
- package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
- package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
- package/roles/propulsion-engineer/ROLE.md +163 -0
- package/roles/propulsion-engineer/SOURCES.md +15 -0
- package/roles/propulsion-engineer/cli.py +168 -0
- package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
- package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
- package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
- package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
- package/roles/space-systems-engineer/ROLE.md +161 -0
- package/roles/space-systems-engineer/SOURCES.md +49 -0
- package/roles/space-systems-engineer/cli.py +173 -0
- package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
- package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
- package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
- package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
- package/roles/state-estimation-engineer/ROLE.md +154 -0
- package/roles/state-estimation-engineer/SOURCES.md +27 -0
- package/roles/state-estimation-engineer/cli.py +326 -0
- package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
- package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
- package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
- package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
- package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
- package/roles/structures-loads-engineer/ROLE.md +171 -0
- package/roles/structures-loads-engineer/SOURCES.md +44 -0
- package/roles/structures-loads-engineer/cli.py +219 -0
- package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
- package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
- package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
- package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
- package/roles/structures-loads-engineer/tests/test_structures_loads_core.py +261 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test ndt_core: the executable engine of the NDT Engineer role.
|
|
3
|
+
|
|
4
|
+
Proves the role can DO its job standalone (no AeroSkills needed):
|
|
5
|
+
method selection per defect/material, eddy current / penetrant /
|
|
6
|
+
radiography / CT / magnetic particle / leak / AE / shearography math
|
|
7
|
+
anchored to the bound ndt leaf logic values, personnel qualification,
|
|
8
|
+
report generation, and evidence-gate checks.
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import unittest
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
15
|
+
"..", "core"))
|
|
16
|
+
from ndt_core import ( # noqa: E402
|
|
17
|
+
SENSITIVITY_RANK, add_months_clamped, anomaly_disposition,
|
|
18
|
+
applicable_methods, bath_concentration_check, build_report,
|
|
19
|
+
capillary_pressure, certification_status, check_report,
|
|
20
|
+
check_report_markdown, coil_ampere_turns_high_fill,
|
|
21
|
+
coil_ampere_turns_low_fill, coil_current_from_turns,
|
|
22
|
+
conductivity_from_iacs, contrast_ratio, cost_rank,
|
|
23
|
+
crack_radius_from_width, ct_number, density_verdict,
|
|
24
|
+
developer_coverage_mass, discontinuity_class, disposition,
|
|
25
|
+
dwell_time_for_depth, eddy_current_density_ratio, effective_diameter_hollow,
|
|
26
|
+
effective_ld_ratio, example_item, example_report_markdown, exposure_time,
|
|
27
|
+
felicity_ratio, frequency_for_depth, gauge_resolution_time,
|
|
28
|
+
geometric_unsharpness, group_hits_to_events, head_shot_current,
|
|
29
|
+
hit_threshold_check, indication_is_linear, iqi_sensitivity_percent,
|
|
30
|
+
kaiser_effect_check, level_meets, magnification, magnetization_for_defect,
|
|
31
|
+
material_class_from_ct_number, min_detectable_strain, particle_sensitivity,
|
|
32
|
+
particle_size_class, phase_lag_degrees, porosity_fraction,
|
|
33
|
+
pressure_decay_rate, projection_count, qualification_review, recert_due_date,
|
|
34
|
+
render_report_markdown, residual_field_verdict, rt_setup_verdict,
|
|
35
|
+
scan_time, select_frequency_for_flaw, select_load, select_method,
|
|
36
|
+
sensitivity_rank, shear_for_defect, solenoid_field_strength,
|
|
37
|
+
source_location_linear, standard_depth_of_penetration, strain_from_phase,
|
|
38
|
+
supervision_valid, tangential_field_verdict, tube_energy_kv,
|
|
39
|
+
upgrade_eligible, vision_due_date, void_diameter, voxel_size,
|
|
40
|
+
washburn_penetration_depth,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Method selection (ndt-method-selection leaf anchors)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestMethodSelection(unittest.TestCase):
|
|
49
|
+
|
|
50
|
+
def test_applicable_sets(self):
|
|
51
|
+
self.assertEqual(applicable_methods("surface", "ferromagnetic"),
|
|
52
|
+
["MT", "PT"])
|
|
53
|
+
self.assertEqual(applicable_methods("surface", "non-ferromagnetic"),
|
|
54
|
+
["ET", "PT"])
|
|
55
|
+
self.assertEqual(applicable_methods("surface", "non-conductive"),
|
|
56
|
+
["PT"])
|
|
57
|
+
self.assertEqual(applicable_methods("near-surface", "ferromagnetic"),
|
|
58
|
+
["ET", "UT"])
|
|
59
|
+
self.assertEqual(applicable_methods("internal", "ferromagnetic"),
|
|
60
|
+
["RT", "UT"])
|
|
61
|
+
|
|
62
|
+
def test_sensitivity_and_cost(self):
|
|
63
|
+
self.assertEqual(SENSITIVITY_RANK["UT"], 5)
|
|
64
|
+
self.assertEqual(sensitivity_rank("RT"), 4)
|
|
65
|
+
self.assertEqual(cost_rank("PT"), 1)
|
|
66
|
+
self.assertEqual(cost_rank("RT"), 4)
|
|
67
|
+
|
|
68
|
+
def test_select_surface_non_ferromagnetic(self):
|
|
69
|
+
pick = select_method("surface", "non-ferromagnetic")
|
|
70
|
+
self.assertEqual(pick["method"], "ET")
|
|
71
|
+
self.assertEqual(pick["alternates"], ["PT"])
|
|
72
|
+
self.assertIn("highest-sensitivity", pick["rationale"])
|
|
73
|
+
|
|
74
|
+
def test_select_internal_top_ut(self):
|
|
75
|
+
pick = select_method("internal", "ferromagnetic")
|
|
76
|
+
self.assertEqual(pick["method"], "UT")
|
|
77
|
+
self.assertEqual(pick["alternates"], ["RT"])
|
|
78
|
+
|
|
79
|
+
def test_surface_ferromagnetic_tie_breaks_to_pt(self):
|
|
80
|
+
# MT and PT both rank 3; the fixed tie-break order picks PT.
|
|
81
|
+
pick = select_method("surface", "ferromagnetic")
|
|
82
|
+
self.assertEqual(pick["method"], "PT")
|
|
83
|
+
self.assertIn("MT", pick["alternates"])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Eddy current (eddy-current-inspection leaf anchors)
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestEddyCurrent(unittest.TestCase):
|
|
92
|
+
|
|
93
|
+
def test_conductivity_from_iacs(self):
|
|
94
|
+
self.assertEqual(conductivity_from_iacs(100), 5.8e7)
|
|
95
|
+
self.assertEqual(conductivity_from_iacs(30), 1.74e7)
|
|
96
|
+
self.assertEqual(conductivity_from_iacs(5), 2.9e6)
|
|
97
|
+
|
|
98
|
+
def test_standard_depth_anchors(self):
|
|
99
|
+
self.assertAlmostEqual(standard_depth_of_penetration(1e5, 5.8e7),
|
|
100
|
+
2.0898e-4, places=8)
|
|
101
|
+
self.assertAlmostEqual(standard_depth_of_penetration(60, 5.8e7),
|
|
102
|
+
8.5316e-3, places=6)
|
|
103
|
+
|
|
104
|
+
def test_frequency_for_depth_roundtrip(self):
|
|
105
|
+
f = frequency_for_depth(1e-3, 5.8e7)
|
|
106
|
+
self.assertAlmostEqual(standard_depth_of_penetration(f, 5.8e7),
|
|
107
|
+
1e-3, places=6)
|
|
108
|
+
|
|
109
|
+
def test_select_frequency_subsurface_anchor(self):
|
|
110
|
+
# 1 mm flaw under aluminum 30 pct IACS, factor 2.0 -> 3639.4103 Hz
|
|
111
|
+
self.assertAlmostEqual(
|
|
112
|
+
select_frequency_for_flaw(1e-3, 1.74e7, penetration_factor=2.0),
|
|
113
|
+
3639.4103, places=2)
|
|
114
|
+
# surface sharp response factor 0.5 -> 58230.5653 Hz
|
|
115
|
+
self.assertAlmostEqual(
|
|
116
|
+
select_frequency_for_flaw(1e-3, 1.74e7, penetration_factor=0.5),
|
|
117
|
+
58230.5653, places=1)
|
|
118
|
+
|
|
119
|
+
def test_density_ratio_and_phase(self):
|
|
120
|
+
self.assertAlmostEqual(eddy_current_density_ratio(1e-3, 1e-3),
|
|
121
|
+
0.3679, places=4)
|
|
122
|
+
self.assertAlmostEqual(eddy_current_density_ratio(0.0, 1e-3), 1.0)
|
|
123
|
+
self.assertAlmostEqual(phase_lag_degrees(1e-3, 1e-3), 57.2958,
|
|
124
|
+
places=4)
|
|
125
|
+
self.assertAlmostEqual(phase_lag_degrees(1e-3, 2e-3), 28.6479,
|
|
126
|
+
places=4)
|
|
127
|
+
|
|
128
|
+
def test_subsurface_flaw_stays_within_one_delta(self):
|
|
129
|
+
f = select_frequency_for_flaw(1e-3, 1.74e7, penetration_factor=2.0)
|
|
130
|
+
delta = standard_depth_of_penetration(f, 1.74e7)
|
|
131
|
+
self.assertAlmostEqual(delta, 2e-3)
|
|
132
|
+
self.assertAlmostEqual(eddy_current_density_ratio(1e-3, delta),
|
|
133
|
+
0.6065, places=4)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Liquid penetrant (liquid-penetrant-inspection leaf anchors)
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestLiquidPenetrant(unittest.TestCase):
|
|
142
|
+
|
|
143
|
+
def test_capillary_pressure_anchor(self):
|
|
144
|
+
self.assertAlmostEqual(capillary_pressure(0.032, 0, 1e-6), 64000.0)
|
|
145
|
+
|
|
146
|
+
def test_washburn_dwell_roundtrip(self):
|
|
147
|
+
depth = washburn_penetration_depth(0.032, 5, 0.008, 1e-6, 300)
|
|
148
|
+
t = dwell_time_for_depth(depth, 0.032, 5, 0.008, 1e-6)
|
|
149
|
+
self.assertAlmostEqual(t, 300.0, places=6)
|
|
150
|
+
|
|
151
|
+
def test_dwell_scales_with_depth_squared(self):
|
|
152
|
+
one = dwell_time_for_depth(1e-3, 0.032, 5, 0.008, 1e-6)
|
|
153
|
+
two = dwell_time_for_depth(2e-3, 0.032, 5, 0.008, 1e-6)
|
|
154
|
+
self.assertAlmostEqual(two, 4.0 * one, places=6)
|
|
155
|
+
|
|
156
|
+
def test_crack_radius_bleedout_developer_contrast(self):
|
|
157
|
+
self.assertAlmostEqual(crack_radius_from_width(2e-6), 1e-6)
|
|
158
|
+
self.assertAlmostEqual(contrast_ratio(0.8, 0.05), 0.9375, places=4)
|
|
159
|
+
self.assertAlmostEqual(developer_coverage_mass(1.0, 0.15), 0.15)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Radiography (radiographic-inspection leaf anchors)
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestRadiography(unittest.TestCase):
|
|
168
|
+
|
|
169
|
+
def test_unsharpness_exposure_iqi_anchors(self):
|
|
170
|
+
self.assertAlmostEqual(geometric_unsharpness(3.0, 500.0, 30.0),
|
|
171
|
+
0.18, places=6)
|
|
172
|
+
self.assertAlmostEqual(exposure_time(4.0, 600.0, 900.0), 16.0 / 9.0,
|
|
173
|
+
places=6)
|
|
174
|
+
self.assertAlmostEqual(iqi_sensitivity_percent(0.6, 30.0), 2.0)
|
|
175
|
+
|
|
176
|
+
def test_density_verdicts(self):
|
|
177
|
+
self.assertEqual(density_verdict(2.5)["verdict"], "acceptable")
|
|
178
|
+
self.assertEqual(density_verdict(1.5)["verdict"], "too-low")
|
|
179
|
+
self.assertEqual(density_verdict(4.5)["verdict"], "too-high")
|
|
180
|
+
|
|
181
|
+
def test_discontinuity_classification(self):
|
|
182
|
+
self.assertEqual(discontinuity_class("round globular gas pockets"),
|
|
183
|
+
"porosity")
|
|
184
|
+
self.assertEqual(discontinuity_class("sharp elongated hairline"),
|
|
185
|
+
"crack")
|
|
186
|
+
|
|
187
|
+
def test_setup_verdict_acceptable(self):
|
|
188
|
+
v = rt_setup_verdict(0.18, 2.0, 2.5)
|
|
189
|
+
self.assertTrue(v["acceptable"])
|
|
190
|
+
self.assertEqual(v["reasons"], [])
|
|
191
|
+
|
|
192
|
+
def test_setup_verdict_fails_on_bad_density(self):
|
|
193
|
+
v = rt_setup_verdict(0.18, 2.0, 1.2)
|
|
194
|
+
self.assertFalse(v["acceptable"])
|
|
195
|
+
self.assertTrue(any("density" in r for r in v["reasons"]))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Computed tomography (computed-tomography leaf anchors)
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestComputedTomography(unittest.TestCase):
|
|
204
|
+
|
|
205
|
+
def test_geometry(self):
|
|
206
|
+
self.assertAlmostEqual(magnification(0.3, 0.3), 2.0)
|
|
207
|
+
self.assertAlmostEqual(voxel_size(200e-6, 0.3, 0.3), 1.0e-4)
|
|
208
|
+
|
|
209
|
+
def test_projection_and_energy(self):
|
|
210
|
+
self.assertEqual(projection_count(1024), 1609)
|
|
211
|
+
self.assertAlmostEqual(tube_energy_kv("aluminum", 50.0), 350.0)
|
|
212
|
+
self.assertAlmostEqual(scan_time(1609, 0.1), 160.9)
|
|
213
|
+
|
|
214
|
+
def test_ct_number_and_porosity(self):
|
|
215
|
+
self.assertAlmostEqual(ct_number(28.0, 20.0), 400.0)
|
|
216
|
+
self.assertEqual(material_class_from_ct_number(400.0), "light-alloy")
|
|
217
|
+
self.assertEqual(material_class_from_ct_number(-1000.0), "air-or-gas")
|
|
218
|
+
self.assertAlmostEqual(porosity_fraction(64000, 8000000), 0.8)
|
|
219
|
+
d = void_diameter(64000, 1.0e-4)
|
|
220
|
+
self.assertAlmostEqual(d, 0.0049628, places=5)
|
|
221
|
+
self.assertEqual(void_diameter(0, 1.0e-4), 0.0)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Magnetic particle (magnetic-particle-inspection leaf anchors)
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestMagneticParticle(unittest.TestCase):
|
|
230
|
+
|
|
231
|
+
def test_magnetization_current_anchors(self):
|
|
232
|
+
self.assertEqual(head_shot_current(2.0, 800.0), 1600.0)
|
|
233
|
+
self.assertAlmostEqual(effective_diameter_hollow(2.0, 1.0),
|
|
234
|
+
1.7320508, places=6)
|
|
235
|
+
self.assertAlmostEqual(effective_ld_ratio(8.0, 1.7320508), 4.6188,
|
|
236
|
+
places=4)
|
|
237
|
+
self.assertAlmostEqual(coil_ampere_turns_low_fill(4.0), 11250.0)
|
|
238
|
+
self.assertAlmostEqual(coil_ampere_turns_high_fill(4.0), 5833.33,
|
|
239
|
+
places=2)
|
|
240
|
+
self.assertAlmostEqual(coil_current_from_turns(11250.0, 250), 45.0)
|
|
241
|
+
self.assertAlmostEqual(solenoid_field_strength(1000.0, 0.25), 4000.0)
|
|
242
|
+
|
|
243
|
+
def test_field_verdict_bands(self):
|
|
244
|
+
self.assertEqual(tangential_field_verdict(4000.0), "adequate")
|
|
245
|
+
self.assertEqual(tangential_field_verdict(2000.0), "low")
|
|
246
|
+
self.assertEqual(tangential_field_verdict(5000.0), "high")
|
|
247
|
+
self.assertEqual(tangential_field_verdict(4000.0, fluorescent=False),
|
|
248
|
+
"high") # visible band tops at 3200 A/m
|
|
249
|
+
|
|
250
|
+
def test_particles_and_bath(self):
|
|
251
|
+
self.assertEqual(particle_size_class(8.0), "extra-fine")
|
|
252
|
+
self.assertEqual(particle_size_class(45.0), "coarse")
|
|
253
|
+
self.assertEqual(particle_sensitivity(8.0), "high")
|
|
254
|
+
self.assertEqual(particle_sensitivity(25.0), "standard")
|
|
255
|
+
self.assertEqual(bath_concentration_check(0.2, "fluorescent"),
|
|
256
|
+
"within-range")
|
|
257
|
+
self.assertEqual(bath_concentration_check(0.05, "fluorescent"),
|
|
258
|
+
"below-range")
|
|
259
|
+
|
|
260
|
+
def test_indication_and_acceptance(self):
|
|
261
|
+
self.assertTrue(indication_is_linear(6.0, 1.5))
|
|
262
|
+
self.assertFalse(indication_is_linear(4.0, 2.0))
|
|
263
|
+
self.assertEqual(magnetization_for_defect("longitudinal"), "circular")
|
|
264
|
+
self.assertEqual(magnetization_for_defect("transverse"), "longitudinal")
|
|
265
|
+
self.assertEqual(residual_field_verdict(5.0), "demagnetize")
|
|
266
|
+
self.assertEqual(residual_field_verdict(2.0), "acceptable")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Leak testing (leak-testing leaf anchors)
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestLeakTesting(unittest.TestCase):
|
|
275
|
+
|
|
276
|
+
def test_decay_rate_and_gauge_anchor(self):
|
|
277
|
+
self.assertAlmostEqual(pressure_decay_rate(5.0, 0.02, 600.0),
|
|
278
|
+
0.1644872, places=6)
|
|
279
|
+
self.assertAlmostEqual(gauge_resolution_time(5.0, 5e-4, 0.2),
|
|
280
|
+
12.3365, places=4)
|
|
281
|
+
|
|
282
|
+
def test_disposition_bands(self):
|
|
283
|
+
self.assertEqual(disposition(0.16, 0.2, "pressure-decay")["verdict"],
|
|
284
|
+
"accept")
|
|
285
|
+
self.assertEqual(disposition(0.30, 0.2, "pressure-decay")["verdict"],
|
|
286
|
+
"reject") # above 1.25x allowable
|
|
287
|
+
self.assertEqual(disposition(0.22, 0.2, "pressure-decay")["verdict"],
|
|
288
|
+
"review")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Acoustic emission + shearography (leaf anchors)
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestAcousticEmission(unittest.TestCase):
|
|
297
|
+
|
|
298
|
+
def test_hits_and_events(self):
|
|
299
|
+
r = hit_threshold_check([40.0, 55.0, 60.0], 50.0)
|
|
300
|
+
self.assertEqual(r["hit_count"], 2)
|
|
301
|
+
events = group_hits_to_events([0.001, 0.002, 0.02, 0.021], 0.01)
|
|
302
|
+
self.assertEqual(len(events), 2)
|
|
303
|
+
|
|
304
|
+
def test_felicity(self):
|
|
305
|
+
self.assertAlmostEqual(felicity_ratio(0.85, 1.0), 0.85)
|
|
306
|
+
self.assertTrue(kaiser_effect_check(1.0, 1.0)["kaiser_effect_holds"])
|
|
307
|
+
self.assertTrue(kaiser_effect_check(0.8, 1.0)["damage_indicated"])
|
|
308
|
+
x = source_location_linear(2.0, [0.0001, 0.0003], 5000.0)
|
|
309
|
+
self.assertAlmostEqual(x, 0.5)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestShearography(unittest.TestCase):
|
|
313
|
+
|
|
314
|
+
def test_strain_phase_anchor(self):
|
|
315
|
+
# 0.5 rad, 5 mm shear, 532 nm -> ~4.23 micron/m strain
|
|
316
|
+
self.assertAlmostEqual(strain_from_phase(0.5, 5.0), 4.2335e-6,
|
|
317
|
+
places=9)
|
|
318
|
+
self.assertAlmostEqual(shear_for_defect(10.0), 5.0)
|
|
319
|
+
|
|
320
|
+
def test_load_and_disposition(self):
|
|
321
|
+
self.assertEqual(select_load(6.0, "vacuum"), 40.0)
|
|
322
|
+
self.assertEqual(select_load(2.0, "vacuum"), 20.0)
|
|
323
|
+
self.assertEqual(anomaly_disposition(3.0, 4.0, 5.0)["verdict"],
|
|
324
|
+
"accept")
|
|
325
|
+
self.assertEqual(anomaly_disposition(4.2, 4.0, 5.0)["verdict"],
|
|
326
|
+
"review")
|
|
327
|
+
self.assertEqual(anomaly_disposition(5.0, 4.0, 5.0)["verdict"],
|
|
328
|
+
"reject")
|
|
329
|
+
|
|
330
|
+
def test_min_detectable(self):
|
|
331
|
+
self.assertAlmostEqual(min_detectable_strain(0.1, 5.0), 2.54e-6,
|
|
332
|
+
places=8)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Personnel qualification (ndt-personnel-qualification leaf anchors)
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class TestPersonnelQualification(unittest.TestCase):
|
|
341
|
+
|
|
342
|
+
def test_due_dates_and_clamp(self):
|
|
343
|
+
self.assertEqual(recert_due_date("2023-06-15"), "2026-06-15")
|
|
344
|
+
self.assertEqual(vision_due_date("2026-02-01"), "2027-02-01")
|
|
345
|
+
self.assertEqual(add_months_clamped("2026-01-31", 1), "2026-02-28")
|
|
346
|
+
self.assertEqual(add_months_clamped("2024-01-31", 1), "2024-02-29")
|
|
347
|
+
|
|
348
|
+
def test_status_current_and_overdue(self):
|
|
349
|
+
self.assertEqual(certification_status("2023-06-15", "2026-06-15",
|
|
350
|
+
"2027-02-01", "2026-09-04"),
|
|
351
|
+
"recert-due")
|
|
352
|
+
self.assertEqual(certification_status("2023-06-15", "2027-06-15",
|
|
353
|
+
"2027-02-01", "2026-09-04"),
|
|
354
|
+
"current")
|
|
355
|
+
|
|
356
|
+
def test_supervision_and_levels(self):
|
|
357
|
+
self.assertFalse(supervision_valid("i", "i"))
|
|
358
|
+
self.assertTrue(supervision_valid("i", "iii"))
|
|
359
|
+
self.assertTrue(supervision_valid("ii", "i")) # II works independently
|
|
360
|
+
self.assertTrue(level_meets("ii", "ii"))
|
|
361
|
+
self.assertFalse(level_meets("i", "ii"))
|
|
362
|
+
|
|
363
|
+
def test_upgrade_eligible(self):
|
|
364
|
+
self.assertTrue(upgrade_eligible("i", "ii", 40, 40, 4, 4, True))
|
|
365
|
+
self.assertFalse(upgrade_eligible("i", "ii", 10, 40, 4, 4, True))
|
|
366
|
+
with self.assertRaises(ValueError):
|
|
367
|
+
upgrade_eligible("i", "iii", 40, 40, 4, 4, True)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
# Report builder + gates + standalone
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class TestReportBuilder(unittest.TestCase):
|
|
376
|
+
|
|
377
|
+
def test_example_report_all_gates_pass(self):
|
|
378
|
+
model = build_report(example_item())
|
|
379
|
+
gates = check_report(model)
|
|
380
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
381
|
+
|
|
382
|
+
def test_example_selections(self):
|
|
383
|
+
model = build_report(example_item())
|
|
384
|
+
tops = [r["top_method"] for r in model["defect_population"]]
|
|
385
|
+
self.assertEqual(tops, ["ET", "UT", "UT"])
|
|
386
|
+
self.assertIn("eddy-current-surface", model["parameter_cards"])
|
|
387
|
+
self.assertIn("computed-tomography", model["parameter_cards"])
|
|
388
|
+
self.assertIn("leak-test", model["parameter_cards"])
|
|
389
|
+
|
|
390
|
+
def test_personnel_qualified_in_example(self):
|
|
391
|
+
model = build_report(example_item())
|
|
392
|
+
review = model["personnel"]["review"]
|
|
393
|
+
self.assertEqual(review["certification_status"], "current")
|
|
394
|
+
self.assertTrue(review["supervision_ok"])
|
|
395
|
+
self.assertTrue(all(r["operator_meets"]
|
|
396
|
+
for r in model["personnel"]["method_levels"]))
|
|
397
|
+
|
|
398
|
+
def test_leak_reject_path_fails_gate_only_on_disposition_data(self):
|
|
399
|
+
# Gate structure: plan gates stay green; the leak card disposition
|
|
400
|
+
# data honestly reflects the tighter allowable.
|
|
401
|
+
item = example_item()
|
|
402
|
+
item.leak["max_allowable_sccs"] = 0.1
|
|
403
|
+
model = build_report(item)
|
|
404
|
+
self.assertEqual(model["parameter_cards"]["leak-test"]["verdict"],
|
|
405
|
+
"reject")
|
|
406
|
+
self.assertTrue(check_report(model)["all_pass"])
|
|
407
|
+
|
|
408
|
+
def test_stale_personnel_fails_qualification_gate(self):
|
|
409
|
+
model = build_report(example_item())
|
|
410
|
+
model["personnel"]["review"]["certification_status"] = "recert-due"
|
|
411
|
+
gates = check_report(model)
|
|
412
|
+
self.assertFalse(gates["personnel_qualified"])
|
|
413
|
+
self.assertFalse(gates["all_pass"])
|
|
414
|
+
|
|
415
|
+
def test_markdown_gates_pass(self):
|
|
416
|
+
md = example_report_markdown()
|
|
417
|
+
gates = check_report_markdown(md)
|
|
418
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
419
|
+
|
|
420
|
+
def test_markdown_missing_approval_fails(self):
|
|
421
|
+
md = example_report_markdown().replace("Not an approval",
|
|
422
|
+
"review complete")
|
|
423
|
+
gates = check_report_markdown(md)
|
|
424
|
+
self.assertFalse(gates["all_pass"])
|
|
425
|
+
|
|
426
|
+
def test_standalone_no_skills_repo(self):
|
|
427
|
+
os.environ["AEROSKILLS_DEV"] = "/nonexistent"
|
|
428
|
+
model = build_report(example_item())
|
|
429
|
+
self.assertTrue(model["defect_population"])
|
|
430
|
+
self.assertTrue(check_report(model)["all_pass"])
|
|
431
|
+
md = render_report_markdown(model)
|
|
432
|
+
self.assertGreater(len(md), 4000)
|
|
433
|
+
self.assertIn("not an approval", md.lower())
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
unittest.main()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Role test: NDT Engineer.
|
|
3
|
+
|
|
4
|
+
Verifies (offline, no network):
|
|
5
|
+
1. Every skill bound in ROLE.md frontmatter RESOLVES to a real leaf in
|
|
6
|
+
the aero-agent-skills repo (the pinned skills_release band).
|
|
7
|
+
2. The workflow stage order is deterministic and complete.
|
|
8
|
+
3. The deliverable template is filled (zero blanks) and carries the
|
|
9
|
+
DRAFT / not-an-approval boundary.
|
|
10
|
+
4. ROLE.md boundaries and the executable core + cli exist.
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import unittest
|
|
16
|
+
|
|
17
|
+
ROLES_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
|
|
18
|
+
os.path.abspath(__file__))))) # repo root (4 up from tests/)
|
|
19
|
+
# Bound-skill resolution requires the Aero Agent Skills checkout. It is a
|
|
20
|
+
# cross-repo dev check: when the skills repo is absent (fresh public clone),
|
|
21
|
+
# skip resolution rather than fail - the role's bound list is verified in CI.
|
|
22
|
+
AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
|
|
23
|
+
HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
|
|
24
|
+
ROLE_DIR = os.path.join(ROLES_REPO, "roles", "ndt-engineer")
|
|
25
|
+
TEMPLATE = os.path.join(ROLE_DIR, "templates", "ndt-plan-template.md")
|
|
26
|
+
|
|
27
|
+
EXPECTED_BOUND = [
|
|
28
|
+
"manufacturing-quality/ndt/ndt-method-selection",
|
|
29
|
+
"manufacturing-quality/ndt/eddy-current-inspection",
|
|
30
|
+
"manufacturing-quality/ndt/liquid-penetrant-inspection",
|
|
31
|
+
"manufacturing-quality/ndt/radiographic-inspection",
|
|
32
|
+
"manufacturing-quality/ndt/computed-tomography",
|
|
33
|
+
"manufacturing-quality/ndt/magnetic-particle-inspection",
|
|
34
|
+
"manufacturing-quality/ndt/leak-testing",
|
|
35
|
+
"manufacturing-quality/ndt/acoustic-emission-inspection",
|
|
36
|
+
"manufacturing-quality/ndt/shearography-inspection",
|
|
37
|
+
"manufacturing-quality/ndt/ndt-personnel-qualification",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
EXPECTED_STAGES = [
|
|
41
|
+
"Method selection per defect class",
|
|
42
|
+
"Eddy current parameter set",
|
|
43
|
+
"Liquid penetrant dwell sizing",
|
|
44
|
+
"Radiography technique verdict",
|
|
45
|
+
"Computed tomography resolution plan",
|
|
46
|
+
"Magnetic particle magnetization plan",
|
|
47
|
+
"Leak test plan and disposition",
|
|
48
|
+
"Acoustic emission monitoring plan",
|
|
49
|
+
"Shearography strain plan",
|
|
50
|
+
"Personnel qualification review",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Each bound leaf must ship the logic file the core mirrors (and that the
|
|
54
|
+
# CLI dispatches for cross-checks when the skills repo is present).
|
|
55
|
+
EXPECTED_LOGIC = {
|
|
56
|
+
"ndt-method-selection": "ndt_selection_logic.py",
|
|
57
|
+
"eddy-current-inspection": "eddy_current_inspection_logic.py",
|
|
58
|
+
"liquid-penetrant-inspection": "liquid_penetrant_inspection_logic.py",
|
|
59
|
+
"radiographic-inspection": "radiographic_inspection.py",
|
|
60
|
+
"computed-tomography": "computed_tomography_logic.py",
|
|
61
|
+
"magnetic-particle-inspection": "magnetic_particle_inspection_logic.py",
|
|
62
|
+
"leak-testing": "leak_testing_logic.py",
|
|
63
|
+
"acoustic-emission-inspection": "acoustic_emission_inspection_logic.py",
|
|
64
|
+
"shearography-inspection": "shearography_inspection_logic.py",
|
|
65
|
+
"ndt-personnel-qualification": "ndt_personnel_qualification_logic.py",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_role_md():
|
|
70
|
+
p = os.path.join(ROLE_DIR, "ROLE.md")
|
|
71
|
+
if not os.path.exists(p):
|
|
72
|
+
return None
|
|
73
|
+
return open(p).read()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestNdtRole(unittest.TestCase):
|
|
77
|
+
|
|
78
|
+
def test_bound_skills_resolve_in_aeroskills(self):
|
|
79
|
+
if not HAS_SKILLS:
|
|
80
|
+
self.skipTest("Aero Agent Skills checkout not present "
|
|
81
|
+
"(cross-repo dev check)")
|
|
82
|
+
for leaf in EXPECTED_BOUND:
|
|
83
|
+
sk = os.path.join(AEROSKILLS, "skills", leaf, "SKILL.md")
|
|
84
|
+
self.assertTrue(os.path.exists(sk),
|
|
85
|
+
f"bound skill not found: {leaf}")
|
|
86
|
+
|
|
87
|
+
def test_bound_leaves_ship_logic_files(self):
|
|
88
|
+
if not HAS_SKILLS:
|
|
89
|
+
self.skipTest("Aero Agent Skills checkout not present")
|
|
90
|
+
for leaf, logic in EXPECTED_LOGIC.items():
|
|
91
|
+
p = os.path.join(AEROSKILLS, "skills",
|
|
92
|
+
"manufacturing-quality/ndt", leaf,
|
|
93
|
+
"scripts", logic)
|
|
94
|
+
self.assertTrue(os.path.exists(p),
|
|
95
|
+
f"logic file missing for {leaf}: {logic}")
|
|
96
|
+
|
|
97
|
+
def test_bound_skills_listed_in_frontmatter(self):
|
|
98
|
+
role = read_role_md()
|
|
99
|
+
self.assertIsNotNone(role)
|
|
100
|
+
for leaf in EXPECTED_BOUND:
|
|
101
|
+
self.assertIn(leaf, role, f"missing in ROLE.md skills_bound: "
|
|
102
|
+
f"{leaf}")
|
|
103
|
+
|
|
104
|
+
def test_workflow_stages_deterministic(self):
|
|
105
|
+
role = read_role_md()
|
|
106
|
+
idx = [role.find(stage) for stage in EXPECTED_STAGES]
|
|
107
|
+
self.assertTrue(all(i >= 0 for i in idx), "a stage is missing")
|
|
108
|
+
self.assertEqual(idx, sorted(idx), "stage order not deterministic")
|
|
109
|
+
|
|
110
|
+
def test_deliverable_template_complete(self):
|
|
111
|
+
self.assertTrue(os.path.exists(TEMPLATE),
|
|
112
|
+
"ndt-plan-template.md missing")
|
|
113
|
+
text = open(TEMPLATE).read()
|
|
114
|
+
# all 5 numbered sections present
|
|
115
|
+
for n in range(1, 6):
|
|
116
|
+
self.assertTrue(re.search(rf"^## {n}\. ", text, re.M),
|
|
117
|
+
f"section {n} missing")
|
|
118
|
+
self.assertIn("Nondestructive Test Plan and Method Selection Report",
|
|
119
|
+
text)
|
|
120
|
+
# ZERO blank ___ fields (filled deliverable)
|
|
121
|
+
self.assertNotIn("___", text)
|
|
122
|
+
low = text.lower()
|
|
123
|
+
self.assertIn("draft", low)
|
|
124
|
+
self.assertIn("not an approval", low)
|
|
125
|
+
|
|
126
|
+
def test_forbidden_lines_present(self):
|
|
127
|
+
role = read_role_md()
|
|
128
|
+
for phrase in ["issue acceptance or disposition approval",
|
|
129
|
+
"certify NDT personnel",
|
|
130
|
+
"sign_off_required: true"]:
|
|
131
|
+
self.assertIn(phrase, role, f"missing boundary: {phrase}")
|
|
132
|
+
|
|
133
|
+
def test_sources_register_exists(self):
|
|
134
|
+
self.assertTrue(os.path.exists(os.path.join(ROLE_DIR,
|
|
135
|
+
"SOURCES.md")))
|
|
136
|
+
|
|
137
|
+
def test_core_engine_exists(self):
|
|
138
|
+
"""100% standard: every role ships an executable core + cli."""
|
|
139
|
+
for f in ["core", "cli.py"]:
|
|
140
|
+
self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, f)),
|
|
141
|
+
f"{f} missing - role is not executable")
|
|
142
|
+
core_dir = os.path.join(ROLE_DIR, "core")
|
|
143
|
+
self.assertTrue(any(f.endswith("_core.py")
|
|
144
|
+
for f in os.listdir(core_dir)))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
unittest.main()
|