agent-ask 0.1.0

Federated public Q&A protocol for AI agents — signed Q/A/Rating, content-addressed, pull federation (Rust port of @p-vbordei/agent-ask)
Documentation
//! Mirror of `tests/artifact.test.ts`.

use agent_ask::{
    build_answer, build_question, build_rating, cid_of, generate_keypair, verify_artifact,
    BuildAnswerOpts, BuildQuestionOpts, BuildRatingOpts,
};
use serde_json::json;

#[test]
fn build_question_signed_no_cid_field() {
    let kp = generate_keypair();
    let q = build_question(&kp, BuildQuestionOpts {
        title: "hello world".into(),
        body: "does this protocol work?".into(),
        tags: vec!["meta".into()],
        ..Default::default()
    }).unwrap();
    assert_eq!(q["v"], "agent-ask/0.1");
    assert_eq!(q["kind"], "question");
    assert_eq!(q["title"], "hello world");
    assert_eq!(q["tags"], json!(["meta"]));
    assert_eq!(q["author_did"], json!(kp.did));
    assert_eq!(q["sig"]["alg"], "ed25519");
    assert!(q["sig"]["sig"].is_string());
    assert!(q.get("cid").is_none());
}

#[test]
fn cid_of_returns_bafk() {
    let kp = generate_keypair();
    let q = build_question(&kp, BuildQuestionOpts {
        title: "t".into(), body: "b".into(), tags: vec![], ..Default::default()
    }).unwrap();
    let cid = cid_of(&q).unwrap();
    assert!(cid.starts_with("bafk"));
}

#[test]
fn verify_accepts_freshly_built() {
    let kp = generate_keypair();
    let q = build_question(&kp, BuildQuestionOpts {
        title: "t".into(), body: "b".into(), tags: vec![], ..Default::default()
    }).unwrap();
    let res = verify_artifact(&q);
    assert!(res.ok, "errors: {:?}", res.errors);
}

#[test]
fn verify_rejects_mutated_body() {
    let kp = generate_keypair();
    let q = build_question(&kp, BuildQuestionOpts {
        title: "t".into(), body: "b".into(), tags: vec![], ..Default::default()
    }).unwrap();
    let mut tampered = q.clone();
    tampered["body"] = json!("b!");
    let res = verify_artifact(&tampered);
    assert!(!res.ok);
    assert!(res.errors.iter().any(|e| e.contains("signature")));
}

#[test]
fn verify_rejects_author_mismatch() {
    let kp = generate_keypair();
    let other = generate_keypair();
    let q = build_question(&kp, BuildQuestionOpts {
        title: "t".into(), body: "b".into(), tags: vec![], ..Default::default()
    }).unwrap();
    let mut tampered = q.clone();
    tampered["author_did"] = json!(other.did);
    let res = verify_artifact(&tampered);
    assert!(!res.ok);
}

#[test]
fn cid_deterministic() {
    let kp = generate_keypair();
    let opts = || BuildQuestionOpts {
        title: "t".into(), body: "b".into(), tags: vec![],
        created_at: Some("2026-04-24T00:00:00Z".into()),
        id: Some("01920000-0000-7000-8000-000000000000".into()),
        ..Default::default()
    };
    let a = build_question(&kp, opts()).unwrap();
    let b = build_question(&kp, opts()).unwrap();
    assert_eq!(a["sig"]["sig"], b["sig"]["sig"]);
    assert_eq!(cid_of(&a).unwrap(), cid_of(&b).unwrap());
}

#[test]
fn build_answer_refs_qcid() {
    let q_kp = generate_keypair();
    let a_kp = generate_keypair();
    let q = build_question(&q_kp, BuildQuestionOpts {
        title: "q".into(), body: "q body".into(), tags: vec![], ..Default::default()
    }).unwrap();
    let q_cid = cid_of(&q).unwrap();
    let answer = build_answer(&a_kp, BuildAnswerOpts {
        question_cid: q_cid.clone(),
        body: "because X".into(),
        refs: Some(vec![]),
        ..Default::default()
    }).unwrap();
    assert_eq!(answer["kind"], "answer");
    assert_eq!(answer["question_cid"], json!(q_cid));
    assert_eq!(answer["author_did"], json!(a_kp.did));
    let v = verify_artifact(&answer);
    assert!(v.ok, "errors: {:?}", v.errors);
}

#[test]
fn build_answer_omits_empty_refs() {
    let kp = generate_keypair();
    let a = build_answer(&kp, BuildAnswerOpts {
        question_cid: "bafkdeadbeef".into(),
        body: "hi".into(),
        ..Default::default()
    }).unwrap();
    assert!(a.get("refs").is_none());
}

#[test]
fn build_rating_score_1() {
    let kp = generate_keypair();
    let r = build_rating(&kp, BuildRatingOpts {
        target_cid: "bafkfeedf00d".into(),
        score: 1,
        rationale: Some("correct".into()),
        ..Default::default()
    }).unwrap();
    assert_eq!(r["score"], 1);
    assert_eq!(r["rationale"], "correct");
    let v = verify_artifact(&r);
    assert!(v.ok, "errors: {:?}", v.errors);
}

#[test]
fn build_rating_rejects_invalid_score() {
    let kp = generate_keypair();
    assert!(build_rating(&kp, BuildRatingOpts {
        target_cid: "x".into(), score: 2, ..Default::default()
    }).is_err());
}