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
19 changes: 10 additions & 9 deletions .trinity/seals/automation_review_jam.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
{
"built_by": "t27c-bootstrap@0.5.2+0d573722c",
"built_by": "t27c-bootstrap@0.5.2+cd8a65666",
"closure": [],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:a53272245acd53b9e42a86c67bf9159b1c30ef5d48ded84ec549a060240cfd27",
"gen_hash_rust": "sha256:9a17dd922d08c6b4093efe761c5d35500666880182272ff02a7abe770e22dd7d",
"gen_hash_verilog": "sha256:3c8adb026c197093c8c1a8d983db20e5510d058b7f0c5dd514046c671b5faf5e",
"gen_hash_zig": "sha256:5ac1fd03c0a29ef6cfc05bc0ec3a5008cf57a44a48ea5ef1e66b26d68a6be89c",
"gen_hash_c": "sha256:7bd51fdd573256d1feec4c938d2569cbd4eb2745f9ba7b65edb18a8feeea9990",
"gen_hash_rust": "sha256:7f00198ac01c2a58b421d96e8d9eac0a1d7595f12038a2e8c8e4b9ea393c0f09",
"gen_hash_verilog": "sha256:7715176baed689595224e6b0f72e2b4fef711b775a7ac80c32f265391fff79a5",
"gen_hash_zig": "sha256:93df76a874717e69a6c84d8b39818b68f52ee548694e507aad41119c1e45ec32",
"module": "review_jam",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-09T20:22:48Z",
"sealed_at": "2026-10-17T14:13:10Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:e1c98dcb48ff491bb006b420f16543c94def9ba52950cc62c54433ea186f9a1e",
"spec_hash": "sha256:4cc0c0e1fa6d6abd3c9b8a7658f8a2d31a8fba24bb7ad4efcf60bf194497a662",
"spec_path": "specs/automation/review-jam.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 27,
"total": 27
"passed": 44,
"total": 44,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
Expand Down
201 changes: 139 additions & 62 deletions specs/automation/review-jam.t27
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Review jam case detection and Queen action mapping
// Part of EPIC #5388 - gHashTag/t27#5700
// Review jam: which case a stuck pull request is in, and the Queen's action.
// Part of EPIC #5388. Contract: gHashTag/t27#9032 (re-filed from #5700).
// `tri jam` (gHashTag/999-multibots-telegraf) imports these numbers instead
// of hand-copying them.

module review_jam;

// Jam case constants (u8, position = value, starting at 0)
// Jam case constants (u8, value = position, starting at 0)
pub const JAM_NONE: u8 = 0;
pub const JAM_NO_REVIEWER: u8 = 1;
pub const JAM_STACKED_ON_OPEN: u8 = 2;
Expand All @@ -15,7 +17,7 @@ pub const JAM_NOISE_RED: u8 = 7;
pub const JAM_DRAFT_OR_FRESH: u8 = 8;
pub const JAM_UNMEASURED: u8 = 9;

// Queen action constants (u8, position = value, starting at 0)
// Queen action constants (u8, value = position, starting at 0)
pub const ACT_NOTHING: u8 = 0;
pub const ACT_DISPATCH_REVIEWER: u8 = 1;
pub const ACT_WAIT_FOR_BASE: u8 = 2;
Expand All @@ -27,18 +29,17 @@ pub const ACT_NAME_NOISE_AND_REVIEW: u8 = 7;
pub const ACT_WAIT: u8 = 8;
pub const ACT_MEASURE_AGAIN: u8 = 9;

// Determine the jam case based on PR state
// First matching rule wins, in this order:
// 1. not measured -> UNMEASURED
// 2. draft or fresh -> DRAFT_OR_FRESH
// 3. stacked on an open PR -> STACKED_ON_OPEN
// 4. conflicting -> CONFLICTING
// 5. labeled reviewed (and still open) -> LABELED_NOT_MERGED
// 6. refused and pushed after -> REFUSED_THEN_PUSHED
// 7. refused -> REFUSED_NOT_FIXED
// 8. only noise checks red -> NOISE_RED
// 9. no reviewer -> NO_REVIEWER
// 10. otherwise NONE
// The jam case of a pull request. The FIRST matching rule wins:
// 1. not measured -> UNMEASURED
// 2. draft or fresh -> DRAFT_OR_FRESH
// 3. stacked on an open PR -> STACKED_ON_OPEN
// 4. conflicting -> CONFLICTING
// 5. labeled reviewed (and still open) -> LABELED_NOT_MERGED
// 6. refused and pushed after -> REFUSED_THEN_PUSHED
// 7. refused -> REFUSED_NOT_FIXED
// 8. only noise checks red -> NOISE_RED
// 9. no reviewer -> NO_REVIEWER
// 10. otherwise -> NONE
pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, conflicting: bool, labeled_reviewed: bool, refused: bool, pushed_after_refusal: bool, has_reviewer: bool, only_noise_red: bool) -> u8 {
if !measured {
return JAM_UNMEASURED;
Expand Down Expand Up @@ -70,54 +71,58 @@ pub fn jam_case(measured: bool, draft_or_fresh: bool, stacked_on_open: bool, con
JAM_NONE
}

// Map jam case to Queen action
// Case maps to action with same position (NONE -> NOTHING, NO_REVIEWER -> DISPATCH_REVIEWER, etc.)
// Any value above 9 -> ACT_MEASURE_AGAIN (unknown case is never "nothing to do")
// The Queen's action for a case: the action with the SAME position.
// Any value above 9 is an unknown case, and an unknown case is never
// "nothing to do": it maps to ACT_MEASURE_AGAIN.
pub fn jam_action(case: u8) -> u8 {
if case > 9 {
return ACT_MEASURE_AGAIN;
}
case
}

// Determine if it's the Queen's turn to act
// True for every case except NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN
// Whether the case is the Queen's turn to act: true for every case except
// NONE, DRAFT_OR_FRESH and STACKED_ON_OPEN.
pub fn jam_is_queens_turn(case: u8) -> bool {
case != JAM_NONE && case != JAM_DRAFT_OR_FRESH && case != JAM_STACKED_ON_OPEN
}

// Tests - one per case, plus edge cases
// ---- jam_case: one test per case ----

test "unmeasured beats every other flag" {
assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED;
}

test "unmeasured with every other flag clear" {
assert jam_case(false, false, false, false, false, false, false, false, false) == JAM_UNMEASURED;
}

test "draft or fresh" {
assert jam_case(true, true, false, false, false, false, false, false, false) == JAM_DRAFT_OR_FRESH;
}

test "stacked on open PR" {
assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN;
assert jam_case(true, false, true, false, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
}

test "conflicting" {
assert jam_case(true, false, false, true, false, false, false, false, false) == JAM_CONFLICTING;
assert jam_case(true, false, false, true, false, false, false, true, false) == JAM_CONFLICTING;
}

test "labeled not merged" {
assert jam_case(true, false, false, false, true, false, false, false, false) == JAM_LABELED_NOT_MERGED;
assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED;
}

test "refused then pushed" {
assert jam_case(true, false, false, false, false, true, true, false, false) == JAM_REFUSED_THEN_PUSHED;
assert jam_case(true, false, false, false, false, true, true, true, false) == JAM_REFUSED_THEN_PUSHED;
}

test "refused not fixed" {
assert jam_case(true, false, false, false, false, true, false, false, false) == JAM_REFUSED_NOT_FIXED;
assert jam_case(true, false, false, false, false, true, false, true, false) == JAM_REFUSED_NOT_FIXED;
}

test "noise red" {
assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED;
assert jam_case(true, false, false, false, false, false, false, true, true) == JAM_NOISE_RED;
}

test "no reviewer" {
Expand All @@ -128,70 +133,142 @@ test "none" {
assert jam_case(true, false, false, false, false, false, false, true, false) == JAM_NONE;
}

test "unknown case maps to measure again" {
assert jam_action(200) == ACT_MEASURE_AGAIN;
}
// ---- jam_case: first-match order, each rule against the next one ----

test "queen's turn for labeled not merged" {
assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true;
test "draft beats stacked" {
assert jam_case(true, true, true, false, false, false, false, true, false) == JAM_DRAFT_OR_FRESH;
}

test "queen's turn for no reviewer" {
assert jam_is_queens_turn(JAM_NO_REVIEWER) == true;
test "stacked and unreviewed is stacked, not no reviewer" {
assert jam_case(true, false, true, false, false, false, false, false, false) == JAM_STACKED_ON_OPEN;
}

test "queen's turn for conflicting" {
assert jam_is_queens_turn(JAM_CONFLICTING) == true;
test "stacked beats conflicting" {
assert jam_case(true, false, true, true, false, false, false, true, false) == JAM_STACKED_ON_OPEN;
}

test "queen's turn for refused then pushed" {
assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true;
test "conflicting beats labeled" {
assert jam_case(true, false, false, true, true, false, false, true, false) == JAM_CONFLICTING;
}

test "queen's turn for refused not fixed" {
assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true;
test "labeled beats refused then pushed" {
assert jam_case(true, false, false, false, true, true, true, true, false) == JAM_LABELED_NOT_MERGED;
}

test "queen's turn for noise red" {
assert jam_is_queens_turn(JAM_NOISE_RED) == true;
test "refused then pushed beats noise red" {
assert jam_case(true, false, false, false, false, true, true, true, true) == JAM_REFUSED_THEN_PUSHED;
}

test "queen's turn for unmeasured" {
assert jam_is_queens_turn(JAM_UNMEASURED) == true;
test "refused not fixed beats noise red" {
assert jam_case(true, false, false, false, false, true, false, true, true) == JAM_REFUSED_NOT_FIXED;
}

test "not queen's turn for none" {
assert jam_is_queens_turn(JAM_NONE) == false;
test "noise red beats no reviewer" {
assert jam_case(true, false, false, false, false, false, false, false, true) == JAM_NOISE_RED;
}

test "not queen's turn for draft or fresh" {
assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false;
test "pushed without a refusal is not refused then pushed" {
assert jam_case(true, false, false, false, false, false, true, true, false) == JAM_NONE;
}

test "not queen's turn for stacked on open" {
assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false;
// ---- jam_action: same position, unknown -> measure again ----

test "action none to nothing" {
assert jam_action(JAM_NONE) == ACT_NOTHING;
}

test "specific scenario: labeled reviewed but still open" {
assert jam_case(true, false, false, false, true, false, false, true, false) == JAM_LABELED_NOT_MERGED;
test "action no reviewer to dispatch reviewer" {
assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER;
}

test "specific scenario: unmeasured with any other flags" {
assert jam_case(false, true, true, true, true, true, true, true, true) == JAM_UNMEASURED;
test "action stacked to wait for base" {
assert jam_action(JAM_STACKED_ON_OPEN) == ACT_WAIT_FOR_BASE;
}

test "action mapping: none to nothing" {
assert jam_action(JAM_NONE) == ACT_NOTHING;
test "action refused not fixed to dispatch coder" {
assert jam_action(JAM_REFUSED_NOT_FIXED) == ACT_DISPATCH_CODER;
}

test "action mapping: no reviewer to dispatch reviewer" {
assert jam_action(JAM_NO_REVIEWER) == ACT_DISPATCH_REVIEWER;
test "action refused then pushed to dispatch re-review" {
assert jam_action(JAM_REFUSED_THEN_PUSHED) == ACT_DISPATCH_RE_REVIEW;
}

test "action mapping: labeled not merged to finish merge" {
test "action labeled not merged to finish merge" {
assert jam_action(JAM_LABELED_NOT_MERGED) == ACT_FINISH_MERGE;
}

test "action mapping: unmeasured to measure again" {
test "action conflicting to rebase by coder" {
assert jam_action(JAM_CONFLICTING) == ACT_REBASE_BY_CODER;
}

test "action noise red to name noise and review" {
assert jam_action(JAM_NOISE_RED) == ACT_NAME_NOISE_AND_REVIEW;
}

test "action draft or fresh to wait" {
assert jam_action(JAM_DRAFT_OR_FRESH) == ACT_WAIT;
}

test "action unmeasured to measure again" {
assert jam_action(JAM_UNMEASURED) == ACT_MEASURE_AGAIN;
}
}

test "action first unknown case measures again" {
assert jam_action(10) == ACT_MEASURE_AGAIN;
}

test "action unknown case 200 measures again" {
assert jam_action(200) == ACT_MEASURE_AGAIN;
}

// ---- jam_is_queens_turn ----

test "queens turn for no reviewer" {
assert jam_is_queens_turn(JAM_NO_REVIEWER) == true;
}

test "queens turn for refused not fixed" {
assert jam_is_queens_turn(JAM_REFUSED_NOT_FIXED) == true;
}

test "queens turn for refused then pushed" {
assert jam_is_queens_turn(JAM_REFUSED_THEN_PUSHED) == true;
}

test "queens turn for labeled not merged" {
assert jam_is_queens_turn(JAM_LABELED_NOT_MERGED) == true;
}

test "queens turn for conflicting" {
assert jam_is_queens_turn(JAM_CONFLICTING) == true;
}

test "queens turn for noise red" {
assert jam_is_queens_turn(JAM_NOISE_RED) == true;
}

test "queens turn for unmeasured" {
assert jam_is_queens_turn(JAM_UNMEASURED) == true;
}

test "not queens turn for none" {
assert jam_is_queens_turn(JAM_NONE) == false;
}

test "not queens turn for draft or fresh" {
assert jam_is_queens_turn(JAM_DRAFT_OR_FRESH) == false;
}

test "not queens turn for stacked on open" {
assert jam_is_queens_turn(JAM_STACKED_ON_OPEN) == false;
}

// ---- the two scenarios of #9032, end to end ----

test "scenario bee-reviewed but still open finishes the merge" {
assert jam_action(jam_case(true, false, false, false, true, false, false, true, false)) == ACT_FINISH_MERGE;
}

test "scenario failed read is never reported as free" {
assert jam_action(jam_case(false, false, false, false, false, false, false, true, false)) == ACT_MEASURE_AGAIN;
}
Loading