Merge pull request 'feat(sidecar): protocol 14 — the console command run forward (PLAN_REDESIGNS §16)' (#30) from feat/command-steps into edge

Reviewed-on: #30
This commit is contained in:
2026-10-16 07:47:50 +00:00
2 changed files with 56 additions and 0 deletions

View File

@@ -208,6 +208,11 @@ use tracing_subscriber::EnvFilter;
/// retry never places a second; the crate is kept when the run ends (D373). `gc.started`,
/// `gc.ended` and `gc.failed` ride the feed like any other event.
///
/// Console commands as event steps (§20.11, the module's PLAN_REDESIGNS.md §16) add one forward,
/// `POST /commands/run`: the commands a step's author typed, run once or once per online
/// recipient. The plugin remembers the key so nothing runs twice; this process keeps nothing and
/// knows nothing of what a command does.
///
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs,
/// §16 the rewards, §17 the map, §18 the optional mods, §19 the walk's fixes, §20 the step editor;

View File

@@ -131,6 +131,10 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
// Protocol 14 (§20.9): place one guarded crate. The plugin remembers the key; the crate is
// kept when the run ends (Guarded Crate cannot end one alone), so nothing is kept here.
.route("/gc/place", post(gc_place))
// Protocol 14 (§20.11): console commands a step's author typed. The plugin keeps the key
// so nothing runs twice, fills the placeholders and decides who is online; this process
// moves the line and knows nothing of what any command does (no allowlist, D384).
.route("/commands/run", post(commands_run))
// Protocol 10 (§29): who took part, what they may redeem, and a line in chat. The tally
// is counted and held by the plugin, the kit catalogue is Kits' own, and whether a chat
// line was already said is the plugin's memory. These move lines, like everything above.
@@ -583,6 +587,16 @@ async fn gc_place(State(st): State<AppState>, Json(body): Json<Value>) -> Respon
forward_object(&st, body, "gc.place", "a guarded crate placement").await
}
/// Run the console commands a step's author typed (protocol 14): once, or once per online
/// recipient. `command.ran` (how many ran, the console's replies, who was skipped as offline, and
/// `repeat` for a key already seen) or `command.error` with a reason, `unknown-command` among
/// them. A step's `teardown` rides its run and the plugin keeps it with the key; `teardownOf`
/// asks for the teardown kept under another key (D385). The plugin remembers the key, so nothing
/// is remembered here.
async fn commands_run(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "command.run", "a console command run").await
}
/// Start counting who takes part in a run. `tally.ok` or `tally.error` with a reason.
async fn tally_open(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "tally.open", "a tally").await
@@ -1346,6 +1360,43 @@ mod tests {
assert!(stamp(json!("Elite"), "gc.place", "r-63").is_none());
}
/// Protocol 14, console commands: a run is stamped `command.run` whatever its body names, so
/// the route runs commands and nothing else, and its key, commands and recipients pass through
/// byte for byte; the placeholders are the plugin's to fill.
#[test]
fn protocol_14_command_run_is_stamped() {
let body = json!({ "cmd": "gc.place", "reqId": "theirs", "key": "12:5",
"commands": ["sr add {steamid} 100", "say \"{name} won\""],
"recipients": [{ "steamId": "76561198000000001", "name": "Some \"One\"" }] });
let stamped = stamp(body, "command.run", "r-64").expect("an object is stamped");
assert_eq!(stamped["cmd"], "command.run");
assert_eq!(stamped["reqId"], "r-64");
assert_eq!(stamped["key"], "12:5");
assert_eq!(stamped["commands"][0], "sr add {steamid} 100");
assert_eq!(stamped["commands"][1], "say \"{name} won\"");
assert_eq!(stamped["recipients"][0]["name"], "Some \"One\"");
assert!(stamp(json!(["rbe"]), "command.run", "r-65").is_none());
// D385: a step's teardown rides its run, and a teardown asked for by key alone names the
// run it is for; both are the plugin's to read.
let run = stamp(
json!({ "key": "12:6", "commands": ["rbe 1"], "teardown": ["rbe stop"] }),
"command.run",
"r-66",
)
.expect("an object is stamped");
assert_eq!(run["teardown"][0], "rbe stop");
let teardown = stamp(
json!({ "key": "12:6:teardown", "teardownOf": "12:6" }),
"command.run",
"r-67",
)
.expect("an object is stamped");
assert_eq!(teardown["cmd"], "command.run");
assert_eq!(teardown["teardownOf"], "12:6");
}
/// Protocol 12: a title set is forwarded whole. The markup inside each `text` is the module's
/// and the plugin's business, so it must arrive byte for byte as the website composed it.
#[test]