Keep partial check details from replacing complete PR status (#38514)

* Keep PR check status tied to complete observations

* Correct checks presentation types and count import

* Preserve localized empty states and declared runtime support

* Correct discovery fixture stack types

* Omit empty failed-check subtitles

* Preserve check identity through refresh and row publication

* Withhold row identity for malformed check errors

* Limit local capability wakes to local task sources

* Correct PR check regression fixtures

* Preserve browser APIs in PR check regression fixture

* Settle PR check adoption after failed detail reads

* Use the parsed PR check detail type during refresh

* Preserve PR metadata across checks discovery and committed query lifetimes

* Keep complete checks proof separate from legacy publication policy

* Keep PR lookup fixtures independent from complete checks collection

* Keep complete-check fixtures within lint contracts

* test: align PR recordings with source check discovery
This commit is contained in:
Brennan Benson
2026-11-05 10:39:54 -08:00
committed by GitHub
parent 384b445585
commit dd1784f55b
128 changed files with 26893 additions and 19460 deletions
@@ -71,19 +71,19 @@ export const MOBILE_WEB_APP_BUNDLE_SCRIPT_SWEEP = [
['./h/[hostId]/[...page].tsx', 8],
['./h/[hostId]/accounts.tsx', 12],
['./h/[hostId]/agent-history/[worktreeId].tsx', 17],
['./h/[hostId]/automations.tsx', 24],
['./h/[hostId]/edit.tsx', 29],
['./h/[hostId]/files/[worktreeId].tsx', 34],
['./h/[hostId]/files/preview/[worktreeId].tsx', 44],
['./h/[hostId]/history/[worktreeId].tsx', 47],
['./h/[hostId]/index.tsx', 55],
['./h/[hostId]/new-workspace.tsx', 66],
['./h/[hostId]/pr/[worktreeId].tsx', 67],
['./h/[hostId]/automations.tsx', 23],
['./h/[hostId]/edit.tsx', 25],
['./h/[hostId]/files/[worktreeId].tsx', 30],
['./h/[hostId]/files/preview/[worktreeId].tsx', 40],
['./h/[hostId]/history/[worktreeId].tsx', 43],
['./h/[hostId]/index.tsx', 52],
['./h/[hostId]/new-workspace.tsx', 65],
['./h/[hostId]/pr/[worktreeId].tsx', 66],
['./h/[hostId]/review/[worktreeId].tsx', 78],
['./h/[hostId]/session/[worktreeId].tsx', 93],
['./h/[hostId]/source-control/[worktreeId].tsx', 101],
['./h/[hostId]/tasks.tsx', 115],
['./h/_layout.tsx', 119]
['./h/[hostId]/session/[worktreeId].tsx', 94],
['./h/[hostId]/source-control/[worktreeId].tsx', 103],
['./h/[hostId]/tasks.tsx', 114],
['./h/_layout.tsx', 118]
]
const sweptScripts = MOBILE_WEB_APP_BUNDLE_SCRIPT_SWEEP.map(([, scripts]) => scripts)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,180 @@
"family": "github.pr-read",
"namedDeltas": [],
"values": {
"026ad16c841b": {
"checks": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
]
},
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"pr-for-branch": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
},
"work-item": {
"ok": true,
"result": {
"assignees": {
"$rpc": "undefined"
},
"baseSha": {
"$rpc": "undefined"
},
"body": "body",
"checks": [],
"comments": [],
"headSha": "head-sha-1",
"item": {
"assignees": [],
"author": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"baseRefName": {
"$rpc": "undefined"
},
"branchName": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"id": "PR_1",
"labels": [],
"latestReviews": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": {
"$rpc": "undefined"
},
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"reviewRequests": [],
"state": "open",
"title": "Recorded",
"type": "pr",
"updatedAt": "",
"url": ""
},
"participants": [],
"pullRequestId": {
"$rpc": "undefined"
}
}
}
},
"0644012ff2a3": {
"status": "fulfilled",
"startedAt": 0,
@@ -540,17 +714,7 @@
}
}
},
"274a4d26a91b": {
"assignable": {
"ok": true,
"result": [
{
"avatarUrl": "",
"login": "octocat",
"name": "Octo Cat"
}
]
},
"2709bde96386": {
"check-details": {
"ok": true,
"result": {
@@ -589,6 +753,9 @@
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -1329,14 +1496,20 @@
}
}
},
"3fb2c86ce340": {
"checks": {
"3d3e04f5c1cb": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -1346,158 +1519,6 @@
}
}
]
},
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"pr-for-branch": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
},
"work-item": {
"ok": true,
"result": {
"assignees": {
"$rpc": "undefined"
},
"baseSha": {
"$rpc": "undefined"
},
"body": "body",
"checks": [],
"comments": [],
"headSha": "head-sha-1",
"item": {
"assignees": [],
"author": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"baseRefName": {
"$rpc": "undefined"
},
"branchName": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"id": "PR_1",
"labels": [],
"latestReviews": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": {
"$rpc": "undefined"
},
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"reviewRequests": [],
"state": "open",
"title": "Recorded",
"type": "pr",
"updatedAt": "",
"url": ""
},
"participants": [],
"pullRequestId": {
"$rpc": "undefined"
}
}
}
},
"40bf5b2f687f": {
@@ -3238,7 +3259,58 @@
}
}
},
"8ad23d6b64f4": {
"9a87ba2ee54f": {
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
}
},
"9bb9d9b8c502": {
"assignable": {
"ok": true,
"result": [
{
"avatarUrl": "",
"login": "octocat",
"name": "Octo Cat"
}
]
},
"check-details": {
"ok": true,
"result": {
@@ -3277,6 +3349,9 @@
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -3440,47 +3515,6 @@
}
}
},
"9a87ba2ee54f": {
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
}
},
"9e3c3581d43a": {
"checks": {
"error": "transport failure",
@@ -3639,7 +3673,16 @@
}
}
},
"a0d70c59fdd1": {
"a197c20578aa": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": "transport failure",
"ok": false
}
},
"a20f0d944ba5": {
"name": "github.prForBranch#1",
"ordinal": 5,
"args": [
@@ -3666,7 +3709,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -3692,15 +3735,6 @@
}
}
},
"a197c20578aa": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": "transport failure",
"ok": false
}
},
"a3e588709bde": {
"hosted-review": {
"ok": true,
@@ -5016,28 +5050,6 @@
"ordinal": 14,
"json": "{\"id\":\"frame-7\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}"
},
"e23eb2e4b033": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
]
}
},
"e97a125cdb88": {
"name": "github.prCheckDetails#1",
"ordinal": 11,
@@ -5268,7 +5280,7 @@
{
"id": "pr-read-surface.prelude:pr-for-branch",
"observation": {
"sender": ["191e3c96ad53", "4bf8f27dd319", "a0d70c59fdd1"],
"sender": ["191e3c96ad53", "4bf8f27dd319", "a20f0d944ba5"],
"payloads": ["1f5765918736", "cdbefce64e00", "1554c8f6753b"],
"settlements": {
"repo-slug": "d89e7b8ce2a0",
@@ -5282,7 +5294,7 @@
{
"id": "pr-read-surface.prelude:work-item",
"observation": {
"sender": ["191e3c96ad53", "4bf8f27dd319", "a0d70c59fdd1", "471a75cee137"],
"sender": ["191e3c96ad53", "4bf8f27dd319", "a20f0d944ba5", "471a75cee137"],
"payloads": ["1f5765918736", "cdbefce64e00", "1554c8f6753b", "0a9f2ff19df4"],
"settlements": {
"repo-slug": "d89e7b8ce2a0",
@@ -5300,7 +5312,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"fa4bf991415d"
],
@@ -5316,9 +5328,9 @@
"hosted-review": "0644012ff2a3",
"pr-for-branch": "d01444e13d4c",
"work-item": "d036e2bdda12",
"checks": "e23eb2e4b033"
"checks": "3d3e04f5c1cb"
},
"state": "3fb2c86ce340",
"state": "026ad16c841b",
"effects": []
}
},
@@ -5328,7 +5340,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"fa4bf991415d",
"e97a125cdb88"
@@ -5346,10 +5358,10 @@
"hosted-review": "0644012ff2a3",
"pr-for-branch": "d01444e13d4c",
"work-item": "d036e2bdda12",
"checks": "e23eb2e4b033",
"checks": "3d3e04f5c1cb",
"check-details": "1c88fe396b45"
},
"state": "8ad23d6b64f4",
"state": "2709bde96386",
"effects": []
}
},
@@ -5359,7 +5371,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"fa4bf991415d",
"e97a125cdb88",
@@ -5381,11 +5393,11 @@
"hosted-review": "0644012ff2a3",
"pr-for-branch": "d01444e13d4c",
"work-item": "d036e2bdda12",
"checks": "e23eb2e4b033",
"checks": "3d3e04f5c1cb",
"check-details": "1c88fe396b45",
"assignable": "a93bcc7122e8"
},
"state": "274a4d26a91b",
"state": "9bb9d9b8c502",
"effects": ["0c83b4de61f4"]
}
},
@@ -5395,7 +5407,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"f689187e3d49"
],
@@ -5423,7 +5435,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"f689187e3d49",
"e97a125cdb88"
@@ -5454,7 +5466,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"f689187e3d49",
"e97a125cdb88",
@@ -5490,7 +5502,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"42539e6b3b48"
],
@@ -5518,7 +5530,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"42539e6b3b48",
"e97a125cdb88"
@@ -5549,7 +5561,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"42539e6b3b48",
"e97a125cdb88",
@@ -5585,7 +5597,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"25b3c7a91f3e"
],
@@ -5613,7 +5625,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"25b3c7a91f3e",
"e97a125cdb88"
@@ -5644,7 +5656,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"25b3c7a91f3e",
"e97a125cdb88",
@@ -5680,7 +5692,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"b0c33981e3c2"
],
@@ -5708,7 +5720,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"b0c33981e3c2",
"e97a125cdb88"
@@ -5739,7 +5751,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"b0c33981e3c2",
"e97a125cdb88",
@@ -5775,7 +5787,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"60c8dc5664ec"
],
@@ -5803,7 +5815,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"60c8dc5664ec",
"e97a125cdb88"
@@ -5834,7 +5846,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"60c8dc5664ec",
"e97a125cdb88",
@@ -5870,7 +5882,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"40bf5b2f687f"
],
@@ -5898,7 +5910,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"40bf5b2f687f",
"e97a125cdb88"
@@ -5929,7 +5941,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"40bf5b2f687f",
"e97a125cdb88",
@@ -5965,7 +5977,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"a7931768c93d"
],
@@ -5993,7 +6005,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"a7931768c93d",
"e97a125cdb88"
@@ -6024,7 +6036,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"a7931768c93d",
"e97a125cdb88",
@@ -6060,7 +6072,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"5f2ed2e370af"
],
@@ -6088,7 +6100,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"5f2ed2e370af",
"e97a125cdb88"
@@ -6119,7 +6131,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"5f2ed2e370af",
"e97a125cdb88",
@@ -6155,7 +6167,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"667e9ccbc173"
],
@@ -6183,7 +6195,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"667e9ccbc173",
"e97a125cdb88"
@@ -6214,7 +6226,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"667e9ccbc173",
"e97a125cdb88",
@@ -6250,7 +6262,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"72d7a13c5968"
],
@@ -6278,7 +6290,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"72d7a13c5968",
"e97a125cdb88"
@@ -6309,7 +6321,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"72d7a13c5968",
"e97a125cdb88",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -141,57 +141,6 @@
"variant": "worktree-summary"
}
},
"1c1552d03dfc": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"28e291738e81": {
"name": "github.prChecks#1",
"ordinal": 8,
@@ -321,40 +270,6 @@
}
}
},
"4777f063108d": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"4b26820600b0": {
"name": "hostedReview.forBranch#1",
"ordinal": 3,
@@ -703,6 +618,88 @@
"kind": "ready"
}
},
"734ec9d15b54": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
}
},
"kind": "ready"
}
},
"7a8dea3aade4": {
"name": "github.prChecks#1",
"ordinal": 8,
@@ -1017,6 +1014,40 @@
},
"kind": "ready"
},
"a98f4468b85b": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"cc1661fa0fce": {
"name": "github.prChecks#1",
"ordinal": 8,
@@ -1087,83 +1118,55 @@
}
}
},
"cfed3ed2a0ae": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
"ccc790f6ce35": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
"repo": "id:repo-9"
}
},
"kind": "ready"
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"d31e02e77233": {
@@ -1354,13 +1357,16 @@
},
"kind": "ready"
},
"e7df27480ce9": {
"e93235fba8e6": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -1535,7 +1541,7 @@
{
"id": "pr-sidebar-load.prelude:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "4777f063108d"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "a98f4468b85b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1547,7 +1553,7 @@
{
"id": "pr-sidebar-load.prelude:pr",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "4fae27394886"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1559,19 +1565,19 @@
{
"id": "pr-sidebar-load.normal:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.result-absent:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "28e291738e81"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "28e291738e81"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "8b09f9d581f7"
@@ -1583,7 +1589,7 @@
{
"id": "pr-sidebar-load.result-null:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "cc58675e9778"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "cc58675e9778"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "8b09f9d581f7"
@@ -1595,7 +1601,7 @@
{
"id": "pr-sidebar-load.inner-ok-missing:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "fd1ab55e8692"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "fd1ab55e8692"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "8b09f9d581f7"
@@ -1607,7 +1613,7 @@
{
"id": "pr-sidebar-load.inner-false-string-error:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "0af3c7549a78"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "0af3c7549a78"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "8b09f9d581f7"
@@ -1619,7 +1625,7 @@
{
"id": "pr-sidebar-load.inner-false-object-error:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "10d2bae8a88d"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "10d2bae8a88d"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "8b09f9d581f7"
@@ -1631,7 +1637,7 @@
{
"id": "pr-sidebar-load.outer-refused:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "7b65a2a2b2bf"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "7b65a2a2b2bf"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "6186688b4e0a"
@@ -1643,7 +1649,7 @@
{
"id": "pr-sidebar-load.outer-refused-no-message:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "39a90edcdc1f"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "39a90edcdc1f"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "5ca71b99d858"
@@ -1655,7 +1661,7 @@
{
"id": "pr-sidebar-load.method-not-found:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9c9faf7a7d56"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "9c9faf7a7d56"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "6bd6616981d2"
@@ -1667,7 +1673,7 @@
{
"id": "pr-sidebar-load.transport-rejection:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "cc1661fa0fce"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "cc1661fa0fce"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "f5d376337dc6"
@@ -1679,7 +1685,7 @@
{
"id": "pr-sidebar-load.transport-rejection-no-message:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "7a8dea3aade4"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "7a8dea3aade4"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "d31e02e77233"
File diff suppressed because it is too large Load Diff
@@ -105,57 +105,6 @@
"variant": "worktree-summary"
}
},
"1c1552d03dfc": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"35103e980aae": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
@@ -192,7 +141,7 @@
}
}
},
"4777f063108d": {
"4780ceb8db5b": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
@@ -207,7 +156,9 @@
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"fallbackPRNumber": {
"$rpc": "null"
},
"linkedPRNumber": {
"$rpc": "null"
},
@@ -217,7 +168,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -377,6 +328,88 @@
}
}
},
"734ec9d15b54": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
}
},
"kind": "ready"
}
},
"7bf83a5e67be": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
@@ -502,11 +535,7 @@
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"973a6e87065b": {
"8c5720abe38c": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
@@ -533,7 +562,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -559,6 +588,10 @@
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"9a7a01989014": {
"name": "github.prChecks#1",
"ordinal": 8,
@@ -639,7 +672,7 @@
}
}
},
"cc4dd30d2afd": {
"a98f4468b85b": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
@@ -654,9 +687,7 @@
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
@@ -666,7 +697,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -675,83 +706,55 @@
"startedAt": 0
}
},
"cfed3ed2a0ae": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
"ccc790f6ce35": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
"repo": "id:repo-9"
}
},
"kind": "ready"
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"d8e9ec6c11ee": {
@@ -800,13 +803,56 @@
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-9\",\"prNumber\":12,\"headSha\":\"head-sha-1\"}}"
},
"e169ebaf642b": "unloaded",
"e7df27480ce9": {
"e8a558288b9c": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
"args": [
{
"name": "method",
"value": "hostedReview.forBranch"
},
{
"name": "params",
"value": {
"active": true,
"branch": "feature",
"linkedGitHubPR": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "refused",
"message": "outer refused"
},
"id": "frame-1",
"ok": false
}
}
},
"e93235fba8e6": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -873,46 +919,6 @@
}
},
"kind": "ready"
},
"e8a558288b9c": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
"args": [
{
"name": "method",
"value": "hostedReview.forBranch"
},
{
"name": "params",
"value": {
"active": true,
"branch": "feature",
"linkedGitHubPR": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "refused",
"message": "outer refused"
},
"id": "frame-1",
"ok": false
}
}
}
},
"recording": {
@@ -921,7 +927,7 @@
{
"id": "pr-sidebar-load.normal:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "4777f063108d"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "a98f4468b85b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -933,7 +939,7 @@
{
"id": "pr-sidebar-load.normal:pr",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "4fae27394886"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -945,19 +951,19 @@
{
"id": "pr-sidebar-load.normal:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.result-absent:branch-hint",
"observation": {
"sender": ["35103e980aae", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["35103e980aae", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -969,7 +975,7 @@
{
"id": "pr-sidebar-load.result-absent:pr",
"observation": {
"sender": ["35103e980aae", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["35103e980aae", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -981,19 +987,19 @@
{
"id": "pr-sidebar-load.result-absent:loaded",
"observation": {
"sender": ["35103e980aae", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["35103e980aae", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.result-null:branch-hint",
"observation": {
"sender": ["9f5541cd3208", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["9f5541cd3208", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1005,7 +1011,7 @@
{
"id": "pr-sidebar-load.result-null:pr",
"observation": {
"sender": ["9f5541cd3208", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["9f5541cd3208", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1017,19 +1023,19 @@
{
"id": "pr-sidebar-load.result-null:loaded",
"observation": {
"sender": ["9f5541cd3208", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["9f5541cd3208", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.inner-ok-missing:branch-hint",
"observation": {
"sender": ["489d375fcdeb", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["489d375fcdeb", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1041,7 +1047,7 @@
{
"id": "pr-sidebar-load.inner-ok-missing:pr",
"observation": {
"sender": ["489d375fcdeb", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["489d375fcdeb", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1053,19 +1059,19 @@
{
"id": "pr-sidebar-load.inner-ok-missing:loaded",
"observation": {
"sender": ["489d375fcdeb", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["489d375fcdeb", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.inner-false-string-error:branch-hint",
"observation": {
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1077,7 +1083,7 @@
{
"id": "pr-sidebar-load.inner-false-string-error:pr",
"observation": {
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1089,19 +1095,19 @@
{
"id": "pr-sidebar-load.inner-false-string-error:loaded",
"observation": {
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.inner-false-object-error:branch-hint",
"observation": {
"sender": ["879f943fd73b", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["879f943fd73b", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1113,7 +1119,7 @@
{
"id": "pr-sidebar-load.inner-false-object-error:pr",
"observation": {
"sender": ["879f943fd73b", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["879f943fd73b", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1125,19 +1131,19 @@
{
"id": "pr-sidebar-load.inner-false-object-error:loaded",
"observation": {
"sender": ["879f943fd73b", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["879f943fd73b", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.outer-refused:branch-hint",
"observation": {
"sender": ["e8a558288b9c", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["e8a558288b9c", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1149,7 +1155,7 @@
{
"id": "pr-sidebar-load.outer-refused:pr",
"observation": {
"sender": ["e8a558288b9c", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["e8a558288b9c", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1161,19 +1167,19 @@
{
"id": "pr-sidebar-load.outer-refused:loaded",
"observation": {
"sender": ["e8a558288b9c", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["e8a558288b9c", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.outer-refused-no-message:branch-hint",
"observation": {
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1185,7 +1191,7 @@
{
"id": "pr-sidebar-load.outer-refused-no-message:pr",
"observation": {
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1197,19 +1203,19 @@
{
"id": "pr-sidebar-load.outer-refused-no-message:loaded",
"observation": {
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.method-not-found:branch-hint",
"observation": {
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1221,7 +1227,7 @@
{
"id": "pr-sidebar-load.method-not-found:pr",
"observation": {
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1233,19 +1239,19 @@
{
"id": "pr-sidebar-load.method-not-found:loaded",
"observation": {
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.transport-rejection:branch-hint",
"observation": {
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1257,7 +1263,7 @@
{
"id": "pr-sidebar-load.transport-rejection:pr",
"observation": {
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1269,19 +1275,19 @@
{
"id": "pr-sidebar-load.transport-rejection:loaded",
"observation": {
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.transport-rejection-no-message:branch-hint",
"observation": {
"sender": ["636ab64ed281", "7cbf1de78fc1", "cc4dd30d2afd"],
"sender": ["636ab64ed281", "7cbf1de78fc1", "4780ceb8db5b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1293,7 +1299,7 @@
{
"id": "pr-sidebar-load.transport-rejection-no-message:pr",
"observation": {
"sender": ["636ab64ed281", "7cbf1de78fc1", "973a6e87065b", "4fae27394886"],
"sender": ["636ab64ed281", "7cbf1de78fc1", "8c5720abe38c", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1305,12 +1311,12 @@
{
"id": "pr-sidebar-load.transport-rejection-no-message:loaded",
"observation": {
"sender": ["636ab64ed281", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"sender": ["636ab64ed281", "7cbf1de78fc1", "8c5720abe38c", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
}
@@ -4,6 +4,57 @@
"family": "session.pr-sidebar",
"namedDeltas": [],
"values": {
"0016d5409c8c": {
"name": "github.prForBranch#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"03dabf7904c5": {
"name": "worktree.show#1",
"ordinal": 2,
@@ -173,57 +224,6 @@
}
}
},
"1c1552d03dfc": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"3fdee65f324d": {
"name": "worktree.show#1",
"ordinal": 2,
@@ -256,40 +256,6 @@
}
}
},
"4777f063108d": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"4b26820600b0": {
"name": "hostedReview.forBranch#1",
"ordinal": 3,
@@ -397,55 +363,86 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo-9::/w\"}}"
},
"6b20a29acbfe": {
"name": "github.prForBranch#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
"734ec9d15b54": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
}
}
},
"kind": "ready"
}
},
"7cbf1de78fc1": {
@@ -521,6 +518,40 @@
"status": "pending",
"startedAt": 0
},
"96a1d9584809": {
"name": "github.prForBranch#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"9a7a01989014": {
"name": "github.prChecks#1",
"ordinal": 8,
@@ -595,6 +626,40 @@
"startedAt": 0
}
},
"a98f4468b85b": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"ade44fd82eca": {
"name": "worktree.show#1",
"ordinal": 2,
@@ -630,103 +695,78 @@
}
}
},
"ccc790f6ce35": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"cf54ab7bda72": {
"name": "github.prForBranch#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"github.prForBranch\",\"params\":{\"repo\":\"id:repo-9\",\"branch\":\"feature\",\"linkedPRNumber\":null,\"fallbackPRNumber\":12,\"currentHeadOid\":null}}"
},
"cfed3ed2a0ae": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
}
},
"kind": "ready"
}
},
"dcec20ae0055": {
"name": "github.prChecks#1",
"ordinal": 9,
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-9\",\"prNumber\":12,\"headSha\":\"head-sha-1\"}}"
},
"e169ebaf642b": "unloaded",
"e7df27480ce9": {
"e93235fba8e6": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -794,40 +834,6 @@
},
"kind": "ready"
},
"eb44c4f75728": {
"name": "github.prForBranch#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"f14112e24d21": {
"name": "worktree.show#1",
"ordinal": 2,
@@ -941,7 +947,7 @@
{
"id": "pr-sidebar-load.normal:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "4777f063108d"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "a98f4468b85b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -953,7 +959,7 @@
{
"id": "pr-sidebar-load.normal:pr",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "4fae27394886"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -965,19 +971,19 @@
{
"id": "pr-sidebar-load.normal:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
},
{
"id": "pr-sidebar-load.result-absent:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "7eb2a61711fc", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "7eb2a61711fc", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -989,7 +995,7 @@
{
"id": "pr-sidebar-load.result-absent:pr",
"observation": {
"sender": ["07639c4f1e7f", "7eb2a61711fc", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "7eb2a61711fc", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1001,19 +1007,19 @@
{
"id": "pr-sidebar-load.result-absent:loaded",
"observation": {
"sender": ["07639c4f1e7f", "7eb2a61711fc", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "7eb2a61711fc", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.result-null:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "552f85dcd6b4", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "552f85dcd6b4", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1025,7 +1031,7 @@
{
"id": "pr-sidebar-load.result-null:pr",
"observation": {
"sender": ["07639c4f1e7f", "552f85dcd6b4", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "552f85dcd6b4", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1037,19 +1043,19 @@
{
"id": "pr-sidebar-load.result-null:loaded",
"observation": {
"sender": ["07639c4f1e7f", "552f85dcd6b4", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "552f85dcd6b4", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.inner-ok-missing:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1061,7 +1067,7 @@
{
"id": "pr-sidebar-load.inner-ok-missing:pr",
"observation": {
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1073,19 +1079,19 @@
{
"id": "pr-sidebar-load.inner-ok-missing:loaded",
"observation": {
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.inner-false-string-error:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "ade44fd82eca", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "ade44fd82eca", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1097,7 +1103,7 @@
{
"id": "pr-sidebar-load.inner-false-string-error:pr",
"observation": {
"sender": ["07639c4f1e7f", "ade44fd82eca", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "ade44fd82eca", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1109,19 +1115,19 @@
{
"id": "pr-sidebar-load.inner-false-string-error:loaded",
"observation": {
"sender": ["07639c4f1e7f", "ade44fd82eca", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "ade44fd82eca", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.inner-false-object-error:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "f8ad59a07e10", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "f8ad59a07e10", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1133,7 +1139,7 @@
{
"id": "pr-sidebar-load.inner-false-object-error:pr",
"observation": {
"sender": ["07639c4f1e7f", "f8ad59a07e10", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "f8ad59a07e10", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1145,19 +1151,19 @@
{
"id": "pr-sidebar-load.inner-false-object-error:loaded",
"observation": {
"sender": ["07639c4f1e7f", "f8ad59a07e10", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "f8ad59a07e10", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.outer-refused:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "f14112e24d21", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "f14112e24d21", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1169,7 +1175,7 @@
{
"id": "pr-sidebar-load.outer-refused:pr",
"observation": {
"sender": ["07639c4f1e7f", "f14112e24d21", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "f14112e24d21", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1181,19 +1187,19 @@
{
"id": "pr-sidebar-load.outer-refused:loaded",
"observation": {
"sender": ["07639c4f1e7f", "f14112e24d21", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "f14112e24d21", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.outer-refused-no-message:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "5087d259003b", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "5087d259003b", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1205,7 +1211,7 @@
{
"id": "pr-sidebar-load.outer-refused-no-message:pr",
"observation": {
"sender": ["07639c4f1e7f", "5087d259003b", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "5087d259003b", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1217,19 +1223,19 @@
{
"id": "pr-sidebar-load.outer-refused-no-message:loaded",
"observation": {
"sender": ["07639c4f1e7f", "5087d259003b", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "5087d259003b", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.method-not-found:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "1a1f37b92aca", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "1a1f37b92aca", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1241,7 +1247,7 @@
{
"id": "pr-sidebar-load.method-not-found:pr",
"observation": {
"sender": ["07639c4f1e7f", "1a1f37b92aca", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "1a1f37b92aca", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1253,19 +1259,19 @@
{
"id": "pr-sidebar-load.method-not-found:loaded",
"observation": {
"sender": ["07639c4f1e7f", "1a1f37b92aca", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "1a1f37b92aca", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.transport-rejection:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "03dabf7904c5", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "03dabf7904c5", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1277,7 +1283,7 @@
{
"id": "pr-sidebar-load.transport-rejection:pr",
"observation": {
"sender": ["07639c4f1e7f", "03dabf7904c5", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "03dabf7904c5", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1289,19 +1295,19 @@
{
"id": "pr-sidebar-load.transport-rejection:loaded",
"observation": {
"sender": ["07639c4f1e7f", "03dabf7904c5", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "03dabf7904c5", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
},
{
"id": "pr-sidebar-load.transport-rejection-no-message:branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "3fdee65f324d", "eb44c4f75728"],
"sender": ["07639c4f1e7f", "3fdee65f324d", "96a1d9584809"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1313,7 +1319,7 @@
{
"id": "pr-sidebar-load.transport-rejection-no-message:pr",
"observation": {
"sender": ["07639c4f1e7f", "3fdee65f324d", "6b20a29acbfe", "a6cb6a8ef089"],
"sender": ["07639c4f1e7f", "3fdee65f324d", "0016d5409c8c", "a6cb6a8ef089"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -1325,12 +1331,12 @@
{
"id": "pr-sidebar-load.transport-rejection-no-message:loaded",
"observation": {
"sender": ["07639c4f1e7f", "3fdee65f324d", "6b20a29acbfe", "13bc6662625a"],
"sender": ["07639c4f1e7f", "3fdee65f324d", "0016d5409c8c", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": []
}
}
File diff suppressed because it is too large Load Diff
@@ -45,45 +45,20 @@
}
}
},
"79b747202f2f": {
"check-details": {
"ok": true,
"result": {
"annotations": [],
"completedAt": {
"$rpc": "null"
},
"conclusion": "success",
"detailsUrl": {
"$rpc": "null"
},
"jobs": [],
"name": "build",
"startedAt": {
"$rpc": "null"
},
"status": "completed",
"summary": {
"$rpc": "null"
},
"text": {
"$rpc": "null"
},
"title": {
"$rpc": "null"
},
"url": {
"$rpc": "null"
}
}
},
"checks": {
"3d3e04f5c1cb": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -183,6 +158,28 @@
}
}
},
"9ad46f0a24c7": {
"checks": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
]
}
},
"c0db062cb341": {
"name": "github.prChecks#1",
"ordinal": 1,
@@ -234,7 +231,43 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.prCheckDetails\",\"params\":{\"repo\":\"id:repo-9\",\"checkRunId\":7,\"checkName\":\"build\",\"url\":null,\"prRepo\":{\"owner\":\"fork-owner\",\"repo\":\"fork-repo\",\"host\":\"github.enterprise.test\"}}}"
},
"daf4d0570339": {
"dd2205e9f2fb": {
"name": "github.prChecks#2",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-9\",\"prNumber\":12}}"
},
"ff556464fa8e": {
"check-details": {
"ok": true,
"result": {
"annotations": [],
"completedAt": {
"$rpc": "null"
},
"conclusion": "success",
"detailsUrl": {
"$rpc": "null"
},
"jobs": [],
"name": "build",
"startedAt": {
"$rpc": "null"
},
"status": "completed",
"summary": {
"$rpc": "null"
},
"text": {
"$rpc": "null"
},
"title": {
"$rpc": "null"
},
"url": {
"$rpc": "null"
}
}
},
"checks": {
"ok": true,
"result": [
@@ -242,33 +275,9 @@
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"observedHeadSha": {
"$rpc": "undefined"
}
}
]
}
},
"dd2205e9f2fb": {
"name": "github.prChecks#2",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-9\",\"prNumber\":12}}"
},
"e23eb2e4b033": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -290,9 +299,9 @@
"sender": ["c0db062cb341"],
"payloads": ["08e5cc0c504e"],
"settlements": {
"fork-checks": "e23eb2e4b033"
"fork-checks": "3d3e04f5c1cb"
},
"state": "daf4d0570339",
"state": "9ad46f0a24c7",
"effects": []
}
},
@@ -302,10 +311,10 @@
"sender": ["c0db062cb341", "997e2aac6183"],
"payloads": ["08e5cc0c504e", "d982d58879cb"],
"settlements": {
"fork-checks": "e23eb2e4b033",
"fork-checks": "3d3e04f5c1cb",
"fork-check-details": "1c88fe396b45"
},
"state": "79b747202f2f",
"state": "ff556464fa8e",
"effects": []
}
},
@@ -315,11 +324,11 @@
"sender": ["c0db062cb341", "997e2aac6183", "88a456608739"],
"payloads": ["08e5cc0c504e", "d982d58879cb", "dd2205e9f2fb"],
"settlements": {
"fork-checks": "e23eb2e4b033",
"fork-checks": "3d3e04f5c1cb",
"fork-check-details": "1c88fe396b45",
"no-head-sha": "e23eb2e4b033"
"no-head-sha": "3d3e04f5c1cb"
},
"state": "79b747202f2f",
"state": "ff556464fa8e",
"effects": []
}
}
+254 -242
View File
@@ -4,6 +4,180 @@
"family": "github.pr-read",
"namedDeltas": [],
"values": {
"026ad16c841b": {
"checks": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
]
},
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"pr-for-branch": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
},
"work-item": {
"ok": true,
"result": {
"assignees": {
"$rpc": "undefined"
},
"baseSha": {
"$rpc": "undefined"
},
"body": "body",
"checks": [],
"comments": [],
"headSha": "head-sha-1",
"item": {
"assignees": [],
"author": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"baseRefName": {
"$rpc": "undefined"
},
"branchName": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"id": "PR_1",
"labels": [],
"latestReviews": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": {
"$rpc": "undefined"
},
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"reviewRequests": [],
"state": "open",
"title": "Recorded",
"type": "pr",
"updatedAt": "",
"url": ""
},
"participants": [],
"pullRequestId": {
"$rpc": "undefined"
}
}
}
},
"0644012ff2a3": {
"status": "fulfilled",
"startedAt": 0,
@@ -138,17 +312,7 @@
"ordinal": 2,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-9\"}}"
},
"274a4d26a91b": {
"assignable": {
"ok": true,
"result": [
{
"avatarUrl": "",
"login": "octocat",
"name": "Octo Cat"
}
]
},
"2709bde96386": {
"check-details": {
"ok": true,
"result": {
@@ -187,6 +351,9 @@
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -350,14 +517,20 @@
}
}
},
"3fb2c86ce340": {
"checks": {
"3d3e04f5c1cb": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -367,158 +540,6 @@
}
}
]
},
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"pr-for-branch": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
},
"work-item": {
"ok": true,
"result": {
"assignees": {
"$rpc": "undefined"
},
"baseSha": {
"$rpc": "undefined"
},
"body": "body",
"checks": [],
"comments": [],
"headSha": "head-sha-1",
"item": {
"assignees": [],
"author": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"baseRefName": {
"$rpc": "undefined"
},
"branchName": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"id": "PR_1",
"labels": [],
"latestReviews": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": {
"$rpc": "undefined"
},
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"reviewRequests": [],
"state": "open",
"title": "Recorded",
"type": "pr",
"updatedAt": "",
"url": ""
},
"participants": [],
"pullRequestId": {
"$rpc": "undefined"
}
}
}
},
"41113a109089": {
@@ -761,7 +782,58 @@
"ordinal": 12,
"json": "{\"id\":\"frame-6\",\"deviceToken\":\"recording-device\",\"method\":\"github.prCheckDetails\",\"params\":{\"repo\":\"id:repo-9\",\"checkRunId\":7,\"checkName\":\"build\",\"url\":null}}"
},
"8ad23d6b64f4": {
"9a87ba2ee54f": {
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
}
},
"9bb9d9b8c502": {
"assignable": {
"ok": true,
"result": [
{
"avatarUrl": "",
"login": "octocat",
"name": "Octo Cat"
}
]
},
"check-details": {
"ok": true,
"result": {
@@ -800,6 +872,9 @@
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -963,48 +1038,7 @@
}
}
},
"9a87ba2ee54f": {
"hosted-review": {
"ok": true,
"result": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksSummary": {
"$rpc": "undefined"
},
"headSha": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"provider": "github",
"reviewDecision": {
"$rpc": "undefined"
},
"state": "open",
"status": "pending",
"title": "Recorded",
"updatedAt": "2026-01-01",
"url": "https://x/12"
}
},
"repo-slug": {
"ok": true,
"result": {
"host": "github.com",
"owner": "orca",
"repo": "orca"
}
}
},
"a0d70c59fdd1": {
"a20f0d944ba5": {
"name": "github.prForBranch#1",
"ordinal": 5,
"args": [
@@ -1031,7 +1065,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -1377,28 +1411,6 @@
"ordinal": 14,
"json": "{\"id\":\"frame-7\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}"
},
"e23eb2e4b033": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
]
}
},
"e97a125cdb88": {
"name": "github.prCheckDetails#1",
"ordinal": 11,
@@ -1578,7 +1590,7 @@
{
"id": "pr-for-branch",
"observation": {
"sender": ["191e3c96ad53", "4bf8f27dd319", "a0d70c59fdd1"],
"sender": ["191e3c96ad53", "4bf8f27dd319", "a20f0d944ba5"],
"payloads": ["1f5765918736", "cdbefce64e00", "1554c8f6753b"],
"settlements": {
"repo-slug": "d89e7b8ce2a0",
@@ -1592,7 +1604,7 @@
{
"id": "work-item",
"observation": {
"sender": ["191e3c96ad53", "4bf8f27dd319", "a0d70c59fdd1", "471a75cee137"],
"sender": ["191e3c96ad53", "4bf8f27dd319", "a20f0d944ba5", "471a75cee137"],
"payloads": ["1f5765918736", "cdbefce64e00", "1554c8f6753b", "0a9f2ff19df4"],
"settlements": {
"repo-slug": "d89e7b8ce2a0",
@@ -1610,7 +1622,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"fa4bf991415d"
],
@@ -1626,9 +1638,9 @@
"hosted-review": "0644012ff2a3",
"pr-for-branch": "d01444e13d4c",
"work-item": "d036e2bdda12",
"checks": "e23eb2e4b033"
"checks": "3d3e04f5c1cb"
},
"state": "3fb2c86ce340",
"state": "026ad16c841b",
"effects": []
}
},
@@ -1638,7 +1650,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"fa4bf991415d",
"e97a125cdb88"
@@ -1656,10 +1668,10 @@
"hosted-review": "0644012ff2a3",
"pr-for-branch": "d01444e13d4c",
"work-item": "d036e2bdda12",
"checks": "e23eb2e4b033",
"checks": "3d3e04f5c1cb",
"check-details": "1c88fe396b45"
},
"state": "8ad23d6b64f4",
"state": "2709bde96386",
"effects": []
}
},
@@ -1669,7 +1681,7 @@
"sender": [
"191e3c96ad53",
"4bf8f27dd319",
"a0d70c59fdd1",
"a20f0d944ba5",
"471a75cee137",
"fa4bf991415d",
"e97a125cdb88",
@@ -1691,11 +1703,11 @@
"hosted-review": "0644012ff2a3",
"pr-for-branch": "d01444e13d4c",
"work-item": "d036e2bdda12",
"checks": "e23eb2e4b033",
"checks": "3d3e04f5c1cb",
"check-details": "1c88fe396b45",
"assignable": "a93bcc7122e8"
},
"state": "274a4d26a91b",
"state": "9bb9d9b8c502",
"effects": ["0c83b4de61f4"]
}
}
@@ -4,6 +4,54 @@
"family": "github.pr-read",
"namedDeltas": [],
"values": {
"0159703ebadf": {
"name": "github.prForBranch#2",
"ordinal": 3,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": {
"$rpc": "null"
},
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-2",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"state": "open"
}
}
}
}
},
"04c85ffba34f": {
"pr-for-branch": {
"error": "The host sent a reply this app could not read (github.prForBranch)",
@@ -37,7 +85,7 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.prForBranch\",\"params\":{\"repo\":\"id:repo-9\",\"branch\":\"feature\",\"linkedPRNumber\":null,\"fallbackPRNumber\":null,\"currentHeadOid\":null}}"
},
"302342a5179d": {
"2df5868d46c4": {
"name": "github.prForBranch#1",
"ordinal": 1,
"args": [
@@ -64,7 +112,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -89,71 +137,7 @@
"ok": false
}
},
"67ff0839185a": {
"name": "github.prForBranch#2",
"ordinal": 3,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": {
"$rpc": "null"
},
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-2",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"state": "open"
}
}
}
}
},
"7c6cc446e5da": {
"name": "github.prForBranch#3",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"github.prForBranch\",\"params\":{\"repo\":\"id:repo-9\",\"branch\":\"feature\",\"linkedPRNumber\":null,\"fallbackPRNumber\":null,\"currentHeadOid\":null}}"
},
"8a5cb8b66303": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": {
"$rpc": "null"
}
}
},
"c75314a70300": {
"6ecd2f6475ab": {
"name": "github.prForBranch#3",
"ordinal": 5,
"args": [
@@ -180,7 +164,7 @@
{
"name": "options",
"value": {
"$rpc": "absent"
"beforeSend": "[local beforeSend]"
}
}
],
@@ -197,6 +181,22 @@
}
}
},
"7c6cc446e5da": {
"name": "github.prForBranch#3",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"github.prForBranch\",\"params\":{\"repo\":\"id:repo-9\",\"branch\":\"feature\",\"linkedPRNumber\":null,\"fallbackPRNumber\":null,\"currentHeadOid\":null}}"
},
"8a5cb8b66303": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"ok": true,
"result": {
"$rpc": "null"
}
}
},
"fe9c1046b91d": {
"status": "fulfilled",
"startedAt": 0,
@@ -213,7 +213,7 @@
{
"id": "upstream",
"observation": {
"sender": ["302342a5179d"],
"sender": ["2df5868d46c4"],
"payloads": ["1a108e871484"],
"settlements": {
"upstream": "fe9c1046b91d"
@@ -225,7 +225,7 @@
{
"id": "malformed",
"observation": {
"sender": ["302342a5179d", "67ff0839185a"],
"sender": ["2df5868d46c4", "0159703ebadf"],
"payloads": ["1a108e871484", "213941c6e0b6"],
"settlements": {
"upstream": "fe9c1046b91d",
@@ -238,7 +238,7 @@
{
"id": "no-pr",
"observation": {
"sender": ["302342a5179d", "67ff0839185a", "c75314a70300"],
"sender": ["2df5868d46c4", "0159703ebadf", "6ecd2f6475ab"],
"payloads": ["1a108e871484", "213941c6e0b6", "7c6cc446e5da"],
"settlements": {
"upstream": "fe9c1046b91d",
@@ -65,91 +65,6 @@
"variant": "worktree-summary"
}
},
"1c1552d03dfc": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"4777f063108d": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"4b26820600b0": {
"name": "hostedReview.forBranch#1",
"ordinal": 3,
@@ -389,6 +304,91 @@
"status": "pending",
"startedAt": 0
},
"a98f4468b85b": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"ccc790f6ce35": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"dcec20ae0055": {
"name": "github.prChecks#1",
"ordinal": 9,
@@ -402,7 +402,7 @@
{
"id": "branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "4777f063108d"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "a98f4468b85b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -414,7 +414,7 @@
{
"id": "pr",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "4fae27394886"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -426,7 +426,7 @@
{
"id": "checks-refused",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "54d7c17a92f3"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "54d7c17a92f3"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "8b09f9d581f7"
+170 -164
View File
@@ -65,91 +65,6 @@
"variant": "worktree-summary"
}
},
"1c1552d03dfc": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"4777f063108d": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"4b26820600b0": {
"name": "hostedReview.forBranch#1",
"ordinal": 3,
@@ -188,6 +103,88 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo-9::/w\"}}"
},
"734ec9d15b54": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
}
},
"kind": "ready"
}
},
"7cbf1de78fc1": {
"name": "worktree.show#1",
"ordinal": 2,
@@ -271,83 +268,89 @@
}
}
},
"cfed3ed2a0ae": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"status": "completed",
"url": {
"$rpc": "null"
},
"workflowRunId": {
"$rpc": "undefined"
}
}
],
"checksError": {
"$rpc": "null"
},
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
"a98f4468b85b": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"autoMergeEnabled": {
"$rpc": "undefined"
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
"headDivergedFromMergedPRAtOid": {
"$rpc": "undefined"
},
"headRefName": {
"$rpc": "undefined"
},
"headSha": "head-sha-1",
"mergeMethodSettings": {
"$rpc": "undefined"
},
"mergeQueueRequired": {
"$rpc": "undefined"
},
"mergeStateStatus": {
"$rpc": "undefined"
},
"mergeable": "MERGEABLE",
"number": 12,
"prRepo": {
"$rpc": "undefined"
},
"reviewDecision": {
"$rpc": "undefined"
},
"stack": {
"$rpc": "undefined"
},
"state": "open",
"title": "Recorded",
"updatedAt": "",
"url": "https://x/12"
},
"pushTarget": {
"$rpc": "null"
"repo": "id:repo-9"
}
},
"kind": "ready"
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "pending",
"startedAt": 0
}
},
"ccc790f6ce35": {
"name": "github.prForBranch#1",
"ordinal": 6,
"args": [
{
"name": "method",
"value": "github.prForBranch"
},
{
"name": "params",
"value": {
"branch": "feature",
"currentHeadOid": {
"$rpc": "null"
},
"fallbackPRNumber": 12,
"linkedPRNumber": {
"$rpc": "null"
},
"repo": "id:repo-9"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"fetchedAt": 0,
"kind": "found",
"pr": {
"headSha": "head-sha-1",
"mergeable": "MERGEABLE",
"number": 12,
"state": "open",
"title": "Recorded",
"url": "https://x/12"
}
}
}
}
},
"dcec20ae0055": {
@@ -356,13 +359,16 @@
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-9\",\"prNumber\":12,\"headSha\":\"head-sha-1\"}}"
},
"e169ebaf642b": "unloaded",
"e7df27480ce9": {
"e93235fba8e6": {
"data": {
"checks": [
{
"checkRunId": 7,
"conclusion": "success",
"name": "build",
"observedHeadSha": {
"$rpc": "undefined"
},
"status": "completed",
"url": {
"$rpc": "null"
@@ -437,7 +443,7 @@
{
"id": "branch-hint",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "4777f063108d"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "a98f4468b85b"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -449,7 +455,7 @@
{
"id": "pr",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "4fae27394886"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "4fae27394886"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "9270aeb7d9c6"
@@ -461,12 +467,12 @@
{
"id": "loaded",
"observation": {
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "ccc790f6ce35", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "cfed3ed2a0ae"
"load": "734ec9d15b54"
},
"state": "e7df27480ce9",
"state": "e93235fba8e6",
"effects": ["15c9074079de"]
}
}
@@ -4,73 +4,6 @@
"family": "tasks.item-review-github",
"namedDeltas": [],
"values": {
"024b63e57ad7": {
"name": "detailPayload",
"ordinal": 25,
"value": {
"assignees": ["octocat"],
"baseSha": "base-sha",
"body": "body",
"checks": [
{
"conclusion": "SUCCESS",
"name": "build",
"status": "COMPLETED",
"url": ""
}
],
"comments": [
{
"author": "octocat",
"body": "please fix",
"createdAt": "2020-01-01T00:00:00.000Z",
"id": 501,
"isResolved": false,
"line": 12,
"path": "src/index.ts",
"threadId": "thread-1"
},
{
"author": "octocat",
"body": "a thought",
"createdAt": "2020-01-01T00:00:00.000Z",
"id": "comment-2"
}
],
"files": [
{
"additions": 2,
"deletions": 1,
"oldPath": {
"$rpc": "undefined"
},
"path": "src/index.ts",
"status": "modified",
"viewerViewedState": "UNVIEWED"
}
],
"headSha": "head-sha",
"labels": ["bug"],
"latestReviews": [],
"provider": "github",
"pullRequestId": "PR_kwDO",
"reviewDecision": {
"$rpc": "null"
},
"reviewRequests": [
{
"avatarUrl": {
"$rpc": "null"
},
"login": "octocat",
"name": {
"$rpc": "null"
}
}
],
"targetKey": "repo-1\u0000owner/repo\u0000pr\u000012"
}
},
"04f768fb79f6": {
"name": "actionItem",
"ordinal": 6,
@@ -158,58 +91,61 @@
"targetKey": "repo-1\u0000owner/repo\u0000pr\u000012"
}
},
"0bcfcbfb9718": {
"name": "items",
"ordinal": 23,
"value": [
{
"provider": "github",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 1,
"pending": 0,
"state": "success",
"total": 1
},
"id": "github:pr:12",
"labels": ["bug"],
"latestReviews": [],
"number": 12,
"prRepo": {
"host": "github.com",
"owner": "owner",
"repo": "repo"
},
"repoId": "repo-1",
"reviewDecision": {
"$rpc": "null"
},
"reviewRequests": [
{
"avatarUrl": {
"$rpc": "null"
},
"login": "octocat",
"name": {
"$rpc": "null"
}
}
],
"state": "open",
"type": "pr",
"url": "https://github.com/owner/repo/pull/12"
"1c3b130f9436": {
"name": "actionItem",
"ordinal": 26,
"value": {
"provider": "github",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 1,
"pending": 0,
"state": "success",
"total": 1
},
"title": "A pull request"
}
]
"id": "github:pr:12",
"labels": ["bug"],
"latestReviews": [],
"number": 12,
"prRepo": {
"host": "github.com",
"owner": "owner",
"repo": "repo"
},
"repoId": "repo-1",
"reviewDecision": {
"$rpc": "null"
},
"reviewRequests": [
{
"avatarUrl": {
"$rpc": "null"
},
"login": "octocat",
"name": {
"$rpc": "null"
}
}
],
"state": "open",
"type": "pr",
"url": "https://github.com/owner/repo/pull/12"
},
"title": "A pull request"
}
},
"1fc52836dac4": {
"name": "itemReviewersDraft",
"ordinal": 9,
"value": "octocat"
},
"22b1e4293201": {
"name": "mutatingStatus",
"ordinal": 28,
"value": false
},
"26f442c9e2e5": {
"name": "detailPayload",
"ordinal": 7,
@@ -525,19 +461,76 @@
"title": "A pull request"
}
},
"6ebf8126530a": {
"name": "github.prChecks#1",
"ordinal": 22,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"prRepo\":{\"owner\":\"owner\",\"repo\":\"repo\",\"host\":\"github.com\"},\"headSha\":\"head-sha\",\"noCache\":true}}"
"69b4fb187f59": {
"name": "detailPayload",
"ordinal": 27,
"value": {
"assignees": ["octocat"],
"baseSha": "base-sha",
"body": "body",
"checks": [
{
"conclusion": "SUCCESS",
"name": "build",
"status": "COMPLETED",
"url": ""
}
],
"comments": [
{
"author": "octocat",
"body": "please fix",
"createdAt": "2020-01-01T00:00:00.000Z",
"id": 501,
"isResolved": false,
"line": 12,
"path": "src/index.ts",
"threadId": "thread-1"
},
{
"author": "octocat",
"body": "a thought",
"createdAt": "2020-01-01T00:00:00.000Z",
"id": "comment-2"
}
],
"files": [
{
"additions": 2,
"deletions": 1,
"oldPath": {
"$rpc": "undefined"
},
"path": "src/index.ts",
"status": "modified",
"viewerViewedState": "UNVIEWED"
}
],
"headSha": "head-sha",
"labels": ["bug"],
"latestReviews": [],
"provider": "github",
"pullRequestId": "PR_kwDO",
"reviewDecision": {
"$rpc": "null"
},
"reviewRequests": [
{
"avatarUrl": {
"$rpc": "null"
},
"login": "octocat",
"name": {
"$rpc": "null"
}
}
],
"targetKey": "repo-1\u0000owner/repo\u0000pr\u000012"
}
},
"886c60a7e62e": {
"name": "mutatingStatus",
"ordinal": 12,
"value": true
},
"8b5917d7d93c": {
"6f47b9866251": {
"name": "github.prChecks#1",
"ordinal": 20,
"ordinal": 23,
"args": [
{
"name": "method",
@@ -560,6 +553,7 @@
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]",
"timeoutMs": 30000
}
}
@@ -569,7 +563,7 @@
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-2",
"id": "frame-3",
"ok": true,
"result": [
{
@@ -582,65 +576,25 @@
}
}
},
"886c60a7e62e": {
"name": "mutatingStatus",
"ordinal": 12,
"value": true
},
"94caa29bceef": {
"name": "error",
"ordinal": 19,
"value": ""
},
"989e0a05a86f": {
"name": "actionItem",
"ordinal": 24,
"value": {
"provider": "github",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 1,
"pending": 0,
"state": "success",
"total": 1
},
"id": "github:pr:12",
"labels": ["bug"],
"latestReviews": [],
"number": 12,
"prRepo": {
"host": "github.com",
"owner": "owner",
"repo": "repo"
},
"repoId": "repo-1",
"reviewDecision": {
"$rpc": "null"
},
"reviewRequests": [
{
"avatarUrl": {
"$rpc": "null"
},
"login": "octocat",
"name": {
"$rpc": "null"
}
}
],
"state": "open",
"type": "pr",
"url": "https://github.com/owner/repo/pull/12"
},
"title": "A pull request"
}
},
"a17cf032d1cb": {
"name": "mutatingStatus",
"ordinal": 18,
"value": false
},
"acff45feb249": {
"name": "mutatingStatus",
"ordinal": 26,
"value": false
"baa2764f836f": {
"name": "github.prChecks#1",
"ordinal": 24,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"prRepo\":{\"owner\":\"owner\",\"repo\":\"repo\",\"host\":\"github.com\"},\"headSha\":\"head-sha\",\"noCache\":true}}"
},
"c29314fc4896": {
"name": "items",
@@ -681,6 +635,11 @@
}
]
},
"c448fcc040ee": {
"name": "status.get#1",
"ordinal": 22,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}"
},
"c650cf641414": {
"name": "itemReviewersDraft",
"ordinal": 4,
@@ -744,6 +703,93 @@
"ordinal": 13,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.requestPRReviewers\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"prRepo\":{\"owner\":\"owner\",\"repo\":\"repo\",\"host\":\"github.com\"},\"reviewers\":[\"octocat\"]}}"
},
"e38e4b18b697": {
"name": "items",
"ordinal": 25,
"value": [
{
"provider": "github",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 1,
"pending": 0,
"state": "success",
"total": 1
},
"id": "github:pr:12",
"labels": ["bug"],
"latestReviews": [],
"number": 12,
"prRepo": {
"host": "github.com",
"owner": "owner",
"repo": "repo"
},
"repoId": "repo-1",
"reviewDecision": {
"$rpc": "null"
},
"reviewRequests": [
{
"avatarUrl": {
"$rpc": "null"
},
"login": "octocat",
"name": {
"$rpc": "null"
}
}
],
"state": "open",
"type": "pr",
"url": "https://github.com/owner/repo/pull/12"
},
"title": "A pull request"
}
]
},
"e6b60387dab2": {
"name": "status.get#1",
"ordinal": 20,
"args": [
{
"name": "method",
"value": "status.get"
},
{
"name": "params",
"value": {
"$rpc": "undefined"
}
},
{
"name": "options",
"value": {
"beforeSend": "[local beforeSend]",
"failWhenDisconnected": true,
"timeoutMs": 15000
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-2",
"ok": true,
"result": {
"capabilities": ["github.exact-work-item-target.v1"],
"hostDescriptor": {
"installationId": "recorded"
},
"runtimeId": "recorded"
}
}
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
@@ -796,8 +842,8 @@
{
"id": "checks-settled",
"observation": {
"sender": ["cf975363479a", "8b5917d7d93c"],
"payloads": ["d8e1fd46cd19", "6ebf8126530a"],
"sender": ["cf975363479a", "e6b60387dab2", "6f47b9866251"],
"payloads": ["d8e1fd46cd19", "c448fcc040ee", "baa2764f836f"],
"settlements": {
"mount": "eb79a9b3682a",
"reviewers-0": "eb79a9b3682a",
@@ -823,10 +869,10 @@
"a17cf032d1cb",
"94caa29bceef",
"2d8d5e501a0d",
"0bcfcbfb9718",
"989e0a05a86f",
"024b63e57ad7",
"acff45feb249"
"e38e4b18b697",
"1c3b130f9436",
"69b4fb187f59",
"22b1e4293201"
]
}
}
@@ -12576,6 +12576,23 @@
]
}
},
{
"complete": "status.get#1",
"site": "pr-check-discovery",
"params": {
"$undefined": true
},
"reply": {
"ok": true,
"result": {
"runtimeId": "recorded",
"hostDescriptor": {
"installationId": "recorded"
},
"capabilities": ["github.exact-work-item-target.v1"]
}
}
},
{
"checkpoint": "checks-settled"
}
@@ -0,0 +1,119 @@
import { afterEach, expect, it } from 'vitest'
import { buildPrGateRows } from './pr-tab-gates'
import { markCompletePRCheckOwner } from '../../../../src/shared/github/complete-pr-check-owner'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
import { activateMobileUiLocale, mobileI18n } from '../../i18n/mobile-i18n'
const pr: PRInfo = {
number: 42,
title: 'PR',
state: 'open',
url: 'https://github.com/acme/widgets/pull/42',
checksStatus: 'failure',
updatedAt: '2026-11-04',
mergeable: 'UNKNOWN',
headSha: 'head'
}
const checks = [
{ name: 'Build', status: 'completed' as const, conclusion: 'success' as const, url: null }
]
afterEach(() => activateMobileUiLocale('en'))
it.each([
['en', '1 check failing'],
['es', '1 comprobación fallida']
] as const)(
'keeps the whole PR blocking with partial passing rows in %s',
async (locale, title) => {
mobileI18n.addResourceBundle(
'es',
'translation',
{
pullRequest: {
gates: {
checksFailing_one: '{{count}} comprobación fallida',
checksFailing_other: '{{count}} comprobaciones fallidas'
}
}
},
true,
true
)
await activateMobileUiLocale(locale)
const owner = markCompletePRCheckOwner(
{
...pr,
checksSummary: {
state: 'failure',
total: 101,
passed: 100,
failed: 1,
cancelled: 0,
pending: 0,
neutral: 0
}
},
'host-epoch'
)
expect(
buildPrGateRows({ pr: owner, details: null, checks, checksError: null })[0]
).toMatchObject({ key: 'checks', tone: 'blocking', title, subtitle: null })
expect(
buildPrGateRows({ pr: owner, details: null, checks, checksError: 'Rows unavailable' })[0].tone
).toBe('blocking')
// The unchanged host path keeps its existing row-based display.
expect(buildPrGateRows({ pr, details: null, checks, checksError: null })[0].tone).toBe('passed')
}
)
it('does not certify passing rows when complete checks are unavailable', () => {
const owner = markCompletePRCheckOwner(
{ ...pr, checksStatus: 'neutral', checksSummary: undefined },
'host-epoch'
)
expect(buildPrGateRows({ pr: owner, details: null, checks, checksError: null })[0].tone).toBe(
'idle'
)
})
it.each([
['en', 'Cancelled'],
['es', 'Cancelado']
] as const)('keeps a cancelled owner truthful without detail rows in %s', async (locale, title) => {
mobileI18n.addResourceBundle(
'es',
'translation',
{ pullRequest: { checkStatus: { cancelled: 'Cancelado' } } },
true,
true
)
await activateMobileUiLocale(locale)
const owner = markCompletePRCheckOwner(
{
...pr,
checksSummary: {
state: 'failure',
total: 1,
passed: 0,
failed: 1,
cancelled: 1,
pending: 0,
neutral: 0
}
},
'host-epoch'
)
for (const checksError of [null, 'Rows unavailable']) {
expect(buildPrGateRows({ pr: owner, details: null, checks: [], checksError })[0]).toMatchObject(
{ title, tone: 'idle', subtitle: null }
)
}
expect(
buildPrGateRows({
pr: owner,
details: null,
checks: [{ name: 'Build', status: 'completed', conclusion: 'cancelled', url: null }],
checksError: null
})[0].subtitle
).toBeTruthy()
expect(
buildPrGateRows({ pr, details: null, checks: [], checksError: null })[0].subtitle
).toBeNull()
})
@@ -1,3 +1,5 @@
import { isCompletePRCheckOwner } from '../../../../src/shared/github/complete-pr-check-owner'
import { getProviderChecksOutcome } from '../../../../src/shared/source-control/checks/provider-check-summary'
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types'
@@ -34,16 +36,17 @@ export function buildPrGateRows(input: GateInput): PrGateRow[] {
// Null details means still loading; a placeholder means the read failed.
const detailsLoading = input.details === null
return [
checksGate(input.checks, input.checksError),
checksGate(input.checks, input.checksError, input.pr),
threadsGate(details, detailsLoading, input.botAuthorOverrides),
approvalsGate(input.pr, details, detailsLoading),
conflictsGate(input.pr, details?.item.baseRefName ?? null)
]
}
function checksGate(checks: PRCheckDetail[], checksError: string | null): PrGateRow {
function checksGate(checks: PRCheckDetail[], checksError: string | null, pr: PRInfo): PrGateRow {
const base = { key: 'checks', target: 'checks' } as const
if (checksError !== null) {
const complete = isCompletePRCheckOwner(pr)
if ((!complete && checksError !== null) || (complete && !pr.checksSummary)) {
return {
...base,
tone: 'idle',
@@ -51,23 +54,30 @@ function checksGate(checks: PRCheckDetail[], checksError: string | null): PrGate
subtitle: checksError
}
}
const ownerSummary = complete ? pr.checksSummary : undefined
const broken = checks.filter((check) => checkOutcome(check) === 'failure')
if (broken.length > 0) {
const brokenCount = ownerSummary
? ownerSummary.failed - (ownerSummary.cancelled ?? 0)
: broken.length
if (brokenCount > 0) {
return {
...base,
tone: 'blocking',
title:
broken.length === 1
brokenCount === 1
? translate('pullRequest.gates.checksFailing_one', '{{count}} check failing', {
count: broken.length
count: brokenCount
})
: translate('pullRequest.gates.checksFailing_other', '{{count}} checks failing', {
count: broken.length
count: brokenCount
}),
subtitle: broken.map((check) => check.name).join(', ')
subtitle: broken.length > 0 ? broken.map((check) => check.name).join(', ') : null
}
}
const summary = summarizePRChecks(checks)
const rowSummary = summarizePRChecks(checks)
const summary = ownerSummary
? { ...ownerSummary, outcome: getProviderChecksOutcome(ownerSummary), label: rowSummary.label }
: rowSummary
if (summary.pending > 0) {
return {
...base,
@@ -93,7 +103,7 @@ function checksGate(checks: PRCheckDetail[], checksError: string | null): PrGate
...base,
tone: 'idle',
title: translate('pullRequest.checkStatus.cancelled', 'Cancelled'),
subtitle: summary.label
subtitle: checks.length > 0 ? rowSummary.label : null
}
}
if (summary.total === 0) {
@@ -1,3 +1,5 @@
import { prChecksSchema } from './github-pr-entity-reply-schema'
import { detailCheckListSchema } from '../tasks/task-provider-entity-reply-schema'
import { describe, expect, it } from 'vitest'
import { githubPrCheckDetailsSchema } from './github-pr-check-reply-schema'
@@ -30,3 +32,25 @@ describe('optional job log outcome', () => {
}
)
})
it('preserves only immutable optional row identity through both existing readers without losing old or malformed rows', () => {
const payload = JSON.parse(
JSON.stringify([
{
name: 'proved',
status: 'completed',
conclusion: 'action_required',
observedHeadSha: 'a'.repeat(40)
},
{ name: 'legacy', status: 'completed', conclusion: 'success' },
{ name: 'malformed', status: 'completed', conclusion: 'success', observedHeadSha: 'branch' }
])
)
for (const schema of [prChecksSchema, detailCheckListSchema]) {
const checks = schema.parse(payload)
expect(checks).toHaveLength(3)
expect(checks[0]).toMatchObject({ name: 'proved', observedHeadSha: 'a'.repeat(40) })
expect(checks[1].observedHeadSha).toBeUndefined()
expect(checks[2].observedHeadSha).toBeUndefined()
}
})
@@ -1,3 +1,4 @@
import { normalizePRCheckHeadSha } from '../../../src/shared/github/pr-check-head'
import { z } from 'zod'
import { openEnum, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage'
import type { GitHubReaction, PRComment } from '../../../src/shared/github/comment-types'
@@ -236,7 +237,8 @@ const checkDetailSchema = z
conclusion: salvagedOptional('conclusion', z.enum(CHECK_RUN_CONCLUSION)),
url: prText('url'),
checkRunId: prCount('checkRunId'),
workflowRunId: prCount('workflowRunId')
workflowRunId: prCount('workflowRunId'),
observedHeadSha: z.unknown().optional().transform(normalizePRCheckHeadSha)
})
.transform((check): PRCheckDetail => ({
name: check.name,
@@ -244,7 +246,8 @@ const checkDetailSchema = z
conclusion: check.conclusion ?? null,
url: check.url ?? null,
checkRunId: check.checkRunId,
workflowRunId: check.workflowRunId
workflowRunId: check.workflowRunId,
observedHeadSha: check.observedHeadSha
}))
export const prChecksSchema = salvagingArray(checkDetailSchema)
+68 -16
View File
@@ -1,3 +1,9 @@
import {
GITHUB_CONNECTION_CHANGED,
githubOperationResponse
} from '../transport/github-target-invocation'
import { captureMobilePRCheckContract } from '../transport/pr-check-observation-contract'
import { acceptPRCheckObservation } from '../../../src/shared/github/complete-pr-check-owner'
import type { RpcClient } from '../transport/rpc-client'
import { withGitHubTargetOperation } from '../tasks/github-work-item-operation'
import { requireGitHubWorkItemTargetDetails } from '../../../src/shared/github/work-item-target'
@@ -95,7 +101,7 @@ export function fetchHostedReviewForBranch(
}
// Raise decoded upstream-error here so the sidebar retains host text; it is not a protocol decode failure.
export function fetchPRForBranch(
export async function fetchPRForBranch(
client: RpcOperationSender,
worktreeId: string,
args: {
@@ -103,24 +109,70 @@ export function fetchPRForBranch(
linkedPRNumber?: number | null
fallbackPRNumber?: number | null
currentHeadOid?: string | null
refresh?: boolean
}
): Promise<GitHubPrReadOutcome<PRInfo | null>> {
return settleGithubPrRead(
{
operation: githubPrForBranchRead.operation,
interpret: (reply) => resolveGithubPrForBranchOutcome(githubPrForBranchRead.interpret(reply))
},
() =>
githubPrForBranchRead.request(
client,
githubPrRequestParams(githubPrForBranchRead.operation.method, worktreeId, {
branch: args.branch,
linkedPRNumber: args.linkedPRNumber ?? null,
fallbackPRNumber: args.fallbackPRNumber ?? null,
currentHeadOid: args.currentHeadOid ?? null
})
return observe(true, args.refresh === true)
async function observe(
adopt: boolean,
refresh = false
): Promise<GitHubPrReadOutcome<PRInfo | null>> {
const contract = await captureMobilePRCheckContract(
client,
{
repo: mobileRepoSelectorFromWorktreeId(worktreeId)
},
refresh
)
try {
let sourceMismatch = false
const result = await settleGithubPrRead(
{
operation: githubPrForBranchRead.operation,
interpret: (reply) =>
resolveGithubPrForBranchOutcome(githubPrForBranchRead.interpret(reply))
},
async () => {
const reply = await githubPrForBranchRead.request(
contract.client ?? client,
githubPrRequestParams(githubPrForBranchRead.operation.method, worktreeId, {
branch: args.branch,
linkedPRNumber: args.linkedPRNumber ?? null,
fallbackPRNumber: args.fallbackPRNumber ?? null,
currentHeadOid: args.currentHeadOid ?? null
}),
contract.options
)
sourceMismatch =
!reply.ok &&
['runtime_source_mismatch', 'runtime_source_unverifiable'].includes(reply.error.code)
return githubOperationResponse(reply)
}
)
)
if (adopt && sourceMismatch && contract.routeCurrent()) {
return observe(false, true)
}
if (!contract.current()) {
if (adopt && !contract.complete && contract.routeCurrent()) {
return observe(false)
}
return { ok: false, error: GITHUB_CONNECTION_CHANGED }
}
if (result.ok && result.result && contract.complete) {
return {
ok: true,
result: acceptPRCheckObservation(result.result, null, {
scope: contract.scope!,
complete: true,
current: contract.current()
})!
}
}
return result
} finally {
contract.retire()
}
}
}
function resolveGithubPrForBranchOutcome(outcome: GitHubPrForBranchOutcome | null): PRInfo | null {
@@ -80,6 +80,7 @@ export type PrSidebarLoadDeps = {
linkedPRNumber?: number | null
fallbackPRNumber?: number | null
currentHeadOid?: string | null
refresh?: boolean
}
) => Promise<GitHubPrReadOutcome<PRInfo | null>>
fetchWorkItemDetails: (
@@ -131,6 +132,7 @@ export async function loadPrSidebarData(
branch: string
headSha?: string | null
prRepo?: GitHubPrRepoSlug | null
refresh?: boolean
}
): Promise<PrSidebarState> {
try {
@@ -148,7 +150,8 @@ export async function loadPrSidebarData(
branch: args.branch,
linkedPRNumber,
fallbackPRNumber: linkedPRNumber === null ? branchHint : null,
currentHeadOid: requestHeadOid
currentHeadOid: requestHeadOid,
...(args.refresh ? { refresh: true } : {})
})
let linkedPR = link.linkedPR
let prOutcome = await fetchPR(linkedPR)
@@ -0,0 +1,141 @@
import { expect, it, vi } from 'vitest'
import { fetchPRForBranch } from './github-pr-rpc'
import { GITHUB_CONNECTION_CHANGED } from '../transport/github-target-invocation'
import type { RpcClient, SendRequestOptions } from '../transport/rpc-client'
import type { RpcResponse } from '../transport/types'
import { recordRuntimeStatusProbe } from '../transport/runtime-status-probe'
import { isCompletePRCheckOwner } from '../../../src/shared/github/complete-pr-check-owner'
import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as complete } from '../../../src/shared/source-control/runtime/source-control-runtime-capabilities'
const status = (runtimeId: string) => ({
runtimeId,
hostDescriptor: { installationId: 'host' },
capabilities: [complete]
})
const pr = {
number: 42,
state: 'open',
url: 'https://github.com/acme/widgets/pull/42',
headSha: 'h1',
checksStatus: 'failure',
checksSummary: { state: 'failure', total: 101, passed: 100, failed: 1, pending: 0, neutral: 0 }
}
it.each(['adoption', 'restart'])(
'makes one fresh exact read after %s and settles a complete owner',
async (mode) => {
let reads = 0
const sendRequest = vi.fn(async (method: string): Promise<RpcResponse> => {
if (method === 'status.get') {
return { id: 'status', ok: true, result: status(mode === 'restart' ? 'r2' : 'r1') }
}
reads++
if (reads === 1 && mode === 'restart') {
return {
id: 'pr',
ok: false,
error: { code: 'runtime_source_mismatch', message: 'source changed' }
}
}
if (reads === 1 && mode === 'adoption') {
recordRuntimeStatusProbe(client, status('r1'))
}
return { id: 'pr', ok: true, result: pr }
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: supplies the request and connected invocation lifecycle this read uses.
const client = {
sendRequest,
getState: () => 'connected',
getGeneration: () => 1,
getLastConnectedAt: () => 1,
onStateChange: () => () => {}
} as unknown as RpcClient
recordRuntimeStatusProbe(client, mode === 'restart' ? status('r1') : null)
const result = await fetchPRForBranch(client, 'worktree', { branch: 'feature' })
expect(result.ok && result.result && isCompletePRCheckOwner(result.result)).toBe(true)
expect(reads).toBe(2)
expect(sendRequest.mock.calls.filter(([method]) => method === 'status.get')).toHaveLength(
mode === 'restart' ? 1 : 0
)
}
)
function unknownReadSource() {
const probe = Promise.withResolvers<RpcResponse>()
const sendRequest = vi.fn(
(method: string, _params?: unknown, _options?: SendRequestOptions): Promise<RpcResponse> =>
method === 'status.get' ? probe.promise : Promise.resolve({ id: 'pr', ok: true, result: pr })
)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: supplies the connected request lifecycle used by production PR reads.
const client = {
sendRequest,
getState: () => 'connected',
getGeneration: () => 1,
getLastConnectedAt: () => 1,
onStateChange: () => () => {}
} as unknown as RpcClient
return { client, probe, sendRequest }
}
it('settles an ordinary unknown-host PR while discovery is pending and never certifies that old reply', async () => {
const source = unknownReadSource()
const first = await fetchPRForBranch(source.client, 'repo-9::/w', { branch: 'feature' })
expect(first).toMatchObject({
ok: true,
result: { number: 42, checksStatus: 'failure', headSha: 'h1' }
})
expect(first.ok && first.result && isCompletePRCheckOwner(first.result)).toBe(false)
expect(source.sendRequest).toHaveBeenCalledWith(
'github.prForBranch',
{
repo: 'id:repo-9',
branch: 'feature',
linkedPRNumber: null,
fallbackPRNumber: null,
currentHeadOid: null
},
expect.objectContaining({ beforeSend: expect.any(Function) })
)
const discovery = source.sendRequest.mock.calls.find(([method]) => method === 'status.get')!
expect(discovery).toEqual([
'status.get',
undefined,
{ timeoutMs: 15_000, failWhenDisconnected: true, beforeSend: expect.any(Function) }
])
expect(() => discovery[2]!.beforeSend!()).not.toThrow()
source.probe.resolve({ id: 'status', ok: true, result: status('r1') })
await vi.waitFor(() => {
expect(() => discovery[2]!.beforeSend!()).toThrow(GITHUB_CONNECTION_CHANGED)
})
expect(first.ok && first.result && isCompletePRCheckOwner(first.result)).toBe(false)
const fresh = await fetchPRForBranch(source.client, 'repo-9::/w', { branch: 'feature' })
expect(fresh).toMatchObject({
ok: true,
result: { number: 42, checksSummary: { total: 101, passed: 100, failed: 1 } }
})
expect(fresh.ok && fresh.result && isCompletePRCheckOwner(fresh.result)).toBe(true)
})
it.each(['rejected', 'refused'])(
'keeps ordinary legacy PR reads useful after discovery is %s',
async (outcome) => {
const source = unknownReadSource()
const first = await fetchPRForBranch(source.client, 'repo-9::/w', { branch: 'feature' })
expect(first).toMatchObject({ ok: true, result: { number: 42, checksStatus: 'failure' } })
const discovery = source.sendRequest.mock.calls.find(([method]) => method === 'status.get')!
if (outcome === 'rejected') {
source.probe.reject(new Error('transport unavailable'))
} else {
source.probe.resolve({
id: 'status',
ok: false,
error: { code: 'refused', message: 'refused' }
})
}
await vi.waitFor(() => {
expect(() => discovery[2]!.beforeSend!()).toThrow(GITHUB_CONNECTION_CHANGED)
})
const next = await fetchPRForBranch(source.client, 'repo-9::/w', { branch: 'feature' })
expect(next).toMatchObject({ ok: true, result: { number: 42, checksStatus: 'failure' } })
expect(next.ok && next.result && isCompletePRCheckOwner(next.result)).toBe(false)
}
)
@@ -1,3 +1,7 @@
import {
completePRCheckOwnerScope,
acceptPRCheckObservation
} from '../../../src/shared/github/complete-pr-check-owner'
import { useCallback, useRef } from 'react'
import { prSidebarResourceKey, type PrSidebarState } from './mobile-pr-sidebar-state'
@@ -20,6 +24,25 @@ export function settleLoadedPr(
next: PrSidebarState,
background: boolean
): { settled: PrSidebarState; result: 'applied' | 'failed' } {
if (
accepted.kind === 'ready' &&
next.kind === 'ready' &&
completePRCheckOwnerScope(next.data.pr) !== undefined &&
completePRCheckOwnerScope(next.data.pr) === completePRCheckOwnerScope(accepted.data.pr) &&
!next.data.pr.checksSummary
) {
next = {
...next,
data: {
...next.data,
pr: acceptPRCheckObservation(next.data.pr, accepted.data.pr, {
scope: completePRCheckOwnerScope(next.data.pr)!,
complete: true,
current: true
})!
}
}
}
if (!background || !pollChecksReadFailed(accepted, next)) {
return { settled: next, result: 'applied' }
}
@@ -164,7 +164,8 @@ export function useMobilePrSidebarController(input: PrSidebarControllerInput) {
if (!keepVisible) {
publishState({ kind: 'loading' })
}
const next = await loadPrSidebarData(deps, { worktreeId, branch, headSha })
const refresh = !options?.background
const next = await loadPrSidebarData(deps, { worktreeId, branch, headSha, refresh })
if (!owner.live || !shouldApplyResult(seq, owner.loadSeq)) {
return 'stale'
}
@@ -0,0 +1,275 @@
import { expect, it, vi } from 'vitest'
import { refreshMobileTaskPRChecks } from './mobile-task-pr-check-refresh'
import { item as makeItem } from './github-task-identity.test-fixture'
import { githubTaskKey } from './github-task-identity'
import { readRuntimeStatusProbe, recordRuntimeStatusProbe } from '../transport/runtime-status-probe'
import type { RpcClient, SendRequestOptions } from '../transport/rpc-client'
import type { RpcResponse } from '../transport/types'
import type { ActionableTaskItem, TaskItem } from './mobile-tasks-project-workspace-types'
import type { DetailPayload } from './mobile-tasks-provider-detail-types'
import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as complete } from '../../../src/shared/source-control/runtime/source-control-runtime-capabilities'
import { isCompletePRCheckOwner } from '../../../src/shared/github/complete-pr-check-owner'
const failure = {
state: 'failure' as const,
total: 101,
passed: 100,
failed: 1,
pending: 0,
neutral: 0
}
it.each(['owner', 'rows', 'adoption'])(
'publishes the other refresh result when %s fails',
async (fails) => {
const item = makeItem('acme')
const source = { ...item.source, headSha: 'h1' }
const selected = { ...item, source }
let action: ActionableTaskItem | null = selected
let items: TaskItem[] = [selected]
let details: DetailPayload | null = {
provider: 'github',
targetKey: githubTaskKey(source),
body: '',
labels: [],
assignees: [],
comments: [],
reviewRequests: [],
latestReviews: [],
files: [],
checks: [],
headSha: 'h1'
}
const sendRequest = vi.fn(async (method: string) => {
if (method === 'status.get') {
return {
id: 's',
ok: true as const,
result: {
runtimeId: 'r1',
hostDescriptor: { installationId: 'host' },
capabilities: [complete]
}
}
}
if (method === 'github.workItemByOwnerRepo') {
if (fails === 'owner') {
throw new Error('owner failed')
}
return { id: 'o', ok: true as const, result: { ...source, checksSummary: failure } }
}
if (fails === 'rows') {
throw new Error('rows failed')
}
return {
id: 'c',
ok: true as const,
result: [{ name: 'Build', status: 'completed', conclusion: 'success', url: null }]
}
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: supplies the connected request and invocation lifecycle used by this refresh.
const client = {
sendRequest,
getState: () => 'connected',
getGeneration: () => 1,
getLastConnectedAt: () => 1,
onStateChange: () => () => {}
} as unknown as RpcClient
if (fails !== 'adoption') {
recordRuntimeStatusProbe(client, {
runtimeId: 'r1',
hostDescriptor: { installationId: 'host' },
capabilities: [complete]
})
}
const finish = vi.fn()
await refreshMobileTaskPRChecks(
{
client,
detailPayload: details,
tryBeginMutation: () => finish,
setError: vi.fn(),
claimPublication: () => () => true,
setActionItem: (update) => {
action = typeof update === 'function' ? update(action) : update
},
setItems: (update) => {
items = typeof update === 'function' ? update(items) : update
},
setDetailPayload: (update) => {
details = typeof update === 'function' ? update(details) : update
}
},
selected
)
expect(finish).toHaveBeenCalledOnce()
if (fails !== 'owner') {
expect(
sendRequest.mock.calls.filter(([method]) => method === 'github.workItemByOwnerRepo')
).toHaveLength(1)
expect(items[0].provider === 'github' && items[0].source.checksSummary).toEqual(failure)
} else {
expect(details?.provider === 'github' && details.checks).toHaveLength(1)
expect(action?.provider === 'github' && isCompletePRCheckOwner(action.source)).toBe(true)
expect(action?.provider === 'github' && action.source.checksSummary).toBeUndefined()
}
}
)
it.each(['reject', 'refuse', 'unknown', 'retired-selection', 'retired-owner'])(
'settles unknown-host refresh independently of rows: %s',
async (outcome) => {
const item = makeItem('acme')
const selected = {
...item,
source: {
...item.source,
headSha: 'h1',
checksSummary: {
state: 'success' as const,
total: 1,
passed: 1,
failed: 0,
pending: 0,
neutral: 0
}
}
}
let action: ActionableTaskItem | null = selected
let items: TaskItem[] = [selected]
let details: DetailPayload | null = {
provider: 'github',
targetKey: githubTaskKey(selected.source),
body: '',
labels: [],
assignees: [],
comments: [],
reviewRequests: [],
latestReviews: [],
files: [],
checks: [{ name: 'Previous', status: 'completed', conclusion: 'success', url: null }],
headSha: 'h1'
}
const status = Promise.withResolvers<RpcResponse>()
const rows = Promise.withResolvers<RpcResponse>()
const owner = Promise.withResolvers<RpcResponse>()
const rowsStarted = Promise.withResolvers<void>()
const ownerStarted = Promise.withResolvers<void>()
const sendRequest = vi.fn(
async (method: string, _params?: unknown, _options?: SendRequestOptions) => {
if (method === 'status.get') {
return status.promise
}
if (method === 'github.workItemByOwnerRepo') {
ownerStarted.resolve()
return owner.promise
}
rowsStarted.resolve()
return rows.promise
}
)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: connected fixture supplies the request and invocation lifecycle exercised below.
const client = {
sendRequest,
getState: () => 'connected',
getGeneration: () => 1,
getLastConnectedAt: () => 1,
onStateChange: () => () => {}
} as unknown as RpcClient
let current = true
const finish = vi.fn()
const setError = vi.fn()
const pending = refreshMobileTaskPRChecks(
{
client,
detailPayload: details,
tryBeginMutation: () => finish,
setError,
claimPublication: () => () => current,
setActionItem: (update) => {
action = typeof update === 'function' ? update(action) : update
},
setItems: (update) => {
items = typeof update === 'function' ? update(items) : update
},
setDetailPayload: (update) => {
details = typeof update === 'function' ? update(details) : update
}
},
selected
)
await rowsStarted.promise
expect(finish).not.toHaveBeenCalled()
if (outcome === 'unknown') {
rows.resolve({
id: 'c',
ok: true,
result: [{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }]
})
await pending
expect(
sendRequest.mock.calls.filter(([method]) => method === 'github.workItemByOwnerRepo')
).toHaveLength(0)
expect(items[0].provider === 'github' && items[0].source.checksSummary?.state).toBe('failure')
status.resolve({ id: 's', ok: true, result: { capabilities: [] } })
} else {
status.resolve({
id: 's',
ok: true,
result: {
runtimeId: 'r1',
hostDescriptor: { installationId: 'host' },
capabilities: [complete]
}
})
await vi.waitFor(() =>
expect(readRuntimeStatusProbe(client)?.capabilities).toEqual([complete])
)
if (outcome === 'retired-selection') {
current = false
}
if (outcome === 'refuse') {
rows.resolve({ id: 'c', ok: false, error: { code: 'UNAVAILABLE', message: 'rows failed' } })
} else {
rows.reject(new Error('rows failed'))
}
if (outcome !== 'retired-selection') {
await ownerStarted.promise
if (outcome === 'retired-owner') {
current = false
}
owner.resolve({ id: 'o', ok: true, result: { ...selected.source, checksSummary: failure } })
}
await pending
const requests = sendRequest.mock.calls.filter(
([method]) => method === 'github.workItemByOwnerRepo'
)
if (outcome === 'retired-selection') {
expect(requests).toHaveLength(0)
expect(setError.mock.calls).toEqual([['']])
} else {
expect(requests).toHaveLength(1)
expect(requests[0][1]).toMatchObject({
repo: 'id:clone',
owner: 'acme',
ownerRepo: 'widgets',
host: 'github.corp',
number: 93,
type: 'pr'
})
expect(requests[0][2]?.envelope?.expectedRuntimeSource).toEqual({
sourceId: 'host',
runtimeId: 'r1'
})
expect(setError.mock.calls).toEqual([[''], ['rows failed']])
}
const expected = outcome.startsWith('retired') ? selected.source.checksSummary : failure
expect(action?.provider === 'github' && action.source.checksSummary).toEqual(expected)
expect(items[0].provider === 'github' && items[0].source.checksSummary).toEqual(expected)
expect(details?.provider === 'github' && details.checks).toEqual([
{ name: 'Previous', status: 'completed', conclusion: 'success', url: null }
])
}
expect(finish).toHaveBeenCalledOnce()
}
)
@@ -0,0 +1,205 @@
import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types'
import { matchesGitHubWorkItemTarget } from '../../../src/shared/github/work-item-target'
import { acceptPRCheckObservation } from '../../../src/shared/github/complete-pr-check-owner'
import { captureMobilePRCheckContract } from '../transport/pr-check-observation-contract'
import { githubWorkItemBySlugRead } from './mobile-task-source-search-operations'
import { githubTaskKey, sameGitHubTask, requireGitHubTaskTarget } from './github-task-identity'
import { buildGitHubCheckSummary } from './github-check-summary'
import type { TaskItem } from './mobile-tasks-project-workspace-types'
import { githubPullRequestChecksRead } from './mobile-task-item-state-operations'
import type { HostedCommentReviewActionsInput } from './use-mobile-tasks-hosted-comment-review-actions'
type RefreshModel = Pick<
HostedCommentReviewActionsInput,
| 'client'
| 'detailPayload'
| 'tryBeginMutation'
| 'setError'
| 'claimPublication'
| 'setDetailPayload'
| 'setActionItem'
| 'setItems'
>
export async function refreshMobileTaskPRChecks(
model: RefreshModel,
item: Extract<TaskItem, { provider: 'github' }>
): Promise<void> {
const {
client,
detailPayload,
tryBeginMutation,
setError,
claimPublication,
setDetailPayload,
setActionItem,
setItems
} = model
if (!client || item.source.type !== 'pr') {
return
}
const finish = tryBeginMutation()
if (!finish) {
return
}
setError('')
const selected = claimPublication(item, detailPayload)
const requestedHead = detailPayload?.provider === 'github' ? detailPayload.headSha : undefined
let contract = await captureMobilePRCheckContract(
client,
{ repo: `id:${item.source.repoId}` },
true
)
const current = () => selected() && contract.current()
const publish = (owner: GitHubWorkItem | null) => {
const update = (source: typeof item.source) =>
acceptPRCheckObservation(
{
...source,
headSha: owner?.headSha ?? source.headSha,
checksSummary: owner?.checksSummary
},
source,
{ scope: contract.scope!, complete: true, current: current(), trusted: owner !== null }
) ?? source
setActionItem((prior) =>
sameGitHubTask(prior, item) ? { ...prior, source: update(prior.source) } : prior
)
setItems((prior) =>
prior.map((candidate) =>
sameGitHubTask(candidate, item)
? { ...candidate, source: update(candidate.source) }
: candidate
)
)
if (owner) {
setDetailPayload((prior) =>
prior?.provider === 'github' && prior.targetKey === githubTaskKey(item.source)
? {
...prior,
headSha: owner.headSha,
checks: prior.headSha === owner.headSha ? prior.checks : []
}
: prior
)
}
}
try {
const repository = requireGitHubTaskTarget(item.source).repository
const readOwner = async (): Promise<GitHubWorkItem | null> => {
try {
const reply = await githubWorkItemBySlugRead.request(
contract.client ?? client,
{
repo: `id:${item.source.repoId}`,
owner: repository.owner,
ownerRepo: repository.repo,
host: repository.host,
number: item.source.number,
type: 'pr'
},
contract.options
)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this existing lookup parser preserves provider fields; exact identity is validated below before acceptance.
const observed = githubWorkItemBySlugRead.interpret(reply) as GitHubWorkItem | null
if (
matchesGitHubWorkItemTarget(observed, {
repository,
type: 'pr',
number: item.source.number
})
) {
return observed
}
} catch {
/* Rows remain independently useful when the complete owner is unavailable. */
}
return null
}
let owner: GitHubWorkItem | null = null
if (contract.complete) {
owner = await readOwner()
if (!current()) {
return
}
publish(owner)
}
let checks: ReturnType<typeof githubPullRequestChecksRead.interpret> | undefined
try {
const reply = await githubPullRequestChecksRead.request(
contract.client ?? client,
{
repo: `id:${item.source.repoId}`,
prNumber: item.source.number,
prRepo: repository,
headSha: owner?.headSha ?? requestedHead,
noCache: true
},
{ timeoutMs: 30_000, ...contract.options }
)
checks = githubPullRequestChecksRead.interpret(reply)
} catch (error) {
if (selected() && contract.routeCurrent()) {
setError(error instanceof Error ? error.message : 'Failed to refresh checks')
}
}
if (!selected() || !contract.routeCurrent()) {
return
}
if (checks) {
const returnedChecks = checks
setDetailPayload((prior) =>
prior?.provider === 'github' && prior.targetKey === githubTaskKey(item.source)
? { ...prior, checks: returnedChecks }
: prior
)
}
if (!contract.complete && !current() && selected() && contract.routeCurrent()) {
const previous = contract
const replacement = await captureMobilePRCheckContract(client, {
repo: `id:${item.source.repoId}`
})
if (!selected() || !previous.routeCurrent() || !replacement.current()) {
replacement.retire()
return
}
contract = replacement
previous.retire()
if (contract.complete) {
owner = await readOwner()
if (current()) {
publish(owner)
}
}
}
if (checks && !contract.complete && current()) {
const summary = buildGitHubCheckSummary(checks)
const update = (source: typeof item.source) => ({
...source,
checksSummary:
requestedHead !== undefined &&
source.headSha === requestedHead &&
source.checksSummary?.state === summary.state
? source.checksSummary
: summary
})
setActionItem((prior) =>
sameGitHubTask(prior, item) ? { ...prior, source: update(prior.source) } : prior
)
setItems((prior) =>
prior.map((candidate) =>
sameGitHubTask(candidate, item)
? { ...candidate, source: update(candidate.source) }
: candidate
)
)
}
} catch (error) {
if (current()) {
setError(error instanceof Error ? error.message : 'Failed to refresh checks')
}
} finally {
contract.retire()
finish()
}
}
@@ -1,3 +1,4 @@
import { normalizePRCheckHeadSha } from '../../../src/shared/github/pr-check-head'
import { z } from 'zod'
import type { GitHubPRFile } from '../../../src/shared/github/pull-request-types'
import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage'
@@ -62,7 +63,8 @@ export const detailCheckListSchema = salvagingArray(
name: z.string(),
status: z.string(),
conclusion: prNullableText('conclusion'),
url: prNullableText('url')
url: prNullableText('url'),
observedHeadSha: z.unknown().optional().transform(normalizePRCheckHeadSha)
})
)
@@ -1,11 +1,8 @@
import { refreshMobileTaskPRChecks } from './mobile-task-pr-check-refresh'
import { githubTaskKey, sameGitHubTask, requireGitHubTaskTarget } from './github-task-identity'
import { useClipboardWriter } from '../platform/clipboard'
import type { HostedMetadataActionsModel } from './use-mobile-tasks-hosted-metadata-actions'
import {
buildGitHubCheckSummary,
scheduleMobileTaskCopyFeedbackReset,
useCallback
} from './mobile-tasks-dependencies'
import { scheduleMobileTaskCopyFeedbackReset, useCallback } from './mobile-tasks-dependencies'
import {
type DetailComment,
type GitHubAssignableUser,
@@ -17,10 +14,7 @@ import {
gitlabIssueCommentWrite,
gitlabMergeRequestCommentWrite
} from './mobile-task-item-comment-operations'
import {
githubPullRequestChecksRead,
githubReviewerRequest
} from './mobile-task-item-state-operations'
import { githubReviewerRequest } from './mobile-task-item-state-operations'
export type HostedCommentReviewActionsInput = Pick<
HostedMetadataActionsModel,
@@ -272,81 +266,13 @@ export function useMobileTasksHostedCommentReviewActions<
[claimPublication, client, detailPayload, itemReviewersDraft, tryBeginMutation]
)
const refreshGitHubChecks = async (
item: Extract<TaskItem, { provider: 'github' }>
): Promise<void> => {
if (!client || item.source.type !== 'pr') {
return
}
const finish = tryBeginMutation()
if (!finish) {
return
}
setError('')
const isCurrent = claimPublication(item, detailPayload)
const requestedHeadSha =
detailPayload?.provider === 'github' ? detailPayload.headSha : undefined
try {
const reply = await githubPullRequestChecksRead.request(
client,
{
repo: `id:${item.source.repoId}`,
prNumber: item.source.number,
prRepo: requireGitHubTaskTarget(item.source).repository,
headSha: requestedHeadSha,
noCache: true
},
{ timeoutMs: 30_000 }
)
const checks = githubPullRequestChecksRead.interpret(reply)
if (!isCurrent()) {
return
}
const checksSummary = buildGitHubCheckSummary(checks)
const summaryFor = (source: typeof item.source) =>
requestedHeadSha !== undefined &&
source.headSha === requestedHeadSha &&
source.checksSummary?.state === checksSummary.state
? source.checksSummary
: checksSummary
setDetailPayload((current) =>
current?.provider === 'github' && current.targetKey === githubTaskKey(item.source)
? { ...current, checks }
: current
)
setActionItem((current) =>
sameGitHubTask(current, item)
? {
...current,
source: { ...current.source, checksSummary: summaryFor(current.source) }
}
: current
)
setItems((current) =>
current.map((candidate) =>
sameGitHubTask(candidate, item)
? {
...candidate,
source: { ...candidate.source, checksSummary: summaryFor(candidate.source) }
}
: candidate
)
)
} catch (err) {
if (!isCurrent()) {
return
}
setError(err instanceof Error ? err.message : 'Failed to refresh checks')
} finally {
finish()
}
}
return Object.assign(model, {
addHostedItemComment,
copyTaskLink,
copyTextToClipboard,
requestGitHubReviewers,
refreshGitHubChecks
refreshGitHubChecks: (item: Extract<TaskItem, { provider: 'github' }>) =>
refreshMobileTaskPRChecks(model, item)
})
}
@@ -74,6 +74,7 @@ export const C2_WORK_ITEM_CLOSURE_FAMILIES: PageClosurePins = {
'tk-item-reply-merge': { verdict: 'bridge-contract' }
},
'tasks.item-review-github': {
'matrix-tasks.item-review-github-pr-check-discovery': { verdict: 'bridge-contract' },
'matrix-tasks.item-review-github-github.prchecks-1': { verdict: 'bridge-contract' },
'matrix-tasks.item-review-github-github.requestprreviewers-1': { verdict: 'bridge-contract' },
'tk-item-review-github': { verdict: 'bridge-contract' }
@@ -1,6 +1,7 @@
import type { MountAdapter } from '../recording-scenario'
import type { operationModuleLoader } from '../operation-module-loader'
import type * as LineMutations from '../../../session/github-pr-review-line-mutations'
import { legacyShellStatusFixture } from '../legacy-shell-status-fixture'
const WORKTREE = 'repo-9::/w'
const PR_NUMBER = 12
@@ -10,7 +11,8 @@ export function githubPrMountAdapters(
modules: ReturnType<typeof operationModuleLoader>
): Record<string, MountAdapter> {
return {
'session.pr-reads': ({ client }) => {
'session.pr-reads': ({ client: suppliedClient }) => {
const client = legacyShellStatusFixture(suppliedClient)
const reads = modules.load<typeof import('../../../session/github-pr-rpc')>(
'mobile/src/session/github-pr-rpc.ts'
)
@@ -1,6 +1,7 @@
import type { MountAdapter } from '../recording-scenario'
import type { operationModuleLoader } from '../operation-module-loader'
import type { PrSidebarLoadDeps } from '../../../session/mobile-pr-sidebar-state'
import { legacyShellStatusFixture } from '../legacy-shell-status-fixture'
const WORKTREE = 'repo-9::/w'
const BRANCH = 'feature'
@@ -10,7 +11,8 @@ export function prSidebarMountAdapters(
modules: ReturnType<typeof operationModuleLoader>
): Record<string, MountAdapter> {
return {
'session.pr-sidebar': ({ client }) => {
'session.pr-sidebar': ({ client: suppliedClient }) => {
const client = legacyShellStatusFixture(suppliedClient)
const reads = modules.load<typeof import('../../../session/github-pr-rpc')>(
'mobile/src/session/github-pr-rpc.ts'
)
@@ -5,6 +5,7 @@ import type { RecordedValue } from './recording-values'
// Producers whose input or recorded product contract this PR changed (the re-recorded set). Each earns its own
// direct page outcomes; none is judged by a legacy label or counted in the unchanged tally.
export const CHANGED_PRODUCERS: readonly string[] = [
'matrix-tasks.item-review-github-pr-check-discovery',
'matrix-github.pr-read-github.listassignableusers-1',
'matrix-github.pr-read-github.prcheckdetails-1',
'matrix-github.pr-read-github.prchecks-1',
@@ -44,6 +44,10 @@ const AFTER_WORK_ITEM = ['work-item', 'checks', 'check-details', 'assignable']
const AFTER_HOSTED_REVIEW = ['hosted-review', 'pr-for-branch', ...AFTER_WORK_ITEM]
export const READ_AND_PROJECT_REFUSALS: Readonly<Record<string, PageRefusal>> = {
'matrix-tasks.item-review-github-pr-check-discovery': {
request: 'status.get#1',
checkpoints: { 'tk-item-review-github.result-absent:checks-settled': [] }
},
'matrix-github.pr-read-github.listassignableusers-1': surfaceRead(
'github.listAssignableUsers#1',
'assignable',
@@ -0,0 +1,6 @@
import type { RpcClient } from '../../transport/rpc-client'
// Ordinary parity reads use the same cached-null legacy input as the page's actual init.
export function legacyShellStatusFixture(client: RpcClient): RpcClient {
return client.getShellHostStatus ? client : { ...client, getShellHostStatus: () => null }
}
@@ -2,6 +2,7 @@ import type * as BridgePortPairModule from '../../mobile-web-shell/bridge/bridge
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { ScriptedRpcTransport } from './scripted-rpc-transport'
import { legacyShellStatusFixture } from './legacy-shell-status-fixture'
import { recordingStep } from './recording-step'
import { materializeScenario } from './scenario-execution-mode'
import {
@@ -531,3 +532,21 @@ it('declared bridge-only probes preserve exact outcomes and never hide later dif
})
}
}, 120_000)
it('uses the actual page init cached-null status as the ordinary legacy recording input', async () => {
const transport = new ScriptedRpcTransport()
const native = legacyShellStatusFixture(transport.client)
const pair = createBridgePortPair({ rpc: transport.client })
try {
await pair.flush()
expect(native.getShellHostStatus?.()).toBeNull()
const init = pair.toPage.map((json) => JSON.parse(json)).find((frame) => frame.type === 'init')
expect(init).toMatchObject({ type: 'init' })
expect(init).not.toHaveProperty('hostStatus')
expect(pair.client.getShellHostStatus()).toBeNull()
expect(legacyShellStatusFixture(pair.client)).toBe(pair.client)
} finally {
await disposeBridgePortPair(pair)
await transport.dispose()
}
})
@@ -0,0 +1,184 @@
import { expect, it, vi } from 'vitest'
import type { RpcClient } from './rpc-client'
import { recordRuntimeStatusProbe } from './runtime-status-probe'
import { captureMobilePRCheckContract } from './pr-check-observation-contract'
import {
GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as complete,
GITHUB_EXACT_WORK_ITEM_TARGET_RUNTIME_CAPABILITY as exact
} from '../../../src/shared/source-control/runtime/source-control-runtime-capabilities'
function host(capabilities: string[], observed = true) {
let generation = 1
let state: ReturnType<RpcClient['getState']> = 'connected'
const listeners = new Set<Parameters<RpcClient['onStateChange']>[0]>()
const sendRequest = vi.fn(async () => ({
id: 'status',
ok: true as const,
result: { runtimeId: 'runtime-1', hostDescriptor: { installationId: 'host-1' }, capabilities }
}))
const client = {
sendRequest,
getGeneration: () => generation,
getState: () => state,
getLastConnectedAt: () => generation,
onStateChange: (listener: Parameters<RpcClient['onStateChange']>[0]) => {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
},
carriesRpcEnvelope: () => true
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture implements every client operation used by the contract.
const rpcClient = client as unknown as RpcClient
if (observed) {
recordRuntimeStatusProbe(rpcClient, {
runtimeId: 'runtime-1',
hostDescriptor: { installationId: 'host-1' },
capabilities
})
}
return {
client: rpcClient,
sendRequest,
replace: () => {
state = 'disconnected'
listeners.forEach((listener) => listener(state))
generation++
state = 'connected'
}
}
}
it('reuses the existing execution-host status across N and 10N reads and fences replacement', async () => {
const source = host([complete, exact])
for (const n of [10, 100]) {
const contracts = await Promise.all(
Array.from({ length: n }, () =>
captureMobilePRCheckContract(source.client, { repo: 'id:repo' })
)
)
expect(contracts.every((contract) => contract.complete && contract.current())).toBe(true)
expect(source.sendRequest).not.toHaveBeenCalled()
contracts.forEach((contract) => contract.retire())
}
const pending = await captureMobilePRCheckContract(source.client, { repo: 'id:repo' })
source.replace()
expect(pending.current()).toBe(false)
pending.retire()
})
it('uses the executing host verdict when a new relay points at an old host', async () => {
const relay = host([complete, exact])
const execution = host([])
const routed = {
...relay.client,
requestRoute: () => ({ client: execution.client, intermediary: relay.client })
}
const contract = await captureMobilePRCheckContract(routed, { repo: 'id:server-repo' })
expect(contract.complete).toBe(false)
expect(contract.options).not.toHaveProperty('envelope')
contract.retire()
})
it('does not certify a capable execution host through an unproven old intermediary', async () => {
const relay = host([])
const execution = host([complete, exact])
const routed = {
...relay.client,
requestRoute: () => ({ client: execution.client, intermediary: relay.client })
}
const contract = await captureMobilePRCheckContract(routed, { repo: 'id:server-repo' })
expect(contract.complete).toBe(false)
contract.retire()
})
it('refresh relearns a relayed runtime restart without a phone connection edge', async () => {
const execution = host([complete, exact])
const old = await captureMobilePRCheckContract(execution.client, { repo: 'id:repo' })
execution.sendRequest.mockResolvedValueOnce({
id: 'status',
ok: true,
result: {
runtimeId: 'runtime-2',
hostDescriptor: { installationId: 'host-1' },
capabilities: [complete, exact]
}
})
const fresh = await captureMobilePRCheckContract(execution.client, { repo: 'id:repo' }, true)
expect(old.current()).toBe(false)
expect(fresh.complete && fresh.current()).toBe(true)
expect(
'envelope' in fresh.options && fresh.options.envelope?.expectedRuntimeSource?.runtimeId
).toBe('runtime-2')
old.retire()
fresh.retire()
})
it('retires a repository route switch while both clients stay connected', async () => {
const first = host([complete, exact])
const second = host([complete, exact])
let selected = first.client
const router = { ...first.client, requestRoute: () => ({ client: selected }) }
const pending = await captureMobilePRCheckContract(router, { repo: 'id:repo' })
selected = second.client
expect(pending.current()).toBe(false)
expect(() => 'beforeSend' in pending.options && pending.options.beforeSend()).toThrow()
pending.retire()
})
it('uses later same-epoch probe evidence after transient discovery failure without delaying legacy reads', async () => {
const source = host([])
recordRuntimeStatusProbe(source.client, null)
const deferred = Promise.withResolvers<never>()
source.sendRequest.mockReturnValueOnce(deferred.promise)
const legacy = await captureMobilePRCheckContract(source.client, { repo: 'id:repo' }, true)
expect(legacy.complete).toBe(false)
deferred.reject(new Error('temporary'))
await Promise.resolve()
recordRuntimeStatusProbe(source.client, {
runtimeId: 'runtime-1',
hostDescriptor: { installationId: 'host-1' },
capabilities: [complete, exact]
})
const adopted = await captureMobilePRCheckContract(source.client, { repo: 'id:repo' })
expect(adopted.complete && adopted.current()).toBe(true)
expect(legacy.current()).toBe(false)
legacy.retire()
adopted.retire()
})
it.each(['ordinary', 'refresh'])(
'coalesces concurrent %s discovery without delaying legacy reads',
async (mode) => {
const source = host([complete, exact], mode === 'refresh')
if (mode === 'refresh') {
recordRuntimeStatusProbe(source.client, null)
}
const status = Promise.withResolvers<Awaited<ReturnType<typeof source.sendRequest>>>()
source.sendRequest.mockReturnValueOnce(status.promise)
const reads = await Promise.all(
Array.from({ length: 100 }, () =>
captureMobilePRCheckContract(source.client, { repo: 'id:repo' }, mode === 'refresh')
)
)
expect(reads.every((read) => !read.complete)).toBe(true)
expect(source.sendRequest).toHaveBeenCalledTimes(1)
status.resolve({
id: 's',
ok: true,
result: {
runtimeId: 'runtime-1',
hostDescriptor: { installationId: 'host-1' },
capabilities: [complete, exact]
}
})
await status.promise
await vi.waitFor(async () => {
const next = await captureMobilePRCheckContract(source.client, { repo: 'id:repo' })
expect(next.complete).toBe(true)
next.retire()
})
reads.forEach((read) => read.retire())
}
)
@@ -0,0 +1,184 @@
import { z } from 'zod'
import type { RpcClient, SendRequestOptions } from './rpc-client'
import type { RpcOperationSender } from './rpc-operation-sender'
import { captureGitHubInvocation, supportsExactGitHubOperations } from './github-target-invocation'
import {
readRuntimeStatusProbe,
recordRuntimeStatusProbe,
runtimeStatusProbeEpoch
} from './runtime-status-probe'
import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY } from '../../../src/shared/source-control/runtime/source-control-runtime-capabilities'
type Status = Awaited<ReturnType<ReturnType<typeof captureGitHubInvocation>['status']>>
type Discovery = {
epoch: string
pending?: Promise<Status | null | undefined>
attempted: boolean
status?: Status
}
const discoveries = new WeakMap<RpcClient, Discovery>()
const statusSchema = z.object({
runtimeId: z.string().optional(),
hostDescriptor: z.object({ installationId: z.string() }).optional(),
capabilities: z.array(z.string()).optional()
})
const sourceSchema = z.object({
runtimeId: z.string(),
hostDescriptor: z.object({ installationId: z.string() })
})
const supporting = (status: Status | null | undefined) =>
status?.capabilities?.includes(GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY) === true
function knownStatus(client: RpcClient): Status | null | undefined {
const prior = discoveries.get(client)
const proof = prior?.epoch === runtimeStatusProbeEpoch(client) ? prior.status : undefined
const cached = readRuntimeStatusProbe(client)
const value = cached === undefined ? client.getShellHostStatus?.() : cached
const parsed = statusSchema.safeParse(value)
if (!parsed.success) {
return proof ?? (value === undefined ? undefined : null)
}
const status = parsed.data
if (supporting(proof) && (!status.runtimeId || status.runtimeId === proof?.runtimeId)) {
return {
...proof,
...status,
runtimeId: proof?.runtimeId,
hostDescriptor: proof?.hostDescriptor,
capabilities: proof?.capabilities
}
}
if (supporting(status) && prior) {
prior.status = status
}
return status
}
function statusFor(client: RpcClient, refresh: boolean): Promise<Status | null | undefined> {
const known = knownStatus(client)
const epoch = runtimeStatusProbeEpoch(client)
let entry = discoveries.get(client)
if (!entry || entry.epoch !== epoch) {
entry = { epoch, attempted: known !== undefined, status: known ?? undefined }
discoveries.set(client, entry)
}
if (!entry.pending && (!entry.attempted || refresh)) {
entry.attempted = true
const invocation = captureGitHubInvocation(client)
const captured = entry
captured.pending = invocation
.status({ timeoutMs: 15_000, failWhenDisconnected: true })
.then(
(status) => {
const accepted =
supporting(known) && status.runtimeId === known?.runtimeId
? { ...status, capabilities: known?.capabilities }
: status
if (supporting(accepted)) {
captured.status = accepted
}
recordRuntimeStatusProbe(client, accepted)
return accepted
},
() => known
)
.finally(() => {
captured.pending = undefined
invocation.retire()
})
}
// Unknown/legacy reads proceed immediately. A user refresh of a known capable producer relearns a relayed restart.
return refresh && supporting(known) && entry.pending ? entry.pending : Promise.resolve(known)
}
export async function captureMobilePRCheckContract(
sender: RpcOperationSender,
params: { repo: string },
refresh = false
) {
if (!isRpcClient(sender)) {
return {
scope: undefined,
complete: false,
current: () => true,
options: {} satisfies SendRequestOptions,
routeCurrent: () => true,
retire: () => {}
}
}
const origin = sender
const route = () => origin.requestRoute?.(params) ?? { client: origin }
const { client, intermediary } = route()
const sameRoute = () => {
const next = route()
return next.client === client && next.intermediary === intermediary
}
const invocation = captureGitHubInvocation(client, sameRoute)
const relayInvocation = intermediary && captureGitHubInvocation(intermediary, sameRoute)
const [status, relay] = await Promise.all([
statusFor(client, refresh),
intermediary ? statusFor(intermediary, refresh) : null
])
const source = sourceSchema.safeParse(status)
const complete =
status?.capabilities?.includes(GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY) === true &&
source.success &&
(!intermediary ||
(relay &&
supportsExactGitHubOperations(relay) &&
(client.carriesRpcEnvelope?.() ?? true))) === true
const started = runtimeStatusProbeEpoch(client)
const routeCurrent = () => {
try {
invocation.assertCurrent()
relayInvocation?.assertCurrent()
const now = knownStatus(client)
return (
runtimeStatusProbeEpoch(client) === started &&
(!source.success || now?.runtimeId === source.data.runtimeId)
)
} catch {
return false
}
}
const current = () =>
routeCurrent() &&
(knownStatus(client)?.capabilities?.includes(
GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY
) ===
true) ===
(status?.capabilities?.includes(GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY) === true)
const beforeSend = () => {
invocation.assertCurrent()
relayInvocation?.assertCurrent()
}
return {
scope: source.success
? `${source.data.hostDescriptor.installationId}:${source.data.runtimeId}:${started}`
: undefined,
complete,
current,
routeCurrent,
client,
retire: () => {
invocation.retire()
relayInvocation?.retire()
},
options: {
beforeSend,
...(complete && source.success
? {
envelope: {
expectedRuntimeSource: {
sourceId: source.data.hostDescriptor.installationId,
runtimeId: source.data.runtimeId
}
}
}
: {})
} satisfies SendRequestOptions
}
}
function isRpcClient(sender: RpcOperationSender): sender is RpcClient {
return 'getState' in sender
}
+23 -1
View File
@@ -1,8 +1,26 @@
import type { RpcClient } from './rpc-client'
import type { UnvalidatedRpcRequestPort } from './unvalidated-rpc-request-port'
import { hostStatusProbe, readProbedHostStatus } from './host-status-probe-operations'
import type { HostStatusReply } from './host-status-reply-schema'
import { isLogicalClientCutoverError } from './stable-logical-rpc-client'
type ProbeClient = UnvalidatedRpcRequestPort &
Partial<Pick<RpcClient, 'getGeneration' | 'getLastConnectedAt'>>
const observed = new WeakMap<object, { epoch: string; status: HostStatusReply | null }>()
export const runtimeStatusProbeEpoch = (client: ProbeClient) =>
`${client.getGeneration?.()}:${client.getLastConnectedAt?.()}`
export function readRuntimeStatusProbe(client: ProbeClient): HostStatusReply | null | undefined {
const entry = observed.get(client)
return entry?.epoch === runtimeStatusProbeEpoch(client) ? entry.status : undefined
}
export function recordRuntimeStatusProbe(
client: ProbeClient,
status: HostStatusReply | null
): void {
observed.set(client, { epoch: runtimeStatusProbeEpoch(client), status })
}
// Cutover/timeouts may reject status.get without a connection-state edge; retry until one answer lands.
const CUTOVER_RETRY_DELAY_MS = 250
const FAILURE_RETRY_BASE_DELAY_MS = 1_000
@@ -11,10 +29,11 @@ const FAILURE_RETRY_MAX_DELAY_MS = 15_000
// Deliver unreadable answers as null without retry; refusals/rejections retry and notify onAttemptFailed after scheduling.
// Accept the raw sender type because capability callers need not hold a full client.
export function startRuntimeStatusProbe(
client: UnvalidatedRpcRequestPort,
client: ProbeClient,
onStatus: (status: HostStatusReply | null) => void,
onAttemptFailed?: () => void
): () => void {
const epoch = runtimeStatusProbeEpoch(client)
let cancelled = false
let retryTimer: ReturnType<typeof setTimeout> | null = null
let failureRetries = 0
@@ -30,6 +49,9 @@ export function startRuntimeStatusProbe(
scheduleRetry(false)
return
}
if (epoch === runtimeStatusProbeEpoch(client)) {
observed.set(client, { epoch, status: landed.status })
}
onStatus(landed.status)
},
(error: unknown) => {
@@ -47,10 +47,12 @@ import { resolveReviewThread } from './client/update/resolve-review-thread'
import { setPRAutoMerge } from './client/merge/pr-auto-merge'
import { updatePRTitle } from './client/update/pr-details'
import { _getMergeQueueCacheSizeForTests } from './client/detect/repository-merge-metadata-cache'
import { resetGraphQLRateLimitGuardMocks } from './client-test-harness'
import {
installIndependentCheckFixtures,
resetGraphQLRateLimitGuardMocks
} from './client-test-harness'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
resolvePRRepositoryCandidatesMock,
gitExecFileAsyncMock,
@@ -59,15 +61,20 @@ const {
acquireMock
} = clientMocks
const metadataRequests = vi.fn()
const unexpectedChecks: string[][] = []
afterEach(() => expect(unexpectedChecks).toEqual([]))
describe('GitHub GraphQL rate-limit guard', () => {
beforeEach(() => {
resetGraphQLRateLimitGuardMocks(clientMocks)
installIndependentCheckFixtures(clientMocks, metadataRequests, unexpectedChecks)
})
afterEach(() => vi.restoreAllMocks())
it('uses explicit PR repo for merge and title mutations', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 7,
@@ -111,7 +118,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
).resolves.toBe(true)
expect(getOwnerRepoMock).not.toHaveBeenCalled()
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
1,
['api', 'repos/stablyai/orca/pulls/7', '--include'],
{
@@ -119,7 +126,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
host: 'github.com'
}
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
[
'pr',
@@ -128,11 +135,11 @@ describe('GitHub GraphQL rate-limit guard', () => {
'--repo',
'stablyai/orca',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root', host: 'github.com' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
['pr', 'merge', '7', '--squash', '--repo', 'stablyai/orca'],
expect.objectContaining({
@@ -141,7 +148,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
host: 'github.com'
})
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
4,
[
'api',
@@ -156,7 +163,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
it('sets and disables PR auto-merge with explicit PR repos and SSH context', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid' })
@@ -178,19 +185,19 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
1,
['api', 'repos/stablyai/orca/pulls/7', '--include'],
{
host: 'github.com'
}
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
['pr', 'view', '7', '--json', 'id,headRefOid,baseRefName', '--repo', 'stablyai/orca'],
{ host: 'github.com' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
expect.arrayContaining([
'api',
@@ -207,7 +214,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
host: 'github.com'
})
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
4,
['pr', 'merge', '7', '--disable-auto', '--repo', 'stablyai/orca'],
expect.objectContaining({
@@ -215,11 +222,11 @@ describe('GitHub GraphQL rate-limit guard', () => {
host: 'github.com'
})
)
expect(ghExecFileAsyncMock.mock.calls[0]?.[1]).not.toHaveProperty('cwd')
expect(metadataRequests.mock.calls[0]?.[1]).not.toHaveProperty('cwd')
})
it('enables auto-merge without invoking the direct merge command', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid' })
@@ -235,12 +242,12 @@ describe('GitHub GraphQL rate-limit guard', () => {
).resolves.toEqual({ ok: true })
expect(
ghExecFileAsyncMock.mock.calls.some((call) =>
metadataRequests.mock.calls.some((call) =>
(call[0] as string[]).some((arg) => arg.includes('enablePullRequestAutoMerge'))
)
).toBe(true)
expect(
ghExecFileAsyncMock.mock.calls.some(
metadataRequests.mock.calls.some(
(call) =>
call[0][0] === 'pr' && call[0][1] === 'merge' && (call[0] as string[]).includes('--auto')
)
@@ -248,7 +255,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
it('rejects auto-merge for GitHub-registered stacks', async () => {
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 202,
title: 'Stack API',
@@ -274,11 +281,11 @@ describe('GitHub GraphQL rate-limit guard', () => {
ok: false,
error: 'GitHub does not support auto-merge for stacked pull requests.'
})
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
expect(metadataRequests).toHaveBeenCalledTimes(1)
})
it('translates the GitHub clean-status rejection into an actionable message', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid' })
@@ -300,7 +307,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
it('preserves a provider rejection when eligibility changes before enabling auto-merge', async () => {
const providerError =
'gh: Pull request Pull request is in unstable status (enablePullRequestAutoMerge)'
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid' })
@@ -318,12 +325,12 @@ describe('GitHub GraphQL rate-limit guard', () => {
error: `Failed to enable auto-merge for pull request: ${providerError}`
})
expect(
ghExecFileAsyncMock.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
metadataRequests.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
).toBe(false)
})
it('names a failed disable-auto-merge operation', async () => {
ghExecFileAsyncMock.mockRejectedValueOnce(new Error('gh: pull request is already closed'))
metadataRequests.mockRejectedValueOnce(new Error('gh: pull request is already closed'))
await expect(
setPRAutoMerge('/repo-root', 7, false, 'squash', undefined, {
owner: 'stablyai',
@@ -340,7 +347,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
it('preserves REST merge eligibility and an existing auto-merge request in PR details', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock
metadataRequests
.mockRejectedValueOnce(new Error('GraphQL unavailable'))
.mockResolvedValueOnce({
stdout: JSON.stringify({
@@ -377,7 +384,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
it('uses the queue-aware gh merge path when the base branch has a merge queue', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid', baseRefName: 'main' })
@@ -395,12 +402,12 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
expect.arrayContaining(['api', 'graphql', '-f', 'branch=main']),
{ cwd: '/repo-root', host: 'github.com' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
4,
['pr', 'merge', '7', '--auto', '--squash', '--repo', 'stablyai/orca'],
expect.objectContaining({
@@ -410,14 +417,14 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
)
expect(
ghExecFileAsyncMock.mock.calls.some((call) =>
metadataRequests.mock.calls.some((call) =>
(call[0] as string[]).some((arg) => arg.includes('enablePullRequestAutoMerge'))
)
).toBe(false)
})
it('enqueues instead of gh pr merge --auto when a required merge queue disallows auto-merge', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid', baseRefName: 'main' })
@@ -437,8 +444,8 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(4)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenCalledTimes(4)
expect(metadataRequests).toHaveBeenNthCalledWith(
4,
[
'api',
@@ -459,7 +466,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
it('blocks direct merge when GitHub reports required approval', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({
@@ -491,8 +498,8 @@ describe('GitHub GraphQL rate-limit guard', () => {
error: 'This pull request requires review approval before it can be merged.'
})
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(3)
expect(ghExecFileAsyncMock.mock.calls[2]?.[0]).toContain('graphql')
expect(metadataRequests).toHaveBeenCalledTimes(3)
expect(metadataRequests.mock.calls[2]?.[0]).toContain('graphql')
})
it('detects merge queues once per base branch and blocks direct merges', async () => {
@@ -512,7 +519,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
baseRefOid: 'base-oid',
headRefOid: 'head-oid'
}
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({ stdout: JSON.stringify(prView) })
.mockResolvedValueOnce({
@@ -536,13 +543,13 @@ describe('GitHub GraphQL rate-limit guard', () => {
mergePR('/repo-root', 7, 'squash', undefined, { owner: 'stablyai', repo: 'orca' })
).resolves.toMatchObject({ ok: false })
expect(
ghExecFileAsyncMock.mock.calls.filter((call) => call[0].includes('graphql'))
).toHaveLength(1)
expect(ghExecFileAsyncMock.mock.calls[2]?.[0]).toEqual(
expect(metadataRequests.mock.calls.filter((call) => call[0].includes('graphql'))).toHaveLength(
1
)
expect(metadataRequests.mock.calls[2]?.[0]).toEqual(
expect.arrayContaining(['-f', 'owner=stablyai', '-f', 'repo=orca', '-f', 'branch=true'])
)
expect(ghExecFileAsyncMock.mock.calls[2]?.[0]).not.toContain('-F')
expect(metadataRequests.mock.calls[2]?.[0]).not.toContain('-F')
})
it('caches unknown merge queue probes after GraphQL failures', async () => {
@@ -563,7 +570,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
baseRefOid: 'base-oid',
headRefOid: 'head-oid'
}
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify(prView) })
.mockRejectedValueOnce(new Error('network is down'))
.mockResolvedValueOnce({ stdout: '{}' })
@@ -577,9 +584,9 @@ describe('GitHub GraphQL rate-limit guard', () => {
mergeQueueRequired: null
})
expect(
ghExecFileAsyncMock.mock.calls.filter((call) => call[0].includes('graphql'))
).toHaveLength(1)
expect(metadataRequests.mock.calls.filter((call) => call[0].includes('graphql'))).toHaveLength(
1
)
})
it('reuses repository metadata between PR details and sidebar refreshes', async () => {
@@ -599,7 +606,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
baseRefName: 'main',
headRefOid: 'head-oid'
}
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
metadataRequests.mockImplementation(async (args: string[]) => {
if (args.includes('graphql')) {
return {
stdout: JSON.stringify({
@@ -618,15 +625,13 @@ describe('GitHub GraphQL rate-limit guard', () => {
autoMergeAllowed: true
})
expect(
ghExecFileAsyncMock.mock.calls.filter(([args]) => args.includes('graphql'))
).toHaveLength(1)
expect(metadataRequests.mock.calls.filter(([args]) => args.includes('graphql'))).toHaveLength(1)
})
it('bounds merge metadata cache entries across many base branches', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' })
let prViewCount = 0
ghExecFileAsyncMock.mockImplementation(async (args) => {
metadataRequests.mockImplementation(async (args) => {
if (args.includes('graphql')) {
return { stdout: JSON.stringify({ data: { repository: { mergeQueue: null } } }) }
}
@@ -678,7 +683,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
baseRefName: 'main',
headRefOid: 'head-oid'
}
ghExecFileAsyncMock.mockImplementation(async (args, options) => {
metadataRequests.mockImplementation(async (args, options) => {
if (args.includes('graphql')) {
const enterprise = options?.host === 'github.acme-corp.com'
return {
@@ -710,7 +715,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
expect(githubDotCom?.autoMergeAllowed).toBe(true)
expect(enterprise?.autoMergeAllowed).toBe(false)
const graphqlCalls = ghExecFileAsyncMock.mock.calls.filter(([args]) => args.includes('graphql'))
const graphqlCalls = metadataRequests.mock.calls.filter(([args]) => args.includes('graphql'))
expect(graphqlCalls).toHaveLength(2)
expect(graphqlCalls[1]?.[1]).toEqual(expect.objectContaining({ host: 'github.acme-corp.com' }))
})
@@ -731,11 +736,11 @@ describe('GitHub GraphQL rate-limit guard', () => {
).resolves.toBeNull()
expect(acquireMock).not.toHaveBeenCalled()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
expect(metadataRequests).not.toHaveBeenCalled()
})
it('returns conflicting file details instead of running gh merge when PR is dirty', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({
@@ -775,11 +780,11 @@ describe('GitHub GraphQL rate-limit guard', () => {
'- src/conflict.ts'
})
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
})
it('does not run merge conflict preflight for SSH-backed repos', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) })
.mockResolvedValueOnce({
stdout: JSON.stringify({
@@ -806,15 +811,15 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(3)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenCalledTimes(3)
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
['pr', 'merge', '7', '--squash', '--repo', 'stablyai/orca'],
expect.objectContaining({
env: expect.objectContaining({ GH_PROMPT_DISABLED: '1' })
})
)
expect(ghExecFileAsyncMock.mock.calls[0]?.[1]).not.toHaveProperty('cwd')
expect(metadataRequests.mock.calls[0]?.[1]).not.toHaveProperty('cwd')
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
})
@@ -828,7 +833,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
await expect(resolveReviewThread('/repo-root', 'thread-1', true)).resolves.toBe(false)
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
expect(metadataRequests).not.toHaveBeenCalled()
expect(noteRateLimitSpendMock).not.toHaveBeenCalled()
})
})
@@ -1,6 +1,6 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/gh-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
@@ -39,24 +39,28 @@ vi.mock('./github-api-repository', async (importOriginal) =>
import { getPRForBranchOutcome } from './client/lookup/pr-for-branch-outcome'
import { getPRForBranch } from './client/lookup/get-pr-for-branch'
import { resetPRForBranchMocks } from './client-test-harness'
import { installIndependentCheckFixtures, resetPRForBranchMocks } from './client-test-harness'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
resolvePRRepositoryCandidatesMock,
gitExecFileAsyncMock,
rateLimitGuardMock
} = clientMocks
const metadataRequests = vi.fn()
const unexpectedChecks: string[][] = []
afterEach(() => expect(unexpectedChecks).toEqual([]))
describe('getPRForBranch', () => {
beforeEach(() => {
resetPRForBranchMocks(clientMocks)
installIndependentCheckFixtures(clientMocks, metadataRequests, unexpectedChecks)
})
it('ignores merged PRs discovered only by branch lookup when the branch moved on', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
@@ -101,7 +105,7 @@ describe('getPRForBranch', () => {
it('shows a merged branch PR when it still matches the current HEAD', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
@@ -154,7 +158,7 @@ describe('getPRForBranch', () => {
const mockMergedBranchPRLookupBehindHead = (prNumber = 6011): void => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
@@ -195,13 +199,13 @@ describe('getPRForBranch', () => {
it('shows a merged branch PR when the worktree head is one of its own commits', async () => {
mockMergedBranchPRLookupBehindHead()
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([{ number: 6011 }, { number: 42 }])
})
const pr = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
['api', 'repos/acme/widgets/commits/bbbb2222bbbb2222/pulls?per_page=100&page=1'],
expect.anything()
@@ -216,25 +220,25 @@ describe('getPRForBranch', () => {
it('keeps hiding a merged branch PR when the head belongs to a different PR (reused branch)', async () => {
mockMergedBranchPRLookupBehindHead()
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([{ number: 42 }])
})
const pr = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
expect(pr).toBeNull()
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(3)
expect(metadataRequests).toHaveBeenCalledTimes(3)
mockMergedBranchPRLookupBehindHead()
const second = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
expect(second).toBeNull()
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(5)
expect(metadataRequests).toHaveBeenCalledTimes(5)
})
it('keeps hiding a merged branch PR when the commit membership probe fails', async () => {
mockMergedBranchPRLookupBehindHead()
ghExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 422: No commit found'))
metadataRequests.mockRejectedValueOnce(new Error('HTTP 422: No commit found'))
const pr = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
@@ -252,28 +256,28 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
expect(pr).toBeNull()
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
})
it('reuses the cached membership answer instead of re-querying per poll', async () => {
mockMergedBranchPRLookupBehindHead()
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([{ number: 6011 }])
})
const first = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
expect(first).toMatchObject({ number: 6011, confirmedContainedHeadOid: 'bbbb2222bbbb2222' })
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(3)
expect(metadataRequests).toHaveBeenCalledTimes(3)
mockMergedBranchPRLookupBehindHead()
const second = await getPRForBranch('/repo-root', 'fix-hibernation-wake')
expect(second).toMatchObject({ number: 6011, confirmedContainedHeadOid: 'bbbb2222bbbb2222' })
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(5)
expect(metadataRequests).toHaveBeenCalledTimes(5)
})
it('uses the caller-supplied worktree head for the membership probe without shelling out', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
@@ -342,7 +346,7 @@ describe('getPRForBranch', () => {
candidates: [{ owner: 'acme', repo: 'widgets' }],
headRepo: { owner: 'acme', repo: 'widgets' }
})
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({
number: prNumber,
title: 'Merged linked PR',
@@ -362,7 +366,7 @@ describe('getPRForBranch', () => {
it('stamps confirmedContainedHeadOid for a linked merged PR when HEAD is its commit', async () => {
mockMergedLinkedPRLookup()
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([{ number: 7447 }])
})
@@ -385,7 +389,7 @@ describe('getPRForBranch', () => {
it('stamps headDivergedFromMergedPRAtOid for a linked merged PR with a definite miss', async () => {
mockMergedLinkedPRLookup()
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([{ number: 42 }])
})
@@ -407,7 +411,7 @@ describe('getPRForBranch', () => {
mockMergedLinkedPRLookup()
// A short second page proves absence after a full first page.
const fullPage = Array.from({ length: 100 }, (_, index) => ({ number: 1000 + index }))
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify(fullPage) })
.mockResolvedValueOnce({ stdout: JSON.stringify([{ number: 2000 }]) })
@@ -426,7 +430,7 @@ describe('getPRForBranch', () => {
// Full pages through the cap cannot prove absence, so preserve the durable link.
const fullPage = Array.from({ length: 100 }, (_, index) => ({ number: 1000 + index }))
for (let page = 0; page < 5; page += 1) {
ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: JSON.stringify(fullPage) })
metadataRequests.mockResolvedValueOnce({ stdout: JSON.stringify(fullPage) })
}
const outcome = await getPRForBranchOutcome('/repo-root', 'new-work', 7447, null, null, {
@@ -442,7 +446,7 @@ describe('getPRForBranch', () => {
it('stamps linked merged divergence via the PR url when no repo candidates resolve', async () => {
// Without resolved candidates, derive the membership-probe repository from the PR URL.
resolvePRRepositoryCandidatesMock.mockResolvedValueOnce({ candidates: [], headRepo: null })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 7447,
@@ -469,7 +473,7 @@ describe('getPRForBranch', () => {
kind: 'found',
pr: { number: 7447, state: 'merged', headDivergedFromMergedPRAtOid: 'bbbb2222bbbb2222' }
})
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
expect(metadataRequests).toHaveBeenCalledWith(
['api', 'repos/acme/widgets/commits/bbbb2222bbbb2222/pulls?per_page=100&page=1'],
expect.anything()
)
@@ -491,12 +495,12 @@ describe('getPRForBranch', () => {
expect(outcome.kind === 'found' ? outcome.pr.headDivergedFromMergedPRAtOid : undefined).toBe(
undefined
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
})
it('leaves linked merged divergence unset when the membership probe throws', async () => {
mockMergedLinkedPRLookup()
ghExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 422: No commit found'))
metadataRequests.mockRejectedValueOnce(new Error('HTTP 422: No commit found'))
const outcome = await getPRForBranchOutcome('/repo-root', 'new-work', 7447, null, null, {
currentHeadOid: 'bbbb2222bbbb2222'
@@ -510,7 +514,7 @@ describe('getPRForBranch', () => {
it('leaves linked merged divergence unset when the membership probe returns a non-array payload', async () => {
mockMergedLinkedPRLookup()
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({ message: 'Server Error' })
})
@@ -533,6 +537,6 @@ describe('getPRForBranch', () => {
expect(outcome.kind === 'found' ? outcome.pr.headDivergedFromMergedPRAtOid : undefined).toBe(
undefined
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
})
})
@@ -1,3 +1,4 @@
import { completePRCheckGraphQLResponse } from '../../shared/github/complete-pr-check-test-fixture'
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/gh-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -78,6 +79,16 @@ describe('getPRForBranch', () => {
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'CANCELLED' }]
})
})
.mockResolvedValueOnce(
completePRCheckGraphQLResponse(7, 'head-oid', [
{
__typename: 'CheckRun',
status: 'COMPLETED',
conclusion: 'CANCELLED',
checkSuite: { databaseId: 1 }
}
])
)
const pr = await getPRForBranch('/repo-root', 'feature/test')
expect(ghExecFileAsyncMock.mock.calls.slice(0, 2).map(([args]) => args.slice(0, 2))).toEqual([
['api', 'repos/acme/widgets/pulls?head=acme%3Afeature%2Ftest&state=all&per_page=10'],
@@ -130,7 +141,12 @@ describe('getPRForBranch', () => {
['pr', 'list'],
['pr', 'view']
])
expect(pr).toMatchObject({ number: 7, checksStatus: 'failure' })
expect(pr).toMatchObject({
number: 7,
title: 'Partial check page',
headSha: 'head-oid',
checksStatus: 'neutral'
})
expect(pr?.checksSummary).toBeUndefined()
})
@@ -549,7 +565,7 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
+77 -6
View File
@@ -84,7 +84,7 @@ vi.mock('./rate-limit', () => ({
spendsSharedGitHubComQuota: spendsSharedGitHubComQuotaMock
}))
import { getPRChecks } from './client/check/get-pr-checks'
import { getPRChecks, getPRChecksViaRestFallback } from './client/check/get-pr-checks'
import { rerunPRChecks } from './client/check/rerun-pr-checks'
import { _resetOwnerRepoCache } from './gh-utils'
@@ -97,11 +97,13 @@ beforeEach(() => {
function graphQLChecksResponse({
contexts = [],
checkSuites = [],
headRefOid = 'head-oid'
headRefOid = 'head-oid',
commitOid = headRefOid
}: {
contexts?: unknown[]
checkSuites?: unknown[]
headRefOid?: string
commitOid?: string
} = {}): { stdout: string } {
return {
stdout: JSON.stringify({
@@ -113,6 +115,7 @@ function graphQLChecksResponse({
nodes: [
{
commit: {
oid: commitOid,
statusCheckRollup: { contexts: { nodes: contexts } },
checkSuites: { nodes: checkSuites }
}
@@ -451,6 +454,7 @@ describe('getPRChecks', () => {
.mockRejectedValueOnce(new Error('GraphQL rollup failed'))
.mockResolvedValueOnce({
stdout: JSON.stringify({
total_count: 1,
check_runs: [
{
id: 88,
@@ -465,6 +469,7 @@ describe('getPRChecks', () => {
})
.mockResolvedValueOnce({
stdout: JSON.stringify({
total_count: 1,
statuses: [
{
context: 'continuous-integration/jenkins/pr-merge',
@@ -474,19 +479,24 @@ describe('getPRChecks', () => {
]
})
})
.mockResolvedValueOnce({ stdout: JSON.stringify({ check_suites: [] }) })
.mockResolvedValueOnce({ stdout: JSON.stringify({ total_count: 0, check_suites: [] }) })
const checks = await getPRChecks('/repo-root', 42, 'head-oid')
expectGraphQLRollupCall()
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
2,
['api', '--cache', '60s', 'repos/acme/widgets/commits/head-oid/check-runs?per_page=100'],
[
'api',
'--cache',
'60s',
'repos/acme/widgets/commits/head-oid/check-runs?per_page=100&page=1'
],
{ cwd: '/repo-root', host: 'github.com' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
3,
['api', '--cache', '60s', 'repos/acme/widgets/commits/head-oid/status?per_page=100'],
['api', '--cache', '60s', 'repos/acme/widgets/commits/head-oid/status?per_page=100&page=1'],
{ cwd: '/repo-root', host: 'github.com' }
)
expect(checks).toEqual([
@@ -559,7 +569,7 @@ describe('getPRChecks', () => {
stdout: JSON.stringify([{ name: 'lint', state: 'PASS', link: 'https://example.com/lint' }])
})
const checks = await getPRChecks('/repo-root', 42, 'stale-head')
const checks = await getPRChecks('/repo-root', 42, 'a'.repeat(40))
expectGraphQLRollupCall()
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
@@ -762,3 +772,64 @@ describe('getPRChecks', () => {
await expect(getPRChecks('/repo-root', 42)).rejects.toThrow('rate limited')
})
})
it('carries the actual matching GraphQL commit identity rather than the requested head', async () => {
vi.clearAllMocks()
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce(
graphQLChecksResponse({
headRefOid: 'b'.repeat(40),
contexts: [{ __typename: 'StatusContext', context: 'Build', state: 'SUCCESS' }]
})
)
const checks = await getPRChecks('/repo-root', 42, 'a'.repeat(40))
expect(JSON.parse(JSON.stringify(checks))).toEqual([
{
name: 'Build',
status: 'completed',
conclusion: 'success',
url: null,
observedHeadSha: 'b'.repeat(40)
}
])
ghExecFileAsyncMock.mockResolvedValueOnce(
graphQLChecksResponse({
headRefOid: 'a'.repeat(40),
commitOid: 'b'.repeat(40),
contexts: [{ __typename: 'StatusContext', context: 'Build', state: 'SUCCESS' }]
})
)
expect((await getPRChecks('/repo-root', 42, 'a'.repeat(40)))[0].observedHeadSha).toBeUndefined()
})
it('carries immutable REST endpoint identity through useful incomplete enrichment', async () => {
vi.clearAllMocks()
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout: JSON.stringify({
total_count: 1,
check_runs: [
{
id: 88,
name: 'Build',
status: 'completed',
conclusion: 'failure',
html_url: '',
details_url: null
}
]
})
})
.mockRejectedValueOnce(new Error('statuses unavailable'))
.mockResolvedValueOnce({ stdout: JSON.stringify({ total_count: 0, check_suites: [] }) })
const checks = await getPRChecksViaRestFallback(
{ owner: 'acme', repo: 'widgets', host: 'github.com' },
'a'.repeat(40),
{ cwd: '/repo-root' },
true
)
expect(checks).toMatchObject([{ name: 'Build', observedHeadSha: 'a'.repeat(40) }])
expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain(
`repos/acme/widgets/commits/${'a'.repeat(40)}/check-runs?per_page=100&page=1`
)
})
@@ -1,6 +1,6 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/gh-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
@@ -41,23 +41,23 @@ vi.mock('./github-api-repository', async (importOriginal) =>
)
import { getPRForBranch } from './client/lookup/get-pr-for-branch'
import { resetPRForBranchMocks } from './client-test-harness'
import { installIndependentCheckFixtures, resetPRForBranchMocks } from './client-test-harness'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
resolvePRRepositoryCandidatesMock,
gitExecFileAsyncMock
} = clientMocks
const { getOwnerRepoMock, resolvePRRepositoryCandidatesMock, gitExecFileAsyncMock } = clientMocks
const metadataRequests = vi.fn()
const unexpectedChecks: string[][] = []
afterEach(() => expect(unexpectedChecks).toEqual([]))
describe('getPRForBranch', () => {
beforeEach(() => {
resetPRForBranchMocks(clientMocks)
installIndependentCheckFixtures(clientMocks, metadataRequests, unexpectedChecks)
})
it('derives a read-only conflict summary for conflicting PRs when the base ref exists locally', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -92,7 +92,7 @@ describe('getPRForBranch', () => {
it('routes local WSL branch status and conflict summary git probes through the selected distro', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -123,7 +123,7 @@ describe('getPRForBranch', () => {
expect(resolvePRRepositoryCandidatesMock).toHaveBeenCalledWith('/repo-root', null, {
wslDistro: 'Ubuntu'
})
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
expect(metadataRequests).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({
cwd: '/repo-root',
@@ -154,7 +154,7 @@ describe('getPRForBranch', () => {
it('treats GitHub DIRTY merge state as conflicting when mergeable is still unknown', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 42,
@@ -191,7 +191,7 @@ describe('getPRForBranch', () => {
it('omits conflict summaries for SSH-backed repos', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -216,7 +216,7 @@ describe('getPRForBranch', () => {
it('keeps conflicted file paths when git merge-tree exits 1 with stdout', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -248,7 +248,7 @@ describe('getPRForBranch', () => {
it('falls back to the legacy merge-tree invocation when Git lacks --merge-base', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -337,7 +337,7 @@ describe('getPRForBranch', () => {
// Change head OID to bypass the result cache while retaining the learned Git capability.
const pushedBranchLookup = { ...branchLookup, head: { ref: 'feature/test', sha: 'head-oid-2' } }
const pushedExactLookup = { ...exactLookup, headRefOid: 'head-oid-2' }
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({ stdout: JSON.stringify([branchLookup]) })
.mockResolvedValueOnce({ stdout: JSON.stringify(exactLookup) })
.mockResolvedValueOnce({
@@ -376,7 +376,7 @@ describe('getPRForBranch', () => {
it('does not retry legacy merge-tree for older Git failures unrelated to --merge-base', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -424,7 +424,7 @@ describe('getPRForBranch', () => {
it('marks the conflict summary as locally clean when GitHub reports dirty but merge-tree has no conflicted files', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
@@ -461,7 +461,7 @@ describe('getPRForBranch', () => {
it('falls back to GitHub baseRefOid when fetching or resolving the base ref fails', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
File diff suppressed because it is too large Load Diff
+88 -96
View File
@@ -1,6 +1,6 @@
import type * as GitExecFileModule from '../git/command-runner/git-exec-file'
import type * as GhExecFileModule from '../git/command-runner/gh-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GithubApiRepositoryModule from './github-api-repository'
import type * as GitHubEnterpriseRepositoryModule from './github-enterprise-repository'
@@ -38,18 +38,18 @@ vi.mock('./github-api-repository', async (importOriginal) =>
)
import { getPRForBranch } from './client/lookup/get-pr-for-branch'
import { resetPRForBranchMocks } from './client-test-harness'
import { installIndependentCheckFixtures, resetPRForBranchMocks } from './client-test-harness'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
resolvePRRepositoryCandidatesMock,
gitExecFileAsyncMock
} = clientMocks
const { getOwnerRepoMock, resolvePRRepositoryCandidatesMock, gitExecFileAsyncMock } = clientMocks
const metadataRequests = vi.fn()
const unexpectedChecks: string[][] = []
afterEach(() => expect(unexpectedChecks).toEqual([]))
describe('getPRForBranch', () => {
beforeEach(() => {
resetPRForBranchMocks(clientMocks)
installIndependentCheckFixtures(clientMocks, metadataRequests, unexpectedChecks)
})
it('looks up a linked PR number across PR repo candidates', async () => {
@@ -61,28 +61,26 @@ describe('getPRForBranch', () => {
headRepo: { owner: 'fork', repo: 'orca' }
})
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'linked-head-oid\n', stderr: '' })
ghExecFileAsyncMock
.mockRejectedValueOnce(new Error('HTTP 404: Not Found'))
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 99,
title: 'Linked fork PR',
state: 'OPEN',
url: 'https://github.com/fork/orca/pull/99',
statusCheckRollup: [],
updatedAt: '2026-03-28T00:00:00Z',
isDraft: false,
mergeable: 'MERGEABLE',
baseRefName: 'main',
headRefName: 'feature/test',
baseRefOid: 'base-oid',
headRefOid: 'linked-head-oid'
})
metadataRequests.mockRejectedValueOnce(new Error('HTTP 404: Not Found')).mockResolvedValueOnce({
stdout: JSON.stringify({
number: 99,
title: 'Linked fork PR',
state: 'OPEN',
url: 'https://github.com/fork/orca/pull/99',
statusCheckRollup: [],
updatedAt: '2026-03-28T00:00:00Z',
isDraft: false,
mergeable: 'MERGEABLE',
baseRefName: 'main',
headRefName: 'feature/test',
baseRefOid: 'base-oid',
headRefOid: 'linked-head-oid'
})
})
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
1,
[
'pr',
@@ -91,11 +89,11 @@ describe('getPRForBranch', () => {
'--repo',
'stablyai/orca',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
[
'pr',
@@ -104,7 +102,7 @@ describe('getPRForBranch', () => {
'--repo',
'fork/orca',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
@@ -114,7 +112,7 @@ describe('getPRForBranch', () => {
it('prefers exact linked PR lookup when the repo identity is known', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'linked-head-oid\n', stderr: '' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 99,
title: 'Linked PR',
@@ -133,9 +131,9 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'feature/local-worktree', 99)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
expect(metadataRequests).toHaveBeenCalledWith(
[
'pr',
'view',
@@ -143,7 +141,7 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
@@ -157,7 +155,7 @@ describe('getPRForBranch', () => {
it('caches exact REST stack probes across linked PR refreshes', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
metadataRequests.mockImplementation(async (args: string[]) => {
if (args[0] === 'pr') {
return {
stdout: JSON.stringify({
@@ -190,16 +188,14 @@ describe('getPRForBranch', () => {
await getPRForBranch('/repo-root', 'feature', 99, 'ssh-1')
expect(
ghExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'api' && args[1]?.includes('/99')
)
metadataRequests.mock.calls.filter(([args]) => args[0] === 'api' && args[1]?.includes('/99'))
).toHaveLength(2)
expect(ghExecFileAsyncMock.mock.calls.filter(([args]) => args[0] === 'pr')).toHaveLength(3)
expect(metadataRequests.mock.calls.filter(([args]) => args[0] === 'pr')).toHaveLength(3)
})
it('caches failed REST stack probes across linked PR refreshes', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
metadataRequests.mockImplementation(async (args: string[]) => {
if (args[0] === 'pr') {
return {
stdout: JSON.stringify({
@@ -224,15 +220,13 @@ describe('getPRForBranch', () => {
await getPRForBranch('/repo-root', 'feature', 99)
expect(
ghExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'api' && args[1]?.includes('/99')
)
metadataRequests.mock.calls.filter(([args]) => args[0] === 'api' && args[1]?.includes('/99'))
).toHaveLength(1)
})
it('hydrates repository merge method settings for exact PR lookups', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 99,
@@ -279,7 +273,7 @@ describe('getPRForBranch', () => {
})
expect(pr?.mergeQueueRequired).toBe(false)
expect(pr?.autoMergeAllowed).toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
expect.arrayContaining([
'api',
@@ -298,7 +292,7 @@ describe('getPRForBranch', () => {
it('isolates viewer-dependent merge metadata across SSH connections', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
let metadataProbe = 0
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
metadataRequests.mockImplementation(async (args: string[]) => {
if (args.includes('graphql')) {
metadataProbe += 1
return {
@@ -346,7 +340,7 @@ describe('getPRForBranch', () => {
it('treats linked PR metadata as authoritative even when the branch head differs', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'current-worktree-head\n', stderr: '' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 99,
@@ -384,36 +378,34 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(pr?.number).toBe(99)
})
it('does not fall back to branch discovery when linked PR metadata is stale', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
.mockRejectedValueOnce(new Error('HTTP 404: Not Found'))
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
title: 'Branch PR',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/42',
statusCheckRollup: [],
updatedAt: '2026-03-28T00:00:00Z',
isDraft: false,
mergeable: 'MERGEABLE',
baseRefName: 'main',
headRefName: 'feature/test',
baseRefOid: 'base-oid',
headRefOid: 'head-oid'
}
])
})
metadataRequests.mockRejectedValueOnce(new Error('HTTP 404: Not Found')).mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 42,
title: 'Branch PR',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/42',
statusCheckRollup: [],
updatedAt: '2026-03-28T00:00:00Z',
isDraft: false,
mergeable: 'MERGEABLE',
baseRefName: 'main',
headRefName: 'feature/test',
baseRefOid: 'base-oid',
headRefOid: 'head-oid'
}
])
})
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
1,
[
'pr',
@@ -422,17 +414,17 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
expect(metadataRequests).toHaveBeenCalledTimes(1)
expect(pr).toBeNull()
})
it('returns no PR when linked PR REST fallback also misses', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockRejectedValueOnce(new Error('GraphQL: could not resolve to PullRequest'))
.mockRejectedValueOnce(new Error('HTTP 404: Not Found'))
.mockResolvedValueOnce({
@@ -456,20 +448,20 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
['api', 'repos/acme/widgets/pulls/99', '--include'],
{
cwd: '/repo-root'
}
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(pr).toBeNull()
})
it('returns no PR when linked PR REST fallback has an unclassified failure', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockRejectedValueOnce(new Error('GraphQL: server exploded'))
.mockRejectedValueOnce(new Error('HTTP 500: server error'))
.mockResolvedValueOnce({
@@ -493,26 +485,26 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
['api', 'repos/acme/widgets/pulls/99', '--include'],
{
cwd: '/repo-root'
}
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(pr).toBeNull()
})
it('does not continue to branch discovery when linked PR REST fallback is rate limited', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
metadataRequests
.mockRejectedValueOnce(new Error('GraphQL: API rate limit already exceeded'))
.mockRejectedValueOnce(new Error('REST API rate limit already exceeded'))
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
1,
[
'pr',
@@ -521,24 +513,24 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
['api', 'repos/acme/widgets/pulls/99', '--include'],
{
cwd: '/repo-root'
}
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(pr).toBeNull()
})
it('uses linked PR number as the source of truth when provided', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 77,
title: 'Linked PR lookup',
@@ -557,8 +549,8 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'refs/heads/local-created-from-pr', 77)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledWith(
[
'pr',
'view',
@@ -566,7 +558,7 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
@@ -575,7 +567,7 @@ describe('getPRForBranch', () => {
it('normalizes exact linked PR fallback metadata when no GitHub remote is resolved', async () => {
getOwnerRepoMock.mockResolvedValueOnce(null)
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 77,
title: 'Linked fallback PR',
@@ -596,13 +588,13 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/non-github-repo', 'feature/test', 77)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
expect(metadataRequests).toHaveBeenCalledWith(
[
'pr',
'view',
'77',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/non-github-repo' }
)
@@ -617,7 +609,7 @@ describe('getPRForBranch', () => {
it('falls back to gh pr view when the remote cannot be resolved to GitHub', async () => {
getOwnerRepoMock.mockResolvedValueOnce(null)
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
ghExecFileAsyncMock.mockResolvedValueOnce({
metadataRequests.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 7,
title: 'Fallback lookup',
@@ -636,13 +628,13 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/non-github-repo', 'feature/test')
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
expect(metadataRequests).toHaveBeenCalledWith(
[
'pr',
'view',
'feature/test',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/non-github-repo' }
)
@@ -654,7 +646,7 @@ describe('getPRForBranch', () => {
it('falls back to REST number lookup when linked PR GraphQL lookup is rate limited', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'linked-head-oid\n', stderr: '' })
ghExecFileAsyncMock
metadataRequests
.mockRejectedValueOnce(new Error('GraphQL: API rate limit already exceeded'))
.mockResolvedValueOnce({
stdout: JSON.stringify({
@@ -673,7 +665,7 @@ describe('getPRForBranch', () => {
const pr = await getPRForBranch('/repo-root', 'feature/test', 99)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
1,
[
'pr',
@@ -682,18 +674,18 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
2,
['api', 'repos/acme/widgets/pulls/99', '--include'],
{
cwd: '/repo-root'
}
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(metadataRequests).toHaveBeenCalledTimes(2)
expect(pr).toMatchObject({
number: 99,
state: 'merged',
@@ -475,9 +475,10 @@ describe('GitHub PR local runtime routing', () => {
) {
return { stdout: '[]' }
}
if (endpoint.endsWith('/commits/head-sha/check-runs?per_page=100')) {
if (endpoint.endsWith('/commits/head-sha/check-runs?per_page=100&page=1')) {
return {
stdout: JSON.stringify({
total_count: 1,
check_runs: [
{
id: 88,
@@ -490,11 +491,11 @@ describe('GitHub PR local runtime routing', () => {
})
}
}
if (endpoint.endsWith('/commits/head-sha/status?per_page=100')) {
return { stdout: JSON.stringify({ statuses: [] }) }
if (endpoint.endsWith('/commits/head-sha/status?per_page=100&page=1')) {
return { stdout: JSON.stringify({ total_count: 0, statuses: [] }) }
}
if (endpoint.endsWith('/commits/head-sha/check-suites?per_page=100')) {
return { stdout: JSON.stringify({ check_suites: [] }) }
if (endpoint.endsWith('/commits/head-sha/check-suites?per_page=100&page=1')) {
return { stdout: JSON.stringify({ total_count: 0, check_suites: [] }) }
}
if (endpoint.endsWith('/check-runs/88')) {
return {
@@ -39,20 +39,28 @@ vi.mock('./github-api-repository', async (importOriginal) =>
import { getPRForBranch } from './client/lookup/get-pr-for-branch'
import { mergePR } from './client/merge/merge-pr'
import { resetGraphQLRateLimitGuardMocks } from './client-test-harness'
import {
installIndependentCheckFixtures,
resetGraphQLRateLimitGuardMocks
} from './client-test-harness'
const { ghExecFileAsyncMock, getOwnerRepoMock, acquireMock, releaseMock } = clientMocks
const { getOwnerRepoMock, acquireMock, releaseMock } = clientMocks
const metadataRequests = vi.fn()
const unexpectedChecks: string[][] = []
afterEach(() => expect(unexpectedChecks).toEqual([]))
describe('GitHub GraphQL rate-limit guard', () => {
beforeEach(() => {
resetGraphQLRateLimitGuardMocks(clientMocks)
installIndependentCheckFixtures(clientMocks, metadataRequests, unexpectedChecks)
})
afterEach(() => vi.restoreAllMocks())
it('hydrates GitHub-registered stack metadata for exact linked PRs', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca', host: 'github.com' })
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
data: {
@@ -74,7 +82,20 @@ describe('GitHub GraphQL rate-limit guard', () => {
headRefOid: 'a'.repeat(40),
headRepositoryOwner: { login: 'stablyai' },
headRepository: { name: 'orca' },
commits: { nodes: [{ commit: { oid: 'a'.repeat(40), statusCheckRollup: null } }] },
commits: {
nodes: [
{
commit: {
oid: 'a'.repeat(40),
statusCheckRollup: null,
checkSuites: {
nodes: [],
pageInfo: { hasNextPage: false, endCursor: null }
}
}
}
]
},
stack: { number: 51, size: 2, baseRefName: 'main' },
stackEntry: { position: 2 }
}
@@ -160,10 +181,10 @@ describe('GitHub GraphQL rate-limit guard', () => {
]
})
expect(pr?.mergeQueueRequired).toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(4)
expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain('number=202')
expect(ghExecFileAsyncMock.mock.calls[1][0]).toContain('branch=stack/models')
const mergeQueueMetadataCall = ghExecFileAsyncMock.mock.calls.find(
expect(metadataRequests).toHaveBeenCalledTimes(4)
expect(metadataRequests.mock.calls[0][0]).toContain('number=202')
expect(metadataRequests.mock.calls[1][0]).toContain('branch=stack/models')
const mergeQueueMetadataCall = metadataRequests.mock.calls.find(
([args]) => args.includes('graphql') && args.includes('branch=main')
)
expect(mergeQueueMetadataCall?.[0]).toEqual(expect.arrayContaining(['-f', 'branch=main']))
@@ -185,7 +206,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
baseSha: undefined
}
])('uses async merge only for GitHub-registered stacks using $objectFormat', async (scenario) => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 202,
@@ -219,7 +240,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
const mergeCall = ghExecFileAsyncMock.mock.calls.find(([args]) =>
const mergeCall = metadataRequests.mock.calls.find(([args]) =>
args.includes('repos/stablyai/orca/pulls/202/merge-async')
)
expect(mergeCall?.[0]).toEqual(
@@ -234,12 +255,12 @@ describe('GitHub GraphQL rate-limit guard', () => {
expect(acquireMock).toHaveBeenCalledTimes(2)
expect(releaseMock).toHaveBeenCalledTimes(2)
expect(
ghExecFileAsyncMock.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
metadataRequests.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
).toBe(false)
})
it('never falls back to legacy merge after an async stack merge transport failure', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 202,
@@ -267,7 +288,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: false, error: 'socket closed after request submission' })
expect(
ghExecFileAsyncMock.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
metadataRequests.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
).toBe(false)
})
@@ -558,12 +579,12 @@ describe('GitHub GraphQL rate-limit guard', () => {
])('fails closed on $failure', async (scenario) => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
if (scenario.probeResponse instanceof Error) {
ghExecFileAsyncMock.mockRejectedValueOnce(scenario.probeResponse)
metadataRequests.mockRejectedValueOnce(scenario.probeResponse)
} else {
ghExecFileAsyncMock.mockResolvedValueOnce(scenario.probeResponse)
metadataRequests.mockResolvedValueOnce(scenario.probeResponse)
}
// Stub legacy merge fallthrough so disabling the guard cannot fail on an unrelated missing fixture.
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 202,
@@ -589,20 +610,18 @@ describe('GitHub GraphQL rate-limit guard', () => {
expect.soft(result).toEqual({ ok: false, error: scenario.expectedError })
expect
.soft(ghExecFileAsyncMock.mock.calls.map(([args]) => args))
.soft(metadataRequests.mock.calls.map(([args]) => args))
.toEqual([['api', 'repos/stablyai/orca/pulls/202', '--include']])
expect.soft(ghExecFileAsyncMock.mock.calls[0]?.[1]).toEqual(scenario.expectedOptions)
expect.soft(metadataRequests.mock.calls[0]?.[1]).toEqual(scenario.expectedOptions)
expect
.soft(
ghExecFileAsyncMock.mock.calls.some(([args]) =>
metadataRequests.mock.calls.some(([args]) =>
args.includes('repos/stablyai/orca/pulls/202/merge-async')
)
)
.toBe(false)
expect
.soft(
ghExecFileAsyncMock.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge')
)
.soft(metadataRequests.mock.calls.some(([args]) => args[0] === 'pr' && args[1] === 'merge'))
.toBe(false)
expect.soft(acquireMock).toHaveBeenCalledTimes(1)
expect.soft(releaseMock).toHaveBeenCalledTimes(1)
@@ -619,7 +638,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
{ stackVariant: 'omits stack', stackField: {} },
{ stackVariant: 'sets stack to null', stackField: { stack: null } }
])('keeps legacy merge when an ordinary GitHub response $stackVariant', async (scenario) => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 13866,
@@ -659,7 +678,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
['pr', 'merge', '13866', '--squash', '--repo', 'stablyai/orca'],
expect.objectContaining({ env: expect.objectContaining({ GH_PROMPT_DISABLED: '1' }) })
@@ -669,7 +688,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
it('keeps legacy merge for unregistered dependent PR chains', async () => {
ghExecFileAsyncMock
metadataRequests
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 202,
@@ -704,7 +723,7 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
).resolves.toEqual({ ok: true })
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
expect(metadataRequests).toHaveBeenNthCalledWith(
3,
['pr', 'merge', '202', '--squash', '--repo', 'stablyai/orca'],
expect.objectContaining({ env: expect.objectContaining({ GH_PROMPT_DISABLED: '1' }) })
+150
View File
@@ -1,3 +1,5 @@
import type { Mock } from 'vitest'
import { completePRCheckGraphQLResponse } from '../../shared/github/complete-pr-check-test-fixture'
import type { GitHubClientMocks } from './client-test-mocks'
import { _resetOwnerRepoCache } from './gh-utils'
import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-metadata-cache'
@@ -83,3 +85,151 @@ export function resetGraphQLRateLimitGuardMocks(mocks: GitHubClientMocks): void
_resetPRStackSummaryCacheForTests()
__resetPRConflictSummaryCachesForTests()
}
export function mockPRLookupRoutes(
mocks: GitHubClientMocks,
routes: [string, () => Promise<{ stdout: string }>][],
unavailableChecks = false,
unexpectedRequests: string[][] = [],
contexts: Record<string, unknown>[] = []
): void {
const { ghExecFileAsyncMock } = mocks
const metadata = new Map<string, Record<string, unknown>>()
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
const repo = args[args.indexOf('--repo') + 1]
const key =
args[0] === 'pr'
? args[1] === 'view'
? `view:${repo}:${args[2]}`
: `list:${repo}:${args[args.indexOf('--head') + 1]}`
: args[1]
const index = routes.findIndex(([route]) => route === key)
if (index !== -1) {
const [, respond] = routes.splice(index, 1)[0]!
const result = await respond()
if (args[0] === 'pr' && args[1] === 'view') {
metadata.set(`${repo}:${args[2]}`, JSON.parse(result.stdout))
}
return result
}
if (args[0] === 'api') {
const target = [...metadata].find(
([identity]) => args[1] === `repos/${identity.replace(':', '/pulls/')}`
)
if (target) {
const [, item] = target
return {
stdout: JSON.stringify({
number: item.number,
title: item.title,
state: String(item.state).toLowerCase(),
html_url: item.url,
updated_at: item.updatedAt,
draft: item.isDraft,
head: { ref: item.headRefName, sha: item.headRefOid },
base: { ref: item.baseRefName, sha: item.baseRefOid }
})
}
}
}
if (
args[1] === 'graphql' &&
args.some((arg) => arg.startsWith('query=') && arg.includes('statusCheckRollup'))
) {
const field = (name: string) =>
args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1)
const item = metadata.get(`${field('owner')}/${field('repo')}:${field('number')}`)
if (item) {
if (unavailableChecks) {
throw new Error('checks unavailable')
}
return completePRCheckGraphQLResponse(
Number(item.number),
String(item.headRefOid),
contexts
)
}
}
if (
unavailableChecks &&
args[0] === 'api' &&
[...metadata].some(
([target, item]) =>
args[1] ===
`repos/${target.split(':')[0]}/commits/${String(item.headRefOid)}/check-runs?per_page=100&page=1`
)
) {
throw new Error('checks recovery unavailable')
}
unexpectedRequests.push(args)
throw new Error('unexpected gh request')
})
}
/** Keep metadata/mutation scripts independent of head-pinned check collection. */
export function installIndependentCheckFixtures(
mocks: GitHubClientMocks,
metadataRequests: Mock,
unexpectedChecks: string[][]
): void {
const targets = new Map<
string,
{ number: number; head: string; contexts: Record<string, unknown>[] }
>()
metadataRequests.mockReset()
unexpectedChecks.length = 0
mocks.ghExecFileAsyncMock.mockImplementation(async (args: string[], ...options: unknown[]) => {
const field = (name: string) =>
args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1)
if (
args[1] === 'graphql' &&
args.some(
(arg) =>
arg.startsWith('query=') &&
arg.includes('statusCheckRollup') &&
!arg.includes('updatedAt')
)
) {
const target = targets.get(`${field('owner')}/${field('repo')}:${field('number')}`)
if (target) {
return completePRCheckGraphQLResponse(target.number, target.head, target.contexts)
}
unexpectedChecks.push(args)
throw new Error('checks requested for an unregistered metadata target')
}
const response = await metadataRequests(args, ...options)
if (response?.stdout) {
const exact = args[0] === 'pr' && args[1] === 'view'
const restRepo =
args[0] === 'api'
? /^repos\/([^/]+\/[^/]+)\/pulls(?:\/\d+|\?head=)/.exec(args[1])?.[1]
: undefined
if (exact || restRepo) {
const parsed = JSON.parse(response.stdout)
for (const item of Array.isArray(parsed) ? parsed : [parsed]) {
if (!item || typeof item !== 'object') {
continue
}
const head = exact ? item.headRefOid : item.head?.sha
const repo =
restRepo ??
(args.includes('--repo')
? args[args.indexOf('--repo') + 1]
: /https?:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\//.exec(item.url)?.[1])
if (repo && typeof item.number === 'number' && typeof head === 'string') {
targets.set(`${repo}:${item.number}`, {
number: item.number,
head,
contexts: (item.statusCheckRollup ?? []).map((row: Record<string, unknown>) => ({
__typename: row.__typename ?? (row.context ? 'StatusContext' : 'CheckRun'),
...(!row.context ? { checkSuite: { databaseId: 1 } } : {}),
...row
}))
})
}
}
}
}
return response
})
}
@@ -131,7 +131,7 @@ describe('getPRForBranch', () => {
'--repo',
'acme/widgets',
'--json',
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
],
{ cwd: '/repo-root' }
)
@@ -53,13 +53,55 @@ import { _resetMergeQueueCacheForTests } from './client/detect/repository-merge-
import { _resetOwnerRepoCache } from './gh-utils'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
// Only fixture pr view; best-effort merge metadata failures must not hide its summary.
// Metadata and complete checks are independent provider responses.
function mockPullRequestDetail(payload: Record<string, unknown>): void {
const head = 'a'.repeat(40)
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] !== 'pr' || args[1] !== 'view') {
throw new Error('unexpected gh call')
if (args[0] === 'pr' && args[1] === 'view') {
return { stdout: JSON.stringify({ ...payload, headRefOid: head }) }
}
return { stdout: JSON.stringify(payload) }
if (args.includes('graphql') && args.some((arg) => arg.includes('statusCheckRollup'))) {
return {
stdout: JSON.stringify({
data: {
repository: {
pullRequest: {
number: payload.number,
headRefOid: head,
commits: {
nodes: [
{
commit: {
oid: head,
statusCheckRollup: {
contexts: {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these test payloads contain object rollup rows only.
nodes: (payload.statusCheckRollup as Record<string, unknown>[]).map(
(raw) => ({
...raw,
__typename:
raw.__typename ?? (raw.state ? 'StatusContext' : 'CheckRun'),
...(!raw.state ? { checkSuite: { databaseId: 7 } } : {})
})
),
pageInfo: { hasNextPage: false, endCursor: null }
}
},
checkSuites: {
nodes: [],
pageInfo: { hasNextPage: false, endCursor: null }
}
}
}
]
}
}
}
}
})
}
}
throw new Error('unexpected gh call')
})
}
@@ -0,0 +1,26 @@
import type { GitHubApiRepository } from '../../github-api-repository'
import type { GhExecOptions } from '../github-exec-scope'
import { assertRateLimitBudget } from '../lookup/pr-lookup-rate-limit'
import { readGraphQLPRCheckRollup } from './pr-check-rollup'
import { readRestPRCheckRollup } from './rest-pr-check-pages'
export async function readCompletePRCheckObservation(
repository: GitHubApiRepository,
number: number,
headSha: string | undefined,
options: GhExecOptions
): Promise<unknown[] | undefined> {
if (!headSha) {
return undefined
}
try {
await assertRateLimitBudget('graphql', repository, options)
return await readGraphQLPRCheckRollup(repository, number, headSha, options)
} catch {
try {
return await readRestPRCheckRollup(repository, headSha, options)
} catch {
return undefined
}
}
}
+53 -72
View File
@@ -1,3 +1,4 @@
import { normalizePRCheckHeadSha } from '../../../../shared/github/pr-check-head'
import {
withGitHubRepoExecution,
githubRepoContext,
@@ -14,21 +15,21 @@ import { mapCheckStatus, mapCheckConclusion } from '../../mappers'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import type { GhExecOptions } from './../github-exec-scope'
import { assertRateLimitBudget } from './../lookup/pr-lookup-rate-limit'
import {
PR_CHECKS_ROLLUP_QUERY,
type GraphQLPRChecksResponse,
type RestCheckRun,
type RestCommitStatus,
type RestCheckSuite
} from './pr-checks-graphql-query'
import { PR_CHECKS_ROLLUP_QUERY, type GraphQLPRChecksResponse } from './pr-checks-graphql-query'
import {
mapRestCheckRun,
mapRestCommitStatus,
mapGraphQLPRChecksResponse,
getPendingApprovalCheckSuiteName,
getPendingApprovalCheckSuiteUrl
getPendingApprovalCheckSuiteUrl,
isPendingApprovalSuiteWithoutRuns
} from './pr-checks-response-mapping'
import { parseActionsRunId } from './check-detail-field-mapping'
import {
readRestPRCheckRuns,
readRestPRCommitStatuses,
readRestPRCheckSuites
} from './rest-pr-check-pages'
export async function getPRChecksViaRestFallback(
ownerRepo: GitHubApiRepository,
headSha: string | undefined,
@@ -38,82 +39,63 @@ export async function getPRChecksViaRestFallback(
if (!headSha) {
return null
}
try {
await assertRateLimitBudget('core', ownerRepo, ghOptions)
} catch (err) {
console.warn('getPRChecks skipped REST fallback, falling back to gh pr checks:', err)
return null
}
await acquire()
try {
const cacheArgs = noCache ? [] : ['--cache', '60s']
const encodedHeadSha = encodeURIComponent(headSha)
const { stdout } = await ghExecFileAsync(
[
'api',
...cacheArgs,
`repos/${ownerRepo.owner}/${ownerRepo.repo}/commits/${encodedHeadSha}/check-runs?per_page=100`
],
ghOptions
const runs = await readRestPRCheckRuns(ownerRepo, headSha, ghOptions, Boolean(noCache))
const checks = runs.map((run) =>
mapRestCheckRun({
...run,
name: run.name ?? '',
html_url: run.html_url ?? '',
details_url: run.details_url ?? null
})
)
noteRepositoryRateLimitSpend(ownerRepo, 'core', 1, ghOptions)
const checkRunData = JSON.parse(stdout) as {
check_runs?: RestCheckRun[]
}
const checkRuns = (checkRunData.check_runs ?? []).map(mapRestCheckRun)
const checkRunNames = new Set(checkRuns.map((check) => check.name))
let legacyStatuses: PRCheckDetail[] = []
const checkRunNames = new Set(checks.map((check) => check.name))
try {
const statusResult = await ghExecFileAsync(
[
'api',
...cacheArgs,
`repos/${ownerRepo.owner}/${ownerRepo.repo}/commits/${encodedHeadSha}/status?per_page=100`
],
ghOptions
const statuses = await readRestPRCommitStatuses(
ownerRepo,
headSha,
ghOptions,
Boolean(noCache)
)
checks.push(
...statuses
.map(mapRestCommitStatus)
.filter(
(check): check is PRCheckDetail => check !== null && !checkRunNames.has(check.name)
)
)
noteRepositoryRateLimitSpend(ownerRepo, 'core', 1, ghOptions)
const statusData = JSON.parse(statusResult.stdout) as {
statuses?: RestCommitStatus[]
}
legacyStatuses = (statusData.statuses ?? [])
.map(mapRestCommitStatus)
.filter((check): check is PRCheckDetail => check !== null && !checkRunNames.has(check.name))
} catch (err) {
// Keep richer check-run rows when best-effort legacy-status enrichment fails.
// Detail rows remain useful when best-effort enrichment fails.
console.warn('getPRChecks REST status fallback failed:', err)
}
let pendingApprovalChecks: PRCheckDetail[] = []
try {
const suitesResult = await ghExecFileAsync(
[
'api',
...cacheArgs,
`repos/${ownerRepo.owner}/${ownerRepo.repo}/commits/${encodedHeadSha}/check-suites?per_page=100`
],
ghOptions
const suites = await readRestPRCheckSuites(ownerRepo, headSha, ghOptions, Boolean(noCache))
const represented = new Set(
runs.flatMap((run) => (run.check_suite ? [run.check_suite.id] : []))
)
checks.push(
...suites
.filter((suite) =>
isPendingApprovalSuiteWithoutRuns(
suite.conclusion,
suite.id !== undefined && represented.has(suite.id)
)
)
.map((suite, index) => ({
name: getPendingApprovalCheckSuiteName(suite, headSha, index),
status: 'completed' as const,
conclusion: 'action_required' as const,
url: getPendingApprovalCheckSuiteUrl(ownerRepo, headSha, suite.id)
}))
)
noteRepositoryRateLimitSpend(ownerRepo, 'core', 1, ghOptions)
const suitesData = JSON.parse(suitesResult.stdout) as {
check_suites?: RestCheckSuite[]
}
pendingApprovalChecks = (suitesData.check_suites ?? [])
.filter((suite) => suite.conclusion?.toLowerCase() === 'action_required')
.map((suite, index) => ({
name: getPendingApprovalCheckSuiteName(suite, headSha, index),
status: 'completed' as const,
conclusion: 'action_required' as const,
url: getPendingApprovalCheckSuiteUrl(ownerRepo, headSha, suite.id)
}))
} catch (err) {
console.warn('getPRChecks REST check-suite fallback failed:', err)
}
const checks = [...checkRuns, ...legacyStatuses, ...pendingApprovalChecks]
return checks.length > 0 ? checks : null
const observedHeadSha = normalizePRCheckHeadSha(headSha)
return checks.length > 0
? checks.map((check) => (observedHeadSha ? { ...check, observedHeadSha } : check))
: null
} catch (err) {
console.warn('getPRChecks via REST fallback failed, falling back to gh pr checks:', err)
return null
@@ -132,7 +114,6 @@ async function readPRChecks(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<PRCheckDetail[]> {
void headSha
const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution(
repoPath,
prRepo,
@@ -0,0 +1,223 @@
import { z } from 'zod'
import { ghExecFileAsync } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import type { GhExecOptions } from '../github-exec-scope'
import { isPendingApprovalSuiteWithoutRuns } from './pr-checks-response-mapping'
type ConnectionKind = 'contexts' | 'suites'
export function prCheckRollupFields(kind?: ConnectionKind): string {
return `commits(last: 1) { nodes { commit {
oid
${
kind !== 'suites'
? `statusCheckRollup { contexts(first: 100, after: $checksCursor) {
nodes { __typename ... on CheckRun { status conclusion checkSuite { databaseId } }
... on StatusContext { state } }
pageInfo { hasNextPage endCursor }
} }`
: ''
}
${
kind !== 'contexts'
? `checkSuites(first: 100, after: $suitesCursor) {
nodes { databaseId conclusion }
pageInfo { hasNextPage endCursor }
}`
: ''
}
} } }`
}
const checkConclusion = z.enum([
'ACTION_REQUIRED',
'CANCELLED',
'FAILURE',
'NEUTRAL',
'SKIPPED',
'STALE',
'STARTUP_FAILURE',
'SUCCESS',
'TIMED_OUT'
])
const contextSchema = z.discriminatedUnion('__typename', [
z.object({
__typename: z.literal('CheckRun'),
status: z.enum(['COMPLETED', 'IN_PROGRESS', 'QUEUED', 'REQUESTED', 'WAITING', 'PENDING']),
conclusion: checkConclusion.nullable(),
checkSuite: z.object({ databaseId: z.number() })
}),
z.object({
__typename: z.literal('StatusContext'),
state: z.enum(['ERROR', 'EXPECTED', 'FAILURE', 'PENDING', 'SUCCESS'])
})
])
const connectionSchema = z.object({
nodes: z.array(contextSchema),
pageInfo: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullable() })
})
export const prCheckRollupCommitsSchema = z.object({
nodes: z
.array(
z.object({
commit: z.object({
oid: z.string().min(1),
statusCheckRollup: z.object({ contexts: connectionSchema }).nullable().optional(),
checkSuites: z
.object({
nodes: z.array(
z.object({ databaseId: z.number(), conclusion: checkConclusion.nullable() })
),
pageInfo: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullable() })
})
.optional()
})
})
)
.max(1)
})
type CheckCommits = z.infer<typeof prCheckRollupCommitsSchema>
type CheckPage = { number: number; headRefOid: string; commits: CheckCommits }
const pageSchema = z.object({
data: z.object({
repository: z.object({
pullRequest: z.object({
number: z.number(),
headRefOid: z.string(),
commits: prCheckRollupCommitsSchema
})
})
}),
errors: z.array(z.unknown()).optional()
})
export async function collectPRCheckRollup(
first: CheckPage,
readPage: (kind: ConnectionKind, cursor: string) => Promise<CheckPage>
): Promise<unknown[]> {
const initial = first.commits.nodes[0]?.commit
if (
!initial ||
initial.oid !== first.headRefOid ||
!initial.checkSuites ||
initial.statusCheckRollup === undefined
) {
throw new Error('Incomplete PR checks observation')
}
const collect = async (kind: ConnectionKind) => {
const connection = (commit: typeof initial) =>
kind === 'contexts' ? commit.statusCheckRollup?.contexts : commit.checkSuites
let page = connection(initial)
const nodes: Record<string, unknown>[] = [...(page?.nodes ?? [])]
const cursors = new Set<string>()
while (page?.pageInfo.hasNextPage) {
const cursor = page.pageInfo.endCursor
if (!cursor || cursors.has(cursor)) {
throw new Error('Incomplete PR checks continuation')
}
cursors.add(cursor)
const next = await readPage(kind, cursor)
const commit = next.commits.nodes[0]?.commit
if (
next.number !== first.number ||
next.headRefOid !== first.headRefOid ||
!commit ||
commit.oid !== initial.oid
) {
throw new Error('PR head changed during checks observation')
}
page = connection(commit)
if (!page) {
throw new Error('Missing PR checks continuation')
}
nodes.push(...page.nodes)
}
return nodes
}
const contexts = await collect('contexts')
const suites = await collect('suites')
const representedSuites = new Set(
contexts.flatMap((context) => {
const suite = context.checkSuite
return context.__typename === 'CheckRun' &&
typeof suite === 'object' &&
suite !== null &&
'databaseId' in suite &&
typeof suite.databaseId === 'number'
? [suite.databaseId]
: []
})
)
return [
...contexts,
...suites
.filter((suite) =>
isPendingApprovalSuiteWithoutRuns(
suite.conclusion,
typeof suite.databaseId === 'number' && representedSuites.has(suite.databaseId)
)
)
.map(() => ({ status: 'COMPLETED', conclusion: 'ACTION_REQUIRED' }))
]
}
export function createPRCheckRollupPageReader(
repository: GitHubApiRepository,
number: number,
options: GhExecOptions
): (kind?: ConnectionKind, cursor?: string) => Promise<CheckPage> {
return async (kind, cursor) => {
const cursors = [
...(kind !== 'suites' ? ['$checksCursor: String'] : []),
...(kind !== 'contexts' ? ['$suitesCursor: String'] : [])
].join(', ')
const query = `query($owner: String!, $repo: String!, $number: Int!,
${cursors}) {
repository(owner: $owner, name: $repo) { pullRequest(number: $number) {
number headRefOid ${prCheckRollupFields(kind)}
} }
}`
const args = [
'api',
'graphql',
'-f',
`query=${query}`,
'-f',
`owner=${repository.owner}`,
'-f',
`repo=${repository.repo}`,
'-F',
`number=${number}`
]
if (cursor) {
args.push('-f', `${kind === 'suites' ? 'suitesCursor' : 'checksCursor'}=${cursor}`)
}
noteRepositoryRateLimitSpend(repository, 'graphql', 1, options)
const { stdout } = await ghExecFileAsync(args, {
...options,
...githubHostExecOptions(repository)
})
const page = pageSchema.parse(JSON.parse(stdout))
if (page.errors?.length) {
throw new Error('Incomplete PR checks response')
}
return page.data.repository.pullRequest
}
}
export async function readGraphQLPRCheckRollup(
repository: GitHubApiRepository,
number: number,
headSha: string,
options: GhExecOptions
): Promise<unknown[]> {
const readPage = createPRCheckRollupPageReader(repository, number, options)
const first = await readPage()
if (first.number !== number || first.headRefOid !== headSha) {
throw new Error('PR head changed before checks observation')
}
return collectPRCheckRollup(first, readPage)
}
@@ -6,6 +6,7 @@ query($owner: String!, $repo: String!, $pr: Int!) {
commits(last: 1) {
nodes {
commit {
oid
statusCheckRollup {
contexts(first: 100) {
nodes {
@@ -53,6 +54,8 @@ query($owner: String!, $repo: String!, $pr: Int!) {
`
export type GraphQLPRChecksResponse = {
errors?: unknown[]
data?: {
repository?: {
pullRequest?: {
@@ -66,6 +69,8 @@ export type GraphQLPRChecksResponse = {
}
export type GraphQLPRChecksCommit = {
oid?: string | null
statusCheckRollup?: {
contexts?: {
nodes?: GraphQLStatusCheckContext[] | null
@@ -1,3 +1,4 @@
import { normalizePRCheckHeadSha } from '../../../../shared/github/pr-check-head'
import type { PRCheckDetail } from '../../../../shared/github/check-types'
import { githubRepositoryWebHost, type GitHubApiRepository } from '../../github-api-repository'
import {
@@ -107,6 +108,12 @@ export function mapGraphQLPendingApprovalCheckSuite(
}
}
export function isPendingApprovalSuiteWithoutRuns(conclusion: unknown, hasRuns: boolean): boolean {
return (
typeof conclusion === 'string' && conclusion.toLowerCase() === 'action_required' && !hasRuns
)
}
export function mapGraphQLPRChecksResponse(
ownerRepo: GitHubApiRepository,
response: GraphQLPRChecksResponse
@@ -137,16 +144,28 @@ export function mapGraphQLPRChecksResponse(
.map(mapGraphQLStatusContext)
.filter((check): check is PRCheckDetail => check !== null && !checkRunNames.has(check.name))
const pendingApprovalChecks = (commit.checkSuites?.nodes ?? [])
.filter((suite) => suite.conclusion?.toLowerCase() === 'action_required')
.filter((suite) => {
const suiteId = nullableNumber(suite.databaseId)
return suiteId === null || !checkSuiteIdsWithRuns.has(suiteId)
return isPendingApprovalSuiteWithoutRuns(
suite.conclusion,
suiteId !== null && checkSuiteIdsWithRuns.has(suiteId)
)
})
.map((suite, index) =>
mapGraphQLPendingApprovalCheckSuite(ownerRepo, suite, pullRequest.headRefOid, index)
)
return [...checkRuns, ...legacyStatuses, ...pendingApprovalChecks]
const observedHeadSha = normalizePRCheckHeadSha(commit.oid)
const provedHead =
observedHeadSha &&
observedHeadSha === normalizePRCheckHeadSha(pullRequest.headRefOid) &&
(response.errors === undefined ||
(Array.isArray(response.errors) && response.errors.length === 0))
? observedHeadSha
: undefined
return [...checkRuns, ...legacyStatuses, ...pendingApprovalChecks].map((check) =>
provedHead ? { ...check, observedHeadSha: provedHead } : check
)
}
export function getPendingApprovalCheckSuiteName(
@@ -0,0 +1,135 @@
import { z } from 'zod'
import { ghExecFileAsync } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import type { GhExecOptions } from '../github-exec-scope'
import { assertRateLimitBudget } from '../lookup/pr-lookup-rate-limit'
import { isPendingApprovalSuiteWithoutRuns } from './pr-checks-response-mapping'
const checkRunSchema = z.object({
id: z.number().optional(),
name: z.string().optional(),
status: z.string(),
conclusion: z.string().nullable(),
html_url: z.string().nullish(),
details_url: z.string().nullish(),
check_suite: z.object({ id: z.number() }).nullish()
})
const commitStatusSchema = z.object({
context: z.string().optional(),
state: z.string(),
target_url: z.string().nullish()
})
const checkSuiteSchema = z.object({
id: z.number().optional(),
status: z.string().nullable(),
conclusion: z.string().nullable(),
app: z.object({ name: z.string().nullish(), slug: z.string().nullish() }).nullish()
})
async function readPages<T>(
repository: GitHubApiRepository,
headSha: string,
options: GhExecOptions,
endpoint: string,
field: string,
row: z.ZodType<T>,
noCache: boolean
): Promise<T[]> {
const schema = z.looseObject({
total_count: z.number().int().nonnegative()
})
const rows: T[] = []
let total: number | undefined
for (let page = 1; ; page++) {
await assertRateLimitBudget('core', repository, options)
const path = `repos/${repository.owner}/${repository.repo}/commits/${encodeURIComponent(headSha)}/${endpoint}?per_page=100&page=${page}`
const { stdout } = await ghExecFileAsync(
['api', ...(noCache ? [] : ['--cache', '60s']), path],
{ ...options, ...githubHostExecOptions(repository) }
)
noteRepositoryRateLimitSpend(repository, 'core', 1, options)
const result = schema.parse(JSON.parse(stdout))
const count = result.total_count
const pageRows = z.array(row).parse(result[field])
if (total !== undefined && total !== count) {
throw new Error('PR checks changed during REST pagination')
}
total = count
rows.push(...pageRows)
if (rows.length > total || (pageRows.length === 0 && rows.length < total)) {
throw new Error('Incomplete REST PR checks response')
}
if (rows.length === total) {
return rows
}
}
}
export function readRestPRCheckRuns(
repository: GitHubApiRepository,
headSha: string,
options: GhExecOptions,
noCache = true
) {
return readPages(
repository,
headSha,
options,
'check-runs',
'check_runs',
checkRunSchema,
noCache
)
}
export function readRestPRCommitStatuses(
repository: GitHubApiRepository,
headSha: string,
options: GhExecOptions,
noCache = true
) {
return readPages(repository, headSha, options, 'status', 'statuses', commitStatusSchema, noCache)
}
export function readRestPRCheckSuites(
repository: GitHubApiRepository,
headSha: string,
options: GhExecOptions,
noCache = true
) {
return readPages(
repository,
headSha,
options,
'check-suites',
'check_suites',
checkSuiteSchema,
noCache
)
}
export async function readRestPRCheckRollup(
repository: GitHubApiRepository,
headSha: string,
options: GhExecOptions
): Promise<unknown[]> {
const runs = await readRestPRCheckRuns(repository, headSha, options)
const statuses = await readRestPRCommitStatuses(repository, headSha, options)
const suites = await readRestPRCheckSuites(repository, headSha, options)
const representedSuites = new Set(
runs.flatMap((run) => (run.check_suite ? [run.check_suite.id] : []))
)
return [
...runs.map((run) => ({ status: run.status, conclusion: run.conclusion })),
...statuses,
...suites
.filter((suite) =>
isPendingApprovalSuiteWithoutRuns(
suite.conclusion,
suite.id !== undefined && representedSuites.has(suite.id)
)
)
.map(() => ({ status: 'COMPLETED', conclusion: 'ACTION_REQUIRED' }))
]
}
@@ -17,6 +17,26 @@ import {
type MainWorkItem
} from './../map/work-item-field-coercion'
import { mapIssueWorkItem, mapPullRequestWorkItem } from './../map/work-item'
import { readCompletePRCheckObservation } from '../check/complete-pr-check-observation'
import { summarizePRCheckRollup } from '../../../../shared/source-control/checks/pr-check-status'
import { ownerRepoFromPullRequestUrl } from '../github-exec-scope'
async function withCompletePRChecks(
item: MainWorkItem,
repository: GitHubApiRepository | null,
number: number,
options: GhExecOptions
): Promise<MainWorkItem> {
const source = repository ?? ownerRepoFromPullRequestUrl(item.url)
const rollup =
source && item.number === number
? await readCompletePRCheckObservation(source, number, item.headSha, options)
: undefined
return {
...item,
checksSummary: rollup === undefined ? undefined : summarizePRCheckRollup(rollup)
}
}
export async function fetchIssueWorkItem(
repoPath: string,
ownerRepo: GitHubApiRepository | null,
@@ -142,7 +162,12 @@ export async function fetchPullRequestWorkItem(
ghOptions
)
const item = JSON.parse(stdout) as Record<string, unknown>
const mapped = mapPullRequestWorkItem(item, ownerRepo)
const mapped = await withCompletePRChecks(
mapPullRequestWorkItem(item, ownerRepo),
ownerRepo,
number,
ghOptions
)
// Best-effort merge metadata must not trigger a REST fallback that drops latestReviews.
const baseRefName = typeof item.baseRefName === 'string' ? item.baseRefName : undefined
try {
@@ -170,9 +195,11 @@ export async function fetchPullRequestWorkItem(
['api', `repos/${ownerRepo.owner}/${ownerRepo.repo}/pulls/${number}`],
ghOptions
)
const mapped = mapPullRequestWorkItem(
JSON.parse(stdout) as Record<string, unknown>,
ownerRepo
const mapped = await withCompletePRChecks(
mapPullRequestWorkItem(JSON.parse(stdout) as Record<string, unknown>, ownerRepo),
ownerRepo,
number,
ghOptions
)
const reviewFields = await fetchPullRequestReviewFields(number, ownerRepo, ghOptions)
return { ...mapped, ...reviewFields }
@@ -188,7 +215,12 @@ export async function fetchPullRequestWorkItem(
['pr', 'view', String(number), '--json', WORK_ITEM_PR_DETAIL_JSON_FIELDS],
ghOptions
)
return mapPullRequestWorkItem(JSON.parse(stdout) as Record<string, unknown>)
return withCompletePRChecks(
mapPullRequestWorkItem(JSON.parse(stdout) as Record<string, unknown>),
null,
number,
ghOptions
)
}
export async function fetchPullRequestWorkItemFromCandidates(
@@ -9,17 +9,19 @@ import type { GhExecOptions } from './../github-exec-scope'
import { isNoPullRequestError } from './../gh-error-predicates'
import {
PR_LOOKUP_JSON_FIELDS,
normalizePullRequestLookupData,
PR_BRANCH_LIST_JSON_FIELDS,
mapRestPullRequest,
normalizePullRequestLookupData,
pullRequestMatchesHeadRepository,
type PullRequestLookupData,
type PullRequestLookupPolicy,
type RestPullRequest
} from './pull-request-lookup-data'
import { getPRByNumber } from './pr-number-lookup'
import { hydratePullRequestLookupData } from './pull-request-lookup-hydration'
import { revalidateGitHubRestJson } from '../../github-rest-revalidation'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { ownerRepoFromPullRequestUrl } from '../github-exec-scope'
// Why: GitHub lists a branch's PRs newest first, so with a page of one a closed PR opened after
// the live one wins. Fetch a page and prefer the open PR; fall back to the newest one.
@@ -72,7 +74,7 @@ export async function getFallbackPRListForBranch(
const list = JSON.parse(stdout) as PullRequestLookupData[]
const pr = preferOpenPullRequest(list)
// Branch lists paginate PRs, not their nested check contexts.
return pr ? { ...pr, checksUnavailable: true } : null
return pr ? { ...pr, statusCheckRollup: [], checksUnavailable: true } : null
}
export async function hydrateBranchLookupWithExactPR(
@@ -189,13 +191,20 @@ export async function lookupPRByBranchName(args: {
args.ghOptions
)
const parsedData: PullRequestLookupData = JSON.parse(stdout)
const lookup = {
data: normalizePullRequestLookupData({
...parsedData,
checksUnavailable: false
}),
dataRepo: null
}
const repository = ownerRepoFromPullRequestUrl(parsedData.url)
const data = repository
? await hydratePullRequestLookupData(
repository,
{ ...parsedData, checksUnavailable: true },
args.ghOptions,
args.executionScope
)
: {
...normalizePullRequestLookupData(parsedData),
statusCheckRollup: [],
checksUnavailable: true
}
const lookup = { data, dataRepo: null }
return !args.policy || (await args.policy.accepts(lookup.data, null))
? lookup
: { data: null, dataRepo: null }
@@ -1,3 +1,4 @@
import { classifyPRRefreshError } from '../../pr-refresh-error-classification'
import { ghExecFileAsync } from '../../gh-utils'
import type { OwnerRepo, ghRepoExecOptions } from '../../gh-utils'
import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository'
@@ -8,8 +9,8 @@ import {
} from './../gh-error-predicates'
import {
PR_LOOKUP_JSON_FIELDS,
mapRestPullRequest,
normalizePullRequestLookupData,
mapRestPullRequest,
type PullRequestLookupData,
type PullRequestLookupPolicy,
type RestPullRequest
@@ -18,6 +19,7 @@ import { hydratePullRequestLookupData } from './pull-request-lookup-hydration'
import { isGitObjectId, isUsableRestStackMetadata } from './rest-stack-metadata-validation'
import { revalidateGitHubRestJson } from '../../github-rest-revalidation'
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import { ownerRepoFromPullRequestUrl, sameOwnerRepo } from '../github-exec-scope'
import { readPullRequestSidebarQuery } from './pull-request-sidebar-query'
export async function getRestPRByNumber(
ownerRepo: GitHubApiRepository,
@@ -61,22 +63,59 @@ export async function getPRByNumber(
options: { includeStackSummary?: boolean } = {}
): Promise<PullRequestLookupData | null> {
try {
const sidebarData =
options.includeStackSummary && knownPullRequestData === undefined
? await readPullRequestSidebarQuery(ownerRepo, number, ghOptions)
: undefined
let sidebarData: Awaited<ReturnType<typeof readPullRequestSidebarQuery>>
try {
sidebarData =
options.includeStackSummary || knownPullRequestData !== undefined
? await readPullRequestSidebarQuery(ownerRepo, number, ghOptions)
: undefined
} catch (error) {
if (
!knownPullRequestData ||
['repo_unavailable', 'permission', 'rate_limited', 'auth'].includes(
classifyPRRefreshError(error)
)
) {
throw error
}
// Optional combined reads retain the exact metadata recovery path.
sidebarData = undefined
}
if (sidebarData === null) {
return null
}
if (sidebarData) {
const { stack, ...prData } = sidebarData
const known = knownPullRequestData
const sameIdentity =
known &&
known.number === number &&
sameOwnerRepo(ownerRepoFromPullRequestUrl(known.url), ownerRepo) &&
known.headRepositoryOwner?.login?.toLowerCase() ===
prData.headRepositoryOwner?.login?.toLowerCase() &&
known.headRepository?.name?.toLowerCase() === prData.headRepository?.name?.toLowerCase()
const compatibleStack =
sameIdentity &&
known.headRefOid === prData.headRefOid &&
known.baseRefOid === prData.baseRefOid &&
stack &&
known.stack?.number === stack.number &&
known.stack.position === stack.position &&
known.stack.baseRefName === stack.baseRefName
? { ...known.stack, ...stack }
: stack
const hydrated = await hydratePullRequestLookupData(
ownerRepo,
prData,
{
...prData,
...(sameIdentity && known.headDefaultBranchName
? { headDefaultBranchName: known.headDefaultBranchName }
: {})
},
ghOptions,
executionScope
)
return { ...hydrated, ...(stack ? { stack } : {}) }
return { ...hydrated, ...(compatibleStack ? { stack: compatibleStack } : {}) }
}
noteRepositoryRateLimitSpend(ownerRepo, 'graphql', 1, ghOptions)
const { stdout } = await ghExecFileAsync(
@@ -97,7 +136,7 @@ export async function getPRByNumber(
{
...knownPullRequestData,
...exactData,
checksUnavailable: false,
checksUnavailable: true,
...(knownPullRequestData?.stack ? { stack: knownPullRequestData.stack } : {})
},
ghOptions,
@@ -114,7 +153,12 @@ export async function getPRByNumber(
? await getRestPRByNumber(ownerRepo, number, ghOptions)
: knownPullRequestData
return restData
? hydratePullRequestLookupData(ownerRepo, restData, ghOptions, executionScope)
? hydratePullRequestLookupData(
ownerRepo,
{ ...restData, checksUnavailable: true },
ghOptions,
executionScope
)
: null
} catch (restErr) {
if (isNotFoundGhError(restErr)) {
@@ -171,13 +215,20 @@ export async function lookupPRByNumber(args: {
args.ghOptions
)
const parsedData: PullRequestLookupData = JSON.parse(stdout)
const lookup = {
data: normalizePullRequestLookupData({
...parsedData,
checksUnavailable: false
}),
dataRepo: null
}
const repository = ownerRepoFromPullRequestUrl(parsedData.url)
const data = repository
? await hydratePullRequestLookupData(
repository,
{ ...parsedData, checksUnavailable: true },
args.ghOptions,
args.executionScope
)
: {
...normalizePullRequestLookupData(parsedData),
statusCheckRollup: [],
checksUnavailable: true
}
const lookup = { data, dataRepo: null }
return !args.policy || (await args.policy.accepts(lookup.data, null))
? lookup
: { data: null, dataRepo: null }
@@ -40,7 +40,10 @@ export function assemblePRRefreshFoundOutcome(args: {
title: data.title,
state: mapPRState(data.state, data.isDraft),
url: data.url,
checksStatus: checksSummary.state === 'none' ? 'neutral' : checksSummary.state,
checksStatus:
data.checksUnavailable !== false || checksSummary.state === 'none'
? 'neutral'
: checksSummary.state,
...(data.checksUnavailable === false ? { checksSummary } : {}),
updatedAt: data.updatedAt,
mergeable,
@@ -81,10 +81,10 @@ export type RestPullRequest = {
}
export const PR_LOOKUP_JSON_FIELDS =
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,reviewDecision,mergeStateStatus,autoMergeRequest,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
export const PR_BRANCH_LIST_JSON_FIELDS =
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
'number,title,state,url,updatedAt,isDraft,mergeable,baseRefName,headRefName,baseRefOid,headRefOid,headRepositoryOwner,headRepository,isCrossRepository'
export type GitHubPRBranchLookupOptions = HostedReviewExecutionOptions & {
userRefresh?: boolean
@@ -6,6 +6,7 @@ import {
normalizePullRequestLookupData,
type PullRequestLookupData
} from './pull-request-lookup-data'
import { readCompletePRCheckObservation } from '../check/complete-pr-check-observation'
export async function hydratePullRequestLookupData(
ownerRepo: OwnerRepo,
data: PullRequestLookupData,
@@ -13,6 +14,10 @@ export async function hydratePullRequestLookupData(
executionScope: string
): Promise<PullRequestLookupData> {
const normalized = normalizePullRequestLookupData(data)
const checks =
data.checksUnavailable === false
? data.statusCheckRollup
: await readCompletePRCheckObservation(ownerRepo, data.number, data.headRefOid, ghOptions)
const hasRichMergeFields =
'reviewDecision' in data || 'mergeStateStatus' in data || 'autoMergeRequest' in data
const mergeMetadata =
@@ -26,6 +31,8 @@ export async function hydratePullRequestLookupData(
: undefined
return {
...normalized,
statusCheckRollup: checks ?? [],
checksUnavailable: checks === undefined,
...(mergeMetadata ? { mergeQueueRequired: mergeMetadata.mergeQueueRequired } : {}),
...(mergeMetadata ? { autoMergeAllowed: mergeMetadata.autoMergeAllowed } : {}),
...(mergeMetadata?.mergeMethodSettings
@@ -5,26 +5,16 @@ import { githubHostExecOptions, type GitHubApiRepository } from '../../github-ap
import { noteRepositoryRateLimitSpend } from '../../rate-limit'
import type { GhExecOptions } from '../github-exec-scope'
import type { PullRequestLookupData } from './pull-request-lookup-data'
import {
collectPRCheckRollup,
createPRCheckRollupPageReader,
prCheckRollupCommitsSchema,
prCheckRollupFields
} from '../check/pr-check-rollup'
const CHECKS_FIELDS = `commits(last: 1) {
nodes {
commit {
oid
statusCheckRollup {
contexts(first: 100, after: $checksCursor) {
nodes {
__typename
... on CheckRun { status conclusion }
... on StatusContext { state }
}
pageInfo { hasNextPage endCursor }
}
}
}
}
}`
const CHECKS_FIELDS = prCheckRollupFields()
const SIDEBAR_QUERY = `query($owner: String!, $repo: String!, $number: Int!, $checksCursor: String) {
const SIDEBAR_QUERY = `query($owner: String!, $repo: String!, $number: Int!, $checksCursor: String, $suitesCursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
number title state url updatedAt isDraft mergeable reviewDecision mergeStateStatus
@@ -39,31 +29,6 @@ const SIDEBAR_QUERY = `query($owner: String!, $repo: String!, $number: Int!, $ch
}
}`
const CHECKS_QUERY = `query($owner: String!, $repo: String!, $number: Int!, $checksCursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) { number ${CHECKS_FIELDS} }
}
}`
const commitsSchema = z.object({
nodes: z
.array(
z.object({
commit: z.object({
oid: z.string(),
statusCheckRollup: z
.object({
contexts: z.object({
nodes: z.array(z.record(z.string(), z.unknown()).nullable()),
pageInfo: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullable() })
})
})
.nullable()
})
})
)
.max(1)
})
const errorsSchema = z
.array(
z.object({
@@ -91,7 +56,7 @@ const pullRequestSchema = z.object({
isCrossRepository: z.boolean().optional(),
headRepositoryOwner: z.object({ login: z.string() }).nullable(),
headRepository: z.object({ name: z.string() }).nullable(),
commits: commitsSchema,
commits: z.unknown(),
stack: z.unknown().optional(),
stackEntry: z.unknown().optional()
})
@@ -109,14 +74,6 @@ const missingEnvelopeSchema = z.object({
data: z.object({ repository: z.object({ pullRequest: z.null() }) }),
errors: errorsSchema
})
const checksEnvelopeSchema = z.object({
data: z.object({
repository: z.object({
pullRequest: z.object({ number: z.number(), commits: commitsSchema })
})
}),
errors: errorsSchema
})
async function readQuery(
repository: GitHubApiRepository,
@@ -188,13 +145,16 @@ export async function readPullRequestSidebarQuery(
return undefined
}
const envelope = parsed.data
const stackErrors = envelope.errors ?? []
const errors = envelope.errors ?? []
const stackErrors = errors.filter(
({ path }) => path?.[2] === 'stack' || path?.[2] === 'stackEntry'
)
if (
stackErrors.some(
errors.some(
({ path }) =>
path?.[0] !== 'repository' ||
path[1] !== 'pullRequest' ||
(path[2] !== 'stack' && path[2] !== 'stackEntry')
(path[2] !== 'stack' && path[2] !== 'stackEntry' && path[2] !== 'commits')
)
) {
if (response.failure) {
@@ -206,37 +166,21 @@ export async function readPullRequestSidebarQuery(
if (pr.number !== number) {
return undefined
}
let contexts = pr.commits.nodes[0]?.commit.statusCheckRollup?.contexts
const statusCheckRollup = contexts?.nodes.flatMap((node) => (node ? [node] : [])) ?? []
const cursors = new Set<string>()
while (contexts?.pageInfo.hasNextPage) {
const cursor = contexts.pageInfo.endCursor
if (!cursor || cursors.has(cursor)) {
return undefined
let statusCheckRollup: unknown[] | undefined
try {
if (!pr.headRefOid || errors.some(({ path }) => path?.[2] === 'commits')) {
throw new Error('Missing PR head')
}
cursors.add(cursor)
const page = await readQuery(repository, number, options, CHECKS_QUERY, cursor)
if (page.failure) {
throw page.failure.error
}
const parsedPage = checksEnvelopeSchema.safeParse(page.payload)
if (!parsedPage.success) {
return undefined
}
const next = parsedPage.data
if (
next.errors?.length ||
next.data.repository.pullRequest.number !== number ||
next.data.repository.pullRequest.commits.nodes[0]?.commit.oid !==
pr.commits.nodes[0]?.commit.oid
) {
return undefined
}
contexts = next.data.repository.pullRequest.commits.nodes[0]?.commit.statusCheckRollup?.contexts
if (!contexts) {
return undefined
}
statusCheckRollup.push(...contexts.nodes.flatMap((node) => (node ? [node] : [])))
statusCheckRollup = await collectPRCheckRollup(
{
number: pr.number,
headRefOid: pr.headRefOid,
commits: prCheckRollupCommitsSchema.parse(pr.commits)
},
createPRCheckRollupPageReader(repository, number, options)
)
} catch {
// Metadata remains usable when complete checks need REST recovery.
}
const stack = stackSchema.safeParse(pr.stack)
const entry = stackEntrySchema.safeParse(pr.stackEntry)
@@ -248,8 +192,8 @@ export async function readPullRequestSidebarQuery(
...metadata,
baseRefOid: pr.baseRefOid ?? undefined,
headRefOid: pr.headRefOid ?? undefined,
statusCheckRollup,
checksUnavailable: false,
statusCheckRollup: statusCheckRollup ?? [],
checksUnavailable: statusCheckRollup === undefined,
stackMetadataChecked,
stack:
stackMetadataChecked && validStack
@@ -12,7 +12,7 @@ export const WORK_ITEM_PR_LIST_JSON_FIELDS =
// Keep first-paint reviewers in list payloads; defer costly checks, review decisions and merge metadata.
export const WORK_ITEM_PR_DETAIL_JSON_FIELDS =
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRefOid,headRepositoryOwner,additions,deletions,changedFiles,reviewDecision,reviewRequests,latestReviews,assignees,statusCheckRollup,mergeable,mergeStateStatus,autoMergeRequest,maintainerCanModify'
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRefOid,headRepositoryOwner,additions,deletions,changedFiles,reviewDecision,reviewRequests,latestReviews,assignees,mergeable,mergeStateStatus,autoMergeRequest,maintainerCanModify'
/** Preserve API-provided avatars with logins; dotcom login-based URLs do not identify Enterprise avatars. */
export function authorFieldsFromUnknown(
@@ -0,0 +1,181 @@
import { beforeEach, expect, it, vi } from 'vitest'
const { exec } = vi.hoisted(() => ({ exec: vi.fn() }))
vi.mock('../git/command-runner/gh-exec-file', async (original) => ({
...(await original<Record<string, unknown>>()),
ghExecFileAsync: exec
}))
vi.mock('./rate-limit', () => ({
noteRepositoryRateLimitSpend: vi.fn(),
repositoryRateLimitGuard: () => ({ blocked: false }),
spendsSharedGitHubComQuota: () => false
}))
import { readCompletePRCheckObservation } from './client/check/complete-pr-check-observation'
import { summarizePRCheckRollup } from '../../shared/source-control/checks/pr-check-status'
const repository = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const sha = 'a'.repeat(40)
const success = {
__typename: 'CheckRun',
status: 'COMPLETED',
conclusion: 'SUCCESS',
checkSuite: { databaseId: 7 }
}
const failure = { ...success, conclusion: 'FAILURE' }
const connection = (nodes: unknown[], cursor: string | null = null) => ({
nodes,
pageInfo: { hasNextPage: cursor !== null, endCursor: cursor }
})
function page(
contexts: unknown[],
suites: unknown[] = [],
checksCursor: string | null = null,
suitesCursor: string | null = null,
head = sha
) {
return {
stdout: JSON.stringify({
data: {
repository: {
pullRequest: {
number: 42,
headRefOid: head,
commits: {
nodes: [
{
commit: {
oid: head,
statusCheckRollup: { contexts: connection(contexts, checksCursor) },
checkSuites: connection(suites, suitesCursor)
}
}
]
}
}
}
}
})
}
}
const read = () => readCompletePRCheckObservation(repository, 42, sha, { cwd: '/repo' })
beforeEach(() => vi.resetAllMocks())
it('counts a hidden failure and suite-only blocker after both first pages without counting a represented suite twice', async () => {
exec
.mockResolvedValueOnce(
page(
Array.from({ length: 100 }, () => success),
[{ databaseId: 7, conclusion: 'ACTION_REQUIRED' }],
'checks-2',
'suites-2'
)
)
.mockResolvedValueOnce(page([failure]))
.mockResolvedValueOnce(page([], [{ databaseId: 8, conclusion: 'ACTION_REQUIRED' }]))
const rollup = await read()
expect(summarizePRCheckRollup(rollup!)).toEqual({
state: 'failure',
total: 102,
passed: 100,
failed: 2,
cancelled: 0,
pending: 0,
neutral: 0
})
})
it('counts unnamed and name-colliding failed observations instead of using detail-list filtering', async () => {
exec.mockResolvedValueOnce(
page([
success,
{ ...failure, name: '' },
{ __typename: 'StatusContext', context: 'same-name', state: 'FAILURE' }
])
)
expect(summarizePRCheckRollup((await read())!)).toMatchObject({
state: 'failure',
total: 3,
failed: 2
})
})
it.each(['failed-page', 'changed-head', 'broken-cursor'])(
'never certifies the successful prefix after %s',
async (kind) => {
exec.mockResolvedValueOnce(page([success], [], 'checks-2'))
if (kind === 'failed-page') {
exec.mockRejectedValueOnce(new Error('Unavailable'))
} else {
exec.mockResolvedValueOnce(
page(
[success],
[],
kind === 'broken-cursor' ? 'checks-2' : null,
null,
kind === 'changed-head' ? 'b'.repeat(40) : sha
)
)
}
exec.mockRejectedValue(new Error('REST unavailable'))
expect(await read()).toBeUndefined()
}
)
it('recovers every REST page and endpoint before publishing a full verdict', async () => {
exec.mockImplementation(async (args: string[]) => {
if (args.includes('graphql')) {
throw new Error('GraphQL unavailable')
}
const path = args.at(-1)!
expect(args).not.toContain('--cache')
if (path.includes('/check-runs?')) {
return {
stdout: JSON.stringify({
total_count: 101,
check_runs: path.endsWith('page=1')
? Array.from({ length: 100 }, () => ({
status: 'completed',
conclusion: 'success',
check_suite: { id: 7 }
}))
: [{ status: 'completed', conclusion: 'cancelled', check_suite: { id: 7 } }]
})
}
}
if (path.includes('/status?')) {
return {
stdout: JSON.stringify({ total_count: 1, statuses: [{ state: 'failure', context: '' }] })
}
}
return {
stdout: JSON.stringify({
total_count: 1,
check_suites: [{ id: 8, status: 'completed', conclusion: 'action_required' }]
})
}
})
expect(summarizePRCheckRollup((await read())!)).toEqual({
state: 'failure',
total: 103,
passed: 100,
failed: 3,
cancelled: 1,
pending: 0,
neutral: 0
})
})
it('distinguishes exhausted empty REST collections from a missing required subread', async () => {
let failStatuses = false
exec.mockImplementation(async (args: string[]) => {
if (args.includes('graphql') || (failStatuses && args.at(-1)!.includes('/status?'))) {
throw new Error('Unavailable')
}
return {
stdout: JSON.stringify({ total_count: 0, check_runs: [], statuses: [], check_suites: [] })
}
})
expect(await read()).toEqual([])
failStatuses = true
expect(await read()).toBeUndefined()
})
@@ -644,7 +644,7 @@ it.each(['error', 'absent'] as const)(
it('recovers visible merged review through Bob complete membership iteration after Alice error and reuses Bob confirmation', async () => {
request = (args, child, token) =>
queueMicrotask(() => {
const membership = args.find((arg) => arg.includes('/commits/'))
const membership = args.find((arg) => arg.includes('/commits/') && arg.includes('/pulls?'))
if (membership && token === 'Alice-token') {
child.stderr?.emit('data', Buffer.from('request failed'))
child.emit('exit', 1, null)
@@ -697,8 +697,11 @@ it('recovers visible merged review through Bob complete membership iteration aft
})
expect(
requests
.filter((r) => r.args.some((arg) => arg.includes('/commits/')))
.map((r) => ({ endpoint: r.args.find((arg) => arg.includes('/commits/')), token: r.token }))
.filter((r) => r.args.some((arg) => arg.includes('/commits/') && arg.includes('/pulls?')))
.map((r) => ({
endpoint: r.args.find((arg) => arg.includes('/commits/') && arg.includes('/pulls?')),
token: r.token
}))
).toEqual([
{
endpoint: 'repos/acme/widgets/commits/bbbb2222bbbb2222/pulls?per_page=100&page=1',
@@ -12,7 +12,8 @@ vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
}))
vi.mock('./rate-limit', () => ({
noteRepositoryRateLimitSpend: spend,
repositoryRateLimitGuard: () => ({ blocked: false })
repositoryRateLimitGuard: () => ({ blocked: false }),
spendsSharedGitHubComQuota: () => false
}))
import { getPRByNumber, lookupPRByNumber } from './client/lookup/pr-number-lookup'
@@ -33,9 +34,17 @@ function commits(checks: unknown[] = [successCheck], endCursor?: string, oid = '
{
commit: {
oid,
checkSuites: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } },
statusCheckRollup: {
contexts: {
nodes: checks,
nodes: checks.map((raw) =>
raw &&
typeof raw === 'object' &&
'__typename' in raw &&
raw.__typename === 'CheckRun'
? { ...raw, checkSuite: { databaseId: 7 } }
: raw
),
pageInfo: { hasNextPage: Boolean(endCursor), endCursor: endCursor ?? null }
}
}
@@ -136,9 +145,7 @@ describe('sidebar PR query', () => {
).resolves.toMatchObject({
number: 42
})
expect(exec).toHaveBeenCalledTimes(2)
expect(exec.mock.calls[1][0]).toContain('repos/acme/widgets/pulls/42')
expect(spend.mock.calls.filter(([, bucket]) => bucket === 'graphql')).toHaveLength(1)
}
)
@@ -166,18 +173,18 @@ describe('sidebar PR query', () => {
}
)
it('uses REST recovery directly if a continuation request fails', async () => {
it('retains PR metadata but never certifies a successful prefix after continuation failure', async () => {
exec
.mockResolvedValueOnce(response({ ...fields, commits: commits([successCheck], 'next') }))
.mockRejectedValueOnce(new Error('ETIMEDOUT'))
.mockResolvedValueOnce(restResponse())
.mockRejectedValue(new Error('Unavailable'))
await expect(
getPRByNumber(repository, 42, options, 'local:host', undefined, { includeStackSummary: true })
).resolves.toMatchObject({
number: 42
number: 42,
title: fields.title,
checksUnavailable: true,
statusCheckRollup: []
})
expect(exec).toHaveBeenCalledTimes(3)
expect(exec.mock.calls[2][0]).toContain('repos/acme/widgets/pulls/42')
})
it('keeps capability hydration on the PR base before attaching its stack summary', async () => {
@@ -300,7 +307,9 @@ describe('sidebar PR query', () => {
)
})
)
.mockResolvedValueOnce(response({ number: 42, commits: commits([null, failingCheck]) }))
.mockResolvedValueOnce(
response({ number: 42, headRefOid: fields.headRefOid, commits: commits([failingCheck]) })
)
const data = await getPRByNumber(repository, 42, options, 'local:host', undefined, {
includeStackSummary: true
})
@@ -312,26 +321,29 @@ describe('sidebar PR query', () => {
})
it.each([
{ number: 42, commits: commits([], 'next') },
{ number: 42, commits: commits([], undefined, 'c'.repeat(40)) }
{ number: 42, headRefOid: fields.headRefOid, commits: commits([], 'next') },
{ number: 42, headRefOid: fields.headRefOid, commits: commits([], undefined, 'c'.repeat(40)) }
])(
'recovers through the rich CLI if checks pagination stops advancing or the commit changes',
'keeps metadata without a certified summary when checks pagination stops advancing or the commit changes',
async (nextPage) => {
exec
.mockResolvedValueOnce(response({ ...fields, commits: commits([successCheck], 'next') }))
.mockResolvedValueOnce(response(nextPage))
.mockResolvedValueOnce(legacyResponse())
.mockRejectedValue(new Error('Unavailable'))
const data = await getPRByNumber(repository, 42, options, 'local:host', undefined, {
includeStackSummary: true
})
expect(deriveCheckStatus(data?.statusCheckRollup)).toBe('success')
expect(exec).toHaveBeenCalledTimes(3)
expect(exec.mock.calls[2][0].slice(0, 2)).toEqual(['pr', 'view'])
expect(data).toMatchObject({
number: 42,
title: fields.title,
checksUnavailable: true,
statusCheckRollup: []
})
}
)
it('preserves known branch discovery metadata without adding another query', async () => {
exec.mockResolvedValueOnce(legacyResponse())
it('preserves known branch discovery metadata while reusing the combined exact check page', async () => {
exec.mockResolvedValueOnce(response())
const known = {
...fields,
statusCheckRollup: [],
@@ -341,8 +353,7 @@ describe('sidebar PR query', () => {
}
const data = await getPRByNumber(repository, 42, options, 'local:host', known)
expect(data).toMatchObject({ headDefaultBranchName: 'trunk', stackMetadataChecked: true })
expect(exec).toHaveBeenCalledTimes(1)
expect(exec.mock.calls[0][0].slice(0, 2)).toEqual(['pr', 'view'])
expect(exec.mock.calls[0][0].slice(0, 2)).toEqual(['api', 'graphql'])
})
it.each([
@@ -370,7 +381,6 @@ describe('sidebar PR query', () => {
usedExactNumberLookup: true
})
expect(data.stackMetadataChecked).toBe(true)
expect(exec).toHaveBeenCalledTimes(2)
expect(exec.mock.calls[1][0]).toContain('repos/acme/widgets/pulls/42')
}
)
@@ -407,12 +417,11 @@ describe('sidebar PR query', () => {
).resolves.toMatchObject({
title: 'CLI fallback'
})
expect(exec).toHaveBeenCalledTimes(2)
expect(exec.mock.calls[1][0].slice(0, 2)).toEqual(['pr', 'view'])
})
it.each(['github.corp.example', undefined])(
'keeps the existing read for host %s',
'uses host-scoped metadata and the complete collector for host %s',
async (host) => {
exec.mockResolvedValueOnce({ stdout: JSON.stringify({ ...fields, statusCheckRollup: [] }) })
await expect(
@@ -420,7 +429,6 @@ describe('sidebar PR query', () => {
includeStackSummary: true
})
).resolves.toMatchObject({ number: 42 })
expect(exec).toHaveBeenCalledTimes(1)
expect(exec.mock.calls[0][0].slice(0, 2)).toEqual(['pr', 'view'])
}
)
@@ -450,3 +458,113 @@ describe('sidebar PR query', () => {
expect(exec.mock.calls[0][1]).toEqual({ ...options, ...execution, host: 'github.com' })
})
})
it.each([
{ checks: [null], errors: undefined },
{
checks: [{ __typename: 'CheckRun', status: 'FUTURE_STATUS', conclusion: null }],
errors: undefined
},
{
checks: [successCheck],
errors: [
{ path: ['repository', 'pullRequest', 'commits', 'nodes', 0, 'commit', 'statusCheckRollup'] }
]
}
])(
'keeps valid metadata and stack when strict checks are unavailable: %j',
async ({ checks, errors }) => {
exec.mockResolvedValueOnce(
response(
{
...fields,
commits: commits(checks),
stack: { number: 7, size: 2, baseRefName: 'main' },
stackEntry: { position: 1 }
},
errors
)
)
await expect(readPullRequestSidebarQuery(repository, 42, options)).resolves.toMatchObject({
title: fields.title,
checksUnavailable: true,
stackMetadataChecked: true,
stack: { number: 7, size: 2, position: 1 }
})
}
)
it.each([1, 10])(
'reuses combined exact hydration with independent context and suite populations at N=%i',
async (n) => {
const contextPages = 2 * n
const suitePages = 3 * n
exec.mockImplementation(async (args: string[]) => {
const contextCursor = args.find((arg) => arg.startsWith('checksCursor='))
const suiteCursor = args.find((arg) => arg.startsWith('suitesCursor='))
const contextIndex = contextCursor ? Number(contextCursor.split('=')[1]) : 0
const suiteIndex = suiteCursor ? Number(suiteCursor.split('=')[1]) : 0
const lastContext = contextIndex === contextPages - 1
const commit = {
oid: fields.headRefOid,
...(!suiteCursor
? {
statusCheckRollup: {
contexts: {
nodes: [
lastContext ? { __typename: 'StatusContext', state: 'FAILURE' } : successCheck
],
pageInfo: {
hasNextPage: !lastContext,
endCursor: lastContext ? null : String(contextIndex + 1)
}
}
}
}
: {}),
...(!contextCursor
? {
checkSuites: {
nodes: [{ databaseId: suiteIndex + 1, conclusion: 'SUCCESS' }],
pageInfo: {
hasNextPage: suiteIndex < suitePages - 1,
endCursor: suiteIndex < suitePages - 1 ? String(suiteIndex + 1) : null
}
}
}
: {})
}
return response({ ...fields, commits: { nodes: [{ commit }] } })
})
const known = {
...fields,
stack: undefined,
statusCheckRollup: [],
headDefaultBranchName: 'trunk'
}
const observed = await getPRByNumber(repository, 42, options, 'local:host', known)
expect(observed).toMatchObject({ headDefaultBranchName: 'trunk', checksUnavailable: false })
expect(observed?.statusCheckRollup).toHaveLength(contextPages)
expect(observed?.statusCheckRollup?.at(-1)).toEqual({
__typename: 'StatusContext',
state: 'FAILURE'
})
expect(exec).toHaveBeenCalledTimes(1 + (contextPages - 1) + (suitePages - 1))
expect(exec.mock.calls.every(([args]) => args[0] === 'api' && args[1] === 'graphql')).toBe(true)
}
)
it('retains the rich exact metadata path when the optional combined read fails', async () => {
exec
.mockRejectedValueOnce(new Error('unsupported optional query'))
.mockResolvedValueOnce(legacyResponse())
await expect(
getPRByNumber(repository, 42, options, 'local:host', {
...fields,
stack: undefined,
statusCheckRollup: [],
headDefaultBranchName: 'trunk'
})
).resolves.toMatchObject({ reviewDecision: null, headDefaultBranchName: 'trunk' })
expect(exec.mock.calls[1][0].slice(0, 2)).toEqual(['pr', 'view'])
})
@@ -1,3 +1,4 @@
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/github/check-types'
export type CheckDetailsLoadState = {
@@ -16,6 +17,7 @@ export type GitHubChecksTabState = {
contextKey: string
contextOwner: GitHubChecksContextOwner
sourceChecks: GitHubChecksSource
checkObservationOwner?: GitHubWorkItem
localChecks: PRCheckDetail[] | null
expandedCheckKey: string | null
detailsByCheckKey: Record<string, CheckDetailsLoadState>
@@ -45,9 +47,13 @@ export function resolveGitHubChecksTabState(
if (state.contextKey !== contextKey) {
return createGitHubChecksTabState(sourceChecks, contextKey)
}
return state.sourceChecks === sourceChecks
? state
: resetGitHubChecksTabForSource(state, sourceChecks)
if (state.sourceChecks === sourceChecks) {
return state
}
const reset = resetGitHubChecksTabForSource(state, sourceChecks)
return sourceChecks === state.localChecks
? { ...reset, checkObservationOwner: state.checkObservationOwner }
: reset
}
export function resetGitHubChecksTabForSource(
@@ -66,11 +72,13 @@ export function resetGitHubChecksTabForSource(
export function updateGitHubChecksTabLocalChecks(
state: GitHubChecksTabState,
localChecks: PRCheckDetail[]
localChecks: PRCheckDetail[],
checkObservationOwner?: GitHubWorkItem
): GitHubChecksTabState {
return {
...state,
localChecks
localChecks,
checkObservationOwner
}
}
@@ -1,3 +1,4 @@
import { readPRCheckRefreshOwner } from '@/lib/pr-check-refresh-owner'
import type React from 'react'
import { toast } from 'sonner'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
@@ -25,6 +26,8 @@ export type ChecksTabActionContext = {
sourceContext?: TaskSourceContext | null
repoId: string | null
repoPath: string | null
item: GitHubWorkItem
onOwnerUpdated?: (item: GitHubWorkItem) => void
itemNumber: number
itemRepoId: string
headSha: string | undefined
@@ -63,6 +66,23 @@ export async function refreshGitHubChecksTab(
ctx.activeChecksRefreshRequestIdRef.current = refreshRequestId
ctx.setRefreshingOwner({ contextOwner: refreshContextOwner, requestId: refreshRequestId })
try {
const observation = await readPRCheckRefreshOwner({
repoPath: ctx.repoPath,
repoId: ctx.repoId ?? ctx.itemRepoId,
sourceContext: ctx.sourceContext,
ownerRepo: ctx.prRepo,
item: ctx.item,
accepts: () =>
ctx.mountedRef.current &&
ctx.committedChecksContextOwnerRef.current === refreshContextOwner &&
ctx.activeChecksRefreshRequestIdRef.current === refreshRequestId
})
if (!observation.current()) {
return null
}
if (observation.owner) {
ctx.onOwnerUpdated?.(observation.owner)
}
const nextChecks = await (ctx.runtimeHost
? callRuntimeRpc<PRCheckDetail[]>(
{ kind: 'environment', environmentId: ctx.runtimeHost.environmentId },
@@ -70,24 +90,25 @@ export async function refreshGitHubChecksTab(
{
repo: getGitHubRuntimeRepoId(ctx.sourceContext, ctx.repoId ?? ctx.itemRepoId),
prNumber: ctx.itemNumber,
headSha: ctx.headSha,
headSha: observation.owner?.headSha ?? ctx.headSha,
prRepo: ctx.prRepo,
noCache: true
},
{ timeoutMs: 30_000 }
{ timeoutMs: 30_000, ...observation.rpcOptions }
)
: window.api.gh.prChecks({
repoPath: ctx.repoPath ?? '',
repoId: ctx.repoId ?? undefined,
sourceContext: ctx.sourceContext,
prNumber: ctx.itemNumber,
headSha: ctx.headSha,
headSha: observation.owner?.headSha ?? ctx.headSha,
prRepo: ctx.prRepo,
noCache: true,
// Refresh and rerun are user acts: they may start a stopped WSL distro.
wakeWslDistro: true
}))
if (
!observation.current() ||
!ctx.mountedRef.current ||
ctx.committedChecksContextOwnerRef.current !== refreshContextOwner ||
ctx.activeChecksRefreshRequestIdRef.current !== refreshRequestId
@@ -96,7 +117,11 @@ export async function refreshGitHubChecksTab(
}
ctx.setChecksState((current) =>
current.contextOwner === refreshContextOwner
? updateGitHubChecksTabLocalChecks(resetGitHubChecksTabForSource(current), nextChecks)
? updateGitHubChecksTabLocalChecks(
resetGitHubChecksTabForSource(current),
nextChecks,
observation.owner
)
: current
)
ctx.onChecksUpdated(nextChecks)
@@ -151,6 +151,7 @@ export function ChecksTabCompactHeader({
counts,
summaryLabel,
listLength,
showSummary = false,
refreshAction,
rerunAction,
secondaryActions
@@ -159,6 +160,7 @@ export function ChecksTabCompactHeader({
summaryColor: string
counts: { failing: number; pending: number }
summaryLabel: string
showSummary?: boolean
listLength: number
refreshAction: React.JSX.Element
rerunAction: React.JSX.Element | null
@@ -179,7 +181,7 @@ export function ChecksTabCompactHeader({
<div className="text-[13px] font-medium leading-5 text-foreground">
{translate('auto.components.GitHubItemDialog.4bd1f5b055', 'Checks')}
</div>
{listLength > 0 && (
{(listLength > 0 || showSummary) && (
<div className="truncate text-[11px] leading-4 text-muted-foreground">
{summaryLabel}
</div>
@@ -1,5 +1,8 @@
import { PRCheckEmptyDetails } from '@/components/pr-check-empty-details'
import { isCompletePRCheckOwner } from '../../../../../shared/github/complete-pr-check-owner'
import { usePRCheckRefreshOwner } from '@/hooks/use-pr-check-refresh-owner'
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { CircleDashed, LoaderCircle } from 'lucide-react'
import { LoaderCircle } from 'lucide-react'
import { useMountedRef } from '@/hooks/useMountedRef'
import { cn } from '@/lib/utils'
import { CHECK_COLOR } from '@/components/right-sidebar/checks-panel/presentation/check-presentation'
@@ -21,11 +24,11 @@ import type {
} from '../../../../../shared/github/work-item-types'
import type { TaskSourceContext } from '../../../../../shared/task-source-context'
import { getTaskSourceCacheScope } from '../../../../../shared/task-source-context'
import { translate } from '@/i18n/i18n'
import { sortChecksBySeverity } from '../../../../../shared/source-control/checks/pr-check-severity-order'
import {
getCheckCountChips,
getCheckCounts,
getOverallCheckCounts,
getChecksSummaryLabel
} from '@/components/pr-check-counts'
import { resolvePullRequestRepo } from '@/components/github/github-work-item-identity'
@@ -64,6 +67,7 @@ export function ChecksTab({
variant?: 'compact' | 'page'
onChecksUpdated: (checks: PRCheckDetail[]) => void
}): React.JSX.Element {
const { checkOwner, onOwnerUpdated } = usePRCheckRefreshOwner(item, sourceContext)
const [fixingChecks, setFixingChecks] = useState(false)
const mountedRef = useMountedRef()
const prRepo = useMemo(() => resolvePullRequestRepo(item), [item])
@@ -109,8 +113,17 @@ export function ChecksTab({
const sorted = sortChecksBySeverity(list)
const failedChecks = getBrokenChecks(list)
const counts = getCheckCounts(list)
const summaryLabel = getChecksSummaryLabel(list)
const { SummaryIcon, summaryColor } = getChecksTabSummaryPresentation(counts)
const overallCounts = getOverallCheckCounts(
list,
checkOwner,
resolvedChecksState.checkObservationOwner
)
const summaryLabel = getChecksSummaryLabel(
list,
checkOwner,
resolvedChecksState.checkObservationOwner
)
const { SummaryIcon, summaryColor } = getChecksTabSummaryPresentation(overallCounts)
const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0)
const handleRefresh = useCallback(
@@ -122,9 +135,11 @@ export function ChecksTab({
sourceContext,
repoId,
repoPath,
item: checkOwner,
onOwnerUpdated,
itemNumber: item.number,
itemRepoId: item.repoId,
headSha,
headSha: checkOwner.headSha,
prRepo,
mountedRef,
committedChecksContextOwnerRef,
@@ -140,11 +155,11 @@ export function ChecksTab({
),
[
canUseChecksRepoContext,
headSha,
item.number,
item.repoId,
item,
checkOwner,
mountedRef,
onChecksUpdated,
onOwnerUpdated,
runtimeHost,
prRepo,
repoId,
@@ -162,9 +177,11 @@ export function ChecksTab({
sourceContext,
repoId,
repoPath,
itemNumber: item.number,
itemRepoId: item.repoId,
headSha,
item: checkOwner,
onOwnerUpdated,
itemNumber: checkOwner.number,
itemRepoId: checkOwner.repoId,
headSha: checkOwner.headSha,
prRepo,
mountedRef,
committedChecksContextOwnerRef,
@@ -181,11 +198,10 @@ export function ChecksTab({
),
[
canUseChecksRepoContext,
headSha,
item.number,
item.repoId,
checkOwner,
mountedRef,
onChecksUpdated,
onOwnerUpdated,
prRepo,
runtimeHost,
rerunning,
@@ -215,6 +231,8 @@ export function ChecksTab({
sourceContext,
repoId,
repoPath,
item,
onOwnerUpdated,
itemNumber: item.number,
itemRepoId: item.repoId,
headSha,
@@ -236,10 +254,10 @@ export function ChecksTab({
[
canUseChecksRepoContext,
headSha,
item.number,
item.repoId,
item,
mountedRef,
onChecksUpdated,
onOwnerUpdated,
runtimeHost,
prRepo,
repoId,
@@ -283,7 +301,8 @@ export function ChecksTab({
<ChecksTabCompactHeader
SummaryIcon={SummaryIcon}
summaryColor={summaryColor}
counts={counts}
counts={overallCounts}
showSummary={isCompletePRCheckOwner(checkOwner)}
summaryLabel={summaryLabel}
listLength={list.length}
refreshAction={refreshAction}
@@ -303,37 +322,17 @@ export function ChecksTab({
)
}
if (list.length === 0) {
if (variant === 'page') {
return (
<div className="flex flex-col gap-3 px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<CircleDashed className="size-4 shrink-0 text-muted-foreground" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-[13px] font-medium text-foreground">
{translate('auto.components.GitHubItemDialog.ecffebc251', 'No checks found')}
</span>
<span className="truncate text-[11px] text-muted-foreground">
{translate(
'auto.components.GitHubItemDialog.90020cc1f3',
'This pull request has no reported checks yet.'
)}
</span>
</div>
{actions}
</div>
</div>
)
}
return (
<>
{compactHeader}
<div className="flex flex-col items-center justify-center gap-1 px-4 py-6 text-center">
<CircleDashed className="size-4 text-muted-foreground/60" />
<div className="text-[12px] text-muted-foreground">
{translate('auto.components.GitHubItemDialog.e52bed9264', 'No checks reported yet')}
</div>
</div>
</>
<PRCheckEmptyDetails
dialog
owner={checkOwner}
variant={variant}
compactHeader={compactHeader}
actions={actions}
SummaryIcon={SummaryIcon}
summaryColor={summaryColor}
summaryLabel={summaryLabel}
/>
)
}
if (variant === 'page') {
@@ -345,7 +344,7 @@ export function ChecksTab({
className={cn(
'size-4 shrink-0',
summaryColor,
counts.pending > 0 && counts.failing === 0 && 'animate-spin'
overallCounts.pending > 0 && overallCounts.failing === 0 && 'animate-spin'
)}
/>
<div className="flex min-w-0 flex-1 items-center gap-2">
@@ -1,3 +1,4 @@
import { projectPRCheckOwner } from '../../../../../shared/github/complete-pr-check-owner'
import { useAppStore } from '@/store'
import { useGitHubItemTargetContext } from './github-work-item-target-context'
import {
@@ -102,7 +103,14 @@ export function useGitHubWorkItemDetails({
// The opened row and the loaded item both show the current record's facts; main's item lacks the row's repo.
const taskItems = useMemo(
() =>
workItem ? [workItem, ...(rawDetails ? [{ ...workItem, ...rawDetails.item }] : [])] : [],
workItem
? [
workItem,
...(rawDetails
? [projectPRCheckOwner(rawDetails.item, { ...workItem, ...rawDetails.item })]
: [])
]
: [],
[workItem, rawDetails]
)
const projectedItems = useGitHubTaskItems(taskItems)
@@ -194,7 +202,7 @@ export function useGitHubWorkItemDetails({
if (!workItem || !details?.item) {
return projectedWorkItem
}
return {
return projectPRCheckOwner(details.item, {
...projectedWorkItem,
...details.item,
repoId: workItem.repoId,
@@ -202,7 +210,7 @@ export function useGitHubWorkItemDetails({
number: workItem.number,
url: workItem.url,
...(resource?.type === 'pr' ? { prRepo: resource.repository } : {})
}
})
}, [details?.item, workItem, projectedWorkItem, resource])
useEffect(() => {
@@ -1,3 +1,5 @@
import type * as LocalRuntimeCapabilities from '@/runtime/local-runtime-capabilities'
import { completePRCheckGraphQLResponse } from '../../../../../shared/github/complete-pr-check-test-fixture'
import type * as GitRunner from '../../../../../main/git/command-runner/gh-exec-file'
import type { GitHubRepoContext } from '../../../../../main/github/github-repository-identity'
import type { PRComment } from '../../../../../shared/github/comment-types'
@@ -143,7 +145,8 @@ vi.mock('@/runtime/runtime-rpc-client', () => ({
callRuntimeRpc: io.rpc,
runtimeEnvironmentSupportsCapability: async () => io.capable
}))
vi.mock('@/runtime/local-runtime-capabilities', () => ({
vi.mock('@/runtime/local-runtime-capabilities', async (importOriginal) => ({
...(await importOriginal<typeof LocalRuntimeCapabilities>()),
ensureLocalRuntimeCapabilities: async () => ['github.exact-work-item-target.v1']
}))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
@@ -311,34 +314,15 @@ beforeEach(() => {
})
}
if (query.includes('statusCheckRollup')) {
return response({
data: {
repository: {
pullRequest: {
commits: {
nodes: [
{
commit: {
statusCheckRollup: {
contexts: {
nodes: [
{
__typename: 'CheckRun',
name: `${owner} check`,
status: 'COMPLETED',
conclusion: 'SUCCESS'
}
]
}
}
}
}
]
}
}
}
return completePRCheckGraphQLResponse(93, `${owner}-head`, [
{
__typename: 'CheckRun',
name: `${owner} check`,
status: 'COMPLETED',
conclusion: 'SUCCESS',
checkSuite: { databaseId: 7 }
}
})
])
}
if (args.some((a) => a.includes('/files?'))) {
return response([
+59 -5
View File
@@ -1,3 +1,9 @@
import { normalizePRCheckHeadSha } from '../../../shared/github/pr-check-head'
import {
isCompletePRCheckOwner,
samePRCheckObservation
} from '../../../shared/github/complete-pr-check-owner'
import type { ProviderCheckSummary } from '../../../shared/github/pull-request-types'
import { translate } from '../i18n/i18n'
import { summarizeProviderChecks } from '../../../shared/source-control/checks/provider-check-summary'
import type { PRCheckDetail } from '../../../shared/github/check-types'
@@ -35,6 +41,45 @@ export function getCheckCounts(checks: readonly PRCheckDetail[]): PRCheckCounts
}
}
export function getOverallCheckCounts(
checks: readonly PRCheckDetail[],
owner?: { headSha?: string; checksSummary?: ProviderCheckSummary },
rowsOwner?: { checksSummary?: ProviderCheckSummary }
): PRCheckCounts {
if (!owner || !isCompletePRCheckOwner(owner)) {
return getCheckCounts(checks)
}
const summary = owner.checksSummary
const failed = (summary?.failed ?? 0) - (summary?.cancelled ?? 0)
const rows = getCheckCounts(checks)
const distinctApprovals = new Set(
checks
.filter((check) => getCheckConclusion(check) === 'action_required')
.map((check) => check.checkRunId ?? check.workflowRunId ?? check.url ?? check.name)
).size
const explained =
rowsOwner &&
samePRCheckObservation(owner, rowsOwner) &&
checks.length > 0 &&
checks.every(
(check) =>
normalizePRCheckHeadSha(check.observedHeadSha) !== undefined &&
normalizePRCheckHeadSha(check.observedHeadSha) === normalizePRCheckHeadSha(owner.headSha)
) &&
failed > 0 &&
distinctApprovals === failed &&
rows.needsAction === distinctApprovals &&
rows.failing === 0
return {
passing: summary?.passed ?? 0,
failing: explained ? 0 : failed,
cancelled: summary?.cancelled ?? 0,
needsAction: explained ? failed : 0,
pending: summary?.pending ?? 0,
neutral: summary?.neutral ?? 0
}
}
/** `tone` keys the caller's `CHECK_COLOR`/`CHECK_ICON` maps so styling stays with the surface. */
export type PRCheckCountChip = {
tone: 'success' | 'failure' | 'cancelled' | 'action_required' | 'pending' | 'neutral'
@@ -95,9 +140,18 @@ export function getCheckCountChips(counts: PRCheckCounts): PRCheckCountChip[] {
return chips
}
export function getChecksSummaryLabel(checks: readonly PRCheckDetail[]): string {
const counts = getCheckCounts(checks)
if (checks.length === 0) {
export function getChecksSummaryLabel(
checks: readonly PRCheckDetail[],
owner?: { headSha?: string; checksSummary?: ProviderCheckSummary },
rowsOwner?: { checksSummary?: ProviderCheckSummary }
): string {
const complete = owner && isCompletePRCheckOwner(owner)
if (complete && !owner.checksSummary) {
return translate('auto.components.right.sidebar.ChecksPanel.976cefd02f', 'Checks unavailable')
}
const total = complete ? owner.checksSummary!.total : checks.length
const counts = getOverallCheckCounts(checks, owner, rowsOwner)
if (total === 0) {
return translate('auto.components.prCheckCounts.noChecksFound', 'No checks found')
}
if (counts.failing > 0) {
@@ -123,14 +177,14 @@ export function getChecksSummaryLabel(checks: readonly PRCheckDetail[]): string
if (counts.cancelled > 0) {
return translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
}
if (counts.passing === checks.length) {
if (counts.passing === total) {
return translate('auto.components.prCheckCounts.allChecksPassing', 'All checks passing')
}
return translate(
'auto.components.prCheckCounts.checksPassingOfTotal',
'{{passing}} of {{count}} checks passing',
{
count: checks.length,
count: total,
passing: counts.passing,
defaultValue_one: '{{passing}} of {{count}} check passing'
}
@@ -0,0 +1,69 @@
import { defineFixture } from '../component-fixtures/component-fixture'
import { PRCheckEmptyDetails } from './pr-check-empty-details'
import { ChecksCompactHeader } from './pull-request-page/checks/toolbar'
import { ChecksTabCompactHeader } from './github-item-dialog/inspect-pull-request/checks-tab-header'
import { getChecksTabSummaryPresentation } from './github-item-dialog/inspect-pull-request/checks-tab-summary'
import { getOverallCheckCounts, getChecksSummaryLabel } from './pr-check-counts'
import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner'
import { Button } from './ui/button'
const owner = acceptPRCheckObservation(
{
number: 42,
url: 'https://github.com/acme/widgets/pull/42',
headSha: 'h1',
mergeable: 'UNKNOWN' as const
},
null,
{ scope: 'fixture', complete: true, current: true }
)!
function EmptyChecks({
variant,
dialog = false
}: {
variant: 'page' | 'compact'
dialog?: boolean
}) {
const counts = getOverallCheckCounts([], owner)
const presentation = getChecksTabSummaryPresentation(counts)
const summaryLabel = getChecksSummaryLabel([], owner)
const Header = dialog ? ChecksTabCompactHeader : ChecksCompactHeader
const action = (
<Button variant="outline" size="xs">
Refresh
</Button>
)
return (
<div className="bg-background text-foreground">
<PRCheckEmptyDetails
dialog={dialog}
owner={owner}
variant={variant}
{...presentation}
summaryLabel={summaryLabel}
actions={action}
compactHeader={
<Header
{...presentation}
counts={counts}
showSummary
summaryLabel={summaryLabel}
listLength={0}
refreshAction={action}
rerunAction={null}
secondaryActions={null}
/>
}
/>
</div>
)
}
export const page = defineFixture({ width: 720, render: () => <EmptyChecks variant="page" /> })
export const compact = defineFixture({
width: 305,
render: () => <EmptyChecks variant="compact" />
})
export const dialog = defineFixture({
width: 420,
render: () => <EmptyChecks variant="compact" dialog />
})
@@ -0,0 +1,75 @@
import type React from 'react'
import { CircleDashed } from 'lucide-react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { isCompletePRCheckOwner } from '../../../shared/github/complete-pr-check-owner'
import type { ProviderCheckSummary } from '../../../shared/github/pull-request-types'
export function PRCheckEmptyDetails({
owner,
dialog = false,
variant,
compactHeader,
actions,
SummaryIcon,
summaryColor,
summaryLabel
}: {
dialog?: boolean
owner?: { checksSummary?: ProviderCheckSummary }
variant: 'page' | 'compact'
compactHeader: React.JSX.Element
actions: React.JSX.Element
SummaryIcon: React.ComponentType<{ className?: string }>
summaryColor: string
summaryLabel: string
}): React.JSX.Element {
const complete = owner && isCompletePRCheckOwner(owner)
const noChecks = !complete || owner.checksSummary?.total === 0
const detailLabel = noChecks
? dialog
? translate('auto.components.GitHubItemDialog.e52bed9264', 'No checks reported yet')
: translate('auto.components.PullRequestPage.a18d01cda3', 'No checks reported yet')
: translate('auto.components.prCheckCounts.noCheckDetails', 'No check details available')
if (variant === 'compact') {
return (
<>
{compactHeader}
<div className="flex flex-col items-center justify-center gap-1 px-4 py-6 text-center">
<CircleDashed className="size-4 text-muted-foreground/60" />
<div className="text-[12px] text-muted-foreground">{detailLabel}</div>
</div>
</>
)
}
return (
<div className="flex flex-col gap-3 px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<SummaryIcon className={cn('size-4 shrink-0', summaryColor)} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-[13px] font-medium text-foreground">
{noChecks
? dialog
? translate('auto.components.GitHubItemDialog.ecffebc251', 'No checks found')
: translate('auto.components.PullRequestPage.45877f5089', 'No checks found')
: summaryLabel}
</span>
<span className="truncate text-[11px] text-muted-foreground">
{noChecks
? dialog
? translate(
'auto.components.GitHubItemDialog.90020cc1f3',
'This pull request has no reported checks yet.'
)
: translate(
'auto.components.PullRequestPage.3912daf310',
'This pull request has no reported checks yet.'
)
: detailLabel}
</span>
</div>
{actions}
</div>
</div>
)
}
@@ -0,0 +1,205 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { CircleDashed } from 'lucide-react'
import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner'
import { getCheckCounts, getOverallCheckCounts, getChecksSummaryLabel } from './pr-check-counts'
import { ChecksTabLayouts } from './pull-request-page/checks/layouts'
import { ChecksCompactHeader } from './pull-request-page/checks/toolbar'
import { ChecksTabCompactHeader } from './github-item-dialog/inspect-pull-request/checks-tab-header'
import { getChecksTabSummaryPresentation } from './github-item-dialog/inspect-pull-request/checks-tab-summary'
import { PRTriageStrip } from './right-sidebar/checks-panel/presentation/triage-strip'
import { i18n } from '@/i18n/i18n'
import type { PRCheckDetail } from '../../../shared/github/check-types'
const owner = {
number: 42,
url: 'https://github.com/acme/widgets/pull/42',
headSha: 'a'.repeat(40),
mergeable: 'UNKNOWN' as const
}
const context = { scope: 'account:runtime', complete: true, current: true }
const blocked = {
state: 'failure' as const,
total: 101,
passed: 100,
failed: 1,
pending: 0,
neutral: 0
}
const rows: PRCheckDetail[] = [
{
name: 'Approval',
status: 'completed',
conclusion: 'action_required',
url: null,
observedHeadSha: 'a'.repeat(40)
}
]
afterEach(async () => {
cleanup()
await i18n.changeLanguage('en')
})
it.each([ChecksCompactHeader, ChecksTabCompactHeader])(
'keeps row approval chips while only current associated rows explain an owner failure',
(Header) => {
const accepted = acceptPRCheckObservation({ ...owner, checksSummary: blocked }, null, context)!
const counts = getCheckCounts(rows)
expect(counts).toMatchObject({ passing: 0, failing: 0, needsAction: 1 })
const overall = getOverallCheckCounts(rows, accepted, accepted)
render(
<Header
{...getChecksTabSummaryPresentation(overall)}
counts={overall}
summaryLabel={getChecksSummaryLabel(rows, accepted, accepted)}
listLength={1}
refreshAction={<button>Refresh</button>}
rerunAction={null}
secondaryActions={null}
/>
)
expect(screen.getByText('1 check needs action')).toBeTruthy()
cleanup()
const newer = acceptPRCheckObservation(
{ ...owner, checksSummary: { ...blocked } },
accepted,
context
)!
const nextCounts = getOverallCheckCounts(rows, newer, accepted)
render(
<Header
{...getChecksTabSummaryPresentation(nextCounts)}
counts={nextCounts}
summaryLabel={getChecksSummaryLabel(rows, newer, accepted)}
listLength={1}
refreshAction={<button>Refresh</button>}
rerunAction={null}
secondaryActions={null}
/>
)
expect(screen.getByText('1 check failing')).toBeTruthy()
expect(
getOverallCheckCounts(
rows,
acceptPRCheckObservation(
{ ...owner, checksSummary: { ...blocked, failed: 2, total: 102 } },
null,
context
)!,
accepted
).failing
).toBe(2)
}
)
it.each(['page', 'compact'] as const)(
'does not claim zero whole checks in the empty %s renderer',
async (variant) => {
i18n.addResourceBundle(
'es',
'translation',
{
auto: {
components: {
right: { sidebar: { ChecksPanel: { '976cefd02f': 'Comprobaciones no disponibles' } } },
prCheckCounts: { noCheckDetails: 'No hay detalles de comprobaciones' }
}
}
},
true,
true
)
await i18n.changeLanguage('es')
const accepted = acceptPRCheckObservation(owner, null, context)!
const summaryLabel = getChecksSummaryLabel([], accepted)
render(
<ChecksTabLayouts
owner={accepted}
loading={false}
listLength={0}
variant={variant}
compactHeader={<div>{summaryLabel}</div>}
actions={<button>Refresh</button>}
SummaryIcon={CircleDashed}
summaryColor=""
summaryLabel={summaryLabel}
counts={getCheckCounts([])}
summaryPending={false}
sorted={[]}
renderCheckRow={() => <div />}
fixChecksAgentDialog={<span />}
/>
)
expect(screen.getByText('Comprobaciones no disponibles')).toBeTruthy()
expect(screen.getByText('No hay detalles de comprobaciones')).toBeTruthy()
expect(screen.queryByText('No checks found')).toBeNull()
}
)
it('keeps the row Fix action available when the complete owner is unavailable', () => {
const accepted = acceptPRCheckObservation(owner, null, context)!
render(
<PRTriageStrip
review={accepted}
checks={[{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }]}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
)
expect(screen.getByText('Checks unavailable')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Fix' }).hasAttribute('disabled')).toBe(false)
})
it('keeps a successful whole owner presentation when independent rows still contain an old failure', () => {
const accepted = acceptPRCheckObservation(
{ ...owner, checksSummary: { ...blocked, state: 'success', total: 1, passed: 1, failed: 0 } },
null,
context
)!
render(
<PRTriageStrip
review={accepted}
checks={[{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }]}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
)
expect(screen.getByText('No blocking PR action')).toBeTruthy()
expect(screen.queryByText('0 failing checks')).toBeNull()
})
it.each([false, true])('keeps a failed parent pipeline fixable with empty jobs: %s', (empty) => {
const fix = vi.fn()
const props = {
review: owner,
reviewKind: 'MR' as const,
checks: empty ? [] : rows.map((row) => ({ ...row, conclusion: 'success' as const })),
pipelineCheck: {
name: 'Pipeline',
status: 'completed' as const,
conclusion: 'failure' as const,
url: null
},
isResolvingConflictsWithAI: false,
onResolveConflictsWithAI: () => {},
isFixingChecksWithAI: false,
onFixChecksWithAI: fix
}
const view = render(<PRTriageStrip {...props} />)
fireEvent.click(screen.getByRole('button', { name: 'Fix' }))
expect(fix).toHaveBeenCalledOnce()
view.rerender(<PRTriageStrip {...props} fixChecksDisabled />)
expect(screen.getByRole('button', { name: 'Fix' }).hasAttribute('disabled')).toBe(true)
view.rerender(<PRTriageStrip {...props} isFixingChecksWithAI />)
expect(screen.getByRole('button', { name: 'Fix' }).hasAttribute('disabled')).toBe(true)
const accepted = acceptPRCheckObservation({ ...owner, checksSummary: blocked }, null, context)!
view.rerender(
<PRTriageStrip {...props} reviewKind="PR" review={accepted} checks={[]} pipelineCheck={null} />
)
expect(screen.getByRole('button', { name: 'Fix' }).hasAttribute('disabled')).toBe(true)
})
@@ -0,0 +1,126 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { refreshPullRequestChecks } from './pull-request-page/checks/refresh'
import {
refreshGitHubChecksTab,
type ChecksTabActionContext
} from './github-item-dialog/inspect-pull-request/checks-tab-actions'
import { createGitHubChecksTabState } from './github-checks-tab-state'
import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-capabilities'
import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as capability } from '../../../shared/source-control/runtime/source-control-runtime-capabilities'
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
import { getOverallCheckCounts } from './pr-check-counts'
const { lookup, rows } = vi.hoisted(() => ({ lookup: vi.fn(), rows: vi.fn() }))
vi.mock('@/store', () => ({ useAppStore: { getState: () => ({}) } }))
vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
const item: GitHubWorkItem = {
id: 'pr-42',
repoId: 'repo-1',
number: 42,
type: 'pr',
title: 'PR',
state: 'open',
url: 'https://github.com/acme/widgets/pull/42',
labels: [],
author: null,
updatedAt: '2026-11-04',
headSha: 'head',
prRepo: { owner: 'acme', repo: 'widgets', host: 'github.com' }
}
const summary = {
state: 'failure' as const,
total: 101,
passed: 100,
failed: 1,
cancelled: 0,
pending: 0,
neutral: 0
}
function context() {
return {
canUseChecksRepoContext: true,
runtimeHost: null,
repoId: item.repoId,
repoPath: '/repo',
item,
itemNumber: 42,
itemRepoId: item.repoId,
headSha: 'head',
prRepo: item.prRepo!,
mountedRef: { current: true },
committedChecksContextOwnerRef: {
current: createGitHubChecksTabState([], 'repo').contextOwner
},
nextChecksRefreshRequestIdRef: { current: 0 },
activeChecksRefreshRequestIdRef: { current: null },
nextCheckDetailsRequestIdRef: { current: 0 },
setRefreshingOwner: vi.fn(),
setChecksState: vi.fn(),
setRerunningOwner: vi.fn(),
onChecksUpdated: vi.fn(),
onOwnerUpdated: vi.fn()
} satisfies ChecksTabActionContext
}
beforeEach(() => {
lookup.mockReset()
rows.mockReset()
Object.assign(globalThis, {
window: { api: { gh: { prChecks: rows, workItemByOwnerRepo: lookup } } }
})
rows.mockResolvedValue([{ name: 'Build', status: 'completed', conclusion: 'success', url: null }])
setLocalRuntimeCapabilitiesForTests([capability])
})
afterEach(() => setLocalRuntimeCapabilitiesForTests(null))
it.each(['page', 'dialog'])(
'refreshes the %s owner and uses its counts while returned rows remain partial',
async (surface) => {
const ctx = context()
const refresh = () =>
surface === 'page' ? refreshPullRequestChecks(ctx) : refreshGitHubChecksTab(ctx)
lookup.mockResolvedValue({ ...item, checksSummary: summary })
const checks = await refresh()
const owner = ctx.onOwnerUpdated.mock.calls[0][0]
expect(getOverallCheckCounts(checks!, owner)).toMatchObject({ passing: 100, failing: 1 })
lookup.mockResolvedValue({
...item,
checksSummary: { ...summary, state: 'success', total: 101, passed: 101, failed: 0 }
})
await refresh()
expect(ctx.onOwnerUpdated.mock.calls[1][0].checksSummary).toMatchObject({
state: 'success',
passed: 101
})
const deferred = Promise.withResolvers<GitHubWorkItem>()
lookup.mockReturnValue(deferred.promise)
const pending = refresh()
ctx.committedChecksContextOwnerRef.current = {}
deferred.resolve({ ...item, checksSummary: summary })
await pending
expect(ctx.onOwnerUpdated).toHaveBeenCalledTimes(2)
}
)
it('keeps the supported old-host refresh parameters and row-only publication', async () => {
setLocalRuntimeCapabilitiesForTests([])
const ctx = context()
await refreshPullRequestChecks(ctx)
expect(lookup).not.toHaveBeenCalled()
expect(ctx.onOwnerUpdated).not.toHaveBeenCalled()
expect(ctx.onChecksUpdated.mock.calls[0][0]).toHaveLength(1)
expect(rows.mock.calls[0][0]).toMatchObject({ prNumber: 42, headSha: 'head', noCache: true })
})
it.each(['page', 'dialog'])('settles %s owner and row failures independently', async (surface) => {
const ctx = context()
const refresh = () =>
surface === 'page' ? refreshPullRequestChecks(ctx) : refreshGitHubChecksTab(ctx)
lookup.mockResolvedValue({ ...item, checksSummary: summary })
rows.mockRejectedValueOnce(new Error('rows failed'))
await refresh()
expect(ctx.onOwnerUpdated.mock.calls[0][0].checksSummary).toEqual(summary)
expect(ctx.onChecksUpdated).not.toHaveBeenCalled()
lookup.mockRejectedValueOnce(new Error('owner failed'))
await refresh()
expect(ctx.onChecksUpdated.mock.calls[0][0]).toHaveLength(1)
expect(ctx.onOwnerUpdated.mock.calls[1][0].checksSummary).toBeUndefined()
})
@@ -0,0 +1,207 @@
// @vitest-environment happy-dom
import { useSyncExternalStore } from 'react'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-capabilities'
import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as capability } from '../../../shared/source-control/runtime/source-control-runtime-capabilities'
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
import type { PRCheckDetail } from '../../../shared/github/check-types'
import { TooltipProvider } from '@/components/ui/tooltip'
import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner'
import {
patchCachedPRChecks,
subscribeWorkItemDetailsCache,
touchWorkItemDetailsCache,
workItemDetailsCache
} from './github-item-dialog/load-item-details/work-item-details-cache'
import { ChecksTab as PageChecks } from './pull-request-page/checks/tab'
import { ChecksTab as DialogChecks } from './github-item-dialog/inspect-pull-request/checks-tab'
const { lookup, rows, rerun } = vi.hoisted(() => ({
lookup: vi.fn(),
rows: vi.fn(),
rerun: vi.fn()
}))
vi.mock('@/store', async () => {
const { getDefaultSettings } = await import('../../../shared/constants')
const state = {
settings: getDefaultSettings('/tmp/pr-check-tab-actions'),
repos: [],
projects: [],
runtimeStatusByEnvironmentId: new Map()
}
return {
useAppStore: Object.assign((select: (state: unknown) => unknown) => select(state), {
getState: () => state
})
}
})
vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
function actions({
onRefresh,
onRerun
}: {
onRefresh: () => void
onRerun: (failedOnly: boolean) => void
}) {
const refreshAction = <button onClick={onRefresh}>Refresh</button>
const rerunAction = <button onClick={() => onRerun(true)}>Rerun</button>
return {
refreshAction,
rerunAction,
actions: (
<>
{refreshAction}
{rerunAction}
</>
),
secondaryActions: null,
fixBrokenChecksAction: null
}
}
vi.mock('./pull-request-page/checks/toolbar', async (original) => ({
...(await original<Record<string, unknown>>()),
buildChecksToolbar: actions
}))
vi.mock('./github-item-dialog/inspect-pull-request/checks-tab-header', async (original) => ({
...(await original<Record<string, unknown>>()),
ChecksTabActions: actions
}))
const initial: GitHubWorkItem = {
id: 'pr-42',
repoId: 'repo-1',
number: 42,
type: 'pr',
title: 'PR',
state: 'open',
url: 'https://github.com/acme/widgets/pull/42',
labels: [],
author: null,
updatedAt: '2026-11-04',
headSha: 'a'.repeat(40),
prRepo: { owner: 'acme', repo: 'widgets', host: 'github.com' }
}
const failed: PRCheckDetail[] = [
{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }
]
const summary = {
state: 'failure' as const,
total: 1,
passed: 0,
failed: 1,
cancelled: 0,
pending: 0,
neutral: 0
}
const cacheKey = 'pr-checks-partial-actions-test'
function Harness({ Tab, parent = initial }: { Tab: typeof PageChecks; parent?: GitHubWorkItem }) {
const checks = useSyncExternalStore(
subscribeWorkItemDetailsCache,
() => workItemDetailsCache.get(cacheKey)?.details?.checks ?? failed
)
return (
<TooltipProvider>
<Tab
item={parent}
repoPath="/repo"
repoId="repo-1"
headSha={parent.headSha}
checks={checks}
loading={false}
onChecksUpdated={(next) => patchCachedPRChecks(cacheKey, next)}
/>
</TooltipProvider>
)
}
beforeEach(() => {
vi.clearAllMocks()
touchWorkItemDetailsCache(cacheKey, {
details: { item: initial, body: '', comments: [], checks: failed },
fetchedAt: 0
})
lookup.mockResolvedValue({ ...initial, headSha: 'b'.repeat(40), checksSummary: summary })
rows.mockResolvedValue(failed)
rerun.mockResolvedValue({ ok: true, count: 1 })
Object.assign(window, {
api: { gh: { prChecks: rows, workItemByOwnerRepo: lookup, rerunPRChecks: rerun } }
})
setLocalRuntimeCapabilitiesForTests([capability])
})
afterEach(() => {
cleanup()
workItemDetailsCache.delete(cacheKey)
setLocalRuntimeCapabilitiesForTests(null)
})
it.each([PageChecks, DialogChecks])(
'reruns the refreshed head while the parent still has the prior head',
async (Tab) => {
render(<Harness Tab={Tab} />)
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
})
expect(rows.mock.calls[0][0]).toMatchObject({ headSha: 'b'.repeat(40) })
lookup.mockRejectedValueOnce(new Error('owner unavailable'))
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Rerun' }))
})
expect(rerun.mock.calls[0][0]).toMatchObject({
prNumber: 42,
headSha: 'b'.repeat(40),
failedOnly: true
})
expect(rows.mock.calls[1][0]).toMatchObject({ headSha: 'b'.repeat(40) })
expect(screen.getByText('1 check failing')).toBeTruthy()
}
)
it.each([PageChecks, DialogChecks])(
'keeps approval explanation across its cache echo and retires it for a newer owner',
async (Tab) => {
lookup.mockResolvedValue({ ...initial, checksSummary: summary })
rows.mockResolvedValue([
{
name: 'Approval',
status: 'completed',
conclusion: 'action_required',
url: null,
observedHeadSha: 'a'.repeat(40)
}
])
const view = render(<Harness Tab={Tab} />)
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
})
expect(screen.getByText('1 check needs action')).toBeTruthy()
const newer = acceptPRCheckObservation({ ...initial, checksSummary: { ...summary } }, null, {
scope: 'workspace:local:local',
complete: true,
current: true
})!
view.rerender(<Harness Tab={Tab} parent={newer} />)
expect(screen.getByText('1 check failing')).toBeTruthy()
}
)
it.each([PageChecks, DialogChecks])(
'keeps a mismatched approval head conservative after its cache echo',
async (Tab) => {
lookup.mockResolvedValue({ ...initial, checksSummary: summary })
rows.mockResolvedValue([
{
name: 'Approval',
status: 'completed',
conclusion: 'action_required',
url: null,
observedHeadSha: 'b'.repeat(40)
}
])
render(<Harness Tab={Tab} />)
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
})
expect(screen.getByText('1 check failing')).toBeTruthy()
expect(screen.getByText('Approval')).toBeTruthy()
}
)
@@ -1,13 +1,16 @@
import type { ProviderCheckSummary } from '../../../../../shared/github/pull-request-types'
import { PRCheckEmptyDetails } from '@/components/pr-check-empty-details'
import React from 'react'
import { CircleDashed, LoaderCircle } from 'lucide-react'
import { LoaderCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { CHECK_COLOR } from '@/components/right-sidebar/checks-panel/presentation/check-presentation'
import { getCheckCountChips, type getCheckCounts } from '@/components/pr-check-counts'
import { getCheckDetailsKey } from '@/components/github/pr-check-presentation'
import { translate } from '@/i18n/i18n'
import type { PRCheckDetail } from '../../../../../shared/github/check-types'
export function ChecksTabLayouts({
owner,
summaryPending,
loading,
listLength,
variant,
@@ -21,6 +24,8 @@ export function ChecksTabLayouts({
renderCheckRow,
fixChecksAgentDialog
}: {
owner?: { checksSummary?: ProviderCheckSummary }
summaryPending: boolean
loading: boolean
listLength: number
variant: 'compact' | 'page'
@@ -45,37 +50,16 @@ export function ChecksTabLayouts({
)
}
if (listLength === 0) {
if (variant === 'page') {
return (
<div className="flex flex-col gap-3 px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<CircleDashed className="size-4 shrink-0 text-muted-foreground" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-[13px] font-medium text-foreground">
{translate('auto.components.PullRequestPage.45877f5089', 'No checks found')}
</span>
<span className="truncate text-[11px] text-muted-foreground">
{translate(
'auto.components.PullRequestPage.3912daf310',
'This pull request has no reported checks yet.'
)}
</span>
</div>
{actions}
</div>
</div>
)
}
return (
<>
{compactHeader}
<div className="flex flex-col items-center justify-center gap-1 px-4 py-6 text-center">
<CircleDashed className="size-4 text-muted-foreground/60" />
<div className="text-[12px] text-muted-foreground">
{translate('auto.components.PullRequestPage.a18d01cda3', 'No checks reported yet')}
</div>
</div>
</>
<PRCheckEmptyDetails
owner={owner}
variant={variant}
compactHeader={compactHeader}
actions={actions}
SummaryIcon={SummaryIcon}
summaryColor={summaryColor}
summaryLabel={summaryLabel}
/>
)
}
if (variant === 'page') {
@@ -85,11 +69,7 @@ export function ChecksTabLayouts({
<div className="flex flex-col gap-3 px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<SummaryIcon
className={cn(
'size-4 shrink-0',
summaryColor,
counts.pending > 0 && counts.failing === 0 && 'animate-spin'
)}
className={cn('size-4 shrink-0', summaryColor, summaryPending && 'animate-spin')}
/>
<div className="flex min-w-0 flex-1 items-center gap-2">
<span className="truncate text-[13px] font-medium text-foreground">
@@ -1,3 +1,4 @@
import { readPRCheckRefreshOwner } from '@/lib/pr-check-refresh-owner'
import { toast } from 'sonner'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context'
@@ -35,6 +36,7 @@ export async function refreshPullRequestChecks(args: {
headSha: string | undefined
prRepo: GitHubOwnerRepo | null
mountedRef: { current: boolean }
onOwnerUpdated?: (item: GitHubWorkItem) => void
onChecksUpdated: (checks: PRCheckDetail[]) => void
}): Promise<PRCheckDetail[] | null> {
if (!args.canUseChecksRepoContext) {
@@ -55,6 +57,23 @@ export async function refreshPullRequestChecks(args: {
args.activeChecksRefreshRequestIdRef.current = refreshRequestId
args.setRefreshingOwner({ contextOwner: refreshContextOwner, requestId: refreshRequestId })
try {
const observation = await readPRCheckRefreshOwner({
repoPath: args.repoPath,
repoId: args.repoId ?? args.item.repoId,
sourceContext: args.sourceContext,
ownerRepo: args.prRepo,
item: args.item,
accepts: () =>
args.mountedRef.current &&
args.committedChecksContextOwnerRef.current === refreshContextOwner &&
args.activeChecksRefreshRequestIdRef.current === refreshRequestId
})
if (!observation.current()) {
return null
}
if (observation.owner) {
args.onOwnerUpdated?.(observation.owner)
}
const nextChecks = await (args.runtimeHost
? callRuntimeRpc<PRCheckDetail[]>(
{ kind: 'environment', environmentId: args.runtimeHost.environmentId },
@@ -62,24 +81,25 @@ export async function refreshPullRequestChecks(args: {
{
repo: getGitHubRuntimeRepoId(args.sourceContext, args.repoId ?? args.item.repoId),
prNumber: args.item.number,
headSha: args.headSha,
headSha: observation.owner?.headSha ?? args.headSha,
prRepo: args.prRepo,
noCache: true
},
{ timeoutMs: 30_000 }
{ timeoutMs: 30_000, ...observation.rpcOptions }
)
: window.api.gh.prChecks({
repoPath: args.repoPath ?? '',
repoId: args.repoId ?? undefined,
sourceContext: args.sourceContext,
prNumber: args.item.number,
headSha: args.headSha,
headSha: observation.owner?.headSha ?? args.headSha,
prRepo: args.prRepo,
noCache: true,
// Refresh and rerun are user acts: they may start a stopped WSL distro.
wakeWslDistro: true
}))
if (
!observation.current() ||
!args.mountedRef.current ||
args.committedChecksContextOwnerRef.current !== refreshContextOwner ||
args.activeChecksRefreshRequestIdRef.current !== refreshRequestId
@@ -88,7 +108,11 @@ export async function refreshPullRequestChecks(args: {
}
args.setChecksState((current) =>
current.contextOwner === refreshContextOwner
? updateGitHubChecksTabLocalChecks(resetGitHubChecksTabForSource(current), nextChecks)
? updateGitHubChecksTabLocalChecks(
resetGitHubChecksTabForSource(current),
nextChecks,
observation.owner
)
: current
)
args.onChecksUpdated(nextChecks)
@@ -1,3 +1,5 @@
import { isCompletePRCheckOwner } from '../../../../../shared/github/complete-pr-check-owner'
import { usePRCheckRefreshOwner } from '@/hooks/use-pr-check-refresh-owner'
import { getChecksTabSummaryPresentation } from '@/components/github-item-dialog/inspect-pull-request/checks-tab-summary'
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useAppStore } from '@/store'
@@ -9,7 +11,11 @@ import {
type GitHubChecksContextOwner
} from '@/components/github-checks-tab-state'
import { getCheckDetailsKey } from '@/components/github/pr-check-presentation'
import { getCheckCounts, getChecksSummaryLabel } from '@/components/pr-check-counts'
import {
getCheckCounts,
getOverallCheckCounts,
getChecksSummaryLabel
} from '@/components/pr-check-counts'
import { getBrokenChecks } from '../../../../../shared/source-control/checks/pr-checks-fix-prompt'
import { sortChecksBySeverity } from '../../../../../shared/source-control/checks/pr-check-severity-order'
import { githubRepoIdentityKey } from '../../../../../shared/github/repository-identity-key'
@@ -68,6 +74,7 @@ export function ChecksTab({
)
const repos = useAppStore((s) => s.repos)
const projects = useAppStore((s) => s.projects)
const { checkOwner, onOwnerUpdated } = usePRCheckRefreshOwner(item, sourceContext)
const [fixingChecks, setFixingChecks] = useState(false)
const [fixChecksComposerPrompt, setFixChecksComposerPrompt] = useState<string | null>(null)
const mountedRef = useMountedRef()
@@ -160,8 +167,17 @@ export function ChecksTab({
const sorted = sortChecksBySeverity(list)
const failedChecks = getBrokenChecks(list)
const counts = getCheckCounts(list)
const summaryLabel = getChecksSummaryLabel(list)
const { SummaryIcon, summaryColor } = getChecksTabSummaryPresentation(counts)
const overallCounts = getOverallCheckCounts(
list,
checkOwner,
resolvedChecksState.checkObservationOwner
)
const summaryLabel = getChecksSummaryLabel(
list,
checkOwner,
resolvedChecksState.checkObservationOwner
)
const { SummaryIcon, summaryColor } = getChecksTabSummaryPresentation(overallCounts)
const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0)
const handleRefresh = useCallback(
@@ -178,18 +194,19 @@ export function ChecksTab({
sourceContext,
repoId,
repoPath,
item,
headSha,
item: checkOwner,
headSha: checkOwner.headSha,
prRepo,
mountedRef,
onOwnerUpdated,
onChecksUpdated
}),
[
canUseChecksRepoContext,
headSha,
item,
checkOwner,
mountedRef,
onChecksUpdated,
onOwnerUpdated,
runtimeHost,
prRepo,
repoId,
@@ -209,8 +226,8 @@ export function ChecksTab({
sourceContext,
repoId,
repoPath,
item,
headSha,
item: checkOwner,
headSha: checkOwner.headSha,
prRepo,
failedOnly,
mountedRef,
@@ -219,8 +236,7 @@ export function ChecksTab({
[
canUseChecksRepoContext,
handleRefresh,
headSha,
item,
checkOwner,
mountedRef,
prRepo,
runtimeHost,
@@ -305,7 +321,8 @@ export function ChecksTab({
SummaryIcon={SummaryIcon}
summaryColor={summaryColor}
summaryLabel={summaryLabel}
counts={counts}
counts={overallCounts}
showSummary={isCompletePRCheckOwner(checkOwner)}
listLength={list.length}
refreshAction={refreshAction}
rerunAction={rerunAction}
@@ -353,6 +370,8 @@ export function ChecksTab({
summaryColor={summaryColor}
summaryLabel={summaryLabel}
counts={counts}
summaryPending={overallCounts.pending > 0 && overallCounts.failing === 0}
owner={checkOwner}
sorted={sorted}
renderCheckRow={renderCheckRow}
fixChecksAgentDialog={fixChecksAgentDialog}
@@ -151,6 +151,7 @@ export function ChecksCompactHeader({
summaryLabel,
counts,
listLength,
showSummary = false,
refreshAction,
rerunAction,
secondaryActions
@@ -159,6 +160,7 @@ export function ChecksCompactHeader({
summaryColor: string
summaryLabel: string
counts: { pending: number; failing: number }
showSummary?: boolean
listLength: number
refreshAction: React.JSX.Element
rerunAction: React.JSX.Element | null
@@ -179,7 +181,7 @@ export function ChecksCompactHeader({
<div className="text-[13px] font-medium leading-5 text-foreground">
{translate('auto.components.PullRequestPage.94d95cf1f7', 'Checks')}
</div>
{listLength > 0 && (
{(listLength > 0 || showSummary) && (
<div className="truncate text-[11px] leading-4 text-muted-foreground">
{summaryLabel}
</div>
@@ -3,11 +3,13 @@ import { Check, Copy, Files } from 'lucide-react'
import { Button } from '@/components/ui/button'
import type {
PRConflictSummary,
ProviderCheckSummary,
PRMergeableState
} from '../../../../../../shared/github/pull-request-types'
import { translate } from '@/i18n/i18n'
export type ConflictReview = {
checksSummary?: ProviderCheckSummary
mergeable: PRMergeableState
conflictSummary?: PRConflictSummary
}
@@ -1,3 +1,4 @@
import { acceptPRCheckObservation } from '../../../../../../shared/github/complete-pr-check-owner'
import { defineFixture } from '../../../../component-fixtures/component-fixture'
import { PRTriageStrip } from './triage-strip'
import { ChecksList } from './checks-list'
@@ -48,3 +49,28 @@ export const cancelledChecks = defineFixture({
</div>
)
})
export const unavailableWithFailedRow = defineFixture({
width: 305,
render: () => (
<div className="bg-background text-foreground">
<PRTriageStrip
review={acceptPRCheckObservation(
{
number: 42,
url: 'https://github.com/acme/widgets/pull/42',
headSha: 'h1',
mergeable: 'UNKNOWN' as const
},
null,
{ scope: 'fixture', complete: true, current: true }
)!}
checks={[{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }]}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
</div>
)
})
@@ -1,3 +1,4 @@
import { isCompletePRCheckOwner } from '../../../../../../shared/github/complete-pr-check-owner'
import React from 'react'
import {
AlertTriangle,
@@ -52,14 +53,28 @@ export function PRTriageStrip({
onFixChecksWithAI: () => void
fixChecksDisabled?: boolean
fixChecksDisabledReason?: string
}): React.JSX.Element {
}): React.JSX.Element | null {
const resolvedReview = review ?? pr
// Share the pill's classifier so completed checks without conclusions remain unresolved rather than spinning.
const summary = summarizeProviderChecks(checks)
const failingCount = summary.failed - (summary.cancelled ?? 0)
const outcome = getProviderChecksOutcome(summary)
const complete = reviewKind === 'PR' && resolvedReview && isCompletePRCheckOwner(resolvedReview)
const summary = complete ? resolvedReview.checksSummary : summarizeProviderChecks(checks)
const brokenCheckCount = getBrokenChecks(checks).length
const pendingCount = summary.pending
if (!summary && brokenCheckCount === 0) {
return resolvedReview?.mergeable === 'CONFLICTING' ? (
<ConflictTriageStrip
reviewKind={reviewKind}
isResolvingConflictsWithAI={isResolvingConflictsWithAI}
onResolveConflictsWithAI={onResolveConflictsWithAI}
resolveConflictsDisabled={resolveConflictsDisabled}
resolveConflictsDisabledReason={resolveConflictsDisabledReason}
/>
) : null
}
const rowSummary = summarizeProviderChecks(checks)
const displaySummary = summary ?? rowSummary
const failingCount = displaySummary.failed - (displaySummary.cancelled ?? 0)
const outcome = getProviderChecksOutcome(displaySummary)
const pendingCount = displaySummary.pending
const pipelineFailed = pipelineCheck ? isBrokenCheck(pipelineCheck) : false
const conflictStrip =
@@ -73,7 +88,11 @@ export function PRTriageStrip({
/>
) : null
if (brokenCheckCount > 0 || pipelineFailed) {
if (
((!complete || !summary) && brokenCheckCount > 0) ||
(complete && (summary?.failed ?? 0) > 0) ||
pipelineFailed
) {
return (
<>
{conflictStrip}
@@ -91,28 +110,33 @@ export function PRTriageStrip({
/>
<div className="min-w-0 flex-1">
<div className="truncate text-[11px] font-medium text-foreground">
{pipelineCheck && pipelineFailed
{!summary
? translate(
'auto.components.right.sidebar.checks.panel.content.pipelineFailed',
'{{name}} failed',
{ name: pipelineCheck.name }
'auto.components.right.sidebar.ChecksPanel.976cefd02f',
'Checks unavailable'
)
: outcome === 'cancelled'
: pipelineCheck && pipelineFailed
? translate(
'auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0',
'Cancelled'
'auto.components.right.sidebar.checks.panel.content.pipelineFailed',
'{{name}} failed',
{ name: pipelineCheck.name }
)
: outcome === 'pending'
: outcome === 'cancelled'
? translate(
'auto.components.right.sidebar.checks.panel.content.pendingChecks',
'{{count}} checks pending',
{ count: pendingCount, defaultValue_one: '{{count}} check pending' }
'auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0',
'Cancelled'
)
: translate(
'auto.components.right.sidebar.checks.panel.content.failingChecks',
'{{count}} failing checks',
{ count: failingCount, defaultValue_one: '{{count}} failing check' }
)}
: outcome === 'pending'
? translate(
'auto.components.right.sidebar.checks.panel.content.pendingChecks',
'{{count}} checks pending',
{ count: pendingCount, defaultValue_one: '{{count}} check pending' }
)
: translate(
'auto.components.right.sidebar.checks.panel.content.failingChecks',
'{{count}} failing checks',
{ count: failingCount, defaultValue_one: '{{count}} failing check' }
)}
</div>
<div className="truncate text-[10px] text-muted-foreground">
{translate(
@@ -125,7 +149,11 @@ export function PRTriageStrip({
type="button"
variant="outline"
size="xs"
disabled={isFixingChecksWithAI || fixChecksDisabled}
disabled={
isFixingChecksWithAI ||
fixChecksDisabled ||
(brokenCheckCount === 0 && !pipelineFailed)
}
title={fixChecksDisabled ? fixChecksDisabledReason : undefined}
onClick={onFixChecksWithAI}
>
@@ -177,7 +205,7 @@ export function PRTriageStrip({
}
// An all-unresolved list must match the grey pill, not show a green tick.
if (summary.state === 'neutral') {
if (displaySummary.state === 'neutral') {
return (
<div className="border-b border-border px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
@@ -187,7 +215,7 @@ export function PRTriageStrip({
{translate(
'auto.components.right.sidebar.checks.panel.content.unresolvedChecks',
'{{count}} checks unresolved',
{ count: summary.neutral, defaultValue_one: '{{count}} check unresolved' }
{ count: displaySummary.neutral, defaultValue_one: '{{count}} check unresolved' }
)}
</div>
<div className="truncate text-[10px] text-muted-foreground">
@@ -0,0 +1,317 @@
import { createCompatibleRuntimeStatusResponse } from '@/runtime/runtime-compatibility-test-fixture'
import { useAppStore } from '@/store'
import { StrictMode, Suspense, startTransition, useState } from 'react'
// @vitest-environment happy-dom
import { act, cleanup, render, renderHook, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import type { TaskPageJiraCreationMetadataModel } from '../jira/use-task-page-jira-creation-metadata'
import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as capability } from '../../../../../shared/source-control/runtime/source-control-runtime-capabilities'
import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-capabilities'
import { useTaskPageGitHubListProjection } from './use-task-page-github-list-projection'
const { lookup } = vi.hoisted(() => ({ lookup: vi.fn() }))
vi.mock('@/store', async () => {
const { create } = await import('zustand')
return { useAppStore: create(() => ({ runtimeStatusByEnvironmentId: new Map() })) }
})
vi.mock('@/hooks/useGitHubTaskItems', () => ({
useGitHubTaskItems: (items: GitHubWorkItem[]) => items
}))
vi.mock('./use-task-page-github-list-pending', () => ({
useTaskPageGitHubListPending: () => false
}))
vi.mock('@/lib/github-work-item-source-lookup', () => ({
lookupGitHubWorkItemByOwnerRepoForSource: lookup
}))
const repo = {
id: 'repo-1',
path: '/repo',
displayName: 'Repo',
badgeColor: '',
addedAt: 1,
kind: 'git' as const
}
const prRepo = { owner: 'acme', repo: 'widgets', host: 'github.com' }
function item(number: number): GitHubWorkItem {
return {
id: `pr-${number}`,
repoId: repo.id,
number,
type: 'pr',
title: 'PR',
state: 'open',
url: `https://github.com/acme/widgets/pull/${number}`,
labels: [],
author: null,
updatedAt: '2026-11-04',
headSha: 'head',
prRepo
}
}
function model(items: GitHubWorkItem[], patch = vi.fn()) {
const input = {
repos: [repo],
selectedRepos: [repo],
fetchPRChecks: vi.fn().mockResolvedValue([]),
taskSource: 'github',
githubMode: 'items',
tasksFiltering: false,
githubPageSize: 100,
pages: [items],
currentPage: 0,
countedTotalPages: 1,
provenPageLimit: 1,
patchTaskPageWorkItemRows: patch,
perRepoSourceState: [],
activeGithubTaskKind: 'prs',
githubWorkItemMutation: { softHiddenItemKeys: new Set() },
selectedReposKey: 'repo',
taskRefreshNonce: 0,
workItemsInvalidationNonce: 0,
appliedTaskSearch: '',
paginationGenerationRef: { current: 1 }
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies all fields the projection hook reads.
return input as unknown as TaskPageJiraCreationMetadataModel
}
beforeEach(() => {
lookup.mockReset()
setLocalRuntimeCapabilitiesForTests([capability])
})
afterEach(() => {
cleanup()
setLocalRuntimeCapabilitiesForTests(null)
})
it.each([100, 1000])(
'bounds automatic exact hydration at N=%i and coalesces demand while failures terminate',
async (n) => {
const items = Array.from({ length: n }, (_, i) => item(i + 1))
const input = model(items)
lookup.mockResolvedValue(null)
const view = renderHook(() => useTaskPageGitHubListProjection(input))
await act(async () => {})
expect(lookup).toHaveBeenCalledTimes(20)
act(() => {
view.result.current.ensurePRChecksLoaded(items[20])
view.result.current.ensurePRChecksLoaded(items[20])
})
await act(async () => {})
expect(lookup).toHaveBeenCalledTimes(21)
view.rerender()
await act(async () => {})
expect(lookup).toHaveBeenCalledTimes(21)
expect(input.fetchPRChecks).not.toHaveBeenCalled()
}
)
it('publishes exact-owner counts, rejects a newer owner replacement, and retires source changes', async () => {
const row = item(42)
let current = row
const patch = vi.fn((_key, values, accepts: (item: GitHubWorkItem) => boolean) => {
if (accepts(current)) {
current = { ...current, ...values }
}
})
const input = model([row], patch)
const read = Promise.withResolvers<GitHubWorkItem>()
lookup.mockReturnValue(read.promise)
const view = renderHook(() => useTaskPageGitHubListProjection(input))
const summary = {
state: 'failure' as const,
total: 101,
passed: 100,
failed: 1,
cancelled: 0,
pending: 0,
neutral: 0
}
await act(async () => {
read.resolve({ ...row, checksSummary: summary })
await read.promise
})
expect(current.checksSummary).toEqual(summary)
// A late hydration read cannot replace a newer full observation on the same head.
input.taskRefreshNonce++
input.paginationGenerationRef.current++
const second = Promise.withResolvers<GitHubWorkItem>()
lookup.mockReturnValue(second.promise)
view.rerender()
current = { ...row, checksSummary: { ...summary, failed: 2, total: 102 } }
await act(async () => {
second.resolve({ ...row, checksSummary: summary })
await second.promise
})
expect(current.checksSummary?.failed).toBe(2)
const third = Promise.withResolvers<GitHubWorkItem>()
lookup.mockReturnValue(third.promise)
input.taskRefreshNonce++
view.rerender()
view.unmount()
const previousCalls = patch.mock.calls.length
await act(async () => {
third.resolve({ ...row, checksSummary: summary })
await third.promise
})
expect(patch.mock.calls.length).toBe(previousCalls)
})
it('does not rerender or rearm failed demand on unrelated host writes or capacity pressure; explicit refresh retries', async () => {
const input = model(Array.from({ length: 200 }, (_, i) => item(i + 1)))
lookup.mockResolvedValue(null)
let renders = 0
const view = renderHook(() => {
renders++
return useTaskPageGitHubListProjection(input)
})
await act(async () => {})
act(() => {
for (const row of input.pages[0] ?? []) {
view.result.current.ensurePRChecksLoaded(row)
}
})
await act(async () => {})
const calls = lookup.mock.calls.length
const before = renders
act(() => useAppStore.setState({ runtimeStatusByEnvironmentId: new Map() }))
await act(async () => {})
expect(renders).toBe(before)
view.rerender()
await act(async () => {})
expect(lookup).toHaveBeenCalledTimes(calls)
input.taskRefreshNonce++
view.rerender()
await act(async () => {})
expect(lookup).toHaveBeenCalledTimes(calls + 20)
})
it('ignores an unselected remote source change and hydrates again when the selected source changes', async () => {
const selectedRepo = { ...repo, executionHostId: 'runtime:host-a' as const }
const unselectedRepo = { ...repo, id: 'repo-b', executionHostId: 'runtime:host-b' as const }
const setStatus = (host: string, runtimeId: string) => {
const response = createCompatibleRuntimeStatusResponse(runtimeId)
if (!response.ok) {
throw new Error('Expected a runtime status')
}
const statuses = new Map(useAppStore.getState().runtimeStatusByEnvironmentId)
statuses.set(host, { status: { ...response.result, capabilities: [capability] }, checkedAt: 1 })
useAppStore.setState({ runtimeStatusByEnvironmentId: statuses })
}
setStatus('host-a', 'a1')
setStatus('host-b', 'b1')
const input = model([item(42)])
input.repos = [selectedRepo, unselectedRepo]
input.selectedRepos = [selectedRepo]
lookup.mockResolvedValue(null)
let renders = 0
renderHook(() => {
renders++
return useTaskPageGitHubListProjection(input)
})
await act(async () => {})
expect(lookup).toHaveBeenCalledTimes(1)
const before = renders
act(() => setStatus('host-b', 'b2'))
await act(async () => {})
expect(renders).toBe(before)
expect(lookup).toHaveBeenCalledTimes(1)
act(() => setLocalRuntimeCapabilitiesForTests([]))
await act(async () => {})
expect(renders).toBe(before)
act(() => setStatus('host-a', 'a2'))
await act(async () => {})
expect(renders).toBeGreaterThan(before)
expect(lookup).toHaveBeenCalledTimes(2)
})
it('publishes one committed demand read through StrictMode effect replay', async () => {
const row = item(42)
let current = row
const patch = vi.fn((_key, values, accepts: (item: GitHubWorkItem) => boolean) => {
if (accepts(current)) {
current = { ...current, ...values }
}
})
const pending = Promise.withResolvers<GitHubWorkItem>()
const summary = {
state: 'failure' as const,
total: 1,
passed: 0,
failed: 1,
cancelled: 0,
pending: 0,
neutral: 0
}
lookup.mockReturnValue(pending.promise)
const input = model([row], patch)
renderHook(() => useTaskPageGitHubListProjection(input), { wrapper: StrictMode })
await act(async () => {})
expect(lookup).toHaveBeenCalledOnce()
await act(async () => {
pending.resolve({ ...row, checksSummary: summary })
await pending.promise
})
expect(current.checksSummary).toEqual(summary)
})
it.each([true, false])(
'only a committed query replacement retires a pending read (suspend=%s)',
async (suspend) => {
const row = item(42)
let current = row
const patch = vi.fn((_key, values, accepts: (item: GitHubWorkItem) => boolean) => {
if (accepts(current)) {
current = { ...current, ...values }
}
})
const original = Promise.withResolvers<GitHubWorkItem>()
const replacement = Promise.withResolvers<GitHubWorkItem>()
const blocked = Promise.withResolvers<void>()
const summary = {
state: 'failure' as const,
total: 1,
passed: 0,
failed: 1,
cancelled: 0,
pending: 0,
neutral: 0
}
lookup.mockReturnValueOnce(original.promise).mockReturnValueOnce(replacement.promise)
const first = model([row], patch)
const second = { ...first, appliedTaskSearch: 'replacement' }
let navigate!: (next: boolean) => void
function Consumer({ next }: { next: boolean }) {
useTaskPageGitHubListProjection(next ? second : first)
if (next && suspend) {
throw blocked.promise
}
return <span>{next ? 'Replacement query' : 'Original query'}</span>
}
function Harness() {
const [next, setNext] = useState(false)
navigate = setNext
return (
<Suspense fallback={<span>Waiting</span>}>
<Consumer next={next} />
</Suspense>
)
}
const view = render(<Harness />)
await act(async () => {
startTransition(() => navigate(true))
})
expect(screen.getByText(suspend ? 'Original query' : 'Replacement query')).toBeTruthy()
await act(async () => {
original.resolve({ ...row, checksSummary: summary })
await original.promise
})
expect(current.checksSummary).toEqual(suspend ? summary : undefined)
view.unmount()
await act(async () => {
replacement.resolve({ ...row, checksSummary: summary })
blocked.resolve()
await replacement.promise
})
}
)
@@ -1,3 +1,4 @@
import { projectPRCheckOwner } from '../../../../../shared/github/complete-pr-check-owner'
import type { ParsedTaskQuery } from '../../../../../shared/task-query'
import type { GitHubAssignableUser } from '../../../../../shared/github/pull-request-types'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
@@ -143,7 +144,7 @@ export function getRegistryMergedTaskPageGitHubWorkItem(
: { ...merged, reviewRequests: composed }
}
return merged
return projectPRCheckOwner(item, merged)
}
export function recomputeSoftHideForItem(args: {
@@ -1,3 +1,4 @@
import { projectPRCheckOwner } from '../../../../../shared/github/complete-pr-check-owner'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import type { TaskSourceContext } from '../../../../../shared/task-source-context'
import { parseGitHubIssueOrPRLink } from '../../../../../shared/github/links'
@@ -32,7 +33,10 @@ export function patchTaskPageGitHubWorkItemPages(
return
}
nextPage ??= page.slice()
nextPage[itemIndex] = { ...item, ...patch }
nextPage[itemIndex] = projectPRCheckOwner(
patch,
projectPRCheckOwner(item, { ...item, ...patch })
)
})
if (nextPage) {
nextPages ??= pages.slice()
@@ -1,32 +1,20 @@
import { useCallback, useMemo } from 'react'
import { useGitHubTaskItems } from '@/hooks/useGitHubTaskItems'
import type { TaskPageJiraCreationMetadataModel } from '../jira/use-task-page-jira-creation-metadata'
import { getWorkItemRepo } from '@/lib/work-item-repo-row'
import { useCallback, useMemo, useEffect } from 'react'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import { taskPageGitHubItemKey } from '@/components/task-page/github/task-page-github-work-item-mutation-keys'
import { deriveTaskPagePRCheckSummary } from '../task-page-pr-check-summary'
import { deriveAdvertisedTotalPages } from '../../../../../shared/work-item-empty-page'
import {
GITHUB_PR_TASK_GRID_CLASS,
GITHUB_TASK_GRID_CLASS,
PR_CHECKS_EAGER_PREFETCH_LIMIT
} from '../task-page-source-context'
import { getTaskPageRepoSourceContext } from '../../../lib/task-repo-source-context'
import { sameOptionalGitHubOwnerRepo } from './task-page-github-review-model'
import { GITHUB_PR_TASK_GRID_CLASS, GITHUB_TASK_GRID_CLASS } from '../task-page-source-context'
import { useTaskPageGitHubListPending } from './use-task-page-github-list-pending'
import { useTaskPagePRCheckHydration } from './use-task-page-pr-check-hydration'
export function useTaskPageGitHubListProjection(model: TaskPageJiraCreationMetadataModel) {
const {
repos,
fetchPRChecks,
taskSource,
githubMode,
tasksFiltering,
githubPageSize,
pages,
currentPage,
countedTotalPages,
provenPageLimit,
patchTaskPageWorkItemRows,
perRepoSourceState,
activeGithubTaskKind,
githubWorkItemMutation
@@ -101,56 +89,11 @@ export function useTaskPageGitHubListProjection(model: TaskPageJiraCreationMetad
const githubTaskGridClass = showPRManagementColumns
? GITHUB_PR_TASK_GRID_CLASS
: GITHUB_TASK_GRID_CLASS
const ensurePRChecksLoaded = useCallback(
(item: GitHubWorkItem): void => {
if (item.type !== 'pr' || item.checksSummary) {
return
}
const repo = getWorkItemRepo(repos, item)
if (!repo) {
return
}
const requestedHeadSha = item.headSha
const requestedPRRepo = item.prRepo ?? null
void fetchPRChecks(
repo.path,
item.number,
item.branchName,
item.headSha,
item.prRepo ?? null,
{
repoId: repo.id,
sourceContext: getTaskPageRepoSourceContext(repo, 'github')
}
).then((checks) => {
const checksSummary = deriveTaskPagePRCheckSummary(checks)
patchTaskPageWorkItemRows(
{
id: item.id,
repoId: item.repoId
},
{
checksSummary
},
(currentItem) =>
currentItem.type === 'pr' &&
currentItem.headSha === requestedHeadSha &&
sameOptionalGitHubOwnerRepo(currentItem.prRepo, requestedPRRepo) &&
(currentItem.checksSummary?.cancelled === undefined ||
currentItem.checksSummary.state !== checksSummary.state)
)
})
},
[fetchPRChecks, patchTaskPageWorkItemRows, repos]
const ensurePRChecksLoaded = useTaskPagePRCheckHydration(
model,
filteredWorkItems,
showPRManagementColumns
)
useEffect(() => {
if (taskSource !== 'github' || githubMode !== 'items' || !showPRManagementColumns) {
return
}
for (const item of filteredWorkItems.slice(0, PR_CHECKS_EAGER_PREFETCH_LIMIT)) {
ensurePRChecksLoaded(item)
}
}, [ensurePRChecksLoaded, filteredWorkItems, githubMode, showPRManagementColumns, taskSource])
let lastLoadedPageIndex = 0
for (let index = 0; index < pages.length; index += 1) {
if (pages[index] !== null) {
@@ -0,0 +1,220 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useSyncExternalStore
} from 'react'
import {
readLocalRuntimeCapabilityObservation,
subscribeLocalRuntimeCapabilityObservation
} from '@/runtime/local-runtime-capabilities'
import { useAppStore } from '@/store'
import { capturePRCheckContract, type PRCheckContract } from '@/lib/pr-check-observation-contract'
import { getGitHubSourceRuntimeTarget } from '@/lib/github-source-runtime-context'
import { lookupGitHubWorkItemByOwnerRepoForSource } from '@/lib/github-work-item-source-lookup'
import { resolvePullRequestRepo } from '@/components/github/github-work-item-identity'
import {
acceptPRCheckObservation,
projectPRCheckOwner,
completePRCheckOwnerScope
} from '../../../../../shared/github/complete-pr-check-owner'
import { getWorkItemRepo } from '@/lib/work-item-repo-row'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import type { TaskPageJiraCreationMetadataModel } from '../jira/use-task-page-jira-creation-metadata'
import { deriveTaskPagePRCheckSummary } from '../task-page-pr-check-summary'
import { PR_CHECKS_EAGER_PREFETCH_LIMIT } from '../task-page-source-context'
import { getTaskPageRepoSourceContext } from '../../../lib/task-repo-source-context'
import { sameOptionalGitHubOwnerRepo } from './task-page-github-review-model'
export function useTaskPagePRCheckHydration(
model: TaskPageJiraCreationMetadataModel,
filteredWorkItems: GitHubWorkItem[],
showPRManagementColumns: boolean
) {
const { repos, fetchPRChecks, taskSource, githubMode, patchTaskPageWorkItemRows } = model
const sourceContracts = useMemo(
() =>
model.selectedRepos.map((repo) => {
const source = getTaskPageRepoSourceContext(repo, 'github')
return { source, target: getGitHubSourceRuntimeTarget(source) }
}),
[model.selectedRepos]
)
const localSelected = sourceContracts.some(({ target }) => target.kind === 'local')
const localCapabilities = useSyncExternalStore(
subscribeLocalRuntimeCapabilityObservation,
useCallback(
() => (localSelected ? readLocalRuntimeCapabilityObservation() : null),
[localSelected]
)
)
const runtimeStatuses = useAppStore((state) =>
sourceContracts
.map(({ source, target }) => {
const contract = capturePRCheckContract(() => state, target, source)
return `${contract.scope}:${contract.complete}`
})
.join('|')
)
const checkReads = useMemo(() => {
// A new list query owns its own bounded demand reads.
void model.selectedReposKey
void model.taskRefreshNonce
void model.workItemsInvalidationNonce
void model.appliedTaskSearch
return new Map<string, PRCheckContract>()
}, [
model.selectedReposKey,
model.taskRefreshNonce,
model.workItemsInvalidationNonce,
model.appliedTaskSearch
])
const activeCheckReads = useRef<typeof checkReads | null>(null)
useLayoutEffect(() => {
activeCheckReads.current = checkReads
return () => {
if (activeCheckReads.current === checkReads) {
activeCheckReads.current = null
}
}
}, [checkReads])
const ensurePRChecksLoaded = useCallback(
(item: GitHubWorkItem): void => {
if (item.type !== 'pr') {
return
}
// Capability observations wake the existing demand trigger without adding per-row subscriptions.
void localCapabilities
void runtimeStatuses
const repo = getWorkItemRepo(repos, item)
if (!repo) {
return
}
const sourceContext = getTaskPageRepoSourceContext(repo, 'github')
const runtimeContract = capturePRCheckContract(
useAppStore.getState,
getGitHubSourceRuntimeTarget(sourceContext),
sourceContext
)
const generation = model.paginationGenerationRef.current
const contract = {
...runtimeContract,
current: () =>
runtimeContract.current() &&
activeCheckReads.current === checkReads &&
model.paginationGenerationRef.current === generation
}
if (
item.checksSummary &&
(!contract.complete || completePRCheckOwnerScope(item) === contract.scope)
) {
return
}
const readKey = JSON.stringify([
item.repoId,
item.id,
item.headSha,
sourceContext,
contract.complete
])
if (checkReads.get(readKey)?.current()) {
return
}
checkReads.set(readKey, contract)
// The query's loaded/demanded rows bound attempts; eviction must not re-arm failed eager reads.
const requestedHeadSha = item.headSha
const requestedPRRepo = item.prRepo ?? null
const owner = resolvePullRequestRepo(item)
const read =
contract.complete && owner
? lookupGitHubWorkItemByOwnerRepoForSource({
repoPath: repo.path,
repoId: repo.id,
sourceContext,
...owner,
number: item.number,
type: 'pr',
accepts: contract.current,
checkContract: runtimeContract
})
: fetchPRChecks(
repo.path,
item.number,
item.branchName,
item.headSha,
item.prRepo ?? null,
{
repoId: repo.id,
sourceContext
}
)
void read
.then((result) => {
if (!contract.current()) {
return
}
const observed = Array.isArray(result) ? null : result
if (
contract.complete &&
(!observed ||
observed.headSha !== requestedHeadSha ||
!sameOptionalGitHubOwnerRepo(observed.prRepo ?? null, requestedPRRepo))
) {
return
}
const checksSummary = Array.isArray(result)
? deriveTaskPagePRCheckSummary(result)
: observed?.checksSummary
if (!checksSummary && !contract.complete) {
return
}
const accepted =
observed &&
acceptPRCheckObservation(observed, item, { ...contract, current: contract.current() })
patchTaskPageWorkItemRows(
{
id: item.id,
repoId: item.repoId
},
contract.complete && accepted
? projectPRCheckOwner(accepted, {
checksSummary: accepted.checksSummary,
number: item.number,
url: item.url,
prRepo: item.prRepo,
headSha: item.headSha
})
: { checksSummary },
(currentItem) =>
currentItem.type === 'pr' &&
(!contract.complete || currentItem.checksSummary === item.checksSummary) &&
currentItem.headSha === requestedHeadSha &&
sameOptionalGitHubOwnerRepo(currentItem.prRepo, requestedPRRepo) &&
(currentItem.checksSummary?.cancelled === undefined ||
currentItem.checksSummary.state !== checksSummary?.state ||
contract.complete)
)
})
.catch(() => {})
},
[
localCapabilities,
runtimeStatuses,
checkReads,
fetchPRChecks,
model.paginationGenerationRef,
patchTaskPageWorkItemRows,
repos
]
)
useEffect(() => {
if (taskSource !== 'github' || githubMode !== 'items' || !showPRManagementColumns) {
return
}
for (const item of filteredWorkItems.slice(0, PR_CHECKS_EAGER_PREFETCH_LIMIT)) {
ensurePRChecksLoaded(item)
}
}, [ensurePRChecksLoaded, filteredWorkItems, githubMode, showPRManagementColumns, taskSource])
return ensurePRChecksLoaded
}
@@ -0,0 +1,76 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, expect, it } from 'vitest'
import { usePRCheckRefreshOwner } from './use-pr-check-refresh-owner'
import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner'
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
afterEach(cleanup)
const item: GitHubWorkItem = {
id: 'pr42',
repoId: 'repo',
number: 42,
type: 'pr',
url: 'https://github.com/acme/widgets/pull/42',
title: 'PR',
state: 'open',
headSha: 'h1',
updatedAt: '',
labels: [],
author: null
}
const summary = {
state: 'failure' as const,
total: 101,
passed: 100,
failed: 1,
pending: 0,
neutral: 0
}
const context = { scope: 'source:r1', complete: true, current: true }
it('retains a refreshed head across parent allocation and sequential refresh, then honors a newer parent observation', () => {
const view = renderHook(({ parent }) => usePRCheckRefreshOwner(parent), {
initialProps: { parent: item }
})
act(() =>
view.result.current.onOwnerUpdated(
acceptPRCheckObservation({ ...item, headSha: 'h2', checksSummary: summary }, null, context)!
)
)
view.rerender({ parent: { ...item } })
expect(view.result.current.checkOwner.headSha).toBe('h2')
act(() =>
view.result.current.onOwnerUpdated(
acceptPRCheckObservation(
{ ...view.result.current.checkOwner, checksSummary: undefined },
null,
context
)!
)
)
expect(view.result.current.checkOwner.checksSummary).toEqual(summary)
const newer = acceptPRCheckObservation(
{
...item,
headSha: 'h2',
checksSummary: { ...summary, state: 'success', failed: 0, passed: 101 }
},
null,
context
)!
view.rerender({ parent: newer })
expect(view.result.current.checkOwner.checksSummary?.state).toBe('success')
})
it('does not reuse last-good after the producing runtime changes', () => {
const view = renderHook(() => usePRCheckRefreshOwner(item))
act(() =>
view.result.current.onOwnerUpdated(
acceptPRCheckObservation({ ...item, checksSummary: summary }, null, context)!
)
)
act(() =>
view.result.current.onOwnerUpdated(
acceptPRCheckObservation(item, null, { ...context, scope: 'source:r2' })!
)
)
expect(view.result.current.checkOwner.checksSummary).toBeUndefined()
})

Some files were not shown because too many files have changed in this diff Show More