Distinguish cancelled PR checks from failures (#37789)

Show cancelled PR checks as Cancelled with separate counts on desktop and mobile, preserving genuine failures, pending checks, existing action permissions and older-client fallback. Carry complete provider summaries through caches and presentation; preserve Azure source completeness, folder attention priority and muted cancellation details.

Tracked in #15847 and STA-5102.
This commit is contained in:
Brennan Benson
2026-11-03 18:23:26 -08:00
committed by GitHub
parent 63df66d9b6
commit a9676ea6eb
139 changed files with 23705 additions and 20739 deletions
@@ -89,9 +89,6 @@
"auto.components.GitHubItemDialog.e15a8b77ef": {
"ko": "이 검사에는 인라인 세부정보가 없습니다."
},
"auto.components.GitHubItemDialog.e31651a224": {
"ko": "실패한 체크 다시 실행"
},
"auto.components.GitHubItemDialog.e45324fbed": {
"ko": "검사 세부정보를 로드하지 못했습니다."
},
@@ -257,9 +254,6 @@
"auto.components.PullRequestPage.54cddd1858": {
"ko": "모든 체크 다시 실행"
},
"auto.components.PullRequestPage.68605516dd": {
"ko": "실패한 체크 다시 실행"
},
"auto.components.PullRequestPage.6885c619e7": {
"ko": "repos 경로가 없으면 답글을 달 수 없습니다."
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -215,77 +215,6 @@
}
}
},
"2b42882f6c21": {
"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",
"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"
},
"2ff9cdff091a": {
"kind": "none"
},
@@ -513,82 +442,6 @@
"message": "The host sent a reply this app could not read (github.prForBranch)"
}
},
"6a8e42e4e43a": {
"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",
"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,
@@ -789,6 +642,85 @@
}
}
},
"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,
@@ -878,6 +810,80 @@
}
}
},
"e7df27480ce9": {
"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"
},
"ea6a7aa78063": {
"kind": "error",
"message": "outer refused"
@@ -967,9 +973,9 @@
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -156,77 +156,6 @@
}
}
},
"2b42882f6c21": {
"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",
"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"
},
"35103e980aae": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
@@ -448,82 +377,6 @@
}
}
},
"6a8e42e4e43a": {
"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",
"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,
@@ -822,6 +675,85 @@
"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"
},
"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"
}
},
"d8e9ec6c11ee": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
@@ -868,6 +800,80 @@
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.prChecks\",\"params\":{\"repo\":\"id:repo-9\",\"prNumber\":12,\"headSha\":\"head-sha-1\"}}"
},
"e169ebaf642b": "unloaded",
"e7df27480ce9": {
"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"
},
"e8a558288b9c": {
"name": "hostedReview.forBranch#1",
"ordinal": 1,
@@ -942,9 +948,9 @@
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -978,9 +984,9 @@
"sender": ["35103e980aae", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1014,9 +1020,9 @@
"sender": ["9f5541cd3208", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1050,9 +1056,9 @@
"sender": ["489d375fcdeb", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1086,9 +1092,9 @@
"sender": ["05ddd233b0e4", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1122,9 +1128,9 @@
"sender": ["879f943fd73b", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1158,9 +1164,9 @@
"sender": ["e8a558288b9c", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1194,9 +1200,9 @@
"sender": ["d8e9ec6c11ee", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1230,9 +1236,9 @@
"sender": ["7bf83a5e67be", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1266,9 +1272,9 @@
"sender": ["5bf993b3adf7", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -1302,9 +1308,9 @@
"sender": ["636ab64ed281", "7cbf1de78fc1", "973a6e87065b", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "7c9620d17ba0", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
}
@@ -224,77 +224,6 @@
}
}
},
"2b42882f6c21": {
"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",
"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"
},
"3fdee65f324d": {
"name": "worktree.show#1",
"ordinal": 2,
@@ -468,82 +397,6 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo-9::/w\"}}"
},
"6a8e42e4e43a": {
"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",
"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"
}
},
"6b20a29acbfe": {
"name": "github.prForBranch#1",
"ordinal": 5,
@@ -782,12 +635,165 @@
"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": {
"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"
},
"eb44c4f75728": {
"name": "github.prForBranch#1",
"ordinal": 5,
@@ -962,9 +968,9 @@
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
},
@@ -998,9 +1004,9 @@
"sender": ["07639c4f1e7f", "7eb2a61711fc", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1034,9 +1040,9 @@
"sender": ["07639c4f1e7f", "552f85dcd6b4", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1070,9 +1076,9 @@
"sender": ["07639c4f1e7f", "f6b2f58dcf76", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1106,9 +1112,9 @@
"sender": ["07639c4f1e7f", "ade44fd82eca", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1142,9 +1148,9 @@
"sender": ["07639c4f1e7f", "f8ad59a07e10", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1178,9 +1184,9 @@
"sender": ["07639c4f1e7f", "f14112e24d21", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1214,9 +1220,9 @@
"sender": ["07639c4f1e7f", "5087d259003b", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1250,9 +1256,9 @@
"sender": ["07639c4f1e7f", "1a1f37b92aca", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1286,9 +1292,9 @@
"sender": ["07639c4f1e7f", "03dabf7904c5", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
},
@@ -1322,9 +1328,9 @@
"sender": ["07639c4f1e7f", "3fdee65f324d", "6b20a29acbfe", "13bc6662625a"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "cf54ab7bda72", "9fac855f9003"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": []
}
}
@@ -267,39 +267,6 @@
"ordinal": 8,
"value": true
},
"18a362320fb7": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"20f8d00e8abe": {
"name": "detailLoading",
"ordinal": 12,
@@ -384,6 +351,80 @@
"$rpc": "null"
}
},
"4838d50cb348": {
"error": "",
"item": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
},
"items": [
{
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
],
"loading": false,
"payload": {
"assignees": [],
"body": "body",
"comments": [],
"labels": ["bug"],
"pipelineJobs": [],
"provider": "gitlab"
}
},
"4c8bef485c5e": {
"error": "outer refused",
"item": {
@@ -429,6 +470,40 @@
"$rpc": "null"
}
},
"5c69a0ab4415": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"5ecb7a6ae6e9": {
"name": "gitlab.workItemDetails#1",
"ordinal": 7,
@@ -692,73 +767,11 @@
}
}
},
"894fa253aa45": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"8ac545c58ec2": {
"name": "gitlab.workItemDetails#1",
"ordinal": 9,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemDetails\",\"params\":{\"repo\":\"id:repo-1\",\"iid\":4,\"type\":\"issue\",\"projectRef\":{\"host\":\"gitlab.com\",\"path\":\"group/project\"}}}"
},
"978d59506567": {
"name": "items",
"ordinal": 10,
"value": [
{
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
]
},
"988fd3d6f1c4": {
"name": "detailRefreshSeq",
"ordinal": 5,
@@ -809,78 +822,6 @@
"$rpc": "null"
}
},
"9fa102187add": {
"error": "",
"item": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
},
"items": [
{
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
],
"loading": false,
"payload": {
"assignees": [],
"body": "body",
"comments": [],
"labels": ["bug"],
"pipelineJobs": [],
"provider": "gitlab"
}
},
"a4f1a8696a24": {
"name": "detailError",
"ordinal": 2,
@@ -901,6 +842,37 @@
"ordinal": 10,
"value": "Details not found"
},
"ae6cd385ac1c": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"b39230648d91": {
"error": "Unknown method",
"item": {
@@ -968,13 +940,29 @@
"ordinal": 10,
"value": "Unknown method"
},
"c90b732e3d51": {
"cc2ece9478f0": {
"name": "actionItem",
"ordinal": 1,
"value": {
"$rpc": "null"
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
},
"f3032e4b7cc7": {
"error": "",
"item": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -1002,6 +990,7 @@
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -1034,14 +1023,7 @@
"provider": "gitlab"
}
},
"cc2ece9478f0": {
"name": "actionItem",
"ordinal": 1,
"value": {
"$rpc": "null"
}
},
"d759afce345e": {
"f6ff31c3f54f": {
"name": "items",
"ordinal": 10,
"value": [
@@ -1050,6 +1032,7 @@
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -1059,15 +1042,12 @@
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
@@ -1076,14 +1056,6 @@
}
]
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
},
"f791a44b1e80": {
"name": "gitlab.workItemDetails#1",
"ordinal": 7,
@@ -1126,6 +1098,42 @@
}
}
}
},
"f9f4fedb976a": {
"name": "items",
"ordinal": 10,
"value": [
{
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
]
}
},
"recording": {
@@ -1139,7 +1147,7 @@
"settlements": {
"mount": "eb79a9b3682a"
},
"state": "9fa102187add",
"state": "4838d50cb348",
"effects": [
"cc2ece9478f0",
"a4f1a8696a24",
@@ -1148,8 +1156,8 @@
"988fd3d6f1c4",
"274b8ecb4f73",
"185dd46f0a38",
"d759afce345e",
"18a362320fb7",
"f9f4fedb976a",
"5c69a0ab4415",
"20f8d00e8abe",
"3d683d8ca027"
]
@@ -1207,7 +1215,7 @@
"settlements": {
"mount": "eb79a9b3682a"
},
"state": "c90b732e3d51",
"state": "f3032e4b7cc7",
"effects": [
"cc2ece9478f0",
"a4f1a8696a24",
@@ -1216,8 +1224,8 @@
"988fd3d6f1c4",
"274b8ecb4f73",
"185dd46f0a38",
"978d59506567",
"894fa253aa45",
"f6ff31c3f54f",
"ae6cd385ac1c",
"20f8d00e8abe",
"b8892300f59b"
]
@@ -1231,7 +1239,7 @@
"settlements": {
"mount": "eb79a9b3682a"
},
"state": "c90b732e3d51",
"state": "f3032e4b7cc7",
"effects": [
"cc2ece9478f0",
"a4f1a8696a24",
@@ -1240,8 +1248,8 @@
"988fd3d6f1c4",
"274b8ecb4f73",
"185dd46f0a38",
"978d59506567",
"894fa253aa45",
"f6ff31c3f54f",
"ae6cd385ac1c",
"20f8d00e8abe",
"b8892300f59b"
]
@@ -1255,7 +1263,7 @@
"settlements": {
"mount": "eb79a9b3682a"
},
"state": "c90b732e3d51",
"state": "f3032e4b7cc7",
"effects": [
"cc2ece9478f0",
"a4f1a8696a24",
@@ -1264,8 +1272,8 @@
"988fd3d6f1c4",
"274b8ecb4f73",
"185dd46f0a38",
"978d59506567",
"894fa253aa45",
"f6ff31c3f54f",
"ae6cd385ac1c",
"20f8d00e8abe",
"b8892300f59b"
]
File diff suppressed because it is too large Load Diff
@@ -49,62 +49,6 @@
}
}
},
"0d6e25838291": {
"data": {
"checks": [],
"checksError": "The host sent a reply this app could not read (github.prChecks)",
"details": {
"$rpc": "null"
},
"pr": {
"autoMergeAllowed": {
"$rpc": "undefined"
},
"autoMergeEnabled": {
"$rpc": "undefined"
},
"checksStatus": "pending",
"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"
},
"0e35af4e1fa3": {
"name": "github.prForBranch#1",
"ordinal": 7,
@@ -275,6 +219,65 @@
}
}
},
"5b9634cf47d1": {
"data": {
"checks": [],
"checksError": "The host sent a reply this app could not read (github.prChecks)",
"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"
},
"5dbd52d6a1c7": {
"name": "worktree.show#1",
"ordinal": 4,
@@ -318,17 +321,7 @@
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"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",
"ecc073fe6e31": {
"8b09f9d581f7": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
@@ -347,6 +340,9 @@
"$rpc": "undefined"
},
"checksStatus": "pending",
"checksSummary": {
"$rpc": "undefined"
},
"confirmedContainedHeadOid": {
"$rpc": "undefined"
},
@@ -388,7 +384,17 @@
},
"kind": "ready"
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"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"
},
"recording": {
"scenario": "pr-sidebar-checks-refused",
@@ -423,9 +429,9 @@
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "54d7c17a92f3"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "ecc073fe6e31"
"load": "8b09f9d581f7"
},
"state": "0d6e25838291",
"state": "5b9634cf47d1",
"effects": ["15c9074079de"]
}
}
+156 -150
View File
@@ -116,77 +116,6 @@
}
}
},
"2b42882f6c21": {
"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",
"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"
},
"4777f063108d": {
"name": "github.prForBranch#1",
"ordinal": 6,
@@ -259,82 +188,6 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo-9::/w\"}}"
},
"6a8e42e4e43a": {
"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",
"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,
@@ -418,12 +271,165 @@
}
}
},
"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"
"e169ebaf642b": "unloaded",
"e7df27480ce9": {
"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"
}
},
"recording": {
"scenario": "pr-sidebar-load",
@@ -458,9 +464,9 @@
"sender": ["07639c4f1e7f", "7cbf1de78fc1", "1c1552d03dfc", "9a7a01989014"],
"payloads": ["4b26820600b0", "5dbd52d6a1c7", "0e35af4e1fa3", "dcec20ae0055"],
"settlements": {
"load": "6a8e42e4e43a"
"load": "cfed3ed2a0ae"
},
"state": "2b42882f6c21",
"state": "e7df27480ce9",
"effects": ["15c9074079de"]
}
}
@@ -4,83 +4,14 @@
"family": "tasks.item-detail-gitlab",
"namedDeltas": [],
"values": {
"16125975286b": {
"name": "detailLoading",
"ordinal": 3,
"value": false
},
"185dd46f0a38": {
"name": "detailLoading",
"ordinal": 8,
"value": true
},
"18a362320fb7": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"20f8d00e8abe": {
"name": "detailLoading",
"ordinal": 12,
"value": false
},
"274b8ecb4f73": {
"name": "actionItem",
"ordinal": 6,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"id": "gitlab:issue:4",
"labels": ["bug"],
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"6d1464c64f13": {
"12958fc227ba": {
"error": "",
"item": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -111,6 +42,7 @@
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -161,6 +93,77 @@
"provider": "gitlab"
}
},
"16125975286b": {
"name": "detailLoading",
"ordinal": 3,
"value": false
},
"185dd46f0a38": {
"name": "detailLoading",
"ordinal": 8,
"value": true
},
"20f8d00e8abe": {
"name": "detailLoading",
"ordinal": 12,
"value": false
},
"274b8ecb4f73": {
"name": "actionItem",
"ordinal": 6,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"id": "gitlab:issue:4",
"labels": ["bug"],
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"5c69a0ab4415": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"72c0fc81a418": {
"name": "detailPayload",
"ordinal": 4,
@@ -284,7 +287,15 @@
"$rpc": "null"
}
},
"d759afce345e": {
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
},
"f9f4fedb976a": {
"name": "items",
"ordinal": 10,
"value": [
@@ -293,6 +304,7 @@
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -318,14 +330,6 @@
"title": "A GitLab issue"
}
]
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
}
},
"recording": {
@@ -339,7 +343,7 @@
"settlements": {
"mount": "eb79a9b3682a"
},
"state": "6d1464c64f13",
"state": "12958fc227ba",
"effects": [
"cc2ece9478f0",
"a4f1a8696a24",
@@ -348,8 +352,8 @@
"988fd3d6f1c4",
"274b8ecb4f73",
"185dd46f0a38",
"d759afce345e",
"18a362320fb7",
"f9f4fedb976a",
"5c69a0ab4415",
"20f8d00e8abe",
"b4d8ed508667"
]
@@ -66,39 +66,6 @@
"ordinal": 8,
"value": true
},
"18a362320fb7": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"20f8d00e8abe": {
"name": "detailLoading",
"ordinal": 12,
@@ -138,30 +105,14 @@
"provider": "gitlab"
}
},
"72c0fc81a418": {
"name": "detailPayload",
"ordinal": 4,
"value": {
"$rpc": "null"
}
},
"8ac545c58ec2": {
"name": "gitlab.workItemDetails#1",
"ordinal": 9,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemDetails\",\"params\":{\"repo\":\"id:repo-1\",\"iid\":4,\"type\":\"issue\",\"projectRef\":{\"host\":\"gitlab.com\",\"path\":\"group/project\"}}}"
},
"988fd3d6f1c4": {
"name": "detailRefreshSeq",
"ordinal": 5,
"value": 0
},
"9fa102187add": {
"4838d50cb348": {
"error": "",
"item": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -192,6 +143,7 @@
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -227,6 +179,57 @@
"provider": "gitlab"
}
},
"5c69a0ab4415": {
"name": "actionItem",
"ordinal": 11,
"value": {
"key": "gitlab:repo-1:issue:4",
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
"pending": 0,
"state": "none",
"total": 0
},
"id": "gitlab:issue:4",
"labels": ["bug"],
"mergeable": "MERGEABLE",
"number": 4,
"projectRef": {
"host": "gitlab.com",
"path": "group/project"
},
"repoId": "repo-1",
"reviewDecision": "approved",
"reviewerCount": 0,
"state": "opened",
"type": "issue",
"url": "https://gitlab.com/group/project/-/issues/4"
},
"title": "A GitLab issue"
}
},
"72c0fc81a418": {
"name": "detailPayload",
"ordinal": 4,
"value": {
"$rpc": "null"
}
},
"8ac545c58ec2": {
"name": "gitlab.workItemDetails#1",
"ordinal": 9,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemDetails\",\"params\":{\"repo\":\"id:repo-1\",\"iid\":4,\"type\":\"issue\",\"projectRef\":{\"host\":\"gitlab.com\",\"path\":\"group/project\"}}}"
},
"988fd3d6f1c4": {
"name": "detailRefreshSeq",
"ordinal": 5,
"value": 0
},
"a4f1a8696a24": {
"name": "detailError",
"ordinal": 2,
@@ -239,7 +242,15 @@
"$rpc": "null"
}
},
"d759afce345e": {
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
},
"f9f4fedb976a": {
"name": "items",
"ordinal": 10,
"value": [
@@ -248,6 +259,7 @@
"provider": "gitlab",
"source": {
"checksSummary": {
"cancelled": 0,
"failed": 0,
"neutral": 0,
"passed": 0,
@@ -273,14 +285,6 @@
"title": "A GitLab issue"
}
]
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
}
},
"recording": {
@@ -294,7 +298,7 @@
"settlements": {
"mount": "eb79a9b3682a"
},
"state": "9fa102187add",
"state": "4838d50cb348",
"effects": [
"cc2ece9478f0",
"a4f1a8696a24",
@@ -303,8 +307,8 @@
"988fd3d6f1c4",
"274b8ecb4f73",
"185dd46f0a38",
"d759afce345e",
"18a362320fb7",
"f9f4fedb976a",
"5c69a0ab4415",
"20f8d00e8abe",
"3d683d8ca027"
]
@@ -0,0 +1,158 @@
import { createElement, type ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { PRCheckRunDetails } from '../../../../src/shared/github/check-types'
import { activateMobileUiLocale, translate } from '../../i18n/mobile-i18n'
import { colors } from '../../theme/mobile-theme'
import { PrCheckFailureRow } from './PrCheckFailureRow'
vi.mock('react-native', () => {
const Primitive = ({
children,
style
}: {
children?: ReactNode
style?: object | (object | false)[]
}) => {
const flat = Object.assign({}, ...[style].flat())
return (
<div data-color={flat.color} data-border-color={flat.borderLeftColor}>
{children}
</div>
)
}
return {
ActivityIndicator: () => null,
Pressable: Primitive,
Text: Primitive,
View: Primitive,
StyleSheet: { create: <T,>(styles: T) => styles }
}
})
vi.mock('lucide-react-native', () => ({
CircleX: ({ color }: { color: string }) => <i data-check-color={color} />,
Copy: 'i',
ExternalLink: 'i',
MoreHorizontal: 'i',
RotateCw: 'i',
Sparkles: 'i',
SquareTerminal: 'i'
}))
vi.mock('../../i18n/use-mobile-translation', () => ({ useMobileTranslation: () => translate }))
vi.mock('../../theme/use-mobile-color-scheme', () => ({ useMobileColorScheme: () => 'dark' }))
vi.mock('../../platform/clipboard', () => ({ useClipboardWriter: () => ({ writeText: vi.fn() }) }))
vi.mock('../../platform/external-link', () => ({ openExternalLink: vi.fn() }))
vi.mock('../../platform/haptics', () => ({ triggerError: vi.fn(), triggerSuccess: vi.fn() }))
vi.mock('../list/ListGroup', () => ({ LIST_LEADING_SLOT: 20, LIST_ROW_PADDING: 16 }))
vi.mock('../nav-bar/PullDownMenu', () => ({ PullDownMenu: () => null }))
vi.mock('./PrMiniButton', () => ({ PrMiniButton: () => null }))
vi.mock('./PRCheckDetail', () => ({ PRCheckDetailView: () => null }))
describe('expanded check diagnostic tone', () => {
afterEach(async () => {
await activateMobileUiLocale('en')
})
it.each([
{ conclusion: 'cancelled', diagnostic: 'title', locale: 'en' },
{ conclusion: 'cancelled', diagnostic: 'title', locale: 'de' },
{ conclusion: 'cancelled', diagnostic: 'notice', locale: 'en' },
{ conclusion: 'cancelled', diagnostic: 'failureAnnotation', locale: 'en' },
{ conclusion: 'cancelled', diagnostic: 'cancelledStep', locale: 'en' },
{ conclusion: 'cancelled', diagnostic: 'failureStep', locale: 'en' },
{ conclusion: 'failure', diagnostic: 'title', locale: 'en' },
{ conclusion: 'failure', diagnostic: 'failureAnnotation', locale: 'en' }
] as const)(
'$conclusion with $diagnostic in $locale preserves the diagnostic meaning',
async ({ conclusion, diagnostic, locale }) => {
await activateMobileUiLocale(locale)
const hasAnnotation = diagnostic === 'notice' || diagnostic === 'failureAnnotation'
const hasStep = diagnostic === 'cancelledStep' || diagnostic === 'failureStep'
const details: PRCheckRunDetails = {
name: 'Build',
status: 'completed',
conclusion,
url: null,
detailsUrl: null,
startedAt: null,
completedAt: null,
title: 'Workflow ended',
summary: null,
text: null,
annotations: hasAnnotation
? [
{
path: 'build.ts',
startLine: 3,
endLine: 3,
annotationLevel: diagnostic === 'failureAnnotation' ? 'failure' : 'notice',
title: 'Build diagnostic',
message: 'Diagnostic content',
rawDetails: null
}
]
: [],
jobs: hasStep
? [
{
id: 1,
name: 'Build',
status: 'completed',
conclusion,
startedAt: null,
completedAt: null,
url: null,
logTail: null,
steps: [
{
name: 'Build step',
status: 'completed',
conclusion: diagnostic === 'failureStep' ? 'failure' : 'cancelled',
startedAt: null,
completedAt: null
}
]
}
]
: []
}
const markup = renderToStaticMarkup(
createElement(PrCheckFailureRow, {
check: { name: details.name, status: 'completed', conclusion, url: null },
entry: { status: 'loaded', details },
initiallyOpen: true,
onOpen: vi.fn(),
actionable: false,
fixLabel: 'Fix',
onFix: vi.fn(),
fixBusy: false,
fixAvailable: true,
onRerun: vi.fn(),
rerunBusy: false
})
)
const tone = conclusion === 'cancelled' ? colors.textSecondary : colors.statusRed
expect(markup).toContain(`data-border-color="${tone}"`)
expect(markup).toContain(`data-check-color="${tone}"`)
expect(markup).toContain(conclusion === 'cancelled' ? 'Cancelled' : 'Failed')
const displayed = hasAnnotation
? 'Diagnostic content'
: hasStep
? 'Build step'
: 'Workflow ended'
expect(markup).toContain(displayed)
if (hasAnnotation || hasStep) {
const realFailure = diagnostic === 'failureAnnotation' || diagnostic === 'failureStep'
const diagnosticColor = realFailure
? colors.statusRed
: hasAnnotation
? colors.textMuted
: colors.textPrimary
expect(markup).toContain(`data-color="${diagnosticColor}">${displayed}</div>`)
}
if (hasAnnotation) {
expect(markup).toContain('build.ts:3')
}
}
)
})
@@ -62,7 +62,9 @@ export function PrCheckFailureRow({
const details = entry?.status === 'loaded' ? entry.details : null
const content = details ? presentCheckDetail(details) : null
const annotation = content?.annotations[0] ?? null
const fallbackLine = content?.jobs[0]?.failedSteps[0]?.name ?? content?.summaryLines[1] ?? null
const step = content?.jobs[0]?.failedSteps[0] ?? null
const fallbackLine = step?.name ?? content?.summaryLines[1] ?? null
const isCancelled = check.conclusion === 'cancelled'
const errorText = annotation
? [annotation.locator, annotation.title, annotation.message].filter(Boolean).join('\n')
: fallbackLine
@@ -128,16 +130,22 @@ export function PrCheckFailureRow({
accessibilityLabel={check.name}
>
<View style={styles.leading}>
<CircleX size={20} color={colors.statusRed} strokeWidth={2} />
<CircleX
size={20}
color={isCancelled ? colors.textSecondary : colors.statusRed}
strokeWidth={2}
/>
</View>
<View style={styles.titles}>
<Text style={styles.title} numberOfLines={1}>
{check.name}
</Text>
<Text style={styles.subtitle} numberOfLines={1}>
{duration
? t('pullRequest.screens.failedAfter', 'Failed after {{duration}}', { duration })
: t('pullRequest.screens.failed', 'Failed')}
{isCancelled
? t('pullRequest.checkStatus.cancelled', 'Cancelled')
: duration
? t('pullRequest.screens.failedAfter', 'Failed after {{duration}}', { duration })
: t('pullRequest.screens.failed', 'Failed')}
</Text>
</View>
</Pressable>
@@ -161,18 +169,33 @@ export function PrCheckFailureRow({
) : entry.status === 'error' ? (
<Text style={styles.muted}>{entry.message}</Text>
) : annotation ? (
<View style={styles.error}>
<View style={[styles.error, isCancelled && styles.cancelledDetail]}>
<Text style={styles.codeMuted} numberOfLines={1}>
{annotation.locator}
</Text>
{annotation.title ? <Text style={styles.codeText}>{annotation.title}</Text> : null}
<Text style={styles.codeError} numberOfLines={6}>
<Text
style={
isCancelled && annotation.level !== 'failure'
? styles.codeMuted
: styles.codeError
}
numberOfLines={6}
>
{annotation.message}
</Text>
</View>
) : fallbackLine ? (
<View style={styles.error}>
<Text style={styles.codeText}>{fallbackLine}</Text>
<View style={[styles.error, isCancelled && styles.cancelledDetail]}>
<Text
style={
isCancelled && step && step.state !== 'cancelled'
? styles.codeError
: styles.codeText
}
>
{fallbackLine}
</Text>
</View>
) : null}
{actionable ? (
@@ -266,6 +289,7 @@ const styles = themedStyleSheet(() => ({
paddingVertical: 2,
gap: 2
},
cancelledDetail: { borderLeftColor: colors.textSecondary },
codeMuted: {
color: colors.textMuted,
fontFamily: typography.monoFamily,
@@ -0,0 +1,94 @@
import { createElement, type ReactNode } from 'react'
import type * as ReactModule from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import type { ScreenProps } from './pr-tab-screen-frame'
const observed = vi.hoisted((): { rows: { check: PRCheckDetail; initiallyOpen: boolean }[] } => ({
rows: []
}))
vi.mock('react', async (original) => ({
...(await original<typeof ReactModule>()),
useEffect: (effect: () => void) => effect()
}))
vi.mock('react-native', () => ({
View: ({ children }: { children: ReactNode }) => <div>{children}</div>,
ScrollView: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Text: ({ children }: { children: ReactNode }) => <span>{children}</span>,
StyleSheet: { create: <T,>(styles: T) => styles }
}))
vi.mock('lucide-react-native', () => ({
ChevronDown: 'i',
CircleCheck: 'i',
CircleMinus: 'i',
LoaderCircle: 'i'
}))
vi.mock('../../theme/use-mobile-color-scheme', () => ({ useMobileColorScheme: () => 'dark' }))
vi.mock('../../i18n/use-mobile-translation', async () => ({
useMobileTranslation: () => (key: string, fallback: string) => fallback
}))
vi.mock('../../platform/clipboard', () => ({ useClipboardWriter: () => ({ writeText: vi.fn() }) }))
vi.mock('../../platform/external-link', () => ({ openExternalLink: vi.fn() }))
vi.mock('../../session/pr-ai-triage-prompt', () => ({ buildPrFixChecksPrompt: vi.fn() }))
vi.mock('../../session/use-mobile-launch-agent-label', () => ({
useMobileLaunchAgentLabel: () => null
}))
vi.mock('../host-protocol-gates-context', () => ({
useHostProtocolGates: () => ({ hostCapabilities: {} })
}))
vi.mock('../DisclosureChevron', () => ({ DisclosureChevron: () => null }))
vi.mock('../list/ListGroup', () => ({
LIST_LEADING_INSET: 0,
ListGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
SectionHeader: () => null,
ListRow: () => null
}))
vi.mock('./pr-tab-screen-frame', () => ({
ScreenFrame: ({ children }: { children: ReactNode }) => <div>{children}</div>,
NotReady: () => null
}))
vi.mock('./use-mobile-pr-tab', () => ({ readyPrData: (tab: { data: unknown }) => tab.data }))
vi.mock('./PrCheckFailureRow', () => ({
PrCheckFailureRow: (props: (typeof observed.rows)[number]) => {
observed.rows.push(props)
return null
}
}))
import { PrChecksScreen } from './PrChecksScreen'
describe('PrChecksScreen cancellation sections', () => {
it.each([1, 3])(
'shares one notice and three detail reads across %i failures plus cancellation',
(failures) => {
observed.rows = []
const ensure = vi.fn()
const checks: PRCheckDetail[] = Array.from({ length: failures + 1 }, (_, index) => ({
name: `job-${index}`,
status: 'completed',
conclusion: index < failures ? 'failure' : 'cancelled',
url: null
}))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: mocked dependencies consume only the screen fields supplied by this fixture.
const tab = {
data: { pr: { number: 7 }, checks },
checkDetails: {},
ensureCheckDetail: ensure,
actions: { error: 'Retry failed', isBusy: () => false },
triage: {
availability: 'available',
isBusy: () => false,
noticeFor: () => ({ error: 'Agent launch failed' })
}
} as unknown as ScreenProps['tab']
const markup = renderToStaticMarkup(createElement(PrChecksScreen, { tab, onBack: () => {} }))
expect(markup.split('Retry failed')).toHaveLength(2)
expect(markup.split('Agent launch failed')).toHaveLength(2)
const opened = observed.rows.filter((row) => row.initiallyOpen).map((row) => row.check.name)
expect(opened).toEqual(checks.slice(0, 3).map((check) => check.name))
expect(ensure.mock.calls.map(([check]) => check.name)).toEqual(opened)
expect(observed.rows.at(-1)?.initiallyOpen).toBe(failures < 3)
}
)
})
@@ -34,13 +34,14 @@ export function PrChecksScreen({ tab, onBack, bottomInset = 0 }: ScreenProps) {
() => checks.filter((check) => checkOutcome(check) === 'failure'),
[checks]
)
const cancelled = checks.filter((check) => checkOutcome(check) === 'cancelled')
const { hostCapabilities } = useHostProtocolGates()
const fixAgent = useMobileLaunchAgentLabel({
client: tab.client,
worktreeId: tab.worktreeId,
hostCapabilities,
actionId: 'fixChecks',
enabled: failing.length > 0
enabled: failing.length + cancelled.length > 0
})
const fixLabel = fixAgent
? t('pullRequest.screens.fixWith', 'Fix with {{agent}}', { agent: fixAgent })
@@ -50,10 +51,20 @@ export function PrChecksScreen({ tab, onBack, bottomInset = 0 }: ScreenProps) {
const other = checks.filter((check) => checkOutcome(check) === 'neutral')
const [showPassed, setShowPassed] = useState(false)
// Open and prefetch only the first few failures; a wide matrix would otherwise fan out one host read per job.
// One bounded identity set owns both initial expansion and its detail reads across sections.
const initialDetails = useMemo(
() =>
new Map(
checks
.filter((check) => ['failure', 'cancelled'].includes(checkOutcome(check)))
.slice(0, PREFETCHED_FAILURES)
.map((check) => [prCheckKey(check), check])
),
[checks]
)
useEffect(() => {
failing.slice(0, PREFETCHED_FAILURES).forEach(ensure)
}, [ensure, failing])
initialDetails.forEach(ensure)
}, [ensure, initialDetails])
const fixChecks = (target: PRCheckDetail[]) => {
if (data) {
@@ -77,6 +88,7 @@ export function PrChecksScreen({ tab, onBack, bottomInset = 0 }: ScreenProps) {
<ScrollView contentContainerStyle={{ paddingBottom: bottomInset + spacing.lg }}>
<ChecksSummary
failing={failing.length}
cancelled={cancelled.length}
running={running.length}
passed={passed.length}
live={tab.polling}
@@ -86,38 +98,45 @@ export function PrChecksScreen({ tab, onBack, bottomInset = 0 }: ScreenProps) {
{data.checks.length === 0 && !data.checksError ? (
<Text style={styles.muted}>{t('pullRequest.checksSection.noChecks', 'No checks')}</Text>
) : null}
{failing.length > 0 ? (
<View>
<SectionHeader title={t('pullRequest.screens.failing', 'Failing')} />
<ListGroup style={styles.plain} separatorInset={LIST_LEADING_INSET}>
{failing.map((check, index) => (
<PrCheckFailureRow
key={prCheckKey(check)}
initiallyOpen={index < PREFETCHED_FAILURES}
onOpen={() => ensure(check)}
check={check}
entry={cache[prCheckKey(check)]}
actionable={isBrokenCheck(check)}
fixLabel={fixLabel}
onFix={() => fixChecks([check])}
fixBusy={tab.triage.isBusy('fix-checks')}
fixAvailable={tab.triage.availability === 'available'}
onRerun={tab.actions.rerunFailingChecks}
rerunBusy={tab.actions.isBusy({ kind: 'rerun' })}
/>
))}
</ListGroup>
<View style={styles.notes}>
<AgentLaunchNotice
availability={tab.triage.availability}
success={notice.success}
error={notice.error}
warning={notice.warning}
undeliveredPrompt={notice.undeliveredPrompt}
errorStyle={styles.error}
/>
{tab.actions.error ? <Text style={styles.error}>{tab.actions.error}</Text> : null}
{[
{ label: t('pullRequest.screens.failing', 'Failing'), checks: failing },
{ label: t('pullRequest.checkStatus.cancelled', 'Cancelled'), checks: cancelled }
]
.filter((section) => section.checks.length > 0)
.map((section) => (
<View key={section.label}>
<SectionHeader title={section.label} />
<ListGroup style={styles.plain} separatorInset={LIST_LEADING_INSET}>
{section.checks.map((check) => (
<PrCheckFailureRow
key={prCheckKey(check)}
initiallyOpen={initialDetails.has(prCheckKey(check))}
onOpen={() => ensure(check)}
check={check}
entry={cache[prCheckKey(check)]}
actionable={isBrokenCheck(check)}
fixLabel={fixLabel}
onFix={() => fixChecks([check])}
fixBusy={tab.triage.isBusy('fix-checks')}
fixAvailable={tab.triage.availability === 'available'}
onRerun={tab.actions.rerunFailingChecks}
rerunBusy={tab.actions.isBusy({ kind: 'rerun' })}
/>
))}
</ListGroup>
</View>
))}
{failing.length + cancelled.length > 0 ? (
<View style={styles.notes}>
<AgentLaunchNotice
availability={tab.triage.availability}
success={notice.success}
error={notice.error}
warning={notice.warning}
undeliveredPrompt={notice.undeliveredPrompt}
errorStyle={styles.error}
/>
{tab.actions.error ? <Text style={styles.error}>{tab.actions.error}</Text> : null}
</View>
) : null}
{running.length > 0 ? (
@@ -195,12 +214,14 @@ function foldedNames(checks: PRCheckDetail[], t: Translate): string {
// A bar split by outcome, then the counts in words, so the state reads before any row.
function ChecksSummary({
failing,
cancelled,
running,
passed,
live,
t
}: {
failing: number
cancelled: number
running: number
passed: number
// The screen polls while it is open, so it says so instead of offering a refresh button.
@@ -208,10 +229,11 @@ function ChecksSummary({
t: Translate
}) {
useMobileColorScheme()
if (failing + running + passed === 0) {
if (failing + cancelled + running + passed === 0) {
return null
}
const rest = [
cancelled > 0 ? `${cancelled} ${t('pullRequest.checkStatus.cancelled', 'Cancelled')}` : null,
running > 0 ? t('pullRequest.screens.running', '{{count}} running', { count: running }) : null,
passed > 0 ? t('pullRequest.checksSection.passed', '{{count}} passed', { count: passed }) : null
].filter(Boolean)
@@ -221,6 +243,9 @@ function ChecksSummary({
{failing > 0 ? (
<View style={[styles.segment, styles.segmentFailing, { flex: failing }]} />
) : null}
{cancelled > 0 ? (
<View style={[styles.segment, styles.segmentCancelled, { flex: cancelled }]} />
) : null}
{running > 0 ? (
<View style={[styles.segment, styles.segmentRunning, { flex: running }]} />
) : null}
@@ -294,6 +319,7 @@ const styles = themedStyleSheet(() => ({
segment: { borderRadius: radii.pill },
segmentFailing: { backgroundColor: colors.statusRed },
segmentRunning: { backgroundColor: colors.statusAmber },
segmentCancelled: { backgroundColor: colors.textMuted },
segmentPassed: { backgroundColor: colors.statusGreen },
counts: { flexShrink: 1, color: colors.textSecondary, fontSize: typography.metaSize },
countsFailing: { color: colors.statusRed, fontWeight: '600' },
@@ -1,3 +1,4 @@
import { summarizeProviderChecks } from '../../../../src/shared/provider-check-summary'
import { describe, expect, it } from 'vitest'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
import { presentPrMerge } from './pr-actions-state'
@@ -52,4 +53,26 @@ describe('presentPrMerge', () => {
hint: '#201 is still a draft.'
})
})
it('keeps the provider-authorized merge action available with cancelled checks', () => {
const checksSummary = summarizeProviderChecks([
{ status: 'completed', conclusion: 'success' },
{ status: 'completed', conclusion: 'cancelled' }
])
expect(
presentPrMerge(
{
...pr,
mergeable: 'MERGEABLE',
mergeStateStatus: 'CLEAN',
checksStatus: 'failure',
checksSummary
},
'squash'
)
).toMatchObject({
label: 'Squash and merge',
hint: null,
enabled: true
})
})
})
@@ -0,0 +1,25 @@
import { expect, it } from 'vitest'
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import { summarizePRChecks } from './pr-checks-presentation'
function check(over: Partial<PRCheckDetail>): PRCheckDetail {
return {
name: 'ci',
status: 'completed',
conclusion: 'success',
url: null,
...over
}
}
it('presents cancellations without hiding real failure or pending checks', () => {
const cancelled = check({ conclusion: 'cancelled' })
expect(summarizePRChecks([cancelled, check({})])).toMatchObject({
outcome: 'cancelled',
failed: 0,
cancelled: 1,
label: '1 Cancelled · 1 passed'
})
expect(summarizePRChecks([cancelled, check({ conclusion: 'pending' })]).outcome).toBe('pending')
expect(summarizePRChecks([cancelled, check({ conclusion: 'failure' })]).outcome).toBe('failure')
})
@@ -1,12 +1,8 @@
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import type {
PRState,
ProviderCheckSummary
} from '../../../../src/shared/github/pull-request-types'
import type { PRState } from '../../../../src/shared/github/pull-request-types'
import {
classifyCheckOutcome,
summarizeProviderChecks,
type CheckOutcome as SharedCheckOutcome
getProviderChecksOutcome,
summarizeProviderChecks
} from '../../../../src/shared/provider-check-summary'
import { prStateToken } from '../pr-state-token'
import { translate } from '../../i18n/mobile-i18n'
@@ -22,17 +18,10 @@ export type MobileStatusToken =
| 'textSecondary'
export type CheckOutcome = 'success' | 'pending' | 'failure' | 'neutral'
type CheckPresentationOutcome = CheckOutcome | 'cancelled'
const OUTCOME_BY_SHARED: Record<SharedCheckOutcome, CheckOutcome> = {
passed: 'success',
failed: 'failure',
pending: 'pending',
neutral: 'neutral'
}
// Share desktop classifier: duplicate logic misclassified skipped as unresolved and action_required as pending.
export function checkOutcome(check: PRCheckDetail): CheckOutcome {
return OUTCOME_BY_SHARED[classifyCheckOutcome(check)]
export function checkOutcome(check: PRCheckDetail): CheckPresentationOutcome | 'none' {
return getProviderChecksOutcome(summarizeProviderChecks([check]))
}
export type PRChecksSummary = {
@@ -40,18 +29,11 @@ export type PRChecksSummary = {
passed: number
pending: number
failed: number
outcome: CheckOutcome | 'none'
cancelled: number
outcome: CheckPresentationOutcome | 'none'
label: string
}
const OUTCOME_BY_STATE: Record<ProviderCheckSummary['state'], CheckOutcome | 'none'> = {
success: 'success',
failure: 'failure',
pending: 'pending',
neutral: 'neutral',
none: 'none'
}
export function summarizePRChecks(checks: readonly PRCheckDetail[]): PRChecksSummary {
if (checks.length === 0) {
return {
@@ -59,13 +41,17 @@ export function summarizePRChecks(checks: readonly PRCheckDetail[]): PRChecksSum
passed: 0,
pending: 0,
failed: 0,
cancelled: 0,
outcome: 'none',
label: translate('pullRequest.prChecksPresentation.noChecks', 'No checks')
}
}
// Counts and the worst-case rollup come from the shared summarizer; only the label wording is mobile's.
const { total, passed, pending, failed, neutral, state } = summarizeProviderChecks(checks)
const outcome = OUTCOME_BY_STATE[state]
const summary = summarizeProviderChecks(checks)
const { total, passed, pending, neutral } = summary
const cancelled = summary.cancelled ?? 0
const failed = summary.failed - cancelled
const outcome = getProviderChecksOutcome(summary)
const parts: string[] = []
if (failed > 0) {
const count = failed
@@ -77,6 +63,9 @@ export function summarizePRChecks(checks: readonly PRCheckDetail[]): PRChecksSum
})
)
}
if (cancelled > 0) {
parts.push(`${cancelled} ${translate('pullRequest.checkStatus.cancelled', 'Cancelled')}`)
}
if (pending > 0) {
const count = pending
parts.push(
@@ -110,6 +99,7 @@ export function summarizePRChecks(checks: readonly PRCheckDetail[]): PRChecksSum
passed,
pending,
failed,
cancelled,
outcome,
label: parts.join(' · ')
}
@@ -122,7 +112,7 @@ export function prChecksSummaryLabel(summary: PRChecksSummary, checksError: stri
: translate('pullRequest.prChecksPresentation.unavailable', 'Checks unavailable')
}
export function checkOutcomeToken(outcome: CheckOutcome | 'none'): MobileStatusToken {
export function checkOutcomeToken(outcome: CheckPresentationOutcome | 'none'): MobileStatusToken {
switch (outcome) {
case 'success':
return 'statusGreen'
@@ -88,6 +88,14 @@ function checksGate(checks: PRCheckDetail[], checksError: string | null): PrGate
: null
}
}
if (summary.outcome === 'cancelled') {
return {
...base,
tone: 'idle',
title: translate('pullRequest.checkStatus.cancelled', 'Cancelled'),
subtitle: summary.label
}
}
if (summary.total === 0) {
return {
...base,
@@ -1,3 +1,4 @@
import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary'
import { createElement } from 'react'
import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
@@ -128,3 +129,17 @@ describe('the long-press details sheet', () => {
expect(texts(render({ cliCreated: true }).root)[2]).toBe('Created from Orca CLI')
})
})
it('renders a cancelled branch PR without calling it failing', () => {
render({
linkedPR: {
number: 4,
state: 'open',
checksStatus: 'failure',
checksSummary: summarizeProviderChecks([{ status: 'completed', conclusion: 'cancelled' }])
}
})
const text = JSON.stringify(renderer?.toJSON())
expect(text).toContain('Cancelled')
expect(text).not.toContain('Checks: Failing')
})
@@ -58,7 +58,8 @@ function checkLabels(t: Translate): Record<string, string> {
return {
success: t('hostScreen.workspaceDetailsSheet.checksPassing', 'Checks: Passing'),
failure: t('hostScreen.workspaceDetailsSheet.checksFailing', 'Checks: Failing'),
pending: t('hostScreen.workspaceDetailsSheet.checksPending', 'Checks: Pending')
pending: t('hostScreen.workspaceDetailsSheet.checksPending', 'Checks: Pending'),
cancelled: t('pullRequest.checkStatus.cancelled', 'Cancelled')
}
}
-2
View File
@@ -3222,7 +3222,6 @@
"pipelineJobs_one": "{{count}} Job",
"pipelineJobs_other": "{{count}} Jobs",
"rerunAll": "Alle erneut ausführen",
"rerunFailed": "Fehlgeschlagene erneut ausführen",
"subIssueTitle": "Titel des Unter-Issues",
"subIssues": "Untergeordnete Issues"
},
@@ -3453,7 +3452,6 @@
"requestReview": "Review anfordern",
"requestReviewers": "Prüfer anfordern",
"rerunAll": "Alle erneut ausführen",
"rerunFailed": "Fehlgeschlagene erneut ausführen",
"reviewers": "Prüfer",
"viewFile": "Anzeigen"
},
+5 -3
View File
@@ -1681,6 +1681,9 @@
"conflictsChecking": "Checking for conflicts",
"noConflictsWithBase": "No conflicts with {{base}}",
"noConflicts": "No conflicts"
},
"checkStatus": {
"cancelled": "Cancelled"
}
},
"workspace": {
@@ -3818,7 +3821,6 @@
"pipelineJobs_other": "{{count}} jobs",
"description": "Description",
"checks": "Checks",
"rerunFailed": "Rerun failed",
"rerunAll": "Rerun all",
"noChecks": "No checks found.",
"pipeline": "Pipeline",
@@ -4018,7 +4020,6 @@
"requestReviewers": "Request reviewers",
"requestReview": "Request review",
"checks": "Checks",
"rerunFailed": "Rerun failed",
"rerunAll": "Rerun all",
"noChecks": "No checks found.",
"changedFiles": "Changed files",
@@ -4270,7 +4271,8 @@
},
"runtimeHydration": {
"loadFailed": "Failed to load Tasks settings"
}
},
"rerunUnsuccessful": "Retry checks"
},
"terminal": {
"webView": {
-2
View File
@@ -3311,7 +3311,6 @@
"pipelineJobs_one": "{{count}} trabajo",
"pipelineJobs_other": "{{count}} trabajos",
"rerunAll": "Reejecutar todos",
"rerunFailed": "Reejecutar fallidos",
"subIssueTitle": "Título del sub-issue",
"subIssues": "Sub-issues"
},
@@ -3545,7 +3544,6 @@
"requestReview": "Solicitar revisión",
"requestReviewers": "Solicitar revisores",
"rerunAll": "Reejecutar todos",
"rerunFailed": "Reejecutar fallidos",
"reviewers": "Revisores",
"viewFile": "Ver"
},
-2
View File
@@ -3311,7 +3311,6 @@
"pipelineJobs_one": "{{count}} job",
"pipelineJobs_other": "{{count}} jobs",
"rerunAll": "Tout relancer",
"rerunFailed": "Relancer les vérifications échouées",
"subIssueTitle": "Titre du sous-ticket",
"subIssues": "Sous-tickets"
},
@@ -3545,7 +3544,6 @@
"requestReview": "Demander une revue",
"requestReviewers": "Demander des reviewers",
"rerunAll": "Tout relancer",
"rerunFailed": "Relancer celles en échec",
"reviewers": "Reviewers",
"viewFile": "Afficher"
},
-2
View File
@@ -3218,7 +3218,6 @@
"pipelineJobs_one": "{{count}} 件のジョブ",
"pipelineJobs_other": "{{count}} 件のジョブ",
"rerunAll": "すべて再実行",
"rerunFailed": "失敗分を再実行",
"subIssueTitle": "サブ Issue のタイトル",
"subIssues": "サブ Issue"
},
@@ -3449,7 +3448,6 @@
"requestReview": "レビューを依頼",
"requestReviewers": "レビューの依頼先",
"rerunAll": "すべて再実行",
"rerunFailed": "失敗分を再実行",
"reviewers": "レビュアー",
"viewFile": "表示"
},
-2
View File
@@ -3218,7 +3218,6 @@
"pipelineJobs_one": "작업 {{count}}개",
"pipelineJobs_other": "작업 {{count}}개",
"rerunAll": "모두 재실행",
"rerunFailed": "실패한 체크 재실행",
"subIssueTitle": "하위 이슈 제목",
"subIssues": "하위 이슈"
},
@@ -3449,7 +3448,6 @@
"requestReview": "리뷰 요청",
"requestReviewers": "리뷰어 요청",
"rerunAll": "모두 재실행",
"rerunFailed": "실패한 체크 재실행",
"reviewers": "리뷰어",
"viewFile": "보기"
},
-2
View File
@@ -3311,7 +3311,6 @@
"pipelineJobs_one": "{{count}} job",
"pipelineJobs_other": "{{count}} jobs",
"rerunAll": "Reexecutar todas",
"rerunFailed": "Reexecutar as que falharam",
"subIssueTitle": "Título da sub-issue",
"subIssues": "Sub-issues"
},
@@ -3545,7 +3544,6 @@
"requestReview": "Solicitar revisão",
"requestReviewers": "Solicitar revisores",
"rerunAll": "Reexecutar todas",
"rerunFailed": "Reexecutar as que falharam",
"reviewers": "Revisores",
"viewFile": "Ver"
},
-2
View File
@@ -3400,7 +3400,6 @@
"pipelineJobs_one": "{{count}} задание",
"pipelineJobs_other": "{{count}} задания",
"rerunAll": "Повторить все",
"rerunFailed": "Повторить неуспешные",
"subIssueTitle": "Название подзадачи",
"subIssues": "Подзадачи"
},
@@ -3637,7 +3636,6 @@
"requestReview": "Запросить проверку",
"requestReviewers": "Укажите рецензентов",
"rerunAll": "Перезапустить все",
"rerunFailed": "Перезапустить неудачные",
"reviewers": "Рецензенты",
"viewFile": "Просмотреть"
},
-2
View File
@@ -3222,7 +3222,6 @@
"pipelineJobs_one": "{{count}} iş",
"pipelineJobs_other": "{{count}} iş",
"rerunAll": "Tümünü yeniden çalıştır",
"rerunFailed": "Başarısızları yeniden çalıştır",
"subIssueTitle": "Alt sorun başlığı",
"subIssues": "Alt sorunlar"
},
@@ -3453,7 +3452,6 @@
"requestReview": "İnceleme iste",
"requestReviewers": "İnceleme istenecek kişiler",
"rerunAll": "Tümünü yeniden çalıştır",
"rerunFailed": "Başarısızları yeniden çalıştır",
"reviewers": "İnceleyenler",
"viewFile": "Görüntüle"
},
-2
View File
@@ -3218,7 +3218,6 @@
"pipelineJobs_one": "{{count}} 項作業",
"pipelineJobs_other": "{{count}} 項作業",
"rerunAll": "全部重新執行",
"rerunFailed": "重新執行失敗項目",
"subIssueTitle": "子議題標題",
"subIssues": "子議題"
},
@@ -3449,7 +3448,6 @@
"requestReview": "請求審查",
"requestReviewers": "邀請審查人",
"rerunAll": "全部重新執行",
"rerunFailed": "重新執行失敗項目",
"reviewers": "審查人",
"viewFile": "檢視"
},
-2
View File
@@ -3218,7 +3218,6 @@
"pipelineJobs_one": "{{count}} 个作业",
"pipelineJobs_other": "{{count}} 个作业",
"rerunAll": "重新运行全部",
"rerunFailed": "重新运行失败项",
"subIssueTitle": "子议题标题",
"subIssues": "子议题"
},
@@ -3449,7 +3448,6 @@
"requestReview": "请求评审",
"requestReviewers": "邀请评审人",
"rerunAll": "全部重新运行",
"rerunFailed": "重新运行失败项",
"reviewers": "评审人",
"viewFile": "查看"
},
@@ -159,7 +159,9 @@ const checkSummarySchema = z
passed: prCount('passed'),
failed: prCount('failed'),
pending: prCount('pending'),
neutral: prCount('neutral')
neutral: prCount('neutral'),
cancelled: salvagedOptional('cancelled', z.number().int().nonnegative()),
scope: z.literal('pipeline').optional()
})
.transform((summary): ProviderCheckSummary => ({
state: summary.state,
@@ -167,7 +169,14 @@ const checkSummarySchema = z
passed: summary.passed ?? 0,
failed: summary.failed ?? 0,
pending: summary.pending ?? 0,
neutral: summary.neutral ?? 0
neutral: summary.neutral ?? 0,
...(summary.cancelled !== undefined &&
[summary.total, summary.passed, summary.failed, summary.pending, summary.neutral].every(
(count) => count !== undefined
)
? { cancelled: summary.cancelled }
: {}),
...(summary.scope ? { scope: summary.scope } : {})
}))
export function prCheckSummary(name: string) {
@@ -58,6 +58,7 @@ export const hostedReviewForBranchSchema = z
state: salvagedOptional('state', z.enum(HOSTED_REVIEW_STATE)),
url: prText('url'),
status: salvagedOptional('status', z.enum(CHECK_STATUS)),
checksSummary: prCheckSummary('checksSummary'),
updatedAt: prText('updatedAt'),
mergeable: salvagedOptional('mergeable', z.enum(MERGEABLE_STATE)),
reviewDecision: salvagedOptional('reviewDecision', z.enum(REVIEW_DECISION).nullable()),
@@ -76,6 +77,7 @@ export const hostedReviewForBranchSchema = z
state: review.state ?? 'open',
url: review.url ?? '',
status: review.status ?? 'pending',
checksSummary: review.checksSummary,
updatedAt: review.updatedAt ?? '',
mergeable: review.mergeable ?? 'UNKNOWN',
reviewDecision: review.reviewDecision,
@@ -119,6 +121,7 @@ const pullRequestSchema = z
title: prText('title'),
url: prText('url'),
checksStatus: salvagedOptional('checksStatus', z.enum(CHECK_STATUS)),
checksSummary: prCheckSummary('checksSummary'),
updatedAt: prText('updatedAt'),
mergeable: salvagedOptional('mergeable', z.enum(MERGEABLE_STATE)),
reviewDecision: salvagedOptional('reviewDecision', z.enum(REVIEW_DECISION).nullable()),
@@ -143,6 +146,7 @@ const pullRequestSchema = z
state: pr.state,
url: pr.url ?? '',
checksStatus: pr.checksStatus ?? 'pending',
checksSummary: pr.checksSummary,
updatedAt: pr.updatedAt ?? '',
mergeable: pr.mergeable ?? 'UNKNOWN',
reviewDecision: pr.reviewDecision,
@@ -21,6 +21,7 @@ import {
githubWorkItemDetailsSchema,
hostedReviewForBranchSchema
} from './github-pr-read-reply-schema'
import { getProviderChecksOutcome } from '../../../src/shared/provider-check-summary'
import { prChecksSchema } from './github-pr-entity-reply-schema'
import {
reviewCreatedTerminalSchema,
@@ -552,3 +553,44 @@ describe('a newer host is not refused', () => {
expect(inventory.layoutVersion).toBe(3)
})
})
it('keeps additive cancellation summaries through explicit PR and hosted-review parsers', () => {
const summary = {
state: 'failure',
total: 1,
passed: 0,
failed: 1,
pending: 0,
neutral: 0,
cancelled: 1
}
const pr = reads(githubPrForBranchSchema, {
number: 4,
state: 'open',
checksStatus: 'failure',
checksSummary: summary
})
expect(pr?.kind === 'found' && pr.pr.checksSummary).toEqual(summary)
const readReview = (checksSummary: unknown) =>
reads(hostedReviewForBranchSchema, {
provider: 'github',
number: 4,
status: 'failure',
checksSummary
})
expect(readReview(summary)?.checksSummary).toEqual(summary)
expect(readReview({ ...summary, scope: 'pipeline' })?.checksSummary?.scope).toBe('pipeline')
for (const invalid of [
undefined,
{ ...summary, cancelled: -1 },
{ ...summary, cancelled: 2 },
{ ...summary, cancelled: '1' },
{ ...summary, scope: 'future_scope' },
{ ...summary, passed: undefined },
{ ...summary, failed: 0, pending: 1, cancelled: 0 }
]) {
const review = readReview(invalid)
expect(review?.number).toBe(4)
expect(getProviderChecksOutcome(review?.checksSummary, 'failure')).toBe('failure')
}
})
@@ -54,6 +54,7 @@ describe('buildGitHubCheckSummary', () => {
it('rolls up GitLab jobs with unknown terminal statuses as neutral', () => {
expect(buildGitLabCheckSummary([{ status: 'success' }, { status: 'future_status' }])).toEqual({
cancelled: 0,
state: 'success',
total: 2,
passed: 1,
@@ -139,6 +140,7 @@ describe('mobile check classification', () => {
}[])('$name never reads as failing', ({ statuses, expected }) => {
expect(buildGitLabCheckSummary(statuses.map((status) => ({ status })))).toEqual({
...expected,
cancelled: 0,
total: statuses.length
})
})
+4 -1
View File
@@ -10,5 +10,8 @@ export type GitHubCheckSummary = ProviderCheckSummary
// Use the desktop classifier so skipped checks cannot disagree across mobile and desktop.
export function buildGitHubCheckSummary(checks: GitHubCheckLike[]): GitHubCheckSummary {
return summarizeProviderChecks(checks)
const summary = summarizeProviderChecks(checks)
// Detail lists do not prove completeness; only whole-PR reads publish cancellation metadata.
delete summary.cancelled
return summary
}
+10 -5
View File
@@ -4,7 +4,10 @@ import type {
} from '../../../src/shared/github/pull-request-types'
import { translate } from '../i18n/mobile-i18n'
import { resolveGitHubPRMergeState } from '../../../src/shared/github/pull-request-merge-state'
import { getProviderChecksLabel } from '../../../src/shared/provider-check-summary'
import {
getProviderChecksLabel,
getProviderChecksOutcome
} from '../../../src/shared/provider-check-summary'
export type MobileHostedReviewStatus = {
state?: string
@@ -76,7 +79,9 @@ export function getHostedMergeLabel(item: MobileHostedReviewStatus): string | nu
}
export function getHostedChecksLabel(item: { checksSummary?: ProviderCheckSummary }): string {
return getProviderChecksLabel(item.checksSummary)
return getProviderChecksOutcome(item.checksSummary) === 'cancelled'
? translate('pullRequest.checkStatus.cancelled', 'Cancelled')
: getProviderChecksLabel(item.checksSummary)
}
export function getHostedReviewSignalTone(
@@ -104,13 +109,13 @@ export function getHostedReviewSignalTone(
return 'neutral'
}
if (signal === 'checks') {
if (item.checksSummary?.state === 'success') {
if (getProviderChecksOutcome(item.checksSummary) === 'success') {
return 'success'
}
if (item.checksSummary?.state === 'failure') {
if (getProviderChecksOutcome(item.checksSummary) === 'failure') {
return 'danger'
}
if (item.checksSummary?.state === 'pending') {
if (getProviderChecksOutcome(item.checksSummary) === 'pending') {
return 'warning'
}
return 'neutral'
@@ -24,7 +24,8 @@ export const SUCCESS_CHECKS = {
passed: 1,
failed: 0,
pending: 0,
neutral: 0
neutral: 0,
cancelled: 0
} as const
export function gitlabItem(
@@ -167,7 +167,7 @@ export function renderMobileTasksItemDetailContent(model: ConnectionPresentation
onPress={() => void rerunGitHubChecks(actionItem, true)}
>
<Text style={styles.inlineSaveText}>
{translate('tasks.itemDetailContent.rerunFailed', 'Rerun failed')}
{translate('tasks.rerunUnsuccessful', 'Retry checks')}
</Text>
</Pressable>
<Pressable
@@ -162,7 +162,7 @@ export function renderMobileTasksProjectReviewPanels(model: ConnectionPresentati
onPress={() => void rerunProjectGitHubChecks(projectRowItem, true)}
>
<Text style={styles.inlineSaveText}>
{translate('tasks.projectReviewPanels.rerunFailed', 'Rerun failed')}
{translate('tasks.rerunUnsuccessful', 'Retry checks')}
</Text>
</Pressable>
<Pressable
@@ -33,6 +33,7 @@ export type GitHubWorkItem = {
author: string | null
branchName?: string
baseRefName?: string
headSha?: string
isCrossRepository?: boolean
additions?: number
deletions?: number
@@ -284,6 +284,8 @@ export function useMobileTasksHostedCommentReviewActions<
}
setError('')
const isCurrent = claimPublication(item, detailPayload)
const requestedHeadSha =
detailPayload?.provider === 'github' ? detailPayload.headSha : undefined
try {
const reply = await githubPullRequestChecksRead.request(
client,
@@ -291,7 +293,7 @@ export function useMobileTasksHostedCommentReviewActions<
repo: `id:${item.source.repoId}`,
prNumber: item.source.number,
prRepo: requireGitHubTaskTarget(item.source).repository,
headSha: detailPayload?.provider === 'github' ? detailPayload.headSha : undefined,
headSha: requestedHeadSha,
noCache: true
},
{ timeoutMs: 30_000 }
@@ -301,6 +303,12 @@ export function useMobileTasksHostedCommentReviewActions<
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 }
@@ -310,7 +318,7 @@ export function useMobileTasksHostedCommentReviewActions<
sameGitHubTask(current, item)
? {
...current,
source: { ...current.source, checksSummary }
source: { ...current.source, checksSummary: summaryFor(current.source) }
}
: current
)
@@ -319,7 +327,7 @@ export function useMobileTasksHostedCommentReviewActions<
sameGitHubTask(candidate, item)
? {
...candidate,
source: { ...candidate.source, checksSummary }
source: { ...candidate.source, checksSummary: summaryFor(candidate.source) }
}
: candidate
)
@@ -35,6 +35,7 @@ function gitLabHydrationUnchanged(
current.total === summary.total &&
current.passed === summary.passed &&
current.failed === summary.failed &&
current.cancelled === summary.cancelled &&
current.pending === summary.pending &&
current.neutral === summary.neutral &&
current.scope === summary.scope &&
@@ -1,3 +1,4 @@
import { getProviderChecksOutcome } from '../../../src/shared/provider-check-summary'
import type { CheckStatus } from '../../../src/shared/github/pull-request-types'
import {
getWorkspaceAttachmentKey,
@@ -21,7 +22,7 @@ export type WorkspaceDetailLink = {
url?: string
/** Review state (`open`, `merged`, ...); reviews only. */
state?: string
checks?: CheckStatus
checks?: CheckStatus | 'cancelled'
}
export type WorkspaceOrigin = { kind: 'automation'; name?: string } | { kind: 'cli' }
@@ -73,6 +74,8 @@ export function buildWorkspaceDetailLinks(row: Worktree): WorkspaceDetailLink[]
url: branchPR.url ?? link.url,
state: pr.state,
checks: pr.checksStatus
? getProviderChecksOutcome(pr.checksSummary, pr.checksStatus)
: undefined
})
const index = items.findIndex((item) => isBranchPRAttachment(item, branchPR, row.repoId))
if (index !== -1) {
@@ -71,6 +71,7 @@ type AzureDevOpsAuthConfig = {
export type AzureDevOpsRequestOptions = {
searchParams?: Record<string, string | number>
timeoutMs?: number
onResponse?: (response: Response) => void
}
function envValue(name: string): string | null {
@@ -171,6 +172,7 @@ export async function requestAzureDevOpsJsonAtBase<T>(
const url = apiUrl(baseUrl, path, options.searchParams)
const response = await doFetch(url)
if (response.ok) {
options.onResponse?.(response)
return (await response.json()) as T
}
const body = response.status === 400 ? await response.text().catch(() => '') : ''
+140
View File
@@ -140,6 +140,15 @@ describe('Azure DevOps client', () => {
state: 'open',
url: 'https://dev.azure.com/acme/Project/_git/repo/pullrequest/18',
status: 'success',
checksSummary: {
state: 'success',
total: 1,
passed: 1,
failed: 0,
cancelled: 0,
pending: 0,
neutral: 0
},
updatedAt: '2026-05-11T00:00:00Z',
mergeable: 'MERGEABLE',
headSha: 'abc123'
@@ -178,6 +187,137 @@ describe('Azure DevOps client', () => {
).rejects.toThrow(/Azure DevOps request failed/)
})
it.each<{
name: string
prBody: unknown
prContinuation?: boolean
commitFails?: boolean
commitContinuation?: boolean
commitRequests?: number
complete: boolean
}>([
{ name: 'failed PR source', prBody: null, complete: false },
{ name: 'malformed PR source', prBody: {}, complete: false },
{
name: 'continuing empty PR source',
prBody: { value: [] },
prContinuation: true,
complete: false
},
{ name: 'complete empty PR source', prBody: { value: [] }, complete: true },
{ name: 'failed commit fallback', prBody: { value: [] }, commitFails: true, complete: false },
{
name: 'continuing commit fallback',
prBody: { value: [] },
commitContinuation: true,
complete: false
},
{
name: 'continuing nonempty PR source',
prBody: { value: [{ state: 'canceled' }] },
prContinuation: true,
commitRequests: 0,
complete: false
}
])(
'preserves cancellation authority with $name',
async ({
prBody,
prContinuation,
commitFails,
commitContinuation,
commitRequests,
complete
}) => {
primeGitExecWithDefaultBranch()
const requests: string[] = []
const response = (body: unknown, continuation = false): Response =>
body === null
? new Response('Unavailable', { status: 500 })
: Response.json(body, {
headers: continuation ? { 'x-ms-continuationtoken': 'next' } : {}
})
globalThis.fetch = vi.fn<typeof globalThis.fetch>(async (input) => {
const path = new URL(String(input)).pathname
requests.push(path)
if (path.endsWith('/repositories/repo')) {
return Response.json({ id: 'repo-guid' })
}
if (path.endsWith('/pullRequests')) {
return Response.json({
value: [
{
pullRequestId: 18,
title: 'Azure branch',
status: 'active',
lastMergeSourceCommit: { commitId: 'head' }
}
]
})
}
if (path.endsWith('/pullRequests/18/statuses')) {
return response(prBody, prContinuation)
}
if (path.endsWith('/commits/head/statuses')) {
return response(
commitFails ? null : { value: [{ state: 'canceled' }] },
commitContinuation
)
}
throw new Error(`Unexpected request: ${path}`)
})
const pr = await getAzureDevOpsPullRequestForBranch('/repo', 'feature/azure')
expect(pr?.status).toBe(commitFails ? 'neutral' : 'failure')
expect(pr?.checksSummary).toEqual(
complete
? {
state: 'failure',
total: 1,
passed: 0,
failed: 1,
cancelled: 1,
pending: 0,
neutral: 0
}
: undefined
)
expect(requests.filter((path) => path.includes('/commits/'))).toHaveLength(
commitRequests ?? 1
)
}
)
it('keeps embedded statuses without a source SHA conservative', async () => {
primeGitExecWithDefaultBranch()
globalThis.fetch = vi.fn<typeof globalThis.fetch>(async (input) => {
const path = new URL(String(input)).pathname
if (path.endsWith('/repositories/repo')) {
return Response.json({ id: 'repo-guid' })
}
if (path.endsWith('/pullRequests')) {
return Response.json({
value: [
{
pullRequestId: 18,
title: 'Azure branch',
status: 'active',
statuses: [{ state: 'canceled' }]
}
]
})
}
if (path.endsWith('/pullRequests/18/statuses')) {
return Response.json({ value: [] })
}
throw new Error(`Unexpected request: ${path}`)
})
const pr = await getAzureDevOpsPullRequestForBranch('/repo', 'feature/azure')
expect(pr?.status).toBe('failure')
expect(pr?.checksSummary).toBeUndefined()
expect(globalThis.fetch).toHaveBeenCalledTimes(3)
})
it('uses the most recent branch PR instead of preferring stale active PRs', async () => {
gitExecFileAsyncMock.mockResolvedValue({
stdout: 'https://dev.azure.com/acme/Project/_git/repo\n'
+33 -22
View File
@@ -1,5 +1,6 @@
import {
deriveAzureDevOpsStatus,
summarizeAzureDevOpsStatuses,
mapAzureDevOpsPullRequest,
mapAzureDevOpsPullRequestState,
type AzureDevOpsPullRequestInfo,
@@ -73,32 +74,37 @@ async function getPullRequestStatuses(
repo: AzureDevOpsRepoRef,
repoIdOrName: string,
pr: RawAzureDevOpsPullRequest
): Promise<RawAzureDevOpsStatus[]> {
const raw = await requestAzureDevOpsJson<
RawAzureDevOpsStatus[] | { value?: RawAzureDevOpsStatus[] }
>(
repo,
`/_apis/git/repositories/${encodePathSegment(repoIdOrName)}/pullRequests/${encodePathSegment(
String(pr.pullRequestId)
)}/statuses`
): Promise<{ statuses: RawAzureDevOpsStatus[]; complete: boolean }> {
const read = async (path: string) => {
let hasMore = false
const raw = await requestAzureDevOpsJson<
RawAzureDevOpsStatus[] | { value?: RawAzureDevOpsStatus[] }
>(repo, path, {
onResponse: (response) => {
hasMore = Boolean(response.headers.get('x-ms-continuationtoken'))
}
})
return {
statuses: readStatusList(raw),
complete: raw !== null && !hasMore && (Array.isArray(raw) || Array.isArray(raw.value))
}
}
const root = `/_apis/git/repositories/${encodePathSegment(repoIdOrName)}`
const prStatuses = await read(
`${root}/pullRequests/${encodePathSegment(String(pr.pullRequestId))}/statuses`
)
const prStatuses = readStatusList(raw)
if (prStatuses.length > 0) {
if (prStatuses.statuses.length > 0) {
return prStatuses
}
const commitId = pr.lastMergeSourceCommit?.commitId?.trim()
if (!commitId) {
return pr.statuses ?? []
return { statuses: pr.statuses ?? [], complete: false }
}
const commitStatuses = await read(`${root}/commits/${encodePathSegment(commitId)}/statuses`)
return {
statuses: commitStatuses.statuses,
complete: prStatuses.complete && commitStatuses.complete
}
const commitStatuses = await requestAzureDevOpsJson<
RawAzureDevOpsStatus[] | { value?: RawAzureDevOpsStatus[] }
>(
repo,
`/_apis/git/repositories/${encodePathSegment(repoIdOrName)}/commits/${encodePathSegment(
commitId
)}/statuses`
)
return readStatusList(commitStatuses)
}
async function normalizePullRequest(
@@ -107,8 +113,13 @@ async function normalizePullRequest(
webBaseUrl: string,
raw: RawAzureDevOpsPullRequest
): Promise<AzureDevOpsPullRequestInfo | null> {
const statuses = await getPullRequestStatuses(repo, repoIdOrName, raw)
return mapAzureDevOpsPullRequest(raw, deriveAzureDevOpsStatus(statuses), webBaseUrl)
const { statuses, complete } = await getPullRequestStatuses(repo, repoIdOrName, raw)
return mapAzureDevOpsPullRequest(
raw,
deriveAzureDevOpsStatus(statuses),
webBaseUrl,
complete ? summarizeAzureDevOpsStatuses(statuses) : undefined
)
}
function sortPullRequestsForBranch(
+26 -2
View File
@@ -1,4 +1,9 @@
import type { CheckStatus, PRMergeableState } from '../../shared/github/pull-request-types'
import { summarizeProviderChecks } from '../../shared/provider-check-summary'
import type {
CheckStatus,
PRMergeableState,
ProviderCheckSummary
} from '../../shared/github/pull-request-types'
export type RawAzureDevOpsStatus = {
state?: string | null
@@ -31,6 +36,7 @@ export type AzureDevOpsPullRequestInfo = {
state: 'open' | 'closed' | 'merged' | 'draft'
url: string
status: CheckStatus
checksSummary?: ProviderCheckSummary
updatedAt: string
mergeable: PRMergeableState
headSha?: string
@@ -104,10 +110,27 @@ export function deriveAzureDevOpsStatus(statuses: readonly RawAzureDevOpsStatus[
return 'neutral'
}
export function summarizeAzureDevOpsStatuses(
statuses: readonly RawAzureDevOpsStatus[]
): ProviderCheckSummary {
const summary = summarizeProviderChecks(
statuses.map(({ state }) => ({
status: 'completed',
conclusion: ['canceled', 'cancelled'].includes(state?.trim().toLowerCase() ?? '')
? 'cancelled'
: { success: 'success', failure: 'failure', pending: 'pending', neutral: 'neutral' }[
classifyAzureDevOpsStatus(state)
]
}))
)
return { ...summary, state: statuses.length ? deriveAzureDevOpsStatus(statuses) : 'none' }
}
export function mapAzureDevOpsPullRequest(
raw: RawAzureDevOpsPullRequest,
status: CheckStatus,
webBaseUrl: string
webBaseUrl: string,
checksSummary?: ProviderCheckSummary
): AzureDevOpsPullRequestInfo | null {
if (typeof raw.pullRequestId !== 'number' || !raw.title) {
return null
@@ -120,6 +143,7 @@ export function mapAzureDevOpsPullRequest(
url:
raw._links?.web?.href ?? `${webBaseUrl.replace(/\/+$/, '')}/pullrequest/${raw.pullRequestId}`,
status,
...(checksSummary ? { checksSummary } : {}),
updatedAt: raw.closedDate ?? raw.creationDate ?? '',
mergeable: mapAzureDevOpsMergeable(raw.mergeStatus),
...(headSha ? { headSha } : {})
+9
View File
@@ -215,6 +215,15 @@ describe('Bitbucket client', () => {
state: 'open',
url: 'https://bitbucket.org/team/repo/pull-requests/7',
status: 'success',
checksSummary: {
state: 'success',
total: 1,
passed: 1,
failed: 0,
cancelled: 0,
pending: 0,
neutral: 0
},
updatedAt: '2026-05-10T00:00:00.000Z',
mergeable: 'UNKNOWN',
headSha: 'abc123'
+17 -7
View File
@@ -1,6 +1,7 @@
import type { CheckStatus } from '../../shared/github/pull-request-types'
import type { CheckStatus, ProviderCheckSummary } from '../../shared/github/pull-request-types'
import {
deriveBitbucketBuildStatus,
summarizeBitbucketBuildStatuses,
mapBitbucketPullRequest,
mapBitbucketPullRequestState,
type BitbucketPullRequestInfo,
@@ -121,15 +122,24 @@ function allStateFilter(): string {
async function getBuildStatus(
repo: BitbucketRepoRef,
headSha: string | undefined
): Promise<CheckStatus> {
): Promise<{
status: CheckStatus
checksSummary?: ProviderCheckSummary
}> {
if (!headSha) {
return 'neutral'
return { status: 'neutral' }
}
const data = await requestJson<{ values?: RawBitbucketBuildStatus[] }>(
const data = await requestJson<{ values?: RawBitbucketBuildStatus[]; next?: string }>(
`/repositories/${encodedRepoPath(repo)}/commit/${encodeURIComponent(headSha)}/statuses/build`,
{ searchParams: { pagelen: '100' } }
)
return deriveBitbucketBuildStatus(data?.values ?? [])
const statuses = data?.values ?? []
return {
status: deriveBitbucketBuildStatus(statuses),
...(data && Array.isArray(data.values) && !data.next
? { checksSummary: summarizeBitbucketBuildStatuses(statuses) }
: {})
}
}
async function normalizePullRequest(
@@ -137,8 +147,8 @@ async function normalizePullRequest(
raw: RawBitbucketPullRequest
): Promise<BitbucketPullRequestInfo | null> {
const headSha = raw.source?.commit?.hash?.trim()
const status = await getBuildStatus(repo, headSha)
return mapBitbucketPullRequest(raw, status)
const { status, checksSummary } = await getBuildStatus(repo, headSha)
return mapBitbucketPullRequest(raw, status, checksSummary)
}
// Avoid keychain prompts: validate stored credentials only when the secret is already cached.
+30 -2
View File
@@ -1,4 +1,9 @@
import type { CheckStatus, PRMergeableState } from '../../shared/github/pull-request-types'
import { summarizeProviderChecks } from '../../shared/provider-check-summary'
import type {
CheckStatus,
PRMergeableState,
ProviderCheckSummary
} from '../../shared/github/pull-request-types'
export type RawBitbucketPullRequest = {
id?: number
@@ -31,6 +36,7 @@ export type BitbucketPullRequestInfo = {
state: 'open' | 'closed' | 'merged'
url: string
status: CheckStatus
checksSummary?: ProviderCheckSummary
updatedAt: string
mergeable: PRMergeableState
headSha?: string
@@ -75,9 +81,30 @@ export function deriveBitbucketBuildStatus(
return 'neutral'
}
export function summarizeBitbucketBuildStatuses(
statuses: readonly RawBitbucketBuildStatus[]
): ProviderCheckSummary {
const summary = summarizeProviderChecks(
statuses.map(({ state }) => ({
status: 'completed',
conclusion:
{
SUCCESSFUL: 'success',
FAILED: 'failure',
ERROR: 'failure',
STOPPED: 'cancelled',
INPROGRESS: 'pending',
PENDING: 'pending'
}[state?.trim().toUpperCase() ?? ''] ?? 'neutral'
}))
)
return { ...summary, state: statuses.length ? deriveBitbucketBuildStatus(statuses) : 'none' }
}
export function mapBitbucketPullRequest(
raw: RawBitbucketPullRequest,
status: CheckStatus
status: CheckStatus,
checksSummary?: ProviderCheckSummary
): BitbucketPullRequestInfo | null {
if (typeof raw.id !== 'number' || !raw.title || !raw.links?.html?.href) {
return null
@@ -89,6 +116,7 @@ export function mapBitbucketPullRequest(
state: mapBitbucketPullRequestState(raw.state),
url: raw.links.html.href,
status,
...(checksSummary ? { checksSummary } : {}),
updatedAt: raw.updated_on ?? '',
mergeable: 'UNKNOWN',
...(headSha ? { headSha } : {})
@@ -53,6 +53,87 @@ describe('getPRForBranch', () => {
resetPRForBranchMocks(clientMocks)
})
it('publishes complete cancellation after REST discovery is replaced by exact hydration', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 7,
title: 'Cancelled run',
state: 'open',
html_url: 'https://github.com/acme/widgets/pull/7',
head: { ref: 'feature/test', sha: 'head-oid' },
base: { ref: 'main' }
}
])
})
.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 7,
title: 'Cancelled run',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/7',
headRefOid: 'head-oid',
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'CANCELLED' }]
})
})
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'],
['pr', 'view']
])
expect(pr).toMatchObject({
checksStatus: 'failure',
headSha: 'head-oid',
checksSummary: {
state: 'failure',
total: 1,
passed: 0,
failed: 1,
cancelled: 1,
pending: 0,
neutral: 0
}
})
})
it('keeps branch-list fallback coarse when exact hydration cannot read a failure beyond page 100', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
resolvePRRepositoryCandidatesMock.mockResolvedValue({
candidates: [{ owner: 'acme', repo: 'widgets' }],
headRepo: null
})
const completeChecks = Array.from({ length: 101 }, (_, index) => ({
status: 'COMPLETED',
conclusion: index === 100 ? 'FAILURE' : 'CANCELLED'
}))
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'pr' && args[1] === 'list') {
return {
stdout: JSON.stringify([
{
number: 7,
title: 'Partial check page',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/7',
headRefOid: 'head-oid',
statusCheckRollup: completeChecks.slice(0, 100)
}
])
}
}
throw new Error('network unavailable')
})
const pr = await getPRForBranch('/repo-root', 'feature/test')
expect(ghExecFileAsyncMock.mock.calls.slice(0, 2).map(([args]) => args.slice(0, 2))).toEqual([
['pr', 'list'],
['pr', 'view']
])
expect(pr).toMatchObject({ number: 7, checksStatus: 'failure' })
expect(pr?.checksSummary).toBeUndefined()
})
it.each([
{ restEnabled: false, graphqlEnabled: true },
{ restEnabled: true, graphqlEnabled: false }
@@ -232,6 +313,7 @@ describe('getPRForBranch', () => {
expect(pr?.mergeable).toBe('MERGEABLE')
expect(pr?.prRepo).toEqual({ owner: 'acme', repo: 'widgets' })
expect(pr?.headRepo).toEqual({ owner: 'acme', repo: 'widgets' })
expect(pr?.checksSummary).toBeUndefined()
})
it('resolves fork PRs from the upstream PR repo with the origin head owner', async () => {
@@ -97,8 +97,24 @@ describe('work item checksSummary', () => {
total: 3,
passed: 2,
failed: 0,
cancelled: 0,
pending: 0,
neutral: 1
})
})
it.each([
[[{ state: 'SUCCESS' }, { state: 'EXPECTED' }], 'pending', 0],
[[{ state: 'SUCCESS' }, { status: 'COMPLETED', conclusion: 'CANCELLED' }], 'failure', 1]
] as const)('preserves the raw task rollup contract for %j', async (rollup, state, cancelled) => {
mockPullRequestDetail({
number: 43,
title: 'Waiting checks',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/43',
statusCheckRollup: rollup
})
const item = await getWorkItem('/repo-root', 43, 'pr', null, {}, 'origin')
expect(item?.checksSummary).toMatchObject({ state, total: 2, passed: 1, cancelled })
})
})
@@ -70,7 +70,9 @@ export async function getFallbackPRListForBranch(
{ ...ghOptions, ...githubHostExecOptions(prRepo) }
)
const list = JSON.parse(stdout) as PullRequestLookupData[]
return preferOpenPullRequest(list) ?? null
const pr = preferOpenPullRequest(list)
// Branch lists paginate PRs, not their nested check contexts.
return pr ? { ...pr, checksUnavailable: true } : null
}
export async function hydrateBranchLookupWithExactPR(
@@ -186,8 +188,12 @@ export async function lookupPRByBranchName(args: {
['pr', 'view', args.branchName, '--json', PR_LOOKUP_JSON_FIELDS],
args.ghOptions
)
const parsedData: PullRequestLookupData = JSON.parse(stdout)
const lookup = {
data: normalizePullRequestLookupData(JSON.parse(stdout) as PullRequestLookupData),
data: normalizePullRequestLookupData({
...parsedData,
checksUnavailable: false
}),
dataRepo: null
}
return !args.policy || (await args.policy.accepts(lookup.data, null))
@@ -97,6 +97,7 @@ export async function getPRByNumber(
{
...knownPullRequestData,
...exactData,
checksUnavailable: false,
...(knownPullRequestData?.stack ? { stack: knownPullRequestData.stack } : {})
},
ghOptions,
@@ -169,8 +170,12 @@ export async function lookupPRByNumber(args: {
['pr', 'view', String(args.number), '--json', PR_LOOKUP_JSON_FIELDS],
args.ghOptions
)
const parsedData: PullRequestLookupData = JSON.parse(stdout)
const lookup = {
data: normalizePullRequestLookupData(JSON.parse(stdout) as PullRequestLookupData),
data: normalizePullRequestLookupData({
...parsedData,
checksUnavailable: false
}),
dataRepo: null
}
return !args.policy || (await args.policy.accepts(lookup.data, null))
@@ -1,10 +1,11 @@
import { summarizePRCheckRollup } from '../../../../shared/pr-check-status'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-request-refresh-types'
import type {
PRConflictSummary,
PRMergeableState,
GitHubPRStack
} from '../../../../shared/github/pull-request-types'
import { deriveCheckStatus, mapPRState } from '../../mappers'
import { mapPRState } from '../../mappers'
import type { OwnerRepo } from '../../gh-utils'
import type { PullRequestLookupData } from './pull-request-lookup-data'
@@ -30,6 +31,7 @@ export function assemblePRRefreshFoundOutcome(args: {
headDivergedFromMergedPRAtOid,
conflictSummary
} = args
const checksSummary = summarizePRCheckRollup(data.statusCheckRollup)
return {
kind: 'found',
fetchedAt: Date.now(),
@@ -38,7 +40,8 @@ export function assemblePRRefreshFoundOutcome(args: {
title: data.title,
state: mapPRState(data.state, data.isDraft),
url: data.url,
checksStatus: deriveCheckStatus(data.statusCheckRollup),
checksStatus: checksSummary.state === 'none' ? 'neutral' : checksSummary.state,
...(data.checksUnavailable === false ? { checksSummary } : {}),
updatedAt: data.updatedAt,
mergeable,
...(data.reviewDecision !== undefined ? { reviewDecision: data.reviewDecision } : {}),
@@ -24,6 +24,7 @@ export type PullRequestLookupData = {
state: string
url: string
statusCheckRollup: unknown[]
checksUnavailable?: boolean
updatedAt: string
isDraft?: boolean
mergeable: string
@@ -131,6 +132,7 @@ export function mapRestPullRequest(pr: RestPullRequest): PullRequestLookupData {
state: pr.merged_at ? 'MERGED' : pr.state,
url: pr.html_url ?? pr.url ?? '',
statusCheckRollup: [],
checksUnavailable: true,
updatedAt: pr.updated_at ?? '',
isDraft: pr.draft,
mergeable: mapRestPRMergeable(pr),
@@ -249,6 +249,7 @@ export async function readPullRequestSidebarQuery(
baseRefOid: pr.baseRefOid ?? undefined,
headRefOid: pr.headRefOid ?? undefined,
statusCheckRollup,
checksUnavailable: false,
stackMetadataChecked,
stack:
stackMetadataChecked && validStack
@@ -32,7 +32,8 @@ export function buildRuntimeWorktreePsSummaries(args: {
state: cachedPR.state,
title: cachedPR.title,
url: cachedPR.url,
checksStatus: cachedPR.checksStatus
checksStatus: cachedPR.checksStatus,
...(cachedPR.checksSummary ? { checksSummary: cachedPR.checksSummary } : {})
}
: meta?.linkedPR != null
? { number: meta.linkedPR, state: UNCACHED_LINKED_PR_STATE }
@@ -41,6 +41,7 @@ export function mapBitbucketReview(pr: BitbucketPullRequestInfo): HostedReviewIn
state: pr.state,
url: pr.url,
status: pr.status,
...(pr.checksSummary ? { checksSummary: pr.checksSummary } : {}),
updatedAt: pr.updatedAt,
mergeable: pr.mergeable,
...(pr.headSha ? { headSha: pr.headSha } : {})
@@ -55,6 +56,7 @@ export function mapAzureDevOpsReview(pr: AzureDevOpsPullRequestInfo): HostedRevi
state: pr.state,
url: pr.url,
status: pr.status,
...(pr.checksSummary ? { checksSummary: pr.checksSummary } : {}),
updatedAt: pr.updatedAt,
mergeable: pr.mergeable,
...(pr.headSha ? { headSha: pr.headSha } : {})
@@ -34,101 +34,121 @@ describe('Azure DevOps hosted review integration', () => {
_resetAzureDevOpsRepoRefCache()
})
it('resolves an Azure Repos PR through real git remote parsing and HTTP API calls', async () => {
const seen: SeenRequest[] = []
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`)
seen.push({
pathname: url.pathname,
search: url.search,
authorization: req.headers.authorization
})
if (url.pathname === '/acme/Project/_apis/git/repositories/repo') {
sendJson(res, {
id: 'repo-guid',
webUrl: 'https://dev.azure.com/acme/Project/_git/repo'
it.each(['canceled', 'cancelled', 'failed'])(
'preserves %s through Azure lookup and hosted review mapping',
async (state) => {
const seen: SeenRequest[] = []
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`)
seen.push({
pathname: url.pathname,
search: url.search,
authorization: req.headers.authorization
})
return
}
if (url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests') {
expect(url.searchParams.get('searchCriteria.sourceRefName')).toBe(
'refs/heads/feature/azure'
if (url.pathname === '/acme/Project/_apis/git/repositories/repo') {
sendJson(res, {
id: 'repo-guid',
webUrl: 'https://dev.azure.com/acme/Project/_git/repo'
})
return
}
if (url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests') {
expect(url.searchParams.get('searchCriteria.sourceRefName')).toBe(
'refs/heads/feature/azure'
)
sendJson(res, {
value: [
{
pullRequestId: 31,
title: 'Azure branch',
status: 'active',
creationDate: '2026-05-16T00:00:00Z',
mergeStatus: 'succeeded',
lastMergeSourceCommit: { commitId: 'abc123' }
}
]
})
return
}
if (
url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/31/statuses'
) {
sendJson(res, {
value: [
{ state: 'succeeded' },
...(state === 'failed' ? [{ state: 'cancelled' }] : []),
{ state }
]
})
return
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'not found' }))
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const repoPath = await mkdtemp(join(tmpdir(), 'orca-azure-review-'))
try {
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('expected TCP server address')
}
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = `http://127.0.0.1:${address.port}/acme/Project`
await execFileAsync('git', ['init'], { cwd: repoPath })
await execFileAsync(
'git',
['remote', 'add', 'origin', 'https://dev.azure.com/acme/Project/_git/repo'],
{ cwd: repoPath }
)
sendJson(res, {
value: [
{
pullRequestId: 31,
title: 'Azure branch',
status: 'active',
creationDate: '2026-05-16T00:00:00Z',
mergeStatus: 'succeeded',
lastMergeSourceCommit: { commitId: 'abc123' }
}
]
await expect(
getHostedReviewForBranch({
executionHostId: 'local',
repoPath,
branch: 'refs/heads/feature/azure'
})
).resolves.toEqual({
provider: 'azure-devops',
number: 31,
title: 'Azure branch',
state: 'open',
url: 'https://dev.azure.com/acme/Project/_git/repo/pullrequest/31',
status: 'failure',
checksSummary: {
state: 'failure',
total: state === 'failed' ? 3 : 2,
passed: 1,
failed: state === 'failed' ? 2 : 1,
cancelled: 1,
pending: 0,
neutral: 0
},
updatedAt: '2026-05-16T00:00:00Z',
mergeable: 'MERGEABLE',
headSha: 'abc123'
})
return
}
if (
url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/31/statuses'
) {
sendJson(res, { value: [{ state: 'succeeded' }] })
return
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'not found' }))
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const repoPath = await mkdtemp(join(tmpdir(), 'orca-azure-review-'))
try {
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('expected TCP server address')
}
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = `http://127.0.0.1:${address.port}/acme/Project`
await execFileAsync('git', ['init'], { cwd: repoPath })
await execFileAsync(
'git',
['remote', 'add', 'origin', 'https://dev.azure.com/acme/Project/_git/repo'],
{ cwd: repoPath }
)
await expect(
getHostedReviewForBranch({
executionHostId: 'local',
repoPath,
branch: 'refs/heads/feature/azure'
expect(seen.map((request) => request.pathname)).toEqual([
'/acme/Project/_apis/git/repositories/repo',
'/acme/Project/_apis/git/repositories/repo-guid/pullRequests',
'/acme/Project/_apis/git/repositories/repo-guid/pullRequests/31/statuses'
])
expect(seen.every((request) => request.authorization === 'Basic OmxvY2FsLXBhdA==')).toBe(
true
)
} finally {
await rm(repoPath, { recursive: true, force: true })
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
).resolves.toEqual({
provider: 'azure-devops',
number: 31,
title: 'Azure branch',
state: 'open',
url: 'https://dev.azure.com/acme/Project/_git/repo/pullrequest/31',
status: 'success',
updatedAt: '2026-05-16T00:00:00Z',
mergeable: 'MERGEABLE',
headSha: 'abc123'
})
expect(seen.map((request) => request.pathname)).toEqual([
'/acme/Project/_apis/git/repositories/repo',
'/acme/Project/_apis/git/repositories/repo-guid/pullRequests',
'/acme/Project/_apis/git/repositories/repo-guid/pullRequests/31/statuses'
])
expect(seen.every((request) => request.authorization === 'Basic OmxvY2FsLXBhdA==')).toBe(true)
} finally {
await rm(repoPath, { recursive: true, force: true })
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
}
}
})
)
it('prefers an active Azure Repos PR over a newer abandoned PR for the same branch', async () => {
const seen: SeenRequest[] = []
@@ -219,6 +239,15 @@ describe('Azure DevOps hosted review integration', () => {
title: 'Active branch',
state: 'open',
status: 'success',
checksSummary: {
state: 'success',
total: 1,
passed: 1,
failed: 0,
cancelled: 0,
pending: 0,
neutral: 0
},
mergeable: 'MERGEABLE',
headSha: 'active123'
})
@@ -39,93 +39,112 @@ describe('Bitbucket hosted review integration', () => {
__resetHostedReviewBranchCacheForTests()
})
it('resolves a Bitbucket PR through real git remote parsing and HTTP API calls', async () => {
const seen: SeenRequest[] = []
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`)
seen.push({
pathname: url.pathname,
search: url.search,
authorization: req.headers.authorization
})
it.each([false, true])(
'preserves stopped builds only for a complete Bitbucket page (next=%s)',
async (next) => {
const seen: SeenRequest[] = []
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`)
seen.push({
pathname: url.pathname,
search: url.search,
authorization: req.headers.authorization
})
if (url.pathname === '/2.0/repositories/team/repo/pullrequests') {
sendJson(res, {
values: [
{
id: 12,
title: 'Local Bitbucket branch',
state: 'OPEN',
updated_on: '2026-05-15T00:00:00Z',
links: {
html: { href: 'https://bitbucket.org/team/repo/pull-requests/12' }
},
source: {
branch: { name: 'feature/bitbucket' },
commit: { hash: 'abc123' }
if (url.pathname === '/2.0/repositories/team/repo/pullrequests') {
sendJson(res, {
values: [
{
id: 12,
title: 'Local Bitbucket branch',
state: 'OPEN',
updated_on: '2026-05-15T00:00:00Z',
links: {
html: { href: 'https://bitbucket.org/team/repo/pull-requests/12' }
},
source: {
branch: { name: 'feature/bitbucket' },
commit: { hash: 'abc123' }
}
}
}
]
]
})
return
}
if (url.pathname === '/2.0/repositories/team/repo/commit/abc123/statuses/build') {
sendJson(res, {
values: [{ state: 'SUCCESSFUL' }, { state: 'STOPPED' }],
...(next ? { next: 'https://api.bitbucket.org/next' } : {})
})
return
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'not found' }))
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const repoPath = await mkdtemp(join(tmpdir(), 'orca-bitbucket-review-'))
try {
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('expected TCP server address')
}
process.env.ORCA_BITBUCKET_API_BASE_URL = `http://127.0.0.1:${address.port}/2.0`
await execFileAsync('git', ['init'], { cwd: repoPath })
await execFileAsync('git', ['remote', 'add', 'origin', 'git@bitbucket.org:team/repo.git'], {
cwd: repoPath
})
return
}
if (url.pathname === '/2.0/repositories/team/repo/commit/abc123/statuses/build') {
sendJson(res, { values: [{ state: 'SUCCESSFUL' }] })
return
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'not found' }))
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const repoPath = await mkdtemp(join(tmpdir(), 'orca-bitbucket-review-'))
try {
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('expected TCP server address')
}
process.env.ORCA_BITBUCKET_API_BASE_URL = `http://127.0.0.1:${address.port}/2.0`
await execFileAsync('git', ['init'], { cwd: repoPath })
await execFileAsync('git', ['remote', 'add', 'origin', 'git@bitbucket.org:team/repo.git'], {
cwd: repoPath
})
await expect(
getHostedReviewForBranch({
executionHostId: 'local',
repoPath,
branch: 'refs/heads/feature/bitbucket'
await expect(
getHostedReviewForBranch({
executionHostId: 'local',
repoPath,
branch: 'refs/heads/feature/bitbucket'
})
).resolves.toEqual({
provider: 'bitbucket',
number: 12,
title: 'Local Bitbucket branch',
state: 'open',
url: 'https://bitbucket.org/team/repo/pull-requests/12',
status: 'failure',
...(!next
? {
checksSummary: {
state: 'failure',
total: 2,
passed: 1,
failed: 1,
cancelled: 1,
pending: 0,
neutral: 0
}
}
: {}),
updatedAt: '2026-05-15T00:00:00Z',
mergeable: 'UNKNOWN',
headSha: 'abc123'
})
).resolves.toEqual({
provider: 'bitbucket',
number: 12,
title: 'Local Bitbucket branch',
state: 'open',
url: 'https://bitbucket.org/team/repo/pull-requests/12',
status: 'success',
updatedAt: '2026-05-15T00:00:00Z',
mergeable: 'UNKNOWN',
headSha: 'abc123'
})
expect(seen.map((request) => request.pathname)).toEqual([
'/2.0/repositories/team/repo/pullrequests',
'/2.0/repositories/team/repo/commit/abc123/statuses/build'
])
expect(seen.every((request) => request.authorization === 'Bearer local-token')).toBe(true)
const query = new URLSearchParams(seen[0].search)
expect(query.get('q')).toContain('source.branch.name = "feature/bitbucket"')
expect(query.getAll('state')).toEqual(['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'])
} finally {
await rm(repoPath, { recursive: true, force: true })
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
expect(seen.map((request) => request.pathname)).toEqual([
'/2.0/repositories/team/repo/pullrequests',
'/2.0/repositories/team/repo/commit/abc123/statuses/build'
])
expect(seen.every((request) => request.authorization === 'Bearer local-token')).toBe(true)
const query = new URLSearchParams(seen[0].search)
expect(query.get('q')).toContain('source.branch.name = "feature/bitbucket"')
expect(query.getAll('state')).toEqual(['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'])
} finally {
await rm(repoPath, { recursive: true, force: true })
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
}
}
})
)
it('does not cache a transient API failure as a definitive no-review result', async () => {
vi.useFakeTimers({ now: Date.now() })
@@ -1,3 +1,7 @@
import {
getProviderChecksOutcome,
type CheckPresentationOutcome
} from '../../../../../shared/provider-check-summary'
import { useState } from 'react'
import { ChevronRight, CircleDot, GitMerge, GitPullRequest } from 'lucide-react'
import { translate } from '@/i18n/i18n'
@@ -8,7 +12,6 @@ import { getReviewStateTone } from '@/components/github/review-state-presentatio
import { workspaceAttachmentReferenceLabel } from '../../../../../shared/workspace-attachments'
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
import type { HostedReviewState } from '../../../../../shared/hosted-review'
import type { CheckStatus } from '../../../../../shared/github/pull-request-types'
import { ReviewIcon } from '../../sidebar/worktree-review-helpers'
import { workspaceAttachmentLabel } from '../../sidebar/worktree-attachment-editing'
import type { WorkspaceLinkedItemRow } from '../../sidebar/workspace-hover-attachments'
@@ -98,7 +101,11 @@ function LinkedItemRow({
issue: (number) => `#${number}`,
mr: (number) => `!${number}`
})
const status = live ? reviewChecksLabel(live.status) : linkedTaskState(row)
const status = live
? reviewChecksLabel(
live.status ? getProviderChecksOutcome(live.checksSummary, live.status) : undefined
)
: linkedTaskState(row)
const reviewState = live?.state ? reviewStateLabel(live.state) : undefined
return (
<BrowserNewTabRow
@@ -172,7 +179,10 @@ function reviewStateLabel(state: HostedReviewState): string {
return translate('browser.newTab.state.open', 'Open')
}
function reviewChecksLabel(status: CheckStatus | undefined): string | undefined {
function reviewChecksLabel(status: CheckPresentationOutcome | undefined): string | undefined {
if (status === 'cancelled') {
return translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
}
if (status === 'success') {
return translate('browser.newTab.checks.passing', 'Checks passing')
}
@@ -118,7 +118,7 @@ export function ChecksTabActions({
onSelect={() => void onRerun(true)}
>
<RefreshCw className="size-4" />
{translate('auto.components.GitHubItemDialog.e31651a224', 'Rerun failed checks')}
{translate('checks.rerunUnsuccessful', 'Rerun unsuccessful')}
</DropdownMenuItem>
<DropdownMenuItem disabled={rerunning} onSelect={() => void onRerun(false)}>
<RefreshCw className="size-4" />
@@ -14,9 +14,11 @@ export function getChecksTabSummaryPresentation(counts: ReturnType<typeof getChe
? CHECK_ICON.action_required
: counts.pending > 0
? CHECK_ICON.pending
: counts.passing > 0
? CHECK_ICON.success
: CircleDashed
: counts.cancelled > 0
? CHECK_ICON.cancelled
: counts.passing > 0
? CHECK_ICON.success
: CircleDashed
const summaryColor =
counts.failing > 0
? CHECK_COLOR.failure
@@ -24,8 +26,10 @@ export function getChecksTabSummaryPresentation(counts: ReturnType<typeof getChe
? CHECK_COLOR.action_required
: counts.pending > 0
? CHECK_COLOR.pending
: counts.passing > 0
? CHECK_COLOR.success
: 'text-muted-foreground'
: counts.cancelled > 0
? CHECK_COLOR.cancelled
: counts.passing > 0
? CHECK_COLOR.success
: 'text-muted-foreground'
return { SummaryIcon, summaryColor }
}
@@ -1,3 +1,4 @@
import { summarizeProviderChecks } from '../../../shared/provider-check-summary'
import { describe, expect, it } from 'vitest'
import type { GitHubPRMergeStateInput } from '../../../shared/github/pull-request-merge-state'
import { presentGitHubPRMergeState } from './github-pr-merge-state'
@@ -272,4 +273,16 @@ describe('presentGitHubPRMergeState', () => {
presentGitHubPRMergeState(pr({ state: 'draft', mergeQueueRequired: true })).autoMergeAction
).toBeNull()
})
it('names cancelled checks accurately without changing the provider merge permission', () => {
const checksSummary = summarizeProviderChecks([
{ status: 'completed', conclusion: 'success' },
{ status: 'completed', conclusion: 'cancelled' }
])
expect(presentGitHubPRMergeState(pr({ checksStatus: 'failure', checksSummary }))).toMatchObject(
{
label: 'Cancelled',
directMergeAvailable: true
}
)
})
})
@@ -1,3 +1,4 @@
import { getProviderChecksOutcome } from '../../../shared/provider-check-summary'
import {
resolveGitHubPRMergeState,
type GitHubPRAutoMergeActionIntent,
@@ -63,7 +64,8 @@ function presentAutoMergeAction(
}
function presentMergeStateKind(
kind: GitHubPRMergeStateKind
kind: GitHubPRMergeStateKind,
item: GitHubPRMergeStateInput
): Pick<GitHubPRMergeStatePresentation, 'label' | 'tone' | 'tooltip'> {
switch (kind) {
case 'merged':
@@ -161,6 +163,13 @@ function presentMergeStateKind(
)
}
case 'mergeable_checks_failed':
if (getProviderChecksOutcome(item.checksSummary, item.checksStatus) === 'cancelled') {
return {
label: translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled'),
tone: MUTED_TONE,
tooltip: translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
}
}
return {
label: translate('auto.components.github.pr.merge.state.87fa36ac83', 'Checks failed'),
tone: DANGER_TONE,
@@ -222,7 +231,7 @@ export function presentGitHubPRMergeState(
): GitHubPRMergeStatePresentation {
const decision = resolveGitHubPRMergeState(item)
return {
...presentMergeStateKind(decision.kind),
...presentMergeStateKind(decision.kind, item),
directMergeAvailable: decision.directMergeAvailable,
autoMergeAction: presentAutoMergeAction(decision.autoMergeAction)
}
@@ -24,6 +24,7 @@ describe('getCheckCounts', () => {
expect(getCheckCounts(checks)).toEqual({
passing: 5,
failing: 0,
cancelled: 0,
needsAction: 0,
pending: 0,
neutral: 0
@@ -38,6 +39,7 @@ describe('getCheckCounts', () => {
expect(getCheckCounts([check('action_required'), check('failure')])).toEqual({
passing: 0,
failing: 1,
cancelled: 0,
needsAction: 1,
pending: 0,
neutral: 0
@@ -53,6 +55,7 @@ describe('getCheckCounts', () => {
expect(getCheckCounts(checks)).toEqual({
passing: 0,
failing: 0,
cancelled: 0,
needsAction: 0,
pending: 0,
neutral: 1
@@ -60,3 +63,10 @@ describe('getCheckCounts', () => {
expect(getChecksSummaryLabel(checks)).toBe('0 of 1 check passing')
})
})
it('keeps cancelled chips distinct and pending ahead of cancellation', () => {
const checks = [check('success'), check('cancelled')]
expect(getCheckCounts(checks)).toMatchObject({ passing: 1, failing: 0, cancelled: 1 })
expect(getChecksSummaryLabel(checks)).toBe('Cancelled')
expect(getChecksSummaryLabel([...checks, check('pending')])).toBe('1 check pending')
})
+13 -2
View File
@@ -5,6 +5,7 @@ import type { PRCheckDetail } from '../../../shared/github/check-types'
export type PRCheckCounts = {
passing: number
failing: number
cancelled: number
needsAction: number
pending: number
neutral: number
@@ -26,7 +27,8 @@ export function getCheckCounts(checks: readonly PRCheckDetail[]): PRCheckCounts
).length
return {
passing: summary.passed,
failing: summary.failed - needsAction,
failing: summary.failed - needsAction - (summary.cancelled ?? 0),
cancelled: summary.cancelled ?? 0,
needsAction,
pending: summary.pending,
neutral: summary.neutral
@@ -35,7 +37,7 @@ export function getCheckCounts(checks: readonly PRCheckDetail[]): PRCheckCounts
/** `tone` keys the caller's `CHECK_COLOR`/`CHECK_ICON` maps so styling stays with the surface. */
export type PRCheckCountChip = {
tone: 'success' | 'failure' | 'action_required' | 'pending' | 'neutral'
tone: 'success' | 'failure' | 'cancelled' | 'action_required' | 'pending' | 'neutral'
label: string
}
@@ -58,6 +60,12 @@ export function getCheckCountChips(counts: PRCheckCounts): PRCheckCountChip[] {
})
})
}
if (counts.cancelled > 0) {
chips.push({
tone: 'cancelled',
label: `${counts.cancelled} ${translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')}`
})
}
if (counts.needsAction > 0) {
chips.push({
tone: 'action_required',
@@ -112,6 +120,9 @@ export function getChecksSummaryLabel(checks: readonly PRCheckDetail[]): string
defaultValue_one: '{{count}} check pending'
})
}
if (counts.cancelled > 0) {
return translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
}
if (counts.passing === checks.length) {
return translate('auto.components.prCheckCounts.allChecksPassing', 'All checks passing')
}
@@ -158,17 +158,21 @@ const PARITY_CASES: ParityCase[] = [
describe('provider check classification parity', () => {
it.each(PARITY_CASES)(
'$name resolves identically on every desktop surface',
'$name preserves shared counts across complete and detail sources',
({ checks, expected, gitLabJobStatuses, gitLabPipelineStatus }) => {
const summary = { ...expected, total: checks.length }
const summary = {
...expected,
total: checks.length,
cancelled: checks.filter((check) => check.conclusion === 'cancelled').length
}
expect(summarizeProviderChecks(checks)).toEqual(summary)
expect(deriveTaskPagePRCheckSummary(checks)).toEqual(summary)
expect(deriveTaskPagePRCheckSummary(checks)).toEqual({ ...summary, cancelled: undefined })
// Keep pane counts aligned with sidebar rollups, including the neutral unresolved bucket.
const paneCounts = getCheckCounts(checks)
expect({
passed: paneCounts.passing,
// Both panes split action_required into its own amber chip; it is still the failed bucket.
failed: paneCounts.failing + paneCounts.needsAction,
failed: paneCounts.failing + paneCounts.needsAction + paneCounts.cancelled,
pending: paneCounts.pending,
neutral: paneCounts.neutral
}).toEqual({
@@ -1,8 +1,7 @@
import { getChecksTabSummaryPresentation } from '@/components/github-item-dialog/inspect-pull-request/checks-tab-summary'
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { CircleDashed } from 'lucide-react'
import { useAppStore } from '@/store'
import { useMountedRef } from '@/hooks/useMountedRef'
import { CHECK_COLOR, CHECK_ICON } from '@/components/right-sidebar/checks-panel/check-presentation'
import {
createGitHubChecksTabState,
resolveGitHubChecksTabState,
@@ -162,27 +161,7 @@ export function ChecksTab({
const failedChecks = getBrokenChecks(list)
const counts = getCheckCounts(list)
const summaryLabel = getChecksSummaryLabel(list)
// All-neutral checks remain unresolved; list length alone cannot justify a green tick.
const SummaryIcon =
counts.failing > 0
? CHECK_ICON.failure
: counts.needsAction > 0
? CHECK_ICON.action_required
: counts.pending > 0
? CHECK_ICON.pending
: counts.passing > 0
? CHECK_ICON.success
: CircleDashed
const summaryColor =
counts.failing > 0
? CHECK_COLOR.failure
: counts.needsAction > 0
? CHECK_COLOR.action_required
: counts.pending > 0
? CHECK_COLOR.pending
: counts.passing > 0
? CHECK_COLOR.success
: 'text-muted-foreground'
const { SummaryIcon, summaryColor } = getChecksTabSummaryPresentation(counts)
const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0)
const handleRefresh = useCallback(
@@ -118,7 +118,7 @@ export function buildChecksToolbar({
onSelect={() => onRerun(true)}
>
<RefreshCw className="size-4" />
{translate('auto.components.PullRequestPage.68605516dd', 'Rerun failed checks')}
{translate('checks.rerunUnsuccessful', 'Rerun unsuccessful')}
</DropdownMenuItem>
<DropdownMenuItem disabled={rerunning} onSelect={() => onRerun(false)}>
<RefreshCw className="size-4" />
@@ -93,6 +93,7 @@ export default function HostedReviewActions({
mergeStateStatus: review.mergeStateStatus,
reviewDecision: review.reviewDecision,
checksStatus: review.status,
checksSummary: review.checksSummary,
autoMergeEnabled: review.autoMergeEnabled,
autoMergeAllowed: review.autoMergeAllowed,
mergeQueueRequired: review.mergeQueueRequired
@@ -1,6 +1,8 @@
import { getProviderChecksOutcome } from '../../../../shared/provider-check-summary'
import type { AppState } from '../../store/types'
import { getRepoMapFromState, getWorktreeMapFromState } from '../../store/selectors'
import type { CheckStatus } from '../../../../shared/github/pull-request-types'
import type { CheckStatus as CoarseCheckStatus } from '../../../../shared/github/pull-request-types'
type CheckStatus = CoarseCheckStatus | 'cancelled'
import { getGitHubPRCacheKey } from '../../../../shared/github/github-cache-key'
import { getHostedReviewCacheKey } from '../../store/slices/hosted-review-cache-identity'
import { isGitHubPRSuppressed } from '../../../../shared/worktree/github-pr-suppression'
@@ -104,7 +106,7 @@ function computeActiveChecksStatus(state: ActiveChecksStatusState): CheckStatus
)
const hostedReview = state.hostedReviewCache?.[hostedReviewCacheKey]?.data ?? null
if (hostedReview && hostedReview.provider !== 'github') {
return hostedReview.status
return getProviderChecksOutcome(hostedReview.checksSummary, hostedReview.status)
}
if (
(activeWorktree.linkedGitLabMR ?? null) !== null ||
@@ -116,10 +118,10 @@ function computeActiveChecksStatus(state: ActiveChecksStatusState): CheckStatus
}
const branchPR = state.prCache[prCacheKey]?.data ?? null
if (branchPR && !isGitHubPRSuppressed(activeWorktree, branchPR.number)) {
return branchPR.checksStatus
return getProviderChecksOutcome(branchPR.checksSummary, branchPR.checksStatus)
}
return hostedReview?.provider === 'github' &&
!isGitHubPRSuppressed(activeWorktree, hostedReview.number)
? hostedReview.status
? getProviderChecksOutcome(hostedReview.checksSummary, hostedReview.status)
: null
}
@@ -1,7 +1,8 @@
import React from 'react'
import { MoreHorizontal } from 'lucide-react'
import type { ActiveRightSidebarTab } from '@/store/slices/editor'
import type { CheckStatus } from '../../../../shared/github/pull-request-types'
import type { CheckStatus as CoarseCheckStatus } from '../../../../shared/github/pull-request-types'
type CheckStatus = CoarseCheckStatus | 'cancelled'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { RIGHT_SIDEBAR_HEADER_NO_DRAG_CLASS_NAME } from './right-sidebar-titlebar-drag-regions'
@@ -33,14 +34,17 @@ const STATUS_DOT_COLOR: Record<CheckStatus, string> = {
success: 'bg-emerald-500',
failure: 'bg-rose-500',
pending: 'bg-amber-500',
neutral: 'bg-muted-foreground'
neutral: 'bg-muted-foreground',
cancelled: 'bg-muted-foreground'
}
function activityItemAriaLabel(item: ActivityBarItem, status?: CheckStatus | null): string {
const base = item.shortcut ? `${item.title} (${item.shortcut})` : item.title
return status === 'failure'
? `${base} — ${translate('auto.components.right.sidebar.activityBar.error', 'Error')}`
: base
: status === 'cancelled'
? `${base} — ${translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')}`
: base
}
export function TopActivityOverflowMenu({
@@ -78,7 +82,9 @@ export function TopActivityOverflowMenu({
aria-label={
hiddenItemStatus === 'failure'
? `${moreTabsLabel} — ${translate('auto.components.right.sidebar.activityBar.error', 'Error')}`
: moreTabsLabel
: hiddenItemStatus === 'cancelled'
? `${moreTabsLabel} — ${translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')}`
: moreTabsLabel
}
>
<MoreHorizontal size={16} />
@@ -534,3 +534,38 @@ describe('CheckJobLogTail', () => {
expect(markup).toContain('max-h-[min(50vh,32rem)]')
})
})
it.each([
[['cancelled', 'success'], 'Cancelled', 'text-muted-foreground'],
[['cancelled', 'pending'], '1 check pending', 'text-amber-500'],
[['cancelled', 'failure'], '1 failing check', 'text-rose-500']
] as const)('renders triage for %j and retains Fix', (conclusions, label, tone) => {
const checks = conclusions.map((conclusion) => ({
name: conclusion,
status: 'completed' as const,
conclusion,
url: null
}))
const markup = renderToStaticMarkup(
<PRTriageStrip
checks={checks}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
)
expect(markup).toContain(label)
expect(markup).toContain(tone)
expect(markup).toContain('Fix')
const list = renderWithTooltips(
<ChecksList checks={checks} checksLoading={false} checkDetailsContextKey="repo:42" />
)
expect(list).toContain('1 Cancelled')
if (conclusions.some((conclusion) => conclusion === 'failure')) {
expect(list).toContain('1 failing')
expect(list).not.toContain('2 failing')
} else {
expect(list).not.toContain('failing')
}
})
@@ -44,6 +44,7 @@ export function ChecksList(props: ChecksListProps): React.JSX.Element {
rows,
passingCount,
failingCount,
cancelledCount,
pendingCount,
neutralCount,
toggleCheckExpanded,
@@ -82,6 +83,13 @@ export function ChecksList(props: ChecksListProps): React.JSX.Element {
)}
</span>
)}
{cancelledCount > 0 && (
<span className="flex items-center gap-1 text-muted-foreground">
<CircleDashed className="size-3" />
{cancelledCount}{' '}
{translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')}
</span>
)}
{pendingCount > 0 && (
<span className="flex items-center gap-1">
<LoaderCircle className="size-3 text-amber-500" />
@@ -1,5 +1,6 @@
import { defineFixture } from '../../../component-fixtures/component-fixture'
import { PRTriageStrip } from './triage-strip'
import { ChecksList } from './checks-list'
export const conflictsAndFailedChecks = defineFixture({
width: 305,
@@ -21,3 +22,29 @@ export const conflictsAndFailedChecks = defineFixture({
</div>
)
})
const cancellationChecks = [
{ name: 'build', status: 'completed', conclusion: 'success', url: null },
{ name: 'old run', status: 'completed', conclusion: 'cancelled', url: null }
] as const
export const cancelledChecks = defineFixture({
width: 305,
render: () => (
<div className="bg-background text-foreground">
<PRTriageStrip
checks={[...cancellationChecks]}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
<ChecksList
checks={[...cancellationChecks]}
checksLoading={false}
checkDetailsContextKey="fixture:cancelled"
onLoadCheckDetails={async () => null}
/>
</div>
)
})
@@ -11,7 +11,10 @@ import {
import { Button } from '@/components/ui/button'
import type { PRCheckDetail } from '../../../../../shared/github/check-types'
import type { ConflictReview } from './conflict-summary'
import { summarizeProviderChecks } from '../../../../../shared/provider-check-summary'
import {
summarizeProviderChecks,
getProviderChecksOutcome
} from '../../../../../shared/provider-check-summary'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { CHECK_COLOR } from './check-presentation'
@@ -50,7 +53,8 @@ export function PRTriageStrip({
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
const failingCount = summary.failed - (summary.cancelled ?? 0)
const outcome = getProviderChecksOutcome(summary)
const brokenCheckCount = getBrokenChecks(checks).length
const pendingCount = summary.pending
const pipelineFailed = pipelineCheck ? isBrokenCheck(pipelineCheck) : false
@@ -72,7 +76,16 @@ export function PRTriageStrip({
{conflictStrip}
<div className="border-b border-border px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
<CircleX className="size-3.5 shrink-0 text-rose-500" />
<CircleX
className={cn(
'size-3.5 shrink-0',
pipelineFailed || outcome === 'failure'
? CHECK_COLOR.failure
: outcome === 'pending'
? CHECK_COLOR.pending
: CHECK_COLOR.cancelled
)}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-[11px] font-medium text-foreground">
{pipelineCheck && pipelineFailed
@@ -81,11 +94,22 @@ export function PRTriageStrip({
'{{name}} failed',
{ name: pipelineCheck.name }
)
: translate(
'auto.components.right.sidebar.checks.panel.content.failingChecks',
'{{count}} failing checks',
{ count: failingCount, defaultValue_one: '{{count}} failing check' }
)}
: outcome === 'cancelled'
? translate(
'auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0',
'Cancelled'
)
: 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(
@@ -67,10 +67,12 @@ export function useChecksListState({
// Share the pill's classifier for skipped passes and completed-without-verdict unresolved checks.
const {
passed: passingCount,
failed: failingCount,
failed: inclusiveFailingCount,
cancelled: cancelledCount = 0,
pending: pendingCount,
neutral: neutralCount
} = summarizeProviderChecks(checks)
const failingCount = inclusiveFailingCount - cancelledCount
useEffect(() => {
const validKeys = new Set(rows.map((row) => row.key))
@@ -344,6 +346,7 @@ export function useChecksListState({
rows,
passingCount,
failingCount,
cancelledCount,
pendingCount,
neutralCount,
toggleCheckExpanded,
@@ -1,3 +1,4 @@
import { getProviderChecksOutcome } from '../../../../shared/provider-check-summary'
import type { CheckStatus } from '../../../../shared/github/pull-request-types'
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
import { translate } from '@/i18n/i18n'
@@ -60,13 +61,17 @@ export function classifyKnownReviewStatus(review: HostedReviewInfo): ParentPrChe
if (review.state === 'draft') {
return 'draft'
}
if (review.status === 'failure') {
const outcome = getProviderChecksOutcome(review.checksSummary, review.status)
if (outcome === 'cancelled') {
return 'cancelled'
}
if (outcome === 'failure') {
return 'failing'
}
if (review.status === 'pending') {
if (outcome === 'pending') {
return 'pending'
}
if (review.status === 'success') {
if (outcome === 'success') {
return 'success'
}
return 'neutral'
@@ -86,6 +91,7 @@ export function groupForRowStatus(status: ParentPrChecksRowStatus): ParentPrChec
return 'merged'
case 'success':
return 'passing'
case 'cancelled':
case 'draft':
case 'neutral':
return 'draftOrNoChecks'
@@ -102,7 +108,7 @@ export function groupForRowStatus(status: ParentPrChecksRowStatus): ParentPrChec
export function getRowCheckTone(
status: ParentPrChecksRowStatus,
review: HostedReviewInfo | null | undefined
): CheckStatus {
): CheckStatus | 'cancelled' {
if (
['failing', 'conflict', 'closed', 'linkedDetailsUnavailable', 'refreshError'].includes(status)
) {
@@ -114,7 +120,7 @@ export function getRowCheckTone(
if (status === 'success' || status === 'merged') {
return 'success'
}
return review?.status ?? 'neutral'
return status === 'cancelled' ? 'cancelled' : (review?.status ?? 'neutral')
}
export function getRowSummary(
@@ -136,6 +142,8 @@ export function getRowSummary(
)
}
switch (status) {
case 'cancelled':
return translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
case 'failing':
return translate(
'auto.components.rightSidebar.parentPrChecks.rowSummary.checksFailing',
@@ -1,6 +1,7 @@
import type { PRCheckDetail } from '../../../../shared/github/check-types'
import type {
CheckStatus,
ProviderCheckSummary,
GitHubRepositoryIdentity,
PRInfo
} from '../../../../shared/github/pull-request-types'
@@ -32,6 +33,7 @@ export type ParentPrChecksRowStatus =
| 'refreshError'
| 'unsupported'
| 'unavailable'
| 'cancelled'
| 'failing'
| 'pending'
| 'success'
@@ -58,7 +60,8 @@ export type ParentPrChecksRow = {
branch: string | null
status: ParentPrChecksRowStatus
group: ParentPrChecksGroupKey
checkTone: CheckStatus
checkTone: CheckStatus | 'cancelled'
checksSummary?: ProviderCheckSummary
title: string
reviewNumber: number | null
reviewLabel: string | null
@@ -22,6 +22,7 @@ import {
type ParentPrChecksProjection,
type ParentPrChecksRefreshOutcome,
type ParentPrChecksRow,
type ParentPrChecksRowStatus,
type ParentPrChecksSummary
} from './parent-pr-checks-row-types'
import {
@@ -125,7 +126,7 @@ function buildParentPrChecksRow(
hasFallbackReview: fallbackDisplay !== null
})
const checkDetails = getCheckDetails(args, review, branch)
const detailNames = getCheckDetailNames(checkDetails)
const detailNames = getCheckDetailNames(checkDetails, status)
return {
id: args.worktree.id,
@@ -136,6 +137,7 @@ function buildParentPrChecksRow(
status,
group: groupForRowStatus(status),
checkTone: getRowCheckTone(status, review),
checksSummary: review?.checksSummary,
title: review?.title ?? fallbackDisplay?.title ?? branch ?? args.worktree.displayName,
reviewNumber: review?.number ?? fallbackDisplay?.number ?? null,
reviewLabel: getReviewLabel(review, fallbackDisplay),
@@ -221,12 +223,15 @@ function getCheckDetails(
return getGitHubChecksEntry({ ...args, repo: args.repo }, review)?.data ?? []
}
function getCheckDetailNames(checks: readonly PRCheckDetail[]): string[] {
function getCheckDetailNames(
checks: readonly PRCheckDetail[],
status: ParentPrChecksRowStatus
): string[] {
const interesting = checks.filter(
(check) =>
check.conclusion === 'failure' ||
check.conclusion === 'timed_out' ||
check.conclusion === 'cancelled' ||
(check.conclusion === 'cancelled' && status === 'cancelled') ||
check.conclusion === 'action_required' ||
check.conclusion === 'pending' ||
check.conclusion === null ||
@@ -1,6 +1,7 @@
import React from 'react'
import type { ActiveRightSidebarTab, ActivityBarPosition } from '@/store/slices/editor'
import type { CheckStatus } from '../../../../shared/github/pull-request-types'
import type { CheckStatus as CoarseCheckStatus } from '../../../../shared/github/pull-request-types'
type CheckStatus = CoarseCheckStatus | 'cancelled'
import { cn } from '@/lib/utils'
import { TooltipProvider } from '@/components/ui/tooltip'
import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
@@ -25,6 +25,7 @@ export type HostedReviewActionInfo = Pick<
Partial<
Pick<
HostedReviewInfo,
| 'checksSummary'
| 'reviewDecision'
| 'autoMergeEnabled'
| 'autoMergeAllowed'
@@ -1,3 +1,4 @@
import { getProviderChecksOutcome } from '../../../../shared/provider-check-summary'
import React from 'react'
import { translate } from '@/i18n/i18n'
import type { WorkspaceReviewChecksSummary } from '../../../../shared/workspace-review-checks'
@@ -5,16 +6,19 @@ import { ReviewIcon } from './worktree-review-helpers'
import { reviewStateLabel } from './WorktreeCardMetadataStatusBadges'
export function workspaceReviewChecksLabel(summary: WorkspaceReviewChecksSummary): string {
const outcome = getProviderChecksOutcome(summary)
const status =
summary.state === 'failure'
? translate('workspace.links.checksFailed', 'Failed')
: summary.state === 'pending'
? translate('workspace.links.checksPending', 'Pending')
: summary.state === 'success'
? translate('workspace.links.checksPassing', 'Passing')
: summary.state === 'unknown'
? translate('workspace.links.checksUnknown', 'Unknown')
: translate('workspace.links.checksNeutral', 'Neutral')
outcome === 'cancelled'
? translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
: outcome === 'failure'
? translate('workspace.links.checksFailed', 'Failed')
: outcome === 'pending'
? translate('workspace.links.checksPending', 'Pending')
: outcome === 'success'
? translate('workspace.links.checksPassing', 'Passing')
: outcome === 'unknown'
? translate('workspace.links.checksUnknown', 'Unknown')
: translate('workspace.links.checksNeutral', 'Neutral')
const checks = translate(
'workspace.links.allReviewChecks',
'Reviews: {{count}} · Checks: {{status}}',
@@ -1,6 +1,6 @@
import React from 'react'
import type { GitConflictOperation } from '../../../../shared/git-status-types'
import type { CheckStatus } from '../../../../shared/github/pull-request-types'
import type { CheckPresentationOutcome } from '../../../../shared/provider-check-summary'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import { translate } from '@/i18n/i18n'
@@ -8,7 +8,7 @@ export function branchDisplayName(branch: string): string {
return branch.replace(/^refs\/heads\//, '')
}
export function checksLabel(status: CheckStatus): string {
export function checksLabel(status: CheckPresentationOutcome): string {
switch (status) {
case 'success':
return translate('auto.components.sidebar.WorktreeCardHelpers.checksPassing', 'Passing')
@@ -16,7 +16,11 @@ export function checksLabel(status: CheckStatus): string {
return translate('auto.components.sidebar.WorktreeCardHelpers.checksFailing', 'Failing')
case 'pending':
return translate('auto.components.sidebar.WorktreeCardHelpers.checksPending', 'Pending')
case 'cancelled':
return translate('auto.components.editor.CheckRunDetailsPanel.91a4c7e2b0', 'Cancelled')
case 'neutral':
case 'none':
case 'unknown':
return ''
}
}
@@ -1,3 +1,4 @@
import { getProviderChecksOutcome } from '../../../../shared/provider-check-summary'
import React from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { getReviewChecksTone } from './worktree-review-helpers'
@@ -86,10 +87,13 @@ export function ReviewStateBadge({
}
export function ReviewChecksBadge({
status
status: coarseStatus,
checksSummary
}: {
checksSummary?: WorktreeCardPrDisplay['checksSummary']
status: WorktreeCardPrDisplay['status']
}): React.JSX.Element | null {
const status = coarseStatus ? getProviderChecksOutcome(checksSummary, coarseStatus) : undefined
if (!status || status === 'neutral') {
return null
}
@@ -102,6 +106,16 @@ export function ReviewChecksBadge({
}
)
if (status === 'cancelled') {
return (
<MetadataStatusBadge
label={label}
className="border-border bg-muted/30 text-muted-foreground"
>
<CircleX />
</MetadataStatusBadge>
)
}
if (status === 'success') {
return (
<MetadataStatusBadge
@@ -167,10 +181,13 @@ export function reviewStateLabel(
}
export function ReviewChecksIndicator({
status
status: coarseStatus,
checksSummary
}: {
checksSummary?: WorktreeCardPrDisplay['checksSummary']
status: WorktreeCardPrDisplay['status']
}): React.JSX.Element | null {
const status = coarseStatus ? getProviderChecksOutcome(checksSummary, coarseStatus) : undefined
if (!status || status === 'neutral') {
return null
}
@@ -181,7 +198,12 @@ export function ReviewChecksIndicator({
status: checksLabel(status)
}
)
const Icon = status === 'success' ? CircleCheck : status === 'failure' ? CircleX : Clock
const Icon =
status === 'success'
? CircleCheck
: status === 'failure' || status === 'cancelled'
? CircleX
: Clock
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -166,7 +166,9 @@ export function WorktreeCardReferenceRow({
</span>
) : null}
<span className={cn('shrink-0', stale && STALE_SHIMMER)}>{label}</span>
{review?.status ? <ReviewChecksIndicator status={review.status} /> : null}
{review?.status ? (
<ReviewChecksIndicator status={review.status} checksSummary={review.checksSummary} />
) : null}
{sourceText ? (
<>
<span aria-hidden className="shrink-0">
@@ -1,3 +1,4 @@
import { getProviderChecksOutcome } from '../../../../shared/provider-check-summary'
import React from 'react'
import { Bell, GitBranch, Moon } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
@@ -5,7 +6,7 @@ import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { observeOffscreenAnimation } from '@/lib/offscreen-animation-pause'
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
import { FilledBellIcon } from './WorktreeCardHelpers'
import { FilledBellIcon, checksLabel } from './WorktreeCardHelpers'
import StatusIndicator from './StatusIndicator'
import { useWorktreeActivityStatus } from './use-worktree-activity-status'
import { useIsSleepingWorktree } from './use-worktree-sleep-state'
@@ -86,13 +87,19 @@ function getReviewStatusLabel(review: WorktreeCardPrDisplay): string {
if (review.state === 'draft') {
return translate('workspace.reviewStatus.draft', '{{review}}: Draft', values)
}
if (review.status === 'failure') {
const outcome = review.status
? getProviderChecksOutcome(review.checksSummary, review.status)
: undefined
if (outcome === 'cancelled') {
return `${getReviewLabel(review)}: ${checksLabel(outcome)}`
}
if (outcome === 'failure') {
return translate('workspace.reviewStatus.checksFailed', '{{review}} checks: Failed', values)
}
if (review.status === 'pending') {
if (outcome === 'pending') {
return translate('workspace.reviewStatus.checksPending', '{{review}} checks: Pending', values)
}
if (review.status === 'success') {
if (outcome === 'success') {
return translate('workspace.reviewStatus.checksPassing', '{{review}} checks: Passing', values)
}
return translate('workspace.reviewStatus.open', '{{review}}: Open', values)
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { WorktreeReferenceStack } from './WorktreeReferenceStack'
import { getWorkspaceAttachmentKey } from '../../../../shared/workspace-attachment-normalization'
import { referenceAttachment, referenceReview } from './workspace-reference-fixtures.test-support'
import { summarizeProviderChecks } from '../../../../shared/provider-check-summary'
afterEach(cleanup)
describe('fixed workspace reference stacks', () => {
@@ -30,4 +31,45 @@ describe('fixed workspace reference stacks', () => {
expect(container.querySelector('[data-reference-count]')?.textContent).toBe('3')
expect(container.textContent).toContain('PR #2 · draft')
})
it.each([
['failure', 'text-rose-500/85'],
['pending', 'text-amber-500/85'],
['success', 'text-muted-foreground/60']
] as const)(
'orders cancellation between pending and passing with a %s sibling',
(status, tone) => {
const items = [referenceAttachment(1), referenceAttachment(2)]
const cancelledIndex = status === 'success' ? 1 : 0
const cancellation = {
status: 'failure' as const,
checksSummary: summarizeProviderChecks([{ status: 'completed', conclusion: 'cancelled' }])
}
const details = Object.fromEntries(
items.map((item, index) => [
getWorkspaceAttachmentKey(item),
{
title: '',
url: item.url ?? '',
review: referenceReview(
item.number,
index === cancelledIndex ? cancellation : { status }
)
}
])
)
const { container } = render(<WorktreeReferenceStack items={items} details={details} />)
expect(container.querySelector('[data-reference-front] svg')?.classList.contains(tone)).toBe(
true
)
expect(container.textContent).toContain(`PR #${cancelledIndex + 1} · open · Cancelled`)
if (status !== 'success') {
expect(container.textContent).toContain(
`PR #2 · open · ${status}, PR #1 · open · Cancelled`
)
} else {
expect(container.textContent).toContain('PR #2 · open · Cancelled, PR #1 · open · success')
}
}
)
})

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