Skip to content

Commit b7066a9

Browse files
committed
tests: consensus round-trip + self-governance routes
End-to-end §11 verification through the real executor: a destructive task opens a proposal instead of executing, the pending-vote guard blocks duplicates, unanimous approval mints the one-shot permit, and the connector runs exactly once with the result on the blackboard. API integration tests for propose-intent and cast-vote routes, including 400s on malformed bodies and ids.
1 parent 4543aa7 commit b7066a9

3 files changed

Lines changed: 434 additions & 1 deletion

File tree

apps/springtaled/tests/api_integration.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,3 +646,80 @@ async fn test_cycle_autonomy() {
646646
assert_eq!(status, StatusCode::OK);
647647
assert!(json["level"].is_string());
648648
}
649+
650+
/// §5.5 formation self-governance routes — propose-intent enqueues the
651+
/// Fever-gated consensus command; cast_vote validates ids before
652+
/// enqueueing a ballot.
653+
#[tokio::test]
654+
async fn test_propose_intent_and_cast_vote_routes() {
655+
let (router, token) = build_test_app(true);
656+
657+
// Create a formation to address.
658+
let body = serde_json::json!({
659+
"name": "Governance Squad",
660+
"intent": "Reconnoiter",
661+
"guard_mode": false,
662+
"agents": [{
663+
"connector_name": "connector-test",
664+
"trigger_name": "event_received",
665+
"action_connector": "connector-test",
666+
"action_name": "send_message"
667+
}]
668+
});
669+
let req = Request::post("/formations/deploy-team")
670+
.header("Authorization", format!("Bearer {token}"))
671+
.header("Content-Type", "application/json")
672+
.body(Body::from(serde_json::to_vec(&body).unwrap()))
673+
.unwrap();
674+
let (status, json) = send(router.clone(), req).await;
675+
assert_eq!(status, StatusCode::CREATED);
676+
let fid = json["formation_id"].as_str().unwrap().to_owned();
677+
678+
// Propose an intent change — the route enqueues the consensus
679+
// command (the Fever gate is enforced by the bot event loop).
680+
let req = Request::post(format!("/formations/{fid}/propose-intent"))
681+
.header("Authorization", format!("Bearer {token}"))
682+
.header("Content-Type", "application/json")
683+
.body(Body::from(
684+
serde_json::to_vec(&serde_json::json!({ "intent": "Execute" })).unwrap(),
685+
))
686+
.unwrap();
687+
let (status, json) = send(router.clone(), req).await;
688+
assert_eq!(status, StatusCode::OK);
689+
assert_eq!(json["proposed"], fid);
690+
691+
// Missing intent body → 400.
692+
let req = Request::post(format!("/formations/{fid}/propose-intent"))
693+
.header("Authorization", format!("Bearer {token}"))
694+
.header("Content-Type", "application/json")
695+
.body(Body::from(b"{}".to_vec()))
696+
.unwrap();
697+
let (status, _) = send(router.clone(), req).await;
698+
assert_eq!(status, StatusCode::BAD_REQUEST);
699+
700+
// Cast a ballot with well-formed ids → enqueued (200).
701+
let vote_id = uuid::Uuid::new_v4();
702+
let voter = uuid::Uuid::new_v4();
703+
let req = Request::post(format!("/formations/{fid}/votes/{vote_id}"))
704+
.header("Authorization", format!("Bearer {token}"))
705+
.header("Content-Type", "application/json")
706+
.body(Body::from(
707+
serde_json::to_vec(&serde_json::json!({ "voter": voter, "approve": true })).unwrap(),
708+
))
709+
.unwrap();
710+
let (status, json) = send(router.clone(), req).await;
711+
assert_eq!(status, StatusCode::OK);
712+
assert_eq!(json["voted"], vote_id.to_string());
713+
714+
// Malformed voter uuid → 400 from the runtime op's validation.
715+
let req = Request::post(format!("/formations/{fid}/votes/{vote_id}"))
716+
.header("Authorization", format!("Bearer {token}"))
717+
.header("Content-Type", "application/json")
718+
.body(Body::from(
719+
serde_json::to_vec(&serde_json::json!({ "voter": "not-a-uuid", "approve": true }))
720+
.unwrap(),
721+
))
722+
.unwrap();
723+
let (status, _) = send(router, req).await;
724+
assert_eq!(status, StatusCode::BAD_REQUEST);
725+
}

0 commit comments

Comments
 (0)