Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions src/bin/choir/init.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ fn init_test_tracked_pr(
mergeable: @poller.Mergeable::Unknown,
merge_state_status: @poller.MergeStateStatus::Unknown,
merged_at_unix: None,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen: false,
orphan_notified: false,
Expand Down
37 changes: 37 additions & 0 deletions src/bin/choir/statusline.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ fn statusline_test_tracked_pr(
mergeable: @poller.Mergeable::Unknown,
merge_state_status: @poller.MergeStateStatus::Unknown,
merged_at_unix: None,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen: true,
orphan_notified: false,
Expand Down Expand Up @@ -726,6 +727,7 @@ fn statusline_pr_token(
Some(0),
review_mode=tracked.review_mode,
review_required~,
audit_receipt_state=tracked.audit_receipt_state,
) {
return match merge_mode {
@poller.MergeOperatorMode::Manual => "\u001b[32m#\{n}M\u001b[0m"
Expand All @@ -743,6 +745,41 @@ fn statusline_pr_token(
}
}

///|
test "statusline_pr_summary gates merge-ready glyph on audit receipt state" {
let project_dir = "/tmp/choir-statusline-pr-audit-state-" +
@sys.get_pid().to_string() +
"-" +
@env.now().reinterpret_as_int64().to_string()
ignore(@sys.rm_rf(project_dir))
assert_true(@sys.create_dir_all(project_dir + "/.choir/state"))
ignore(
@sys.write_file_sync(
project_dir + "/.choir/config.toml",
"automerge = false\n",
),
)
let pending = {
..statusline_test_tracked_pr(84, "main.leaf-audit-pending"),
audit_receipt_state: @types.AuditReceiptState::Pending(head_sha="x"),
}
let valid = {
..statusline_test_tracked_pr(85, "main.leaf-audit-valid"),
audit_receipt_state: @types.AuditReceiptState::Valid,
}
ignore(
@sys.write_file_sync(
project_dir + "/.choir/state/poller_state.json",
"[" + pending.to_json() + "," + valid.to_json() + "]",
),
)
let summary = statusline_pr_summary(project_dir)
ignore(@sys.rm_rf(project_dir))
assert_true(summary.body.contains("#84"))
assert_false(summary.body.contains("#84M"))
assert_true(summary.body.contains("#85M"))
}

///|
fn statusline_pr_summary(project_dir : String) -> StatuslinePrSummary {
let tracked_prs = @poller.read_state_file(project_dir)
Expand Down
1 change: 1 addition & 0 deletions src/poller/detect_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn detect_test_tracked(
mergeable: Mergeable::Unknown,
merge_state_status: MergeStateStatus::Unknown,
merged_at_unix: None,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at,
copilot_issue_comment_seen: false,
orphan_notified: false,
Expand Down
1 change: 1 addition & 0 deletions src/poller/poller.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ pub(all) struct TrackedPR {
mut mergeable : Mergeable
mut merge_state_status : MergeStateStatus
mut merged_at_unix : Int64?
mut audit_receipt_state : @types.AuditReceiptState
mut last_timeout_at : Int64
/// True once the poller has observed an issue-thread comment authored by a
/// GitHub Copilot bot login (REST `/issues/{n}/comments`), distinct from review objects.
Expand Down
86 changes: 86 additions & 0 deletions src/poller/poller_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,92 @@ test "poller_backoff_delay_after_failure exponential ladder capped at 900s" {
assert_eq(@poller.poller_backoff_delay_after_failure(50), 900L)
}

///|
test "TrackedPR audit receipt state defaults for old JSON and round-trips" {
let old_json =
#|[
#| {
#| "pr_number": 41,
#| "agent_id": "main.leaf",
#| "branch": "work/leaf",
#| "parent_branch": "main",
#| "pr_url": "https://example.test/pr/41",
#| "first_seen": 1,
#| "last_review_state": "approved",
#| "last_review_bodies": "",
#| "last_sha": "old-head",
#| "notified_parent": true,
#| "last_ci_rollup": "success",
#| "last_timeout_at": 0
#| }
#|]
let old = @poller.parse_tracked_prs_json(old_json)
assert_eq(old.length(), 1)
assert_eq(old[0].audit_receipt_state, @types.AuditReceiptState::NotRequired)

let pending_json =
#|[
#| {
#| "pr_number": 42,
#| "agent_id": "main.leaf",
#| "branch": "work/leaf",
#| "parent_branch": "main",
#| "pr_url": "https://example.test/pr/42",
#| "first_seen": 1,
#| "last_review_state": "approved",
#| "last_review_bodies": "",
#| "last_sha": "head",
#| "notified_parent": true,
#| "last_ci_rollup": "success",
#| "last_timeout_at": 0,
#| "audit_receipt_state": ["Pending",{"head_sha":"abc"}]
#| }
#|]
let pending = @poller.parse_tracked_prs_json(pending_json)
assert_eq(
pending[0].audit_receipt_state,
@types.AuditReceiptState::Pending(head_sha="abc"),
)
let pending_round_trip = @poller.parse_tracked_prs_json(
"[" + pending[0].to_json() + "]",
)
assert_eq(
pending_round_trip[0].audit_receipt_state,
@types.AuditReceiptState::Pending(head_sha="abc"),
)

let findings_json =
#|[
#| {
#| "pr_number": 43,
#| "agent_id": "main.leaf",
#| "branch": "work/leaf",
#| "parent_branch": "main",
#| "pr_url": "https://example.test/pr/43",
#| "first_seen": 1,
#| "last_review_state": "approved",
#| "last_review_bodies": "",
#| "last_sha": "head",
#| "notified_parent": true,
#| "last_ci_rollup": "success",
#| "last_timeout_at": 0,
#| "audit_receipt_state": ["FindingsRemain",{"head_sha":"abc","count":2}]
#| }
#|]
let findings = @poller.parse_tracked_prs_json(findings_json)
assert_eq(
findings[0].audit_receipt_state,
@types.AuditReceiptState::FindingsRemain(head_sha="abc", count=2),
)
let findings_round_trip = @poller.parse_tracked_prs_json(
"[" + findings[0].to_json() + "]",
)
assert_eq(
findings_round_trip[0].audit_receipt_state,
@types.AuditReceiptState::FindingsRemain(head_sha="abc", count=2),
)
}

///|
test "PollerState merge receipts are bounded markers" {
let ps = @poller.PollerState::new()
Expand Down
1 change: 1 addition & 0 deletions src/poller/pr_health_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn pr_health_sample(
mergeable: Mergeable::Unknown,
merge_state_status: MergeStateStatus::Unknown,
merged_at_unix: merged_at,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen: false,
orphan_notified: false,
Expand Down
11 changes: 11 additions & 0 deletions src/poller/state.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pub fn TrackedPR::to_json(self : TrackedPR) -> String {
"mergeable": self.mergeable.to_wire_string().to_json(),
"merge_state_status": self.merge_state_status.to_wire_string().to_json(),
"merged_at_unix": tracked_pr_optional_int64_to_json(self.merged_at_unix),
"audit_receipt_state": self.audit_receipt_state.to_json(),
"last_timeout_at": self.last_timeout_at.to_json(),
"copilot_issue_comment_seen": self.copilot_issue_comment_seen.to_json(),
"orphan_notified": self.orphan_notified.to_json(),
Expand Down Expand Up @@ -275,6 +276,13 @@ fn parse_tracked_pr(fields : Map[String, Json]) -> TrackedPR? {
None => MergeStateStatus::Unknown
}
let merged_at_unix = @types.json_field_int64(fields, "merged_at_unix")
let audit_receipt_state = match fields.get("audit_receipt_state") {
Some(value) =>
@json.from_json(value) catch {
_ => @types.AuditReceiptState::NotRequired
}
None => @types.AuditReceiptState::NotRequired
}
let last_timeout_at = match
@types.json_field_int64(fields, "last_timeout_at") {
Some(value) => value
Expand Down Expand Up @@ -355,6 +363,7 @@ fn parse_tracked_pr(fields : Map[String, Json]) -> TrackedPR? {
mergeable,
merge_state_status,
merged_at_unix,
audit_receipt_state,
last_timeout_at,
copilot_issue_comment_seen,
orphan_notified,
Expand Down Expand Up @@ -434,6 +443,7 @@ pub fn PollerState::track(
mergeable: Mergeable::Unknown,
merge_state_status: MergeStateStatus::Unknown,
merged_at_unix: None,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen: false,
orphan_notified: false,
Expand Down Expand Up @@ -563,6 +573,7 @@ pub fn PollerState::track_recovered(
mergeable: Mergeable::Unknown,
merge_state_status: MergeStateStatus::Unknown,
merged_at_unix: None,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen,
orphan_notified: false,
Expand Down
1 change: 1 addition & 0 deletions src/poller/tl_decision_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ fn sample_tracked(
mergeable: Mergeable::Unknown,
merge_state_status: MergeStateStatus::Unknown,
merged_at_unix: None,
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen: false,
orphan_notified: false,
Expand Down
4 changes: 3 additions & 1 deletion src/server/delivery_test_helpers_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ async fn ServerState::process_poller_tick_with_test(
git_pull_capture? : async (@workspace.Command) -> (Int, String) = server_delivery_test_capture,
hook_capture? : async (String, String, String?) -> (Int, String) = server_delivery_test_hook_capture,
hook_path_exists? : (String) -> Bool = server_delivery_test_path_entry_exists,
path_exists? : (String) -> Bool = @sys.path_entry_exists,
audit_receipt_exists? : (String) -> Bool = server_delivery_test_path_entry_exists,
audit_receipt_read? : (String) -> String = server_delivery_test_read_file,
git_state_capture? : async (
Expand Down Expand Up @@ -195,6 +196,7 @@ async fn ServerState::process_poller_tick_with_test(
kill_pgid~,
git_capture=server_delivery_test_git_capture,
bd_capture=server_delivery_test_capture,
path_exists=@sys.path_entry_exists,
path_exists~,
read_file=audit_receipt_read,
)
}
81 changes: 81 additions & 0 deletions src/server/handler_poll_delivery.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,20 @@ fn poll_events_pr_merged_numbers(
pr_numbers
}

///|
fn poll_events_terminal_pr_numbers(
events : Array[@poller.PollEvent],
) -> Array[Int] {
let pr_numbers : Array[Int] = []
for event in events {
match event {
PRMerged(pr) | PRClosedUnmerged(pr) => pr_numbers.push(pr.pr_number)
_ => ()
}
}
pr_numbers
}

///|
fn poll_event_tracked_snapshot(
event : @poller.PollEvent,
Expand Down Expand Up @@ -937,6 +951,7 @@ fn synthetic_untracked_merged_pr(
} else {
Some(info.merged_at_unix)
},
audit_receipt_state: @types.AuditReceiptState::NotRequired,
last_timeout_at: 0L,
copilot_issue_comment_seen: false,
orphan_notified: false,
Expand Down Expand Up @@ -2292,6 +2307,52 @@ async fn ServerState::retry_auto_resolve_outdated_review_threads(
events
}

///|
async fn ServerState::refresh_tracked_pr_audit_receipt_states(
self : ServerState,
capture : async (@workspace.Command) -> (Int, String),
path_exists : (String) -> Bool,
read_file : (String) -> String,
skip_pr_numbers? : Array[Int] = [],
only_pr_number? : Int? = None,
) -> Unit {
let tracked_prs : Array[@poller.TrackedPR] = []
for tracked in self.poller.tracked.values() {
let matches_target = match only_pr_number {
Some(pr_number) => tracked.pr_number == pr_number
None => true
}
if !skip_pr_numbers.contains(tracked.pr_number) && matches_target {
tracked_prs.push(tracked)
}
}
if tracked_prs.length() == 0 {
return
}
let default_branch = @tools.detect_default_branch_with_capture(
self.config.project_dir,
capture,
)
for tracked in tracked_prs {
match self.poller.get(tracked.pr_number) {
Some(current) => {
let audit_receipt_state = @tools.audit_receipt_advisory_state_for_tracked_pr(
self.config.project_dir,
current.parent_branch,
default_branch,
current.last_sha,
current.first_seen,
capture,
read_file,
path_exists,
)
self.poller.restore({ ..current, audit_receipt_state, })
}
None => ()
}
}
}

///|
async fn deliver_pending_poll_events(
state : ServerState,
Expand Down Expand Up @@ -3793,6 +3854,7 @@ pub async fn ServerState::process_poller_tick_with(
git_pull_runner? : async (String, async (@workspace.Command) -> (Int, String)) -> Bool = run_parent_git_pull_ff_only,
sleep_fn? : async (Int) -> Unit = @async.sleep,
kill_pgid? : (Int) -> Unit = default_kill_pgid_best_effort,
read_file? : (String) -> String = self.read_file,
) -> Unit {
self.mutex_registry.expire(now)
self.warn_on_disk_config_divergence()
Expand Down Expand Up @@ -3856,6 +3918,12 @@ pub async fn ServerState::process_poller_tick_with(
git_pull_runner~,
sleep_fn~,
)
self.refresh_tracked_pr_audit_receipt_states(
capture,
path_exists,
read_file,
skip_pr_numbers=poll_events_terminal_pr_numbers(pending_events),
)
self.refresh_beads_status_cache_if_due(now, bd_capture, path_exists)
tick_summary.changes = pending_events.length()
emit_tick_summary_line(
Expand Down Expand Up @@ -4094,6 +4162,12 @@ pub async fn ServerState::process_poller_tick(
audit_receipt_read~,
sleep_fn~,
)
self.refresh_tracked_pr_audit_receipt_states(
capture,
@exec.path_entry_exists,
self.read_file,
skip_pr_numbers=poll_events_terminal_pr_numbers(pending_events),
)
self.refresh_beads_status_cache_if_due(
now, @exec.capture_command_stdout_with_stderr_on_error, @exec.path_entry_exists,
)
Expand Down Expand Up @@ -4185,6 +4259,13 @@ pub async fn ServerState::refresh_pr_with(
audit_receipt_read~,
sleep_fn~,
)
self.refresh_tracked_pr_audit_receipt_states(
capture,
path_exists,
self.read_file,
skip_pr_numbers=poll_events_terminal_pr_numbers(pending_events),
only_pr_number=Some(pr_number),
)
if had_pending_events {
self.request_goal_judge_tick()
}
Expand Down
Loading
Loading