From 291fa97c3caf8f32ca8962e3c8b30dff1310d4af Mon Sep 17 00:00:00 2001 From: Justin <9146678+brickfrog@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:38:02 -0500 Subject: [PATCH] fix: thread cached merge gate inputs --- src/server/handler_disconnect.mbt | 94 +++++++++++++++++++++++++++- src/server/handler_poll_delivery.mbt | 28 +++++++++ src/server/state.mbt | 3 + src/server/state_test.mbt | 15 +++++ 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/server/handler_disconnect.mbt b/src/server/handler_disconnect.mbt index 592b8040..0242e879 100644 --- a/src/server/handler_disconnect.mbt +++ b/src/server/handler_disconnect.mbt @@ -740,13 +740,14 @@ fn ServerState::leaf_merge_gate_ready_for_agent( self.config.pr_policy.reviewer, self.config.pr_policy.skip_copilot_on_feature_branch, tracked.parent_branch, - "", + self.cached_default_branch, ) if @poller.tl_tracked_pr_merge_gate_snapshot_ready( tracked, unresolved_threads, review_mode=tracked.review_mode, review_required~, + audit_receipt_state=tracked.audit_receipt_state, ) { return true } @@ -755,6 +756,97 @@ fn ServerState::leaf_merge_gate_ready_for_agent( false } +///| +test "leaf merge gate threads audit state and cached default branch" { + let make_state = fn( + label : String, + audit_receipt_state : @types.AuditReceiptState, + review : @poller.ReviewState, + ) -> ServerState { + let project_dir = "/tmp/choir-leaf-merge-gate-" + + label + + "-" + + @sys.get_pid().to_string() + + "-" + + @env.now().reinterpret_as_int64().to_string() + ignore(@sys.create_dir_all(project_dir + "/.choir/state/kv")) + let state = ServerState::new( + { + ..@types.Config::default(), + project_dir, + pr_policy: { + ..@types.PrPolicy::default(), + reviewer: @types.Reviewer::Copilot, + }, + }, + make_hermetic_recovery_provider(), + ) + let agent_id = "main.leaf-merge-gate" + state.poller.track( + 901, agent_id, "feature/leaf", "feature/base", "https://example.test/pr/901", + 0L, + ) + match state.poller.get(901) { + Some(tracked) => + state.poller.restore({ + ..tracked, + last_review_state: review, + last_ci_rollup: @poller.CiRollupStatus::Success, + audit_receipt_state, + unresolved_thread_ids: [], + unresolved_thread_ids_fetched: true, + }) + None => abort("expected tracked leaf PR") + } + state + } + + let pending = make_state( + "pending", + @types.AuditReceiptState::Pending(head_sha="head"), + @poller.ReviewState::Approved, + ) + assert_false(pending.leaf_merge_gate_ready_for_agent("main.leaf-merge-gate")) + ignore(@sys.rm_rf(pending.config.project_dir)) + + let findings = make_state( + "findings", + @types.AuditReceiptState::FindingsRemain(head_sha="head", count=2), + @poller.ReviewState::Approved, + ) + assert_false(findings.leaf_merge_gate_ready_for_agent("main.leaf-merge-gate")) + ignore(@sys.rm_rf(findings.config.project_dir)) + + let valid = make_state( + "valid", + @types.AuditReceiptState::Valid, + @poller.ReviewState::Approved, + ) + assert_true(valid.leaf_merge_gate_ready_for_agent("main.leaf-merge-gate")) + ignore(@sys.rm_rf(valid.config.project_dir)) + + let not_required = make_state( + "not-required", + @types.AuditReceiptState::NotRequired, + @poller.ReviewState::Approved, + ) + assert_true( + not_required.leaf_merge_gate_ready_for_agent("main.leaf-merge-gate"), + ) + ignore(@sys.rm_rf(not_required.config.project_dir)) + + let default_branch = make_state( + "default-branch", + @types.AuditReceiptState::NotRequired, + @poller.ReviewState::None_, + ) + assert_eq(default_branch.cached_default_branch, "main") + assert_true( + default_branch.leaf_merge_gate_ready_for_agent("main.leaf-merge-gate"), + ) + ignore(@sys.rm_rf(default_branch.config.project_dir)) +} + ///| fn worker_no_handoff_terminal_process( lifecycle : @phase.AgentLifecycle, diff --git a/src/server/handler_poll_delivery.mbt b/src/server/handler_poll_delivery.mbt index 4198d5b4..55e71cb4 100644 --- a/src/server/handler_poll_delivery.mbt +++ b/src/server/handler_poll_delivery.mbt @@ -2333,6 +2333,7 @@ async fn ServerState::refresh_tracked_pr_audit_receipt_states( self.config.project_dir, capture, ) + self.cached_default_branch = default_branch for tracked in tracked_prs { match self.poller.get(tracked.pr_number) { Some(current) => { @@ -2353,6 +2354,33 @@ async fn ServerState::refresh_tracked_pr_audit_receipt_states( } } +///| +async test "poller audit refresh caches detected default branch" { + let project_dir = "/tmp/choir-tick-default-branch-" + + @sys.get_pid().to_string() + + "-" + + @env.now().reinterpret_as_int64().to_string() + ignore(@sys.create_dir_all(project_dir + "/.choir/state/kv")) + let state = ServerState::new( + { ..@types.Config::default(), project_dir, }, + make_hermetic_recovery_provider(), + ) + state.poller.track( + 902, "main.leaf-tick-default", "feature/leaf", "feature/base", "https://example.test/pr/902", + 0L, + ) + state.refresh_tracked_pr_audit_receipt_states( + async fn(_cmd) { + @async.sleep(0) + (0, "origin/develop\n") + }, + fn(_) { false }, + fn(_) { "" }, + ) + assert_eq(state.cached_default_branch, "develop") + ignore(@sys.rm_rf(project_dir)) +} + ///| async fn deliver_pending_poll_events( state : ServerState, diff --git a/src/server/state.mbt b/src/server/state.mbt index 927aeda2..da51b415 100644 --- a/src/server/state.mbt +++ b/src/server/state.mbt @@ -306,6 +306,8 @@ pub struct ServerState { poller : @poller.PollerState kv_store : @kv.KvStore run_id : String + // Synchronous watchdog gate cache; populated by the poller tick, main before its first tick. + mut cached_default_branch : String pending_disconnects : Map[String, Int64] agent_pids : Map[String, Int] disconnect_contexts : Map[String, DisconnectContext] @@ -446,6 +448,7 @@ pub fn ServerState::new( poller, kv_store: @kv.KvStore::new(config.project_dir), run_id, + cached_default_branch: "main", pending_disconnects: {}, agent_pids: {}, disconnect_contexts: {}, diff --git a/src/server/state_test.mbt b/src/server/state_test.mbt index 94edb2f3..26ae2033 100644 --- a/src/server/state_test.mbt +++ b/src/server/state_test.mbt @@ -1,3 +1,18 @@ +///| +test "ServerState::new defaults cached default branch to main" { + let project_dir = "/tmp/choir-cached-default-branch-init-" + + @sys.get_pid().to_string() + + "-" + + @env.now().reinterpret_as_int64().to_string() + ignore(@sys.create_dir_all(project_dir + "/.choir/state/kv")) + let state = ServerState::new( + { ..@types.Config::default(), project_dir, }, + make_hermetic_recovery_provider(), + ) + assert_eq(state.cached_default_branch, "main") + ignore(@sys.rm_rf(project_dir)) +} + ///| test "register_agent_pid + forget_agent_pid" { let project_dir = "/tmp/choir-agent-pid-state-" +