git-ents.gitmain
⌘K
foforge
namespace.rs605 lines · 24.1 KB · rusthistorycomment on this file
1//! Refname namespaces under `refs/meta/*`.
2//!
3//! Every builder here composes a refname and validates it through gitoxide's
4//! own [`gix::refs::FullName`] (`arch.no-object-store-trait`'s sibling rule:
5//! never define a parallel refname type). [`classify`] is the inverse
6//! direction — given a refname, which entity's namespace it falls in — for
7//! callers (the gate, `receive`) that need to route on a pushed ref without
8//! duplicating this module's namespace table.
9//!
10//! Spec coverage: `meta-ref.namespace`, `meta-ref.granularity`,
11//! `meta-ref.inbox`, plus the `refs/meta/toolchains/*`
12//! (`model.toolchain`), `refs/meta/redactions/*` (`model.redaction`),
13//! `refs/meta/reviews/*` (`model.review`), and `refs/meta/pins/*`
14//! (`model.review-pin`) namespaces.
15
16use gix::refs::{FullName, FullNameRef};
17
18use crate::member::MemberId;
19use crate::{Error, Result};
20
21fn build(name: String) -> Result<FullName> {
22 FullName::try_from(name.clone()).map_err(|source| Error::InvalidRefName { name, source })
23}
24
25/// The fixed ref for repository-global account state (`meta-ref.granularity`:
26/// "Repository-global state with a single writer-of-record MUST instead live
27/// on one fixed ref").
28pub const ACCOUNT_REF: &str = "refs/meta/account";
29
30/// The fixed ref for repository-global configuration
31/// (`meta-ref.granularity`).
32pub const CONFIG_REF: &str = "refs/meta/config";
33
34/// The ref holding the member named `id` — `refs/meta/member/<id>`
35/// (`meta-ref.granularity`).
36///
37/// # Examples
38///
39/// ```
40/// use ents_model::{MemberId, namespace};
41///
42/// let name = namespace::member_ref(&MemberId::new("jdc")).expect("valid id");
43/// assert_eq!(name.as_bstr(), "refs/meta/member/jdc");
44/// ```
45// @relation(meta-ref.granularity, scope=function)
46pub fn member_ref(id: &MemberId) -> Result<FullName> {
47 build(format!("refs/meta/member/{id}"))
48}
49
50/// The ref holding the issue named `id` — `refs/meta/issues/<id>`
51/// (`meta-ref.granularity`).
52// @relation(meta-ref.granularity, scope=function)
53pub fn issue_ref(id: &str) -> Result<FullName> {
54 build(format!("refs/meta/issues/{id}"))
55}
56
57/// The ref holding the comment named `id` — `refs/meta/comments/<id>`
58/// (`meta-ref.granularity`).
59// @relation(meta-ref.granularity, scope=function)
60pub fn comment_ref(id: &str) -> Result<FullName> {
61 build(format!("refs/meta/comments/{id}"))
62}
63
64/// The ref holding one reviewer's review of one commit —
65/// `refs/meta/reviews/<target>/<member>` (`meta-ref.granularity`,
66/// `model.review`), where `<target>` is the oid of the first commit the
67/// review judged and `<member>` is the reviewer's member id: a composite
68/// natural key (`meta-ref.identity-binding`) with no minted id anywhere,
69/// so one review thread lives per (target, reviewer) and all reviews of a
70/// commit enumerate by ref prefix.
71///
72/// # Examples
73///
74/// ```
75/// use ents_model::{MemberId, namespace};
76///
77/// let name = namespace::review_ref("deadbeef", &MemberId::new("jdc")).expect("valid");
78/// assert_eq!(name.as_bstr(), "refs/meta/reviews/deadbeef/jdc");
79/// ```
80// @relation(meta-ref.granularity, model.review, meta-ref.identity-binding, scope=function)
81pub fn review_ref(target: &str, member: &MemberId) -> Result<FullName> {
82 build(format!("refs/meta/reviews/{target}/{member}"))
83}
84
85/// The retention pin for one reviewer's review of one commit —
86/// `refs/meta/pins/reviews/<target>/<member>` (`model.review-pin`): the
87/// entity's own canonical suffix (`reviews/<target>/<member>`) prefixed
88/// with `pins/`, the same way `meta-ref.inbox` prefixes one, so two entity
89/// kinds can never collide under the same pin id.
90///
91/// A pin ref's commits carry the empty tree, never an entity — the sole
92/// exception to `meta-ref.namespace`'s tree-is-the-entity shape; the
93/// commits exist purely to keep the reviewed commit and its ancestry
94/// reachable. Because a pin's ancestry deliberately reaches into code
95/// history, the gate's parentless-roots walk is never applied to a pin
96/// (`meta-ref.identity-binding`).
97///
98/// # Examples
99///
100/// ```
101/// use ents_model::{MemberId, namespace};
102///
103/// let name = namespace::review_pin_ref("deadbeef", &MemberId::new("jdc")).expect("valid");
104/// assert_eq!(name.as_bstr(), "refs/meta/pins/reviews/deadbeef/jdc");
105/// ```
106// @relation(model.review-pin, meta-ref.namespace, meta-ref.identity-binding, scope=function)
107pub fn review_pin_ref(target: &str, member: &MemberId) -> Result<FullName> {
108 build(format!("refs/meta/pins/reviews/{target}/{member}"))
109}
110
111/// The `(target, member)` a review or review-pin refname names, or `None`
112/// when `name` is not a well-formed `refs/meta/reviews/<target>/<member>`
113/// or `refs/meta/pins/reviews/<target>/<member>` ref (`model.review`).
114///
115/// The gate recomputes a review's composite key from its signed content
116/// and compares it to this parse (`meta-ref.identity-binding`,
117/// `gate.identity-binding`), so the parser lives here next to the builder
118/// rather than re-derived at the call site.
119///
120/// # Examples
121///
122/// ```
123/// use ents_model::{MemberId, namespace};
124///
125/// let name: gix::refs::FullName = "refs/meta/reviews/deadbeef/jdc".try_into().expect("valid");
126/// assert_eq!(
127/// namespace::parse_review_ref(name.as_ref()),
128/// Some(("deadbeef".to_owned(), MemberId::new("jdc"))),
129/// );
130///
131/// let pin: gix::refs::FullName = "refs/meta/pins/reviews/deadbeef/jdc".try_into().expect("valid");
132/// assert_eq!(
133/// namespace::parse_review_ref(pin.as_ref()),
134/// Some(("deadbeef".to_owned(), MemberId::new("jdc"))),
135/// );
136/// ```
137// @relation(model.review, meta-ref.identity-binding, scope=function)
138#[must_use]
139pub fn parse_review_ref(name: &FullNameRef) -> Option<(String, MemberId)> {
140 let path = name.as_bstr().to_string();
141 let rest = path
142 .strip_prefix("refs/meta/reviews/")
143 .or_else(|| path.strip_prefix("refs/meta/pins/reviews/"))?;
144 let (target, member) = rest.split_once('/')?;
145 if target.is_empty() || member.is_empty() || member.contains('/') {
146 return None;
147 }
148 Some((target.to_owned(), MemberId::new(member)))
149}
150
151/// The `(effect, short_oid)` a result refname names, or `None` when `name`
152/// is not a well-formed `refs/meta/results/<effect>/<short-oid>` or
153/// `refs/meta/self/<member>/<effect>/<short-oid>` ref
154/// (`effect.results-writeback`, `meta-ref.inbox`).
155///
156/// The gate recomputes a result's composite key from its signed tree's
157/// effect and target fields and compares it to this parse
158/// (`model.result-identity`, `gate.identity-binding`).
159///
160/// # Examples
161///
162/// ```
163/// use ents_model::namespace;
164///
165/// let name: gix::refs::FullName = "refs/meta/results/unit/abc123".try_into().expect("valid");
166/// assert_eq!(
167/// namespace::parse_result_ref(name.as_ref()),
168/// Some(("unit".to_owned(), "abc123".to_owned())),
169/// );
170///
171/// let self_run: gix::refs::FullName = "refs/meta/self/jdc/unit/abc123".try_into().expect("valid");
172/// assert_eq!(
173/// namespace::parse_result_ref(self_run.as_ref()),
174/// Some(("unit".to_owned(), "abc123".to_owned())),
175/// );
176/// ```
177// @relation(model.result-identity, meta-ref.identity-binding, scope=function)
178#[must_use]
179pub fn parse_result_ref(name: &FullNameRef) -> Option<(String, String)> {
180 let path = name.as_bstr().to_string();
181 let rest = path.strip_prefix("refs/meta/")?;
182 let tail = if let Some(canonical) = rest.strip_prefix("results/") {
183 canonical.to_owned()
184 } else {
185 let self_run = rest.strip_prefix("self/")?;
186 // refs/meta/self/<member>/<effect>/<short-oid>: drop the member.
187 let (_, effect_and_oid) = self_run.split_once('/')?;
188 effect_and_oid.to_owned()
189 };
190 let (effect, short_oid) = tail.split_once('/')?;
191 if effect.is_empty() || short_oid.is_empty() || short_oid.contains('/') {
192 return None;
193 }
194 Some((effect.to_owned(), short_oid.to_owned()))
195}
196
197/// The ref holding the effect named `name` — `refs/meta/effects/<name>`
198/// (`meta-ref.granularity`, `effect.definition`).
199// @relation(meta-ref.granularity, effect.definition, scope=function)
200pub fn effect_ref(name: &str) -> Result<FullName> {
201 build(format!("refs/meta/effects/{name}"))
202}
203
204/// The canonical ref for one effect's result on one tested commit —
205/// `refs/meta/results/<effect>/<short_oid>` (`meta-ref.granularity`,
206/// `effect.definition`: derived from the effect's own name, never a
207/// stored pattern).
208// @relation(meta-ref.granularity, effect.definition, scope=function)
209pub fn result_ref(effect: &str, short_oid: &str) -> Result<FullName> {
210 build(format!("refs/meta/results/{effect}/{short_oid}"))
211}
212
213/// The ref mirroring one effect's result that `member` produced on their own
214/// executor — `refs/meta/self/<member>/<effect>/<short_oid>`
215/// (`meta-ref.inbox`, `effect.self-run`).
216///
217/// `self` is its own top-level namespace, a fixed segment from the spec's
218/// namespace table, so the canonical results glob
219/// (`refs/meta/results/<effect>/*`) and the self-run glob
220/// (`refs/meta/self/<member>/*`) are disjoint by construction.
221///
222/// # Examples
223///
224/// ```
225/// use ents_model::{MemberId, namespace};
226///
227/// let name = namespace::self_result_ref(&MemberId::new("jdc"), "unit", "abc123")
228/// .expect("valid segments");
229/// assert_eq!(name.as_bstr(), "refs/meta/self/jdc/unit/abc123");
230/// ```
231// @relation(meta-ref.inbox, scope=function)
232pub fn self_result_ref(member: &MemberId, effect: &str, short_oid: &str) -> Result<FullName> {
233 build(format!("refs/meta/self/{member}/{effect}/{short_oid}"))
234}
235
236/// The member segment of a `refs/meta/self/<member>/...` refname, or `None`
237/// when `name` is not under the self-run namespace (`meta-ref.inbox`).
238///
239/// The gate keys self-run authorization on this segment — a member may write
240/// only their *own* self-run mirror — so it is extracted here, next to the
241/// namespace table it belongs to, rather than re-parsed by every caller.
242///
243/// # Examples
244///
245/// ```
246/// use ents_model::{MemberId, namespace};
247///
248/// let name: gix::refs::FullName = "refs/meta/self/jdc/unit/abc123".try_into().expect("valid");
249/// assert_eq!(namespace::self_run_owner(name.as_ref()), Some(MemberId::new("jdc")));
250///
251/// let canonical: gix::refs::FullName = "refs/meta/results/unit/abc123".try_into().expect("valid");
252/// assert_eq!(namespace::self_run_owner(canonical.as_ref()), None);
253/// ```
254// @relation(meta-ref.inbox, scope=function)
255#[must_use]
256pub fn self_run_owner(name: &FullNameRef) -> Option<MemberId> {
257 let path = name.as_bstr().to_string();
258 let rest = path.strip_prefix("refs/meta/self/")?;
259 let (member, _) = rest.split_once('/')?;
260 Some(MemberId::new(member))
261}
262
263/// The ref holding one inbox entity authored by `member`, awaiting
264/// adoption — `refs/meta/inbox/<member>/<id>` (`meta-ref.inbox`).
265///
266/// The member segment leads, symmetric with [`self_result_ref`], so the
267/// gate's authorization keys off the refname alone: a member — either
268/// provenance — may write only under its own segment, and nobody,
269/// admins included, writes into another member's inbox.
270///
271/// # Examples
272///
273/// ```
274/// use ents_model::{MemberId, namespace};
275///
276/// let name = namespace::inbox_ref(&MemberId::new("jdc"), "issue-42").expect("valid");
277/// assert_eq!(name.as_bstr(), "refs/meta/inbox/jdc/issue-42");
278/// ```
279// @relation(meta-ref.inbox, scope=function)
280pub fn inbox_ref(member: &MemberId, id: &str) -> Result<FullName> {
281 build(format!("refs/meta/inbox/{member}/{id}"))
282}
283
284/// The member segment of a `refs/meta/inbox/<member>/...` refname, or
285/// `None` when `name` is not under the inbox namespace or carries no
286/// member segment (`meta-ref.inbox`).
287///
288/// Mirrors [`self_run_owner`]: the gate keys inbox authorization on this
289/// segment, so it is extracted here, next to the namespace table.
290///
291/// # Examples
292///
293/// ```
294/// use ents_model::{MemberId, namespace};
295///
296/// let name: gix::refs::FullName = "refs/meta/inbox/jdc/issue-42".try_into().expect("valid");
297/// assert_eq!(namespace::inbox_owner(name.as_ref()), Some(MemberId::new("jdc")));
298///
299/// // The legacy unscoped shape has no owner to authorize.
300/// let unscoped: gix::refs::FullName = "refs/meta/inbox/issue-42".try_into().expect("valid");
301/// assert_eq!(namespace::inbox_owner(unscoped.as_ref()), None);
302/// ```
303// @relation(meta-ref.inbox, scope=function)
304#[must_use]
305pub fn inbox_owner(name: &FullNameRef) -> Option<MemberId> {
306 let path = name.as_bstr().to_string();
307 let rest = path.strip_prefix("refs/meta/inbox/")?;
308 let (member, _) = rest.split_once('/')?;
309 Some(MemberId::new(member))
310}
311
312/// The ref holding the toolchain manifest named `name` —
313/// `refs/meta/toolchains/<name>` (`model.toolchain`).
314// @relation(model.toolchain, scope=function)
315pub fn toolchain_ref(name: &str) -> Result<FullName> {
316 build(format!("refs/meta/toolchains/{name}"))
317}
318
319/// The ref holding the redaction record named `id` —
320/// `refs/meta/redactions/<id>` (`model.redaction`).
321// @relation(model.redaction, scope=function)
322pub fn redaction_ref(id: &str) -> Result<FullName> {
323 build(format!("refs/meta/redactions/{id}"))
324}
325
326/// The ref holding one claim — `refs/meta/claims/<id>`, where `<id>` is
327/// the claim's own genesis commit oid: the sign-then-name envelope, same
328/// as a comment or an issue. Unlike those, this ref is append-once — the
329/// tip IS the genesis; a changed assertion is a new claim, never an
330/// advance.
331///
332/// A claim's ledger commit carries its binding's witness commits as
333/// parents, so its ancestry deliberately reaches into code history exactly
334/// as a review pin's does — the gate's parentless-roots walk must never
335/// apply to a claim, the same carve-out [`review_pin_ref`]'s doc already
336/// describes for pins.
337///
338/// # Examples
339///
340/// ```
341/// use ents_model::namespace;
342///
343/// let name = namespace::claim_ref("deadbeef").expect("valid");
344/// assert_eq!(name.as_bstr(), "refs/meta/claims/deadbeef");
345/// ```
346pub fn claim_ref(id: &str) -> Result<FullName> {
347 build(format!("refs/meta/claims/{id}"))
348}
349
350/// Which entity namespace a `refs/meta/*` refname falls in.
351///
352/// The inbox and self-run namespaces classify as their own variants even
353/// though `meta-ref.inbox` requires them to "hold the same typed trees as
354/// their canonical counterparts; only the refname rule differs" — that
355/// refname rule is exactly what the gate routes on, so the distinction
356/// belongs in this table rather than re-derived by every caller.
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358#[non_exhaustive]
359pub enum Namespace {
360 /// `refs/meta/member/*`.
361 Member,
362 /// `refs/meta/issues/*`.
363 Issue,
364 /// `refs/meta/comments/*`.
365 Comment,
366 /// `refs/meta/reviews/*`.
367 Review,
368 /// `refs/meta/pins/*` — retention pins (`model.review-pin`,
369 /// [`review_pin_ref`]): empty-tree commits anchoring other content's
370 /// reachability, the sole exception to `meta-ref.namespace`'s
371 /// tree-is-the-entity shape.
372 Pin,
373 /// `refs/meta/effects/*`.
374 Effect,
375 /// `refs/meta/results/*` — canonical results only; a member's self-run
376 /// mirror is [`Namespace::SelfRun`], disjoint by construction
377 /// (`meta-ref.inbox`).
378 Result,
379 /// `refs/meta/self/<member>/*` — self-run result mirrors
380 /// (`meta-ref.inbox`, [`self_result_ref`]).
381 SelfRun,
382 /// `refs/meta/toolchains/*`.
383 Toolchain,
384 /// `refs/meta/redactions/*`.
385 Redaction,
386 /// `refs/meta/inbox/<member>/*` — entities awaiting adoption,
387 /// each under its author's own segment (`meta-ref.inbox`,
388 /// [`inbox_ref`], [`inbox_owner`]).
389 Inbox,
390 /// The fixed `refs/meta/account` ref.
391 Account,
392 /// The fixed `refs/meta/config` ref.
393 Config,
394 /// `refs/meta/claims/*` — one ref per claim ([`claim_ref`]), append-once:
395 /// the tip IS the genesis. A claim's ancestry reaches into code history
396 /// through its binding's witness commits, exactly as [`Namespace::Pin`]'s
397 /// does, so the all-roots walk must never apply to it either.
398 Claim,
399 /// Under `refs/meta/*`, but in no namespace this build of the vocabulary
400 /// knows. `model.extensibility` requires a stock server to carry entity
401 /// types it cannot parse, so the gate and `receive` must be able to
402 /// route an unknown meta namespace generically rather than confuse it
403 /// with a ref that is not forge state at all — which is why this is a
404 /// variant and not a `None`.
405 Unknown,
406}
407
408/// Classify a `refs/meta/*` refname by which entity's namespace it falls in.
409///
410/// Returns `None` only when `name` is not under `refs/meta/*` at all
411/// (`meta-ref.namespace`: "All forge state MUST live under `refs/meta/*`").
412/// A refname under `refs/meta/*` whose namespace this build does not know
413/// classifies as [`Namespace::Unknown`] instead — it is still forge state
414/// (`model.extensibility`), just state this vocabulary cannot interpret.
415///
416/// # Examples
417///
418/// ```
419/// use ents_model::namespace::{self, Namespace};
420///
421/// let name: gix::refs::FullName = "refs/meta/issues/42".try_into().expect("valid");
422/// assert_eq!(namespace::classify(name.as_ref()), Some(Namespace::Issue));
423///
424/// let outside: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
425/// assert_eq!(namespace::classify(outside.as_ref()), None);
426///
427/// let novel: gix::refs::FullName = "refs/meta/widgets/7".try_into().expect("valid");
428/// assert_eq!(namespace::classify(novel.as_ref()), Some(Namespace::Unknown));
429/// ```
430// @relation(meta-ref.namespace, meta-ref.granularity, model.extensibility, scope=function)
431#[must_use]
432pub fn classify(name: &FullNameRef) -> Option<Namespace> {
433 let path = name.as_bstr().to_string();
434 let rest = path.strip_prefix("refs/meta/")?;
435
436 if rest == "account" {
437 return Some(Namespace::Account);
438 }
439 if rest == "config" {
440 return Some(Namespace::Config);
441 }
442 let (segment, _) = rest.split_once('/').unwrap_or((rest, ""));
443 match segment {
444 "member" => Some(Namespace::Member),
445 "issues" => Some(Namespace::Issue),
446 "comments" => Some(Namespace::Comment),
447 "reviews" => Some(Namespace::Review),
448 "pins" => Some(Namespace::Pin),
449 "effects" => Some(Namespace::Effect),
450 "results" => Some(Namespace::Result),
451 "self" => Some(Namespace::SelfRun),
452 "toolchains" => Some(Namespace::Toolchain),
453 "redactions" => Some(Namespace::Redaction),
454 "inbox" => Some(Namespace::Inbox),
455 "claims" => Some(Namespace::Claim),
456 _ => Some(Namespace::Unknown),
457 }
458}
459
460/// Whether a `refs/meta/*` refname is under the inbox namespace —
461/// `refs/meta/inbox/<member>/*` — per `meta-ref.inbox`.
462///
463/// This is namespace membership only; which member owns the segment is
464/// [`inbox_owner`]'s answer. A member's self-run result mirror lives
465/// under its own top-level `refs/meta/self/*` namespace
466/// ([`self_result_ref`]), not under the inbox, so it is deliberately not
467/// matched here.
468///
469/// # Examples
470///
471/// ```
472/// use ents_model::namespace;
473///
474/// let inbox: gix::refs::FullName = "refs/meta/inbox/jdc/issue-42".try_into().expect("valid");
475/// assert!(namespace::is_inbox(inbox.as_ref()));
476///
477/// let canonical: gix::refs::FullName = "refs/meta/results/unit/abc123".try_into().expect("valid");
478/// assert!(!namespace::is_inbox(canonical.as_ref()));
479/// ```
480// @relation(meta-ref.inbox, scope=function)
481#[must_use]
482pub fn is_inbox(name: &FullNameRef) -> bool {
483 let path = name.as_bstr().to_string();
484 let Some(rest) = path.strip_prefix("refs/meta/") else {
485 return false;
486 };
487 rest.starts_with("inbox/")
488}
489
490#[cfg(test)]
491mod tests {
492 #![allow(clippy::expect_used, reason = "unit test")]
493
494 use rstest::rstest;
495
496 use super::*;
497
498 fn name(s: &str) -> FullName {
499 s.try_into().expect("valid refname in test table")
500 }
501
502 #[rstest]
503 #[case::member("refs/meta/member/jdc", Some(Namespace::Member))]
504 #[case::issue("refs/meta/issues/42", Some(Namespace::Issue))]
505 #[case::comment("refs/meta/comments/abc", Some(Namespace::Comment))]
506 #[case::review("refs/meta/reviews/7", Some(Namespace::Review))]
507 #[case::pin("refs/meta/pins/reviews/7", Some(Namespace::Pin))]
508 #[case::effect("refs/meta/effects/unit", Some(Namespace::Effect))]
509 #[case::result("refs/meta/results/unit/abc123", Some(Namespace::Result))]
510 #[case::self_run("refs/meta/self/jdc/unit/abc123", Some(Namespace::SelfRun))]
511 #[case::toolchain("refs/meta/toolchains/rust-stable", Some(Namespace::Toolchain))]
512 #[case::redaction("refs/meta/redactions/abc", Some(Namespace::Redaction))]
513 #[case::inbox("refs/meta/inbox/jdc/issue-42", Some(Namespace::Inbox))]
514 #[case::claim("refs/meta/claims/deadbeef", Some(Namespace::Claim))]
515 #[case::account("refs/meta/account", Some(Namespace::Account))]
516 #[case::config("refs/meta/config", Some(Namespace::Config))]
517 #[case::outside_meta("refs/heads/main", None)]
518 #[case::unrecognized("refs/meta/index/abc", Some(Namespace::Unknown))]
519 #[case::novel_namespace("refs/meta/widgets/7", Some(Namespace::Unknown))]
520 // @relation(meta-ref.namespace, meta-ref.granularity, scope=function, role=Verifies)
521 fn classify_matches_the_namespace_table(
522 #[case] refname: &str,
523 #[case] expected: Option<Namespace>,
524 ) {
525 assert_eq!(classify(name(refname).as_ref()), expected);
526 }
527
528 #[rstest]
529 #[case::inbox_entity("refs/meta/inbox/jdc/issue-42", true)]
530 #[case::unscoped_inbox_is_still_the_namespace("refs/meta/inbox/legacy", true)]
531 #[case::canonical_result("refs/meta/results/unit/abc123", false)]
532 #[case::self_run_mirror("refs/meta/self/jdc/unit/abc123", false)]
533 #[case::member("refs/meta/member/jdc", false)]
534 // @relation(meta-ref.inbox, scope=function, role=Verifies)
535 fn is_inbox_matches_only_inbox_namespaces(#[case] refname: &str, #[case] expected: bool) {
536 assert_eq!(is_inbox(name(refname).as_ref()), expected);
537 }
538
539 #[rstest]
540 #[case::self_run("refs/meta/self/jdc/unit/abc123", Some("jdc"))]
541 #[case::self_run_deep_effect("refs/meta/self/worker-1/it/deadbeef", Some("worker-1"))]
542 #[case::bare_self_segment("refs/meta/self/jdc", None)]
543 #[case::canonical_result("refs/meta/results/unit/abc123", None)]
544 #[case::outside_meta("refs/heads/main", None)]
545 // @relation(meta-ref.inbox, scope=function, role=Verifies)
546 fn self_run_owner_extracts_only_the_self_namespace_member(
547 #[case] refname: &str,
548 #[case] expected: Option<&str>,
549 ) {
550 assert_eq!(
551 self_run_owner(name(refname).as_ref()),
552 expected.map(MemberId::new)
553 );
554 }
555
556 #[rstest]
557 #[case::scoped("refs/meta/inbox/jdc/issue-42", Some("jdc"))]
558 #[case::deep_id("refs/meta/inbox/worker-1/a/b", Some("worker-1"))]
559 #[case::unscoped_legacy("refs/meta/inbox/issue-42", None)]
560 #[case::self_run("refs/meta/self/jdc/unit/abc", None)]
561 #[case::outside_meta("refs/heads/main", None)]
562 // @relation(meta-ref.inbox, scope=function, role=Verifies)
563 fn inbox_owner_extracts_only_the_member_segment(
564 #[case] refname: &str,
565 #[case] expected: Option<&str>,
566 ) {
567 assert_eq!(
568 inbox_owner(name(refname).as_ref()),
569 expected.map(MemberId::new)
570 );
571 }
572
573 #[rstest]
574 // @relation(meta-ref.namespace, effect.definition, scope=function, role=Verifies)
575 fn every_builder_stays_under_refs_meta() {
576 let id = MemberId::new("jdc");
577 let built = [
578 member_ref(&id).expect("valid"),
579 issue_ref("42").expect("valid"),
580 comment_ref("abc").expect("valid"),
581 review_ref("deadbeef", &id).expect("valid"),
582 review_pin_ref("deadbeef", &id).expect("valid"),
583 effect_ref("unit").expect("valid"),
584 result_ref("unit", "abc123").expect("valid"),
585 self_result_ref(&id, "unit", "abc123").expect("valid"),
586 inbox_ref(&id, "issue-42").expect("valid"),
587 toolchain_ref("rust-stable").expect("valid"),
588 redaction_ref("abc").expect("valid"),
589 claim_ref("deadbeef").expect("valid"),
590 ];
591 for name in built {
592 assert!(
593 name.as_bstr().starts_with(b"refs/meta/"),
594 "{name} must live under refs/meta/*"
595 );
596 }
597 }
598
599 #[rstest]
600 // @relation(meta-ref.namespace, scope=function, role=Verifies)
601 fn invalid_component_is_rejected_not_silently_accepted() {
602 let err = issue_ref("../escape").expect_err("must reject a refname with a `..` component");
603 assert!(matches!(err, Error::InvalidRefName { .. }));
604 }
605}