Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 5 additions & 3 deletions bin/fm-contributions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
# API failure leaves error evidence; an expired or absent observation is not
# silence. FM_CONTRIBUTIONS_MAX_AGE (default 900 seconds) bounds freshness.
# A URL whose last good observation is merged or closed is final: it is
# never re-read, stays fresh, and a stale error beside it is cleared once.
# never re-read, stays fresh, and every owner's saved row converges on that
# observation, with a stale error beside it cleared.
# A genuine failure prints its unavailable line only when it starts an episode
# (no prior owner has an error); a successful read ends the episode.
# FM_CONTRIBUTIONS_NOW supplies an ISO UTC clock for tests, otherwise UTC now.
Expand Down Expand Up @@ -338,8 +339,9 @@ settle_final() { # canonical-url task... : copy the URL's final observation to e
jq -n --slurpfile final "$TMP/final.json" '
$final[0] + {error:null,pending:[],notified:[]}' > "$TMP/row.json"
write_record "$task" "$TMP/row.json"
elif jq -e '.error != null' "$TMP/old.json" >/dev/null; then
jq '.error = null' "$TMP/old.json" > "$TMP/row.json"
elif jq -e '(.observation.state | IN("merged","closed") | not) or .error != null' "$TMP/old.json" >/dev/null; then
jq -n --slurpfile final "$TMP/final.json" --slurpfile old "$TMP/old.json" '
$old[0] + {observation:$final[0].observation,checked_at:$final[0].checked_at,error:null}' > "$TMP/row.json"
write_record "$task" "$TMP/row.json"
fi
done
Expand Down
37 changes: 36 additions & 1 deletion tests/fm-contributions.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,41 @@ test_late_owner_inherits_terminal_observation() {
pass 'a late owner inherits a terminal observation without a forge read or wake'
}

test_interrupted_multi_owner_poll_settles_every_owner() {
local home later=2026-09-17T08:00:00Z
home=$(new_home multi-owner-open)
forge_home "$home"
wrap_forge "$home"
record "$home" duplicate 8 open mergeable
mutate_record "$home" duplicate '.records[0].pending=[{token:"evt-1"}] | .records[0].notified=["evt-0"]
| .records[0].checked_at="2026-09-15T08:00:00Z"'
mutate_record "$home" delivery '.records[0].observation.state="merged"'
printf 'down\n' > "$home/forge/fault"
with_home "$home" env FM_CONTRIBUTIONS_NOW="$later" "$ROOT/bin/fm-contributions.sh" poll >/dev/null \
|| fail 'interrupted multi-owner poll failed'
[ ! -s "$home/forge/calls" ] || fail 'a known terminal URL triggered a forge read'
jq -e --slurpfile terminal "$home/data/delivery/contributions.json" '.records[0] | .observation.state == "merged"
and .error == null and .checked_at == $terminal[0].records[0].checked_at
and .pending == [{token:"evt-1"}] and .notified == ["evt-0"]' \
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
"$home/data/duplicate/contributions.json" >/dev/null \
|| fail "an owner whose saved row stayed open did not converge on the known terminal observation: $(cat "$home/data/duplicate/contributions.json")"

home=$(new_home multi-owner-errored)
forge_home "$home"
wrap_forge "$home"
record "$home" duplicate 8 open mergeable
mutate_record "$home" duplicate '.records[0].error="forge observation unavailable or changed during read"'
mutate_record "$home" delivery '.records[0].observation.state="merged"'
printf 'down\n' > "$home/forge/fault"
with_home "$home" env FM_CONTRIBUTIONS_NOW="$later" "$ROOT/bin/fm-contributions.sh" poll >/dev/null \
|| fail 'interrupted multi-owner poll (errored owner) failed'
[ ! -s "$home/forge/calls" ] || fail 'a known terminal URL triggered a forge read (errored owner)'
jq -e '.records[0].observation.state == "merged" and .records[0].error == null' \
"$home/data/duplicate/contributions.json" >/dev/null \
|| fail "an errored owner did not converge on the known terminal observation: $(cat "$home/data/duplicate/contributions.json")"
pass 'a retry converges every owner whose saved row is not terminal, keeping its own acknowledgement state'
}

test_done_task_open_pr_still_observed() {
local home later=2026-09-17T08:00:00Z
home=$(new_home done-open)
Expand Down Expand Up @@ -1029,7 +1064,7 @@ test_late_owner_keeps_failure_episode_suppressed() {
}

failures=0
for test_name in test_actor_coverage test_stale_verdict test_unchecked_is_not_silence test_newest_check_has_no_verdict test_comment_wake test_review_wake test_inline_wake test_ready_issue_wake test_fresh_issue_requires_maintainer test_missing_lane_remains_missing test_partial_freshness_keeps_measured_rows test_malformed_record_cannot_prove_silence test_issue_timeline_and_exact_ack test_verdict_retains_judged_head test_observed_replacement_refreshes_verdict test_unobserved_head_leaves_verdict_unknown test_away_yolo_is_fleet_work test_away_yolo_cross_home_is_fleet_work test_retired_and_unsupported_coverage test_unsupported_forge_is_not_fleet_work test_held_unsupported_forge_is_not_captain_work test_shared_contribution_signal_wakes_once test_watcher_keeps_diagnostics_separate_from_contribution_wakes test_expired_child_unsupported_forge_stays_unmeasured test_watcher_surfaces_new_contribution_once test_home_summary_coverage test_unreadable_pending_is_not_empty test_record_task_identity_matches_dirname_basename test_read_only_views_create_no_state test_budget_refusal_between_calls test_budget_bounded_call_timeout test_genuine_failure_near_deadline_is_unavailable test_shared_url_observed_once test_terminal_contribution_settles test_late_owner_inherits_terminal_observation test_done_task_open_pr_still_observed test_reservation_defers_later_url_when_fifteen_seconds_do_not_remain test_three_second_pr_reads_complete_fresh_in_one_cycle test_slow_read_deadline_kill_is_budget_refusal test_unmeasured_url_does_not_starve_the_tail test_budget_is_cut_down_to_the_watcher_check_bound test_arm_plumbs_a_configured_budget_into_the_check_shim test_unavailable_forge_records_error_and_wakes_once_per_episode test_late_owner_keeps_failure_episode_suppressed; do
for test_name in test_actor_coverage test_stale_verdict test_unchecked_is_not_silence test_newest_check_has_no_verdict test_comment_wake test_review_wake test_inline_wake test_ready_issue_wake test_fresh_issue_requires_maintainer test_missing_lane_remains_missing test_partial_freshness_keeps_measured_rows test_malformed_record_cannot_prove_silence test_issue_timeline_and_exact_ack test_verdict_retains_judged_head test_observed_replacement_refreshes_verdict test_unobserved_head_leaves_verdict_unknown test_away_yolo_is_fleet_work test_away_yolo_cross_home_is_fleet_work test_retired_and_unsupported_coverage test_unsupported_forge_is_not_fleet_work test_held_unsupported_forge_is_not_captain_work test_shared_contribution_signal_wakes_once test_watcher_keeps_diagnostics_separate_from_contribution_wakes test_expired_child_unsupported_forge_stays_unmeasured test_watcher_surfaces_new_contribution_once test_home_summary_coverage test_unreadable_pending_is_not_empty test_record_task_identity_matches_dirname_basename test_read_only_views_create_no_state test_budget_refusal_between_calls test_budget_bounded_call_timeout test_genuine_failure_near_deadline_is_unavailable test_shared_url_observed_once test_terminal_contribution_settles test_late_owner_inherits_terminal_observation test_interrupted_multi_owner_poll_settles_every_owner test_done_task_open_pr_still_observed test_reservation_defers_later_url_when_fifteen_seconds_do_not_remain test_three_second_pr_reads_complete_fresh_in_one_cycle test_slow_read_deadline_kill_is_budget_refusal test_unmeasured_url_does_not_starve_the_tail test_budget_is_cut_down_to_the_watcher_check_bound test_arm_plumbs_a_configured_budget_into_the_check_shim test_unavailable_forge_records_error_and_wakes_once_per_episode test_late_owner_keeps_failure_episode_suppressed; do
( "$test_name" ) || failures=$((failures + 1))
done
[ "$failures" -eq 0 ] || fail "$failures contribution regressions"
Loading