diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index d1431bd..1afe400 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -9,7 +9,18 @@ fi # The issue-flow half of the labels state machine. Decisions are pure strings; # API calls live below the divider so fixture tests can exercise every branch. -STALE_AFTER=$((48 * 3600)) +ISSUEFLOW_NOW="${ISSUEFLOW_NOW:-$(date -u +%s)}" +ISSUEFLOW_STALE_HOURS="${ISSUEFLOW_STALE_HOURS:-48}" +[[ "$ISSUEFLOW_NOW" =~ ^[0-9]+$ ]] || { + echo "issueflow: ISSUEFLOW_NOW must be UTC epoch seconds" >&2 + if [ "${BASH_SOURCE[0]}" = "$0" ]; then exit 1; else return 1; fi +} +[[ "$ISSUEFLOW_STALE_HOURS" =~ ^[0-9]+$ ]] || { + echo "issueflow: ISSUEFLOW_STALE_HOURS must be a non-negative integer" >&2 + if [ "${BASH_SOURCE[0]}" = "$0" ]; then exit 1; else return 1; fi +} +NOW="$ISSUEFLOW_NOW" +STALE_AFTER=$((ISSUEFLOW_STALE_HOURS * 3600)) QUEUE_LABELS=(ready claimed blocked) TRIAGE_ACTORS=() @@ -232,7 +243,6 @@ main() { REPO="${REPO:?set REPO to owner/name}" LABELS_CONF="${LABELS_CONF:-.github/labels.conf}" load_issueflow_config "$LABELS_CONF" - NOW="$(date +%s)" if [ "${EVENT_NAME:-}" = issues ] && [ "${EVENT_ACTION:-}" = opened ]; then reconcile_opened_issue "${EVENT_ISSUE:?set EVENT_ISSUE for issues:opened}" fi diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 186a80e..30290f1 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -31,10 +31,19 @@ check "needs-triage plus queue is a conflict" 0 "FLAG_CONFLICT" queue_decision < # Invariant 2: claims have an owner and either a PR or recent activity. check "claim with open PR stays claimed" 0 "KEEP" claim_decision 1 true 999999 -check "recent claim without PR stays claimed" 0 "KEEP" claim_decision 1 false 60 +check "claim below stale boundary stays claimed" 0 "KEEP" claim_decision 1 false $((STALE_AFTER - 1)) +check "claim exactly at stale boundary stays claimed" 0 "KEEP" claim_decision 1 false "$STALE_AFTER" +check "claim past stale boundary is reclaimed" 0 "RECLAIM" claim_decision 1 false $((STALE_AFTER + 1)) check "unassigned claim is flagged" 0 "FLAG_UNASSIGNED" claim_decision 0 false 60 -check "quiet claim without PR is reclaimed" 0 "RECLAIM" claim_decision 1 false $((STALE_AFTER + 1)) check "quiet unassigned claim is also reclaimed" 0 "RECLAIM" claim_decision 0 false $((STALE_AFTER + 1)) +# shellcheck disable=SC2016 # expansions belong to the isolated bash -c process +check "injected clock is retained" 0 "1700000000" \ + bash -c 'ISSUEFLOW_NOW=1700000000 source "$1"; printf "%s\n" "$NOW"' _ \ + "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" +# shellcheck disable=SC2016 # expansions belong to the isolated bash -c process +check "injected stale hours set the boundary" 0 "10800" \ + bash -c 'ISSUEFLOW_STALE_HOURS=3 source "$1"; printf "%s\n" "$STALE_AFTER"' _ \ + "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" # Invariant 3: blocked declarations parse and release only when all close. refs="$(blocked_references <<< $'Context #99. Blocked by #12 (first), #7 (second). Blocks #44.')"