CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(labeler): add create_report stage module seed with sentinel builder

Introduces create_report.rs and create_report/sentinel.rs with the sentinel
reason builder for conformance-test reports. The sentinel module provides:
- build() to create formatted reason strings: "atproto-devtool conformance test <RFC3339> <run-id>"
- new_run_id() to generate random 16-hex-char identifiers using getrandom
- Hand-rolled RFC3339 UTC formatter and civil date conversion (avoiding chrono/time deps)

Phase 1 scaffolding only; full report submission logic lands in Phase 4.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

authored by

Jack Grigg
Claude Haiku 4.5
and committed by
Tangled
4f97f3e4 bd77b0ba

+141
+1
src/commands/test/labeler.rs
··· 1 1 //! `atproto-devtool test labeler <target>` command. 2 2 3 + pub mod create_report; 3 4 pub mod crypto; 4 5 pub mod http; 5 6 pub mod identity;
+60
src/commands/test/labeler/create_report.rs
··· 1 + //! `report` stage: exercises the labeler's authenticated 2 + //! `com.atproto.moderation.createReport` path. 3 + //! 4 + //! Scaffolding only in Phase 1. Stage `run()` and full public surface land 5 + //! in Phase 4. The `sentinel` submodule is self-contained and is exercised 6 + //! by later phases for the pollution-avoidance sentinel reason string. 7 + 8 + pub mod sentinel; 9 + 10 + #[cfg(test)] 11 + mod sentinel_tests { 12 + use super::sentinel; 13 + 14 + #[test] 15 + fn format_rfc3339_utc_pins_known_points() { 16 + use std::time::UNIX_EPOCH; 17 + // 1970-01-01T00:00:00Z 18 + assert_eq!( 19 + sentinel::build("test0000test0000", UNIX_EPOCH), 20 + "atproto-devtool conformance test 1970-01-01T00:00:00Z test0000test0000" 21 + ); 22 + // 2026-04-20T00:00:00Z (1_776_643_200 UNIX seconds) 23 + let t = UNIX_EPOCH + std::time::Duration::from_secs(1_776_643_200); 24 + let s = sentinel::build("test0000test0000", t); 25 + assert_eq!( 26 + s, 27 + "atproto-devtool conformance test 2026-04-20T00:00:00Z test0000test0000" 28 + ); 29 + // Leap year: 2024-02-29T12:34:56Z (1_709_210_096 UNIX seconds) 30 + let t = UNIX_EPOCH + std::time::Duration::from_secs(1_709_210_096); 31 + let s = sentinel::build("test0000test0000", t); 32 + assert_eq!( 33 + s, 34 + "atproto-devtool conformance test 2024-02-29T12:34:56Z test0000test0000" 35 + ); 36 + } 37 + 38 + #[test] 39 + fn build_contains_prefix_and_run_id() { 40 + use std::time::UNIX_EPOCH; 41 + let s = sentinel::build("abcdef1234567890", UNIX_EPOCH); 42 + assert!(s.starts_with(sentinel::SENTINEL_PREFIX)); 43 + assert!(s.ends_with("abcdef1234567890")); 44 + assert!(s.contains("1970-01-01T00:00:00Z")); 45 + } 46 + 47 + #[test] 48 + fn new_run_id_is_16_hex_chars() { 49 + let id = sentinel::new_run_id(); 50 + assert_eq!(id.len(), 16); 51 + assert!(id.chars().all(|c| c.is_ascii_hexdigit())); 52 + } 53 + 54 + #[test] 55 + fn new_run_id_is_unique_between_calls() { 56 + let a = sentinel::new_run_id(); 57 + let b = sentinel::new_run_id(); 58 + assert_ne!(a, b); 59 + } 60 + }
+80
src/commands/test/labeler/create_report/sentinel.rs
··· 1 + //! Builder for the sentinel `reason` field used in conformance-test reports. 2 + //! 3 + //! Every committing check's report body carries a stable, recognizable 4 + //! string in its `reason` field so that labeler operators can identify and 5 + //! dismiss reports submitted by `atproto-devtool` without mistaking them 6 + //! for real user reports. 7 + //! 8 + //! Format: `atproto-devtool conformance test <RFC3339-UTC> <run-id>` 9 + //! 10 + //! Example: `atproto-devtool conformance test 2026-04-17T12:34:56Z 5f9c1a3b4d7e8a0f` 11 + //! 12 + //! The run-id is a 16-hex-char random nonce generated once per pipeline run 13 + //! (not per check); the same run-id is reused across all report submissions 14 + //! within a single `test labeler` invocation so operators can trace a group 15 + //! of test reports back to one run. 16 + 17 + use std::time::{SystemTime, UNIX_EPOCH}; 18 + 19 + /// Prefix used so operators can grep their moderation queue for 20 + /// conformance-test reports with a single query. 21 + pub const SENTINEL_PREFIX: &str = "atproto-devtool conformance test"; 22 + 23 + /// Build a sentinel reason string. `run_id` should be a stable 16-hex-char 24 + /// identifier for the current test invocation; `now` is the current wall-clock 25 + /// time, typically `SystemTime::now()`. 26 + pub fn build(run_id: &str, now: SystemTime) -> String { 27 + let rfc3339 = format_rfc3339_utc(now); 28 + format!("{SENTINEL_PREFIX} {rfc3339} {run_id}") 29 + } 30 + 31 + /// Hand-rolled RFC 3339 UTC formatter: `YYYY-MM-DDTHH:MM:SSZ`. 32 + /// 33 + /// Avoids a `chrono` / `time` dependency. Leap seconds are not handled; 34 + /// the sentinel reason is a human-readable label, not a parseable timestamp. 35 + /// For times before the UNIX epoch or more than `i64::MAX` seconds in the 36 + /// future we degrade gracefully to `1970-01-01T00:00:00Z`. 37 + fn format_rfc3339_utc(ts: SystemTime) -> String { 38 + let secs = ts 39 + .duration_since(UNIX_EPOCH) 40 + .map(|d| d.as_secs() as i64) 41 + .unwrap_or(0); 42 + let (year, month, day, hour, min, sec) = unix_to_civil(secs); 43 + format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z") 44 + } 45 + 46 + /// Convert UNIX seconds to a civil date-time (UTC) using Howard Hinnant's 47 + /// algorithm for the Gregorian calendar. Correct for all years in [1, 9999]. 48 + fn unix_to_civil(secs: i64) -> (i32, u32, u32, u32, u32, u32) { 49 + // Seconds-of-day. 50 + let days = secs.div_euclid(86_400); 51 + let sod = secs.rem_euclid(86_400); 52 + let hour = (sod / 3600) as u32; 53 + let min = ((sod % 3600) / 60) as u32; 54 + let sec = (sod % 60) as u32; 55 + 56 + // Days since 1970-01-01 -> civil date. Algorithm from 57 + // http://howardhinnant.github.io/date_algorithms.html#civil_from_days. 58 + let z = days + 719_468; 59 + let era = if z >= 0 { z } else { z - 146_096 } / 146_097; 60 + let doe = (z - era * 146_097) as u32; // [0, 146096] 61 + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; // [0, 399] 62 + let y = yoe as i32 + era as i32 * 400; 63 + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] 64 + let mp = (5 * doy + 2) / 153; // [0, 11] 65 + let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31] 66 + let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12] 67 + let year = if m <= 2 { y + 1 } else { y }; 68 + (year, m, d, hour, min, sec) 69 + } 70 + 71 + /// Generate a random 16-hex-char run identifier. Uses `getrandom` 72 + /// (added as a direct dep during this task — see Cargo.toml edit below). 73 + pub fn new_run_id() -> String { 74 + let mut bytes = [0u8; 8]; 75 + getrandom::getrandom(&mut bytes).expect("OS CSPRNG is always available on supported platforms"); 76 + bytes.iter().fold(String::new(), |mut s, b| { 77 + s.push_str(&format!("{b:02x}")); 78 + s 79 + }) 80 + }