Skip to content

Commit 4342da9

Browse files
committed
feat(examples): add end-to-end review pipeline with governance, dedup and metrics
1 parent 1e855bb commit 4342da9

3 files changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Input resolution: --diff-file, --repo-path or a named fixture."""
7+
import subprocess
8+
from pathlib import Path
9+
10+
FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures"
11+
12+
13+
def load_diff(diff_file=None, repo_path=None, fixture=None):
14+
"""Return (diff_text, input_type, input_ref). Exactly one source required."""
15+
sources = [s for s in (diff_file, repo_path, fixture) if s]
16+
if len(sources) != 1:
17+
raise ValueError("provide exactly one of --diff-file, --repo-path, --fixture")
18+
if diff_file:
19+
return Path(diff_file).read_text(encoding="utf-8"), "diff_file", str(diff_file)
20+
if repo_path:
21+
out = subprocess.run(["git", "-C", str(repo_path), "diff", "HEAD"],
22+
capture_output=True, text=True, check=True)
23+
return out.stdout, "repo_path", str(repo_path)
24+
name = fixture if str(fixture).endswith(".diff") else f"{fixture}.diff"
25+
return (FIXTURES_DIR / name).read_text(encoding="utf-8"), "fixture", name
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Deterministic review pipeline: parse -> govern -> sandbox -> merge -> persist -> report."""
7+
import json
8+
import time
9+
from dataclasses import dataclass, field
10+
from pathlib import Path
11+
12+
from trpc_agent_sdk.skills import create_default_skill_repository
13+
14+
from agent.agent import create_review_agent
15+
from filters_cr.governance_filter import GovernanceToolFilter
16+
from storage.store import ReviewStore
17+
18+
from .findings import Finding, dedupe, gate, severity_distribution
19+
from .governance import DEFAULT_ALLOWED_SCRIPTS, GovernanceEngine
20+
from .llm_review import run_llm_review
21+
from .report import build_report, write_reports
22+
from .sandbox import DIFF_WS_PATH, SandboxSession, create_runtime
23+
24+
DEFAULT_SKILL_ROOT = str(Path(__file__).resolve().parents[1] / "skills" / "code-review")
25+
26+
CHECKERS = [
27+
("check_security.py", "security"),
28+
("check_async_leak.py", "async_resource_leak"),
29+
("check_db_lifecycle.py", "db_lifecycle"),
30+
("check_tests_missing.py", "missing_test"),
31+
("check_secrets.py", "secret_leak"),
32+
]
33+
34+
35+
@dataclass
36+
class ReviewOptions:
37+
diff_text: str
38+
input_type: str
39+
input_ref: str
40+
runtime: str = "local"
41+
dry_run: bool = True
42+
db_url: str = "sqlite:///code_review.db"
43+
output_dir: str = "out"
44+
skill_root: str = DEFAULT_SKILL_ROOT
45+
checkers: list = field(default_factory=lambda: list(CHECKERS))
46+
allowed_scripts: tuple = DEFAULT_ALLOWED_SCRIPTS
47+
timeout_sec: float = 60.0
48+
enable_llm: bool = True
49+
50+
51+
@dataclass
52+
class ReviewResult:
53+
task_id: str
54+
report: dict
55+
json_path: str
56+
md_path: str
57+
58+
59+
def _parse_script_findings(stdout: str, warnings: list) -> list[Finding]:
60+
try:
61+
payload = json.loads(stdout)
62+
except (json.JSONDecodeError, ValueError):
63+
warnings.append("checker produced invalid JSON output")
64+
return []
65+
findings = []
66+
for raw in payload.get("findings", []):
67+
try:
68+
findings.append(Finding(**raw))
69+
except (TypeError, ValueError):
70+
warnings.append(f"skipped malformed finding: {raw!r}")
71+
return findings
72+
73+
74+
async def run_review(opts: ReviewOptions) -> ReviewResult:
75+
"""Run the full review. Sandbox failures degrade, never crash."""
76+
start = time.monotonic()
77+
store = ReviewStore(db_url=opts.db_url)
78+
engine = GovernanceEngine(allowed_scripts=opts.allowed_scripts)
79+
task_id = await store.create_task(opts.input_type, opts.input_ref,
80+
opts.runtime, opts.dry_run)
81+
warnings: list[str] = []
82+
filter_events = []
83+
sandbox_outcomes = []
84+
raw_findings: list[Finding] = []
85+
diff_summary: dict = {}
86+
error_distribution: dict[str, int] = {}
87+
sandbox_ms = 0
88+
try:
89+
runtime = await create_runtime(opts.runtime)
90+
if opts.runtime == "local":
91+
warnings.append("local runtime is a development fallback; "
92+
"use container or cube in production")
93+
session = SandboxSession(runtime, opts.skill_root, timeout_sec=opts.timeout_sec)
94+
await session.open(f"cr_{task_id[:12]}")
95+
try:
96+
await session.put_diff(opts.diff_text)
97+
scripts = [("parse_diff.py", "parse")] + list(opts.checkers)
98+
for script, category in scripts:
99+
decision = engine.check_script(script, [DIFF_WS_PATH])
100+
filter_events.append(decision)
101+
await store.add_filter_event(task_id, decision.target, decision.decision,
102+
decision.rule, decision.reason)
103+
if decision.decision != "allow":
104+
continue
105+
outcome = await session.run_script(script)
106+
engine.record_run(outcome.duration_ms / 1000.0)
107+
sandbox_ms += outcome.duration_ms
108+
sandbox_outcomes.append(outcome)
109+
await store.add_sandbox_run(
110+
task_id, script=script, category=category, status=outcome.status,
111+
exit_code=outcome.exit_code, duration_ms=outcome.duration_ms,
112+
timed_out=outcome.timed_out, stdout_summary=outcome.stdout[:4096],
113+
stderr_summary=outcome.stderr[:4096], error_type=outcome.error_type)
114+
if outcome.status != "ok":
115+
key = outcome.error_type or outcome.status
116+
error_distribution[key] = error_distribution.get(key, 0) + 1
117+
warnings.append(f"{script} did not complete ({outcome.status}); "
118+
"coverage degraded")
119+
continue
120+
if script == "parse_diff.py":
121+
try:
122+
diff_summary = json.loads(outcome.stdout).get("summary", {})
123+
except (json.JSONDecodeError, ValueError):
124+
warnings.append("parse_diff.py produced invalid JSON")
125+
else:
126+
raw_findings.extend(_parse_script_findings(outcome.stdout, warnings))
127+
finally:
128+
await session.close()
129+
130+
llm_findings: list[Finding] = []
131+
llm_summary = ""
132+
llm_calls = 0
133+
if opts.enable_llm:
134+
repository = create_default_skill_repository(
135+
str(Path(opts.skill_root).parent), workspace_runtime=runtime)
136+
gov_filter = GovernanceToolFilter(engine, on_event=filter_events.append)
137+
agent = create_review_agent(repository, opts.dry_run, [gov_filter])
138+
llm_findings, llm_summary, llm_warnings = await run_llm_review(
139+
agent, opts.diff_text, raw_findings)
140+
warnings.extend(llm_warnings)
141+
llm_calls = 1
142+
143+
kept, dropped = dedupe(raw_findings + llm_findings)
144+
reported, needs_review = gate(kept)
145+
await store.add_findings(task_id, reported, status="reported")
146+
await store.add_findings(task_id, needs_review, status="needs_human_review")
147+
await store.add_findings(task_id, dropped, status="deduped")
148+
149+
intercepts = sum(1 for e in filter_events if e.decision != "allow")
150+
metrics = {
151+
"total_duration_ms": int((time.monotonic() - start) * 1000),
152+
"sandbox_duration_ms": sandbox_ms,
153+
"tool_calls": len(sandbox_outcomes) + llm_calls,
154+
"intercepts": intercepts,
155+
"findings_total": len(reported),
156+
"severity_distribution": severity_distribution(reported),
157+
"error_distribution": error_distribution,
158+
}
159+
await store.add_metrics(task_id, metrics)
160+
161+
report = build_report(
162+
task_id=task_id, input_ref=opts.input_ref, runtime=opts.runtime,
163+
dry_run=opts.dry_run, diff_summary=diff_summary, reported=reported,
164+
needs_review=needs_review, deduped_count=len(dropped),
165+
filter_events=filter_events, sandbox_outcomes=sandbox_outcomes,
166+
metrics=metrics, llm_summary=llm_summary, warnings=warnings)
167+
json_path, md_path = write_reports(report, opts.output_dir)
168+
await store.add_report(task_id, report, Path(md_path).read_text(encoding="utf-8"))
169+
await store.update_task(task_id, status="completed",
170+
diff_summary=diff_summary, finished=True)
171+
return ReviewResult(task_id=task_id, report=report,
172+
json_path=json_path, md_path=md_path)
173+
except Exception:
174+
await store.update_task(task_id, status="failed", finished=True)
175+
raise
176+
finally:
177+
await store.close()
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""End-to-end pipeline tests over the 8 fixture scenarios (local runtime, dry-run)."""
7+
import json
8+
import shutil
9+
import time
10+
from pathlib import Path
11+
12+
from review.pipeline import CHECKERS, ReviewOptions, run_review
13+
from storage.store import ReviewStore
14+
15+
EXAMPLE_ROOT = Path(__file__).resolve().parents[1]
16+
FIXTURES = EXAMPLE_ROOT / "fixtures"
17+
18+
19+
def _opts(tmp_path, fixture, **kw):
20+
return ReviewOptions(
21+
diff_text=(FIXTURES / fixture).read_text(),
22+
input_type="fixture", input_ref=fixture,
23+
runtime="local", dry_run=True,
24+
db_url=f"sqlite:///{tmp_path}/cr.db",
25+
output_dir=str(tmp_path / "out"), **kw)
26+
27+
28+
async def _bundle(tmp_path, task_id):
29+
store = ReviewStore(db_url=f"sqlite:///{tmp_path}/cr.db")
30+
bundle = await store.get_task_bundle(task_id)
31+
await store.close()
32+
return bundle
33+
34+
35+
async def test_clean_diff_passes(tmp_path):
36+
result = await run_review(_opts(tmp_path, "clean.diff"))
37+
assert result.report["conclusion"] == "pass"
38+
assert result.report["findings"] == []
39+
assert Path(result.json_path).exists() and Path(result.md_path).exists()
40+
bundle = await _bundle(tmp_path, result.task_id)
41+
assert bundle["task"]["status"] == "completed"
42+
assert len(bundle["sandbox_runs"]) == len(CHECKERS) + 1 # + parse_diff
43+
44+
45+
async def test_security_diff_blocked(tmp_path):
46+
result = await run_review(_opts(tmp_path, "security_eval.diff"))
47+
assert result.report["conclusion"] == "blocked"
48+
cats = {f["category"] for f in result.report["findings"]}
49+
assert "security" in cats
50+
51+
52+
async def test_async_leak_detected(tmp_path):
53+
result = await run_review(_opts(tmp_path, "async_leak.diff"))
54+
assert any(f["category"] == "async_resource_leak" for f in result.report["findings"])
55+
56+
57+
async def test_db_lifecycle_detected(tmp_path):
58+
result = await run_review(_opts(tmp_path, "db_lifecycle.diff"))
59+
assert any(f["category"] == "db_lifecycle" for f in result.report["findings"])
60+
assert any(f["confidence"] < 0.6 for f in result.report["needs_human_review"])
61+
62+
63+
async def test_missing_test_detected(tmp_path):
64+
result = await run_review(_opts(tmp_path, "missing_test.diff"))
65+
assert any(f["category"] == "missing_test" for f in result.report["findings"])
66+
67+
68+
async def test_duplicate_finding_deduplicated(tmp_path):
69+
result = await run_review(_opts(tmp_path, "duplicate_finding.diff"))
70+
security = [f for f in result.report["findings"] if f["category"] == "security"]
71+
assert len(security) == 1
72+
assert result.report["summary"]["deduplicated"] >= 1
73+
bundle = await _bundle(tmp_path, result.task_id)
74+
assert any(f["status"] == "deduped" for f in bundle["findings"])
75+
76+
77+
async def test_sandbox_failure_does_not_crash(tmp_path):
78+
skill_copy = tmp_path / "code-review"
79+
shutil.copytree(EXAMPLE_ROOT / "skills" / "code-review", skill_copy)
80+
(skill_copy / "scripts" / "check_broken.py").write_text("import sys\nsys.exit(3)\n")
81+
checkers = list(CHECKERS) + [("check_broken.py", "broken")]
82+
allowed = tuple(s for s, _ in checkers) + ("parse_diff.py",)
83+
result = await run_review(_opts(tmp_path, "sandbox_failure.diff",
84+
skill_root=str(skill_copy), checkers=checkers,
85+
allowed_scripts=allowed))
86+
bundle = await _bundle(tmp_path, result.task_id)
87+
failed = [r for r in bundle["sandbox_runs"] if r["status"] == "failed"]
88+
assert failed and failed[0]["script"] == "check_broken.py"
89+
assert bundle["task"]["status"] == "completed"
90+
91+
92+
async def test_secret_leak_redacted_everywhere(tmp_path):
93+
result = await run_review(_opts(tmp_path, "secret_leak.diff"))
94+
assert any(f["category"] == "secret_leak" for f in result.report["findings"])
95+
secret = "sk-fakefakefakefakefakefake123456"
96+
all_text = Path(result.json_path).read_text() + Path(result.md_path).read_text()
97+
assert secret not in all_text
98+
bundle = await _bundle(tmp_path, result.task_id)
99+
assert secret not in json.dumps(bundle, default=str)
100+
101+
102+
async def test_denied_checker_never_reaches_sandbox(tmp_path):
103+
checkers = list(CHECKERS) + [("evil.py", "evil")]
104+
result = await run_review(_opts(tmp_path, "clean.diff", checkers=checkers))
105+
bundle = await _bundle(tmp_path, result.task_id)
106+
assert all(r["script"] != "evil.py" for r in bundle["sandbox_runs"])
107+
denies = [e for e in bundle["filter_events"] if e["decision"] == "deny"]
108+
assert any("evil.py" in e["target"] for e in denies)
109+
assert result.report["conclusion"] in ("needs_attention", "pass")
110+
111+
112+
async def test_metrics_recorded_and_fast(tmp_path):
113+
start = time.monotonic()
114+
result = await run_review(_opts(tmp_path, "security_eval.diff"))
115+
elapsed = time.monotonic() - start
116+
assert elapsed < 120
117+
m = result.report["metrics"]
118+
assert m["tool_calls"] >= len(CHECKERS) + 1
119+
assert m["total_duration_ms"] > 0
120+
bundle = await _bundle(tmp_path, result.task_id)
121+
assert bundle["metrics"]["findings_total"] == len(result.report["findings"])

0 commit comments

Comments
 (0)