diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 6adfdfb525..1d2d5e3d75 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -55,7 +55,13 @@ jobs: // ---- area: replaces .github/labeler.yml (path -> area) ---- const AREA_RULES = [ ['area:frontend', [/^frontend\//]], - ['area:backend', [/^backend\/app\//, /^backend\/packages\/harness\/deerflow\/(runtime|persistence|config|tools|guardrails|tracing|models|utils|uploads)\//]], + // Cover both backend packages. Keep the harness exclusions aligned + // with the dedicated tree rules below; the offline replay checks this. + ['area:backend', [ + /^backend\/app\//, + /^backend\/packages\/harness\/(?!deerflow\/(?:agents|subagents|reflection|sandbox|skills|mcp)\/)/, + /^backend\/packages\/extension-api\//, + ]], ['area:agents', [/^backend\/packages\/harness\/deerflow\/(agents|subagents|reflection)\//, /(^|\/)langgraph\.json$/, /^backend\/.*\/prompts\//]], ['area:sandbox', [/^docker\//, /^backend\/packages\/harness\/deerflow\/sandbox\//, /(^|\/)Dockerfile$/]], ['area:skills', [/^skills\//, /^backend\/packages\/harness\/deerflow\/skills\//, /^frontend\/src\/core\/skills\//]], diff --git a/backend/tests/test_label_declarations.py b/backend/tests/test_label_declarations.py index 1261df1295..b1fa06b74c 100644 --- a/backend/tests/test_label_declarations.py +++ b/backend/tests/test_label_declarations.py @@ -1,8 +1,11 @@ -"""Offline checks of the triage workflow's literal label declarations.""" +"""Offline checks of the triage workflow's label declarations and area rules.""" from __future__ import annotations import importlib.util +import json +import shutil +import subprocess from pathlib import Path import pytest @@ -193,3 +196,74 @@ def test_every_real_applied_label_is_enforced(files, capsys, label): triage.write_text((REPO_ROOT / ".github/workflows/triage.yml").read_text(encoding="utf-8"), encoding="utf-8") assert checker.main() == 1 assert f" - {label}\n" in capsys.readouterr().err + + +@pytest.mark.skipif(shutil.which("node") is None, reason="Workflow replay requires Node.js") +def test_pr_area_labels_cover_backend_packages(): + harness = "backend/packages/harness" + deerflow = REPO_ROOT / harness / "deerflow" + specific_areas = { + "agents": "area:agents", + "subagents": "area:agents", + "reflection": "area:agents", + "sandbox": "area:sandbox", + "skills": "area:skills", + "mcp": "area:mcp", + } + cases = [ + ([f"{harness}/pyproject.toml", "backend/uv.lock"], {"area:backend", "area:deps"}), + (["backend/packages/extension-api/deerflow_extension_api/__init__.py"], {"area:backend"}), + ([f"{harness}/deerflow/__init__.py"], {"area:backend"}), + ([f"{harness}/deerflow/agents_extra/example.py"], {"area:backend"}), + ( + ["frontend/src/example.tsx", f"{harness}/deerflow/agents/example.py", f"{harness}/deerflow/community/README.md"], + {"area:frontend", "area:agents", "area:backend", "area:docs"}, + ), + ] + for directory in sorted(deerflow.iterdir()): + if directory.is_dir() and directory.name != "__pycache__": + path = directory.relative_to(REPO_ROOT).as_posix() + area = specific_areas.get(directory.name, "area:backend") + cases.append(([f"{path}/example.py"], {area})) + + scripts = checker.workflow_scripts(checker.TRIAGE_WORKFLOW.read_text(encoding="utf-8")) + (script,) = (source for source in scripts if "const AREA_RULES =" in source) + result = subprocess.run( + [ + "node", + "-e", + """ +const { readFileSync } = require('node:fs'); +const { script, changes } = JSON.parse(readFileSync(0, 'utf8')); +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +const applyLabels = new AsyncFunction('github', 'context', 'core', script); + +async function replay(paths) { + const labels = []; + const github = { + paginate: (list) => list(), + rest: { + pulls: { listFiles: () => paths.map(filename => ({ filename })) }, + issues: { + listLabelsOnIssue: () => [], + addLabels: (request) => { labels.push(...request.labels); }, + }, + }, + }; + const context = { repo: { owner: 'test', repo: 'test' }, payload: { pull_request: { number: 1 }, action: 'synchronize' } }; + await applyLabels(github, context, { info() {} }); + return labels.filter(label => label.startsWith('area:')); +} + +Promise.all(changes.map(replay)).then(labels => console.log(JSON.stringify(labels))); +""", + ], + input=json.dumps({"script": script, "changes": [paths for paths, _ in cases]}), + capture_output=True, + encoding="utf-8", + check=False, + timeout=10, + ) + assert result.returncode == 0, result.stderr + for (paths, expected), labels in zip(cases, json.loads(result.stdout), strict=True): + assert sorted(labels) == sorted(expected), paths