feat(review): multi-lens orchestration — 5 parallel opencode subprocesses (security/docs/code-quality/tests/perf)
On by default, opt-out via "reviewers": []. 290 tests pass.
- pilot/opencode_review.py: ReviewerSpec dataclass, default_reviewers(),
parse_reviewers_config(), parse_triage_config(), resolve_reviewers(),
_normalize_lens_finding(), posthash() (matches feedback.py scheme),
_agreement_hash() (severity-free for cross-lens promotion), _tone_strip(),
synthesize() 7-stage (severity_floor → tone-strip → length cap → per-lens
max → per-file cap → dedup by _posthash → cross-lens severity promote →
per-PR cap), run_lenses() (ThreadPoolExecutor pool=4), triage(),
_intersect_with_triage(), _filter_by_skip_if(), run_lenses_review().
run() routes to fan-out when config.reviewers[] present or PRAGENT_REVIEWERS=1.
- pilot/ai_review.py: parse_repo_config learns reviewers[] and triage objects
(id regex /^[a-z0-9][a-z0-9-]{0,31}$/, 8-entry cap, agent_file/model/
severity_floor/max_findings/activation/skip_if_all_changed_paths/hotpath_globs).
review_pr branches to opencode_review.run_lenses_review when configured.
_render_collapsible_usage shows lenses: ... line when present.
- .opencode/agents/{docs,code-quality,triage}.md: 3 new lens subagents.
- .opencode/skills/lens-orchestration/SKILL.md: strict-JSON contract every
lens subagent MUST honor.
- .opencode/agents/pragent.md: slim to coordinator; no more hardcoded
@security/@tests/@perf delegation; loads lens-orchestration skill.
- .opencode/README.md: rewrite 'Add a review lens' recipe for multi-lens.
- pilot/README-webhook.md: new 'Multi-lens pipeline' section (diagram +
default roster + config schema + env vars + cross-lens dedup contract).
- tests: 36 new tests (test_ai_review.py +12 reviewers/triage/usage,
test_opencode_review.py +24 orchestration). posthash golden-vector matches
feedback.py exactly across 5 severity × 2 line cases.
This commit is contained in:
@@ -1458,3 +1458,138 @@ def test_build_user_prompt_injects_additional_context():
|
||||
def test_build_user_prompt_skips_additional_context_when_empty():
|
||||
prompt = build_user_prompt("T", "B", "diff")
|
||||
assert "## Repo-provided context" not in prompt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_repo_config: reviewers[] + triage (multi-lens orchestration)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_parse_repo_config_reviewers_array_basic():
|
||||
raw = json.dumps({
|
||||
"reviewers": [
|
||||
{"id": "security", "severity_floor": "high", "max_findings": 10},
|
||||
{"id": "docs", "agent_file": ".opencode/agents/docs.md"},
|
||||
{"id": "perf", "model": "headroom/glm-5.2:cloud",
|
||||
"skip_if_all_changed_paths": "docs/**"},
|
||||
]
|
||||
})
|
||||
cfg = parse_repo_config(raw)
|
||||
assert cfg["reviewers"] == [
|
||||
{"id": "security", "severity_floor": "high", "max_findings": 10},
|
||||
{"id": "docs", "agent_file": ".opencode/agents/docs.md"},
|
||||
{"id": "perf", "model": "headroom/glm-5.2:cloud",
|
||||
"skip_if_all_changed_paths": "docs/**"},
|
||||
]
|
||||
|
||||
|
||||
def test_parse_repo_config_reviewers_rejects_bad_id():
|
||||
# Punctuation, leading dash, underscore, empty — all silently dropped.
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"reviewers": [
|
||||
{"id": "BAD!!!"},
|
||||
{"id": "-bad-start"},
|
||||
{"id": "ok_under"},
|
||||
{"id": ""},
|
||||
{"id": "good-one"},
|
||||
]
|
||||
}))
|
||||
assert cfg["reviewers"] == [{"id": "good-one"}]
|
||||
|
||||
|
||||
def test_parse_repo_config_reviewers_caps_at_8():
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"reviewers": [{"id": f"l{i}"} for i in range(12)]
|
||||
}))
|
||||
assert len(cfg["reviewers"]) == 8
|
||||
|
||||
|
||||
def test_parse_repo_config_reviewers_drop_non_dict_entries():
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"reviewers": ["not-a-dict", 42, None, {"id": "ok"}]
|
||||
}))
|
||||
assert cfg["reviewers"] == [{"id": "ok"}]
|
||||
|
||||
|
||||
def test_parse_repo_config_reviewers_absent_yields_no_key():
|
||||
cfg = parse_repo_config("{}")
|
||||
assert "reviewers" not in cfg
|
||||
|
||||
|
||||
def test_parse_repo_config_reviewers_activation_validated():
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"reviewers": [
|
||||
{"id": "a", "activation": "auto"},
|
||||
{"id": "b", "activation": "always"},
|
||||
{"id": "c", "activation": "off"},
|
||||
{"id": "d", "activation": "BOGUS"}, # dropped (unsupported)
|
||||
]
|
||||
}))
|
||||
# Only the entries with valid activation carry the key — the BOGUS one
|
||||
# just keeps id (the unknown field is silently dropped, not rejected).
|
||||
assert [r.get("activation") for r in cfg["reviewers"]] == [
|
||||
"auto", "always", "off", None
|
||||
]
|
||||
|
||||
|
||||
def test_parse_repo_config_triage_object_full():
|
||||
cfg = parse_repo_config(json.dumps({
|
||||
"triage": {"enabled": True, "model": "headroom/haiku", "max_lenses": 3}
|
||||
}))
|
||||
assert cfg["triage"] == {"enabled": True, "model": "headroom/haiku", "max_lenses": 3}
|
||||
|
||||
|
||||
def test_parse_repo_config_triage_disabled():
|
||||
cfg = parse_repo_config(json.dumps({"triage": {"enabled": False}}))
|
||||
assert cfg["triage"] == {"enabled": False}
|
||||
|
||||
|
||||
def test_parse_repo_config_triage_malformed_yields_disabled():
|
||||
# Non-object triage value (string, list, number) should disable, not crash.
|
||||
for raw in (
|
||||
'{"triage": "off"}',
|
||||
'{"triage": []}',
|
||||
'{"triage": 42}',
|
||||
):
|
||||
cfg = parse_repo_config(raw)
|
||||
assert cfg.get("triage") == {"enabled": False}, f"failed for {raw}"
|
||||
|
||||
|
||||
def test_parse_repo_config_triage_absent_yields_no_key():
|
||||
cfg = parse_repo_config("{}")
|
||||
assert "triage" not in cfg
|
||||
|
||||
|
||||
def test_parse_repo_config_triage_max_lenses_capped_at_8():
|
||||
cfg = parse_repo_config(json.dumps({"triage": {"max_lenses": 100}}))
|
||||
# 100 is out of range; the key is dropped, not clamped. Caller defaults.
|
||||
assert "max_lenses" not in cfg.get("triage", {})
|
||||
|
||||
|
||||
def test_render_collapsible_usage_shows_lenses_when_multi():
|
||||
usage = {
|
||||
"input": 100, "output": 50, "reasoning": 0,
|
||||
"cache_read": 0, "cache_write": 0, "total": 150,
|
||||
"steps": 12, "duration_s": 8.4,
|
||||
"lenses": ["security", "docs", "tests"],
|
||||
"lens_steps": 12,
|
||||
}
|
||||
out = _render_collapsible_usage(usage, "headroom/glm-5.2:cloud", None)
|
||||
assert "Lenses" in out
|
||||
# All three lens ids are shown in backticks.
|
||||
assert "`security`" in out
|
||||
assert "`docs`" in out
|
||||
assert "`tests`" in out
|
||||
# Step count is surfaced.
|
||||
assert "12" in out
|
||||
|
||||
|
||||
def test_render_collapsible_usage_omits_lenses_when_single_primary():
|
||||
usage = {
|
||||
"input": 100, "output": 50, "reasoning": 0,
|
||||
"cache_read": 0, "cache_write": 0, "total": 150,
|
||||
"steps": 4, "duration_s": 2.0,
|
||||
}
|
||||
out = _render_collapsible_usage(usage, "headroom/glm-5.2:cloud", None)
|
||||
assert "Lenses" not in out
|
||||
|
||||
|
||||
Reference in New Issue
Block a user