This repository has no description
1use std::collections::BTreeSet;
2
3use futures::future::join_all;
4use futures::stream::StreamExt;
5use knot_sim::{Outcome, Step, Trace};
6
7fn status_of(step: &Step) -> Option<u16> {
8 match step.outcome {
9 Outcome::Answered { status, .. } => Some(status.get()),
10 Outcome::Killed => None,
11 }
12}
13
14fn union_has(traces: &[Trace], predicate: impl Fn(&Step) -> bool) -> bool {
15 traces
16 .iter()
17 .any(|trace| trace.steps.iter().any(&predicate))
18}
19
20const READ_OPS: [&str; 11] = [
21 "version",
22 "owner",
23 "listMembers",
24 "didJson",
25 "infoRefs",
26 "branches",
27 "log",
28 "describeRepo",
29 "tree",
30 "blob",
31 "languages",
32];
33
34fn every_step_with<'a>(
35 traces: &'a [Trace],
36 fault: &'a str,
37 holds: impl Fn(&Step) -> bool + 'a,
38) -> (bool, usize) {
39 let matching: Vec<&Step> = traces
40 .iter()
41 .flat_map(|trace| trace.steps.iter())
42 .filter(|step| step.fault == fault)
43 .collect();
44 (matching.iter().all(|step| holds(step)), matching.len())
45}
46
47#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
48async fn the_same_seed_produces_an_identical_whole_system_trace() {
49 futures::stream::iter([1u64, 7, 42, 100, 2026])
50 .for_each(|seed| async move {
51 let first = knot_sim::run(seed, 16).await;
52 let second = knot_sim::run(seed, 16).await;
53 assert_eq!(
54 first, second,
55 "seed {seed} must replay to an identical trace"
56 );
57 assert_eq!(
58 first.digest(),
59 second.digest(),
60 "seed {seed} digest is stable"
61 );
62 })
63 .await;
64}
65
66#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
67async fn concurrent_execution_stays_stable_across_fifty_repeats() {
68 let baseline = knot_sim::run(7, 18).await.digest();
69 let digests: Vec<u64> = futures::stream::iter(0..50)
70 .then(|_| knot_sim::run(7, 18))
71 .map(|trace| trace.digest())
72 .collect()
73 .await;
74 assert!(
75 digests.iter().all(|digest| *digest == baseline),
76 "4-thread schedule leaked into trace: {} of 50 replays diverged",
77 digests.iter().filter(|digest| **digest != baseline).count()
78 );
79}
80
81#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
82async fn distinct_seeds_produce_distinct_traces() {
83 let digests: Vec<u64> = join_all((0u64..8).map(|seed| knot_sim::run(seed, 16)))
84 .await
85 .iter()
86 .map(Trace::digest)
87 .collect();
88 let unique: BTreeSet<u64> = digests.iter().copied().collect();
89 assert_eq!(
90 unique.len(),
91 digests.len(),
92 "every seed must drive different whole-system trace: {digests:?}"
93 );
94}
95
96#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
97async fn the_round_count_extends_the_same_prefix() {
98 let short = knot_sim::run(7, 10).await;
99 let long = knot_sim::run(7, 20).await;
100 assert_ne!(short.digest(), long.digest());
101 assert_eq!(
102 short.steps,
103 long.steps[..short.steps.len()],
104 "longer run extends the same prefix the shorter run produced"
105 );
106}
107
108#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
109async fn the_injected_failures_are_correlated_with_their_observable_outcomes() {
110 let traces = join_all([1u64, 7, 13, 42, 99, 2026].map(|seed| knot_sim::run(seed, 16))).await;
111
112 let (skew_all_401, skew_count) =
113 every_step_with(&traces, "clock_skew", |step| status_of(step) == Some(401));
114 assert!(skew_count > 0, "clock-skew fault must actually fire");
115 assert!(
116 skew_all_401,
117 "every clock-skewed token must produce 401 expiry rejection, not just one example"
118 );
119
120 let (drop_all_503, drop_count) = every_step_with(&traces, "drop_identity", |step| {
121 status_of(step) == Some(503)
122 });
123 assert!(drop_count > 0, "drop-identity fault must actually fire");
124 assert!(
125 drop_all_503,
126 "every dropped identity resolution must surface as 503 upstream-unavailable failure"
127 );
128
129 let (killed_all_killed, killed_count) = every_step_with(&traces, "killed", |step| {
130 matches!(step.outcome, Outcome::Killed) && READ_OPS.contains(&step.op)
131 });
132 assert!(killed_count > 0, "kill fault must actually fire");
133 assert!(
134 killed_all_killed,
135 "killed connection must record a Killed outcome and is only injected on read paths"
136 );
137
138 assert!(
139 union_has(&traces, |step| step.op == "probe"
140 && step.fault == "none"
141 && status_of(step) == Some(403)),
142 "resolved stranger with no fault must be denied 403 by access-control layer"
143 );
144
145 [
146 "addMember",
147 "addCollaborator",
148 "createRepo",
149 "maintain",
150 "describeRepo",
151 "listMembers",
152 "infoRefs",
153 "didJson",
154 "branches",
155 "log",
156 "tree",
157 "blob",
158 "languages",
159 ]
160 .iter()
161 .for_each(|op| {
162 assert!(
163 union_has(&traces, |step| step.op == *op
164 && status_of(step) == Some(200)),
165 "{op} path must succeed against the doubles in at least one seed"
166 );
167 });
168}
169
170#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
171async fn the_final_projection_matches_an_independent_model() {
172 let seeds = [1u64, 7, 13, 42, 99, 2026];
173 let predicted: Vec<knot_sim::Projection> = seeds
174 .iter()
175 .map(|seed| knot_sim::predict(*seed, 18))
176 .collect();
177 let runs = join_all(seeds.map(|seed| knot_sim::run(seed, 18))).await;
178
179 seeds
180 .iter()
181 .zip(predicted.iter())
182 .zip(runs.iter())
183 .for_each(|((seed, model), trace)| {
184 let last = trace.snapshots.last().expect("at least one round");
185 assert_eq!(
186 last.members, model.members,
187 "seed {seed}: executed member projection must equal the independent model"
188 );
189 assert_eq!(
190 last.blocked, model.blocked,
191 "seed {seed}: executed blocklist projection must equal the independent model"
192 );
193 });
194
195 assert!(
196 predicted.iter().any(|model| !model.members.is_empty()),
197 "oracle is vacuous unless at least one seed predicts a non-empty member set"
198 );
199 assert!(
200 predicted.iter().any(|model| !model.blocked.is_empty()),
201 "oracle is vacuous unless at least one seed predicts a non-empty blocklist"
202 );
203}
204
205#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
206async fn maintenance_never_reports_a_failure() {
207 let traces = join_all([1u64, 7, 13, 42, 99, 2026].map(|seed| knot_sim::run(seed, 16))).await;
208 let failures = traces
209 .iter()
210 .flat_map(|trace| trace.steps.iter())
211 .filter(|step| step.op == "maintain" && status_of(step) == Some(500))
212 .count();
213 assert_eq!(
214 failures, 0,
215 "maintenance must succeed on every repo simulation runs it against"
216 );
217 assert!(
218 union_has(&traces, |step| step.op == "maintain"
219 && status_of(step) == Some(200)),
220 "maintenance path must actually run against a populated repo in at least one seed"
221 );
222}
223
224#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
225async fn the_final_projection_state_is_seed_stable() {
226 let first = knot_sim::run(2026, 16).await;
227 let second = knot_sim::run(2026, 16).await;
228 assert_eq!(
229 first.snapshots, second.snapshots,
230 "order-independent COB projections must converge to the same logical state"
231 );
232 let last = first.snapshots.last().expect("at least one round");
233 assert!(
234 last.repos.len() > 1,
235 "simulation must have minted repos beyond the seed repo: {:?}",
236 last.repos
237 );
238}