fix(mobile): report task pages and review state accurately (#36115)

* fix(mobile): report task pages and review state accurately

* test(mobile): record task page error provenance

* test(mobile): record task page errors in reply matrices
This commit is contained in:
Kelvin Amoaba
2026-10-29 22:09:45 +00:00
committed by GitHub
parent 0a75796a03
commit d5611cf274
25 changed files with 1009 additions and 687 deletions
@@ -228,46 +228,6 @@
}
]
},
"49c5fd241816": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"50982acf6b28": {
"name": "github.countWorkItems#1",
"ordinal": 14,
@@ -650,6 +610,47 @@
}
}
},
"d0a974f0c6a1": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"de7bfe07e04b": {
"name": "github.countWorkItems#1",
"ordinal": 14,
@@ -748,7 +749,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -781,7 +782,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -815,7 +816,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -849,7 +850,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -883,7 +884,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -917,7 +918,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -951,7 +952,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -985,7 +986,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -1019,7 +1020,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -1053,7 +1054,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -1087,7 +1088,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -1121,7 +1122,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "1c97db0775ed"
},
"state": "67b5ebc67646",
@@ -198,46 +198,6 @@
}
]
},
"49c5fd241816": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"5e1882223b41": {
"name": "linear.listTeams#1",
"ordinal": 7,
@@ -342,18 +302,6 @@
}
}
},
"737f20de5cc0": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 1,
"items": [],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {}
}
},
"823a0f3cec86": {
"name": "github.listWorkItems#1",
"ordinal": 13,
@@ -369,6 +317,19 @@
"ordinal": 9,
"value": ["team-1"]
},
"925a40556fcf": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 1,
"items": [],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {}
}
},
"93695401e4f8": {
"name": "linear.listTeams#1",
"ordinal": 6,
@@ -451,6 +412,19 @@
}
}
},
"a9e32280eff6": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {}
}
},
"adcbf91f89c3": {
"status": "fulfilled",
"startedAt": 0,
@@ -566,6 +540,47 @@
}
}
},
"d0a974f0c6a1": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"d34ffc174996": {
"name": "github.listWorkItems#1",
"ordinal": 12,
@@ -672,18 +687,6 @@
}
}
},
"e4b031c4e9d5": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {}
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
@@ -786,7 +789,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -819,7 +822,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -841,7 +844,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -874,7 +877,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -896,7 +899,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -929,7 +932,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -951,7 +954,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -984,7 +987,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1006,7 +1009,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -1039,7 +1042,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1061,7 +1064,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -1094,7 +1097,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1116,7 +1119,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -1149,7 +1152,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1171,7 +1174,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "e4b031c4e9d5"
"github-page-2": "a9e32280eff6"
},
"state": "67b5ebc67646",
"effects": [
@@ -1204,7 +1207,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "e4b031c4e9d5",
"github-page-2": "a9e32280eff6",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1226,7 +1229,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -1259,7 +1262,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1281,7 +1284,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0"
"github-page-2": "925a40556fcf"
},
"state": "67b5ebc67646",
"effects": [
@@ -1314,7 +1317,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "737f20de5cc0",
"github-page-2": "925a40556fcf",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1336,7 +1339,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "e4b031c4e9d5"
"github-page-2": "a9e32280eff6"
},
"state": "67b5ebc67646",
"effects": [
@@ -1369,7 +1372,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "e4b031c4e9d5",
"github-page-2": "a9e32280eff6",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -165,46 +165,6 @@
}
]
},
"49c5fd241816": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"5e1882223b41": {
"name": "linear.listTeams#1",
"ordinal": 7,
@@ -694,6 +654,47 @@
"code": "incompatible_reply"
}
},
"d0a974f0c6a1": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"de1f2f536bd0": {
"name": "github.countWorkItems#1",
"ordinal": 13,
@@ -941,7 +942,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -974,7 +975,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1023,7 +1024,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1050,7 +1051,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1093,7 +1094,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1120,7 +1121,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1163,7 +1164,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1190,7 +1191,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1233,7 +1234,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1260,7 +1261,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1303,7 +1304,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1330,7 +1331,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "cb437d5a4339",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1373,7 +1374,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "32a7c0ae7918",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1400,7 +1401,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "32a7c0ae7918",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1443,7 +1444,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "f3b516f62081",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1470,7 +1471,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "f3b516f62081",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1513,7 +1514,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "b948e8307e81",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1540,7 +1541,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "b948e8307e81",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1583,7 +1584,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "a947768bc0ed",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1610,7 +1611,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "a947768bc0ed",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -1653,7 +1654,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "c7584e82c72f",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "bd5c2292b89a",
"effects": ["f57b6504f168", "470893ebd4a7", "5efa1426d38f"]
@@ -1680,7 +1681,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "c7584e82c72f",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "bd5c2292b89a",
@@ -221,46 +221,6 @@
}
]
},
"49c5fd241816": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"520f24cdfcf1": {
"name": "settings.update#1",
"ordinal": 3,
@@ -841,6 +801,47 @@
"ordinal": 3,
"value": false
},
"d0a974f0c6a1": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"d44ba8747c4c": {
"name": "selectedLinearWorkspaceId",
"ordinal": 7,
@@ -1101,7 +1102,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1134,7 +1135,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1183,7 +1184,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "db17b820bb60",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1198,7 +1199,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "db17b820bb60",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1241,7 +1242,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "db17b820bb60",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1256,7 +1257,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "db17b820bb60",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1311,7 +1312,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": [
@@ -1332,7 +1333,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1393,7 +1394,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": [
@@ -1414,7 +1415,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1475,7 +1476,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": [
@@ -1496,7 +1497,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1545,7 +1546,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "32a7c0ae7918",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1560,7 +1561,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "32a7c0ae7918",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1603,7 +1604,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "f3b516f62081",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1618,7 +1619,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "f3b516f62081",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1661,7 +1662,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "b948e8307e81",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1676,7 +1677,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "b948e8307e81",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1719,7 +1720,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "a947768bc0ed",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1734,7 +1735,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "a947768bc0ed",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -1777,7 +1778,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "c7584e82c72f",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "0577812165ed",
"effects": []
@@ -1792,7 +1793,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "c7584e82c72f",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "0577812165ed",
@@ -186,46 +186,6 @@
}
]
},
"49c5fd241816": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"534fb96c7e7f": {
"name": "settings.update#1",
"ordinal": 10,
@@ -567,6 +527,47 @@
}
}
},
"d0a974f0c6a1": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"d6a6916c7399": {
"name": "settings.update#1",
"ordinal": 10,
@@ -732,7 +733,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -765,7 +766,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -807,7 +808,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -840,7 +841,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -882,7 +883,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -915,7 +916,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -957,7 +958,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -990,7 +991,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1032,7 +1033,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1065,7 +1066,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1107,7 +1108,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1140,7 +1141,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1182,7 +1183,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1215,7 +1216,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1257,7 +1258,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1290,7 +1291,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1332,7 +1333,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1365,7 +1366,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1407,7 +1408,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1440,7 +1441,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -1482,7 +1483,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -1515,7 +1516,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
@@ -85,46 +85,6 @@
}
]
},
"49c5fd241816": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"5e1882223b41": {
"name": "linear.listTeams#1",
"ordinal": 7,
@@ -294,6 +254,47 @@
}
}
},
"d0a974f0c6a1": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"errorTypes": [],
"failedCount": 0,
"items": [
{
"key": "github:repo-1:issue:9",
"provider": "github",
"source": {
"author": {
"$rpc": "null"
},
"id": "issue:9",
"labels": [],
"number": 9,
"repoId": "repo-1",
"repoName": "Repo",
"state": "open",
"title": "An issue",
"type": "issue",
"updatedAt": "2020-01-01T00:00:00.000Z",
"url": ""
},
"status": "Open",
"subtitle": "Repo #9",
"title": "An issue",
"updatedAt": "2020-01-01T00:00:00.000Z"
}
],
"sourceErrors": [],
"sourceFallbacks": [],
"sourcesByRepoId": {
"repo-1": {
"issues": "upstream"
}
}
}
},
"de7bfe07e04b": {
"name": "github.countWorkItems#1",
"ordinal": 14,
@@ -392,7 +393,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816"
"github-page-2": "d0a974f0c6a1"
},
"state": "67b5ebc67646",
"effects": [
@@ -425,7 +426,7 @@
"mount": "eb79a9b3682a",
"linear-context-0": "eb79a9b3682a",
"persist-teams-1": "eb79a9b3682a",
"github-page-2": "49c5fd241816",
"github-page-2": "d0a974f0c6a1",
"github-count-3": "adcbf91f89c3"
},
"state": "67b5ebc67646",
+15
View File
@@ -3869,6 +3869,21 @@
},
"worktreeCreateRetry": {
"createFailed": "Failed to create workspace"
},
"hostedMerge": {
"merged": "Merged",
"closed": "Closed",
"conflicts": "Conflicts",
"behind": "Behind",
"blocked": "Blocked",
"checksPending": "Checks pending",
"ableToMerge": "Able to merge",
"unknown": "Unknown"
},
"pagination": {
"pageUnreachable": "Page {{page}} is beyond what GitHub search can return.",
"loadPageFailed": "Page {{page}} could not be loaded from GitHub.",
"noMoreResults": "No more results on page {{page}}."
}
},
"terminal": {
@@ -2,11 +2,35 @@ import { describe, expect, it } from 'vitest'
import type { HostedReviewDecision } from '../../../src/shared/hosted-review'
import {
getHostedChecksLabel,
getHostedMergeLabel,
getHostedReviewLabel,
getHostedReviewSignalTone
} from './mobile-hosted-check-status'
describe('mobile hosted check status', () => {
it.each(['merged', 'closed'] as const)('shows %s before stale merge enrichment', (state) => {
expect(getHostedMergeLabel({ state, mergeable: 'CONFLICTING' })).toBe(
state === 'merged' ? 'Merged' : 'Closed'
)
expect(getHostedReviewSignalTone({ state, mergeable: 'CONFLICTING' }, 'merge')).toBe('neutral')
})
it('does not call pending checks Behind', () => {
const item = {
mergeable: 'MERGEABLE' as const,
checksSummary: {
state: 'pending' as const,
total: 1,
passed: 0,
failed: 0,
pending: 1,
neutral: 0
}
}
expect(getHostedMergeLabel(item)).toBe('Checks pending')
expect(getHostedReviewSignalTone(item, 'merge')).toBe('warning')
})
it('renders a hydrated neutral GitLab summary as unresolved checks', () => {
const summary = {
state: 'neutral' as const,
+65 -19
View File
@@ -2,9 +2,12 @@ import type {
PRMergeableState,
ProviderCheckSummary
} 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'
export type MobileHostedReviewStatus = {
state?: string
checksSummary?: ProviderCheckSummary
reviewDecision?: string | null
reviewRequests?: readonly unknown[]
@@ -29,17 +32,47 @@ export function getHostedReviewLabel(item: MobileHostedReviewStatus): string {
: 'No reviewers'
}
export function getHostedMergeLabel(item: MobileHostedReviewStatus): string {
if (item.mergeable === 'CONFLICTING' || item.mergeStateStatus === 'BLOCKED') {
return 'Conflicts'
function hostedMergeState(item: MobileHostedReviewStatus) {
return resolveGitHubPRMergeState({
state:
item.state === 'merged' || item.state === 'closed' || item.state === 'draft'
? item.state
: 'open',
mergeable: item.mergeable,
mergeStateStatus: item.mergeStateStatus,
checksSummary: item.checksSummary
}).kind
}
export function getHostedMergeLabel(item: MobileHostedReviewStatus): string | null {
switch (hostedMergeState(item)) {
case 'merged':
return translate('tasks.hostedMerge.merged', 'Merged')
case 'closed':
return translate('tasks.hostedMerge.closed', 'Closed')
case 'conflicts':
return translate('tasks.hostedMerge.conflicts', 'Conflicts')
case 'behind':
return translate('tasks.hostedMerge.behind', 'Behind')
case 'blocked':
return translate('tasks.hostedMerge.blocked', 'Blocked')
case 'mergeable_checks_pending':
return translate('tasks.hostedMerge.checksPending', 'Checks pending')
case 'mergeable':
case 'mergeable_checks_failed':
case 'mergeable_checks_passed':
return translate('tasks.hostedMerge.ableToMerge', 'Able to merge')
case 'merge_status_unavailable':
case 'checks_passed':
case 'draft':
return null
case 'review_required':
case 'changes_requested':
case 'merge_queue':
case 'merge_queue_auto_merge_on':
case 'checking':
return translate('tasks.hostedMerge.unknown', 'Unknown')
}
if (item.mergeStateStatus === 'BEHIND' || item.checksSummary?.state === 'pending') {
return 'Behind'
}
if (item.mergeable === 'MERGEABLE' || item.mergeStateStatus === 'CLEAN') {
return 'Able to merge'
}
return 'Unknown'
}
export function getHostedChecksLabel(item: { checksSummary?: ProviderCheckSummary }): string {
@@ -82,14 +115,27 @@ export function getHostedReviewSignalTone(
}
return 'neutral'
}
if (item.mergeable === 'CONFLICTING' || item.mergeStateStatus === 'BLOCKED') {
return 'danger'
switch (hostedMergeState(item)) {
case 'conflicts':
case 'blocked':
return 'danger'
case 'behind':
case 'mergeable_checks_pending':
return 'warning'
case 'mergeable':
case 'mergeable_checks_failed':
case 'mergeable_checks_passed':
return 'success'
case 'merged':
case 'closed':
case 'draft':
case 'review_required':
case 'changes_requested':
case 'merge_queue':
case 'merge_queue_auto_merge_on':
case 'merge_status_unavailable':
case 'checks_passed':
case 'checking':
return 'neutral'
}
if (item.mergeStateStatus === 'BEHIND' || item.checksSummary?.state === 'pending') {
return 'warning'
}
if (item.mergeable === 'MERGEABLE' || item.mergeStateStatus === 'CLEAN') {
return 'success'
}
return 'neutral'
}
@@ -40,9 +40,9 @@ afterEach(() => {
mounted.clear()
})
function reply(request: SentRequest): RpcResponse {
function reply(request: SentRequest, rowCount = 1): RpcResponse {
if (request.method === 'github.countWorkItems') {
return { id: 'count', ok: true, result: { count: 100 } }
return { id: 'count', ok: true, result: 100 }
}
const params = request.args[1]
const query =
@@ -51,19 +51,17 @@ function reply(request: SentRequest): RpcResponse {
id: 'list',
ok: true,
result: {
items: [
{
id: 'issue-1',
type: 'issue',
number: 1,
title: query,
state: 'open',
labels: [],
url: 'https://example.com/issues/1',
updatedAt: '2026-10-27T00:00:00Z',
author: null
}
]
items: Array.from({ length: rowCount }, (_, index) => ({
id: `issue-${index}`,
type: 'issue',
number: index + 1,
title: query,
state: 'open',
labels: [],
url: 'https://example.com/issues/1',
updatedAt: '2026-10-27T00:00:00Z',
author: null
}))
}
}
}
@@ -184,11 +182,11 @@ async function mount(provider: Provider, repoCount = 30, counted = false) {
return { request: request! }
},
pending,
async answer(requests = pending()) {
async answer(requests = pending(), respond = reply) {
await act(async () => {
for (const request of requests) {
answered.add(request)
request.resolve(reply(request))
request.resolve(respond(request))
}
})
},
@@ -312,3 +310,77 @@ describe('task repository request ownership', () => {
expect(fixture.model().setRefreshing).toHaveBeenLastCalledWith(false)
})
})
describe('GitHub next-page source failures', () => {
for (const [source, type, message] of [
['prs', 'rate_limited', 'API rate limit exceeded'],
['issues', 'permission_denied', 'Resource not accessible'],
['issues', 'validation_error', 'Invalid query'],
['prs', 'validation_error', 'Only the first 1000 search results are available']
] as const) {
it(`keeps Next retryable for ${source} ${type}`, async () => {
const fixture = await mount('github', 1)
const first = await fixture.load()
await fixture.answer(fixture.pending(), (request) => reply(request, 72))
await first.request
expect(fixture.model().githubCanLoadUncountedNextPage).toBe(true)
let page: Promise<void> | undefined
await act(async () => {
page = fixture.model().handleGitHubPageChange(1)
})
await fixture.answer(fixture.pending(), () => ({
id: 'page',
ok: true,
result: { items: [], errors: { [source]: { type, message } } }
}))
await page
expect(fixture.model().setError).toHaveBeenLastCalledWith(
'Page 2 could not be loaded from GitHub.'
)
expect(fixture.model().githubCanLoadUncountedNextPage).toBe(true)
expect(fixture.model().githubCountedTotalPages).toBeNull()
expect(fixture.model().githubPages).toHaveLength(1)
})
}
it('keeps a proven issue-search window limit through a late count, resetting on reload', async () => {
const fixture = await mount('github', 1, true)
const first = await fixture.load()
await fixture.answer(fixture.pending(), (request) => reply(request, 72))
await first.request
const count = fixture.pending()
expect(starts(count, 'github.countWorkItems')).toHaveLength(1)
let page: Promise<void> | undefined
await act(async () => {
page = fixture.model().handleGitHubPageChange(1)
})
await fixture.answer(
fixture.pending().filter((request) => !count.includes(request)),
() => ({
id: 'page',
ok: true,
result: {
items: [],
errors: {
issues: {
type: 'validation_error',
message: 'Only the first 1000 search results are available'
}
}
}
})
)
await page
expect(fixture.model().setError).toHaveBeenLastCalledWith(
'Page 2 is beyond what GitHub search can return.'
)
expect(fixture.model().githubCanLoadUncountedNextPage).toBe(false)
await fixture.answer(count)
expect(fixture.model().githubCountedTotalPages).toBe(3)
expect(fixture.model().githubTotalPages).toBe(1)
const fresh = await fixture.load()
await fixture.drain()
await fresh.request
expect(fixture.model().githubTotalPages).toBe(3)
})
})
@@ -0,0 +1,29 @@
import { describe, expect, it, vi } from 'vitest'
import { getGitHubMergeLabel } from './mobile-tasks-hosted-review'
import type { GitHubWorkItem } from './mobile-tasks-provider-detail-types'
vi.mock('./mobile-tasks-item-mapping', () => ({ projectRowType: vi.fn() }))
const item: GitHubWorkItem = {
id: 'pr:30140',
type: 'pr',
number: 30140,
title: 'Page GitHub tasks by number',
state: 'merged',
url: 'https://github.com/stablyai/orca/pull/30140',
labels: [],
updatedAt: '2026-10-16T00:00:00Z',
author: null,
repoId: 'orca',
repoName: 'orca'
}
describe('mobile Tasks GitHub merge label', () => {
it.each(['merged', 'closed'] as const)('uses %s state without merge enrichment', (state) => {
expect(getGitHubMergeLabel({ ...item, state })).toBe(state === 'merged' ? 'Merged' : 'Closed')
})
it('hides the merge label for open list rows without merge enrichment', () => {
expect(getGitHubMergeLabel({ ...item, state: 'open' })).toBeNull()
})
})
+3 -23
View File
@@ -1,3 +1,4 @@
import { getHostedMergeLabel } from './mobile-hosted-check-status'
import { projectRowType } from './mobile-tasks-item-mapping'
import type {
HostedReviewItem,
@@ -131,29 +132,8 @@ export function hostedBranchSummary(item: TaskItem): { head: string; base: strin
return null
}
export function getGitHubMergeLabel(item: GitHubWorkItem): string {
if (item.mergeable === undefined && item.mergeStateStatus === undefined) {
return 'Merge'
}
if (item.state === 'merged') {
return 'Merged'
}
if (item.state === 'closed') {
return 'Closed'
}
if (item.mergeable === 'CONFLICTING') {
return 'Conflicts'
}
if (item.mergeStateStatus === 'BEHIND') {
return 'Behind'
}
if (item.mergeStateStatus === 'BLOCKED') {
return 'Blocked'
}
if (item.mergeable === 'MERGEABLE' || item.mergeStateStatus === 'CLEAN') {
return 'Able to merge'
}
return 'Unknown'
export function getGitHubMergeLabel(item: GitHubWorkItem): string | null {
return getHostedMergeLabel(item)
}
export function getHostedReviewMergeMethodLabel(method: HostedReviewMergeMethod): string {
@@ -178,6 +178,11 @@ export function renderMobileTasksProviderItemList(
const isGitLabMr = item.provider === 'gitlab' && item.source.type === 'mr'
const githubPrDelta = isGitHubPr ? formatGitHubPRDelta(item.source) : null
const branchSummary = hostedBranchSummary(item)
const mergeLabel = isGitHubPr
? getGitHubMergeLabel(item.source)
: isGitLabMr
? getHostedMergeLabel(item.source)
: null
return (
<Pressable
style={({ pressed }) => [styles.taskRow, pressed && styles.taskRowPressed]}
@@ -256,7 +261,7 @@ export function renderMobileTasksProviderItemList(
{getHostedChecksLabel(item.source)}
</Text>
</View>
{isGitHubPr || isGitLabMr ? (
{mergeLabel ? (
<View
style={[
styles.prSignalChip,
@@ -264,9 +269,7 @@ export function renderMobileTasksProviderItemList(
]}
>
<Text style={styles.prSignalText} numberOfLines={1}>
{isGitHubPr
? getGitHubMergeLabel(item.source)
: getHostedMergeLabel(item.source)}
{mergeLabel}
</Text>
</View>
) : null}
@@ -1,3 +1,5 @@
import type { ClassifiedError } from '../../../src/shared/classified-error'
import { getWorkItemPageErrorTypes } from '../../../src/shared/github/work-item-page-errors'
import type { RuntimeHydrationModel } from './use-mobile-tasks-runtime-hydration'
import { readJiraConnection, reconcileJiraConnection } from './jira-mobile-connection'
import {
@@ -138,6 +140,7 @@ export function useMobileTasksProviderLoadActions<Model extends ProviderLoadActi
): Promise<{
items: Array<Extract<TaskItem, { provider: 'github' }>>
failedCount: number
errorTypes: ClassifiedError['type'][]
sourcesByRepoId: Record<string, GitHubRepoSources>
sourceErrors: GitHubIssueSourceError[]
sourceFallbacks: GitHubIssueSourceFallback[]
@@ -161,8 +164,9 @@ export function useMobileTasksProviderLoadActions<Model extends ProviderLoadActi
query: scopeGitHubTaskSearch(appliedQuery, githubKind),
page: taskPageToGitHubApiPage(taskPage)
})
const decoded = githubWorkItemSearchRead.interpret(reply)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the schema requires `items` and each row's `labels`, and types the other eight (`id`, `type`, `number`, `title`, `state`, `url`, `updatedAt`, `author`). Two gaps keep the cast: the salvaged members type as `T | undefined` where GitHubWorkItem declares them required, and `sources`/`errors`/`issueSourceFellBack` are deliberately `z.unknown()` because the two banner extractors below read them member by member with their own guards.
const envelope = githubWorkItemSearchRead.interpret(reply) as {
const envelope = decoded as {
items: Array<Omit<GitHubWorkItem, 'repoId' | 'repoName'>>
sources?: GitHubRepoSources
errors?: { issues?: { message: string } }
@@ -171,6 +175,7 @@ export function useMobileTasksProviderLoadActions<Model extends ProviderLoadActi
return {
items: envelope.items.map((item) => createGitHubTask(repo, item)),
sources: envelope.sources,
errorTypes: getWorkItemPageErrorTypes(decoded.errors),
sourceError: extractGitHubIssueSourceError(repo, envelope),
sourceFallback: extractGitHubIssueSourceFallback(repo, envelope),
repoId: repo.id
@@ -213,6 +218,7 @@ export function useMobileTasksProviderLoadActions<Model extends ProviderLoadActi
.flatMap((result) => result.items)
.sort((a, b) => b.source.number - a.source.number),
failedCount: results.filter((result) => result.error).length,
errorTypes: results.flatMap((result) => result.errorTypes ?? []),
sourcesByRepoId,
sourceErrors,
sourceFallbacks
@@ -1,4 +1,4 @@
import { createElement } from 'react'
import { createElement, useState } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
@@ -7,6 +7,7 @@ vi.mock('./mobile-tasks-dependencies', async () => {
return {
useCallback: react.useCallback,
useMemo: react.useMemo,
useState: react.useState,
...(await import('../../../src/shared/work-items'))
}
})
@@ -43,6 +44,7 @@ function buildModel(
taskUiReady: true,
setError: vi.fn(),
setGithubCurrentPage: vi.fn(),
setGithubCountedTotalPages: vi.fn(),
setGithubLoadingTargetPage: vi.fn(),
setGithubPages: vi.fn(),
setGithubPaginationLoading: vi.fn(),
@@ -53,15 +55,19 @@ function buildModel(
let renderer: ReactTestRenderer | null = null
let changePage: ((page: number) => Promise<void>) | null = null
let canLoadUncountedNextPage = false
let totalPages = 0
function mount(model: ReturnType<typeof buildModel>): void {
function Harness(): null {
const [countedTotalPages, setCountedTotalPages] = useState(model.githubCountedTotalPages)
model.setGithubCountedTotalPages.mockImplementation(setCountedTotalPages)
const actions = useMobileTasksTaskPaginationActions(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture carries every member the page handler reads.
model as unknown as TaskListLoadingModel
{ ...model, githubCountedTotalPages: countedTotalPages } as unknown as TaskListLoadingModel
)
changePage = actions.handleGitHubPageChange
canLoadUncountedNextPage = actions.githubCanLoadUncountedNextPage
totalPages = actions.githubTotalPages
return null
}
act(() => {
@@ -79,7 +85,8 @@ describe('GitHub task page change', () => {
it('asks for the target page by number and shows it', async () => {
const model = buildModel(async () => ({
items: oldSelectionPage,
failedCount: 0
failedCount: 0,
errorTypes: []
}))
mount(model)
await act(async () => {
@@ -110,7 +117,7 @@ describe('GitHub task page change', () => {
})
model.loadGenerationRef.current += 1
await act(async () => {
land({ items: oldSelectionPage, failedCount: 0 })
land({ items: oldSelectionPage, failedCount: 0, errorTypes: [] })
await pending
})
expect(model.setGithubPages).not.toHaveBeenCalled()
@@ -118,6 +125,46 @@ describe('GitHub task page change', () => {
expect(model.setGithubPaginationLoading).toHaveBeenLastCalledWith(false)
})
it('explains a clean empty next page and withdraws speculative Next', async () => {
const model = buildModel(async () => ({ items: [], failedCount: 0, errorTypes: [] }), null)
model.githubPages = [
Array.from({ length: 72 }, (_, index) => ({
key: `github:repo-1:issue:${index}`,
source: { repoId: 'repo-1' }
}))
]
mount(model)
expect(canLoadUncountedNextPage).toBe(true)
await act(async () => {
await changePage?.(1)
})
expect(model.setError).toHaveBeenCalledWith('No more results on page 2.')
expect(canLoadUncountedNextPage).toBe(false)
expect(model.setItems).not.toHaveBeenCalled()
})
it('explains an all-repo failure and keeps the page retryable', async () => {
const model = buildModel(async () => ({ items: [], failedCount: 1, errorTypes: [] }))
mount(model)
await act(async () => {
await changePage?.(1)
})
expect(model.setError).toHaveBeenCalledWith('Page 2 could not be loaded from GitHub.')
expect(model.setGithubCountedTotalPages).not.toHaveBeenCalled()
expect(model.setItems).not.toHaveBeenCalled()
})
it('keeps a real count when an empty page is returned', async () => {
const model = buildModel(async () => ({ items: [], failedCount: 0, errorTypes: [] }), 3)
mount(model)
await act(async () => {
await changePage?.(1)
})
expect(model.setError).toHaveBeenCalledWith('No more results on page 2.')
expect(totalPages).toBe(3)
expect(model.setItems).not.toHaveBeenCalled()
})
describe('without a count, as in the default all-repos view', () => {
function rows(repoId: string, count: number): PageRow[] {
return Array.from({ length: count }, (_, index) => ({
@@ -127,7 +174,7 @@ describe('GitHub task page change', () => {
}
function lastPageOffersNext(page: PageRow[]): boolean {
const model = buildModel(async () => ({ items: [], failedCount: 0 }), null)
const model = buildModel(async () => ({ items: [], failedCount: 0, errorTypes: [] }), null)
model.githubPages = [page]
model.selectedHostedRepos = [{ id: 'repo-a' }, { id: 'repo-b' }]
mount(model)
@@ -1,10 +1,18 @@
import {
applyEmptyPageClamp,
applyWindowPageLimit,
deriveAdvertisedTotalPages,
resolveEmptyPageOutcome
} from '../../../src/shared/work-item-empty-page'
import { translate } from '../i18n/mobile-i18n'
import type { TaskListLoadingModel } from './use-mobile-tasks-task-list-loading'
import {
CROSS_REPO_DISPLAY_LIMIT,
PER_REPO_FETCH_LIMIT,
getTaskPagePerRepoLimit,
useCallback,
useMemo
useMemo,
useState
} from './mobile-tasks-dependencies'
import { type TaskItem, buildPartialRepositoryNotice } from './mobile-tasks-legacy-foundation'
import { linearAccountConnect } from './mobile-task-list-operations'
@@ -42,6 +50,7 @@ export function useMobileTasksTaskPaginationActions(model: TaskListLoadingModel)
selectedHostedRepos,
setError,
setGithubCurrentPage,
setGithubCountedTotalPages,
setGithubLoadingTargetPage,
setGithubPages,
setGithubPaginationLoading,
@@ -109,10 +118,15 @@ export function useMobileTasksTaskPaginationActions(model: TaskListLoadingModel)
[loadTasks]
)
// Zero count uses the desktop’s failed-count fallback.
const githubTotalPages = githubCountedTotalPages
? Math.max(githubPages.length, githubCountedTotalPages)
: githubPages.length
const [windowLimit, setWindowLimit] = useState<{ generation: number; pages: number } | null>(null)
const provenPageLimit =
windowLimit?.generation === loadGenerationRef.current ? windowLimit.pages : null
const githubTotalPages = deriveAdvertisedTotalPages({
loadedPages: githubPages.length,
countedTotalPages: githubCountedTotalPages,
fallbackTotalPages: githubPages.length,
provenPageLimit
})
const githubPerRepoLimit = getTaskPagePerRepoLimit(
selectedHostedRepos.length,
PER_REPO_FETCH_LIMIT,
@@ -120,7 +134,9 @@ export function useMobileTasksTaskPaginationActions(model: TaskListLoadingModel)
)
// One full per-repo page may have more; all-repo mode skips counts and relies on this to reach page two.
const githubCanLoadUncountedNextPage =
!githubCountedTotalPages && hasRepoWithFullPage(githubPages.at(-1) ?? [], githubPerRepoLimit)
!githubCountedTotalPages &&
(provenPageLimit === null || githubPages.length < provenPageLimit) &&
hasRepoWithFullPage(githubPages.at(-1) ?? [], githubPerRepoLimit)
const githubCanShowPagination =
githubTotalPages > 1 || (githubPages.length > 0 && githubCanLoadUncountedNextPage)
const githubPagePickerPages = useMemo(() => {
@@ -165,11 +181,52 @@ export function useMobileTasksTaskPaginationActions(model: TaskListLoadingModel)
const generation = loadGenerationRef.current
const isCurrent = () =>
loadGenerationRef.current === generation && clientRef.current === client
setError('')
setGithubPaginationLoading(true)
setGithubLoadingTargetPage(targetPage)
try {
const page = await fetchGitHubItemsPage(client, selectedHostedRepos, targetPage, isCurrent)
if (!isCurrent() || page.items.length === 0) {
if (!isCurrent()) {
return
}
if (page.items.length === 0) {
const emptyPage = {
target: targetPage,
failedCount: page.failedCount,
errorTypes: page.errorTypes
}
const { reason } = resolveEmptyPageOutcome({ ...emptyPage, countedTotalPages: null })
if (reason === 'window-unreachable') {
setError(
translate(
'tasks.pagination.pageUnreachable',
'Page {{page}} is beyond what GitHub search can return.',
{ page: targetPage + 1 }
)
)
setWindowLimit((previous) => ({
generation,
pages: applyWindowPageLimit(
previous?.generation === generation ? previous.pages : null,
targetPage
)
}))
} else if (reason === 'load-failed') {
setError(
translate(
'tasks.pagination.loadPageFailed',
'Page {{page}} could not be loaded from GitHub.',
{ page: targetPage + 1 }
)
)
} else {
setError(
translate('tasks.pagination.noMoreResults', 'No more results on page {{page}}.', {
page: targetPage + 1
})
)
setGithubCountedTotalPages((previous) => applyEmptyPageClamp(previous, emptyPage))
}
return
}
if (page.failedCount > 0) {
@@ -195,6 +252,7 @@ export function useMobileTasksTaskPaginationActions(model: TaskListLoadingModel)
githubPaginationLoading,
loadGenerationRef,
selectedHostedRepos,
setGithubCountedTotalPages,
tasksSupported
]
)
@@ -15,7 +15,7 @@ import {
} from '@/components/task-page/github/task-page-github-work-item-mutation-pages'
import { clearTaskPageGitHubAuthorityThroughGeneration } from '@/components/task-page/github/task-page-github-work-item-authority-refresh'
import { getOrCreateQuietRevalidateState } from '@/components/task-page/github/task-page-github-work-item-quiet-state'
import { resolveEmptyPageOutcome } from '@/components/task-page/task-page-work-item-pagination'
import { resolveEmptyPageOutcome } from '../../../../../shared/work-item-empty-page'
import {
deriveTaskPageGitHubWorkItemsFetchOptions,
shouldReplaceTaskPageItemsAfterRefresh,
@@ -4,7 +4,7 @@ import { useCallback, useMemo, useEffect } from 'react'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import { taskPageGitHubItemKey } from '@/components/task-page/github/task-page-github-work-item-mutation-keys'
import { deriveTaskPagePRCheckSummary } from '../task-page-pr-check-summary'
import { deriveAdvertisedTotalPages } from '@/components/task-page/task-page-work-item-pagination'
import { deriveAdvertisedTotalPages } from '../../../../../shared/work-item-empty-page'
import {
GITHUB_PR_TASK_GRID_CLASS,
GITHUB_TASK_GRID_CLASS,
@@ -4,9 +4,9 @@ import { stripRepoQualifiers } from '../../../../../shared/task-query'
import { taskPageToGitHubApiPage } from '../../../../../shared/work-items'
import {
resolveEmptyPageOutcome,
applyWindowPageLimit,
applyEmptyPageClamp
} from '@/components/task-page/task-page-work-item-pagination'
applyEmptyPageClamp,
applyWindowPageLimit
} from '../../../../../shared/work-item-empty-page'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { overlayPendingOnTaskPagePages } from '@/components/task-page/github/task-page-github-work-item-mutation-pages'
@@ -2,11 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
import {
accumulateWorkItemPages,
applyEmptyPageClamp,
applyWindowPageLimit,
buildSelectedReposKey,
deriveAdvertisedTotalPages,
resolveEmptyPageOutcome,
workItemIdentity
} from './task-page-work-item-pagination'
@@ -44,162 +40,6 @@ describe('workItemIdentity', () => {
})
})
describe('resolveEmptyPageOutcome', () => {
const base = { failedCount: 0, errorTypes: [] as const, countedTotalPages: null }
it('clamps and reports unreachable when the search window 422 is present', () => {
expect(
resolveEmptyPageOutcome({ ...base, target: 33, errorTypes: ['validation_error'] })
).toEqual({ reason: 'window-unreachable', clampTotalPagesTo: 33 })
expect(
resolveEmptyPageOutcome({
...base,
target: 33,
errorTypes: ['validation_error'],
countedTotalPages: 39
})
).toEqual({ reason: 'window-unreachable', clampTotalPagesTo: 33 })
})
it('resolves as load-failed when a window 422 coincides with a sibling failure', () => {
// A sibling failure says nothing about healthy repos’ remaining pages and must outrank a window clamp.
expect(
resolveEmptyPageOutcome({
...base,
target: 5,
failedCount: 1,
errorTypes: ['validation_error']
})
).toEqual({ reason: 'load-failed', clampTotalPagesTo: null })
expect(
resolveEmptyPageOutcome({
...base,
target: 5,
errorTypes: ['validation_error', 'permission_denied']
})
).toEqual({ reason: 'load-failed', clampTotalPagesTo: null })
})
it('reports a failure without clamping for transient or unclassified errors', () => {
expect(resolveEmptyPageOutcome({ ...base, target: 5, failedCount: 2 })).toEqual({
reason: 'load-failed',
clampTotalPagesTo: null
})
for (const type of ['permission_denied', 'not_found', 'rate_limited', 'unknown'] as const) {
expect(resolveEmptyPageOutcome({ ...base, target: 5, errorTypes: [type] })).toEqual({
reason: 'load-failed',
clampTotalPagesTo: null
})
}
})
it('withdraws the speculative page on clean empty only while the count is unknown', () => {
expect(resolveEmptyPageOutcome({ ...base, target: 2 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: 2
})
expect(resolveEmptyPageOutcome({ ...base, target: 2, countedTotalPages: 0 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: 2
})
// PR failures can become clean-empty results; shrinking a real count would silently hide healthy pages.
expect(resolveEmptyPageOutcome({ ...base, target: 2, countedTotalPages: 34 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: null
})
expect(resolveEmptyPageOutcome({ ...base, target: 0 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: 1
})
})
})
describe('applyEmptyPageClamp', () => {
const clean = { failedCount: 0, errorTypes: [] as const }
it('keeps a real count that resolved between click and response', () => {
// The count can resolve during fetch; clamp against committed state rather than the click-time closure.
expect(applyEmptyPageClamp(33, { ...clean, target: 2 })).toBe(33)
})
it('withdraws the speculative page while the count is unknown or failed', () => {
expect(applyEmptyPageClamp(null, { ...clean, target: 2 })).toBe(2)
expect(applyEmptyPageClamp(0, { ...clean, target: 2 })).toBe(2)
})
it('never touches the count for window or failure outcomes', () => {
expect(
applyEmptyPageClamp(39, { target: 33, failedCount: 0, errorTypes: ['validation_error'] })
).toBe(39)
expect(applyEmptyPageClamp(33, { target: 5, failedCount: 2, errorTypes: [] })).toBe(33)
expect(applyEmptyPageClamp(null, { target: 5, failedCount: 2, errorTypes: [] })).toBe(null)
})
})
describe('applyWindowPageLimit', () => {
it('sets, only lowers, and never goes below one page', () => {
expect(applyWindowPageLimit(null, 33)).toBe(33)
expect(applyWindowPageLimit(33, 20)).toBe(20)
expect(applyWindowPageLimit(20, 33)).toBe(20)
expect(applyWindowPageLimit(null, 0)).toBe(1)
})
})
describe('deriveAdvertisedTotalPages', () => {
it('is order-independent: a count landing after a speculative withdrawal wins', () => {
// A late real count must overwrite speculative end-of-data withdrawal.
expect(
deriveAdvertisedTotalPages({
loadedPages: 1,
countedTotalPages: 39,
fallbackTotalPages: 2,
provenPageLimit: null
})
).toBe(39)
})
it('caps a real count with the proven window limit', () => {
expect(
deriveAdvertisedTotalPages({
loadedPages: 3,
countedTotalPages: 39,
fallbackTotalPages: 4,
provenPageLimit: 33
})
).toBe(33)
})
it('uses the fallback while the count is unknown/failed, still window-capped', () => {
expect(
deriveAdvertisedTotalPages({
loadedPages: 1,
countedTotalPages: null,
fallbackTotalPages: 2,
provenPageLimit: null
})
).toBe(2)
expect(
deriveAdvertisedTotalPages({
loadedPages: 1,
countedTotalPages: 0,
fallbackTotalPages: 5,
provenPageLimit: 3
})
).toBe(3)
})
it('never advertises fewer pages than are loaded', () => {
expect(
deriveAdvertisedTotalPages({
loadedPages: 5,
countedTotalPages: 2,
fallbackTotalPages: 2,
provenPageLimit: 2
})
).toBe(5)
})
})
describe('buildSelectedReposKey', () => {
const contextFor = (r: { id: string }) => ({ provider: 'github', repoId: r.id })
@@ -1,4 +1,3 @@
import type { ClassifiedError } from '../../../../shared/classified-error'
import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
/** Inclusive updatedAt cursors preserve timestamp ties; dedup boundary rows and backfill full display pages, flushing partials on exhaustion/stall. */
@@ -8,70 +7,6 @@ export function workItemIdentity(item: Pick<GitHubWorkItem, 'id' | 'repoId'>): s
return `${item.repoId}:${item.id}`
}
export type EmptyPageOutcome = {
reason: 'window-unreachable' | 'load-failed' | 'end-of-data'
/** Count clamp or null; window limits belong in applyWindowPageLimit, never the count slot. */
clampTotalPagesTo: number | null
}
/** Only all-window 422 errors prove a cap; transient sibling failures win, and clean-empty results may withdraw speculative counts only. */
export function resolveEmptyPageOutcome(args: {
target: number
failedCount: number
errorTypes: readonly ClassifiedError['type'][]
countedTotalPages: number | null
}): EmptyPageOutcome {
const clamp = Math.max(1, Math.floor(args.target))
// A sibling non-window error may hide healthy pages, so it must block the clamp.
const onlyWindowErrors =
args.errorTypes.length > 0 && args.errorTypes.every((type) => type === 'validation_error')
if (onlyWindowErrors && args.failedCount === 0) {
return { reason: 'window-unreachable', clampTotalPagesTo: clamp }
}
if (args.failedCount > 0 || args.errorTypes.length > 0) {
return { reason: 'load-failed', clampTotalPagesTo: null }
}
const countUnknown = args.countedTotalPages === null || args.countedTotalPages === 0
return { reason: 'end-of-data', clampTotalPagesTo: countUnknown ? clamp : null }
}
/** Clamp speculative counts against the latest committed state; store proven 422 limits separately so late real counts can replace speculation. */
export function applyEmptyPageClamp(
previous: number | null,
args: {
target: number
failedCount: number
errorTypes: readonly ClassifiedError['type'][]
}
): number | null {
const outcome = resolveEmptyPageOutcome({ ...args, countedTotalPages: previous })
if (outcome.reason !== 'end-of-data' || outcome.clampTotalPagesTo === null) {
return previous
}
return outcome.clampTotalPagesTo
}
/** Proven window 422 limit: set once, only ever lowered, reset per generation. */
export function applyWindowPageLimit(previous: number | null, target: number): number {
const clamp = Math.max(1, Math.floor(target))
return previous === null ? clamp : Math.min(previous, clamp)
}
/** Cap count/fallback by the proven window limit, retaining loaded pages; separate state makes late count updates order-independent. */
export function deriveAdvertisedTotalPages(args: {
loadedPages: number
countedTotalPages: number | null
fallbackTotalPages: number
provenPageLimit: number | null
}): number {
const uncapped =
args.countedTotalPages && args.countedTotalPages > 0
? Math.max(args.loadedPages, args.countedTotalPages)
: args.fallbackTotalPages
const capped = args.provenPageLimit === null ? uncapped : Math.min(uncapped, args.provenPageLimit)
return Math.max(args.loadedPages, capped)
}
/** Key selection by request fields and resolved timestamp-free source context so fresh repo arrays cannot reset pagination mid-click. */
export function buildSelectedReposKey<
T extends {
@@ -1,3 +1,4 @@
import { getWorkItemPageErrorTypes } from '../../../../shared/github/work-item-page-errors'
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import type { GitHubSlice } from './slice-types'
@@ -10,10 +11,7 @@ import {
PER_REPO_FETCH_LIMIT
} from '../../../../shared/work-items'
import { getTaskSourceCacheScope } from '../../../../shared/task-source-context'
import {
GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN,
isGitHubWorkItemsQueryTooLarge
} from '../slices/github-work-items-query-bounds'
import { isGitHubWorkItemsQueryTooLarge } from '../slices/github-work-items-query-bounds'
import { workItemsCacheKey, workItemsInflightRequestKey } from './cache-identity'
import { isFresh, WORK_ITEMS_CACHE_TTL } from './cache-policy'
import {
@@ -132,25 +130,15 @@ export const createWorkItemAggregateActions = (
...(options?.noCache ? { noCache: true } : {}),
...(options?.wakeWslDistro ? { wakeWslDistro: true } : {})
})
errorTypes.push(...getWorkItemPageErrorTypes(envelope.errors))
// Page-N errors bypass initial-fetch banners; log them to expose silent truncation (design §6).
if (envelope.errors?.issues) {
const { type, message } = envelope.errors.issues
// Only 1000-result-window 422 implies unreachable clamp; classify other validation errors as failures.
errorTypes.push(
type === 'validation_error' &&
!GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN.test(message)
? 'unknown'
: type
)
console.warn(
`[workItems] next page ${r.repoId} issues-side partial failure:`,
envelope.errors.issues
)
}
if (envelope.errors?.prs) {
// Window 422 applies only to issues; PR validation failures never imply unreachable.
const { type } = envelope.errors.prs
errorTypes.push(type === 'validation_error' ? 'unknown' : type)
console.warn(
`[workItems] next page ${r.repoId} prs-side partial failure:`,
envelope.errors.prs
@@ -0,0 +1,43 @@
import type { ClassifiedError } from '../classified-error'
import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from './work-items-query-bounds'
export function getWorkItemPageErrorTypes(errors: unknown): ClassifiedError['type'][] {
if (errors === undefined || errors === null) {
return []
}
if (typeof errors !== 'object') {
return ['unknown']
}
const sources = [
['issues', 'issues' in errors ? errors.issues : undefined],
['prs', 'prs' in errors ? errors.prs : undefined]
] as const
return sources.flatMap(([source, error]): ClassifiedError['type'][] => {
if (error === undefined || error === null) {
return []
}
if (typeof error !== 'object' || !('type' in error)) {
return ['unknown']
}
switch (error.type) {
case 'validation_error':
// Only issue search's 1000-result window proves a page limit.
return [
source === 'issues' &&
'message' in error &&
typeof error.message === 'string' &&
GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN.test(error.message)
? 'validation_error'
: 'unknown'
]
case 'permission_denied':
case 'not_found':
case 'issues_disabled':
case 'rate_limited':
case 'network_error':
return [error.type]
default:
return ['unknown']
}
})
}
+163
View File
@@ -0,0 +1,163 @@
import { describe, expect, it } from 'vitest'
import {
applyEmptyPageClamp,
resolveEmptyPageOutcome,
applyWindowPageLimit,
deriveAdvertisedTotalPages
} from './work-item-empty-page'
describe('resolveEmptyPageOutcome', () => {
const base = { failedCount: 0, errorTypes: [] as const, countedTotalPages: null }
it('clamps and reports unreachable when the search window 422 is present', () => {
expect(
resolveEmptyPageOutcome({ ...base, target: 33, errorTypes: ['validation_error'] })
).toEqual({ reason: 'window-unreachable', clampTotalPagesTo: 33 })
expect(
resolveEmptyPageOutcome({
...base,
target: 33,
errorTypes: ['validation_error'],
countedTotalPages: 39
})
).toEqual({ reason: 'window-unreachable', clampTotalPagesTo: 33 })
})
it('resolves as load-failed when a window 422 coincides with a sibling failure', () => {
// A sibling failure says nothing about healthy repos’ remaining pages and must outrank a window clamp.
expect(
resolveEmptyPageOutcome({
...base,
target: 5,
failedCount: 1,
errorTypes: ['validation_error']
})
).toEqual({ reason: 'load-failed', clampTotalPagesTo: null })
expect(
resolveEmptyPageOutcome({
...base,
target: 5,
errorTypes: ['validation_error', 'permission_denied']
})
).toEqual({ reason: 'load-failed', clampTotalPagesTo: null })
})
it('reports a failure without clamping for transient or unclassified errors', () => {
expect(resolveEmptyPageOutcome({ ...base, target: 5, failedCount: 2 })).toEqual({
reason: 'load-failed',
clampTotalPagesTo: null
})
for (const type of ['permission_denied', 'not_found', 'rate_limited', 'unknown'] as const) {
expect(resolveEmptyPageOutcome({ ...base, target: 5, errorTypes: [type] })).toEqual({
reason: 'load-failed',
clampTotalPagesTo: null
})
}
})
it('withdraws the speculative page on clean empty only while the count is unknown', () => {
expect(resolveEmptyPageOutcome({ ...base, target: 2 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: 2
})
expect(resolveEmptyPageOutcome({ ...base, target: 2, countedTotalPages: 0 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: 2
})
// PR failures can become clean-empty results; shrinking a real count would silently hide healthy pages.
expect(resolveEmptyPageOutcome({ ...base, target: 2, countedTotalPages: 34 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: null
})
expect(resolveEmptyPageOutcome({ ...base, target: 0 })).toEqual({
reason: 'end-of-data',
clampTotalPagesTo: 1
})
})
})
describe('applyEmptyPageClamp', () => {
const clean = { failedCount: 0, errorTypes: [] as const }
it('keeps a real count that resolved between click and response', () => {
// The count can resolve during fetch; clamp against committed state rather than the click-time closure.
expect(applyEmptyPageClamp(33, { ...clean, target: 2 })).toBe(33)
})
it('withdraws the speculative page while the count is unknown or failed', () => {
expect(applyEmptyPageClamp(null, { ...clean, target: 2 })).toBe(2)
expect(applyEmptyPageClamp(0, { ...clean, target: 2 })).toBe(2)
})
it('never touches the count for window or failure outcomes', () => {
expect(
applyEmptyPageClamp(39, { target: 33, failedCount: 0, errorTypes: ['validation_error'] })
).toBe(39)
expect(applyEmptyPageClamp(33, { target: 5, failedCount: 2, errorTypes: [] })).toBe(33)
expect(applyEmptyPageClamp(null, { target: 5, failedCount: 2, errorTypes: [] })).toBe(null)
})
})
describe('applyWindowPageLimit', () => {
it('sets, only lowers, and never goes below one page', () => {
expect(applyWindowPageLimit(null, 33)).toBe(33)
expect(applyWindowPageLimit(33, 20)).toBe(20)
expect(applyWindowPageLimit(20, 33)).toBe(20)
expect(applyWindowPageLimit(null, 0)).toBe(1)
})
})
describe('deriveAdvertisedTotalPages', () => {
it('is order-independent: a count landing after a speculative withdrawal wins', () => {
// A late real count must overwrite speculative end-of-data withdrawal.
expect(
deriveAdvertisedTotalPages({
loadedPages: 1,
countedTotalPages: 39,
fallbackTotalPages: 2,
provenPageLimit: null
})
).toBe(39)
})
it('caps a real count with the proven window limit', () => {
expect(
deriveAdvertisedTotalPages({
loadedPages: 3,
countedTotalPages: 39,
fallbackTotalPages: 4,
provenPageLimit: 33
})
).toBe(33)
})
it('uses the fallback while the count is unknown/failed, still window-capped', () => {
expect(
deriveAdvertisedTotalPages({
loadedPages: 1,
countedTotalPages: null,
fallbackTotalPages: 2,
provenPageLimit: null
})
).toBe(2)
expect(
deriveAdvertisedTotalPages({
loadedPages: 1,
countedTotalPages: 0,
fallbackTotalPages: 5,
provenPageLimit: 3
})
).toBe(3)
})
it('never advertises fewer pages than are loaded', () => {
expect(
deriveAdvertisedTotalPages({
loadedPages: 5,
countedTotalPages: 2,
fallbackTotalPages: 2,
provenPageLimit: 2
})
).toBe(5)
})
})
+65
View File
@@ -0,0 +1,65 @@
import type { ClassifiedError } from './classified-error'
export type EmptyPageOutcome = {
reason: 'window-unreachable' | 'load-failed' | 'end-of-data'
/** Count clamp or null; window limits belong in applyWindowPageLimit, never the count slot. */
clampTotalPagesTo: number | null
}
/** Only all-window 422 errors prove a cap; transient sibling failures win, and clean-empty results may withdraw speculative counts only. */
export function resolveEmptyPageOutcome(args: {
target: number
failedCount: number
errorTypes: readonly ClassifiedError['type'][]
countedTotalPages: number | null
}): EmptyPageOutcome {
const clamp = Math.max(1, Math.floor(args.target))
// A sibling non-window error may hide healthy pages, so it must block the clamp.
const onlyWindowErrors =
args.errorTypes.length > 0 && args.errorTypes.every((type) => type === 'validation_error')
if (onlyWindowErrors && args.failedCount === 0) {
return { reason: 'window-unreachable', clampTotalPagesTo: clamp }
}
if (args.failedCount > 0 || args.errorTypes.length > 0) {
return { reason: 'load-failed', clampTotalPagesTo: null }
}
const countUnknown = args.countedTotalPages === null || args.countedTotalPages === 0
return { reason: 'end-of-data', clampTotalPagesTo: countUnknown ? clamp : null }
}
/** Clamp speculative counts against the latest committed state; store proven 422 limits separately so late real counts can replace speculation. */
export function applyEmptyPageClamp(
previous: number | null,
args: {
target: number
failedCount: number
errorTypes: readonly ClassifiedError['type'][]
}
): number | null {
const outcome = resolveEmptyPageOutcome({ ...args, countedTotalPages: previous })
if (outcome.reason !== 'end-of-data' || outcome.clampTotalPagesTo === null) {
return previous
}
return outcome.clampTotalPagesTo
}
/** Proven window 422 limit: set once, only ever lowered, reset per generation. */
export function applyWindowPageLimit(previous: number | null, target: number): number {
const clamp = Math.max(1, Math.floor(target))
return previous === null ? clamp : Math.min(previous, clamp)
}
/** Cap count/fallback by the proven window limit, retaining loaded pages; separate state makes late count updates order-independent. */
export function deriveAdvertisedTotalPages(args: {
loadedPages: number
countedTotalPages: number | null
fallbackTotalPages: number
provenPageLimit: number | null
}): number {
const uncapped =
args.countedTotalPages && args.countedTotalPages > 0
? Math.max(args.loadedPages, args.countedTotalPages)
: args.fallbackTotalPages
const capped = args.provenPageLimit === null ? uncapped : Math.min(uncapped, args.provenPageLimit)
return Math.max(args.loadedPages, capped)
}