boomtick-cli 0.9.0__tar.gz → 0.9.2__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 (163) hide show
  1. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/PKG-INFO +1 -1
  2. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/boomtick_cli.egg-info/PKG-INFO +1 -1
  3. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/boomtick_cli.egg-info/SOURCES.txt +9 -0
  4. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/cli-schema.json +26 -0
  5. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/cli.py +16 -12
  6. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/config.py +12 -5
  7. boomtick_cli-0.9.2/dev_tools/dist/config.test.js +115 -0
  8. boomtick_cli-0.9.2/dev_tools/dist/lib/error_utils.test.js +23 -0
  9. boomtick_cli-0.9.2/dev_tools/dist/lib/result.test.js +45 -0
  10. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/td-cli.js +27 -0
  11. boomtick_cli-0.9.2/dev_tools/dist/mcp/server.test.js +217 -0
  12. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/contract.js +28 -2
  13. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.create_issue.js +2 -0
  14. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.create_issue.test.js +36 -0
  15. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.create_pull_request.js +11 -13
  16. boomtick_cli-0.9.2/dev_tools/dist/tools/github.create_pull_request.test.js +93 -0
  17. boomtick_cli-0.9.2/dev_tools/dist/tools/jules/shared.test.js +58 -0
  18. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.get_package_scripts.test.js +4 -0
  19. boomtick_cli-0.9.2/dev_tools/dist/tools/repo.read_agent_context.js +16 -0
  20. boomtick_cli-0.9.2/dev_tools/dist/tools/repo.read_agent_context.test.js +10 -0
  21. boomtick_cli-0.9.2/dev_tools/handlers/workflow_installer.py +61 -0
  22. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/models/__init__.py +2 -0
  23. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/orchestrator.py +233 -196
  24. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/pr_overlap.py +22 -8
  25. boomtick_cli-0.9.2/dev_tools/resources/__pycache__/__init__.cpython-312.pyc +0 -0
  26. boomtick_cli-0.9.2/dev_tools/resources/__pycache__/build-repo-context.cpython-312.pyc +0 -0
  27. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/resources/build-repo-context.py +9 -31
  28. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/resources/prompt_constants.json +2 -1
  29. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/schema_gen.py +1 -1
  30. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/ai_service.py +98 -70
  31. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/context_builder.py +14 -5
  32. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/github.py +111 -99
  33. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/vision_service.py +11 -5
  34. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/utils/__init__.py +26 -10
  35. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/pyproject.toml +1 -1
  36. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_config.py +24 -0
  37. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_detect_conflicts.py +12 -4
  38. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_install_workflows.py +2 -2
  39. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_jules_feedback.py +9 -3
  40. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_pr_overlap.py +24 -0
  41. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_search_issues.py +1 -1
  42. boomtick_cli-0.9.2/tests/test_workflow_installer.py +59 -0
  43. boomtick_cli-0.9.0/dev_tools/dist/tools/repo.read_agent_context.js +0 -20
  44. boomtick_cli-0.9.0/dev_tools/resources/__pycache__/__init__.cpython-312.pyc +0 -0
  45. boomtick_cli-0.9.0/dev_tools/resources/__pycache__/build-repo-context.cpython-312.pyc +0 -0
  46. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/MANIFEST.in +0 -0
  47. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/README.md +0 -0
  48. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/boomtick_cli.egg-info/dependency_links.txt +0 -0
  49. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/boomtick_cli.egg-info/entry_points.txt +0 -0
  50. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/boomtick_cli.egg-info/requires.txt +0 -0
  51. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/boomtick_cli.egg-info/top_level.txt +0 -0
  52. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/__init__.py +0 -0
  53. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/constants.py +0 -0
  54. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/daemon.py +0 -0
  55. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/config.js +0 -0
  56. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/evals/run-evals.js +0 -0
  57. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/index.js +0 -0
  58. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/error_utils.js +0 -0
  59. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/git.js +0 -0
  60. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/result.js +0 -0
  61. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/shell.js +0 -0
  62. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/shell.test.js +0 -0
  63. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/lib/test-utils.js +0 -0
  64. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/mcp/definitions.js +0 -0
  65. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/mcp/server.js +0 -0
  66. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/mcp/tools.js +0 -0
  67. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/ddgs.search.js +0 -0
  68. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/ddgs.search.test.js +0 -0
  69. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/ddgs_search.py +0 -0
  70. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.checkout_branch.js +0 -0
  71. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.comment_triage_summary.js +0 -0
  72. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.get_merge_conflict_files.js +0 -0
  73. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.get_pr.js +0 -0
  74. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.get_pr_diff.js +0 -0
  75. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.issue_comment.js +0 -0
  76. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.issue_update.js +0 -0
  77. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.issue_view.js +0 -0
  78. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.open_replacement_pr.js +0 -0
  79. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.search_open_prs.js +0 -0
  80. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/github.search_open_prs.test.js +0 -0
  81. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/cancel-session.js +0 -0
  82. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/create-session.js +0 -0
  83. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/create-session.test.js +0 -0
  84. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/get-messages.js +0 -0
  85. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/get-messages.test.js +0 -0
  86. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/get-pr.js +0 -0
  87. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/get-pr.test.js +0 -0
  88. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/get-session.js +0 -0
  89. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/list-sessions.js +0 -0
  90. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/send-message.js +0 -0
  91. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/send-message.test.js +0 -0
  92. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/shared.js +0 -0
  93. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/trigger-feedback.js +0 -0
  94. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/jules/trigger-feedback.test.js +0 -0
  95. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.commit_patch.js +0 -0
  96. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.create_branch.js +0 -0
  97. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.create_branch.test.js +0 -0
  98. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.create_repair_branch.js +0 -0
  99. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.get_changed_files.js +0 -0
  100. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.get_command_schema.js +0 -0
  101. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.get_package_scripts.js +0 -0
  102. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.get_route_map.js +0 -0
  103. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.logs.js +0 -0
  104. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.read_ci_logs.js +0 -0
  105. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.run_lighthouse.js +0 -0
  106. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.run_playwright.js +0 -0
  107. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/repo.run_tests.js +0 -0
  108. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/dist/tools/types.js +0 -0
  109. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/get_ai_context.py +0 -0
  110. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/handlers/__init__.py +0 -0
  111. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/handlers/command_handler.py +0 -0
  112. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/mcp_server.py +0 -0
  113. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/models/base.py +0 -0
  114. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/models/contracts.py +0 -0
  115. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/resources/__init__.py +0 -0
  116. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/resources/review_template.md +0 -0
  117. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/resources/ux-audit.config.json +0 -0
  118. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/resources/visual_guidelines.json +0 -0
  119. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/review_read_pass.py +0 -0
  120. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/schema_utils.py +0 -0
  121. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/__init__.py +0 -0
  122. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/dependency_graph.py +0 -0
  123. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/jules.py +0 -0
  124. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/repair_service.py +0 -0
  125. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/services/vector_store.py +0 -0
  126. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/td_cli.py +0 -0
  127. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/utils/concurrency.py +0 -0
  128. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/utils/git.py +0 -0
  129. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/ux_report.py +0 -0
  130. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/verify_infra.py +0 -0
  131. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/verify_versions.py +0 -0
  132. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/version_utils.py +0 -0
  133. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/workflows/__init__.py +0 -0
  134. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/workflows/context.py +0 -0
  135. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/workflows/graph.py +0 -0
  136. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/workflows/node.py +0 -0
  137. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/workflows/nodes.py +0 -0
  138. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/dev_tools/workflows/runner.py +0 -0
  139. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/setup.cfg +0 -0
  140. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_build_repo_context.py +0 -0
  141. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_buildx_compliance.py +0 -0
  142. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_concurrency.py +0 -0
  143. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_dependency_graph.py +0 -0
  144. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_fail_fast.py +0 -0
  145. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_github_diff_parser.py +0 -0
  146. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_github_no_gh.py +0 -0
  147. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_github_parser.py +0 -0
  148. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_issue_validation_local.py +0 -0
  149. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_issue_view.py +0 -0
  150. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_jules_batch_send.py +0 -0
  151. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_labels.py +0 -0
  152. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_latency_mitigation.py +0 -0
  153. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_opentelemetry_check.py +0 -0
  154. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_orchestrator_aggregation.py +0 -0
  155. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_orchestrator_heuristics.py +0 -0
  156. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_parse_comment.py +0 -0
  157. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_plan_issue_audit.py +0 -0
  158. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_review_template_validation.py +0 -0
  159. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_schema_utils.py +0 -0
  160. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_verify_ci_metrics.py +0 -0
  161. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_verify_scope.py +0 -0
  162. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_workflow_audit.py +0 -0
  163. {boomtick_cli-0.9.0 → boomtick_cli-0.9.2}/tests/test_workflow_orchestration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boomtick-cli
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: BoomTick CLI — agentic dev-tools for GitHub + AI workflows
5
5
  Author: Ariel Anders
6
6
  Project-URL: Homepage, https://github.com/arii/boomtick
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boomtick-cli
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: BoomTick CLI — agentic dev-tools for GitHub + AI workflows
5
5
  Author: Ariel Anders
6
6
  Project-URL: Homepage, https://github.com/arii/boomtick
@@ -26,17 +26,21 @@ dev_tools/verify_infra.py
26
26
  dev_tools/verify_versions.py
27
27
  dev_tools/version_utils.py
28
28
  dev_tools/dist/config.js
29
+ dev_tools/dist/config.test.js
29
30
  dev_tools/dist/index.js
30
31
  dev_tools/dist/evals/run-evals.js
31
32
  dev_tools/dist/lib/error_utils.js
33
+ dev_tools/dist/lib/error_utils.test.js
32
34
  dev_tools/dist/lib/git.js
33
35
  dev_tools/dist/lib/result.js
36
+ dev_tools/dist/lib/result.test.js
34
37
  dev_tools/dist/lib/shell.js
35
38
  dev_tools/dist/lib/shell.test.js
36
39
  dev_tools/dist/lib/td-cli.js
37
40
  dev_tools/dist/lib/test-utils.js
38
41
  dev_tools/dist/mcp/definitions.js
39
42
  dev_tools/dist/mcp/server.js
43
+ dev_tools/dist/mcp/server.test.js
40
44
  dev_tools/dist/mcp/tools.js
41
45
  dev_tools/dist/tools/contract.js
42
46
  dev_tools/dist/tools/ddgs.search.js
@@ -47,6 +51,7 @@ dev_tools/dist/tools/github.comment_triage_summary.js
47
51
  dev_tools/dist/tools/github.create_issue.js
48
52
  dev_tools/dist/tools/github.create_issue.test.js
49
53
  dev_tools/dist/tools/github.create_pull_request.js
54
+ dev_tools/dist/tools/github.create_pull_request.test.js
50
55
  dev_tools/dist/tools/github.get_merge_conflict_files.js
51
56
  dev_tools/dist/tools/github.get_pr.js
52
57
  dev_tools/dist/tools/github.get_pr_diff.js
@@ -67,6 +72,7 @@ dev_tools/dist/tools/repo.get_package_scripts.test.js
67
72
  dev_tools/dist/tools/repo.get_route_map.js
68
73
  dev_tools/dist/tools/repo.logs.js
69
74
  dev_tools/dist/tools/repo.read_agent_context.js
75
+ dev_tools/dist/tools/repo.read_agent_context.test.js
70
76
  dev_tools/dist/tools/repo.read_ci_logs.js
71
77
  dev_tools/dist/tools/repo.run_lighthouse.js
72
78
  dev_tools/dist/tools/repo.run_playwright.js
@@ -84,10 +90,12 @@ dev_tools/dist/tools/jules/list-sessions.js
84
90
  dev_tools/dist/tools/jules/send-message.js
85
91
  dev_tools/dist/tools/jules/send-message.test.js
86
92
  dev_tools/dist/tools/jules/shared.js
93
+ dev_tools/dist/tools/jules/shared.test.js
87
94
  dev_tools/dist/tools/jules/trigger-feedback.js
88
95
  dev_tools/dist/tools/jules/trigger-feedback.test.js
89
96
  dev_tools/handlers/__init__.py
90
97
  dev_tools/handlers/command_handler.py
98
+ dev_tools/handlers/workflow_installer.py
91
99
  dev_tools/models/__init__.py
92
100
  dev_tools/models/base.py
93
101
  dev_tools/models/contracts.py
@@ -146,4 +154,5 @@ tests/test_search_issues.py
146
154
  tests/test_verify_ci_metrics.py
147
155
  tests/test_verify_scope.py
148
156
  tests/test_workflow_audit.py
157
+ tests/test_workflow_installer.py
149
158
  tests/test_workflow_orchestration.py
@@ -1200,6 +1200,19 @@
1200
1200
  ],
1201
1201
  "default": null,
1202
1202
  "title": "File"
1203
+ },
1204
+ "repo": {
1205
+ "anyOf": [
1206
+ {
1207
+ "type": "string"
1208
+ },
1209
+ {
1210
+ "type": "null"
1211
+ }
1212
+ ],
1213
+ "default": null,
1214
+ "description": "The target repository override (e.g. org/repo).",
1215
+ "title": "Repo"
1203
1216
  }
1204
1217
  },
1205
1218
  "required": [
@@ -1350,6 +1363,19 @@
1350
1363
  "description": "Whether to create the PR as a draft.",
1351
1364
  "title": "Draft",
1352
1365
  "type": "boolean"
1366
+ },
1367
+ "repo": {
1368
+ "anyOf": [
1369
+ {
1370
+ "type": "string"
1371
+ },
1372
+ {
1373
+ "type": "null"
1374
+ }
1375
+ ],
1376
+ "default": null,
1377
+ "description": "The target repository override (e.g. org/repo).",
1378
+ "title": "Repo"
1353
1379
  }
1354
1380
  },
1355
1381
  "required": [
@@ -414,13 +414,14 @@ def audit_pr(ctx, pr_number, fetch, run_audit, submit, cleanup, dry_run, base, e
414
414
  @click.option("--title", required=True, help="Issue title")
415
415
  @click.option("--file", help="Path to file containing issue body")
416
416
  @click.option("--body", help="Literal body text")
417
+ @click.option("--repo", help="Target repository override")
417
418
  @click.pass_context
418
- def create_issue(ctx, title, file, body):
419
+ def create_issue(ctx, title, file, body, repo):
419
420
  """Create a new GitHub issue."""
420
421
  orch = ctx.obj["ORCHESTRATOR"]
421
422
 
422
423
  content = _get_body_content(ctx, orch, file, body)
423
- res = orch.create_issue(title, content)
424
+ res = orch.create_issue(title, content, repo=repo)
424
425
  out(ctx, f"✅ Successfully created issue: {res['issue'].get('html_url')}", data=res)
425
426
 
426
427
 
@@ -560,11 +561,12 @@ def scaffold_issue(ctx, file):
560
561
  @click.option("--head", required=True)
561
562
  @click.option("--base", default=PROJECT_CONFIG.base_branch_name)
562
563
  @click.option("--draft", is_flag=True)
564
+ @click.option("--repo", help="Target repository override")
563
565
  @click.pass_context
564
- def create_pr(ctx, title, body, head, base, draft):
566
+ def create_pr(ctx, title, body, head, base, draft, repo):
565
567
  """Create a new pull request."""
566
568
  orch = ctx.obj["ORCHESTRATOR"]
567
- res = orch.github.create_pull_request(title, body, head, base, draft=draft)
569
+ res = orch.create_pull_request(title, body, head, base, draft=draft, repo=repo)
568
570
  out(ctx, f"✅ Created PR: {res.get('html_url')}", data={"pr": res})
569
571
 
570
572
 
@@ -1554,17 +1556,19 @@ def plan_workflow_audit(ctx, workflow):
1554
1556
 
1555
1557
 
1556
1558
  @agent_group.command(name="install-workflows")
1557
- @click.option("--dry-run/--execute", default=True)
1559
+ @click.option("--target", default=".", help="Target directory to install workflows.")
1560
+ @click.option("--dry-run/--execute", default=True, help="Run without writing files.")
1561
+ @click.option("--force", is_flag=True, default=False, help="Overwrite existing files.")
1558
1562
  @click.pass_context
1559
- def install_workflows(ctx, dry_run):
1560
- """Install and configure Jules/AI automation workflows for submodule or standalone use."""
1561
- orch = ctx.obj["ORCHESTRATOR"]
1563
+ def install_workflows(ctx, target, dry_run, force):
1564
+ """Install and configure standardized GitHub Actions workflows."""
1565
+ from dev_tools.handlers.workflow_installer import install_workflows as do_install
1562
1566
  try:
1563
- res = orch.install_workflows(dry_run=dry_run)
1564
- if res["status"] == "success":
1565
- out(ctx, "✅ Workflows installed successfully.", data=res)
1567
+ success = do_install(target_dir=target, dry_run=dry_run, force=force)
1568
+ if success:
1569
+ out(ctx, "✅ Workflows installation complete.", data={"status": "success"})
1566
1570
  else:
1567
- out(ctx, "⚠️ Workflows partially installed or encountered errors.", data=res)
1571
+ err(ctx, "Workflows installation failed.")
1568
1572
  except Exception as e:
1569
1573
  _handle_unexpected_error(ctx, "agent install-workflows", e)
1570
1574
 
@@ -1,5 +1,4 @@
1
1
  # pylint: disable=import-outside-toplevel,line-too-long,missing-docstring,too-many-branches,too-many-instance-attributes,too-many-statements,use-maxsplit-arg,cyclic-import
2
- from __future__ import annotations
3
2
  import subprocess
4
3
  import re
5
4
 
@@ -13,6 +12,7 @@ from typing import Any, Dict, List, Optional
13
12
  @dataclass(frozen=True)
14
13
  class ProjectConfig:
15
14
  github_repo: str | None = None
15
+ package_name: str | None = None
16
16
  github_token_env: str = "GITHUB_TOKEN"
17
17
  jules_api_url: str | None = None
18
18
  core_dirs: List[str] = field(default_factory=lambda: ["src/layouts/", "src/components/"])
@@ -154,8 +154,9 @@ def load_project_config(path: str | Path = "project_config.json") -> ProjectConf
154
154
  if p.exists():
155
155
  try:
156
156
  raw = json.loads(p.read_text(encoding="utf-8"))
157
- except (json.JSONDecodeError, IOError):
158
- pass
157
+ except (json.JSONDecodeError, IOError) as e:
158
+ import sys
159
+ print(f"Warning: Failed to parse config file {p}: {e}", file=sys.stderr)
159
160
  elif str(path) == "project_config.json":
160
161
  # Check parent directories for project_config.json if not in CWD
161
162
  # This helps when running from subdirectories
@@ -166,8 +167,9 @@ def load_project_config(path: str | Path = "project_config.json") -> ProjectConf
166
167
  try:
167
168
  raw = json.loads(check_path.read_text(encoding="utf-8"))
168
169
  break
169
- except (json.JSONDecodeError, IOError):
170
- pass
170
+ except (json.JSONDecodeError, IOError) as e:
171
+ import sys
172
+ print(f"Warning: Failed to parse candidate config file {check_path}: {e}", file=sys.stderr)
171
173
 
172
174
  def get_list(key: str) -> Optional[List[str]]:
173
175
  val = raw.get(key)
@@ -191,6 +193,11 @@ def load_project_config(path: str | Path = "project_config.json") -> ProjectConf
191
193
  else:
192
194
  kwargs["github_repo"] = _detect_repo_name()
193
195
 
196
+ if "package_name" in raw:
197
+ kwargs["package_name"] = raw["package_name"]
198
+ elif "packageName" in raw:
199
+ kwargs["package_name"] = raw["packageName"]
200
+
194
201
  if "vite_base_path" in raw:
195
202
  kwargs["vite_base_path"] = raw["vite_base_path"]
196
203
  if "gh_path" in raw:
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { execSync } from "child_process";
3
+ vi.mock("child_process", () => ({
4
+ execSync: vi.fn(),
5
+ }));
6
+ describe("config", () => {
7
+ const originalEnv = process.env;
8
+ beforeEach(() => {
9
+ vi.resetModules();
10
+ process.env = { ...originalEnv };
11
+ vi.mocked(execSync).mockReset();
12
+ vi.mocked(execSync).mockReturnValue("{}");
13
+ });
14
+ afterEach(() => {
15
+ process.env = originalEnv;
16
+ });
17
+ describe("initializeConfig", () => {
18
+ it("should load dynamic config from td-cli config view", async () => {
19
+ const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
20
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
21
+ const { initializeConfig } = await import("./config.js");
22
+ const result = initializeConfig();
23
+ expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.anything());
24
+ expect(result).toEqual(mockConfig);
25
+ });
26
+ it("should return cached dynamic config on subsequent calls", async () => {
27
+ const mockConfig = { github_repo: "owner/repo" };
28
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
29
+ const { initializeConfig } = await import("./config.js");
30
+ const result1 = initializeConfig();
31
+ const result2 = initializeConfig();
32
+ expect(result1).toBe(result2);
33
+ expect(execSync).toHaveBeenCalledTimes(1);
34
+ });
35
+ it("should throw critical error when execSync fails in development or CI", async () => {
36
+ process.env.NODE_ENV = "development";
37
+ vi.mocked(execSync).mockImplementation(() => {
38
+ throw new Error("Command failed");
39
+ });
40
+ const { initializeConfig } = await import("./config.js");
41
+ expect(() => initializeConfig()).toThrow(/CRITICAL: Failed to load dynamic config/);
42
+ });
43
+ it("should log warning and set empty config when execSync fails in production", async () => {
44
+ process.env.NODE_ENV = "production";
45
+ process.env.CI = "false";
46
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
47
+ vi.mocked(execSync).mockImplementation(() => {
48
+ throw new Error("Command failed");
49
+ });
50
+ const { initializeConfig } = await import("./config.js");
51
+ const result = initializeConfig();
52
+ expect(result).toEqual({});
53
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("CRITICAL: Failed to load dynamic config"));
54
+ consoleWarnSpy.mockRestore();
55
+ });
56
+ });
57
+ describe("config object getters", () => {
58
+ it("should resolve githubToken from process.env or gh CLI fallback", async () => {
59
+ process.env.GITHUB_TOKEN = "env-token-123";
60
+ const { config } = await import("./config.js");
61
+ expect(config.githubToken).toBe("env-token-123");
62
+ delete process.env.GITHUB_TOKEN;
63
+ vi.mocked(execSync).mockReturnValue("gh-cli-token\n");
64
+ expect(config.githubToken).toBe("gh-cli-token");
65
+ vi.mocked(execSync).mockImplementation(() => {
66
+ throw new Error("gh not found");
67
+ });
68
+ expect(config.githubToken).toBeUndefined();
69
+ });
70
+ it("should resolve githubOwner and githubRepo from env or cached config", async () => {
71
+ process.env.GITHUB_OWNER = "custom-owner";
72
+ process.env.GITHUB_REPO = "custom-repo";
73
+ const { config } = await import("./config.js");
74
+ expect(config.githubOwner).toBe("custom-owner");
75
+ expect(config.githubRepo).toBe("custom-repo");
76
+ });
77
+ it("should throw when githubOwner / githubRepo cannot be determined", async () => {
78
+ delete process.env.GITHUB_OWNER;
79
+ delete process.env.GITHUB_REPO;
80
+ vi.mocked(execSync).mockImplementation(() => {
81
+ throw new Error("td-cli failed");
82
+ });
83
+ process.env.NODE_ENV = "production";
84
+ process.env.CI = "false";
85
+ vi.spyOn(console, "warn").mockImplementation(() => { });
86
+ const { config, initializeConfig } = await import("./config.js");
87
+ initializeConfig();
88
+ expect(() => config.githubOwner).toThrow("GITHUB_OWNER must be set via environment variable or project_config.json");
89
+ expect(() => config.githubRepo).toThrow("GITHUB_REPO must be set via environment variable or project_config.json");
90
+ });
91
+ it("should resolve repoPath from BOOMTICK_REPO_PATH or findRepoRoot fallback", async () => {
92
+ process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
93
+ const { config } = await import("./config.js");
94
+ expect(config.repoPath).toBe("/custom/repo/path");
95
+ });
96
+ it("should resolve defaultBaseBranch, viteBasePath, and ghPath", async () => {
97
+ process.env.DEFAULT_BASE_BRANCH = "develop";
98
+ process.env.VITE_BASE_PATH = "/base/";
99
+ process.env.GH_PATH = "/usr/bin/gh";
100
+ const { config } = await import("./config.js");
101
+ expect(config.defaultBaseBranch).toBe("develop");
102
+ expect(config.viteBasePath).toBe("/base/");
103
+ expect(config.ghPath).toBe("/usr/bin/gh");
104
+ });
105
+ it("should throw when viteBasePath is missing", async () => {
106
+ delete process.env.VITE_BASE_PATH;
107
+ process.env.NODE_ENV = "production";
108
+ process.env.CI = "false";
109
+ vi.spyOn(console, "warn").mockImplementation(() => { });
110
+ const { config, initializeConfig } = await import("./config.js");
111
+ initializeConfig();
112
+ expect(() => config.viteBasePath).toThrow("VITE_BASE_PATH must be set via environment variable or project_config.json");
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { sanitizeError } from "./error_utils.js";
3
+ describe("error_utils", () => {
4
+ describe("sanitizeError", () => {
5
+ it("should return 'Unknown error' when stderr is empty", () => {
6
+ expect(sanitizeError("")).toBe("Unknown error");
7
+ });
8
+ it("should return only the first line of stderr", () => {
9
+ const stderr = "Error: Something failed\n at StackTrace.line1\n at StackTrace.line2";
10
+ expect(sanitizeError(stderr)).toBe("Error: Something failed");
11
+ });
12
+ it("should truncate lines exceeding 200 characters", () => {
13
+ const longLine = "A".repeat(250);
14
+ const result = sanitizeError(longLine);
15
+ expect(result.length).toBe(200);
16
+ expect(result).toBe("A".repeat(200));
17
+ });
18
+ it("should preserve first line if under 200 characters", () => {
19
+ const line = "Fatal error: unexpected token near line 5";
20
+ expect(sanitizeError(line)).toBe(line);
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createSuccessResult, createErrorResult } from "./result.js";
3
+ describe("result utils", () => {
4
+ describe("createSuccessResult", () => {
5
+ it("should format string data directly without JSON stringifying", () => {
6
+ const result = createSuccessResult("simple string message");
7
+ expect(result).toEqual({
8
+ content: [
9
+ {
10
+ type: "text",
11
+ text: "simple string message",
12
+ },
13
+ ],
14
+ });
15
+ expect(result.isError).toBeUndefined();
16
+ });
17
+ it("should format object data as pretty-printed JSON string", () => {
18
+ const objData = { foo: "bar", count: 42 };
19
+ const result = createSuccessResult(objData);
20
+ expect(result).toEqual({
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: JSON.stringify(objData, null, 2),
25
+ },
26
+ ],
27
+ });
28
+ expect(result.isError).toBeUndefined();
29
+ });
30
+ });
31
+ describe("createErrorResult", () => {
32
+ it("should format error result with message and isError set to true", () => {
33
+ const result = createErrorResult("Something went wrong");
34
+ expect(result).toEqual({
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: "Something went wrong",
39
+ },
40
+ ],
41
+ isError: true,
42
+ });
43
+ });
44
+ });
45
+ });
@@ -1,4 +1,31 @@
1
1
  import { runCommand } from "../lib/shell.js";
2
+ import { execSync } from "child_process";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ export const loadStandaloneAgentContext = (repoRoot = process.cwd()) => {
6
+ const configPath = path.join(repoRoot, "project_config.json");
7
+ let config = {};
8
+ if (fs.existsSync(configPath)) {
9
+ try {
10
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
11
+ }
12
+ catch (e) {
13
+ console.warn(`[MCP Warning] Failed to parse ${configPath}:`, e);
14
+ }
15
+ }
16
+ let gitCommit = "unknown";
17
+ try {
18
+ gitCommit = execSync("git rev-parse HEAD", { cwd: repoRoot, encoding: "utf-8" }).trim();
19
+ }
20
+ catch {
21
+ // Fallback if git is unavailable (e.g. Docker container without git)
22
+ }
23
+ return {
24
+ packageName: config.packageName ?? "boomtick",
25
+ gitCommit,
26
+ config,
27
+ };
28
+ };
2
29
  function tryParseJson(text) {
3
30
  try {
4
31
  return JSON.parse(text);
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { BoomtickMCPServer } from "./server.js";
3
+ import fs from "fs/promises";
4
+ import * as healthModule from "./tools.js";
5
+ import * as routeMapModule from "../tools/repo.get_route_map.js";
6
+ import * as prDiffModule from "../tools/github.get_pr_diff.js";
7
+ import * as ciLogsModule from "../tools/repo.read_ci_logs.js";
8
+ import * as lighthouseModule from "../tools/repo.run_lighthouse.js";
9
+ import * as playwrightModule from "../tools/repo.run_playwright.js";
10
+ vi.mock("fs/promises");
11
+ vi.mock("../config.js", () => ({
12
+ config: {
13
+ repoPath: "/mock/repo",
14
+ },
15
+ }));
16
+ describe("BoomtickMCPServer", () => {
17
+ let mcpServer;
18
+ let serverInstance;
19
+ let requestHandlers;
20
+ beforeEach(() => {
21
+ vi.resetAllMocks();
22
+ requestHandlers = new Map();
23
+ mcpServer = new BoomtickMCPServer();
24
+ serverInstance = mcpServer.server;
25
+ // Retrieve registered request handlers from SDK Server instance
26
+ // SDK Server stores request handlers in server._requestHandlers or server.requestHandlers
27
+ const handlers = serverInstance._requestHandlers || serverInstance.requestHandlers;
28
+ if (handlers) {
29
+ for (const [key, handler] of handlers.entries()) {
30
+ requestHandlers.set(key, handler);
31
+ }
32
+ }
33
+ });
34
+ it("should instantiate server with correct name and capabilities", () => {
35
+ expect(serverInstance).toBeDefined();
36
+ });
37
+ describe("ListPrompts", () => {
38
+ it("should return prompts list", async () => {
39
+ const handler = requestHandlers.get("prompts/list");
40
+ expect(handler).toBeDefined();
41
+ const result = await handler({ method: "prompts/list" });
42
+ expect(result.prompts).toBeDefined();
43
+ expect(Array.isArray(result.prompts)).toBe(true);
44
+ });
45
+ });
46
+ describe("GetPrompt", () => {
47
+ it("should read prompt file and return message", async () => {
48
+ const handler = requestHandlers.get("prompts/get");
49
+ expect(handler).toBeDefined();
50
+ vi.mocked(fs.readFile).mockResolvedValue("You are a helpful agent.");
51
+ const result = await handler({
52
+ method: "prompts/get",
53
+ params: { name: "test-agent" },
54
+ });
55
+ expect(fs.readFile).toHaveBeenCalledWith("/mock/repo/mcp/src/agents/test-agent.prompt.md", "utf-8");
56
+ expect(result).toEqual({
57
+ messages: [
58
+ {
59
+ role: "user",
60
+ content: { type: "text", text: "You are a helpful agent." },
61
+ },
62
+ ],
63
+ });
64
+ });
65
+ it("should prevent path traversal attacks", async () => {
66
+ const handler = requestHandlers.get("prompts/get");
67
+ await expect(handler({
68
+ method: "prompts/get",
69
+ params: { name: "../../../etc/passwd" },
70
+ })).rejects.toThrow("Path traversal detected");
71
+ });
72
+ it("should throw when prompt file does not exist", async () => {
73
+ const handler = requestHandlers.get("prompts/get");
74
+ vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
75
+ await expect(handler({
76
+ method: "prompts/get",
77
+ params: { name: "nonexistent" },
78
+ })).rejects.toThrow("Prompt not found: nonexistent");
79
+ });
80
+ });
81
+ describe("ListResources", () => {
82
+ it("should return resources list", async () => {
83
+ const handler = requestHandlers.get("resources/list");
84
+ expect(handler).toBeDefined();
85
+ const result = await handler({ method: "resources/list" });
86
+ expect(result.resources).toBeDefined();
87
+ expect(Array.isArray(result.resources)).toBe(true);
88
+ });
89
+ });
90
+ describe("ReadResource", () => {
91
+ it("should handle repo://package-json", async () => {
92
+ const handler = requestHandlers.get("resources/read");
93
+ vi.mocked(fs.readFile).mockResolvedValue('{"name": "test"}');
94
+ const result = await handler({
95
+ method: "resources/read",
96
+ params: { uri: "repo://package-json" },
97
+ });
98
+ expect(result).toEqual({
99
+ contents: [{ uri: "repo://package-json", mimeType: "application/json", text: '{"name": "test"}' }],
100
+ });
101
+ });
102
+ it("should handle repo://routes", async () => {
103
+ const handler = requestHandlers.get("resources/read");
104
+ vi.spyOn(routeMapModule, "getRouteMapHandler").mockResolvedValue({ routes: ["/"] });
105
+ const result = await handler({
106
+ method: "resources/read",
107
+ params: { uri: "repo://routes" },
108
+ });
109
+ expect(result.contents[0].uri).toBe("repo://routes");
110
+ expect(JSON.parse(result.contents[0].text)).toEqual({ routes: ["/"] });
111
+ });
112
+ it("should handle repo://design-tokens", async () => {
113
+ const handler = requestHandlers.get("resources/read");
114
+ vi.mocked(fs.readFile).mockResolvedValue("export const tokens = {};");
115
+ const result = await handler({
116
+ method: "resources/read",
117
+ params: { uri: "repo://design-tokens" },
118
+ });
119
+ expect(result).toEqual({
120
+ contents: [{ uri: "repo://design-tokens", mimeType: "text/typescript", text: "export const tokens = {};" }],
121
+ });
122
+ });
123
+ it("should handle repo://diff/:prNumber", async () => {
124
+ const handler = requestHandlers.get("resources/read");
125
+ vi.spyOn(prDiffModule, "getPrDiffHandler").mockResolvedValue({ diffText: "diff --git a b" });
126
+ const result = await handler({
127
+ method: "resources/read",
128
+ params: { uri: "repo://diff/42" },
129
+ });
130
+ expect(result).toEqual({
131
+ contents: [{ uri: "repo://diff/42", mimeType: "text/plain", text: "diff --git a b" }],
132
+ });
133
+ });
134
+ it("should handle repo://ci/:prNumber", async () => {
135
+ const handler = requestHandlers.get("resources/read");
136
+ vi.spyOn(ciLogsModule, "readCiLogsHandler").mockResolvedValue({ status: "success" });
137
+ const result = await handler({
138
+ method: "resources/read",
139
+ params: { uri: "repo://ci/42" },
140
+ });
141
+ expect(result.contents[0].uri).toBe("repo://ci/42");
142
+ expect(JSON.parse(result.contents[0].text)).toEqual({ status: "success" });
143
+ });
144
+ it("should handle repo://lighthouse/:branch", async () => {
145
+ const handler = requestHandlers.get("resources/read");
146
+ vi.spyOn(lighthouseModule, "runLighthouseHandler").mockResolvedValue({ score: 100 });
147
+ const result = await handler({
148
+ method: "resources/read",
149
+ params: { uri: "repo://lighthouse/feature-x" },
150
+ });
151
+ expect(result.contents[0].uri).toBe("repo://lighthouse/feature-x");
152
+ expect(JSON.parse(result.contents[0].text)).toEqual({ score: 100 });
153
+ });
154
+ it("should handle repo://playwright/:branch", async () => {
155
+ const handler = requestHandlers.get("resources/read");
156
+ vi.spyOn(playwrightModule, "runPlaywrightHandler").mockResolvedValue({ passed: true });
157
+ const result = await handler({
158
+ method: "resources/read",
159
+ params: { uri: "repo://playwright/feature-x" },
160
+ });
161
+ expect(result.contents[0].uri).toBe("repo://playwright/feature-x");
162
+ expect(JSON.parse(result.contents[0].text)).toEqual({ passed: true });
163
+ });
164
+ it("should throw when resource is unknown", async () => {
165
+ const handler = requestHandlers.get("resources/read");
166
+ await expect(handler({
167
+ method: "resources/read",
168
+ params: { uri: "repo://unknown" },
169
+ })).rejects.toThrow("Resource not found: repo://unknown");
170
+ });
171
+ });
172
+ describe("ListTools", () => {
173
+ it("should return tools list", async () => {
174
+ const handler = requestHandlers.get("tools/list");
175
+ expect(handler).toBeDefined();
176
+ const result = await handler({ method: "tools/list" });
177
+ expect(result.tools).toBeDefined();
178
+ expect(Array.isArray(result.tools)).toBe(true);
179
+ });
180
+ });
181
+ describe("CallTool", () => {
182
+ it("should execute registered tool handler successfully", async () => {
183
+ const handler = requestHandlers.get("tools/call");
184
+ vi.spyOn(healthModule, "healthHandler").mockResolvedValue({ status: "ok" });
185
+ const result = await handler({
186
+ method: "tools/call",
187
+ params: { name: "boomtick.health", arguments: {} },
188
+ });
189
+ expect(result).toEqual({
190
+ content: [{ type: "text", text: JSON.stringify({ status: "ok" }, null, 2) }],
191
+ });
192
+ });
193
+ it("should return error result for unknown tool", async () => {
194
+ const handler = requestHandlers.get("tools/call");
195
+ const result = await handler({
196
+ method: "tools/call",
197
+ params: { name: "nonexistent.tool", arguments: {} },
198
+ });
199
+ expect(result).toEqual({
200
+ content: [{ type: "text", text: "Tool not found: nonexistent.tool" }],
201
+ isError: true,
202
+ });
203
+ });
204
+ it("should return error result when tool execution throws error", async () => {
205
+ const handler = requestHandlers.get("tools/call");
206
+ vi.spyOn(healthModule, "healthHandler").mockRejectedValue(new Error("Health check failed"));
207
+ const result = await handler({
208
+ method: "tools/call",
209
+ params: { name: "boomtick.health", arguments: {} },
210
+ });
211
+ expect(result).toEqual({
212
+ content: [{ type: "text", text: "Health check failed" }],
213
+ isError: true,
214
+ });
215
+ });
216
+ });
217
+ });