iter 0001.tidy: registry.toml, cache-before-fetch, escape registry slugs

Resolves the actionable drift the cycle-close architect review found:
- Ship registry.toml (the spec's acceptance artifact) for aura + data-server,
  so `docsite serve`/`check` run out of the box — the headline live-render was
  not runnable as shipped without it.
- Cache before fetch: the page handler now resolves the branch SHA, serves
  from cache on a hit, and only fetches the source on a miss (the spec
  data-flow). The pre-tidy handler always fetched, paying a wasted Gitea read
  on every cache hit. A new e2e test (a counting fetcher) pins that a second
  request serves from cache without re-fetching.
- Escape the registry slug/page in nav_html's href and the index links —
  operator-trusted data, but consistent with the documented no-injection
  invariant and the rest of the shell (which already escaped).

cargo test green (41: 32 lib + 8 e2e + 1 gated live smoke); clippy clean.

refs #1
This commit is contained in:
2026-06-28 16:55:23 +02:00
parent cb82987c33
commit 55801cf819
5 changed files with 122 additions and 23 deletions
+46
View File
@@ -191,3 +191,49 @@ docs="docs"
"index must list exactly the registry's projects, no more: {body}"
);
}
/// A Fetcher that counts `fetch_raw` calls, wrapping a MockFetcher. Lets the
/// E2E layer prove the cache-before-fetch data-flow.
struct CountingFetcher {
inner: MockFetcher,
fetches: Arc<std::sync::atomic::AtomicUsize>,
}
impl Fetcher for CountingFetcher {
fn branch_sha(&self, repo: &str, branch: &str) -> Result<String, GiteaError> {
self.inner.branch_sha(repo, branch)
}
fn fetch_raw(&self, repo: &str, branch: &str, path: &str) -> Result<String, GiteaError> {
self.fetches.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.fetch_raw(repo, branch, path)
}
}
/// Single source of truth + the revision-keyed cache (invariant 2) at the HTTP
/// layer: two requests for the same page at an unchanged branch SHA must hit the
/// source exactly ONCE — the second serves from cache without re-fetching. The
/// pre-audit handler fetched before checking the cache, so a cache hit still
/// paid the Gitea fetch; this pins the corrected hit -> serve, miss -> fetch flow.
#[tokio::test]
async fn second_request_serves_from_cache_without_refetching() {
let fetches = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let fetcher = CountingFetcher {
inner: MockFetcher::new("a517899")
.with("Brummel/aura", "docs/design/INDEX.md", "# Ledger\n"),
fetches: fetches.clone(),
};
let app = router(AppState {
reg: Arc::new(reg()),
fetcher: Arc::new(fetcher),
cache: Arc::new(Cache::new()),
});
let (s1, _) = get(app.clone(), "/aura/design/INDEX").await;
let (s2, _) = get(app, "/aura/design/INDEX").await;
assert_eq!(s1, StatusCode::OK);
assert_eq!(s2, StatusCode::OK);
assert_eq!(
fetches.load(std::sync::atomic::Ordering::SeqCst),
1,
"the second request must serve from cache, not re-fetch the source",
);
}