返回 CodeWhale
tests.rs
根目录 / crates / tui / src / computer_meter / tests.rs
1 use super::*;
2
3 const FIXTURES: &str = include_str!("fixtures/cases.json");
4
5 #[derive(Debug, Deserialize)]
6 #[serde(rename_all = "camelCase")]
7 struct FixtureFile {
8 cases: Vec<FixtureCase>,
9 }
10
11 #[derive(Debug, Deserialize)]
12 #[serde(rename_all = "camelCase")]
13 struct FixtureCase {
14 id: String,
15 expect: String,
16 #[serde(default)]
17 code: Option<String>,
18 #[serde(default)]
19 accepted_seconds: Option<u64>,
20 #[serde(default)]
21 standard_equivalent_seconds: Option<u64>,
22 #[serde(default)]
23 admission: Option<ComputerAdmissionRequest>,
24 #[serde(default)]
25 observation: Option<ProviderObservation>,
26 #[serde(default)]
27 restated_observation: Option<ProviderObservation>,
28 #[serde(default)]
29 intervals: Vec<FixtureInterval>,
30 #[serde(default)]
31 profile_input: Option<String>,
32 #[serde(default)]
33 profile_id: Option<String>,
34 #[serde(default)]
35 multiplier: Option<u32>,
36 }
37
38 #[derive(Debug, Deserialize)]
39 #[serde(rename_all = "camelCase")]
40 struct FixtureInterval {
41 admission: ComputerAdmissionRequest,
42 observation: ProviderObservation,
43 }
44
45 fn fixtures() -> Vec<FixtureCase> {
46 serde_json::from_str::<FixtureFile>(FIXTURES)
47 .expect("computer meter fixtures must parse")
48 .cases
49 }
50
51 fn issue_case(case: &FixtureCase) -> Result<ComputerMeterReceipt, ComputerMeterError> {
52 let admission = bind_computer_admission(
53 case.admission
54 .clone()
55 .expect("receipt fixture requires admission"),
56 )?;
57 issue_computer_meter_receipt(
58 &admission,
59 case.observation
60 .clone()
61 .expect("receipt fixture requires observation"),
62 )
63 }
64
65 #[test]
66 fn fixtures_cover_provider_accepted_active_seconds_only() {
67 let cases = fixtures();
68 assert!(
69 cases.len() >= 12,
70 "fixture pack must cover accept, idle, queued, stopped, teardown, mismatch, concurrent, historic, correction"
71 );
72
73 for case in &cases {
74 match case.expect.as_str() {
75 "receipt" => {
76 let receipt = issue_case(case).unwrap_or_else(|error| {
77 panic!("{} should mint a receipt, got {}", case.id, error.code())
78 });
79 assert_eq!(receipt.schema_id, COMPUTER_METER_RECEIPT_SCHEMA);
80 assert_eq!(receipt.kind, COMPUTER_METER_RECEIPT_KIND);
81 assert_eq!(receipt.accepted_seconds, case.accepted_seconds.unwrap());
82 assert_eq!(
83 receipt.standard_equivalent_seconds,
84 case.standard_equivalent_seconds.unwrap()
85 );
86 assert_eq!(
87 receipt.standard_equivalent_seconds,
88 receipt.accepted_seconds * u64::from(receipt.multiplier)
89 );
90 let admission = bind_computer_admission(case.admission.clone().unwrap()).unwrap();
91 assert_eq!(receipt.admission_id, admission.admission_id);
92 assert_eq!(receipt.account_id, admission.account_id);
93 assert_eq!(receipt.profile_id, admission.profile_id);
94 assert_eq!(receipt.multiplier, admission.multiplier);
95 assert_eq!(receipt.provider_allocation.cpu, admission.cpu);
96 assert_eq!(receipt.provider_allocation.memory_gib, admission.memory_gib);
97 assert_eq!(receipt.provider_allocation.disk_gib, admission.disk_gib);
98 assert!(receipt.provider_allocation.accepted);
99 assert_eq!(receipt.meter_revision, COMPUTER_METER_REVISION);
100 assert_eq!(receipt.catalog_revision, COMPUTER_CATALOG_REVISION);
101 assert_ne!(receipt.binding_digest, admission.binding_digest);
102 }
103 "error" => {
104 let error = issue_case(case).expect_err(&case.id);
105 assert_eq!(error.code(), case.code.as_deref().unwrap(), "{}", case.id);
106 }
107 "sum" => {
108 let receipts: Vec<_> = case
109 .intervals
110 .iter()
111 .map(|interval| {
112 let admission = bind_computer_admission(interval.admission.clone())
113 .unwrap_or_else(|error| panic!("{}: {}", case.id, error));
114 issue_computer_meter_receipt(&admission, interval.observation.clone())
115 .unwrap_or_else(|error| panic!("{}: {}", case.id, error))
116 })
117 .collect();
118 assert_eq!(
119 sum_standard_equivalent_seconds(&receipts),
120 case.standard_equivalent_seconds.unwrap()
121 );
122 }
123 "decode" => {
124 let profile = decode_computer_profile(case.profile_input.as_deref().unwrap())
125 .unwrap_or_else(|error| panic!("{}: {}", case.id, error));
126 assert_eq!(profile.id.as_str(), case.profile_id.as_deref().unwrap());
127 assert_eq!(profile.multiplier, case.multiplier.unwrap());
128 }
129 "admit_error" => {
130 let error = admit_computer_profile(case.profile_input.as_deref().unwrap())
131 .expect_err(&case.id);
132 assert_eq!(error.code(), case.code.as_deref().unwrap());
133 }
134 "correction" => {
135 let admission = bind_computer_admission(case.admission.clone().unwrap()).unwrap();
136 let original =
137 issue_computer_meter_receipt(&admission, case.observation.clone().unwrap())
138 .unwrap();
139 let original_clone = original.clone();
140 let correction = correct_computer_meter_receipt(
141 &original,
142 &admission,
143 case.restated_observation.clone().unwrap(),
144 )
145 .unwrap();
146 assert_eq!(
147 original, original_clone,
148 "corrections must not mutate the original receipt"
149 );
150 assert_eq!(
151 correction.correction_of.as_deref(),
152 Some(original.receipt_id.as_str())
153 );
154 assert!(correction.lineage.contains(&original.receipt_id));
155 assert_ne!(correction.receipt_id, original.receipt_id);
156 assert_eq!(correction.accepted_seconds, case.accepted_seconds.unwrap());
157 assert_eq!(
158 correction.standard_equivalent_seconds,
159 case.standard_equivalent_seconds.unwrap()
160 );
161 assert_eq!(correction.account_id, original.account_id);
162 assert_eq!(correction.admission_id, original.admission_id);
163 assert_eq!(correction.profile_id, original.profile_id);
164 }
165 other => panic!("unknown fixture expect `{other}`"),
166 }
167 }
168 }
169
170 #[test]
171 fn exact_replay_is_idempotent_and_a_mutated_binding_conflicts() {
172 let case = fixtures()
173 .into_iter()
174 .find(|case| case.id == "accepted-standard-8")
175 .unwrap();
176 let first = issue_case(&case).unwrap();
177 let replay = issue_case(&case).unwrap();
178 assert_eq!(first.receipt_id, replay.receipt_id);
179 assert_eq!(first.binding_digest, replay.binding_digest);
180 assert_computer_meter_receipt_replay(&first, &replay).unwrap();
181
182 let mut mutated = replay.clone();
183 mutated.accepted_seconds = first.accepted_seconds + 1;
184 mutated.standard_equivalent_seconds = mutated.accepted_seconds;
185 assert_eq!(
186 assert_computer_meter_receipt_replay(&first, &mutated)
187 .unwrap_err()
188 .code(),
189 "computer_meter_receipt_replay_conflict"
190 );
191 }
192
193 #[test]
194 fn replay_rejects_fields_the_supplied_digest_does_not_vouch_for() {
195 let case = fixtures()
196 .into_iter()
197 .find(|case| case.id == "accepted-standard-8")
198 .unwrap();
199 let first = issue_case(&case).unwrap();
200
201 // Same id and digest, different interval: the digest no longer matches
202 // the fields it claims to bind.
203 let mut shifted = first.clone();
204 shifted.started_at = "2026-08-31T13:00:00.000Z".to_string();
205 shifted.ended_at = "2026-08-31T13:10:00.000Z".to_string();
206 assert_eq!(
207 assert_computer_meter_receipt_replay(&first, &shifted)
208 .unwrap_err()
209 .code(),
210 "computer_meter_receipt_replay_conflict"
211 );
212
213 // A field outside the digest must still match exactly.
214 let mut restated = first.clone();
215 restated.provider_allocation.accepted = false;
216 assert_eq!(
217 assert_computer_meter_receipt_replay(&first, &restated)
218 .unwrap_err()
219 .code(),
220 "computer_meter_receipt_replay_conflict"
221 );
222 }
223
224 #[test]
225 fn an_interval_that_runs_past_admission_expiry_is_refused() {
226 let case = fixtures()
227 .into_iter()
228 .find(|case| case.id == "accepted-standard-8")
229 .unwrap();
230 let mut admission_request = case.admission.clone().unwrap();
231 let observation = case.observation.clone().unwrap();
232 // Starts before expiry, ends after it.
233 admission_request.expires_at = "2026-08-31T12:05:00.000Z".to_string();
234 let admission = bind_computer_admission(admission_request.clone()).unwrap();
235 assert_eq!(
236 issue_computer_meter_receipt(&admission, observation.clone())
237 .unwrap_err()
238 .code(),
239 "computer_admission_expired"
240 );
241
242 // Ending exactly at expiry is inside the admission (end is exclusive).
243 admission_request.expires_at = observation.ended_at.clone();
244 let admission = bind_computer_admission(admission_request).unwrap();
245 assert!(issue_computer_meter_receipt(&admission, observation).is_ok());
246 }
247
248 #[test]
249 fn admission_binds_profile_multiplier_account_and_expiry_before_dispatch() {
250 let admission = bind_computer_admission(ComputerAdmissionRequest {
251 admission_id: "adm_bind".to_string(),
252 account_id: "acct_demo".to_string(),
253 computer_id: "cmp_bind".to_string(),
254 run_id: "run_bind".to_string(),
255 provider: "daytona".to_string(),
256 profile_id: "standard-16".to_string(),
257 funding_authority: "coding_membership_included".to_string(),
258 quote_id: "quote_bind".to_string(),
259 expires_at: "2026-08-31T18:00:00.000Z".to_string(),
260 meter_revision: String::new(),
261 catalog_revision: String::new(),
262 })
263 .unwrap();
264 assert_eq!(admission.schema_id, COMPUTER_ADMISSION_SCHEMA);
265 assert_eq!(admission.profile_id, ComputerProfileId::Standard16);
266 assert_eq!(admission.cpu, 4);
267 assert_eq!(admission.memory_gib, 16);
268 assert_eq!(admission.disk_gib, 16);
269 assert_eq!(admission.multiplier, 2);
270 assert_eq!(admission.account_id, "acct_demo");
271 assert_eq!(admission.meter_revision, COMPUTER_METER_REVISION);
272 assert!(!admission.binding_digest.is_empty());
273 }
274
275 #[test]
276 fn one_standard_16_matches_two_standard_8_over_the_same_ten_minutes() {
277 let cases = fixtures();
278 let two_eights = cases
279 .iter()
280 .find(|case| case.id == "concurrent-two-standard-8")
281 .unwrap();
282 let sixteen = cases
283 .iter()
284 .find(|case| case.id == "accepted-standard-16")
285 .unwrap();
286 let eight_receipts: Vec<_> = two_eights
287 .intervals
288 .iter()
289 .map(|interval| {
290 let admission = bind_computer_admission(interval.admission.clone()).unwrap();
291 issue_computer_meter_receipt(&admission, interval.observation.clone()).unwrap()
292 })
293 .collect();
294 let sixteen_receipt = issue_case(sixteen).unwrap();
295 assert_eq!(
296 sum_standard_equivalent_seconds(&eight_receipts),
297 sixteen_receipt.standard_equivalent_seconds
298 );
299 assert_eq!(sum_standard_equivalent_seconds(&eight_receipts), 1200);
300 }
301
301 lines RUST