Plan-mode review: approve/reject/revise goals & to-dos before execute (#4105)

This commit is contained in:
Steven Enamakel
2026-06-25 09:24:02 -07:00
committed by GitHub
parent 25e6ae96f3
commit 96437120e5
44 changed files with 1597 additions and 63 deletions
+134
View File
@@ -2519,6 +2519,140 @@ async fn json_rpc_todos_crud_on_personal_board() {
rpc_join.abort();
}
#[tokio::test]
async fn json_rpc_todos_revise_plan_rejects_awaiting() {
// Plan-mode "Send feedback" path: a parked plan (card awaiting approval) is
// cleared by `todos_revise_plan` so the orchestrator can re-plan from the
// user's feedback. Awaiting cards become `rejected`; non-awaiting cards are
// left untouched.
let _env_lock = json_rpc_e2e_env_lock();
let tmp = tempdir().expect("tempdir");
let home = tmp.path();
let openhuman_home = home.join(".openhuman");
let _home_guard = EnvVarGuard::set_to_path("HOME", home);
let _workspace_guard = EnvVarGuard::unset("OPENHUMAN_WORKSPACE");
let _backend_url_guard = EnvVarGuard::unset("BACKEND_URL");
let _vite_backend_url_guard = EnvVarGuard::unset("VITE_BACKEND_URL");
let _api_url_guard = EnvVarGuard::unset("OPENHUMAN_API_URL");
let (api_addr, api_join) = serve_on_ephemeral(mock_upstream_router()).await;
let api_origin = format!("http://{api_addr}");
write_min_config(openhuman_home.as_path(), &api_origin);
let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
let rpc_base = format!("http://{rpc_addr}");
let board = "plan-review-thread";
// 1. Add a card and park it for review (awaiting_approval).
let add = post_json_rpc(
&rpc_base,
9201,
"openhuman.todos_add",
json!({ "thread_id": board, "content": "Refactor schema", "status": "awaiting_approval" }),
)
.await;
let add_result = assert_no_jsonrpc_error(&add, "todos_add awaiting");
let card_id = add_result
.get("cards")
.and_then(Value::as_array)
.and_then(|c| c.first())
.and_then(|c| c.get("id"))
.and_then(Value::as_str)
.expect("parked card id")
.to_string();
// 2. Send feedback → revise_plan rejects the parked card.
let revise = post_json_rpc(
&rpc_base,
9202,
"openhuman.todos_revise_plan",
json!({ "thread_id": board, "feedback": "split into smaller steps" }),
)
.await;
let revise_result = assert_no_jsonrpc_error(&revise, "todos_revise_plan");
let revised_cards = revise_result
.get("cards")
.and_then(Value::as_array)
.expect("cards in revise response");
assert_eq!(revised_cards.len(), 1, "card retained, status changed");
assert_eq!(
revised_cards[0].get("id").and_then(Value::as_str),
Some(card_id.as_str()),
"same card id"
);
assert_eq!(
revised_cards[0].get("status").and_then(Value::as_str),
Some("rejected"),
"parked card is rejected after revise_plan"
);
api_join.abort();
rpc_join.abort();
}
#[tokio::test]
async fn json_rpc_plan_review_decide_unknown_and_invalid() {
// The plan-review gate is in-memory and parks a live turn; over RPC we can
// still exercise the decision surface: deciding an unknown/expired request
// resolves nothing (`resolved: false`), and an invalid decision errors.
let _env_lock = json_rpc_e2e_env_lock();
let tmp = tempdir().expect("tempdir");
let home = tmp.path();
let openhuman_home = home.join(".openhuman");
let _home_guard = EnvVarGuard::set_to_path("HOME", home);
let _workspace_guard = EnvVarGuard::unset("OPENHUMAN_WORKSPACE");
let _backend_url_guard = EnvVarGuard::unset("BACKEND_URL");
let _vite_backend_url_guard = EnvVarGuard::unset("VITE_BACKEND_URL");
let _api_url_guard = EnvVarGuard::unset("OPENHUMAN_API_URL");
let (api_addr, api_join) = serve_on_ephemeral(mock_upstream_router()).await;
let api_origin = format!("http://{api_addr}");
write_min_config(openhuman_home.as_path(), &api_origin);
let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
let rpc_base = format!("http://{rpc_addr}");
// Unknown request id → resolved:false (no parked turn to wake).
let unknown = post_json_rpc(
&rpc_base,
9301,
"openhuman.plan_review_decide",
json!({ "request_id": "plan-does-not-exist", "decision": "approve" }),
)
.await;
let unknown_result = assert_no_jsonrpc_error(&unknown, "plan_review_decide unknown");
assert_eq!(
unknown_result.get("resolved").and_then(Value::as_bool),
Some(false),
"deciding an unknown request resolves nothing"
);
// revise without feedback → error.
let bad_revise = post_json_rpc(
&rpc_base,
9302,
"openhuman.plan_review_decide",
json!({ "request_id": "plan-x", "decision": "revise" }),
)
.await;
assert_jsonrpc_error(&bad_revise, "plan_review_decide revise w/o feedback");
// Unknown decision verb → error.
let bad_decision = post_json_rpc(
&rpc_base,
9303,
"openhuman.plan_review_decide",
json!({ "request_id": "plan-x", "decision": "maybe" }),
)
.await;
assert_jsonrpc_error(&bad_decision, "plan_review_decide invalid decision");
api_join.abort();
rpc_join.abort();
}
#[tokio::test]
async fn json_rpc_thread_goal_lifecycle() {
let _env_lock = json_rpc_e2e_env_lock();