1use serde::Serialize;
9
10use crate::context::ResolverContext;
11use crate::manifest::{AttributionManifest, Disposition};
12use crate::matcher::{select_variant, Selected};
13use crate::time::Timestamp;
14
15pub const DEFAULT_REVALIDATE_MS: u64 = 15 * 60 * 1000;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct Resolution<'m> {
23 pub disposition: Disposition,
25 #[serde(skip)]
29 pub variant: Option<Selected<'m>>,
30 pub as_of: Timestamp,
33 pub revalidate_after: Timestamp,
35}
36
37#[must_use]
45pub fn resolve<'m>(
46 manifest: &'m AttributionManifest,
47 ctx: &ResolverContext,
48 now: Timestamp,
49) -> Resolution<'m> {
50 let disposition = manifest.disposition(now);
51 let variant = match disposition {
52 Disposition::Revoked { .. } => None,
53 _ => select_variant(&manifest.variants, ctx),
54 };
55 let window = variant
56 .and_then(|s| s.variant.revalidate_after_ms)
57 .unwrap_or(DEFAULT_REVALIDATE_MS);
58 Resolution {
59 disposition,
60 variant,
61 as_of: now,
62 revalidate_after: now.plus_ms(window),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::{CanonicalUrl, Channel, MintOptions, MintSpec, Sharer};
70
71 fn minted() -> AttributionManifest {
72 let mut entropy = |buf: &mut [u8]| {
73 buf.fill(11);
74 Ok(())
75 };
76 crate::mint(
77 MintSpec::new(
78 CanonicalUrl::new("ws://a/report.md").unwrap(),
79 Sharer::new("lead").unwrap(),
80 Channel::subagent_general(),
81 ),
82 &MintOptions::default(),
83 &mut entropy,
84 Timestamp::from_unix_ms(1_000),
85 )
86 .unwrap()
87 }
88
89 #[test]
90 fn g3_resolution_carries_freshness() {
91 let m = minted();
93 let now = Timestamp::from_unix_ms(2_000);
94 let r = resolve(&m, &ResolverContext::anonymous_agent(), now);
95 assert_eq!(r.as_of, now);
96 assert_eq!(r.revalidate_after, now.plus_ms(DEFAULT_REVALIDATE_MS));
97 assert!(
98 r.variant.is_some(),
99 "minted manifests always serve (catch-all)"
100 );
101 }
102
103 #[test]
104 fn variant_declared_window_overrides_default() {
105 let mut m = minted();
106 m.variants[0].revalidate_after_ms = Some(1_000);
107 let now = Timestamp::from_unix_ms(0);
108 let r = resolve(&m, &ResolverContext::human(), now);
109 assert_eq!(r.revalidate_after, Timestamp::from_unix_ms(1_000));
110 }
111
112 #[test]
113 fn revoked_serves_nothing_superseded_serves_with_pointer() {
114 let mut m = minted();
115 let now = Timestamp::from_unix_ms(5_000);
116
117 let other = crate::Token::parse("next1").unwrap();
118 m.superseded_by = Some(other);
119 let r = resolve(&m, &ResolverContext::human(), now);
120 assert_eq!(r.disposition, Disposition::Superseded { by: other });
121 assert!(
122 r.variant.is_some(),
123 "superseded still serves; the pointer travels with it"
124 );
125
126 m.revoked_at = Some(now);
127 let r = resolve(&m, &ResolverContext::human(), now);
128 assert_eq!(r.disposition, Disposition::Revoked { at: now });
129 assert!(r.variant.is_none(), "revoked serves nothing");
130 }
131
132 #[test]
133 fn resolve_is_pure_same_inputs_same_output() {
134 let m = minted();
135 let ctx = ResolverContext::anonymous_agent();
136 let now = Timestamp::from_unix_ms(9);
137 assert_eq!(resolve(&m, &ctx, now), resolve(&m, &ctx, now));
138 }
139}