diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 875fc403..88544ab5 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -920,7 +920,7 @@ jobs: || steps.opencode_review_fallback.outputs.review_status == 'success' || steps.opencode_review_second_fallback.outputs.review_status == 'success') env: - GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} + GH_TOKEN: ${{ steps.opencode_app_token.outputs.token != '' && steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN != '' && secrets.OPENCODE_APPROVE_TOKEN || github.token }} OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }} OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} GH_REPOSITORY: ${{ github.repository }} @@ -1030,7 +1030,7 @@ jobs: - name: Approve PR if OpenCode review passed if: always() env: - GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} + GH_TOKEN: ${{ steps.opencode_app_token.outputs.token != '' && steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN != '' && secrets.OPENCODE_APPROVE_TOKEN || github.token }} GH_REPOSITORY: ${{ github.repository }} STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }} diff --git a/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx b/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx index 575684c5..ec9ac270 100644 --- a/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx +++ b/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx @@ -12,21 +12,6 @@ vi.mock("../../i18n", () => ({ })); describe("RoleSwitcher", () => { - - it("renders the title and role options", () => { - const roles = [ - { id: "bass-guitar", name: "Bass Guitar" }, - { id: "lead-vocal", name: "Lead Vocal" } - ]; - - render(); - - expect(screen.getByText("Role-specific View")).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "All Roles" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Bass Guitar" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Lead Vocal" })).toBeInTheDocument(); - }); - it("keeps the all-roles control distinct from a real role whose id is all", () => { const onRoleChange = vi.fn(); diff --git a/apps/desktop/src/lib/job_runner.ts b/apps/desktop/src/lib/job_runner.ts index 7809220e..40682510 100644 --- a/apps/desktop/src/lib/job_runner.ts +++ b/apps/desktop/src/lib/job_runner.ts @@ -58,7 +58,7 @@ function getMockSong(jobId: string): SongRehearsalPack | undefined { * Triggers a mock workspace update to all listeners. */ function triggerMockUpdate() { - const payload = structuredClone(mockWorkspace); + const payload = mockWorkspace; mockListeners.forEach(listener => listener({ payload })); } diff --git a/scripts/checks/verify_supply_chain.py b/scripts/checks/verify_supply_chain.py index 19a6b33a..ca4864c6 100644 --- a/scripts/checks/verify_supply_chain.py +++ b/scripts/checks/verify_supply_chain.py @@ -1216,30 +1216,23 @@ def is_blocking_required_step(block_lines: list[str], block_indent: int) -> bool return [] -def _verify_ci_coverage(missing: list[str]) -> None: +def verify_workflow_coverage() -> list[str]: + """Return workflow trigger and artifact coverage violations.""" + missing: list[str] = [] ci = read_workflow(Path(".github/workflows/ci.yml"), "ci", missing) for token in ["develop", "main", "pull_request", "push", "ci / build-and-test"]: if ci and token not in ci: missing.append(f"ci workflow missing token: {token}") - - -def _verify_sbom_coverage(missing: list[str]) -> None: sbom = read_workflow(Path(".github/workflows/sbom.yml"), "sbom", missing) for token in ["develop", "main", "pull_request", "release:", "tags:"]: if sbom and token not in sbom: missing.append(f"sbom workflow missing trigger token: {token}") - - -def _verify_dependency_review_coverage(missing: list[str]) -> None: review = read_workflow( Path(".github/workflows/dependency-review.yml"), "dependency review", missing ) for token in ["develop", "main", "pull_request"]: if review and token not in review: missing.append(f"dependency review workflow missing trigger token: {token}") - - -def _verify_security_audit_coverage(missing: list[str]) -> None: audit = read_workflow( Path(".github/workflows/security-audit.yml"), "security audit", missing ) @@ -1266,16 +1259,10 @@ def _verify_security_audit_coverage(missing: list[str]) -> None: missing.append( f"security audit workflow missing vulnerability audit token: {token}" ) - - -def _verify_codeql_coverage(missing: list[str]) -> None: codeql = read_workflow(Path(".github/workflows/codeql.yml"), "codeql", missing) for token in ["develop", "main", "pull_request", "push", "codeql"]: if codeql and token not in codeql: missing.append(f"codeql workflow missing token: {token}") - - -def _verify_release_coverage(missing: list[str]) -> None: release = read_workflow(Path(".github/workflows/release.yml"), "release", missing) for token in [ "develop", @@ -1287,18 +1274,12 @@ def _verify_release_coverage(missing: list[str]) -> None: ]: if release and token not in release: missing.append(f"release workflow missing token: {token}") - - -def _verify_secret_scan_coverage(missing: list[str]) -> None: secret_scan = read_workflow( Path(".github/workflows/secret-scan-gate.yml"), "secret scan", missing ) for token in ["develop", "main", "pull_request", "push", "secret-scan-gate"]: if secret_scan and token not in secret_scan: missing.append(f"secret scan workflow missing token: {token}") - - -def _verify_build_coverage(missing: list[str]) -> None: build = read_workflow( Path(".github/workflows/build-baseline.yml"), "build baseline", missing ) @@ -1336,9 +1317,14 @@ def _verify_build_coverage(missing: list[str]) -> None: missing.append( "build workflow should not rely on macos-latest for architecture coverage" ) - - -def _verify_scorecard_coverage(missing: list[str], workflow_paths: list[Path]) -> None: + workflow_paths = sorted(Path(".github/workflows").glob("*.yml")) + sorted( + Path(".github/workflows").glob("*.yaml") + ) + for workflow_path in workflow_paths: + workflow_content = workflow_path.read_text(encoding="utf-8") + missing.extend( + release_artifact_download_decompression_violations(workflow_content) + ) scorecard = read_workflow( Path(".github/workflows/ossf-scorecard.yml"), "ossf scorecard", missing ) @@ -1381,31 +1367,6 @@ def _verify_scorecard_coverage(missing: list[str], workflow_paths: list[Path]) - workflow_content, workflow_path ) ) - - -def verify_workflow_coverage() -> list[str]: - """Return workflow trigger and artifact coverage violations.""" - missing: list[str] = [] - _verify_ci_coverage(missing) - _verify_sbom_coverage(missing) - _verify_dependency_review_coverage(missing) - _verify_security_audit_coverage(missing) - _verify_codeql_coverage(missing) - _verify_release_coverage(missing) - _verify_secret_scan_coverage(missing) - _verify_build_coverage(missing) - - workflow_paths = sorted(Path(".github/workflows").glob("*.yml")) + sorted( - Path(".github/workflows").glob("*.yaml") - ) - for workflow_path in workflow_paths: - workflow_content = workflow_path.read_text(encoding="utf-8") - missing.extend( - release_artifact_download_decompression_violations(workflow_content) - ) - - _verify_scorecard_coverage(missing, workflow_paths) - return missing diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index cdf9f7c1..1c30dd96 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -149,13 +149,11 @@ def is_opencode_context(node: dict[str, Any]) -> bool: """Return whether a status node belongs to OpenCode review.""" if node.get("__typename") == "CheckRun": - workflow = ((node.get("checkSuite") or {}).get("workflowRun") or {}).get( - "workflow" - ) or {} - return ( - node.get("name") == "opencode-review" - or workflow.get("name") == "OpenCode Review" + workflow = ( + ((node.get("checkSuite") or {}).get("workflowRun") or {}).get("workflow") + or {} ) + return node.get("name") == "opencode-review" or workflow.get("name") == "OpenCode Review" return node.get("context") == "opencode-review" @@ -174,12 +172,8 @@ def opencode_in_progress(pr: dict[str, Any]) -> bool: def unresolved_thread_count(pr: dict[str, Any]) -> int: """Count active unresolved review threads.""" - threads = (pr.get("reviewThreads") or {}).get("nodes") or [] - return sum( - 1 - for thread in threads - if not thread.get("isResolved") and not thread.get("isOutdated") - ) + threads = ((pr.get("reviewThreads") or {}).get("nodes") or []) + return sum(1 for thread in threads if not thread.get("isResolved") and not thread.get("isOutdated")) def review_author_login(review: dict[str, Any]) -> str: @@ -193,11 +187,7 @@ def is_opencode_review(review: dict[str, Any]) -> bool: login = review_author_login(review) body = review.get("body") or "" - return ( - login.startswith("opencode-agent") - or "opencode" in login - or "OpenCode Agent" in body - ) + return login.startswith("opencode-agent") or "opencode" in login or "OpenCode Agent" in body def current_head_review_state(pr: dict[str, Any], state: str) -> bool: @@ -234,25 +224,10 @@ def enable_auto_merge(repo: str, pr: dict[str, Any], *, dry_run: bool) -> None: head = pr["headRefOid"] if dry_run: return - run( - [ - "gh", - "pr", - "merge", - number, - "--repo", - repo, - "--auto", - "--merge", - "--match-head-commit", - head, - ] - ) + run(["gh", "pr", "merge", number, "--repo", repo, "--auto", "--merge", "--match-head-commit", head]) -def dispatch_opencode_review( - repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool -) -> None: +def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> None: """Dispatch the OpenCode review workflow for a pull request.""" if dry_run: @@ -281,7 +256,16 @@ def dispatch_opencode_review( ) -def inspect_pr(pr: dict[str, Any], args: argparse.Namespace) -> Decision: +def inspect_pr( + repo: str, + pr: dict[str, Any], + *, + dry_run: bool, + trigger_reviews: bool, + enable_auto_merge_flag: bool, + workflow: str, + base_branch: str, +) -> Decision: """Inspect a pull request and select the scheduler action.""" number = pr["number"] @@ -290,11 +274,9 @@ def inspect_pr(pr: dict[str, Any], args: argparse.Namespace) -> Decision: if pr.get("isDraft"): return Decision(number, "skip", "draft PR") - if base_ref != args.base_branch: - return Decision( - number, "skip", f"base branch is {base_ref}; expected {args.base_branch}" - ) - if head_repo != args.repo: + if base_ref != base_branch: + return Decision(number, "skip", f"base branch is {base_ref}; expected {base_branch}") + if head_repo != repo: return Decision(number, "skip", f"fork or external head repo: {head_repo}") unresolved = unresolved_thread_count(pr) @@ -302,36 +284,22 @@ def inspect_pr(pr: dict[str, Any], args: argparse.Namespace) -> Decision: return Decision(number, "block", f"{unresolved} unresolved review thread(s)") if has_current_head_changes_requested(pr): - return Decision( - number, "block", "current-head OpenCode review requested changes" - ) + return Decision(number, "block", "current-head OpenCode review requested changes") if has_current_head_approval(pr): if pr.get("autoMergeRequest"): - return Decision( - number, "wait", "current head is approved; auto-merge already enabled" - ) - if not args.enable_auto_merge: - return Decision( - number, - "wait", - "current head is approved; auto-merge disabled by scheduler inputs", - ) - enable_auto_merge(args.repo, pr, dry_run=args.dry_run) - return Decision( - number, "auto_merge", "current head is approved; auto-merge enabled" - ) + return Decision(number, "wait", "current head is approved; auto-merge already enabled") + if not enable_auto_merge_flag: + return Decision(number, "wait", "current head is approved; auto-merge disabled by scheduler inputs") + enable_auto_merge(repo, pr, dry_run=dry_run) + return Decision(number, "auto_merge", "current head is approved; auto-merge enabled") if opencode_in_progress(pr): return Decision(number, "wait", "OpenCode review is already in progress") - if args.trigger_reviews: - dispatch_opencode_review( - args.repo, args.review_workflow, pr, dry_run=args.dry_run - ) - return Decision( - number, "review_dispatch", "current head has no OpenCode approval" - ) + if trigger_reviews: + dispatch_opencode_review(repo, workflow, pr, dry_run=dry_run) + return Decision(number, "review_dispatch", "current head has no OpenCode approval") return Decision(number, "block", "current head has no OpenCode approval") @@ -411,36 +379,17 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--project-flow", default=os.environ.get("PROJECT_FLOW", "")) parser.add_argument("--max-prs", type=int, default=100) parser.add_argument("--dry-run", action="store_true") - parser.add_argument( - "--trigger-reviews", action=argparse.BooleanOptionalAction, default=True - ) - parser.add_argument( - "--enable-auto-merge", action=argparse.BooleanOptionalAction, default=True - ) + parser.add_argument("--trigger-reviews", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("--enable-auto-merge", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--review-workflow", default="OpenCode Review") parser.add_argument("--self-test", action="store_true") return parser.parse_args(argv) -def validate_gh_host() -> None: - """Validate GH_HOST environment variable to prevent SSRF.""" - - host = os.environ.get("GH_HOST") - if not host: - return - if not ( - host == "github.com" - or host.endswith(".github.com") - or host.endswith(".githubapp.com") - ): - raise ValueError(f"Invalid GH_HOST: {host}") - - def main(argv: list[str]) -> int: """Run the PR review merge scheduler.""" args = parse_args(argv) - validate_gh_host() if args.self_test: self_test() return 0 @@ -451,7 +400,18 @@ def main(argv: list[str]) -> int: if not args.project_flow: raise SystemExit("--project-flow is required") prs = fetch_open_prs(args.repo, args.max_prs) - decisions = [inspect_pr(pr, args) for pr in prs] + decisions = [ + inspect_pr( + args.repo, + pr, + dry_run=args.dry_run, + trigger_reviews=args.trigger_reviews, + enable_auto_merge_flag=args.enable_auto_merge, + workflow=args.review_workflow, + base_branch=args.base_branch, + ) + for pr in prs + ] print_summary( decisions, dry_run=args.dry_run, diff --git a/services/analysis-engine/tests/test_extractor.py b/services/analysis-engine/tests/test_sections.py similarity index 74% rename from services/analysis-engine/tests/test_extractor.py rename to services/analysis-engine/tests/test_sections.py index 16fa180d..768bef8e 100644 --- a/services/analysis-engine/tests/test_extractor.py +++ b/services/analysis-engine/tests/test_sections.py @@ -1,6 +1,6 @@ """Tests for the section extraction logic and models.""" -from bandscope_analysis.sections.extractor import _normalize_label, extract_sections +from bandscope_analysis.sections.extractor import extract_sections from bandscope_analysis.sections.model import CueAnchorStrategy @@ -81,29 +81,3 @@ def test_extract_sections_unrecognized_label() -> None: assert sections[1]["id"] == "random part-1" assert sections[1]["form_label"] == "random part" assert sections[1]["confidence_level"] == "low" - - -def test_normalize_label() -> None: - """Verify standard label normalization logic.""" - assert _normalize_label("VERSE 1") == "verse" - assert _normalize_label(" chorus 2 ") == "chorus" - assert _normalize_label("pre-chorus") == "pre-chorus" - assert _normalize_label("UNKNOWN") == "unknown" - assert _normalize_label("intro") == "intro" - assert _normalize_label(123) == "123" - - -def test_extract_sections_empty() -> None: - """Verify behavior with an empty arrangement.""" - result = extract_sections([]) - assert result["strategy_used"] == "count" - assert len(result["sections"]) == 0 - - -def test_extract_sections_missing_label() -> None: - """Verify behavior when a section is missing the label key.""" - arrangement = [{"groove": "standard"}] - result = extract_sections(arrangement) - assert len(result["sections"]) == 1 - assert result["sections"][0]["form_label"] == "unknown" - assert result["sections"][0]["id"] == "unknown-1" diff --git a/services/analysis-engine/tests/test_separation.py b/services/analysis-engine/tests/test_separation.py index 81e27cc1..33dceff6 100644 --- a/services/analysis-engine/tests/test_separation.py +++ b/services/analysis-engine/tests/test_separation.py @@ -97,7 +97,7 @@ def test_stem_separator_deduplicates() -> None: def test_stem_separator_invalid_role() -> None: """Test separator handles non-dict roles gracefully.""" separator = StemSeparator() - result = separator.separate( # type: ignore[arg-type] + result = separator.separate( [{"id": "bass", "name": "Bass", "roleType": "instrument"}, "invalid"] ) assert len(result["stems"]) == 1 @@ -135,16 +135,6 @@ def test_stem_separator_keyboard_name_match() -> None: assert result["stems"][0]["category"] == "keys" -def test_stem_separator_missing_id() -> None: - """Test separator handles roles with missing id by generating a fallback id.""" - separator = StemSeparator() - roles = [{"name": "Lead Vocal", "roleType": "vocal"}] - result = separator.separate(roles) - assert len(result["stems"]) == 1 - assert result["stems"][0]["stem_id"] == "stem-role-0" - assert result["stems"][0]["label"] == "Lead Vocal" - - def test_audio_stem_separator_splits_local_audio_into_chunked_stems(tmp_path) -> None: """Ensure local audio is separated into downstream-consumable canonical stems.""" sample_rate = 8_000