rho-coding-agent 1.25.1

A lightweight agent harness inspired by Pi
Documentation
use pretty_assertions::assert_eq;

use rho_sdk::hooks::HookDecision;

use super::*;

#[test]
fn a_versioned_continue_parses() {
    assert_eq!(
        parse_decision(br#"{"version":1,"decision":"continue"}"#),
        Ok(HookDecision::Continue)
    );
}

#[test]
fn a_versioned_deny_carries_its_reason() {
    assert_eq!(
        parse_decision(br#"{"version":1,"decision":"deny","reason":"force push"}"#),
        Ok(HookDecision::Deny {
            reason: "force push".into()
        })
    );
}

#[test]
fn surrounding_whitespace_is_tolerated() {
    assert_eq!(
        parse_decision(b"\n  {\"version\":1,\"decision\":\"continue\"}  \n"),
        Ok(HookDecision::Continue)
    );
}

#[test]
fn unknown_fields_are_ignored_so_newer_handlers_keep_working() {
    assert_eq!(
        parse_decision(br#"{"version":1,"decision":"continue","hint":"future"}"#),
        Ok(HookDecision::Continue)
    );
}

#[test]
fn a_future_schema_version_is_not_read_as_consent() {
    assert_eq!(
        parse_decision(br#"{"version":2,"decision":"continue"}"#),
        Err(DecisionError::SchemaMismatch { found: 2 })
    );
}

#[test]
fn empty_output_is_not_read_as_consent() {
    assert_eq!(parse_decision(b""), Err(DecisionError::Empty));
    assert_eq!(parse_decision(b"   \n"), Err(DecisionError::Empty));
}

#[test]
fn malformed_json_is_reported_rather_than_guessed() {
    assert!(matches!(
        parse_decision(b"{not json"),
        Err(DecisionError::Malformed(_))
    ));
    assert!(matches!(
        parse_decision(b"continue"),
        Err(DecisionError::Malformed(_))
    ));
}

#[test]
fn an_unknown_decision_word_is_rejected() {
    assert_eq!(
        parse_decision(br#"{"version":1,"decision":"allow"}"#),
        Err(DecisionError::UnknownDecision("allow".into()))
    );
}

#[test]
fn a_denial_without_a_reason_is_rejected() {
    assert_eq!(
        parse_decision(br#"{"version":1,"decision":"deny"}"#),
        Err(DecisionError::MissingReason)
    );
    assert_eq!(
        parse_decision(br#"{"version":1,"decision":"deny","reason":"  "}"#),
        Err(DecisionError::MissingReason)
    );
}

#[test]
fn oversized_output_is_refused_before_parsing() {
    let payload = vec![b'x'; MAX_DECISION_BYTES + 1];

    assert_eq!(parse_decision(&payload), Err(DecisionError::TooLarge));
}

#[test]
fn a_long_denial_reason_is_shortened_on_a_character_boundary() {
    let reason = "\u{00e9}".repeat(2000);
    let payload = serde_json::json!({"version": 1, "decision": "deny", "reason": reason});

    let decision = parse_decision(payload.to_string().as_bytes()).unwrap();

    let HookDecision::Deny { reason } = decision else {
        panic!("expected a denial");
    };
    assert!(reason.len() <= MAX_REASON_BYTES);
    assert!(reason.chars().all(|character| character == '\u{00e9}'));
}

#[test]
fn invalid_utf8_does_not_panic_and_does_not_parse() {
    assert!(matches!(
        parse_decision(&[0xff, 0xfe, 0xfd]),
        Err(DecisionError::Malformed(_))
    ));
}