Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions specs/port/trios/crates/trios-bridge/src/relay.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// HTTP relay with SSE — Comet ↔ agent real-time communication.
// Port of gHashTag/trios:crates/trios-bridge/src/relay.rs
// (at 8b229e9489ee, 183 lines) — Roadmap Stage 6, goal #4548, target
// t27c gen-rust.
//
// The original exposes four HTTP endpoints over the bridge: GET
// /events streams every agent message to connected sidepanels over
// SSE (a lagged broadcast message is skipped, not fatal, and the
// stream keeps alive); GET /agents answers the registered agents as
// JSON; POST /dispatch takes the request body — agent_id (the
// literal "broadcast" fans the task to everyone; a specific agent
// must be registered, and when both the issue number and the branch
// name arrive, the router claims the issue first, then the agent's
// status moves to Working with the task as the message, and the
// command rides the broadcast) and answers ok with the dispatched or
// broadcast confirmation, or the not-registered/broadcast-failure
// errors; and GET /health answers the ok status with the agent count.
//
// The HTTP server, the SSE transport and the router state are
// plumbing; the decisions — the four-route table, the lag-skip rule,
// the broadcast literal, the registration gate, the claim-then-status
// ordering, and both response shapes — are ported here as pure
// functions and tested (issue #8684, "Port the decision, not the
// plumbing").

module port::trios_bridge_relay;

// ---------------------------------------------------------------------------
// Original lines 62-69: `relay_app` — the four-route table.
// ---------------------------------------------------------------------------

pub fn relay_route_count() -> usize {
return 4;
}

pub fn relay_route(i: usize) -> str {
if (i == 0) { return "/events"; }
if (i == 1) { return "/agents"; }
if (i == 2) { return "/dispatch"; }
if (i == 3) { return "/health"; }
return "";
}

pub fn relay_route_method(i: usize) -> str {
if (i == 0) { return "get"; }
if (i == 1) { return "get"; }
if (i == 2) { return "post"; }
if (i == 3) { return "get"; }
return "";
}

pub fn relay_app(routes_mouted: usize) -> usize {
return routes_mouted;
}

// ---------------------------------------------------------------------------
// Original lines 72-87: `serve_relay` — the HTTP server starts beside
// the WebSocket bridge; a pure transport.
// ---------------------------------------------------------------------------

// Original lines 72-87: `pub async fn serve_relay(server, addr)` —
// the HTTP server starts beside the WebSocket bridge and serves the
// relay app; a pure transport, with one decision worth keeping: the
// relay runs ALONGSIDE the bridge, never replacing it.
pub fn serve_relay(bridge_running: bool) -> bool {
return bridge_running;
}

// ---------------------------------------------------------------------------
// Original lines 96-109: `sse_events` — the sidepanel stream: a
// lagged broadcast message is skipped, not fatal; the stream keeps
// alive.
// ---------------------------------------------------------------------------

pub fn sse_events(lagged: bool) -> str {
if (lagged) {
return "skip";
}
return "event";
}

pub fn sse_keep_alive() -> bool {
return true;
}

// ---------------------------------------------------------------------------
// Original lines 112-115: `list_agents` — the registered agents as
// JSON.
// ---------------------------------------------------------------------------

pub fn list_agents(registered: usize) -> usize {
return registered;
}

// ---------------------------------------------------------------------------
// Original lines 118-174: `dispatch_task` — the dispatch decisions:
// the broadcast literal fans to everyone; a specific agent must be
// registered; with both the issue and the branch, the router claims
// the issue first, then the status moves to Working with the task as
// the message; the command rides the broadcast either way.
// ---------------------------------------------------------------------------

pub fn dispatch_task(agent_id: str, registered: bool, issue_present: bool, branch_present: bool) -> str {
if (agent_id == "broadcast") {
return "broadcast_to_all";
}
if (!registered) {
return "not registered";
}
if (issue_present and branch_present) {
return "claim_then_status_then_command";
}
return "status_then_command";
}

pub fn dispatch_broadcast_ok_message() -> str {
return "Task broadcast to all agents";
}

pub fn dispatch_requires_both_issue_and_branch(issue_present: bool, branch_present: bool) -> bool {
return issue_present and branch_present;
}

pub fn dispatch_status_moves_to_working() -> str {
return "working";
}

// ---------------------------------------------------------------------------
// Original lines 177-183: `health_check` — the ok status with the
// agent count.
// ---------------------------------------------------------------------------

pub fn health_check(agents_count: usize) -> str {
return "ok";
}

pub fn health_status_value() -> str {
return "ok";
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original.
// ---------------------------------------------------------------------------

test route_table {
assert(relay_route_count() == 4);
assert(relay_route(0) == "/events");
assert(relay_route(1) == "/agents");
assert(relay_route(2) == "/dispatch");
assert(relay_route(3) == "/health");
assert(relay_route_method(0) == "get");
assert(relay_route_method(2) == "post");
assert(relay_route_method(3) == "get");
assert(relay_app(4) == 4);
}

test serve_runs_alongside_the_bridge {
assert(serve_relay(true));
assert(!serve_relay(false));
}

test sse_skips_lag_keeps_alive {
assert(sse_events(true) == "skip");
assert(sse_events(false) == "event");
assert(sse_keep_alive());
}

test list_answers_the_registered {
assert(list_agents(3) == 3);
assert(list_agents(0) == 0);
}

test dispatch_broadcast_literal {
assert(dispatch_task("broadcast", false, false, false) == "broadcast_to_all");
assert(dispatch_broadcast_ok_message() == "Task broadcast to all agents");
}

test dispatch_registration_gate {
assert(dispatch_task("agent-7", false, false, false) == "not registered");
assert(dispatch_task("agent-7", true, false, false) == "status_then_command");
}

test dispatch_claim_requires_both {
assert(dispatch_requires_both_issue_and_branch(true, true));
assert(!dispatch_requires_both_issue_and_branch(true, false));
assert(!dispatch_requires_both_issue_and_branch(false, true));
assert(dispatch_task("agent-7", true, true, true) == "claim_then_status_then_command");
assert(dispatch_task("agent-7", true, true, false) == "status_then_command");
assert(dispatch_status_moves_to_working() == "working");
}

test health_answers_ok_with_count {
assert(health_check(5) == "ok");
assert(health_check(0) == "ok");
assert(health_status_value() == "ok");
}
Loading