From 6cad59f83e98a6fbc2996da9e46664ea141a2f5a Mon Sep 17 00:00:00 2001 From: OoO Date: Sat, 2 May 2026 23:44:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(code-review):=20ADR-020=20=E5=85=A8?= =?UTF-8?q?=E8=87=AA=E5=8B=95=E4=BF=AE=E5=BE=A9=E6=94=BF=E7=AD=96=20?= =?UTF-8?q?=E2=80=94=20=E6=8B=86=E6=8E=89=20CRITICAL/HIGH=20HITL=20?= =?UTF-8?q?=E9=96=98=E9=96=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit post-deploy code review pipeline 改為「任何 finding 一律觸發 AiderHeal」, 局部覆寫 ADR-012 L3 HITL(不影響 schema migration / 流量切換 / customer-facing 廣播 / AIOps prod SSH 等其他 L3 場景)。安全網改為 Git revert + Gitea CI/CD 健康檢查 + 主開關 CODE_REVIEW_AUTO_FIX_ENABLED。 實作: • _ea_orchestrate / _guard_ea_decision / rule fallback 三條路徑統一為 has_findings AND AUTO_FIX_ENABLED → auto_fix=true • _guard 強制 LLM 即使回 auto_fix=False 也升級為 true(核心保證) • CODE_REVIEW_AUTO_FIX_ENABLED 預設 false → true • Telegram 文案移除「需人工審查」,改顯示主開關狀態 • action_plan status pending_review → auto_disabled(語意對齊) • aider_heal_executor 標頭 ADR-014 → ADR-020、補「直推 main」分支策略 文件: • 新增 docs/adr/ADR-020-code-review-full-autoheal.md • ADR-012 加 Note 行反向引用 ADR-020 • README 索引收錄 測試:tests/test_code_review_pipeline_security.py 反轉 HITL 期望, 新增 5 case(含 LLM 降級被 guard 拒絕、LLM human_review_needed=true 被改 false) Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 6 +- docs/adr/ADR-012-agent-action-ladder.md | 1 + docs/adr/ADR-020-code-review-full-autoheal.md | 95 +++++++++++++++++++ docs/adr/README.md | 1 + services/aider_heal_executor.py | 5 +- services/code_review_pipeline_service.py | 49 +++++----- tests/test_code_review_pipeline_security.py | 80 ++++++++++++++-- 7 files changed, 203 insertions(+), 34 deletions(-) create mode 100644 docs/adr/ADR-020-code-review-full-autoheal.md diff --git a/.env.example b/.env.example index e84126c..cae6476 100644 --- a/.env.example +++ b/.env.example @@ -183,8 +183,10 @@ INITIAL_ADMIN_PASSWORD=your_initial_admin_password_here # 不設則所有 /bot/api/* 端點拒絕請求 BOT_API_TOKEN=your_bot_api_token_here -# [選填] Post-deploy AI code review pipeline 自動修復開關 -CODE_REVIEW_AUTO_FIX_ENABLED=false +# [必填] Post-deploy AI code review pipeline 自動修復主開關 +# ADR-020 規定預設 true(任何 finding 一律自動觸發 AiderHeal,安全網=Git+CI/CD 回滾) +# 僅在需要短期關閉自動修復鏈時設為 false +CODE_REVIEW_AUTO_FIX_ENABLED=true # [選填] 僅本機開發可設 true;正式環境不得允許不安全 internal webhook MOMO_ALLOW_INSECURE_INTERNAL_WEBHOOK_FOR_DEV=false diff --git a/docs/adr/ADR-012-agent-action-ladder.md b/docs/adr/ADR-012-agent-action-ladder.md index 1b9c4c9..e1a0cdb 100644 --- a/docs/adr/ADR-012-agent-action-ladder.md +++ b/docs/adr/ADR-012-agent-action-ladder.md @@ -4,6 +4,7 @@ - **Date**: 2026-04-19 - **Deciders**: 統帥 - **Related**: ADR-001(三 Agent 分工), ADR-004(NemoTron Fallback), ADR-007(AI Dual-Write), ADR-011(跨專案隔離), ADR-018(四 AI Agent 自動化控制面) +- **Note**: [ADR-020](ADR-020-code-review-full-autoheal.md) 局部覆寫本 ADR 對「post-deploy code review pipeline」場景的 L3 HITL 規定 — 該場景改採全自動修復 + Git/CI/CD 回滾安全網。本 ADR 對 schema migration / 流量切換 / customer-facing 廣播 / AIOps prod SSH 等其他 L3 場景仍生效。 ## Context diff --git a/docs/adr/ADR-020-code-review-full-autoheal.md b/docs/adr/ADR-020-code-review-full-autoheal.md new file mode 100644 index 0000000..df2bcfe --- /dev/null +++ b/docs/adr/ADR-020-code-review-full-autoheal.md @@ -0,0 +1,95 @@ +# ADR-020: Code Review 全自動修復政策(覆寫 ADR-012 對 code review 的 HITL 限制) + +- **Status**: Accepted +- **Date**: 2026-05-02 +- **Deciders**: 統帥 +- **Related / Supersedes**: 局部覆寫 ADR-012(Agent Action Ladder L3 HITL)對「post-deploy code review pipeline」場景的人工審查門檻;不影響 ADR-012 對其他 L3 場景(如 schema migration、production restart、telegram broadcast)的 HITL 規定 +- **Affects**: `services/code_review_pipeline_service.py`、`services/aider_heal_executor.py`、`.env.example`、Gitea CI/CD 部署 env + +## Context + +ADR-012 把所有「實際修改程式碼」的動作劃為 L3,預設要求 Human-In-The-Loop 審查。`services/code_review_pipeline_service.py` 依此規則實作四道閘門: + +1. `AUTO_FIX_ENABLED` env 預設 `false` +2. EA prompt 寫死「CRITICAL/HIGH → auto_fix=false」 +3. Rule fallback `priority not in {critical, high}` 才允許 auto_fix +4. `_guard_ea_decision` 的 `has_high_risk` override,即使 LLM 回 auto_fix=true 也擋下 + +**結果**:2026-05-02 17:38 commit `52c06f6` 觸發 code review,找到 1 CRITICAL(routes/openclaw_bot_routes.py 缺 ALLOWED_USERS 空集處理)+ 1 HIGH(`_is_authorized` 複雜度過高),Telegram 通知顯示「⚠️ Elephant Alpha:CRITICAL 👁 需人工審查」——但統帥的政策一直是「全自動修復、Git+CI/CD 為回滾安全網」。 + +**根本衝突**: +- ADR-012 假設「程式碼修改 = 高風險 = 必須人工」 +- 統帥實際立場:「Code review 找到的都應該全自動修,安全網不是人,是 Git revert + Gitea CI/CD」 +- 過去 memory 引述為「ADR-014」是筆誤(ADR-014 實為 PPT 系統) + +## Decision + +**對 post-deploy code review pipeline 場景**,覆寫 ADR-012 的 HITL 規定: + +### 四條規則 + +| # | 規則 | 實作位置 | +|---|------|---------| +| 1 | 任何 finding(CRITICAL/HIGH/MEDIUM/LOW)一律 `auto_fix=true` | `_ea_orchestrate` prompt + rule fallback | +| 2 | `_guard_ea_decision` 不再依 severity 決定 auto_fix,只受 `CODE_REVIEW_AUTO_FIX_ENABLED` 主開關控制 | `_guard_ea_decision` | +| 3 | `CODE_REVIEW_AUTO_FIX_ENABLED` 預設值改為 `true`(過去是 `false`) | `services/code_review_pipeline_service.py:43` + `.env.example:187` | +| 4 | Telegram 通知文案移除「需人工審查」,改顯示「自動修復未啟用(旗標)」或「已觸發 AiderHeal」 | `_notify_complete` | + +### 安全網(取代 HITL) + +| 層級 | 機制 | 觸發點 | +|-----|------|-------| +| 1 | AiderHeal 限流:每次最多修 2 個檔案 / 至多 5 個 fix_files | `_trigger_aider_heal:457`、`_nemotron_dispatch fix_files[:5]` | +| 2 | 修復產生的 commit 走 Gitea Action CI/CD pipeline,測試失敗自動拒絕 merge | `.gitea/workflows/cd.yaml` | +| 3 | CD 健康檢查不過 → compose up 失敗 → 服務維持舊版本 | ADR-008 / ADR-010 | +| 4 | Git 歷史完整保留,`git revert ` 一行回滾 | 標準 git 流程 | +| 5 | 主開關 `CODE_REVIEW_AUTO_FIX_ENABLED=false` 可即時切斷整條鏈(不需 redeploy 程式碼) | env var | + +### 邊界(不影響的場景) + +ADR-012 的 HITL 規定**仍對下列場景生效**,本 ADR 不覆寫: + +- Schema migration / DB 結構變更 +- Production 服務重啟、流量切換 +- Telegram 廣播訊息(用戶可見) +- 任何牽涉 customer-facing data 的 mutation +- AIOps AutoHeal 對 production 主機的 SSH 修復動作(ADR-013) + +換言之:本 ADR 只對「post-deploy 的 code review → AiderHeal → 提 commit → CI/CD 自動回測」這一條閉環給綠燈。 + +## Alternatives Considered + +| 方案 | 為何不選 | +|------|---------| +| A. 維持 ADR-012 HITL,用 Telegram 按鈕做 one-click approve | 統帥多次表態「不要人工審查」,且 17:38 截圖顯示 Telegram 出現 HITL 訊息就是引爆點 | +| B. 全部 ADR-012 場景都改全自動 | 風險面過大,schema migration / 流量切換不適用「Git revert」這種事後回滾 | +| C. 用 severity 切:MEDIUM/LOW 自動、CRITICAL/HIGH 仍 HITL | 即現況;統帥明確指出「之前的 HIGH 1-2 → 人工審查門檻是錯誤的」 | +| D. 留 HITL 但加「24 小時無人回應自動 fix」timer | 引入時間視窗複雜度,且仍不符「全自動」的精神 | + +## Consequences + +### 正面 +- Code review pipeline 真正端到端自動化,符合統帥對 AI Agent 的期待 +- HITL 通道清空,Telegram 訊息聚焦「修復進度」而非「等你決定」 +- AiderHeal 修復頻率提高 → 觸發更多 OpenClaw learning data(符合 ADR-007 雙寫) + +### 負面 / 風險 +- AiderHeal 誤判機率提高時,會在 main 留下需 revert 的 commit(雖然 CI/CD 會擋住部署,但歷史會有雜訊) +- 主開關 `CODE_REVIEW_AUTO_FIX_ENABLED` 變成關鍵 env,誤設 false 會悄然斷掉整條鏈 +- ADR-012 文件需註解「ADR-020 已局部覆寫」(待後續 commit 補上) + +### 監控指標 +- `ai_insights.metadata_json.auto_fix_triggered=true` 比例(部署後應 → 100% 在有 finding 時) +- AiderHeal 後 CI/CD 失敗率(觀察 1 週,>30% 要重新評估) +- `git revert` 頻率(>每週 1 次代表 AiderHeal 品質有問題) + +## 實作 checklist + +- [x] `services/code_review_pipeline_service.py` 拆四道閘 +- [x] `.env.example` 預設 true +- [x] `tests/test_code_review_pipeline_security.py` 反轉 HITL 期望(含 LLM 降級被 guard 拒絕的測試) +- [x] `docs/adr/README.md` 索引加 ADR-020 +- [x] `docs/adr/ADR-012-agent-action-ladder.md` 加反向引用註記(**Note** 行) +- [x] `services/aider_heal_executor.py` 標頭從「ADR-014」更正為「ADR-020」並補分支策略說明 +- [x] Memory `feedback_code_review_autoheal.md` ADR 號碼從「ADR-014」改為「ADR-020」 +- [ ] 188 / Gitea env 設 `CODE_REVIEW_AUTO_FIX_ENABLED=true`(程式碼預設已 true,此 env 為冗餘保險,部署時統帥決定) diff --git a/docs/adr/README.md b/docs/adr/README.md index 36a9e40..50faece 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -41,6 +41,7 @@ | [017](ADR-017-modularization-cleanup-roadmap.md) | 模組化收尾路線圖(Phase 3f) | Accepted | 2026-04-29 | | [018](ADR-018-four-agent-ai-automation-control-plane.md) | 四 AI Agent 自動化控制面(Hermes/NemoTron/OpenClaw/ElephantAlpha) | Accepted | 2026-04-29 | | [019](ADR-019-telegram-bot-agentic-conversation-layer.md) | Telegram Bot Agentic Conversation Layer(菜單→Agent 決策統一入口) | Accepted | 2026-05-02 | +| [020](ADR-020-code-review-full-autoheal.md) | Code Review 全自動修復政策(局部覆寫 ADR-012 HITL) | Accepted | 2026-05-02 | ## 規範 diff --git a/services/aider_heal_executor.py b/services/aider_heal_executor.py index 0e47483..3b82c80 100644 --- a/services/aider_heal_executor.py +++ b/services/aider_heal_executor.py @@ -1,10 +1,13 @@ """ services/aider_heal_executor.py -ADR-014: Autonomous Code Heal Pipeline +ADR-020: Autonomous Code Heal Pipeline(Code Review 全自動修復端到端執行器) 透過 SSH 在 110 主機執行 Aider,自動修復 momo-pro repo 的程式碼問題, 修復後直接 git push,觸發 Gitea CD Pipeline 部署。 +分支策略:直推 main,依賴 CD pipeline 健康檢查與 git revert 作回滾安全網。 +(不採 PR 流程,呼應 ADR-020「全自動修復、無人工審查門檻」精神) + 安全護欄: L1 - 檔案白名單(只改 services/ routes/ database/ 內 .py) L2 - diff 限制(>50 行 → 拒絕,不 push) diff --git a/services/code_review_pipeline_service.py b/services/code_review_pipeline_service.py index f813919..bd3c06b 100644 --- a/services/code_review_pipeline_service.py +++ b/services/code_review_pipeline_service.py @@ -40,7 +40,7 @@ _pipeline_lock = threading.Lock() GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") REVIEW_MODEL = os.getenv("OPENCLAW_MODEL", "gemini-2.5-flash") INTERNAL_TOKEN = os.getenv("INTERNAL_WEBHOOK_TOKEN", "") -AUTO_FIX_ENABLED = os.getenv("CODE_REVIEW_AUTO_FIX_ENABLED", "false").lower() == "true" +AUTO_FIX_ENABLED = os.getenv("CODE_REVIEW_AUTO_FIX_ENABLED", "true").lower() == "true" ALLOW_INSECURE_WEBHOOK = os.getenv("MOMO_ALLOW_INSECURE_INTERNAL_WEBHOOK_FOR_DEV", "").lower() == "true" @@ -332,14 +332,14 @@ class CodeReviewPipeline: "auto_fix": true|false, "reasoning": "決策理由(繁體中文,一句話,需含具體數字)", "fix_files": ["需自動修復的檔案(最多5個,所有有問題的檔案)"], - "human_review_needed": true + "human_review_needed": false }} -規則(依 ADR-012 L3 HITL,所有 code fix 預設需要人工審核): -- CRITICAL/HIGH → auto_fix=false,human_review_needed=true -- MEDIUM/LOW 只有在系統明確允許時才可 auto_fix=true +規則(依 ADR-020 全自動修復政策,覆寫 ADR-012 L3 HITL 對 code review 的限制): +- 任何 finding(不論 CRITICAL/HIGH/MEDIUM/LOW)→ auto_fix=true,human_review_needed=false +- 安全網是 Git revert + Gitea CI/CD 回滾,不依賴人工審查門檻 - priority 按最嚴重 severity 決定:CRITICAL>HIGH>MEDIUM>LOW -- fix_files 填入所有有問題的檔案(不限 CRITICAL/HIGH)""" +- fix_files 填入所有有問題的檔案(最多 5 個,AiderHeal 端會再限流)""" resp = elephant_service.generate( prompt=prompt, @@ -352,7 +352,7 @@ class CodeReviewPipeline: except Exception as e: logger.warning("[CodeReview] ElephantAlpha 決策失敗,回退規則: %s", e) - # 規則 fallback:ADR-012 L3 邊界,code fix 預設走 HITL。 + # 規則 fallback:ADR-020 全自動修復政策。任何 finding 一律 auto_fix=true。 has_findings = len(findings) > 0 priority = ( "critical" if critical_n > 0 else @@ -360,7 +360,7 @@ class CodeReviewPipeline: "medium" if sev["medium"] > 0 else "low" if sev["low"] > 0 else "low" ) - auto_fix = bool(has_findings and AUTO_FIX_ENABLED and priority not in {"critical", "high"}) + auto_fix = bool(has_findings and AUTO_FIX_ENABLED) fix_files = list({ f.get("file", "") for f in findings if f.get("file") })[:5] @@ -368,13 +368,14 @@ class CodeReviewPipeline: return { "priority": priority, "auto_fix": auto_fix, - "reasoning": f"ADR-012 HITL 規則:CRITICAL={critical_n} HIGH={high_n} MEDIUM={sev['medium']} LOW={sev['low']},{'允許低風險自動修復' if auto_fix else '建立 action_plan 等待人工審核'}", + "reasoning": f"ADR-020 全自動修復:CRITICAL={critical_n} HIGH={high_n} MEDIUM={sev['medium']} LOW={sev['low']}," + + ("觸發 AiderHeal 自動修復(Git+CI/CD 為回滾安全網)" if auto_fix else "無 finding,無需修復"), "fix_files": fix_files, - "human_review_needed": has_findings and not auto_fix, + "human_review_needed": False, } def _guard_ea_decision(self, decision: Dict, findings: List[Dict]) -> Dict: - """Apply local ADR-012 safety gates even if the LLM suggests auto-fix.""" + """ADR-020 全自動修復政策:有 finding 一律 auto_fix=true,僅受 AUTO_FIX_ENABLED 主開關控制。""" sev = self.state["severity_summary"] priority = (decision.get("priority") or "").lower() or ( "critical" if sev["critical"] > 0 else @@ -382,21 +383,20 @@ class CodeReviewPipeline: "medium" if sev["medium"] > 0 else "low" ) - has_high_risk = sev["critical"] > 0 or sev["high"] > 0 or priority in {"critical", "high"} - wants_auto_fix = bool(decision.get("auto_fix")) - allowed_auto_fix = bool(wants_auto_fix and AUTO_FIX_ENABLED and not has_high_risk) - if wants_auto_fix and not allowed_auto_fix: + has_findings = bool(findings) + allowed_auto_fix = bool(has_findings and AUTO_FIX_ENABLED) + if has_findings and not AUTO_FIX_ENABLED: logger.warning( - "[CodeReview] EA auto_fix overridden by ADR-012 HITL gate priority=%s auto_fix_enabled=%s", - priority, AUTO_FIX_ENABLED, + "[CodeReview] auto_fix 被 CODE_REVIEW_AUTO_FIX_ENABLED=false 主開關擋下 priority=%s", + priority, ) decision["priority"] = priority decision["auto_fix"] = allowed_auto_fix - decision["human_review_needed"] = bool(findings and not allowed_auto_fix) + decision["human_review_needed"] = False decision["reasoning"] = ( f"{decision.get('reasoning', '')} " - f"[ADR-012 gate: auto_fix={'enabled' if allowed_auto_fix else 'blocked'}, priority={priority}]" + f"[ADR-020 全自動修復: auto_fix={'enabled' if allowed_auto_fix else 'flag_disabled'}, priority={priority}]" ).strip() return decision @@ -422,7 +422,7 @@ class CodeReviewPipeline: ('code_review_fix', :desc, :status, :priority, :meta, NOW()) """), { "desc": desc[:500], - "status": "auto_pending" if auto_fix else "pending_review", + "status": "auto_pending" if auto_fix else "auto_disabled", "priority": priority_num, "meta": json.dumps({ "pipeline_id": self.pipeline_id, @@ -567,9 +567,12 @@ class CodeReviewPipeline: if openclaw_report: msg += f"\n{openclaw_report[:400]}\n" - fix_status = "🔧 已觸發自動修復(AiderHeal)" if auto_fix else ( - "👁 需人工審查" if ea.get("human_review_needed") else "✅ 無需修復動作" - ) + if auto_fix: + fix_status = "🔧 已觸發自動修復(AiderHeal)" + elif sev['critical'] + sev['high'] + sev['medium'] + sev['low'] == 0: + fix_status = "✅ 無需修復動作" + else: + fix_status = "🛑 自動修復主開關關閉(CODE_REVIEW_AUTO_FIX_ENABLED=false)" msg += ( f"══════════════════════════\n" f"🤖 Elephant Alpha:{priority.upper()} {fix_status}\n" diff --git a/tests/test_code_review_pipeline_security.py b/tests/test_code_review_pipeline_security.py index 4201ace..f3eb6c6 100644 --- a/tests/test_code_review_pipeline_security.py +++ b/tests/test_code_review_pipeline_security.py @@ -17,23 +17,28 @@ def test_verify_internal_token_allows_explicit_dev_override(monkeypatch): assert module.verify_internal_token("") is True -def test_code_review_guard_blocks_high_risk_auto_fix(monkeypatch): +def test_code_review_guard_auto_fixes_high_risk_findings(monkeypatch): + """ADR-020:CRITICAL/HIGH 不再走 HITL,一律 auto_fix=true。""" import services.code_review_pipeline_service as module monkeypatch.setattr(module, "AUTO_FIX_ENABLED", True) pipeline = module.CodeReviewPipeline("abcdef123456", ["services/example.py"]) - pipeline.state["severity_summary"] = {"critical": 0, "high": 1, "medium": 0, "low": 0} + pipeline.state["severity_summary"] = {"critical": 1, "high": 1, "medium": 0, "low": 0} guarded = pipeline._guard_ea_decision( - {"priority": "high", "auto_fix": True, "reasoning": "建議修復", "fix_files": ["services/example.py"]}, - [{"severity": "HIGH", "file": "services/example.py"}], + {"priority": "critical", "auto_fix": True, "reasoning": "建議修復", "fix_files": ["services/example.py"]}, + [ + {"severity": "CRITICAL", "file": "services/example.py"}, + {"severity": "HIGH", "file": "services/example.py"}, + ], ) - assert guarded["auto_fix"] is False - assert guarded["human_review_needed"] is True + assert guarded["auto_fix"] is True + assert guarded["human_review_needed"] is False -def test_code_review_guard_requires_auto_fix_feature_flag(monkeypatch): +def test_code_review_guard_main_switch_can_disable_auto_fix(monkeypatch): + """ADR-020:主開關 CODE_REVIEW_AUTO_FIX_ENABLED=false 可即時切斷自動修復鏈,但不再回退到 HITL。""" import services.code_review_pipeline_service as module monkeypatch.setattr(module, "AUTO_FIX_ENABLED", False) @@ -46,4 +51,63 @@ def test_code_review_guard_requires_auto_fix_feature_flag(monkeypatch): ) assert guarded["auto_fix"] is False - assert guarded["human_review_needed"] is True + # ADR-020:即使主開關關掉,也不再標記 human_review_needed(不存在 HITL 通道) + assert guarded["human_review_needed"] is False + + +def test_code_review_no_findings_no_auto_fix(monkeypatch): + """無 finding 時,auto_fix=false 但不應標記為 human_review_needed。""" + import services.code_review_pipeline_service as module + + monkeypatch.setattr(module, "AUTO_FIX_ENABLED", True) + pipeline = module.CodeReviewPipeline("abcdef123456", ["services/example.py"]) + pipeline.state["severity_summary"] = {"critical": 0, "high": 0, "medium": 0, "low": 0} + + guarded = pipeline._guard_ea_decision( + {"priority": "low", "auto_fix": False, "reasoning": "無 finding", "fix_files": []}, + [], + ) + + assert guarded["auto_fix"] is False + assert guarded["human_review_needed"] is False + + +def test_guard_upgrades_llm_false_to_true_when_findings_exist(monkeypatch): + """ADR-020 核心保證:即使 LLM EA 回傳 auto_fix=False,只要有 finding 且主開關開,guard 必升級為 true。""" + import services.code_review_pipeline_service as module + + monkeypatch.setattr(module, "AUTO_FIX_ENABLED", True) + pipeline = module.CodeReviewPipeline("abcdef123456", ["services/example.py"]) + pipeline.state["severity_summary"] = {"critical": 0, "high": 0, "medium": 1, "low": 0} + + guarded = pipeline._guard_ea_decision( + {"priority": "medium", "auto_fix": False, "reasoning": "LLM 保守判斷", "fix_files": ["services/example.py"]}, + [{"severity": "MEDIUM", "file": "services/example.py"}], + ) + + # ADR-020 不允許 LLM 把決策降級為 HITL + assert guarded["auto_fix"] is True + assert guarded["human_review_needed"] is False + + +def test_guard_upgrades_llm_human_review_true_to_false(monkeypatch): + """ADR-020:即使 LLM 回 human_review_needed=true,guard 也應強制改為 false。""" + import services.code_review_pipeline_service as module + + monkeypatch.setattr(module, "AUTO_FIX_ENABLED", True) + pipeline = module.CodeReviewPipeline("abcdef123456", ["services/example.py"]) + pipeline.state["severity_summary"] = {"critical": 1, "high": 0, "medium": 0, "low": 0} + + guarded = pipeline._guard_ea_decision( + { + "priority": "critical", + "auto_fix": False, + "human_review_needed": True, + "reasoning": "LLM 想走人工審查", + "fix_files": ["services/example.py"], + }, + [{"severity": "CRITICAL", "file": "services/example.py"}], + ) + + assert guarded["auto_fix"] is True + assert guarded["human_review_needed"] is False