roots: surface recorded results as a checks card on the commit page
commit
7e0aa6eroots: surface recorded results as a checks card on the commit page
Every ResultRecord whose stored target field names the shown commit — the canonical refs/meta/results namespace and every member’s self-run mirror — renders as a pass/fail/error status chip linking to its effect’s page, matched on the tree field the gate itself binds rather than the refname segment. No results renders nothing: a result is only written by a run, so no checks is the ordinary state of a commit, not a pending one.
Assisted-by: Claude:claude-fable-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/cli/ents-web/tests/router.rs
@@ -12,10 +12,11 @@
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use ents_kiln::Toolchain;
-use ents_model::{Account, Effect, MemberId, Provenance, Redaction};
+use ents_model::{Account, Effect, MemberId, Provenance, Redaction, ResultRecord, Status};
use ents_receive::{Mode, NullEventSink};
use ents_testutil::{
- CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity,
+ CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, record_result, write_commit,
+ write_meta_entity,
};
use ents_web::identity::SigningIdentity;
use ents_web::state::AppState;
@@ -2084,6 +2085,82 @@
);
}
+/// The commit page's Checks card (`model.result-identity`,
+/// `model.result-taxonomy`): a recorded result whose stored target names
+/// the shown commit renders as a status chip and its effect's link --
+/// one row per taxonomy value here -- a self-run mirror row additionally
+/// names its member, and a result targeting a different commit stays off
+/// the page (the tree's own `target` field is what is matched, not the
+/// refname).
+#[tokio::test]
+async fn commit_page_lists_recorded_results_as_checks() {
+ let dir = seed_repo(&[("src/main.rs", "fn main() {}\n")]);
+ let oid = head_oid(dir.path());
+ let refs = MemRefStore::default();
+ let objects = ObjectStore::default();
+ record_result(&refs, &objects, "unit", &oid, Status::Pass, None, 1_000);
+ record_result(&refs, &objects, "lint", &oid, Status::Fail, None, 1_001);
+ record_result(&refs, &objects, "deploy", &oid, Status::Error, None, 1_002);
+ // Same effect, different target commit: filtered out by the stored
+ // target field.
+ record_result(
+ &refs,
+ &objects,
+ "unit",
+ "aaaaaaaa",
+ Status::Pass,
+ None,
+ 1_003,
+ );
+ // A self-run mirror row names the member that ran it.
+ let member = MemberId::new("joey");
+ let target = gix::ObjectId::from_hex(oid.as_bytes()).expect("head oid is hex");
+ let self_ref = ents_model::namespace::self_result_ref(&member, "unit", &oid).expect("valid");
+ let mirror = ResultRecord::new("unit", target, Status::Pass);
+ write_meta_entity(&refs, &objects, self_ref, &mirror, None, 1_004);
+
+ let state = Arc::new(AppState::new(
+ Box::new(refs),
+ objects,
+ Box::new(NullEventSink),
+ Mode::Advisory,
+ Box::new(FixtureIdentity {
+ name: "local-user",
+ key: Keypair::from_seed(1),
+ }),
+ dir.path().to_owned(),
+ ));
+ let router = ents_web::router(state);
+
+ let body = get_body(&router, &format!("/commit/{oid}")).await;
+ assert!(body.contains("Checks"), "the Checks card renders");
+ for (chip, effect) in [
+ ("status-pass", "unit"),
+ ("status-fail", "lint"),
+ ("status-error", "deploy"),
+ ] {
+ assert!(
+ body.contains(chip),
+ "the {effect} row carries its {chip} chip"
+ );
+ assert!(
+ body.contains(&format!("/effects/{effect}")),
+ "the {effect} row links to its effect page"
+ );
+ }
+ assert!(
+ body.contains("self-run by joey"),
+ "the mirror row names its member"
+ );
+ // The chip class appears once in the canonical unit row and once in
+ // the self-run mirror row -- never for the other commit's result.
+ assert_eq!(
+ body.matches("status-pass").count(),
+ 2,
+ "the other commit's result stays off the page"
+ );
+}
+
/// `GET /commit/{oid}` on a malformed id is a 404, never a panic or 500.
#[tokio::test]
async fn commit_show_on_an_invalid_oid_is_not_found_not_a_crash() {
crates/cli/ents-web/src/assets/ents.css
@@ -57,6 +57,9 @@
--s-prop: #076678;
--diff-add: #4e9a0622;
--diff-del: #cc241d22;
+ --status-pass: #4e9a06;
+ --status-fail: #cc241d;
+ --status-error: #b57614;
}
@media (prefers-color-scheme: dark) {
:root {
@@ -82,6 +85,9 @@
--s-prop: #83a598;
--diff-add: #b8bb2620;
--diff-del: #fb493420;
+ --status-pass: #b8bb26;
+ --status-fail: #fb4934;
+ --status-error: #fabd2f;
}
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -439,6 +445,12 @@
* (`crate::pages::{comments,issues,commits}`). */
.comment-state, .verdict { display: inline-block; padding: .05rem .5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-size: .78rem; font-weight: 600; text-transform: lowercase; color: var(--color-text-muted); }
.verdict { color: var(--color-text); }
+/* Check-status chips (`crate::pages::commits::checks_section`): one color
+ * per value of the closed pass/fail/error result taxonomy. */
+.status { display: inline-block; font-family: var(--font-mono); font-size: .72rem; font-weight: 700; border-radius: var(--radius-pill); padding: .05rem .6rem; text-transform: lowercase; }
+.status-pass { color: var(--status-pass); background: color-mix(in srgb, var(--status-pass) 12%, transparent); }
+.status-fail { color: var(--status-fail); background: color-mix(in srgb, var(--status-fail) 12%, transparent); }
+.status-error { color: var(--status-error); background: color-mix(in srgb, var(--status-error) 14%, transparent); }
/* The reply/resolve/reopen and comment composer forms on a thread card. */
.comment-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .6rem; padding: .7rem 1.1rem; border-top: 1px solid var(--color-border); }
crates/cli/ents-web/src/pages/commits.rs
@@ -258,6 +258,7 @@
let old_tree_ref = old_tree.as_ref().unwrap_or(&empty_tree);
let (diff, truncated) = diff_sections(&repo, old_tree_ref, &new_tree);
let comments = super::comments::for_commit(&state, object_id);
+ let checks = checks_section(&state, object_id);
let reviews = reviews_section(&state, &session, object_id, &oid);
let (sidebar_rows, _older) = commit_rows(&state, None, PAGE_SIZE);
@@ -301,6 +302,7 @@
}
}
}
+ (checks)
(reviews)
}
(diff)
@@ -338,6 +340,109 @@
}
}
+/// One row of the commit page's "Checks" card: a recorded result targeting
+/// the shown commit.
+struct CheckRow {
+ /// The recording effect's name ([`ents_model::ResultRecord`]'s own
+ /// `effect` field), the row's `/effects/{name}` link.
+ effect: String,
+ /// The run's outcome, one of the closed taxonomy's three values.
+ status: ents_model::Status,
+ /// The self-run mirror's `<member>` segment when the result lives
+ /// there rather than the canonical namespace (`effect.self-run`).
+ self_run: Option<String>,
+ /// The result ref tip's author time, for [`super::ago`].
+ seconds: Option<i64>,
+}
+
+/// A [`ents_model::Status`]'s display word, doubling as its
+/// `.status-<word>` chip class -- the closed pass/fail/error taxonomy
+/// (`model.result-taxonomy`), spelled out here rather than through
+/// `Debug`.
+fn status_label(status: ents_model::Status) -> &'static str {
+ match status {
+ ents_model::Status::Pass => "pass",
+ ents_model::Status::Fail => "fail",
+ ents_model::Status::Error => "error",
+ }
+}
+
+/// The "Checks" card on `GET /commit/{oid}`: every recorded result
+/// (`model.result-identity`) whose stored `target` field names this
+/// commit -- the canonical `refs/meta/results/<effect>/<short-oid>`
+/// namespace and every member's self-run mirror
+/// (`refs/meta/self/<member>/...`), matched on the tree's own `target`
+/// field (the same binding the gate verifies), never the refname's
+/// short-oid segment. Renders nothing at all when no result targets the
+/// commit: a result is only ever written by a run
+/// (`effect.result-taxonomy`), so "no checks" is the ordinary state of
+/// most commits, not a pending one. Best effort: a result ref whose tree
+/// cannot be read back is skipped from this card (it still lists on
+/// `git ents effect log`).
+// @relation(model.result-identity, model.result-taxonomy, scope=function)
+fn checks_section<O: Find + Write>(state: &AppState<O>, commit_id: ObjectId) -> Markup {
+ let mut rows: Vec<CheckRow> = Vec::new();
+ for prefix in ["refs/meta/results/", "refs/meta/self/"] {
+ let Ok(iter) = state.refs.iter_prefix(prefix) else {
+ continue;
+ };
+ for entry in iter {
+ let Ok((name, tip)) = entry else { continue };
+ // One `state.objects()` lock per read -- the same
+ // non-reentrant-`Mutex` care `crate::pages::effects::read_all`
+ // documents.
+ let record = {
+ let objects = state.objects();
+ super::commit_tree(&*objects, tip).ok().and_then(|tree| {
+ facet_git_tree::deserialize::<ents_model::ResultRecord>(&tree, &*objects).ok()
+ })
+ };
+ let Some(record) = record else { continue };
+ if record.target() != commit_id {
+ continue;
+ }
+ let path = name.as_bstr().to_string();
+ let self_run = path
+ .strip_prefix("refs/meta/self/")
+ .and_then(|rest| rest.split('/').next())
+ .map(str::to_owned);
+ let seconds = super::commit_authorship(&*state.objects(), tip)
+ .ok()
+ .map(|(_author, seconds)| seconds);
+ rows.push(CheckRow {
+ effect: record.effect,
+ status: record.status,
+ self_run,
+ seconds,
+ });
+ }
+ }
+ if rows.is_empty() {
+ return html! {};
+ }
+ rows.sort_by(|a, b| (&a.effect, &a.self_run).cmp(&(&b.effect, &b.self_run)));
+ html! {
+ div.card {
+ div.card-header { "Checks" }
+ @for row in &rows {
+ div.card-row {
+ span class={ "status status-" (status_label(row.status)) } {
+ (status_label(row.status))
+ }
+ " "
+ a href={ "/effects/" (row.effect) } { (row.effect) }
+ @if let Some(member) = &row.self_run {
+ span.muted { " \u{b7} self-run by " (member) }
+ }
+ @if let Some(seconds) = row.seconds {
+ span.entry-size { (super::ago(seconds)) }
+ }
+ }
+ }
+ }
+ }
+}
+
/// Every review targeting `commit_id` (`ents_forge::review::list` filtered
/// to this commit, `model.review`), each rendering its verdict prominently,
/// its body as AsciiDoc, and its reviewer (from the review ref's own tip