boomtick-cli 0.7.0__tar.gz → 0.8.0__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.
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/PKG-INFO +1 -1
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/boomtick_cli.egg-info/PKG-INFO +1 -1
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/boomtick_cli.egg-info/SOURCES.txt +2 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/cli.py +14 -5
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/orchestrator.py +87 -29
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/pr_overlap.py +4 -4
- boomtick_cli-0.8.0/dev_tools/resources/__pycache__/__init__.cpython-312.pyc +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/__pycache__/build-repo-context.cpython-312.pyc +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/ai_service.py +5 -4
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/dependency_graph.py +14 -14
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/utils/__init__.py +24 -2
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/pyproject.toml +1 -1
- boomtick_cli-0.8.0/tests/test_detect_conflicts.py +83 -0
- boomtick_cli-0.8.0/tests/test_issue_view.py +75 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_labels.py +10 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_latency_mitigation.py +3 -1
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_search_issues.py +1 -1
- boomtick_cli-0.7.0/dev_tools/resources/__pycache__/__init__.cpython-312.pyc +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/MANIFEST.in +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/README.md +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/boomtick_cli.egg-info/dependency_links.txt +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/boomtick_cli.egg-info/entry_points.txt +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/boomtick_cli.egg-info/requires.txt +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/boomtick_cli.egg-info/top_level.txt +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/__init__.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/cli-schema.json +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/config.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/constants.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/daemon.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/config.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/evals/run-evals.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/index.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/error_utils.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/git.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/result.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/shell.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/shell.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/td-cli.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/lib/test-utils.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/mcp/definitions.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/mcp/server.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/mcp/tools.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/contract.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/ddgs.search.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/ddgs.search.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/ddgs_search.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.checkout_branch.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.comment_triage_summary.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.create_issue.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.create_issue.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.create_pull_request.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.get_merge_conflict_files.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.get_pr.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.get_pr_diff.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.issue_comment.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.issue_update.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.issue_view.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.open_replacement_pr.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.search_open_prs.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.search_open_prs.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/cancel-session.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/create-session.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/create-session.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/get-messages.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/get-messages.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/get-pr.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/get-pr.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/get-session.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/list-sessions.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/send-message.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/send-message.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/shared.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/trigger-feedback.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/trigger-feedback.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.commit_patch.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.create_branch.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.create_branch.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.create_repair_branch.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.get_changed_files.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.get_command_schema.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.get_package_scripts.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.get_package_scripts.test.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.get_route_map.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.logs.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.read_agent_context.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.read_ci_logs.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.run_lighthouse.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.run_playwright.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.run_tests.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/types.js +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/get_ai_context.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/handlers/__init__.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/handlers/command_handler.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/mcp_server.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/models.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/__init__.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/build-repo-context.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/prompt_constants.json +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/review_template.md +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/ux-audit.config.json +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/resources/visual_guidelines.json +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/review_read_pass.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/schema_gen.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/schema_utils.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/__init__.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/context_builder.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/github.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/jules.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/repair_service.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/vector_store.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/services/vision_service.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/td_cli.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/utils/git.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/ux_report.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/verify_infra.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/verify_versions.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/version_utils.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/workflows/__init__.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/workflows/context.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/workflows/graph.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/workflows/node.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/workflows/nodes.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/workflows/runner.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/setup.cfg +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_build_repo_context.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_config.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_dependency_graph.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_fail_fast.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_github_diff_parser.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_github_no_gh.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_github_parser.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_install_workflows.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_issue_validation_local.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_jules_batch_send.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_opentelemetry_check.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_orchestrator_aggregation.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_orchestrator_heuristics.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_parse_comment.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_plan_issue_audit.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_pr_overlap.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_review_template_validation.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_schema_utils.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_verify_ci_metrics.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_workflow_audit.py +0 -0
- {boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/tests/test_workflow_orchestration.py +0 -0
|
@@ -117,12 +117,14 @@ dev_tools/workflows/runner.py
|
|
|
117
117
|
tests/test_build_repo_context.py
|
|
118
118
|
tests/test_config.py
|
|
119
119
|
tests/test_dependency_graph.py
|
|
120
|
+
tests/test_detect_conflicts.py
|
|
120
121
|
tests/test_fail_fast.py
|
|
121
122
|
tests/test_github_diff_parser.py
|
|
122
123
|
tests/test_github_no_gh.py
|
|
123
124
|
tests/test_github_parser.py
|
|
124
125
|
tests/test_install_workflows.py
|
|
125
126
|
tests/test_issue_validation_local.py
|
|
127
|
+
tests/test_issue_view.py
|
|
126
128
|
tests/test_jules_batch_send.py
|
|
127
129
|
tests/test_labels.py
|
|
128
130
|
tests/test_latency_mitigation.py
|
|
@@ -296,6 +296,7 @@ def search_prs(ctx, state, limit, include_drafts, labels):
|
|
|
296
296
|
@click.option("--labels")
|
|
297
297
|
@click.pass_context
|
|
298
298
|
def search_issues(ctx, state, limit, labels):
|
|
299
|
+
"""Search for GitHub issues (excluding pull requests) in the repository."""
|
|
299
300
|
orch = ctx.obj["ORCHESTRATOR"]
|
|
300
301
|
label_list = [l.strip() for l in labels.split(",")] if labels else None
|
|
301
302
|
|
|
@@ -424,10 +425,15 @@ def create_issue(ctx, title, file, body):
|
|
|
424
425
|
|
|
425
426
|
|
|
426
427
|
@gh.command()
|
|
427
|
-
@click.
|
|
428
|
+
@click.argument("issue_number_arg", required=False, type=int)
|
|
429
|
+
@click.option("--issue-number", "issue_number_opt", type=int, help="The GitHub issue number to view")
|
|
428
430
|
@click.pass_context
|
|
429
|
-
def issue_view(ctx,
|
|
431
|
+
def issue_view(ctx, issue_number_arg, issue_number_opt):
|
|
430
432
|
"""View details of a GitHub issue."""
|
|
433
|
+
issue_number = issue_number_arg if issue_number_arg is not None else issue_number_opt
|
|
434
|
+
if issue_number is None:
|
|
435
|
+
err(ctx, "Provide --issue-number or a positional issue number")
|
|
436
|
+
|
|
431
437
|
orch = ctx.obj["ORCHESTRATOR"]
|
|
432
438
|
issue = orch.get_issue_details(issue_number)
|
|
433
439
|
msg = f"Issue #{issue.get('number')}: {issue.get('title')}\nState: {issue.get('state')}\n\n{issue.get('body')}"
|
|
@@ -686,11 +692,14 @@ def verify_versions(ctx, diff_input):
|
|
|
686
692
|
|
|
687
693
|
|
|
688
694
|
@gh.command()
|
|
689
|
-
@click.option("--pr", type=int)
|
|
695
|
+
@click.option("--pr", type=int, help="PR number to detect conflicts for")
|
|
696
|
+
@click.option("--all", "all_prs", is_flag=True, help="Detect conflicts for all open PRs")
|
|
690
697
|
@click.pass_context
|
|
691
|
-
def detect_conflicts(ctx, pr):
|
|
698
|
+
def detect_conflicts(ctx, pr, all_prs):
|
|
699
|
+
if pr is not None and all_prs:
|
|
700
|
+
err(ctx, "Provide either --pr or --all, not both")
|
|
692
701
|
orch = ctx.obj["ORCHESTRATOR"]
|
|
693
|
-
conflicts = orch.handle_detect_conflicts(pr_num=pr)
|
|
702
|
+
conflicts = orch.handle_detect_conflicts(pr_num=pr, all_prs=all_prs)
|
|
694
703
|
_render_conflicts(ctx, conflicts)
|
|
695
704
|
|
|
696
705
|
|
|
@@ -19,6 +19,8 @@ from dev_tools.ux_report import generate_report
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from dev_tools.services.ai_service import AIClient
|
|
21
21
|
from dev_tools.services.vision_service import VisionService
|
|
22
|
+
from dev_tools.workflows.graph import WorkflowGraph
|
|
23
|
+
from dev_tools.workflows.context import WorkflowContext
|
|
22
24
|
|
|
23
25
|
from dev_tools.config import get_config
|
|
24
26
|
from dev_tools.handlers.command_handler import CommandHandler
|
|
@@ -477,6 +479,9 @@ class Orchestrator:
|
|
|
477
479
|
if res is None:
|
|
478
480
|
raise CLIError("Nothing to update. Provide body, labels, or state.")
|
|
479
481
|
|
|
482
|
+
if isinstance(res, list) or not isinstance(res, dict) or not all(k in res for k in ["number", "title", "html_url", "state"]):
|
|
483
|
+
res = self.github.fetch_issue_details(issueNumber)
|
|
484
|
+
|
|
480
485
|
return {"status": "success", "issue": IssueSummary(**res).model_dump()}
|
|
481
486
|
|
|
482
487
|
def post_comment(self, entity_number: int, body: Optional[str]) -> Dict[str, Any]:
|
|
@@ -593,8 +598,11 @@ class Orchestrator:
|
|
|
593
598
|
"total_findings": total_findings,
|
|
594
599
|
}
|
|
595
600
|
|
|
596
|
-
def handle_detect_conflicts(self, pr_num: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
597
|
-
|
|
601
|
+
def handle_detect_conflicts(self, pr_num: Optional[int] = None, all_prs: bool = False) -> List[Dict[str, Any]]:
|
|
602
|
+
if pr_num is not None and all_prs:
|
|
603
|
+
raise CLIError("Provide either pr_num or all_prs, not both")
|
|
604
|
+
target_pr = pr_num if not all_prs else None
|
|
605
|
+
conflicts = self.detect_conflicts(target_pr)
|
|
598
606
|
formatted = []
|
|
599
607
|
for pr_pair, files in conflicts.items():
|
|
600
608
|
formatted.append({"prs": list(pr_pair), "files": files})
|
|
@@ -732,7 +740,7 @@ class Orchestrator:
|
|
|
732
740
|
review_dir = get_or_create_log_dir("reviews")
|
|
733
741
|
ctx_path = os.path.join(review_dir, f"pr-context-{prNumber}.md")
|
|
734
742
|
rev_path = os.path.join(review_dir, f"pr-review-{prNumber}.md")
|
|
735
|
-
res = {"pr": prNumber, "files": {}}
|
|
743
|
+
res: Dict[str, Any] = {"pr": prNumber, "files": {}}
|
|
736
744
|
if fetch:
|
|
737
745
|
repo = get_github_client().get_repo(get_repo_name())
|
|
738
746
|
pr = repo.get_pull(prNumber)
|
|
@@ -991,7 +999,7 @@ class Orchestrator:
|
|
|
991
999
|
try:
|
|
992
1000
|
import importlib_metadata # type: ignore
|
|
993
1001
|
except ImportError:
|
|
994
|
-
importlib_metadata = None
|
|
1002
|
+
importlib_metadata = None # type: ignore[assignment]
|
|
995
1003
|
|
|
996
1004
|
if importlib_metadata:
|
|
997
1005
|
otel_issues = []
|
|
@@ -1926,18 +1934,42 @@ Follow the "Audit comment template" in `docs/agent/issue-audit-rules.md` to post
|
|
|
1926
1934
|
checks = self.github.fetch_check_runs(head_sha)
|
|
1927
1935
|
failed_checks = [c for c in checks if c.get("conclusion") == "failure"]
|
|
1928
1936
|
|
|
1929
|
-
logs = {}
|
|
1937
|
+
logs: dict[str, str] = {}
|
|
1930
1938
|
# Get check suites to find workflow runs
|
|
1931
1939
|
check_suites = self.github.fetch_check_suites(head_sha)
|
|
1932
1940
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
+
all_runs = []
|
|
1942
|
+
import concurrent.futures
|
|
1943
|
+
if check_suites:
|
|
1944
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(check_suites))) as executor:
|
|
1945
|
+
suite_futures = {executor.submit(self.github.fetch_check_runs_for_suite, suite["id"]): suite for suite in check_suites}
|
|
1946
|
+
for future in concurrent.futures.as_completed(suite_futures):
|
|
1947
|
+
try:
|
|
1948
|
+
runs = future.result()
|
|
1949
|
+
all_runs.extend(runs)
|
|
1950
|
+
except Exception as e:
|
|
1951
|
+
suite = suite_futures[future]
|
|
1952
|
+
log_warn(f"Failed to fetch runs for suite {suite.get('id')}: {e}")
|
|
1953
|
+
|
|
1954
|
+
runs_to_fetch = [run for run in all_runs if include_all or run.get("conclusion") == "failure"]
|
|
1955
|
+
|
|
1956
|
+
def fetch_log(run):
|
|
1957
|
+
run_id = run.get("id")
|
|
1958
|
+
if isinstance(run_id, int):
|
|
1959
|
+
return run["name"], self.github.fetch_check_run_logs(run_id, external_id=run.get("external_id"))
|
|
1960
|
+
return None, None
|
|
1961
|
+
|
|
1962
|
+
if runs_to_fetch:
|
|
1963
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(runs_to_fetch))) as executor:
|
|
1964
|
+
log_futures = {executor.submit(fetch_log, run): run for run in runs_to_fetch}
|
|
1965
|
+
for future in concurrent.futures.as_completed(log_futures):
|
|
1966
|
+
try:
|
|
1967
|
+
name, log_content = future.result()
|
|
1968
|
+
if name and isinstance(name, str) and log_content and isinstance(log_content, str):
|
|
1969
|
+
logs[name] = log_content[:10000]
|
|
1970
|
+
except Exception as e:
|
|
1971
|
+
run = log_futures[future]
|
|
1972
|
+
log_warn(f"Failed to fetch log for run {run.get('id')}: {e}")
|
|
1941
1973
|
|
|
1942
1974
|
return {"checks": checks, "failedChecks": failed_checks, "logs": logs}
|
|
1943
1975
|
|
|
@@ -1957,8 +1989,11 @@ Follow the "Audit comment template" in `docs/agent/issue-audit-rules.md` to post
|
|
|
1957
1989
|
# Limit to latest 20 jobs to avoid extreme memory usage
|
|
1958
1990
|
for run in check_runs[:20]:
|
|
1959
1991
|
# Fetch logs via API to avoid terminal paging/buffering issues
|
|
1960
|
-
|
|
1961
|
-
|
|
1992
|
+
run_id = run.get("id")
|
|
1993
|
+
if not isinstance(run_id, int):
|
|
1994
|
+
continue
|
|
1995
|
+
log_content = self.github.fetch_check_run_logs(run_id, external_id=run.get("external_id"))
|
|
1996
|
+
header = f"--- LOGS FOR JOB: {run['name']} (ID: {run_id}) ---"
|
|
1962
1997
|
all_logs.append(header)
|
|
1963
1998
|
# Truncate each log to 20k chars to balance detail vs memory
|
|
1964
1999
|
all_logs.append(log_content[-20000:])
|
|
@@ -2232,9 +2267,21 @@ Follow the "Audit comment template" in `docs/agent/issue-audit-rules.md` to post
|
|
|
2232
2267
|
aggregate_body = ""
|
|
2233
2268
|
successfully_merged = []
|
|
2234
2269
|
|
|
2270
|
+
import concurrent.futures
|
|
2271
|
+
pr_details_map = {}
|
|
2272
|
+
if prNumbers:
|
|
2273
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(prNumbers))) as executor:
|
|
2274
|
+
future_to_pr = {executor.submit(self.github.fetch_pr_details, pr_num): pr_num for pr_num in prNumbers}
|
|
2275
|
+
for future in concurrent.futures.as_completed(future_to_pr):
|
|
2276
|
+
pr_num = future_to_pr[future]
|
|
2277
|
+
try:
|
|
2278
|
+
pr_details_map[pr_num] = future.result()
|
|
2279
|
+
except Exception as e:
|
|
2280
|
+
raise CLIError(f"Failed to fetch PR #{pr_num}: {e}")
|
|
2281
|
+
|
|
2235
2282
|
for pr_num in prNumbers:
|
|
2236
2283
|
# 2. Sequential Extraction & Deterministic Sequence
|
|
2237
|
-
pr_data =
|
|
2284
|
+
pr_data = pr_details_map.get(pr_num) or {}
|
|
2238
2285
|
head_ref = pr_data.get("head", {}).get("ref")
|
|
2239
2286
|
title = pr_data.get("title")
|
|
2240
2287
|
body = pr_data.get("body") or ""
|
|
@@ -2370,15 +2417,15 @@ Follow the "Audit comment template" in `docs/agent/issue-audit-rules.md` to post
|
|
|
2370
2417
|
|
|
2371
2418
|
if os.path.exists(impact_script):
|
|
2372
2419
|
# Use check=False to swallow errors from impact analysis in the planning phase
|
|
2373
|
-
|
|
2420
|
+
impact_res = run_command(
|
|
2374
2421
|
["npx", "tsx", impact_script],
|
|
2375
2422
|
check=False,
|
|
2376
2423
|
log_on_error=False,
|
|
2377
2424
|
)
|
|
2378
|
-
if isinstance(
|
|
2379
|
-
impact_output = (
|
|
2380
|
-
if
|
|
2381
|
-
impact_output = f"Impact analysis failed (exit {
|
|
2425
|
+
if isinstance(impact_res, subprocess.CompletedProcess):
|
|
2426
|
+
impact_output = (impact_res.stdout or "") + (impact_res.stderr or "")
|
|
2427
|
+
if impact_res.returncode != 0:
|
|
2428
|
+
impact_output = f"Impact analysis failed (exit {impact_res.returncode}):\n{impact_output}"
|
|
2382
2429
|
|
|
2383
2430
|
# 6. Existing Review Data
|
|
2384
2431
|
gemini_review = "None."
|
|
@@ -2809,17 +2856,28 @@ Overlapping functionality identified and resolved.
|
|
|
2809
2856
|
file_to_prs: Dict[str, set[int]] = defaultdict(set)
|
|
2810
2857
|
pr_hunks = {}
|
|
2811
2858
|
|
|
2812
|
-
|
|
2859
|
+
def fetch_pr_info(pr_num: int):
|
|
2813
2860
|
details = self.github.fetch_pr_details(pr_num)
|
|
2814
|
-
pr_details[pr_num] = details
|
|
2815
2861
|
files = self.github.fetch_pr_files(pr_num)
|
|
2816
2862
|
diff = self.github.fetch_pr_diff(pr_num)
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2863
|
+
hunks = self._extract_diff_hunks(diff)
|
|
2864
|
+
return pr_num, details, files, hunks
|
|
2865
|
+
|
|
2866
|
+
import concurrent.futures
|
|
2867
|
+
if prNumbers:
|
|
2868
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(prNumbers))) as executor:
|
|
2869
|
+
future_to_pr = {executor.submit(fetch_pr_info, pr_num): pr_num for pr_num in prNumbers}
|
|
2870
|
+
for future in concurrent.futures.as_completed(future_to_pr):
|
|
2871
|
+
pr_num = future_to_pr[future]
|
|
2872
|
+
try:
|
|
2873
|
+
pr_num, details, files, hunks = future.result()
|
|
2874
|
+
pr_details[pr_num] = details
|
|
2875
|
+
pr_hunks[pr_num] = hunks
|
|
2876
|
+
pr_files = {str(f.get("filename")) for f in files if f.get("filename")}
|
|
2877
|
+
for filename_str in pr_files:
|
|
2878
|
+
file_to_prs[filename_str].add(pr_num)
|
|
2879
|
+
except Exception as e:
|
|
2880
|
+
raise CLIError(f"Failed to fetch info for PR #{pr_num}: {e}")
|
|
2823
2881
|
|
|
2824
2882
|
overlapping_files: Dict[str, List[int]] = {
|
|
2825
2883
|
f: sorted(list(prs)) for f, prs in file_to_prs.items() if len(prs) > 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylint: disable=invalid-name,line-too-long,missing-docstring,no-value-for-parameter
|
|
2
2
|
import sys
|
|
3
3
|
from collections import defaultdict
|
|
4
|
-
from typing import Any, Dict, List
|
|
4
|
+
from typing import Any, Dict, List, Set, Tuple
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
from dev_tools.services.github import GitHubClient
|
|
@@ -57,17 +57,17 @@ def get_pr_overlaps(github: GitHubClient, limit: int) -> List[Dict[str, Any]]:
|
|
|
57
57
|
# We use a simple disjoint-set or just group by overlap.
|
|
58
58
|
# For PR consolidation, we want to see pairs/groups that have overlaps.
|
|
59
59
|
|
|
60
|
-
overlaps: Dict[
|
|
60
|
+
overlaps: Dict[Tuple[int, ...], Set[str]] = defaultdict(set)
|
|
61
61
|
for filename, pr_set in file_to_prs.items():
|
|
62
62
|
if len(pr_set) > 1:
|
|
63
63
|
pr_tuple_key = tuple(sorted(list(pr_set)))
|
|
64
64
|
overlaps[pr_tuple_key].add(filename)
|
|
65
65
|
|
|
66
66
|
clusters: List[Dict[str, Any]] = []
|
|
67
|
-
for pr_tuple_key,
|
|
67
|
+
for pr_tuple_key, overlapping_files in overlaps.items():
|
|
68
68
|
clusters.append({
|
|
69
69
|
"prs": list(pr_tuple_key),
|
|
70
|
-
"files": sorted(list(
|
|
70
|
+
"files": sorted(list(overlapping_files)),
|
|
71
71
|
"metadata": {num: pr_metadata[num] for num in pr_tuple_key}
|
|
72
72
|
})
|
|
73
73
|
|
|
Binary file
|
|
@@ -147,7 +147,7 @@ class AIClient:
|
|
|
147
147
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent"
|
|
148
148
|
headers = {"Content-Type": "application/json", "x-goog-api-key": self.gemini_api_key}
|
|
149
149
|
|
|
150
|
-
payload = {"contents": [{"parts": [{"text": prompt}]}]}
|
|
150
|
+
payload: Dict[str, Any] = {"contents": [{"parts": [{"text": prompt}]}]}
|
|
151
151
|
|
|
152
152
|
if schema:
|
|
153
153
|
payload["generationConfig"] = {
|
|
@@ -366,8 +366,8 @@ class AIClient:
|
|
|
366
366
|
needs_specialist = bool(triage_data.get("needsSpecialistReview", True))
|
|
367
367
|
fast_feedback_text = triage_data.get("fastFeedback", "")
|
|
368
368
|
log_info(f"[AI Triage Success] Triage complete. Needs specialist: {needs_specialist}. Model: {triage_model_used}")
|
|
369
|
-
except Exception as
|
|
370
|
-
log_warn(f"⚠️ Triage Agent failed, falling back to Specialist review: {
|
|
369
|
+
except Exception as e:
|
|
370
|
+
log_warn(f"⚠️ Triage Agent failed, falling back to Specialist review: {e}")
|
|
371
371
|
|
|
372
372
|
if not needs_specialist and fast_feedback_text:
|
|
373
373
|
log_info("[AI Triage] Fast triage review sufficient. Bypassing specialist review.")
|
|
@@ -500,7 +500,8 @@ class AIClient:
|
|
|
500
500
|
# CI guard: never approve if checks are failing
|
|
501
501
|
if has_ci_failures and final.get("recommendation") == "Approved":
|
|
502
502
|
final["recommendation"] = "Not Approved"
|
|
503
|
-
|
|
503
|
+
final_comment = final.get("reviewComment") or ""
|
|
504
|
+
final["reviewComment"] = f"CI checks are failing ({failing_names}). Recommendation downgraded.\n\n{final_comment}"
|
|
504
505
|
|
|
505
506
|
self._write_review_file(pr_num, pr, final, chunks, file_reviews)
|
|
506
507
|
return final
|
|
@@ -3,17 +3,16 @@ from collections import deque
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
|
-
from typing import Dict, List, Set
|
|
6
|
+
from typing import Any, Dict, List, Set
|
|
7
7
|
|
|
8
8
|
from dev_tools.utils import CLIError, log_error, log_warn
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
def validate_and_sanitize_graph_data(data: dict) -> dict:
|
|
12
11
|
"""Validates and sanitizes the dependency graph data structure."""
|
|
13
12
|
if not isinstance(data, dict):
|
|
14
13
|
raise ValueError("Graph data must be a dictionary")
|
|
15
14
|
|
|
16
|
-
sanitized = {}
|
|
15
|
+
sanitized: Dict[str, Any] = {}
|
|
17
16
|
if "modules" in data:
|
|
18
17
|
if not isinstance(data["modules"], list):
|
|
19
18
|
raise ValueError("modules must be a list")
|
|
@@ -21,7 +20,7 @@ def validate_and_sanitize_graph_data(data: dict) -> dict:
|
|
|
21
20
|
for mod in data["modules"]:
|
|
22
21
|
if not isinstance(mod, dict):
|
|
23
22
|
raise ValueError("Module must be a dictionary")
|
|
24
|
-
sanitized_mod = {}
|
|
23
|
+
sanitized_mod: Dict[str, Any] = {}
|
|
25
24
|
if "source" in mod:
|
|
26
25
|
if not isinstance(mod["source"], str):
|
|
27
26
|
raise ValueError("source must be a string")
|
|
@@ -33,7 +32,7 @@ def validate_and_sanitize_graph_data(data: dict) -> dict:
|
|
|
33
32
|
for dep in mod["dependencies"]:
|
|
34
33
|
if not isinstance(dep, dict):
|
|
35
34
|
raise ValueError("Dependency must be a dictionary")
|
|
36
|
-
sanitized_dep = {}
|
|
35
|
+
sanitized_dep: Dict[str, Any] = {}
|
|
37
36
|
if "resolved" in dep:
|
|
38
37
|
if not isinstance(dep["resolved"], str):
|
|
39
38
|
raise ValueError("resolved must be a string")
|
|
@@ -125,20 +124,21 @@ class DependencyGraph:
|
|
|
125
124
|
def _parse_modules(self, modules: List[Dict]):
|
|
126
125
|
for mod in modules:
|
|
127
126
|
source = mod.get("source")
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
if isinstance(source, str):
|
|
128
|
+
deps = mod.get("dependencies", [])
|
|
129
|
+
self.graph[source] = deps
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
for dep in deps:
|
|
132
|
+
resolved = dep.get("resolved")
|
|
133
|
+
if isinstance(resolved, str):
|
|
134
|
+
if resolved not in self.reverse_graph:
|
|
135
|
+
self.reverse_graph[resolved] = []
|
|
136
|
+
self.reverse_graph[resolved].append(source)
|
|
137
137
|
|
|
138
138
|
def get_dependencies(self, filepath: str) -> List[str]:
|
|
139
139
|
"""Returns files that the given file depends on."""
|
|
140
140
|
deps = self.graph.get(filepath, [])
|
|
141
|
-
return [d.get("resolved") for d in deps if d.get("resolved")]
|
|
141
|
+
return [str(d.get("resolved")) for d in deps if isinstance(d.get("resolved"), str)]
|
|
142
142
|
|
|
143
143
|
def get_dependents(self, filepath: str) -> List[str]:
|
|
144
144
|
"""Returns files that depend on the given file."""
|
|
@@ -8,7 +8,7 @@ import sys
|
|
|
8
8
|
import time
|
|
9
9
|
import urllib.parse
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union, Literal, overload
|
|
12
12
|
|
|
13
13
|
import requests # type: ignore[import-untyped]
|
|
14
14
|
|
|
@@ -60,7 +60,7 @@ def run_git_commands(
|
|
|
60
60
|
commands: List[List[str]], cwd: Optional[str] = None
|
|
61
61
|
) -> List[Union[str, subprocess.CompletedProcess[str]]]:
|
|
62
62
|
"""Executes a sequence of git commands."""
|
|
63
|
-
results = []
|
|
63
|
+
results: List[Union[str, subprocess.CompletedProcess[str]]] = []
|
|
64
64
|
for cmd in commands:
|
|
65
65
|
results.append(run_command(cmd, cwd=cwd))
|
|
66
66
|
return results
|
|
@@ -712,6 +712,28 @@ def call_ai_service(
|
|
|
712
712
|
return None
|
|
713
713
|
|
|
714
714
|
|
|
715
|
+
@overload
|
|
716
|
+
def run_command(
|
|
717
|
+
cmd: Union[str, List[str]],
|
|
718
|
+
shell: bool = False,
|
|
719
|
+
check: Literal[True] = True,
|
|
720
|
+
input_str: Optional[str] = None,
|
|
721
|
+
log_on_error: bool = True,
|
|
722
|
+
**kwargs: Any,
|
|
723
|
+
) -> str: ...
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@overload
|
|
727
|
+
def run_command(
|
|
728
|
+
cmd: Union[str, List[str]],
|
|
729
|
+
shell: bool = False,
|
|
730
|
+
check: Literal[False] = False,
|
|
731
|
+
input_str: Optional[str] = None,
|
|
732
|
+
log_on_error: bool = True,
|
|
733
|
+
**kwargs: Any,
|
|
734
|
+
) -> subprocess.CompletedProcess[str]: ...
|
|
735
|
+
|
|
736
|
+
|
|
715
737
|
def run_command(
|
|
716
738
|
cmd: Union[str, List[str]],
|
|
717
739
|
shell: bool = False,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# pylint: disable=missing-docstring,protected-access,redefined-outer-name
|
|
2
|
+
import json
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
from dev_tools.cli import cli
|
|
8
|
+
from dev_tools.orchestrator import Orchestrator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def orchestrator():
|
|
13
|
+
with patch("dev_tools.orchestrator.GitHubClient") as mock_gh:
|
|
14
|
+
orch = Orchestrator()
|
|
15
|
+
orch._github = mock_gh.return_value
|
|
16
|
+
yield orch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_orchestrator_detect_conflicts_specific_pr(orchestrator):
|
|
20
|
+
with patch.object(orchestrator, "detect_conflicts") as mock_detect:
|
|
21
|
+
mock_detect.return_value = {(1, 2): ["shared.py"]}
|
|
22
|
+
res = orchestrator.handle_detect_conflicts(pr_num=1)
|
|
23
|
+
mock_detect.assert_called_once_with(1)
|
|
24
|
+
assert len(res) == 1
|
|
25
|
+
assert res[0]["prs"] == [1, 2]
|
|
26
|
+
assert res[0]["files"] == ["shared.py"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_orchestrator_detect_conflicts_all_prs(orchestrator):
|
|
30
|
+
with patch.object(orchestrator, "detect_conflicts") as mock_detect:
|
|
31
|
+
mock_detect.return_value = {(1, 2): ["shared.py"], (3, 4): ["other.py"]}
|
|
32
|
+
res = orchestrator.handle_detect_conflicts(all_prs=True)
|
|
33
|
+
mock_detect.assert_called_once_with(None)
|
|
34
|
+
assert len(res) == 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_cli_detect_conflicts_all():
|
|
38
|
+
runner = CliRunner()
|
|
39
|
+
|
|
40
|
+
with patch("dev_tools.cli.LazyOrchestrator") as mock_lazy:
|
|
41
|
+
mock_orch = MagicMock()
|
|
42
|
+
mock_lazy.return_value = mock_orch
|
|
43
|
+
|
|
44
|
+
# Mock handle_detect_conflicts response
|
|
45
|
+
mock_orch.handle_detect_conflicts.return_value = [
|
|
46
|
+
{"prs": [1, 2], "files": ["shared.py"]}
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Invoke CLI command in JSON mode (default)
|
|
50
|
+
result = runner.invoke(cli, ["gh", "detect-conflicts", "--all"])
|
|
51
|
+
|
|
52
|
+
assert result.exit_code == 0
|
|
53
|
+
data = json.loads(result.output)
|
|
54
|
+
assert data["status"] == "success"
|
|
55
|
+
assert len(data["conflicts"]) == 1
|
|
56
|
+
assert data["conflicts"][0]["prs"] == [1, 2]
|
|
57
|
+
mock_orch.handle_detect_conflicts.assert_called_once_with(pr_num=None, all_prs=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_cli_detect_conflicts_specific_pr():
|
|
61
|
+
runner = CliRunner()
|
|
62
|
+
|
|
63
|
+
with patch("dev_tools.cli.LazyOrchestrator") as mock_lazy:
|
|
64
|
+
mock_orch = MagicMock()
|
|
65
|
+
mock_lazy.return_value = mock_orch
|
|
66
|
+
|
|
67
|
+
mock_orch.handle_detect_conflicts.return_value = []
|
|
68
|
+
|
|
69
|
+
result = runner.invoke(cli, ["gh", "detect-conflicts", "--pr", "123"])
|
|
70
|
+
|
|
71
|
+
assert result.exit_code == 0
|
|
72
|
+
data = json.loads(result.output)
|
|
73
|
+
assert data["status"] == "success"
|
|
74
|
+
mock_orch.handle_detect_conflicts.assert_called_once_with(pr_num=123, all_prs=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cli_detect_conflicts_mutually_exclusive():
|
|
78
|
+
runner = CliRunner()
|
|
79
|
+
|
|
80
|
+
result = runner.invoke(cli, ["gh", "detect-conflicts", "--pr", "123", "--all"])
|
|
81
|
+
|
|
82
|
+
assert result.exit_code != 0
|
|
83
|
+
assert "Provide either --pr or --all, not both" in result.output
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# pylint: disable=missing-docstring,protected-access,redefined-outer-name
|
|
2
|
+
import json
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
from click.testing import CliRunner
|
|
6
|
+
from dev_tools.cli import cli
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cli_issue_view_positional():
|
|
10
|
+
runner = CliRunner()
|
|
11
|
+
|
|
12
|
+
with patch("dev_tools.cli.LazyOrchestrator") as mock_lazy:
|
|
13
|
+
mock_orch = MagicMock()
|
|
14
|
+
mock_lazy.return_value = mock_orch
|
|
15
|
+
|
|
16
|
+
# Mock get_issue_details response
|
|
17
|
+
mock_orch.get_issue_details.return_value = {
|
|
18
|
+
"number": 123,
|
|
19
|
+
"title": "Bug in issue-view",
|
|
20
|
+
"html_url": "https://github.com/owner/repo/issues/123",
|
|
21
|
+
"state": "open",
|
|
22
|
+
"body": "This is a bug report body.",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Invoke CLI with positional argument
|
|
26
|
+
result = runner.invoke(cli, ["gh", "issue-view", "123"])
|
|
27
|
+
|
|
28
|
+
assert result.exit_code == 0
|
|
29
|
+
data = json.loads(result.output)
|
|
30
|
+
assert data["status"] == "success"
|
|
31
|
+
assert data["issue"]["number"] == 123
|
|
32
|
+
assert data["issue"]["title"] == "Bug in issue-view"
|
|
33
|
+
mock_orch.get_issue_details.assert_called_once_with(123)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_cli_issue_view_flag():
|
|
37
|
+
runner = CliRunner()
|
|
38
|
+
|
|
39
|
+
with patch("dev_tools.cli.LazyOrchestrator") as mock_lazy:
|
|
40
|
+
mock_orch = MagicMock()
|
|
41
|
+
mock_lazy.return_value = mock_orch
|
|
42
|
+
|
|
43
|
+
mock_orch.get_issue_details.return_value = {
|
|
44
|
+
"number": 123,
|
|
45
|
+
"title": "Bug in issue-view",
|
|
46
|
+
"html_url": "https://github.com/owner/repo/issues/123",
|
|
47
|
+
"state": "open",
|
|
48
|
+
"body": "This is a bug report body.",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Invoke CLI with --issue-number flag
|
|
52
|
+
result = runner.invoke(cli, ["gh", "issue-view", "--issue-number", "123"])
|
|
53
|
+
|
|
54
|
+
assert result.exit_code == 0
|
|
55
|
+
data = json.loads(result.output)
|
|
56
|
+
assert data["status"] == "success"
|
|
57
|
+
assert data["issue"]["number"] == 123
|
|
58
|
+
mock_orch.get_issue_details.assert_called_once_with(123)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_cli_issue_view_missing():
|
|
62
|
+
runner = CliRunner()
|
|
63
|
+
|
|
64
|
+
with patch("dev_tools.cli.LazyOrchestrator") as mock_lazy:
|
|
65
|
+
mock_orch = MagicMock()
|
|
66
|
+
mock_lazy.return_value = mock_orch
|
|
67
|
+
|
|
68
|
+
# Invoke CLI with no arguments
|
|
69
|
+
result = runner.invoke(cli, ["gh", "issue-view"])
|
|
70
|
+
|
|
71
|
+
assert result.exit_code != 0
|
|
72
|
+
data = json.loads(result.output)
|
|
73
|
+
assert data["status"] == "error"
|
|
74
|
+
assert "Provide --issue-number or a positional issue number" in data["message"]
|
|
75
|
+
mock_orch.get_issue_details.assert_not_called()
|
|
@@ -83,6 +83,16 @@ class TestLabels(unittest.TestCase):
|
|
|
83
83
|
self.orch.github.add_labels.assert_called_once_with(123, ["l1"])
|
|
84
84
|
self.orch.github.update_issue.assert_called_once_with(123, body="new body", state=None)
|
|
85
85
|
|
|
86
|
+
def test_orchestrator_update_issue_add_labels_returns_list(self):
|
|
87
|
+
# Reset setup mocks and make add_labels return a list of labels (as GitHub API actually does)
|
|
88
|
+
self.orch.github.add_labels.return_value = [{"name": "l1"}]
|
|
89
|
+
# When update_issue detects a list or non-IssueSummary dictionary, it fetches full issue details
|
|
90
|
+
res = self.orch.update_issue(123, add_labels=["l1"])
|
|
91
|
+
self.orch.github.add_labels.assert_called_once_with(123, ["l1"])
|
|
92
|
+
self.orch.github.fetch_issue_details.assert_called_once_with(123)
|
|
93
|
+
self.assertEqual(res["status"], "success")
|
|
94
|
+
self.assertEqual(res["issue"]["number"], 123)
|
|
95
|
+
|
|
86
96
|
|
|
87
97
|
if __name__ == "__main__":
|
|
88
98
|
unittest.main()
|
|
@@ -19,8 +19,10 @@ def test_disk_cache_basic(tmp_path):
|
|
|
19
19
|
assert cache.get(key) == val
|
|
20
20
|
|
|
21
21
|
# Test expiration
|
|
22
|
-
cache.set(key, val, ttl=0
|
|
22
|
+
cache.set(key, val, ttl=5.0)
|
|
23
23
|
assert cache.get(key) == val
|
|
24
|
+
|
|
25
|
+
cache.set(key, val, ttl=0.1)
|
|
24
26
|
time.sleep(0.2)
|
|
25
27
|
assert cache.get(key) is None
|
|
26
28
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# pylint: disable=missing-docstring,protected-access,redefined-outer-name
|
|
2
|
+
import json
|
|
2
3
|
from unittest.mock import patch, MagicMock
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -67,7 +68,6 @@ def test_cli_search_issues():
|
|
|
67
68
|
result = runner.invoke(cli, ["gh", "search-issues", "--state", "open", "--limit", "10", "--labels", "bug,high"])
|
|
68
69
|
|
|
69
70
|
assert result.exit_code == 0
|
|
70
|
-
import json
|
|
71
71
|
data = json.loads(result.output)
|
|
72
72
|
assert data["status"] == "success"
|
|
73
73
|
assert len(data["issues"]) == 1
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.comment_triage_summary.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.create_pull_request.js
RENAMED
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.get_merge_conflict_files.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.open_replacement_pr.js
RENAMED
|
File without changes
|
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/github.search_open_prs.test.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/jules/trigger-feedback.test.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boomtick_cli-0.7.0 → boomtick_cli-0.8.0}/dev_tools/dist/tools/repo.get_package_scripts.test.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|