mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 18:07:45 +00:00
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:
@@ -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)
|
||||
|
||||
+725
-695
File diff suppressed because it is too large
Load Diff
+2122
-2068
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
+2660
-2585
File diff suppressed because it is too large
Load Diff
+2918
-2843
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"
|
||||
|
||||
+425
-419
File diff suppressed because it is too large
Load Diff
+240
-234
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
+580
-524
File diff suppressed because it is too large
Load Diff
+625
-487
File diff suppressed because it is too large
Load Diff
+1611
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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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' }
|
||||
)
|
||||
|
||||
@@ -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
@@ -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' }) })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-4
@@ -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)
|
||||
|
||||
+3
-1
@@ -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>
|
||||
|
||||
+47
-48
@@ -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">
|
||||
|
||||
+11
-3
@@ -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(() => {
|
||||
|
||||
+12
-28
@@ -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([
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
@@ -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
|
||||
}
|
||||
|
||||
+26
@@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
+53
-25
@@ -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
|
||||
})
|
||||
}
|
||||
)
|
||||
+2
-1
@@ -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: {
|
||||
|
||||
+5
-1
@@ -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()
|
||||
|
||||
+7
-64
@@ -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
Reference in New Issue
Block a user