strands-coder 1.3.0__tar.gz → 1.4.1__tar.gz

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.
Files changed (26) hide show
  1. {strands_coder-1.3.0 → strands_coder-1.4.1}/PKG-INFO +3 -1
  2. {strands_coder-1.3.0 → strands_coder-1.4.1}/README.md +2 -0
  3. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/context.py +92 -13
  4. strands_coder-1.4.1/tests/test_context_graphql_null.py +133 -0
  5. {strands_coder-1.3.0 → strands_coder-1.4.1}/.github/workflows/agent.yml +0 -0
  6. {strands_coder-1.3.0 → strands_coder-1.4.1}/.github/workflows/auto-release.yml +0 -0
  7. {strands_coder-1.3.0 → strands_coder-1.4.1}/.github/workflows/control.yml +0 -0
  8. {strands_coder-1.3.0 → strands_coder-1.4.1}/.gitignore +0 -0
  9. {strands_coder-1.3.0 → strands_coder-1.4.1}/LICENSE +0 -0
  10. {strands_coder-1.3.0 → strands_coder-1.4.1}/SYSTEM_PROMPT.md +0 -0
  11. {strands_coder-1.3.0 → strands_coder-1.4.1}/action.yml +0 -0
  12. {strands_coder-1.3.0 → strands_coder-1.4.1}/docs/CNAME +0 -0
  13. {strands_coder-1.3.0 → strands_coder-1.4.1}/docs/index.html +0 -0
  14. {strands_coder-1.3.0 → strands_coder-1.4.1}/pyproject.toml +0 -0
  15. {strands_coder-1.3.0 → strands_coder-1.4.1}/setup-aws-oidc.sh +0 -0
  16. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/__init__.py +0 -0
  17. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/agent_runner.py +0 -0
  18. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/__init__.py +0 -0
  19. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/create_subagent.py +0 -0
  20. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/github_tools.py +0 -0
  21. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/projects.py +0 -0
  22. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/scheduler.py +0 -0
  23. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/store_in_kb.py +0 -0
  24. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/system_prompt.py +0 -0
  25. {strands_coder-1.3.0 → strands_coder-1.4.1}/strands_coder/tools/use_github.py +0 -0
  26. {strands_coder-1.3.0 → strands_coder-1.4.1}/tools/use_langfuse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strands-coder
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: Add an AI assistant to your GitHub repositories that can review code, manage issues, and improve over time.
5
5
  Project-URL: Homepage, https://github.com/cagataycali/strands-coder
6
6
  Project-URL: Bug Tracker, https://github.com/cagataycali/strands-coder/issues
@@ -261,6 +261,8 @@ from strands import Agent
261
261
  from strands_coder import use_github, projects, scheduler, store_in_kb
262
262
 
263
263
  # Create agent with GitHub tools
264
+ [![Awesome Strands Agents](https://img.shields.io/badge/Awesome-Strands%20Agents-00FF77?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjkwIiBoZWlnaHQ9IjQ2MyIgdmlld0JveD0iMCAwIDI5MCA0NjMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik05Ny4yOTAyIDUyLjc4ODRDODUuMDY3NCA0OS4xNjY3IDcyLjIyMzQgNTYuMTM4OSA2OC42MDE3IDY4LjM2MTZDNjQuOTgwMSA4MC41ODQzIDcxLjk1MjQgOTMuNDI4MyA4NC4xNzQ5IDk3LjA1MDFMMjM1LjExNyAxMzkuNzc1QzI0NS4yMjMgMTQyLjc2OSAyNDYuMzU3IDE1Ni42MjggMjM2Ljg3NCAxNjEuMjI2TDMyLjU0NiAyNjAuMjkxQy0xNC45NDM5IDI4My4zMTYgLTkuMTYxMDcgMzUyLjc0IDQxLjQ4MzUgMzY3LjU5MUwxODkuNTUxIDQxMS4wMDlMMTkwLjEyNSA0MTEuMTY5QzIwMi4xODMgNDE0LjM3NiAyMTQuNjY1IDQwNy4zOTYgMjE4LjE5NiAzOTUuMzU1QzIyMS43ODQgMzgzLjEyMiAyMTQuNzc0IDM3MC4yOTYgMjAyLjU0MSAzNjYuNzA5TDU0LjQ3MzggMzIzLjI5MUM0NC4zNDQ3IDMyMC4zMjEgNDMuMTg3OSAzMDYuNDM2IDUyLjY4NTcgMzAxLjgzMUwyNTcuMDE0IDIwMi43NjZDMzA0LjQzMiAxNzkuNzc2IDI5OC43NTggMTEwLjQ4MyAyNDguMjMzIDk1LjUxMkw5Ny4yOTAyIDUyLjc4ODRaIiBmaWxsPSIjRkZGRkZGIi8+CjxwYXRoIGQ9Ik0yNTkuMTQ3IDAuOTgxODEyQzI3MS4zODkgLTIuNTc0OTggMjg0LjE5NyA0LjQ2NTcxIDI4Ny43NTQgMTYuNzA3NEMyOTEuMzExIDI4Ljk0OTIgMjg0LjI3IDQxLjc1NyAyNzIuMDI4IDQ1LjMxMzhMNzEuMTcyNyAxMDMuNjcxQzQwLjcxNDIgMTEyLjUyMSAzNy4xOTc2IDE1NC4yNjIgNjUuNzQ1OSAxNjguMDgzTDI0MS4zNDMgMjUzLjA5M0MzMDcuODcyIDI4NS4zMDIgMjk5Ljc5NCAzODIuNTQ2IDIyOC44NjIgNDAzLjMzNkwzMC40MDQxIDQ2MS41MDJDMTguMTcwNyA0NjUuMDg4IDUuMzQ3MDggNDU4LjA3OCAxLjc2MTUzIDQ0NS44NDRDLTEuODIzOSA0MzMuNjExIDUuMTg2MzcgNDIwLjc4NyAxNy40MTk3IDQxNy4yMDJMMjE1Ljg3OCAzNTkuMDM1QzI0Ni4yNzcgMzUwLjEyNSAyNDkuNzM5IDMwOC40NDkgMjIxLjIyNiAyOTQuNjQ1TDQ1LjYyOTcgMjA5LjYzNUMtMjAuOTgzNCAxNzcuMzg2IC0xMi43NzcyIDc5Ljk4OTMgNTguMjkyOCA1OS4zNDAyTDI1OS4xNDcgMC45ODE4MTJaIiBmaWxsPSIjRkZGRkZGIi8+Cjwvc3ZnPgo=&logoColor=white)](https://github.com/cagataycali/awesome-strands-agents)
265
+
264
266
  agent = Agent(
265
267
  tools=[use_github, projects, scheduler, store_in_kb],
266
268
  system_prompt="You are an autonomous GitHub agent."
@@ -217,6 +217,8 @@ from strands import Agent
217
217
  from strands_coder import use_github, projects, scheduler, store_in_kb
218
218
 
219
219
  # Create agent with GitHub tools
220
+ [![Awesome Strands Agents](https://img.shields.io/badge/Awesome-Strands%20Agents-00FF77?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjkwIiBoZWlnaHQ9IjQ2MyIgdmlld0JveD0iMCAwIDI5MCA0NjMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik05Ny4yOTAyIDUyLjc4ODRDODUuMDY3NCA0OS4xNjY3IDcyLjIyMzQgNTYuMTM4OSA2OC42MDE3IDY4LjM2MTZDNjQuOTgwMSA4MC41ODQzIDcxLjk1MjQgOTMuNDI4MyA4NC4xNzQ5IDk3LjA1MDFMMjM1LjExNyAxMzkuNzc1QzI0NS4yMjMgMTQyLjc2OSAyNDYuMzU3IDE1Ni42MjggMjM2Ljg3NCAxNjEuMjI2TDMyLjU0NiAyNjAuMjkxQy0xNC45NDM5IDI4My4zMTYgLTkuMTYxMDcgMzUyLjc0IDQxLjQ4MzUgMzY3LjU5MUwxODkuNTUxIDQxMS4wMDlMMTkwLjEyNSA0MTEuMTY5QzIwMi4xODMgNDE0LjM3NiAyMTQuNjY1IDQwNy4zOTYgMjE4LjE5NiAzOTUuMzU1QzIyMS43ODQgMzgzLjEyMiAyMTQuNzc0IDM3MC4yOTYgMjAyLjU0MSAzNjYuNzA5TDU0LjQ3MzggMzIzLjI5MUM0NC4zNDQ3IDMyMC4zMjEgNDMuMTg3OSAzMDYuNDM2IDUyLjY4NTcgMzAxLjgzMUwyNTcuMDE0IDIwMi43NjZDMzA0LjQzMiAxNzkuNzc2IDI5OC43NTggMTEwLjQ4MyAyNDguMjMzIDk1LjUxMkw5Ny4yOTAyIDUyLjc4ODRaIiBmaWxsPSIjRkZGRkZGIi8+CjxwYXRoIGQ9Ik0yNTkuMTQ3IDAuOTgxODEyQzI3MS4zODkgLTIuNTc0OTggMjg0LjE5NyA0LjQ2NTcxIDI4Ny43NTQgMTYuNzA3NEMyOTEuMzExIDI4Ljk0OTIgMjg0LjI3IDQxLjc1NyAyNzIuMDI4IDQ1LjMxMzhMNzEuMTcyNyAxMDMuNjcxQzQwLjcxNDIgMTEyLjUyMSAzNy4xOTc2IDE1NC4yNjIgNjUuNzQ1OSAxNjguMDgzTDI0MS4zNDMgMjUzLjA5M0MzMDcuODcyIDI4NS4zMDIgMjk5Ljc5NCAzODIuNTQ2IDIyOC44NjIgNDAzLjMzNkwzMC40MDQxIDQ2MS41MDJDMTguMTcwNyA0NjUuMDg4IDUuMzQ3MDggNDU4LjA3OCAxLjc2MTUzIDQ0NS44NDRDLTEuODIzOSA0MzMuNjExIDUuMTg2MzcgNDIwLjc4NyAxNy40MTk3IDQxNy4yMDJMMjE1Ljg3OCAzNTkuMDM1QzI0Ni4yNzcgMzUwLjEyNSAyNDkuNzM5IDMwOC40NDkgMjIxLjIyNiAyOTQuNjQ1TDQ1LjYyOTcgMjA5LjYzNUMtMjAuOTgzNCAxNzcuMzg2IC0xMi43NzcyIDc5Ljk4OTMgNTguMjkyOCA1OS4zNDAyTDI1OS4xNDcgMC45ODE4MTJaIiBmaWxsPSIjRkZGRkZGIi8+Cjwvc3ZnPgo=&logoColor=white)](https://github.com/cagataycali/awesome-strands-agents)
221
+
220
222
  agent = Agent(
221
223
  tools=[use_github, projects, scheduler, store_in_kb],
222
224
  system_prompt="You are an autonomous GitHub agent."
@@ -218,18 +218,24 @@ def fetch_github_event_context() -> str:
218
218
  if "errors" in data:
219
219
  print(f"⚠ GraphQL errors: {data['errors']}")
220
220
  else:
221
- issue_data = data.get("data", {}).get("repository", {}).get("issue", {})
221
+ # GraphQL returns null (not {}) for absent nodes - e.g. when the
222
+ # number refers to a PR (issue: null) or the repo/data is null on a
223
+ # partial error. Chained .get(k, {}) does NOT guard against a present
224
+ # key with a null value, so coalesce every nullable node with `or {}`.
225
+ issue_data = (
226
+ ((data.get("data") or {}).get("repository") or {}).get("issue") or {}
227
+ )
222
228
 
223
229
  # Comments
224
- comments = issue_data.get("comments", {}).get("nodes", [])
225
- total_comments = issue_data.get("comments", {}).get("totalCount", 0)
230
+ comments = (issue_data.get("comments") or {}).get("nodes") or []
231
+ total_comments = (issue_data.get("comments") or {}).get("totalCount", 0)
226
232
 
227
233
  if comments:
228
234
  context_parts.append(
229
235
  f"\n### 💬 Comments ({total_comments} total)\n"
230
236
  )
231
237
  for idx, comment in enumerate(comments, 1):
232
- author = comment.get("author", {}).get("login", "unknown")
238
+ author = (comment.get("author") or {}).get("login", "unknown")
233
239
  body = comment.get("body", "")
234
240
  created = comment.get("createdAt", "")
235
241
  context_parts.append(f"""
@@ -240,10 +246,10 @@ def fetch_github_event_context() -> str:
240
246
  """)
241
247
 
242
248
  # Linked PRs/Issues
243
- timeline = issue_data.get("timelineItems", {}).get("nodes", [])
249
+ timeline = (issue_data.get("timelineItems") or {}).get("nodes") or []
244
250
  linked_items = []
245
251
  for item in timeline:
246
- source = item.get("source", {})
252
+ source = item.get("source") or {}
247
253
  if source:
248
254
  num = source.get("number")
249
255
  title = source.get("title")
@@ -367,11 +373,11 @@ def fetch_github_event_context() -> str:
367
373
  print(f"⚠ GraphQL errors: {data['errors']}")
368
374
  else:
369
375
  pr_data = (
370
- data.get("data", {}).get("repository", {}).get("pullRequest", {})
376
+ ((data.get("data") or {}).get("repository") or {}).get("pullRequest") or {}
371
377
  )
372
378
 
373
379
  # Reviews
374
- reviews = pr_data.get("reviews", {}).get("nodes", [])
380
+ reviews = (pr_data.get("reviews") or {}).get("nodes") or []
375
381
  total_reviews = pr_data.get("reviews", {}).get("totalCount", 0)
376
382
 
377
383
  if reviews:
@@ -397,7 +403,7 @@ def fetch_github_event_context() -> str:
397
403
  f"\n### 💬 Comments ({total_comments} total)\n"
398
404
  )
399
405
  for idx, comment in enumerate(comments, 1):
400
- author = comment.get("author", {}).get("login", "unknown")
406
+ author = (comment.get("author") or {}).get("login", "unknown")
401
407
  body = comment.get("body", "")
402
408
  created = comment.get("createdAt", "")
403
409
  context_parts.append(f"""
@@ -526,15 +532,15 @@ def fetch_github_event_context() -> str:
526
532
  print(f"⚠ GraphQL errors: {data['errors']}")
527
533
  else:
528
534
  disc_data = (
529
- data.get("data", {}).get("repository", {}).get("discussion", {})
535
+ ((data.get("data") or {}).get("repository") or {}).get("discussion") or {}
530
536
  )
531
- comments = disc_data.get("comments", {}).get("nodes", [])
532
- total_comments = disc_data.get("comments", {}).get("totalCount", 0)
537
+ comments = (disc_data.get("comments") or {}).get("nodes") or []
538
+ total_comments = (disc_data.get("comments") or {}).get("totalCount", 0)
533
539
 
534
540
  if comments:
535
541
  context_parts.append(f"\n### 💬 Replies ({total_comments} total)\n")
536
542
  for idx, comment in enumerate(comments, 1):
537
- author = comment.get("author", {}).get("login", "unknown")
543
+ author = (comment.get("author") or {}).get("login", "unknown")
538
544
  body = comment.get("body", "")
539
545
  created = comment.get("createdAt", "")
540
546
  context_parts.append(f"""
@@ -783,6 +789,74 @@ def extract_user_message() -> str:
783
789
  return ""
784
790
 
785
791
 
792
+ def load_agents_md() -> str:
793
+ """
794
+ Load AGENTS.md from the workspace for project-specific rules and learnings.
795
+
796
+ Searches for AGENTS.md in:
797
+ 1. Current working directory (repo root in CI)
798
+ 2. GITHUB_WORKSPACE env var (GitHub Actions workspace)
799
+ 3. Parent directories up to 3 levels (for monorepo support)
800
+
801
+ AGENTS.md is a living document that contains:
802
+ - Code standards and non-negotiable rules
803
+ - Learnings from past reviews and bugs
804
+ - Patterns to follow for new contributions
805
+ - Self-update protocol for autonomous agents
806
+
807
+ Returns formatted markdown for injection into system prompt.
808
+ """
809
+ search_paths = []
810
+
811
+ # 1. Current working directory
812
+ cwd = Path.cwd()
813
+ search_paths.append(cwd / "AGENTS.md")
814
+
815
+ # 2. GitHub Actions workspace
816
+ workspace = os.environ.get("GITHUB_WORKSPACE")
817
+ if workspace:
818
+ search_paths.append(Path(workspace) / "AGENTS.md")
819
+
820
+ # 3. Parent directories (up to 3 levels for monorepos)
821
+ for i in range(1, 4):
822
+ parent = cwd
823
+ for _ in range(i):
824
+ parent = parent.parent
825
+ search_paths.append(parent / "AGENTS.md")
826
+
827
+ # Deduplicate while preserving order
828
+ seen: set[str] = set()
829
+ unique_paths = []
830
+ for p in search_paths:
831
+ resolved = str(p.resolve())
832
+ if resolved not in seen:
833
+ seen.add(resolved)
834
+ unique_paths.append(p)
835
+
836
+ for agents_path in unique_paths:
837
+ try:
838
+ if agents_path.exists() and agents_path.is_file():
839
+ content = agents_path.read_text(encoding="utf-8", errors="ignore")
840
+ if content.strip():
841
+ print(f"✓ AGENTS.md loaded from {agents_path} ({len(content)} chars)")
842
+ return f"""
843
+ ---
844
+ ## 📋 AGENTS.md — Project Rules & Learnings
845
+
846
+ **Source:** `{agents_path}`
847
+ **Last Modified:** {datetime.fromtimestamp(agents_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")}
848
+
849
+ {content}
850
+
851
+ ---
852
+ """
853
+ except Exception as e:
854
+ print(f"⚠ Failed to read {agents_path}: {e}")
855
+ continue
856
+
857
+ return ""
858
+
859
+
786
860
  def build_system_prompt() -> str:
787
861
  """Build comprehensive system prompt from environment variables and context."""
788
862
  # Base system prompt
@@ -797,6 +871,11 @@ def build_system_prompt() -> str:
797
871
  if input_system_prompt:
798
872
  base_prompt = f"{base_prompt}\n\n{input_system_prompt}"
799
873
 
874
+ # Add AGENTS.md project rules and learnings (loaded EARLY so all context is framed by rules)
875
+ agents_md = load_agents_md()
876
+ if agents_md:
877
+ base_prompt = f"{base_prompt}\n\n{agents_md}"
878
+
800
879
  # Add rich GitHub event context (issue threads, PR reviews, etc.)
801
880
  github_event_context = fetch_github_event_context()
802
881
  if github_event_context:
@@ -0,0 +1,133 @@
1
+ """Regression tests for fetch_github_event_context GraphQL-null handling.
2
+
3
+ GitHub's GraphQL API returns ``null`` (not ``{}``) for an absent node - e.g.
4
+ ``repository.issue`` is ``null`` when the queried number is a PR, or when a
5
+ partial error nulls a field. ``dict.get(key, {})`` does NOT guard against a key
6
+ that is present with a ``None`` value, so the old code crashed with::
7
+
8
+ AttributeError: 'NoneType' object has no attribute 'get'
9
+
10
+ at ``issue_data.get("comments", {})``. These tests pin the fix: a null node must
11
+ degrade gracefully (return context built so far), never raise.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from unittest.mock import patch
18
+
19
+ import importlib.util as _ilu
20
+ import os as _os
21
+
22
+ _CTX_PATH = _os.path.join(
23
+ _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))),
24
+ "strands_coder",
25
+ "context.py",
26
+ )
27
+ _spec = _ilu.spec_from_file_location("strands_coder_context", _CTX_PATH)
28
+ _mod = _ilu.module_from_spec(_spec)
29
+ _spec.loader.exec_module(_mod)
30
+ fetch_github_event_context = _mod.fetch_github_event_context
31
+
32
+
33
+ class _FakeResponse:
34
+ def __init__(self, payload: dict):
35
+ self._payload = payload
36
+
37
+ def raise_for_status(self) -> None:
38
+ return None
39
+
40
+ def json(self) -> dict:
41
+ return self._payload
42
+
43
+
44
+ def _ctx(event_name: str, event: dict) -> str:
45
+ return json.dumps(
46
+ {
47
+ "event_name": event_name,
48
+ "event": event,
49
+ "repository": "owner/repo",
50
+ "actor": "tester",
51
+ }
52
+ )
53
+
54
+
55
+ def test_issue_node_null_does_not_raise(monkeypatch):
56
+ """repository.issue == null (number is actually a PR) must not crash."""
57
+ monkeypatch.setenv("GITHUB_CONTEXT", _ctx("issues", {"issue": {"number": 7}, "action": "opened"}))
58
+ monkeypatch.setenv("GITHUB_TOKEN", "x")
59
+ monkeypatch.delenv("PAT_TOKEN", raising=False)
60
+
61
+ with patch("requests.post", return_value=_FakeResponse({"data": {"repository": {"issue": None}}})):
62
+ out = fetch_github_event_context()
63
+
64
+ assert isinstance(out, str)
65
+ assert "owner/repo" in out # raw context header still rendered
66
+
67
+
68
+ def test_repository_null_does_not_raise(monkeypatch):
69
+ """data.repository == null (partial error) must not crash."""
70
+ monkeypatch.setenv("GITHUB_CONTEXT", _ctx("issues", {"issue": {"number": 7}, "action": "opened"}))
71
+ monkeypatch.setenv("GITHUB_TOKEN", "x")
72
+ monkeypatch.delenv("PAT_TOKEN", raising=False)
73
+
74
+ with patch("requests.post", return_value=_FakeResponse({"data": {"repository": None}})):
75
+ out = fetch_github_event_context()
76
+
77
+ assert isinstance(out, str)
78
+
79
+
80
+ def test_pull_request_node_null_does_not_raise(monkeypatch):
81
+ """repository.pullRequest == null must not crash."""
82
+ monkeypatch.setenv(
83
+ "GITHUB_CONTEXT",
84
+ _ctx("pull_request", {"pull_request": {"number": 9}, "action": "opened"}),
85
+ )
86
+ monkeypatch.setenv("GITHUB_TOKEN", "x")
87
+ monkeypatch.delenv("PAT_TOKEN", raising=False)
88
+
89
+ with patch("requests.post", return_value=_FakeResponse({"data": {"repository": {"pullRequest": None}}})):
90
+ out = fetch_github_event_context()
91
+
92
+ assert isinstance(out, str)
93
+
94
+
95
+ def test_discussion_node_null_does_not_raise(monkeypatch):
96
+ """repository.discussion == null must not crash."""
97
+ monkeypatch.setenv(
98
+ "GITHUB_CONTEXT",
99
+ _ctx("discussion", {"discussion": {"number": 3}, "action": "created"}),
100
+ )
101
+ monkeypatch.setenv("GITHUB_TOKEN", "x")
102
+ monkeypatch.delenv("PAT_TOKEN", raising=False)
103
+
104
+ with patch("requests.post", return_value=_FakeResponse({"data": {"repository": {"discussion": None}}})):
105
+ out = fetch_github_event_context()
106
+
107
+ assert isinstance(out, str)
108
+
109
+
110
+ def test_null_comment_author_does_not_raise(monkeypatch):
111
+ """A comment whose author is null (deleted user) must not crash."""
112
+ monkeypatch.setenv("GITHUB_CONTEXT", _ctx("issues", {"issue": {"number": 7}, "action": "opened"}))
113
+ monkeypatch.setenv("GITHUB_TOKEN", "x")
114
+ monkeypatch.delenv("PAT_TOKEN", raising=False)
115
+
116
+ payload = {
117
+ "data": {
118
+ "repository": {
119
+ "issue": {
120
+ "comments": {
121
+ "totalCount": 1,
122
+ "nodes": [{"author": None, "body": "hi", "createdAt": "2026-01-01"}],
123
+ },
124
+ "timelineItems": {"nodes": [{"source": None}]},
125
+ }
126
+ }
127
+ }
128
+ }
129
+ with patch("requests.post", return_value=_FakeResponse(payload)):
130
+ out = fetch_github_event_context()
131
+
132
+ assert isinstance(out, str)
133
+ assert "@unknown" in out
File without changes
File without changes
File without changes
File without changes