claude-dev-env 8.54.1 → 8.54.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.54.1",
3
+ "version": "8.54.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,10 +30,16 @@ CHECK_RUN_PAGE_SIZE: int = 100
30
30
  """How many check runs one request asks for."""
31
31
 
32
32
  REVIEW_THREAD_QUERY: str = """
33
- query($owner: String!, $name: String!, $number: Int!, $pageSize: Int!) {
33
+ query(
34
+ $owner: String!, $name: String!, $number: Int!, $pageSize: Int!, $after: String
35
+ ) {
34
36
  repository(owner: $owner, name: $name) {
35
37
  pullRequest(number: $number) {
36
- reviewThreads(first: $pageSize) {
38
+ reviewThreads(first: $pageSize, after: $after) {
39
+ pageInfo {
40
+ hasNextPage
41
+ endCursor
42
+ }
37
43
  nodes {
38
44
  path
39
45
  isResolved
@@ -52,14 +58,13 @@ query($owner: String!, $name: String!, $number: Int!, $pageSize: Int!) {
52
58
  """
53
59
  """GraphQL query for the review threads on one pull request."""
54
60
 
55
- ALL_THREAD_NODE_KEYS: tuple[str, ...] = (
61
+ ALL_THREAD_CONNECTION_KEYS: tuple[str, ...] = (
56
62
  "data",
57
63
  "repository",
58
64
  "pullRequest",
59
65
  "reviewThreads",
60
- "nodes",
61
66
  )
62
- """Path from the GraphQL answer down to the review-thread list."""
67
+ """Path from the GraphQL answer down to the review-thread connection."""
63
68
 
64
69
  ALL_RESOLVED_KEYS: tuple[str, ...] = ("isResolved", "resolved", "is_resolved")
65
70
  """Field names either route uses for a resolved review thread."""
@@ -174,6 +179,29 @@ NUMBER_VARIABLE: str = "number"
174
179
  PAGE_SIZE_VARIABLE: str = "pageSize"
175
180
  """GraphQL variable naming the page size."""
176
181
 
182
+ AFTER_VARIABLE: str = "after"
183
+ """GraphQL variable naming the cursor a page starts after."""
184
+
185
+ NODES_KEY: str = "nodes"
186
+ """GraphQL connection field carrying one page of records."""
187
+
188
+ PAGE_INFO_KEY: str = "pageInfo"
189
+ """GraphQL connection field describing the page boundaries."""
190
+
191
+ HAS_NEXT_PAGE_KEY: str = "hasNextPage"
192
+ """Page-info field that is true while more records follow."""
193
+
194
+ END_CURSOR_KEY: str = "endCursor"
195
+ """Page-info field naming the cursor the next page starts after."""
196
+
197
+ MAX_REVIEW_THREAD_PAGES: int = 20
198
+ """How many review-thread pages one read follows before it reports an error."""
199
+
200
+ TOO_MANY_THREAD_PAGES_TEMPLATE: str = (
201
+ "review threads on {slug}#{number} span more than {page_count} pages"
202
+ )
203
+ """Error raised when the review threads outrun the page limit."""
204
+
177
205
  ALL_TOKEN_ENVIRONMENT_VARIABLES: tuple[str, ...] = (
178
206
  "GITHUB_TOKEN",
179
207
  "GH_TOKEN",
@@ -26,20 +26,26 @@ import urllib.request
26
26
  from collections.abc import Mapping, Sequence
27
27
 
28
28
  from dev_env_scripts_constants.review_closure_constants import (
29
- ALL_THREAD_NODE_KEYS,
29
+ AFTER_VARIABLE,
30
+ ALL_THREAD_CONNECTION_KEYS,
30
31
  ALL_TOKEN_ENVIRONMENT_VARIABLES,
31
32
  CHECK_RUN_PAGE_SIZE,
32
33
  CHECK_RUNS_ENDPOINT_TEMPLATE,
33
34
  CHECK_RUNS_KEY,
34
35
  COMMENT_PAGE_SIZE,
36
+ END_CURSOR_KEY,
35
37
  GET_METHOD,
36
38
  GITHUB_API_ROOT,
37
39
  GITHUB_GRAPHQL_ENDPOINT,
40
+ HAS_NEXT_PAGE_KEY,
38
41
  MAX_COMMENT_PAGES,
42
+ MAX_REVIEW_THREAD_PAGES,
39
43
  NAME_VARIABLE,
40
44
  NO_SIGN_IN_MESSAGE,
45
+ NODES_KEY,
41
46
  NUMBER_VARIABLE,
42
47
  OWNER_VARIABLE,
48
+ PAGE_INFO_KEY,
43
49
  PAGE_SIZE_VARIABLE,
44
50
  POST_METHOD,
45
51
  PULL_REQUEST_ENDPOINT_TEMPLATE,
@@ -51,6 +57,7 @@ from dev_env_scripts_constants.review_closure_constants import (
51
57
  REVIEW_THREAD_QUERY,
52
58
  REVIEW_THREADS_ENDPOINT_TEMPLATE,
53
59
  SLUG_SEPARATOR,
60
+ TOO_MANY_THREAD_PAGES_TEMPLATE,
54
61
  TOP_LEVEL_COMMENTS_ENDPOINT_TEMPLATE,
55
62
  UNREADABLE_TOP_LEVEL_COMMENT_TEMPLATE,
56
63
  VARIABLES_KEY,
@@ -372,21 +379,46 @@ def _thread_records_over_rest(
372
379
 
373
380
 
374
381
  def _thread_records_over_graphql(slug: str, number: int, token: str) -> list[object]:
382
+ all_records: list[object] = []
383
+ after_cursor: object = None
384
+ for _ in range(MAX_REVIEW_THREAD_PAGES):
385
+ page_records, after_cursor = _thread_page_over_graphql(
386
+ slug, number, token, after_cursor
387
+ )
388
+ all_records.extend(page_records)
389
+ if after_cursor is None:
390
+ return all_records
391
+ raise GitHubError(
392
+ TOO_MANY_THREAD_PAGES_TEMPLATE.format(
393
+ slug=slug, number=number, page_count=MAX_REVIEW_THREAD_PAGES
394
+ )
395
+ )
396
+
397
+
398
+ def _thread_page_over_graphql(
399
+ slug: str, number: int, token: str, after_cursor: object
400
+ ) -> tuple[list[object], object]:
375
401
  document = request_json(
376
402
  POST_METHOD,
377
403
  GITHUB_GRAPHQL_ENDPOINT,
378
404
  token,
379
- _thread_query_payload(slug, number),
405
+ _thread_query_payload(slug, number, after_cursor),
380
406
  )
381
- all_records = functools.reduce(
382
- functools.partial(_field_at, document), ALL_THREAD_NODE_KEYS, document
407
+ connection = functools.reduce(
408
+ functools.partial(_field_at, document), ALL_THREAD_CONNECTION_KEYS, document
383
409
  )
384
- if not isinstance(all_records, list):
410
+ page_records = _field_at(document, connection, NODES_KEY)
411
+ if not isinstance(page_records, list):
385
412
  raise GitHubError(str(document))
386
- return all_records
413
+ page_info = _field_at(document, connection, PAGE_INFO_KEY)
414
+ if not isinstance(page_info, Mapping) or not page_info.get(HAS_NEXT_PAGE_KEY):
415
+ return page_records, None
416
+ return page_records, page_info.get(END_CURSOR_KEY)
387
417
 
388
418
 
389
- def _thread_query_payload(slug: str, number: int) -> dict[str, object]:
419
+ def _thread_query_payload(
420
+ slug: str, number: int, after_cursor: object
421
+ ) -> dict[str, object]:
390
422
  owner, _, name = slug.partition(SLUG_SEPARATOR)
391
423
  return {
392
424
  QUERY_KEY: REVIEW_THREAD_QUERY,
@@ -395,6 +427,7 @@ def _thread_query_payload(slug: str, number: int) -> dict[str, object]:
395
427
  NAME_VARIABLE: name,
396
428
  NUMBER_VARIABLE: number,
397
429
  PAGE_SIZE_VARIABLE: REVIEW_THREAD_PAGE_SIZE,
430
+ AFTER_VARIABLE: after_cursor,
398
431
  },
399
432
  }
400
433
 
@@ -192,6 +192,76 @@ def should_fall_back_to_the_graphql_query(monkeypatch: pytest.MonkeyPatch) -> No
192
192
  assert any("graphql" in each_url for each_url in all_requested_urls)
193
193
 
194
194
 
195
+ def _thread_page(path: str, end_cursor: str | None) -> dict[str, object]:
196
+ return {
197
+ "data": {
198
+ "repository": {
199
+ "pullRequest": {
200
+ "reviewThreads": {
201
+ "pageInfo": {
202
+ "hasNextPage": end_cursor is not None,
203
+ "endCursor": end_cursor,
204
+ },
205
+ "nodes": [
206
+ {
207
+ "path": path,
208
+ "isResolved": False,
209
+ "isOutdated": False,
210
+ "comments": {"nodes": []},
211
+ }
212
+ ],
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+
220
+ def should_read_every_graphql_page_of_review_threads(
221
+ monkeypatch: pytest.MonkeyPatch,
222
+ ) -> None:
223
+ all_pages_by_cursor = {
224
+ None: _thread_page("scripts/first.py", "cursor-1"),
225
+ "cursor-1": _thread_page("scripts/second.py", None),
226
+ }
227
+ all_requested_cursors: list[object] = []
228
+
229
+ @contextmanager
230
+ def fake_urlopen(request: object, timeout: int = 0) -> Iterator[FakeReply]:
231
+ if "ccr/review_threads" in request.full_url:
232
+ yield FakeReply(404, {})
233
+ return
234
+ cursor = json.loads(request.data)["variables"].get("after")
235
+ all_requested_cursors.append(cursor)
236
+ yield FakeReply(200, all_pages_by_cursor[cursor])
237
+
238
+ monkeypatch.setattr(reader.urllib.request, "urlopen", fake_urlopen)
239
+
240
+ all_threads = reader.read_review_threads("jl-cmd/claude-dev-env", 7, TOKEN)
241
+
242
+ assert [each.subject for each in all_threads] == [
243
+ "scripts/first.py",
244
+ "scripts/second.py",
245
+ ]
246
+ assert all_requested_cursors == [None, "cursor-1"]
247
+
248
+
249
+ def should_report_review_threads_that_outrun_the_page_limit(
250
+ monkeypatch: pytest.MonkeyPatch,
251
+ ) -> None:
252
+ @contextmanager
253
+ def fake_urlopen(request: object, timeout: int = 0) -> Iterator[FakeReply]:
254
+ if "ccr/review_threads" in request.full_url:
255
+ yield FakeReply(404, {})
256
+ return
257
+ yield FakeReply(200, _thread_page("scripts/endless.py", "cursor-next"))
258
+
259
+ monkeypatch.setattr(reader.urllib.request, "urlopen", fake_urlopen)
260
+
261
+ with pytest.raises(GitHubError):
262
+ reader.read_review_threads("jl-cmd/claude-dev-env", 7, TOKEN)
263
+
264
+
195
265
  def should_report_a_graphql_answer_carrying_no_threads(
196
266
  monkeypatch: pytest.MonkeyPatch,
197
267
  ) -> None: