use crate::*;
#[test]
fn time_scale_policy_summary_has_a_compact_display() {
let summary = TimeScalePolicySummary::current();
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(
summary.summary_line(),
CURRENT_TIME_SCALE_POLICY_SUMMARY_TEXT
);
assert!(summary.summary_line().contains("TT/TDB"));
assert!(summary.validate().is_ok());
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert_eq!(
time_scale_policy_summary_for_report().summary_line(),
summary.summary_line()
);
}
#[test]
fn time_scale_policy_summary_validate_rejects_blank_fields() {
let summary = TimeScalePolicySummary::new(" ");
let error = summary
.validate()
.expect_err("blank policy prose should fail validation");
assert_eq!(error.to_string(), "time-scale policy summary is blank");
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn time_scale_policy_summary_validate_rejects_policy_drift() {
let summary = TimeScalePolicySummary::new(
"direct backend requests accept TT/TDB; UTC/UT1 inputs require caller-supplied conversion helpers; built-in Delta T model",
);
let error = summary
.validate()
.expect_err("drifted policy prose should fail validation");
assert_eq!(
error.to_string(),
"time-scale policy summary is out of sync with the current posture"
);
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn delta_t_policy_summary_has_a_compact_display() {
let summary = DeltaTPolicySummary::current();
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(summary.summary_line(), CURRENT_DELTA_T_POLICY_SUMMARY_TEXT);
assert!(summary.summary_line().contains("Delta T"));
assert!(summary.validate().is_ok());
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert_eq!(
delta_t_policy_summary_for_report().summary_line(),
summary.summary_line()
);
}
#[test]
fn delta_t_policy_summary_validate_rejects_blank_fields() {
let summary = DeltaTPolicySummary::new(" ");
let error = summary
.validate()
.expect_err("blank Delta T policy prose should fail validation");
assert_eq!(error.to_string(), "Delta T policy summary is blank");
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn delta_t_policy_summary_validate_rejects_policy_drift() {
let summary = DeltaTPolicySummary::new("built-in Delta T modeling is documented elsewhere");
let error = summary
.validate()
.expect_err("drifted Delta T policy prose should fail validation");
assert_eq!(
error.to_string(),
"Delta T policy summary is out of sync with the current posture"
);
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn utc_convenience_policy_summary_has_a_compact_display() {
let summary = UtcConveniencePolicySummary::current();
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(
summary.summary_line(),
CURRENT_UTC_CONVENIENCE_POLICY_SUMMARY_TEXT
);
assert!(summary.summary_line().contains("UTC convenience"));
assert!(summary.validate().is_ok());
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert_eq!(
utc_convenience_policy_summary_for_report().summary_line(),
summary.summary_line()
);
}
#[test]
fn utc_convenience_policy_summary_validate_rejects_blank_fields() {
let summary = UtcConveniencePolicySummary::new(" ");
let error = summary
.validate()
.expect_err("blank UTC convenience prose should fail validation");
assert_eq!(error.to_string(), "UTC convenience policy summary is blank");
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn utc_convenience_policy_summary_validate_rejects_policy_drift() {
let summary = UtcConveniencePolicySummary::new(
"built-in UTC convenience conversion is documented elsewhere",
);
let error = summary
.validate()
.expect_err("drifted UTC convenience policy prose should fail validation");
assert_eq!(
error.to_string(),
"UTC convenience policy summary is out of sync with the current posture"
);
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn validated_utc_convenience_policy_summary_for_report_tracks_the_current_posture() {
assert_eq!(
validated_utc_convenience_policy_summary_for_report(),
CURRENT_UTC_CONVENIENCE_POLICY_SUMMARY_TEXT
);
}
#[test]
fn validated_request_policy_component_summaries_track_the_current_posture() {
assert_eq!(
validated_time_scale_policy_summary_for_report(),
CURRENT_TIME_SCALE_POLICY_SUMMARY_TEXT
);
assert_eq!(
validated_delta_t_policy_summary_for_report(),
CURRENT_DELTA_T_POLICY_SUMMARY_TEXT
);
assert_eq!(
validated_request_policy_summary_for_report(),
current_request_policy_summary()
.validated_summary_line()
.unwrap()
);
assert_eq!(
validated_observer_policy_summary_for_report(),
CURRENT_OBSERVER_POLICY_SUMMARY_TEXT
);
assert_eq!(
validated_apparentness_policy_summary_for_report(),
CURRENT_APPARENTNESS_POLICY_SUMMARY_TEXT
);
}
#[test]
fn request_policy_summary_has_a_compact_display() {
let summary = RequestPolicySummary::current();
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(
summary.summary_line(),
RequestPolicySummary {
time_scale: CURRENT_TIME_SCALE_POLICY_SUMMARY_TEXT,
observer: CURRENT_OBSERVER_POLICY_SUMMARY_TEXT,
apparentness: CURRENT_APPARENTNESS_POLICY_SUMMARY_TEXT,
frame: CURRENT_FRAME_POLICY_SUMMARY_TEXT,
}
.summary_line()
);
assert!(summary.summary_line().contains("time-scale="));
assert!(summary.summary_line().contains("observer="));
assert!(summary.summary_line().contains("apparentness="));
assert!(summary.summary_line().contains("frame="));
assert!(summary.validate().is_ok());
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert_eq!(
validated_request_semantics_summary_for_report(),
validated_request_policy_summary_for_report()
);
assert_eq!(
request_semantics_summary_for_report(),
request_policy_summary_for_report()
);
assert_eq!(
request_semantics_summary_for_report().summary_line(),
request_policy_summary_for_report().summary_line()
);
assert_eq!(
request_semantics_summary_for_report().validated_summary_line(),
request_policy_summary_for_report().validated_summary_line()
);
}
#[test]
fn request_policy_summary_validate_rejects_blank_fields() {
let mut summary = RequestPolicySummary::current();
summary.frame = " ";
let error = summary
.validate()
.expect_err("blank policy prose should fail validation");
assert_eq!(
error,
RequestPolicySummaryValidationError::BlankField { field: "frame" }
);
assert_eq!(
error.to_string(),
"the request-policy summary field `frame` is blank"
);
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn request_policy_summary_validate_rejects_whitespace_padded_fields() {
let mut summary = RequestPolicySummary::current();
summary.observer = " chart houses use observer locations; chart body observers stay separate; body requests stay geocentric; geocentric-only backends reject observer-bearing requests with UnsupportedObserver; malformed observer coordinates remain InvalidObserver; chart-layer topocentric body positions are supported as an opt-in correction (diurnal parallax + diurnal aberration); native-backend topocentric remains unsupported ";
let error = summary
.validate()
.expect_err("whitespace-padded policy prose should fail validation");
assert_eq!(
error,
RequestPolicySummaryValidationError::WhitespacePaddedField { field: "observer" }
);
assert_eq!(
error.to_string(),
"the request-policy summary field `observer` has surrounding whitespace"
);
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn request_policy_summary_validate_rejects_line_breaks() {
let mut summary = RequestPolicySummary::current();
summary.observer = "chart houses use observer locations\nbody requests stay geocentric";
let error = summary
.validate()
.expect_err("multi-line policy prose should fail validation");
assert_eq!(
error,
RequestPolicySummaryValidationError::EmbeddedLineBreak { field: "observer" }
);
assert_eq!(
error.to_string(),
"the request-policy summary field `observer` contains a line break"
);
assert!(summary.validated_summary_line().is_err());
}
#[test]
fn frame_treatment_summary_has_a_compact_display() {
let summary = FrameTreatmentSummary::new(
"geocentric ecliptic inputs; equatorial coordinates are derived with a mean-obliquity transform",
);
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(summary.summary_line(), "geocentric ecliptic inputs; equatorial coordinates are derived with a mean-obliquity transform");
assert_eq!(summary.validate(), Ok(()));
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert_eq!(
validated_frame_treatment_summary_for_report(),
current_request_policy_summary().frame
);
assert!(summary.summary_line().contains("mean-obliquity"));
}
#[test]
fn frame_treatment_summary_rejects_blank_summary_text() {
let summary = FrameTreatmentSummary::new(" ");
assert_eq!(
summary.validate(),
Err(FrameTreatmentSummaryValidationError::BlankSummary)
);
}
#[test]
fn frame_treatment_summary_rejects_whitespace_padded_summary_text() {
let summary = FrameTreatmentSummary::new(
" geocentric ecliptic inputs; equatorial coordinates are derived with a mean-obliquity transform ",
);
assert_eq!(
summary.validate(),
Err(FrameTreatmentSummaryValidationError::WhitespacePaddedSummary)
);
}
#[test]
fn frame_treatment_summary_rejects_embedded_line_breaks() {
let summary = FrameTreatmentSummary::new(
"geocentric ecliptic inputs;\nequatorial coordinates are derived with a mean-obliquity transform",
);
assert_eq!(
summary.validate(),
Err(FrameTreatmentSummaryValidationError::EmbeddedLineBreak)
);
}
#[test]
fn frame_policy_summary_tracks_the_current_posture() {
let summary = FramePolicySummary::new(current_request_policy_summary().frame);
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(
summary.summary_line(),
current_request_policy_summary().frame
);
assert_eq!(summary.validate(), Ok(()));
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert!(summary.summary_line().contains("mean-obliquity"));
}
#[test]
fn frame_policy_summary_rejects_policy_drift() {
let summary = FramePolicySummary::new(
"geocentric ecliptic inputs; equatorial coordinates are derived with a mean-obliquity transform",
);
assert_eq!(
summary.validate(),
Err(FramePolicySummaryValidationError::CurrentPolicyOutOfSync)
);
}
#[test]
fn frame_policy_summary_details_reuse_the_current_posture() {
let summary = frame_policy_summary_details();
assert_eq!(
summary.summary_line(),
current_request_policy_summary().frame
);
assert_eq!(
frame_policy_summary_for_report(),
current_request_policy_summary().frame
);
assert_eq!(
validated_frame_policy_summary_for_report(),
current_request_policy_summary().frame
);
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
}
#[test]
fn native_sidereal_policy_summary_tracks_the_current_posture() {
let summary = native_sidereal_policy_summary_for_report();
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(
summary.summary_line(),
current_native_sidereal_policy_summary().summary_line()
);
assert_eq!(summary.validate(), Ok(()));
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert!(summary
.summary_line()
.contains("native sidereal backend output"));
}
#[test]
fn native_sidereal_policy_summary_rejects_policy_drift() {
let summary =
NativeSiderealPolicySummary::new("native sidereal backend output is documented elsewhere");
assert_eq!(
summary.validate(),
Err(NativeSiderealPolicySummaryValidationError::CurrentPolicyOutOfSync)
);
}
#[test]
fn validated_native_sidereal_policy_summary_for_report_tracks_the_current_posture() {
assert_eq!(
validated_native_sidereal_policy_summary_for_report(),
CURRENT_NATIVE_SIDEREAL_POLICY_SUMMARY_TEXT
);
}
#[test]
fn zodiac_policy_summary_tracks_the_current_posture() {
let summary = current_zodiac_policy_summary();
assert_eq!(summary.to_string(), summary.summary_line());
assert_eq!(
summary.summary_line(),
current_zodiac_policy_summary().summary_line()
);
assert_eq!(summary.validate(), Ok(()));
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
assert_eq!(summary.summary_line(), CURRENT_ZODIAC_POLICY_SUMMARY_TEXT);
}
#[test]
fn zodiac_policy_summary_rejects_invalid_cached_prose() {
assert_eq!(
ZodiacPolicySummary::new(" ").validate(),
Err(ZodiacPolicySummaryValidationError::BlankSummary)
);
assert_eq!(
ZodiacPolicySummary::new(" tropical only ").validate(),
Err(ZodiacPolicySummaryValidationError::WhitespacePaddedSummary)
);
assert_eq!(
ZodiacPolicySummary::new("tropical\nonly").validate(),
Err(ZodiacPolicySummaryValidationError::EmbeddedLineBreak)
);
}
#[test]
fn zodiac_policy_summary_rejects_policy_drift() {
let summary = ZodiacPolicySummary::new("sidereal zodiac output is documented elsewhere");
assert_eq!(
summary.validate(),
Err(ZodiacPolicySummaryValidationError::CurrentPolicyOutOfSync)
);
}
#[test]
fn validated_zodiac_policy_summary_for_report_tracks_the_current_posture() {
assert_eq!(
validated_zodiac_policy_summary_for_report(),
CURRENT_ZODIAC_POLICY_SUMMARY_TEXT
);
}
#[test]
fn observer_policy_summary_validates_the_current_report_prose() {
let summary = observer_policy_summary_for_report();
assert_eq!(summary.summary_line(), summary.to_string());
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
}
#[test]
fn apparentness_policy_summary_validates_the_current_report_prose() {
let summary = apparentness_policy_summary_for_report();
assert_eq!(summary.summary_line(), summary.to_string());
assert_eq!(summary.validated_summary_line(), Ok(summary.summary_line()));
}
#[test]
fn observer_policy_summary_rejects_invalid_cached_prose() {
assert!(matches!(
ObserverPolicySummary::new("").validate(),
Err(ObserverPolicySummaryValidationError::BlankSummary)
));
assert!(matches!(
ObserverPolicySummary::new(" observer ").validate(),
Err(ObserverPolicySummaryValidationError::WhitespacePaddedSummary)
));
assert!(matches!(
ObserverPolicySummary::new("observer\npolicy").validate(),
Err(ObserverPolicySummaryValidationError::EmbeddedLineBreak)
));
assert!(matches!(
ObserverPolicySummary::new("observer policy drift").validate(),
Err(ObserverPolicySummaryValidationError::CurrentPolicyOutOfSync)
));
}
#[test]
fn apparentness_policy_summary_rejects_invalid_cached_prose() {
assert!(matches!(
ApparentnessPolicySummary::new("").validate(),
Err(ApparentnessPolicySummaryValidationError::BlankSummary)
));
assert!(matches!(
ApparentnessPolicySummary::new(" apparent ").validate(),
Err(ApparentnessPolicySummaryValidationError::WhitespacePaddedSummary)
));
assert!(matches!(
ApparentnessPolicySummary::new("apparent\npolicy").validate(),
Err(ApparentnessPolicySummaryValidationError::EmbeddedLineBreak)
));
assert!(matches!(
ApparentnessPolicySummary::new("apparentness policy drift").validate(),
Err(ApparentnessPolicySummaryValidationError::CurrentPolicyOutOfSync)
));
}
#[test]
fn validate_observer_policy_rejects_invalid_observer_locations_even_when_supported() {
let mut observer_request = EphemerisRequest::new(
CelestialBody::Sun,
Instant::new(
pleiades_types::JulianDay::from_days(2_451_545.0),
TimeScale::Tt,
),
);
observer_request.observer = Some(ObserverLocation::new(
Latitude::from_degrees(95.0),
Longitude::from_degrees(-0.1),
Some(45.0),
));
let error = validate_observer_policy(&observer_request, "toy backend", true).expect_err(
"invalid observer locations should fail even when topocentric support is available",
);
assert_eq!(error.kind, EphemerisErrorKind::InvalidObserver);
assert!(error
.message
.contains("observer latitude must stay within [-90, 90]"));
assert!(error.message.contains("received invalid observer location"));
}
#[test]
fn request_policy_summary_validation_rejects_stale_field_text() {
fn assert_field_out_of_sync(
mut summary: RequestPolicySummary,
field: &'static str,
mutate: impl FnOnce(&mut RequestPolicySummary),
) {
mutate(&mut summary);
let error = summary
.validate()
.expect_err("stale request-policy wording should fail validation");
assert_eq!(
error,
RequestPolicySummaryValidationError::FieldOutOfSync { field }
);
assert_eq!(
error.to_string(),
format!(
"the request-policy summary field `{field}` is out of sync with the current posture"
)
);
}
let current = current_request_policy_summary();
assert_field_out_of_sync(current, "time_scale", |summary| {
summary.time_scale =
"direct backend requests accept TT/TDB; UTC/UT1 inputs require caller-supplied conversion helpers";
});
assert_field_out_of_sync(current, "observer", |summary| {
summary.observer = "chart houses use observer locations; chart body observers stay separate; body requests stay geocentric";
});
assert_field_out_of_sync(current, "apparentness", |summary| {
summary.apparentness = "current first-party backends accept mean geometric output only";
});
assert_field_out_of_sync(current, "frame", |summary| {
summary.frame = "ecliptic body positions are the default request shape";
});
}