atproto-devtool 0.1.1

A multitool for the atproto developer ecosystem
Documentation
//! Pollution-avoidance helpers for committing report checks.
//!
//! When the tool actually POSTs a report (positive path only), we have
//! two obligations:
//!
//! 1. Avoid contaminating real moderation queues on public labelers.
//! 2. Be easy to identify and dismiss for operators who see the report.
//!
//! Obligation 1 is satisfied by preferring:
//!   - `reasonOther` reasonType when advertised (signals "this is a test
//!     or edge case, review leisurely"), falling back to the lex-first
//!     advertised value.
//!   - `record` subject type with a hardcoded AT-URI pointing at an
//!     explanation post the tool author publishes (release-gate item),
//!     falling back to `account` subject pointing at the *reporter's*
//!     own DID (the self-mint DID, which is always safe).
//!
//! Obligation 2 is satisfied by the sentinel `reason` string from the
//! `sentinel` module — it's stable and greppable.
//!
//! On *local* labelers the safety constraint relaxes: the queue is the
//! developer's own, so we use lex-first `reasonType` + `account` subject
//! (pointing at the reporter's own DID) to exercise the simplest working
//! shape. This makes the test deterministic for round-trip debugging.

use serde_json::{Value, json};

use crate::common::identity::Did;

/// The record-subject AT-URI used in non-local pollution-safe POSTs.
pub const CONFORMANCE_REPORT_SUBJECT_URI: &str =
    "at://did:plc:bvdrfwiamgi5leqs63q2duro/app.bsky.feed.post/3mjxxrzqtwc2d";

/// The record-subject CID for `CONFORMANCE_REPORT_SUBJECT_URI`.
pub const CONFORMANCE_REPORT_SUBJECT_CID: &str =
    "bafyreigvtlsnrkzac53uluemkc7p7345a2yqa2ct5lg6vglmvfakq4kkxq";

/// Choose the `reasonType` for a committing POST.
///
/// Returns the full NSID string (e.g.,
/// `"com.atproto.moderation.defs#reasonOther"`). Preconditions:
/// `advertised` is non-empty (the stage's contract-published gate
/// guarantees this).
pub fn choose_reason_type(advertised: &[String], is_local: bool) -> String {
    let prefer_other = "com.atproto.moderation.defs#reasonOther";
    if !is_local && advertised.iter().any(|r| r == prefer_other) {
        return prefer_other.to_string();
    }
    advertised
        .first()
        .cloned()
        .unwrap_or_else(|| prefer_other.to_string())
}

/// Choose the `subject` JSON for a committing POST.
///
/// - `advertised_types`: non-empty (contract-published guarantees).
/// - `reporter_did`: the self-mint DID (for the "own reporter" fallback).
/// - `override_did`: if `Some`, always use an `account` subject with this
///   DID (for `--report-subject-did`).
/// - `is_local`: local labelers use the simplest shape for debugging.
pub fn choose_subject(
    advertised_types: &[String],
    reporter_did: &Did,
    override_did: Option<&Did>,
    is_local: bool,
) -> Value {
    if let Some(did) = override_did {
        return json!({
            "$type": "com.atproto.admin.defs#repoRef",
            "did": did.0,
        });
    }
    if !is_local && advertised_types.iter().any(|s| s == "record") {
        return json!({
            "$type": "com.atproto.repo.strongRef",
            "uri": CONFORMANCE_REPORT_SUBJECT_URI,
            "cid": CONFORMANCE_REPORT_SUBJECT_CID,
        });
    }
    // Local, override absent, or `record` not advertised → account subject
    // pointing at the reporter's own DID (always safe).
    json!({
        "$type": "com.atproto.admin.defs#repoRef",
        "did": reporter_did.0,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn choose_reason_type_prefers_other_when_advertised_and_non_local() {
        let advertised = vec![
            "com.atproto.moderation.defs#reasonSpam".to_string(),
            "com.atproto.moderation.defs#reasonOther".to_string(),
        ];
        assert_eq!(
            choose_reason_type(&advertised, false),
            "com.atproto.moderation.defs#reasonOther"
        );
    }

    #[test]
    fn choose_reason_type_uses_lex_first_when_local() {
        let advertised = vec![
            "com.atproto.moderation.defs#reasonSpam".to_string(),
            "com.atproto.moderation.defs#reasonOther".to_string(),
        ];
        assert_eq!(
            choose_reason_type(&advertised, true),
            "com.atproto.moderation.defs#reasonSpam"
        );
    }

    #[test]
    fn choose_reason_type_falls_back_to_first_when_other_absent() {
        let advertised = vec!["com.atproto.moderation.defs#reasonSpam".to_string()];
        assert_eq!(
            choose_reason_type(&advertised, false),
            "com.atproto.moderation.defs#reasonSpam"
        );
    }

    #[test]
    fn choose_subject_local_returns_account_on_reporter() {
        let reporter = Did("did:web:127.0.0.1%3A5000".to_string());
        let subj = choose_subject(
            &["account".to_string(), "record".to_string()],
            &reporter,
            None,
            true,
        );
        assert_eq!(subj["$type"], "com.atproto.admin.defs#repoRef");
        assert_eq!(subj["did"], reporter.0);
    }

    #[test]
    fn choose_subject_non_local_prefers_record_when_advertised() {
        let reporter = Did("did:web:127.0.0.1%3A5000".to_string());
        let subj = choose_subject(
            &["account".to_string(), "record".to_string()],
            &reporter,
            None,
            false,
        );
        assert_eq!(subj["$type"], "com.atproto.repo.strongRef");
        assert_eq!(subj["uri"], CONFORMANCE_REPORT_SUBJECT_URI);
    }

    #[test]
    fn choose_subject_non_local_falls_back_to_account_when_record_absent() {
        let reporter = Did("did:web:127.0.0.1%3A5000".to_string());
        let subj = choose_subject(&["account".to_string()], &reporter, None, false);
        assert_eq!(subj["$type"], "com.atproto.admin.defs#repoRef");
        assert_eq!(subj["did"], reporter.0);
    }

    #[test]
    fn choose_subject_override_always_wins() {
        let reporter = Did("did:web:127.0.0.1%3A5000".to_string());
        let override_did = Did("did:plc:target".to_string());
        let subj = choose_subject(
            &["record".to_string()],
            &reporter,
            Some(&override_did),
            false, // non-local; without override this would be record/strongRef
        );
        assert_eq!(subj["$type"], "com.atproto.admin.defs#repoRef");
        assert_eq!(subj["did"], "did:plc:target");
    }
}