From 5818b5b6dc88c3e9a16d0902a2a32ea73ea1ea05 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:18:49 +0800 Subject: [PATCH] feat: add monthly optimization planner --- .github/workflows/ai_review.yml | 11 ++ .../monthly_optimization_planner.yml | 72 ++++++++ scripts/build_ai_review_payload.py | 92 ++++++++++ scripts/build_monthly_optimization_plan.py | 173 ++++++++++++++++++ scripts/download_ai_review_artifact.py | 77 ++++++++ scripts/post_monthly_optimization_issue.py | 143 +++++++++++++++ tests/test_build_ai_review_payload.py | 36 ++++ tests/test_build_monthly_optimization_plan.py | 98 ++++++++++ tests/test_download_ai_review_artifact.py | 25 +++ ...ly_optimization_planner_workflow_config.py | 30 +++ tests/test_monthly_publish_workflow_config.py | 2 + tests/test_post_monthly_optimization_issue.py | 34 ++++ 12 files changed, 793 insertions(+) create mode 100644 .github/workflows/monthly_optimization_planner.yml create mode 100644 scripts/build_ai_review_payload.py create mode 100644 scripts/build_monthly_optimization_plan.py create mode 100644 scripts/download_ai_review_artifact.py create mode 100644 scripts/post_monthly_optimization_issue.py create mode 100644 tests/test_build_ai_review_payload.py create mode 100644 tests/test_build_monthly_optimization_plan.py create mode 100644 tests/test_download_ai_review_artifact.py create mode 100644 tests/test_monthly_optimization_planner_workflow_config.py create mode 100644 tests/test_post_monthly_optimization_issue.py diff --git a/.github/workflows/ai_review.yml b/.github/workflows/ai_review.yml index dab16ff..3047703 100644 --- a/.github/workflows/ai_review.yml +++ b/.github/workflows/ai_review.yml @@ -203,6 +203,17 @@ jobs: --output-file data/output/ai_review/final_review.md \ --primary-title "Claude Primary Review" + - name: Build final AI review payload + if: steps.claude_review.outcome == 'success' + run: | + python3 scripts/build_ai_review_payload.py \ + --source-repo "${GITHUB_REPOSITORY}" \ + --review-kind upstream_selector \ + --issue-context-file data/output/ai_review/issue_context.json \ + --secondary-review-file data/output/ai_review/secondary_review.json \ + --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --output-file data/output/ai_review/final_review_payload.json + - name: Upload AI review artifact if: steps.claude_review.outcome == 'success' uses: actions/upload-artifact@v7 diff --git a/.github/workflows/monthly_optimization_planner.yml b/.github/workflows/monthly_optimization_planner.yml new file mode 100644 index 0000000..198e005 --- /dev/null +++ b/.github/workflows/monthly_optimization_planner.yml @@ -0,0 +1,72 @@ +name: Monthly Optimization Planner + +"on": + workflow_dispatch: + inputs: + upstream_run_id: + description: "AI review run id from CryptoLeaderRotation" + required: true + downstream_run_id: + description: "AI review run id from BinancePlatform" + required: true + downstream_repo: + description: "Downstream execution repo" + required: true + default: "QuantStrategyLab/BinancePlatform" + +jobs: + planner: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Download upstream AI review artifact + run: | + python3 scripts/download_ai_review_artifact.py \ + --repo "${GITHUB_REPOSITORY}" \ + --run-id "${{ inputs.upstream_run_id }}" \ + --output-dir data/input/upstream \ + --token-env GITHUB_TOKEN + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download downstream AI review artifact + run: | + python3 scripts/download_ai_review_artifact.py \ + --repo "${{ inputs.downstream_repo }}" \ + --run-id "${{ inputs.downstream_run_id }}" \ + --output-dir data/input/downstream \ + --token-env CROSS_REPO_GITHUB_TOKEN + env: + CROSS_REPO_GITHUB_TOKEN: ${{ secrets.CROSS_REPO_GITHUB_TOKEN }} + + - name: Build monthly optimization plan + run: | + python3 scripts/build_monthly_optimization_plan.py \ + --upstream-review-file data/input/upstream/final_review_payload.json \ + --downstream-review-file data/input/downstream/final_review_payload.json \ + --output-dir data/output/monthly_optimization + + - name: Append optimization summary + run: cat data/output/monthly_optimization/optimization_summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Upload planner artifact + uses: actions/upload-artifact@v7 + with: + name: monthly-optimization-plan-${{ inputs.upstream_run_id }}-${{ inputs.downstream_run_id }} + path: data/output/monthly_optimization/ + + - name: Create monthly optimization issue + run: | + python3 scripts/post_monthly_optimization_issue.py \ + --repo "${GITHUB_REPOSITORY}" \ + --plan-file data/output/monthly_optimization/optimization_plan.json \ + --summary-file data/output/monthly_optimization/optimization_summary.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/build_ai_review_payload.py b/scripts/build_ai_review_payload.py new file mode 100644 index 0000000..d61e032 --- /dev/null +++ b/scripts/build_ai_review_payload.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +SCHEMA_VERSION = "2026-04-02" +REPO_ROLE_BY_KIND = { + "upstream_selector": "upstream_selector_review", + "execution_runtime": "execution_runtime_review", +} + + +def build_review_payload( + *, + source_repo: str, + review_kind: str, + issue_context: dict[str, Any], + secondary_review: dict[str, Any], + run_url: str, +) -> dict[str, Any]: + if review_kind not in REPO_ROLE_BY_KIND: + raise ValueError(f"Unsupported review kind: {review_kind}") + + issue_number = int(issue_context["number"]) + issue_title = str(issue_context["title"]).strip() + issue_url = f"https://github.com/{source_repo}/issues/{issue_number}" + + return { + "schema_version": SCHEMA_VERSION, + "source_repo": source_repo, + "repo_role": REPO_ROLE_BY_KIND[review_kind], + "review_kind": review_kind, + "source_issue": { + "number": issue_number, + "title": issue_title, + "url": issue_url, + }, + "run_url": run_url.strip(), + "primary_reviewer": { + "provider": "anthropic", + "display_name": "Claude Primary Review", + }, + "secondary_reviewer": { + "provider": secondary_review["provider"], + "display_name": secondary_review["provider_display_name"], + "model": secondary_review["model"], + }, + "verdict": secondary_review["verdict"], + "risk_level": secondary_review["risk_level"], + "production_recommendation": secondary_review["production_recommendation"], + "summary": secondary_review["summary"], + "key_findings": list(secondary_review.get("key_findings", [])), + "recommended_actions": list(secondary_review.get("recommended_actions", [])), + "follow_up_checks": list(secondary_review.get("follow_up_checks", [])), + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Build the normalized final AI review payload used by downstream planners.", + ) + parser.add_argument("--source-repo", required=True) + parser.add_argument("--review-kind", required=True, choices=sorted(REPO_ROLE_BY_KIND)) + parser.add_argument("--issue-context-file", required=True, type=Path) + parser.add_argument("--secondary-review-file", required=True, type=Path) + parser.add_argument("--run-url", required=True) + parser.add_argument("--output-file", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + issue_context = json.loads(args.issue_context_file.read_text(encoding="utf-8")) + secondary_review = json.loads(args.secondary_review_file.read_text(encoding="utf-8")) + payload = build_review_payload( + source_repo=args.source_repo, + review_kind=args.review_kind, + issue_context=issue_context, + secondary_review=secondary_review, + run_url=args.run_url, + ) + args.output_file.parent.mkdir(parents=True, exist_ok=True) + args.output_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"final_review_payload={args.output_file}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build_monthly_optimization_plan.py b/scripts/build_monthly_optimization_plan.py new file mode 100644 index 0000000..1056685 --- /dev/null +++ b/scripts/build_monthly_optimization_plan.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import argparse +import json +from collections import defaultdict +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + + +RISK_ORDER = {"low": 0, "medium": 1, "high": 2} +SCHEMA_VERSION = "2026-04-02" +REPO_ORDER = ["CryptoLeaderRotation", "CryptoStrategies", "BinancePlatform"] + + +def highest_risk(actions: list[dict[str, Any]]) -> str: + if not actions: + return "low" + return max(actions, key=lambda item: RISK_ORDER.get(str(item.get("risk_level", "low")), 0)).get("risk_level", "low") + + +def sort_actions(actions: list[dict[str, Any]]) -> list[dict[str, Any]]: + return sorted( + actions, + key=lambda item: ( + RISK_ORDER.get(str(item.get("risk_level", "low")), 0), + str(item.get("title", "")), + ), + reverse=True, + ) + + +def normalize_action(source_review: dict[str, Any], action: dict[str, Any]) -> dict[str, Any]: + return { + "source_repo": source_review["source_repo"], + "source_issue_number": source_review["source_issue"]["number"], + "source_issue_title": source_review["source_issue"]["title"], + "source_issue_url": source_review["source_issue"]["url"], + "source_review_kind": source_review["review_kind"], + "owner_repo": action["owner_repo"], + "title": action["title"], + "risk_level": action["risk_level"], + "auto_pr_safe": bool(action.get("auto_pr_safe")), + "experiment_only": bool(action.get("experiment_only")), + "summary": action["summary"], + } + + +def build_plan(upstream_review: dict[str, Any], downstream_review: dict[str, Any]) -> dict[str, Any]: + source_reviews = [upstream_review, downstream_review] + normalized_actions = [ + normalize_action(review, action) + for review in source_reviews + for action in review.get("recommended_actions", []) + ] + repo_groups: dict[str, list[dict[str, Any]]] = defaultdict(list) + for action in normalized_actions: + repo_groups[action["owner_repo"]].append(action) + + repo_action_summary = { + repo: { + "count": len(sort_actions(repo_groups.get(repo, []))), + "highest_risk_level": highest_risk(repo_groups.get(repo, [])), + "actions": sort_actions(repo_groups.get(repo, [])), + } + for repo in REPO_ORDER + if repo_groups.get(repo) + } + + safe_auto_pr_candidates = [action for action in normalized_actions if action["auto_pr_safe"] and action["risk_level"] == "low"] + experiment_candidates = [action for action in normalized_actions if action["experiment_only"]] + human_review_required = [ + action for action in normalized_actions if (not action["auto_pr_safe"]) or action["risk_level"] != "low" + ] + operator_focus = [ + f"{review['source_repo']}: {review['summary']}" + for review in source_reviews + ] + + highest_review_risk = highest_risk([ + {"risk_level": upstream_review["risk_level"]}, + {"risk_level": downstream_review["risk_level"]}, + ]) + + return { + "schema_version": SCHEMA_VERSION, + "generated_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + "source_reviews": source_reviews, + "highest_review_risk": highest_review_risk, + "repo_action_summary": repo_action_summary, + "safe_auto_pr_candidates": sort_actions(safe_auto_pr_candidates), + "experiment_candidates": sort_actions(experiment_candidates), + "human_review_required": sort_actions(human_review_required), + "operator_focus": operator_focus, + } + + +def render_summary_markdown(plan: dict[str, Any]) -> str: + lines = [ + "# Monthly Optimization Planner", + "", + f"- Highest review risk: `{plan['highest_review_risk']}`", + f"- Safe auto-PR candidates: `{len(plan['safe_auto_pr_candidates'])}`", + f"- Experiment candidates: `{len(plan['experiment_candidates'])}`", + f"- Human review required: `{len(plan['human_review_required'])}`", + "", + "## Source Reviews", + ] + for review in plan["source_reviews"]: + lines.extend( + [ + f"- **{review['source_repo']}** `{review['risk_level']}` / `{review['production_recommendation']}`: {review['summary']}", + f" - Source issue: [{review['source_issue']['title']}]({review['source_issue']['url']})", + f" - Run: {review['run_url']}", + ] + ) + + lines.extend(["", "## Recommended Work by Repo"]) + for repo in REPO_ORDER: + repo_summary = plan["repo_action_summary"].get(repo) + if not repo_summary: + continue + lines.extend(["", f"### {repo}"]) + for action in repo_summary["actions"]: + flags: list[str] = [] + if action["auto_pr_safe"]: + flags.append("auto-pr-safe") + if action["experiment_only"]: + flags.append("experiment-only") + flag_suffix = f" [{', '.join(flags)}]" if flags else "" + lines.append( + f"- `{action['risk_level']}` {action['title']}{flag_suffix}: {action['summary']} " + f"(from {action['source_repo']} #{action['source_issue_number']})" + ) + + if plan["operator_focus"]: + lines.extend(["", "## Operator Focus"]) + lines.extend(f"- {item}" for item in plan["operator_focus"]) + + return "\n".join(lines).strip() + "\n" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Build the monthly optimization plan by combining upstream and downstream AI review payloads.", + ) + parser.add_argument("--upstream-review-file", required=True, type=Path) + parser.add_argument("--downstream-review-file", required=True, type=Path) + parser.add_argument("--output-dir", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + upstream_review = json.loads(args.upstream_review_file.read_text(encoding="utf-8")) + downstream_review = json.loads(args.downstream_review_file.read_text(encoding="utf-8")) + plan = build_plan(upstream_review, downstream_review) + args.output_dir.mkdir(parents=True, exist_ok=True) + (args.output_dir / "optimization_plan.json").write_text( + json.dumps(plan, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + (args.output_dir / "optimization_summary.md").write_text( + render_summary_markdown(plan), + encoding="utf-8", + ) + print(f"optimization_plan={args.output_dir / 'optimization_plan.json'}") + print(f"optimization_summary={args.output_dir / 'optimization_summary.md'}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/download_ai_review_artifact.py b/scripts/download_ai_review_artifact.py new file mode 100644 index 0000000..ea30562 --- /dev/null +++ b/scripts/download_ai_review_artifact.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import argparse +import io +import json +import os +import urllib.request +import zipfile +from pathlib import Path +from typing import Any + + +DEFAULT_API_URL = "https://api.github.com" +ARTIFACT_PREFIX = "ai-monthly-review-" + + +def github_request(url: str, token: str, api_url: str = DEFAULT_API_URL) -> Any: + request = urllib.request.Request( + url if url.startswith("http") else f"{api_url.rstrip('/')}/{url.lstrip('/')}", + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "monthly-optimization-planner", + }, + ) + with urllib.request.urlopen(request) as response: + content_type = response.headers.get("Content-Type", "") + body = response.read() + if "application/json" in content_type: + charset = response.headers.get_content_charset("utf-8") + return json.loads(body.decode(charset)) + return body + + +def select_ai_review_artifact(artifacts_payload: dict[str, Any]) -> dict[str, Any]: + artifacts = artifacts_payload.get("artifacts") or [] + candidates = [artifact for artifact in artifacts if str(artifact.get("name", "")).startswith(ARTIFACT_PREFIX)] + if not candidates: + raise ValueError("No ai-monthly-review artifact found for the workflow run") + candidates.sort(key=lambda artifact: int(artifact.get("id", 0)), reverse=True) + return candidates[0] + + +def download_and_extract_artifact(*, repo: str, run_id: int, token: str, output_dir: Path) -> Path: + artifacts_payload = github_request(f"repos/{repo}/actions/runs/{run_id}/artifacts", token) + artifact = select_ai_review_artifact(artifacts_payload) + archive = github_request(artifact["archive_download_url"], token) + output_dir.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(io.BytesIO(archive)) as handle: + handle.extractall(output_dir) + return output_dir + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Download and extract the ai-monthly-review artifact from a workflow run.", + ) + parser.add_argument("--repo", required=True) + parser.add_argument("--run-id", required=True, type=int) + parser.add_argument("--output-dir", required=True, type=Path) + parser.add_argument("--token-env", default="GITHUB_TOKEN") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + token = os.environ.get(args.token_env) + if not token: + raise SystemExit(f"{args.token_env} is required") + download_and_extract_artifact(repo=args.repo, run_id=args.run_id, token=token, output_dir=args.output_dir) + print(f"artifact_dir={args.output_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/post_monthly_optimization_issue.py b/scripts/post_monthly_optimization_issue.py new file mode 100644 index 0000000..ebac833 --- /dev/null +++ b/scripts/post_monthly_optimization_issue.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Any + + +COMMENT_MARKER_PREFIX = "" + + +def build_issue_title(plan: dict[str, Any]) -> str: + labels = [review["source_issue"]["title"].split(": ", 1)[-1] for review in plan.get("source_reviews", [])] + return f"Monthly Optimization Plan: {' / '.join(labels)}" + + +def build_issue_body(plan: dict[str, Any], summary_markdown: str) -> str: + marker = build_marker(plan) + return f"{marker}\n{summary_markdown.strip()}" + + +def github_request(method: str, url: str, token: str, payload: dict[str, Any] | None = None) -> Any: + data = None + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "monthly-optimization-planner", + } + if payload is not None: + data = json.dumps(payload).encode("utf-8") + headers["Content-Type"] = "application/json" + request = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(request) as response: + charset = response.headers.get_content_charset("utf-8") + raw = response.read().decode(charset) + return json.loads(raw) if raw else None + + +def ensure_label(api_url: str, repo: str, token: str) -> None: + label_path = urllib.parse.quote(LABEL_NAME, safe="") + label_url = f"{api_url}/repos/{repo}/labels/{label_path}" + try: + github_request("GET", label_url, token) + except urllib.error.HTTPError as exc: + if exc.code != 404: + raise + github_request( + "POST", + f"{api_url}/repos/{repo}/labels", + token, + { + "name": LABEL_NAME, + "color": LABEL_COLOR, + "description": LABEL_DESCRIPTION, + }, + ) + + +def upsert_issue(*, api_url: str, repo: str, token: str, title: str, body: str) -> int: + issues = github_request( + "GET", + f"{api_url}/repos/{repo}/issues?state=open&labels={urllib.parse.quote(LABEL_NAME)}&per_page=100", + token, + ) + existing = next((issue for issue in issues if build_marker_from_body(issue.get("body", "")) == build_marker_from_body(body)), None) + payload = {"title": title, "body": body, "labels": [LABEL_NAME]} + if existing: + github_request("PATCH", f"{api_url}/repos/{repo}/issues/{existing['number']}", token, payload) + print(f"Updated optimization issue #{existing['number']}") + return int(existing["number"]) + created = github_request("POST", f"{api_url}/repos/{repo}/issues", token, payload) + print(f"Created optimization issue #{created['number']}") + return int(created["number"]) + + +def build_marker_from_body(body: str) -> str: + for line in body.splitlines(): + if line.startswith(COMMENT_MARKER_PREFIX): + return line.strip() + return "" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create or update the monthly optimization issue from the planner outputs.", + ) + parser.add_argument("--repo", required=True) + parser.add_argument("--plan-file", required=True, type=Path) + parser.add_argument("--summary-file", required=True, type=Path) + parser.add_argument("--api-url", default=DEFAULT_API_URL) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + token = os.environ.get("GITHUB_TOKEN") + if not token: + print("GITHUB_TOKEN is required", file=sys.stderr) + return 1 + + plan = json.loads(args.plan_file.read_text(encoding="utf-8")) + summary_markdown = args.summary_file.read_text(encoding="utf-8") + title = build_issue_title(plan) + body = build_issue_body(plan, summary_markdown) + + try: + ensure_label(args.api_url.rstrip("/"), args.repo, token) + issue_number = upsert_issue( + api_url=args.api_url.rstrip("/"), + repo=args.repo, + token=token, + title=title, + body=body, + ) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + print(f"GitHub API request failed: {exc.code} {detail}", file=sys.stderr) + return 1 + + print(f"issue_number={issue_number}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_build_ai_review_payload.py b/tests/test_build_ai_review_payload.py new file mode 100644 index 0000000..196970c --- /dev/null +++ b/tests/test_build_ai_review_payload.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import unittest + +from scripts.build_ai_review_payload import SCHEMA_VERSION, build_review_payload + + +class BuildAiReviewPayloadTests(unittest.TestCase): + def test_build_review_payload_carries_standardized_root_fields(self) -> None: + payload = build_review_payload( + source_repo="QuantStrategyLab/CryptoLeaderRotation", + review_kind="upstream_selector", + issue_context={"number": 11, "title": "Monthly Report Review: 2026-04-01"}, + secondary_review={ + "provider": "openai", + "provider_display_name": "GPT Secondary Review", + "model": "gpt-5.4-mini", + "verdict": "agree", + "risk_level": "low", + "production_recommendation": "keep_production_as_is", + "summary": "Looks consistent.", + "key_findings": ["No blocking issue found."], + "recommended_actions": [], + "follow_up_checks": [], + }, + run_url="https://github.com/example/repo/actions/runs/1", + ) + + self.assertEqual(payload["schema_version"], SCHEMA_VERSION) + self.assertEqual(payload["repo_role"], "upstream_selector_review") + self.assertEqual(payload["source_issue"]["url"], "https://github.com/QuantStrategyLab/CryptoLeaderRotation/issues/11") + self.assertEqual(payload["secondary_reviewer"]["model"], "gpt-5.4-mini") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_build_monthly_optimization_plan.py b/tests/test_build_monthly_optimization_plan.py new file mode 100644 index 0000000..5148cf0 --- /dev/null +++ b/tests/test_build_monthly_optimization_plan.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import unittest + +from scripts.build_monthly_optimization_plan import build_plan, render_summary_markdown + + +class BuildMonthlyOptimizationPlanTests(unittest.TestCase): + def test_build_plan_groups_actions_by_owner_repo(self) -> None: + upstream_review = { + "source_repo": "QuantStrategyLab/CryptoLeaderRotation", + "review_kind": "upstream_selector", + "source_issue": {"number": 11, "title": "Monthly Report Review: 2026-04-01", "url": "https://github.com/a/b/issues/11"}, + "risk_level": "medium", + "production_recommendation": "research_only", + "summary": "Need more challenger evidence.", + "recommended_actions": [ + { + "owner_repo": "CryptoLeaderRotation", + "title": "Add challenger breadth check", + "risk_level": "low", + "auto_pr_safe": True, + "experiment_only": True, + "summary": "Improve evidence coverage.", + } + ], + } + downstream_review = { + "source_repo": "QuantStrategyLab/BinancePlatform", + "review_kind": "execution_runtime", + "source_issue": {"number": 9, "title": "Monthly Execution Review: 2026-03", "url": "https://github.com/a/b/issues/9"}, + "risk_level": "low", + "production_recommendation": "keep_production_as_is", + "summary": "Execution looked healthy.", + "recommended_actions": [ + { + "owner_repo": "BinancePlatform", + "title": "Keep gating summary visible", + "risk_level": "low", + "auto_pr_safe": True, + "experiment_only": False, + "summary": "No change to production behavior.", + } + ], + } + + plan = build_plan(upstream_review, downstream_review) + + self.assertEqual(plan["highest_review_risk"], "medium") + self.assertIn("CryptoLeaderRotation", plan["repo_action_summary"]) + self.assertIn("BinancePlatform", plan["repo_action_summary"]) + self.assertEqual(len(plan["safe_auto_pr_candidates"]), 2) + self.assertEqual(len(plan["experiment_candidates"]), 1) + + def test_render_summary_markdown_mentions_source_reviews_and_repos(self) -> None: + plan = { + "highest_review_risk": "medium", + "safe_auto_pr_candidates": [{}, {}], + "experiment_candidates": [{}], + "human_review_required": [{}], + "source_reviews": [ + { + "source_repo": "QuantStrategyLab/CryptoLeaderRotation", + "risk_level": "medium", + "production_recommendation": "research_only", + "summary": "Need more evidence.", + "source_issue": {"title": "Monthly Report Review: 2026-04-01", "url": "https://github.com/a/b/issues/11"}, + "run_url": "https://github.com/a/b/actions/runs/1", + } + ], + "repo_action_summary": { + "CryptoLeaderRotation": { + "actions": [ + { + "risk_level": "low", + "title": "Add challenger breadth check", + "summary": "Improve evidence coverage.", + "source_repo": "QuantStrategyLab/CryptoLeaderRotation", + "source_issue_number": 11, + "auto_pr_safe": True, + "experiment_only": True, + } + ] + } + }, + "operator_focus": ["QuantStrategyLab/CryptoLeaderRotation: Need more evidence."], + } + + markdown = render_summary_markdown(plan) + + self.assertIn("# Monthly Optimization Planner", markdown) + self.assertIn("QuantStrategyLab/CryptoLeaderRotation", markdown) + self.assertIn("Add challenger breadth check", markdown) + self.assertIn("Operator Focus", markdown) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_download_ai_review_artifact.py b/tests/test_download_ai_review_artifact.py new file mode 100644 index 0000000..b2e055d --- /dev/null +++ b/tests/test_download_ai_review_artifact.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import unittest + +from scripts.download_ai_review_artifact import select_ai_review_artifact + + +class DownloadAiReviewArtifactTests(unittest.TestCase): + def test_select_ai_review_artifact_picks_latest_matching_artifact(self) -> None: + artifact = select_ai_review_artifact( + { + "artifacts": [ + {"id": 10, "name": "other-artifact"}, + {"id": 20, "name": "ai-monthly-review-9"}, + {"id": 30, "name": "ai-monthly-review-11"}, + ] + } + ) + + self.assertEqual(artifact["id"], 30) + self.assertEqual(artifact["name"], "ai-monthly-review-11") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_monthly_optimization_planner_workflow_config.py b/tests/test_monthly_optimization_planner_workflow_config.py new file mode 100644 index 0000000..69518c2 --- /dev/null +++ b/tests/test_monthly_optimization_planner_workflow_config.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = PROJECT_ROOT / ".github" / "workflows" / "monthly_optimization_planner.yml" + + +class MonthlyOptimizationPlannerWorkflowConfigTests(unittest.TestCase): + def test_planner_workflow_downloads_artifacts_and_posts_issue(self) -> None: + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + + self.assertIn("workflow_dispatch:", workflow) + self.assertIn("upstream_run_id:", workflow) + self.assertIn("downstream_run_id:", workflow) + self.assertIn("downstream_repo:", workflow) + self.assertIn("actions: read", workflow) + self.assertIn("CROSS_REPO_GITHUB_TOKEN", workflow) + self.assertIn("download_ai_review_artifact.py", workflow) + self.assertIn("build_monthly_optimization_plan.py", workflow) + self.assertIn("post_monthly_optimization_issue.py", workflow) + self.assertIn("final_review_payload.json", workflow) + self.assertIn("actions/upload-artifact@v7", workflow) + self.assertIn("monthly-optimization-plan-", workflow) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_monthly_publish_workflow_config.py b/tests/test_monthly_publish_workflow_config.py index b3bbaf5..3e982c0 100644 --- a/tests/test_monthly_publish_workflow_config.py +++ b/tests/test_monthly_publish_workflow_config.py @@ -54,10 +54,12 @@ def test_ai_review_workflow_supports_dispatch_and_comment_posting(self) -> None: self.assertIn("post_monthly_ai_review_comment.py", workflow) self.assertIn("render_monthly_ai_review.py", workflow) self.assertIn("run_openai_secondary_review.py", workflow) + self.assertIn("build_ai_review_payload.py", workflow) self.assertIn("steps.claude_review.outputs.execution_file", workflow) self.assertIn("OPENAI_API_KEY", workflow) self.assertIn("OPENAI_SECONDARY_MODEL", workflow) self.assertIn("secondary_review.json", workflow) + self.assertIn("final_review_payload.json", workflow) self.assertIn("actions/upload-artifact@v7", workflow) self.assertNotIn("allowed_tools:", workflow) self.assertNotIn("custom_instructions:", workflow) diff --git a/tests/test_post_monthly_optimization_issue.py b/tests/test_post_monthly_optimization_issue.py new file mode 100644 index 0000000..ebafa2e --- /dev/null +++ b/tests/test_post_monthly_optimization_issue.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import unittest + +from scripts.post_monthly_optimization_issue import build_issue_body, build_issue_title, build_marker + + +class PostMonthlyOptimizationIssueTests(unittest.TestCase): + def test_build_title_and_marker_use_source_reviews(self) -> None: + plan = { + "source_reviews": [ + {"source_repo": "QuantStrategyLab/CryptoLeaderRotation", "source_issue": {"number": 11, "title": "Monthly Report Review: 2026-04-01"}}, + {"source_repo": "QuantStrategyLab/BinancePlatform", "source_issue": {"number": 9, "title": "Monthly Execution Review: 2026-03"}}, + ] + } + + self.assertEqual(build_marker(plan), "") + self.assertEqual(build_issue_title(plan), "Monthly Optimization Plan: 2026-04-01 / 2026-03") + + def test_build_issue_body_prefixes_marker(self) -> None: + plan = { + "source_reviews": [ + {"source_repo": "QuantStrategyLab/CryptoLeaderRotation", "source_issue": {"number": 11, "title": "Monthly Report Review: 2026-04-01"}} + ] + } + + body = build_issue_body(plan, "# Monthly Optimization Planner\n") + + self.assertTrue(body.startswith("