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,105 @@
|
|
|
1
|
+
# Flight Test Performance Data Analysis Report
|
|
2
|
+
|
|
3
|
+
**Aircraft:** FT-2 Worked-Example Turbofan Regional Jet
|
|
4
|
+
**Sortie:** SORTIE-FT2-047
|
|
5
|
+
**Basis:** FAR/CS-25 (context only)
|
|
6
|
+
**Status:** draft-for-review
|
|
7
|
+
|
|
8
|
+
_Flight data below are SIMULATED for the worked example and must be replaced by recorded flight data._
|
|
9
|
+
|
|
10
|
+
## 1. Test conditions and data quality
|
|
11
|
+
|
|
12
|
+
- Field pressure altitude: 152 m (500 ft); OAT 22.0 C (ISA deviation +8.0 K).
|
|
13
|
+
- Test-day density ratio sigma = 0.9588 (rho = 1.1745 kg/m^3); density altitude 436 m (1432 ft).
|
|
14
|
+
- Reference gross weight for corrections: 250000 N. Wing area 122.6 m^2; drag polar cd0 = 0.020, k = 0.042.
|
|
15
|
+
- Data quality: takeoff trace ok; level-acceleration trace ok.
|
|
16
|
+
|
|
17
|
+
## 2. Takeoff performance
|
|
18
|
+
|
|
19
|
+
Run TO-1 at W = 240000 N: liftoff 46.0 m/s at a mean acceleration of 2.30 m/s^2.
|
|
20
|
+
|
|
21
|
+
| Leg | Measured (m) | Predicted (m) |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Ground roll (to rotation) | 440.0 | 460.0 |
|
|
24
|
+
| Rotation | 130.2 | 130.2 |
|
|
25
|
+
| Airborne to 35 ft | 81.8 | 81.8 |
|
|
26
|
+
| **Takeoff distance** | **652.1** | **672.0** |
|
|
27
|
+
|
|
28
|
+
Engine-out (OEI) continued takeoff at V1 = 38.0 m/s, V2 = 46.3 m/s: failure leg 293.5 m + recognition 38.0 m + continued ground 159.0 m + climb 176.4 m = **666.9 m**.
|
|
29
|
+
Accelerate-stop at V1 = 38.0 m/s: accelerate 313.9 m + stop 175.3 m = **489.2 m**.
|
|
30
|
+
|
|
31
|
+
## 3. Landing performance
|
|
32
|
+
|
|
33
|
+
Run LD-1 at W = 232000 N: Vs0 at run weight 33.83 m/s EAS, Vref = 41.61 m/s EAS (1.23 x Vs0), 42.49 m/s TAS at the test-day density ratio.
|
|
34
|
+
|
|
35
|
+
| Segment | Measured (m) | Model (m) |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Airborne (flare) | 212.5 | 212.5 |
|
|
38
|
+
| Ground roll (braking) | 231.5 | 219.2 |
|
|
39
|
+
| **Demonstrated total** | **443.9** | 431.7 |
|
|
40
|
+
|
|
41
|
+
Certified field length = 1.67 x demonstrated = **741.4 m** against a 900 m runway: FITS (margin +158.6 m).
|
|
42
|
+
|
|
43
|
+
## 4. Level-acceleration thrust determination
|
|
44
|
+
|
|
45
|
+
Run LA-1 at 8000 m (ISA density 0.5252 kg/m^3), W = 250000 N, trace smoothed over a 5-sample window; assessment region samples 4-18.
|
|
46
|
+
|
|
47
|
+
| Quantity | Value |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Mean acceleration | 1.000 m/s^2 |
|
|
50
|
+
| Mean specific excess power P_s | 16.315 m/s |
|
|
51
|
+
| Mean excess thrust (drag gap) | 25492.9 N |
|
|
52
|
+
| Mean drag (polar) | 19686.8 N |
|
|
53
|
+
| **Mean thrust available** | **45179.8 N** |
|
|
54
|
+
| Sustained acceleration over band | PASS |
|
|
55
|
+
| P_s at reference weight and standard density | 13.773 m/s |
|
|
56
|
+
|
|
57
|
+
## 5. Cruise performance (fuel flow reduction)
|
|
58
|
+
|
|
59
|
+
Measured fuel flows corrected from the test weight to the reference weight with the sqrt(w_ref/w_test) correction at 10668 m; range performance rp = V_tas / wf_corrected.
|
|
60
|
+
|
|
61
|
+
| Mach | wf (kg/s) | wf corr (kg/s) | rp (m/kg) | fitted rp |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| 0.70 | 0.455 | 0.464 | 447.0 | 445.6 |
|
|
64
|
+
| 0.74 | 0.442 | 0.453 | 484.4 | 488.6 |
|
|
65
|
+
| 0.78 | 0.448 | 0.461 | 501.6 | 497.4 |
|
|
66
|
+
| 0.82 | 0.500 | 0.517 | 470.5 | 471.9 |
|
|
67
|
+
|
|
68
|
+
Quadratic range-performance fit rp(M) = -10708.552 M^2 +16496.371 M -5854.7; maximum range cruise at M = 0.770; long range cruise (99% of max rp) at M = 0.792.
|
|
69
|
+
Fit residuals RMS: 3.1 m/kg (mean rp 475.9 m/kg).
|
|
70
|
+
|
|
71
|
+
## 6. Stall speeds and V-speed corrections
|
|
72
|
+
|
|
73
|
+
| Config | Vs measured (m/s EAS) | Vs predicted at run weight (m/s EAS) | Delta | Margin |
|
|
74
|
+
|---|---|---|---|---|
|
|
75
|
+
| clean | 39.90 | 39.98 | -0.19% | -0.002 |
|
|
76
|
+
| landing | 34.10 | 33.83 | +0.81% | +0.008 |
|
|
77
|
+
|
|
78
|
+
Weight correction v(w) = v_ref * sqrt(w/w_ref) applied to the reference stall speeds Vs1g = 40.80 m/s EAS and Vs0 = 35.11 m/s EAS.
|
|
79
|
+
|
|
80
|
+
## 7. Computed vs predicted scatter
|
|
81
|
+
|
|
82
|
+
| Item | Measured | Predicted | Delta (%) | Within %+10.0 |
|
|
83
|
+
|---|---|---|---|---|
|
|
84
|
+
| takeoff ground roll | 440.0 | 460.0 | +4.53 | PASS |
|
|
85
|
+
| takeoff distance (35 ft) | 652.1 | 672.0 | +3.06 | PASS |
|
|
86
|
+
| landing ground roll | 231.5 | 219.2 | -5.31 | PASS |
|
|
87
|
+
| landing distance | 443.9 | 431.7 | -2.77 | PASS |
|
|
88
|
+
| cruise rp M=0.70 | 447.0 | 445.6 | -0.32 | PASS |
|
|
89
|
+
| cruise rp M=0.74 | 484.4 | 488.6 | +0.87 | PASS |
|
|
90
|
+
| cruise rp M=0.78 | 501.6 | 497.4 | -0.84 | PASS |
|
|
91
|
+
| cruise rp M=0.82 | 470.5 | 471.9 | +0.30 | PASS |
|
|
92
|
+
| Vs1g clean | 39.9 | 40.0 | +0.19 | PASS |
|
|
93
|
+
| Vs1g landing | 34.1 | 33.8 | -0.80 | PASS |
|
|
94
|
+
|
|
95
|
+
Maximum absolute scatter: +5.31% (band +10%) -> within band.
|
|
96
|
+
|
|
97
|
+
## 8. Findings
|
|
98
|
+
|
|
99
|
+
- Takeoff distance legs are complete and the measured ground roll agrees with the constant-acceleration model within the band.
|
|
100
|
+
- Landing certified field length (1.67 x demonstrated) fits the available runway (margin +159 m).
|
|
101
|
+
- Level-acceleration thrust determination is internally consistent (drag-polar closure) and reduces to the reference conditions.
|
|
102
|
+
- Cruise maximum-range Mach and long-range-cruise Mach read off the fitted range-performance curve with an RMS residual of 3.1 m/kg.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
*Generated by Aero Agent Roles flight-test-performance-engineer core. DRAFT for human review by the flight test / performance lead. Not an approval document and not a performance guarantee.*
|
package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test flight_test_performance_core: the executable engine of the
|
|
3
|
+
Flight Test Performance Engineer role.
|
|
4
|
+
|
|
5
|
+
Proves the role can DO its job standalone (no AeroSkills needed):
|
|
6
|
+
ISA reduction to standard conditions, takeoff/landing distance
|
|
7
|
+
reductions, level-acceleration thrust determination, cruise fuel-flow
|
|
8
|
+
reduction, speed corrections, report generation, and evidence gates.
|
|
9
|
+
Anchors are real worked values (ISA physics, leaf-documented method
|
|
10
|
+
anchors, and the deterministic worked-example sortie).
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
17
|
+
"..", "core"))
|
|
18
|
+
from flight_test_performance_core import ( # noqa: E402
|
|
19
|
+
APPROACH_FACTOR, FIELD_LENGTH_FACTOR, G0, OBSTACLE_35FT_M,
|
|
20
|
+
analyze_sortie, build_report, check_report, check_report_markdown,
|
|
21
|
+
constant_accel_distance, corrected_fuel_flow, density_altitude_m,
|
|
22
|
+
drag_from_polar, engine_failure_distance, example_item,
|
|
23
|
+
example_report_markdown, example_report_model, excess_thrust_from_ps,
|
|
24
|
+
ground_roll_integrate, isa_conditions, isa_density_ratio_m,
|
|
25
|
+
ps_at_reference_conditions, range_performance, reduce_accelerate_stop,
|
|
26
|
+
reduce_cruise_points, reduce_landing_run, reduce_level_accel_run,
|
|
27
|
+
reduce_oei_takeoff, reduce_takeoff_run, render_report_markdown,
|
|
28
|
+
specific_excess_power, stall_margin, tas_from_eas, tas_from_mach,
|
|
29
|
+
day_sigma_from_alt_oat, vs1g_from_wing_loading,
|
|
30
|
+
weight_corrected_ps,
|
|
31
|
+
weight_corrected_speed,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestIsaAtmosphere(unittest.TestCase):
|
|
36
|
+
"""Reduction to ISA / standard conditions: real physics anchors."""
|
|
37
|
+
|
|
38
|
+
def test_isa_sea_level_anchor(self):
|
|
39
|
+
t, p, rho = isa_conditions(0.0)
|
|
40
|
+
self.assertAlmostEqual(t, 288.15, places=2)
|
|
41
|
+
self.assertAlmostEqual(p, 101325.0, places=1)
|
|
42
|
+
self.assertAlmostEqual(rho, 1.225, places=5)
|
|
43
|
+
|
|
44
|
+
def test_isa_8000m_anchor(self):
|
|
45
|
+
t, p, rho = isa_conditions(8000.0)
|
|
46
|
+
self.assertAlmostEqual(t, 236.15, places=2)
|
|
47
|
+
self.assertAlmostEqual(rho, 0.52517, places=4)
|
|
48
|
+
|
|
49
|
+
def test_isa_density_ratio_tropopause(self):
|
|
50
|
+
self.assertAlmostEqual(isa_density_ratio_m(11000.0), 0.29707,
|
|
51
|
+
places=4)
|
|
52
|
+
|
|
53
|
+
def test_tas_from_eas_anchor(self):
|
|
54
|
+
# 110 m/s EAS at the 8000 m ISA density -> 168.0 m/s TAS
|
|
55
|
+
self.assertAlmostEqual(tas_from_eas(110.0, 0.52517), 168.0,
|
|
56
|
+
places=1)
|
|
57
|
+
|
|
58
|
+
def test_day_sigma_below_one_hot_day(self):
|
|
59
|
+
sigma = day_sigma_from_alt_oat(152.4, 22.0)
|
|
60
|
+
self.assertGreater(sigma, 0.9)
|
|
61
|
+
self.assertLess(sigma, 1.0)
|
|
62
|
+
|
|
63
|
+
def test_density_altitude_isa_day(self):
|
|
64
|
+
# On the ISA day density altitude ~ pressure altitude (152.4 m).
|
|
65
|
+
da = density_altitude_m(152.4, 14.0)
|
|
66
|
+
self.assertAlmostEqual(da, 152.4, delta=5.0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestSpeedCorrections(unittest.TestCase):
|
|
70
|
+
"""Stall/V-speed corrections (stall-speed leaf methods)."""
|
|
71
|
+
|
|
72
|
+
def test_vs1g_from_wing_loading_anchor(self):
|
|
73
|
+
# W/S = 250000/122.6 N/m^2, CLmax 2.0 at standard density.
|
|
74
|
+
vs = vs1g_from_wing_loading(250000.0 / 122.6, 1.225, 2.0)
|
|
75
|
+
self.assertAlmostEqual(vs, 40.799, places=2)
|
|
76
|
+
|
|
77
|
+
def test_weight_corrected_speed_sqrt_law(self):
|
|
78
|
+
v = weight_corrected_speed(50.0, 250000.0, 225000.0)
|
|
79
|
+
self.assertAlmostEqual(v, 50.0 * 0.948683, places=4)
|
|
80
|
+
|
|
81
|
+
def test_stall_margin(self):
|
|
82
|
+
self.assertAlmostEqual(stall_margin(50.0, 55.0), 0.1, places=6)
|
|
83
|
+
self.assertLess(stall_margin(50.0, 45.0), 0.0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestTakeoffReduction(unittest.TestCase):
|
|
87
|
+
"""Takeoff legs: trapezoid ground roll, rotation, 35-ft climb."""
|
|
88
|
+
|
|
89
|
+
def test_ground_roll_linear_ramp_anchor(self):
|
|
90
|
+
# Linear ramp 0 -> 20 m/s over 10 s integrates to exactly 100 m.
|
|
91
|
+
speeds = [2.0 * i for i in range(11)]
|
|
92
|
+
times = [float(i) for i in range(11)]
|
|
93
|
+
self.assertAlmostEqual(ground_roll_integrate(speeds, times), 100.0,
|
|
94
|
+
places=6)
|
|
95
|
+
|
|
96
|
+
def test_ground_roll_validates(self):
|
|
97
|
+
with self.assertRaises(ValueError):
|
|
98
|
+
ground_roll_integrate([1.0, 2.0], [0.0, 0.0])
|
|
99
|
+
|
|
100
|
+
def test_constant_accel_distance_anchor(self):
|
|
101
|
+
self.assertAlmostEqual(constant_accel_distance(46.0, 2.3), 460.0,
|
|
102
|
+
places=3)
|
|
103
|
+
|
|
104
|
+
def test_engine_failure_distance_linear_anchor(self):
|
|
105
|
+
# v = 2t sampled each second; failure at v=10 m/s -> 25 m.
|
|
106
|
+
speeds = [2.0 * i for i in range(11)]
|
|
107
|
+
times = [float(i) for i in range(11)]
|
|
108
|
+
self.assertAlmostEqual(engine_failure_distance(speeds, times, 10.0),
|
|
109
|
+
25.0, places=3)
|
|
110
|
+
|
|
111
|
+
def test_oei_takeoff_legs_chain(self):
|
|
112
|
+
res = reduce_oei_takeoff(250.0, 38.0, 46.3, 1.0, 2.2, 2.8)
|
|
113
|
+
expect = 250.0 + 38.0 + (46.3 ** 2 - 38.0 ** 2) / (2.0 * 2.2) \
|
|
114
|
+
+ 46.3 * OBSTACLE_35FT_M / 2.8
|
|
115
|
+
self.assertAlmostEqual(res["total_m"], expect, places=3)
|
|
116
|
+
self.assertEqual(res["total_m"], res["failure_m"]
|
|
117
|
+
+ res["recognition_m"] + res["ground_continue_m"]
|
|
118
|
+
+ res["climb_m"])
|
|
119
|
+
|
|
120
|
+
def test_accelerate_stop_total(self):
|
|
121
|
+
res = reduce_accelerate_stop(38.0, 2.3, 0.42 * G0)
|
|
122
|
+
self.assertAlmostEqual(res["total_m"], res["accelerate_m"]
|
|
123
|
+
+ res["stop_m"], places=6)
|
|
124
|
+
|
|
125
|
+
def test_example_takeoff_run_reduces(self):
|
|
126
|
+
item = example_item()
|
|
127
|
+
res = reduce_takeoff_run(item.takeoff_run)
|
|
128
|
+
self.assertGreater(res["ground_roll_m"], 300.0)
|
|
129
|
+
self.assertGreater(res["total_m"], res["ground_roll_m"])
|
|
130
|
+
self.assertAlmostEqual(res["total_m"], res["ground_roll_m"]
|
|
131
|
+
+ res["rotation_m"] + res["climb_35ft_m"],
|
|
132
|
+
places=3)
|
|
133
|
+
# measured trapezoid vs constant-acceleration model differ a bit
|
|
134
|
+
self.assertNotAlmostEqual(res["ground_roll_m"],
|
|
135
|
+
res["ground_roll_predicted_m"], places=0)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestLandingReduction(unittest.TestCase):
|
|
139
|
+
"""Landing distance: Vref = 1.23 Vs0, 1.67 certified factor."""
|
|
140
|
+
|
|
141
|
+
def test_approach_factor(self):
|
|
142
|
+
self.assertEqual(APPROACH_FACTOR, 1.23)
|
|
143
|
+
self.assertEqual(FIELD_LENGTH_FACTOR, 1.67)
|
|
144
|
+
|
|
145
|
+
def test_example_landing_certified(self):
|
|
146
|
+
item = example_item()
|
|
147
|
+
res = reduce_landing_run(item.landing_run,
|
|
148
|
+
item.vs0_ref_eas_ms(), item.w_ref_n,
|
|
149
|
+
day_sigma_from_alt_oat(item.field_pressure_alt_m,
|
|
150
|
+
item.field_oat_c),
|
|
151
|
+
runway_m=item.runway_m)
|
|
152
|
+
self.assertAlmostEqual(res["certified_field_length_m"],
|
|
153
|
+
1.67 * res["demonstrated_m"], places=3)
|
|
154
|
+
self.assertAlmostEqual(res["vref_eas_ms"],
|
|
155
|
+
1.23 * res["vs0_run_eas_ms"], places=6)
|
|
156
|
+
self.assertTrue(res["fits"])
|
|
157
|
+
|
|
158
|
+
def test_short_runway_flagged(self):
|
|
159
|
+
item = example_item()
|
|
160
|
+
res = reduce_landing_run(item.landing_run,
|
|
161
|
+
item.vs0_ref_eas_ms(), item.w_ref_n,
|
|
162
|
+
day_sigma_from_alt_oat(item.field_pressure_alt_m,
|
|
163
|
+
item.field_oat_c),
|
|
164
|
+
runway_m=100.0)
|
|
165
|
+
self.assertFalse(res["fits"])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TestLevelAcceleration(unittest.TestCase):
|
|
169
|
+
"""Total-energy method: P_s, excess thrust, determined thrust."""
|
|
170
|
+
|
|
171
|
+
def test_specific_excess_power_anchor(self):
|
|
172
|
+
# Leaf-documented anchor: 160 m/s at 1 m/s^2 -> 16.3155 m/s.
|
|
173
|
+
self.assertAlmostEqual(specific_excess_power(160.0, 1.0),
|
|
174
|
+
16.3155, places=3)
|
|
175
|
+
|
|
176
|
+
def test_excess_thrust_anchor(self):
|
|
177
|
+
# W 250000 N, P_s 16.3155 m/s at 160 m/s -> 25492.9 N.
|
|
178
|
+
self.assertAlmostEqual(excess_thrust_from_ps(16.3155, 160.0,
|
|
179
|
+
250000.0),
|
|
180
|
+
25492.97, places=1)
|
|
181
|
+
|
|
182
|
+
def test_weight_and_density_corrections(self):
|
|
183
|
+
ps_ref_w = weight_corrected_ps(16.3155, 250000.0, 240000.0)
|
|
184
|
+
self.assertAlmostEqual(ps_ref_w, 16.9953, places=3)
|
|
185
|
+
ps_std = ps_at_reference_conditions(16.3155, 250000.0, 240000.0,
|
|
186
|
+
1.225 * 0.9)
|
|
187
|
+
self.assertAlmostEqual(ps_std, 16.6409, places=3)
|
|
188
|
+
|
|
189
|
+
def test_example_level_accel_reduces(self):
|
|
190
|
+
item = example_item()
|
|
191
|
+
la = item.level_accel_run
|
|
192
|
+
res = reduce_level_accel_run(
|
|
193
|
+
la["times_s"], la["speeds_tas_ms"], la["weight_n"],
|
|
194
|
+
isa_conditions(8000.0)[2], item.s_m2, item.cd0, item.k,
|
|
195
|
+
w_ref_n=item.w_ref_n, rho_std=1.225, window=la["window"])
|
|
196
|
+
self.assertAlmostEqual(res["mean_acceleration"], 1.0, places=3)
|
|
197
|
+
self.assertAlmostEqual(res["mean_specific_excess_power"], 16.3155,
|
|
198
|
+
places=2)
|
|
199
|
+
self.assertTrue(res["sustained_over_band"])
|
|
200
|
+
self.assertGreater(res["mean_thrust_available"],
|
|
201
|
+
res["mean_drag"])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestCruiseReduction(unittest.TestCase):
|
|
205
|
+
"""Fuel flow reduction: sqrt weight correction + quadratic fit."""
|
|
206
|
+
|
|
207
|
+
def test_corrected_fuel_flow_sqrt(self):
|
|
208
|
+
# wf * sqrt(w_ref/w_test): heavier test weight -> lower correction.
|
|
209
|
+
self.assertAlmostEqual(corrected_fuel_flow(0.5, 200000.0,
|
|
210
|
+
180000.0),
|
|
211
|
+
0.5 * 0.948683, places=5)
|
|
212
|
+
|
|
213
|
+
def test_range_performance(self):
|
|
214
|
+
rp = range_performance(230.0, 0.45)
|
|
215
|
+
self.assertAlmostEqual(rp, 511.11, places=1)
|
|
216
|
+
|
|
217
|
+
def test_tas_from_mach_fl350(self):
|
|
218
|
+
# Mach 0.78 at 10668 m (troposphere, T = 218.8 K) -> ~231.3 m/s
|
|
219
|
+
self.assertAlmostEqual(tas_from_mach(0.78, 10668.0), 231.3,
|
|
220
|
+
delta=0.3)
|
|
221
|
+
|
|
222
|
+
def test_example_cruise_fit_finds_maximum(self):
|
|
223
|
+
item = example_item()
|
|
224
|
+
res = reduce_cruise_points(item.cruise_points, item.w_ref_n / G0)
|
|
225
|
+
self.assertEqual(res["verdict"], "maximum-found")
|
|
226
|
+
self.assertIsNotNone(res["max_rp_mach"])
|
|
227
|
+
self.assertTrue(0.7 < res["max_rp_mach"] < 0.85)
|
|
228
|
+
self.assertIsNotNone(res["lrc_mach"])
|
|
229
|
+
self.assertGreater(res["lrc_mach"], res["max_rp_mach"])
|
|
230
|
+
self.assertEqual(len(res["points"]), len(item.cruise_points))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TestReportBuilder(unittest.TestCase):
|
|
234
|
+
"""The report: content model, rendering, worked-example numbers."""
|
|
235
|
+
|
|
236
|
+
def test_analyze_sortie_full(self):
|
|
237
|
+
model = build_report(example_item())
|
|
238
|
+
res = model["results"]
|
|
239
|
+
# every discipline present with real numbers
|
|
240
|
+
self.assertGreater(res["takeoff"]["run"]["total_m"], 0.0)
|
|
241
|
+
self.assertGreater(res["takeoff"]["oei"]["total_m"], 0.0)
|
|
242
|
+
self.assertGreater(res["landing"]["certified_field_length_m"], 0.0)
|
|
243
|
+
self.assertGreater(res["level_accel"]["mean_thrust_available"], 0.0)
|
|
244
|
+
self.assertTrue(0.7 < res["cruise"]["max_rp_mach"] < 0.85)
|
|
245
|
+
self.assertTrue(res["speeds"]["stall_rows"])
|
|
246
|
+
self.assertTrue(res["scatter"]["rows"])
|
|
247
|
+
self.assertTrue(res["scatter"]["within_band"])
|
|
248
|
+
|
|
249
|
+
def test_conditions_recorded(self):
|
|
250
|
+
res = analyze_sortie(example_item())
|
|
251
|
+
c = res["conditions"]
|
|
252
|
+
self.assertGreater(c["density_altitude_m"], 0.0)
|
|
253
|
+
self.assertGreater(c["sigma_test"], 0.9)
|
|
254
|
+
self.assertLess(c["sigma_test"], 1.0)
|
|
255
|
+
|
|
256
|
+
def test_render_has_sections_and_numbers(self):
|
|
257
|
+
md = example_report_markdown()
|
|
258
|
+
for sec in ["## 1. Test conditions", "## 2. Takeoff performance",
|
|
259
|
+
"## 3. Landing performance",
|
|
260
|
+
"## 4. Level-acceleration thrust determination",
|
|
261
|
+
"## 5. Cruise performance",
|
|
262
|
+
"## 6. Stall speeds and V-speed corrections",
|
|
263
|
+
"## 7. Computed vs predicted scatter",
|
|
264
|
+
"## 8. Findings"]:
|
|
265
|
+
self.assertIn(sec, md)
|
|
266
|
+
self.assertIn("density altitude", md)
|
|
267
|
+
self.assertIn("1.67", md)
|
|
268
|
+
self.assertIn("thrust available", md)
|
|
269
|
+
self.assertIn("not an approval", md.lower())
|
|
270
|
+
self.assertIn("draft", md)
|
|
271
|
+
self.assertNotIn("___", md)
|
|
272
|
+
|
|
273
|
+
def test_model_json_roundtrip_plain(self):
|
|
274
|
+
import json
|
|
275
|
+
model = example_report_model()
|
|
276
|
+
text = json.dumps(model) # must not raise (plain JSON types)
|
|
277
|
+
self.assertGreater(len(text), 2000)
|
|
278
|
+
self.assertIn("draft-for-review", text)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class TestGates(unittest.TestCase):
|
|
282
|
+
def test_core_gates_all_pass(self):
|
|
283
|
+
model = build_report(example_item())
|
|
284
|
+
gates = check_report(model)
|
|
285
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
286
|
+
|
|
287
|
+
def test_markdown_gates_all_pass(self):
|
|
288
|
+
md = example_report_markdown()
|
|
289
|
+
gates = check_report_markdown(md)
|
|
290
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
291
|
+
|
|
292
|
+
def test_sign_off_honest_model(self):
|
|
293
|
+
model = build_report(example_item())
|
|
294
|
+
self.assertEqual(model["status"], "draft-for-review")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestStandalone(unittest.TestCase):
|
|
298
|
+
def test_no_skills_repo_required(self):
|
|
299
|
+
os.environ["AEROSKILLS_DEV"] = "/nonexistent"
|
|
300
|
+
model = build_report(example_item())
|
|
301
|
+
self.assertEqual(model["document_type"],
|
|
302
|
+
"Flight Test Performance Data Analysis Report")
|
|
303
|
+
md = render_report_markdown(model)
|
|
304
|
+
self.assertGreater(len(md), 2000)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
unittest.main()
|
package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Role test: Flight Test Performance 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 filled deliverable template (worked example) has zero blanks,
|
|
9
|
+
numbered sections, and the honest DRAFT / not-an-approval markers.
|
|
10
|
+
4. The role carries the executable core + cli (100% standard) and the
|
|
11
|
+
boundary/forbidden lines, and metadata credits the author.
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
ROLES_REPO = os.path.dirname(os.path.dirname(os.path.dirname(
|
|
19
|
+
os.path.dirname(os.path.abspath(__file__))))) # repo root
|
|
20
|
+
sys.path.insert(0, os.path.join(ROLES_REPO, "roles",
|
|
21
|
+
"flight-test-performance-engineer", "core"))
|
|
22
|
+
import flight_test_performance_core as core # noqa: E402
|
|
23
|
+
# Deterministic across midnight: pin fresh renders to the date the
|
|
24
|
+
# committed template carries (CI override wins).
|
|
25
|
+
os.environ.setdefault("ROLE_GEN_DATE", "2026-09-05")
|
|
26
|
+
AEROSKILLS = os.environ.get("AEROSKILLS_DEV",
|
|
27
|
+
os.path.expanduser("~/AeroSkills"))
|
|
28
|
+
HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
|
|
29
|
+
ROLE_DIR = os.path.join(ROLES_REPO, "roles",
|
|
30
|
+
"flight-test-performance-engineer")
|
|
31
|
+
TEMPLATE = os.path.join(ROLE_DIR, "templates",
|
|
32
|
+
"performance-report-template.md")
|
|
33
|
+
|
|
34
|
+
EXPECTED_BOUND = [
|
|
35
|
+
"flight-test-operations/performance/takeoff-distance-determination",
|
|
36
|
+
"flight-test-operations/performance/accelerate-stop-distance",
|
|
37
|
+
"flight-test-operations/performance/engine-failure-takeoff-flight-test",
|
|
38
|
+
"flight-test-operations/performance/landing-distance-determination",
|
|
39
|
+
"flight-test-operations/performance/level-acceleration-test",
|
|
40
|
+
"flight-test-operations/performance/cruise-performance-flight-test",
|
|
41
|
+
"flight-test-operations/performance/climb-performance-flight-test",
|
|
42
|
+
"flight-test-operations/performance/engine-flight-test",
|
|
43
|
+
"flight-test-operations/performance/stall-speed-determination",
|
|
44
|
+
"flight-test-operations/performance/glide-flight-test",
|
|
45
|
+
"flight-test-operations/performance/fuel-jettison-flight-test",
|
|
46
|
+
"flight-test-operations/performance/in-flight-engine-relight-test",
|
|
47
|
+
"flight-test-operations/performance/rotorcraft-performance-flight-test",
|
|
48
|
+
"flight-test-operations/performance/"
|
|
49
|
+
"rotorcraft-forward-flight-performance-test",
|
|
50
|
+
"flight-test-operations/planning/flight-test-data-reduction",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
EXPECTED_STAGES = [
|
|
54
|
+
"1. Data reduction prep", "2. Conditions", "3. Takeoff",
|
|
55
|
+
"4. Landing", "5. Level accel", "6. Cruise", "7. Speeds",
|
|
56
|
+
"8. Climb / other", "9. Rotorcraft (if rotary)", "10. Report",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def read_role_md():
|
|
61
|
+
p = os.path.join(ROLE_DIR, "ROLE.md")
|
|
62
|
+
if not os.path.exists(p):
|
|
63
|
+
return None
|
|
64
|
+
return open(p).read()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestFlightTestPerformanceRole(unittest.TestCase):
|
|
68
|
+
|
|
69
|
+
def test_bound_skills_resolve_in_aeroskills(self):
|
|
70
|
+
if not HAS_SKILLS:
|
|
71
|
+
self.skipTest("Aero Agent Skills checkout not present "
|
|
72
|
+
"(cross-repo dev check)")
|
|
73
|
+
for leaf in EXPECTED_BOUND:
|
|
74
|
+
sk = os.path.join(AEROSKILLS, "skills", leaf, "SKILL.md")
|
|
75
|
+
self.assertTrue(
|
|
76
|
+
os.path.exists(sk),
|
|
77
|
+
"bound skill not found in aero-agent-skills: %s" % leaf)
|
|
78
|
+
|
|
79
|
+
def test_bound_skills_listed_in_frontmatter(self):
|
|
80
|
+
role = read_role_md()
|
|
81
|
+
self.assertIsNotNone(role)
|
|
82
|
+
for leaf in EXPECTED_BOUND:
|
|
83
|
+
self.assertIn(leaf, role,
|
|
84
|
+
"missing in ROLE.md skills_bound: %s" % leaf)
|
|
85
|
+
|
|
86
|
+
def test_workflow_stages_deterministic(self):
|
|
87
|
+
role = read_role_md()
|
|
88
|
+
idx = [role.find(stage) for stage in EXPECTED_STAGES]
|
|
89
|
+
self.assertTrue(all(i >= 0 for i in idx), "a stage is missing")
|
|
90
|
+
self.assertEqual(idx, sorted(idx),
|
|
91
|
+
"stage order not deterministic")
|
|
92
|
+
|
|
93
|
+
def test_deliverable_template_complete(self):
|
|
94
|
+
if not os.path.exists(TEMPLATE):
|
|
95
|
+
self.fail("performance-report-template.md missing")
|
|
96
|
+
text = open(TEMPLATE).read()
|
|
97
|
+
self.assertNotIn("___", text, "template has blank fields")
|
|
98
|
+
# numbered sections 1..8 present
|
|
99
|
+
for n in range(1, 9):
|
|
100
|
+
self.assertTrue(
|
|
101
|
+
re.search(r"^## %d\. " % n, text, re.M),
|
|
102
|
+
"section %d missing" % n)
|
|
103
|
+
self.assertIn("Flight Test Performance Data Analysis Report", text)
|
|
104
|
+
low = text.lower()
|
|
105
|
+
self.assertIn("draft", low)
|
|
106
|
+
self.assertIn("not an approval", low)
|
|
107
|
+
self.assertIn("performance guarantee", low)
|
|
108
|
+
|
|
109
|
+
def test_template_is_the_worked_example(self):
|
|
110
|
+
"""The filled template matches the core's example render."""
|
|
111
|
+
template = open(TEMPLATE).read().strip()
|
|
112
|
+
rendered = core.example_report_markdown().strip()
|
|
113
|
+
self.assertEqual(template, rendered)
|
|
114
|
+
|
|
115
|
+
def test_forbidden_lines_present(self):
|
|
116
|
+
role = read_role_md()
|
|
117
|
+
for phrase in ["clear an envelope", "declare airworthiness",
|
|
118
|
+
"authorize flight", "performance guarantee",
|
|
119
|
+
"sign_off_required: true", "reproduce proprietary"]:
|
|
120
|
+
self.assertIn(phrase, role, "missing boundary: %s" % phrase)
|
|
121
|
+
|
|
122
|
+
def test_author_ashfordeou(self):
|
|
123
|
+
role = read_role_md()
|
|
124
|
+
self.assertIn("ashfordeOU", role)
|
|
125
|
+
|
|
126
|
+
def test_sources_register_exists(self):
|
|
127
|
+
p = os.path.join(ROLE_DIR, "SOURCES.md")
|
|
128
|
+
self.assertTrue(os.path.exists(p), "SOURCES.md missing for role")
|
|
129
|
+
|
|
130
|
+
def test_core_engine_exists(self):
|
|
131
|
+
"""100% standard: every role ships an executable core + cli."""
|
|
132
|
+
for f in ["core", "cli.py"]:
|
|
133
|
+
self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, f)),
|
|
134
|
+
"%s missing - role is not executable" % f)
|
|
135
|
+
core_files = [f for f in os.listdir(os.path.join(ROLE_DIR, "core"))
|
|
136
|
+
if f.endswith("_core.py")]
|
|
137
|
+
self.assertEqual(len(core_files), 1)
|
|
138
|
+
|
|
139
|
+
def test_standalone_cli_smoke(self):
|
|
140
|
+
"""cli.py build must run with no AeroSkills checkout."""
|
|
141
|
+
import subprocess
|
|
142
|
+
env = dict(os.environ)
|
|
143
|
+
env["AEROSKILLS_DEV"] = "/nonexistent"
|
|
144
|
+
out = os.path.join(ROLE_DIR, "tests", ".smoke-report.md")
|
|
145
|
+
try:
|
|
146
|
+
r = subprocess.run(
|
|
147
|
+
[sys_executable(), os.path.join(ROLE_DIR, "cli.py"),
|
|
148
|
+
"build", "--out", out],
|
|
149
|
+
capture_output=True, text=True, env=env, cwd=ROLE_DIR)
|
|
150
|
+
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
|
151
|
+
md = open(out).read()
|
|
152
|
+
self.assertIn("Flight Test Performance Data Analysis Report",
|
|
153
|
+
md)
|
|
154
|
+
finally:
|
|
155
|
+
if os.path.exists(out):
|
|
156
|
+
os.remove(out)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sys_executable():
|
|
160
|
+
import sys
|
|
161
|
+
return sys.executable
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
unittest.main()
|