g1t/services/repos/src/git_ops.rs

436 lines19,868 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1//! Git operations, counted per workspace.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2//!
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3//! Cloudflare Artifacts charges g1t per operation from 2026-10-14 ($0.15
4//! per 1,000) without having said exactly which calls are operations. So
5//! every interaction with the store is metered by kind (meters.rs), and
6//! which meters a workspace is counted for, and how much each is worth, is
7//! data (`operation_mapping`). By default: each clone or fetch (an
8//! upload-pack request that fetches objects, not `ls-refs` and never an
9//! answer g1t served from its own cache), each push (receive-pack), and
10//! making, forking and deleting a repository. The counts go to
11//! `git_operations` by the hour, after answers have gone back. Billing
12//! reads the month's count each day and charges workspaces on the plan for
13//! what is past the amount that is free for everyone (50,000 a month), at
14//! cost plus 20%. A workspace on the plan is never slowed or refused for
15//! git operations or for storage: it pays for them as usage, up to its
16//! spend limit.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17//!
18//! A free workspace is never charged for git operations. Past
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas19//! `GIT_OPERATIONS_FREE_CAP` in a month (50,000, billing's
20//! `GIT_OPERATIONS_INCLUDED`), it is slowed down instead: at most
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21//! `GIT_OPERATIONS_FREE_HOURLY` (60) an hour, answered 429 with when to try
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily22//! again. Whether it is past its cap is decided from counts this isolate
23//! read a moment ago and has added to since, never by asking the database
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'24//! on the way (63 to 98 ms a request, measured).
25//!
26//! Agents' sandboxes, checks, builds and workflow jobs clone, fetch and
27//! push through g1t's git endpoints (`https://g1t.sh/<path>.git`, with a
28//! run credential), never the store directly, so they are counted here
29//! like anyone's; so is git an agent runs itself in its sandbox. A pull
30//! request's working copy counts for the workspace of the repository it
31//! came from (meters.rs). The one sandbox that reads the store directly,
32//! a nightly backup, reports its clone, which is g1t's cost and never a
33//! workspace's (backups.rs). The limits here go by the path asked for, so
34//! a free workspace's requests to a working copy (`pulls/<pull id>`) are
35//! counted for it but never slowed down.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily36
37use std::cell::RefCell;
38use std::collections::HashMap;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily40use g1t_contracts::repos::{GitService, WorkspaceGitOperations};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look41use serde::Deserialize;
42use worker::{D1Database, Env, Fetcher, Response, Result};
43
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily44/// What a request to g1t's git endpoints asks the store.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum GitCall {
47 /// `GET info/refs`: the refs, for a fetch or a push.
48 RefAdvertisement,
49 /// A protocol v2 `ls-refs`.
50 LsRefs,
51 /// An upload-pack request that fetches objects: a clone or fetch.
52 Fetch,
53 /// A push.
54 ReceivePack,
55}
56
57impl GitCall {
58 /// Its meter (meters.rs).
59 pub fn meter(self) -> &'static str {
60 match self {
61 GitCall::RefAdvertisement => "git.info_refs",
62 GitCall::LsRefs => "git.ls_refs",
63 GitCall::Fetch => "git.fetch",
64 GitCall::ReceivePack => "git.receive_pack",
65 }
66 }
67
68 /// The meter for an answer g1t served from its own cache, which never
69 /// reaches the store and is never an operation.
70 pub fn cached_meter(self) -> &'static str {
71 match self {
72 GitCall::RefAdvertisement => "cache.info_refs",
73 GitCall::LsRefs => "cache.ls_refs",
74 GitCall::Fetch => "cache.fetch",
75 GitCall::ReceivePack => "cache.receive_pack",
76 }
77 }
78}
79
80/// What a git request is. `body` is an upload-pack POST's, read already.
81pub fn classify(service: GitService, endpoint: &str, get: bool, body: Option<&[u8]>) -> GitCall {
82 if get || endpoint == "info/refs" {
83 return GitCall::RefAdvertisement;
84 }
85 if service == GitService::ReceivePack {
86 return GitCall::ReceivePack;
87 }
88 let ls_refs = body.is_some_and(|body| {
89 let (lines, _) = crate::land::read_pkt_lines(body);
90 lines.first().is_some_and(|line| line.strip_suffix(b"\n").unwrap_or(line) == b"command=ls-refs")
91 });
92 if ls_refs { GitCall::LsRefs } else { GitCall::Fetch }
93}
94
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look95/// The hour an operation is counted in: `YYYY-MM-DDTHH` of an RFC 3339 time.
96pub fn hour_key(timestamp: &str) -> String {
97 timestamp[..13].to_owned()
98}
99
100/// Whether a free workspace's operation should wait: past the month's cap,
101/// and past the hour's share.
102pub fn slow_down(month_ops: u64, hour_ops: u64, free_cap: u64, hourly: u64) -> bool {
103 month_ops > free_cap && hour_ops > hourly
104}
105
106/// The limits, from the repos service's variables.
107pub struct Limits {
108 pub free_cap: u64,
109 pub hourly: u64,
110}
111
112impl Limits {
113 pub fn from_env(env: &Env) -> Self {
114 let number = |name: &str, default: u64| env.var(name).ok().and_then(|v| v.to_string().parse().ok()).unwrap_or(default);
115 Limits { free_cap: number("GIT_OPERATIONS_FREE_CAP", 50_000), hourly: number("GIT_OPERATIONS_FREE_HOURLY", 60) }
116 }
117}
118
119#[derive(Deserialize)]
120struct Counts {
121 month: Option<f64>,
122 hour: Option<f64>,
123}
124
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily125/// Where a workspace stands this month and hour, as this isolate knows it.
126#[derive(Clone, Debug, Default, PartialEq)]
127pub struct Standing {
128 /// `YYYY-MM-DDTHH` the counts are for.
129 hour_key: String,
130 month: f64,
131 hour: f64,
132 /// When the database was last read for it.
133 read_at: u64,
134}
135
136impl Standing {
137 /// The month's and the hour's counts at `hour_key`, if read recently
138 /// enough to go by: an hour that has turned starts at nothing, a month
139 /// that has turned likewise.
140 pub fn at(&self, hour_key: &str, now: u64) -> Option<(u64, u64)> {
141 if now.saturating_sub(self.read_at) > STANDING_TTL_MS {
142 return None;
143 }
144 let month = if self.hour_key.get(..7) == hour_key.get(..7) { self.month } else { 0.0 };
145 let hour = if self.hour_key == hour_key { self.hour } else { 0.0 };
146 Some((month as u64, hour as u64))
147 }
148
149 /// Adds operations counted here, not yet written.
150 pub fn add(&mut self, hour_key: &str, operations: f64) {
151 if self.hour_key != hour_key {
152 if self.hour_key.get(..7) != hour_key.get(..7) {
153 self.month = 0.0;
154 }
155 self.hour = 0.0;
156 self.hour_key = hour_key.to_owned();
157 }
158 self.month += operations;
159 self.hour += operations;
160 }
161}
162
163/// How long counts read from the database are gone by. Every write of the
164/// meters reads them again (meters.rs), so a busy workspace's are seconds old.
165const STANDING_TTL_MS: u64 = 10 * 60 * 1000;
Merge branch 'worktree-agent-a985c01f43538af01'166/// How long billing's answer about a workspace's plan is kept: a paid
167/// plan for minutes, a free one (which slows the workspace down) for
168/// seconds, so a workspace that just added a plan is not held back for
169/// minutes by an answer from before it did.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily170const PLAN_TTL_MS: u64 = 5 * 60 * 1000;
Merge branch 'worktree-agent-a985c01f43538af01'171const FREE_PLAN_TTL_MS: u64 = 30 * 1000;
172
173/// Whether a plan answer read at `at` (`free` or not) still goes at `now`.
174fn plan_kept(free: bool, at: u64, now: u64) -> bool {
175 now.saturating_sub(at) < if free { FREE_PLAN_TTL_MS } else { PLAN_TTL_MS }
176}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily177
178thread_local! {
179 static STANDING: RefCell<HashMap<String, Standing>> = RefCell::new(HashMap::new());
180 static FREE: RefCell<HashMap<String, (bool, u64)>> = RefCell::new(HashMap::new());
181}
182
183/// Adds operations this isolate counted for `namespace` (meters.rs).
184pub fn note_local(namespace: &str, hour_key: &str, operations: f64) {
185 STANDING.with(|standing| {
186 if let Some(kept) = standing.borrow_mut().get_mut(namespace) {
187 kept.add(hour_key, operations);
188 }
189 });
190}
191
192/// The month's and the hour's counts for `namespace`, as last read and
193/// added to here; `None` when not read lately, which never slows anyone.
194pub fn standing(namespace: &str, hour_key: &str, now: u64) -> Option<(u64, u64)> {
195 STANDING.with(|standing| standing.borrow().get(namespace).and_then(|kept| kept.at(hour_key, now)))
196}
197
198/// Reads `namespace`'s counts again, after the meters were written.
199pub async fn refresh(db: &D1Database, namespace: &str) -> Result<()> {
200 let now = g1t_kit::now_ms();
201 let hour = hour_key(&g1t_contracts::time::rfc3339(now));
202 let counts = db
203 .prepare(
204 "SELECT SUM(operations) AS month, SUM(CASE WHEN hour = ?2 THEN operations END) AS hour
205 FROM git_operations WHERE namespace = ?1 AND substr(hour, 1, 7) = ?3",
206 )
207 .bind(&[namespace.into(), hour.as_str().into(), hour[..7].into()])?
208 .first::<Counts>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily210 let (month, hour_count) = counts.map_or((0.0, 0.0), |c| (c.month.unwrap_or(0.0), c.hour.unwrap_or(0.0)));
211 STANDING.with(|standing| {
212 standing.borrow_mut().insert(namespace.to_owned(), Standing { hour_key: hour, month, hour: hour_count, read_at: now });
213 });
214 Ok(())
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215}
216
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index217/// The hours of `month` (`YYYY-MM`) from `since` on, as the range
218/// `[from, until)` of hour keys; `None` for a month that is not one.
219/// A range on `hour` is what the table's key can find; `substr` is not.
220pub fn month_hours(month: &str, since: Option<&str>) -> Option<(String, String)> {
221 let year: u32 = month.get(..4)?.parse().ok()?;
222 let number: u32 = month.get(5..7)?.parse().ok()?;
223 if month.len() != 7 || &month[4..5] != "-" || !(1..=12).contains(&number) {
224 return None;
225 }
226 let until = if number == 12 { format!("{}-01-01", year + 1) } else { format!("{year}-{:02}-01", number + 1) };
227 let start = format!("{month}-01");
228 let from = match since {
229 Some(since) if since > start.as_str() => since.to_owned(),
230 _ => start,
231 };
232 Some((from, until))
233}
234
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look235/// Each workspace's operations in `month`, from `since` (an hour) on.
236pub async fn totals(db: &D1Database, month: &str, since: Option<&str>, namespace: Option<&str>) -> Result<Vec<WorkspaceGitOperations>> {
237 #[derive(Deserialize)]
238 struct Row {
239 namespace: String,
240 operations: Option<f64>,
241 }
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index242 let Some((from, until)) = month_hours(month, since) else { return Ok(vec![]) };
243 // One workspace's is read by the table's key, namespace first; every
244 // workspace's (billing's daily measure) reads the month's hours.
245 let statement = match namespace {
246 Some(namespace) => db
247 .prepare(
248 "SELECT namespace, SUM(operations) AS operations FROM git_operations
249 WHERE namespace = ?1 AND hour >= ?2 AND hour < ?3
250 GROUP BY namespace",
251 )
252 .bind(&[namespace.into(), from.as_str().into(), until.as_str().into()])?,
253 None => db
254 .prepare(
255 "SELECT namespace, SUM(operations) AS operations FROM git_operations
256 WHERE hour >= ?1 AND hour < ?2
257 GROUP BY namespace",
258 )
259 .bind(&[from.as_str().into(), until.as_str().into()])?,
260 };
261 Ok(statement
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look262 .all()
263 .await?
264 .results::<Row>()?
265 .into_iter()
266 .map(|row| WorkspaceGitOperations { namespace: row.namespace, operations: row.operations.unwrap_or(0.0) as u64 })
267 .collect())
268}
269
270/// Whether billing says the workspace is free. Unknown (billing not bound
271/// or not answering) counts as not free: nothing is slowed down on a guess.
272pub async fn is_free(billing: Option<&Fetcher>, namespace: &str) -> bool {
273 let Some(billing) = billing else { return false };
274 let args = g1t_contracts::billing::EntitlementsArgs { workspace: namespace.to_owned() };
275 match g1t_kit::call::<_, serde_json::Value>(billing, "entitlements", &args).await {
276 Ok(found) => found["plan"].as_str() == Some("free"),
277 Err(error) => {
278 worker::console_error!("could not ask billing about {namespace}: {error}");
279 false
280 }
281 }
282}
283
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily284/// [`is_free`], kept for a few minutes: asked only of a workspace past its
285/// cap, on each of its requests.
286pub async fn is_free_kept(billing: Option<&Fetcher>, namespace: &str) -> bool {
287 let now = g1t_kit::now_ms();
288 let kept = FREE.with(|free| {
Merge branch 'worktree-agent-a985c01f43538af01'289 free.borrow().get(namespace).filter(|(free, at)| plan_kept(*free, *at, now)).map(|(free, _)| *free)
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily290 });
291 if let Some(free) = kept {
292 return free;
293 }
294 let free = is_free(billing, namespace).await;
295 FREE.with(|kept| kept.borrow_mut().insert(namespace.to_owned(), (free, now)));
296 free
297}
298
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look299/// The private storage a free workspace may push to: 1 GB unless set.
300pub fn free_private_bytes(env: &worker::Env) -> i64 {
301 env.var("FREE_PRIVATE_STORAGE_BYTES")
302 .ok()
303 .and_then(|value| value.to_string().parse().ok())
304 .unwrap_or(1_000_000_000)
305}
306
307/// Whether a push to a private repository should be refused: a free
308/// workspace whose private repositories already hold its free amount. Free
309/// workspaces are never charged for storage; past it, pushes stop instead.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas310/// Only ever asked for a free workspace: one on the plan pays for storage
311/// past the free amount and is never refused.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look312pub fn storage_full(private_bytes: i64, free_bytes: i64) -> bool {
313 private_bytes >= free_bytes
314}
315
316/// The answer to a push a free workspace has no room for. Plain text on
317/// the push's first request, which git shows as the reason.
318pub fn storage_full_response(namespace: &str, private_bytes: i64, free_bytes: i64) -> Result<Response> {
319 let gb = |bytes: i64| bytes as f64 / 1_000_000_000.0;
320 let message = format!(
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas321 "{namespace}'s private repositories hold {:.2} GB, and a free workspace has {:.0} GB. Free workspaces are never charged for storage, so pushes to private repositories stop here. Make the repository public, delete what you no longer need, or start the g1t plan, where storage past it is usage at cost plus 20% and pushes never stop: https://g1t.sh/{namespace}/-/billing
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322",
323 gb(private_bytes),
324 gb(free_bytes)
325 );
326 Response::error(message, 403)
327}
328
329/// The answer to a free workspace past its share: try again next hour.
330pub fn too_many(namespace: &str, free_cap: u64, hourly: u64) -> Result<Response> {
331 let message = format!(
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas332 "{namespace} has made more than {free_cap} git operations this month, so g1t allows {hourly} an hour until the month turns. Free workspaces are never charged for git operations. On the g1t plan they are never slowed: past {free_cap} a month they are usage at cost plus 20%: https://g1t.sh/{namespace}/-/billing\n"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 );
334 let response = Response::error(message, 429)?;
335 response.headers().set("retry-after", "3600")?;
336 Ok(response)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
Merge branch 'worktree-agent-a985c01f43538af01'344 fn a_free_plan_is_asked_again_within_a_minute_a_paid_one_kept_longer() {
345 assert!(plan_kept(true, 1_000, 1_000 + FREE_PLAN_TTL_MS - 1));
346 assert!(!plan_kept(true, 1_000, 1_000 + FREE_PLAN_TTL_MS));
347 assert!(plan_kept(false, 1_000, 1_000 + FREE_PLAN_TTL_MS));
348 assert!(!plan_kept(false, 1_000, 1_000 + PLAN_TTL_MS));
349 }
350
351 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look352 fn operations_are_counted_by_the_hour() {
353 assert_eq!(hour_key("2026-10-14T09:59:59.000Z"), "2026-10-14T09");
354 }
355
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index356 #[test]
357 fn a_months_hours_are_a_range_on_the_key() {
358 let range = |month: &str, since: Option<&str>| month_hours(month, since);
359 assert_eq!(range("2026-10", None), Some(("2026-10-01".to_owned(), "2026-11-01".to_owned())));
360 assert_eq!(range("2026-12", None), Some(("2026-12-01".to_owned(), "2027-01-01".to_owned())));
361 // `since` narrows the start, never widens it past the month.
362 assert_eq!(range("2026-10", Some("2026-10-14T00")).map(|r| r.0), Some("2026-10-14T00".to_owned()));
363 assert_eq!(range("2026-10", Some("2026-09-30T23")).map(|r| r.0), Some("2026-10-01".to_owned()));
364 assert_eq!(range("2026-10", Some("")).map(|r| r.0), Some("2026-10-01".to_owned()));
365 // Every hour of the month is in it, and none of the next or last,
366 // as `substr(hour, 1, 7) = month` had it.
367 let (from, until) = range("2026-10", None).unwrap();
368 let inside = |hour: &str| hour >= from.as_str() && hour < until.as_str();
369 assert!(inside("2026-10-01T00") && inside("2026-10-31T23"));
370 assert!(!inside("2026-09-30T23") && !inside("2026-11-01T00"));
371 assert_eq!(range("2026-13", None), None);
372 assert_eq!(range("2026-1", None), None);
373 assert_eq!(range("", None), None);
374 }
375
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily376 fn pkt(payload: &str) -> Vec<u8> {
377 format!("{:04x}{payload}", payload.len() + 4).into_bytes()
378 }
379
380 #[test]
381 fn each_git_request_is_metered_by_what_it_asks() {
382 use GitService::{ReceivePack, UploadPack};
383 assert_eq!(classify(UploadPack, "info/refs", true, None), GitCall::RefAdvertisement);
384 assert_eq!(classify(ReceivePack, "info/refs", true, None), GitCall::RefAdvertisement);
385 let ls_refs = [pkt("command=ls-refs\n"), b"0001".to_vec(), pkt("peel\n"), b"0000".to_vec()].concat();
386 assert_eq!(classify(UploadPack, "git-upload-pack", false, Some(&ls_refs)), GitCall::LsRefs);
387 let fetch = [pkt("command=fetch\n"), b"0001".to_vec(), pkt("want 1111111111111111111111111111111111111111\n"), pkt("done\n"), b"0000".to_vec()].concat();
388 assert_eq!(classify(UploadPack, "git-upload-pack", false, Some(&fetch)), GitCall::Fetch);
389 let v0 = [pkt("want 1111111111111111111111111111111111111111 side-band-64k\n"), b"0000".to_vec(), pkt("done\n")].concat();
390 assert_eq!(classify(UploadPack, "git-upload-pack", false, Some(&v0)), GitCall::Fetch);
391 assert_eq!(classify(ReceivePack, "git-receive-pack", false, None), GitCall::ReceivePack);
392 // By default only fetches and pushes are operations; listing refs,
393 // and anything g1t answered from its cache, never are.
394 let mapping = crate::meters::Mapping::defaults();
395 assert_eq!(mapping.billable(GitCall::Fetch.meter()), 1.0);
396 assert_eq!(mapping.billable(GitCall::ReceivePack.meter()), 1.0);
397 assert_eq!(mapping.billable(GitCall::LsRefs.meter()), 0.0);
398 assert_eq!(mapping.billable(GitCall::RefAdvertisement.meter()), 0.0);
399 for call in [GitCall::RefAdvertisement, GitCall::LsRefs, GitCall::Fetch, GitCall::ReceivePack] {
400 assert_eq!(mapping.billable(call.cached_meter()), 0.0);
401 assert_eq!(mapping.cost(call.cached_meter()), 0.0);
402 }
403 }
404
405 #[test]
406 fn the_standing_kept_here_moves_with_local_counts_and_turns_with_the_hour() {
407 let mut standing = Standing { hour_key: "2026-10-14T09".into(), month: 50_000.0, hour: 59.0, read_at: 1_000 };
408 assert_eq!(standing.at("2026-10-14T09", 1_000), Some((50_000, 59)));
409 standing.add("2026-10-14T09", 2.0);
410 assert_eq!(standing.at("2026-10-14T09", 2_000), Some((50_002, 61)));
411 // A new hour starts at nothing; the month goes on.
412 assert_eq!(standing.at("2026-10-14T10", 2_000), Some((50_002, 0)));
413 standing.add("2026-10-14T10", 1.0);
414 assert_eq!(standing.at("2026-10-14T10", 2_000), Some((50_003, 1)));
415 // A new month too.
416 assert_eq!(standing.at("2026-11-01T00", 2_000), Some((0, 0)));
417 // Read too long ago: not gone by, so nobody is slowed on old news.
418 assert_eq!(standing.at("2026-10-14T10", 1_000 + STANDING_TTL_MS + 1), None);
419 }
420
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look421 #[test]
422 fn a_free_workspace_pushes_until_its_private_storage_is_full() {
423 assert!(!storage_full(999_999_999, 1_000_000_000));
424 assert!(storage_full(1_000_000_000, 1_000_000_000));
425 assert!(storage_full(3_000_000_000, 1_000_000_000));
426 }
427
428 #[test]
429 fn a_free_workspace_is_slowed_only_past_its_monthly_cap() {
430 // Under the cap: never slowed, however busy the hour.
431 assert!(!slow_down(49_999, 5_000, 50_000, 60));
432 // Past it: 60 an hour, then wait.
433 assert!(!slow_down(50_001, 60, 50_000, 60));
434 assert!(slow_down(50_001, 61, 50_000, 60));
435 }
436}

This file's history is long; its oldest lines are credited to the oldest commit read.