補上 action_plans 寫入護欄
All checks were successful
CD Pipeline / deploy (push) Successful in 57s

This commit is contained in:
OoO
2026-05-12 23:35:25 +08:00
parent caa6263872
commit bc3f9cc61a
3 changed files with 115 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, ForeignKey, Index, Float
from sqlalchemy import CheckConstraint, Column, Integer, String, DateTime, Text, Boolean, ForeignKey, Index, Float
from sqlalchemy.orm import relationship
from database.models import Base
from datetime import datetime
@@ -55,6 +55,29 @@ class ActionPlan(Base):
executed_at = Column(DateTime, nullable=True)
__table_args__ = (
CheckConstraint(
"action_type IS NOT NULL OR created_by IS NOT NULL",
name="chk_action_plans_source_marker",
),
CheckConstraint(
"action_type IS NULL OR action_type IN ('auto', 'code_review_fix', 'openclaw_recommendation')",
name="chk_action_plans_action_type",
),
CheckConstraint(
"created_by IS NULL OR created_by IN ("
"'nemotron', 'openclaw', 'code_review_pipeline', "
"'ai_orchestrator', 'watcher_agent', 'agent_actions', "
"'elephant_alpha', 'manual', 'system'"
")",
name="chk_action_plans_created_by",
),
CheckConstraint(
"status IS NULL OR status IN ("
"'pending', 'approved', 'rejected', 'executed', "
"'auto_pending', 'auto_disabled', 'pending_review'"
")",
name="chk_action_plans_status",
),
Index('idx_action_plans_type', 'action_type'),
Index('idx_action_plan_sku_status', 'sku', 'status'),
Index('idx_action_plan_created', 'created_at'),

View File

@@ -0,0 +1,78 @@
-- =============================================================================
-- Migration 037: action_plans source/status guardrails
-- 日期: 2026-05-12 台北
-- =============================================================================
-- 背景:
-- action_plans 是 Group ACodeReview/OpenClaw與 Group BNemoTron
-- 共用的 superset schema。migration 019 解了缺欄,但沒有 caller/source 護欄,
-- 新寫入容易變成 action_type、created_by 都空的不可歸因資料。
--
-- 設計:
-- 1. 使用 NOT VALID不掃描歷史資料但 PostgreSQL 仍會檢查新 INSERT/UPDATE。
-- 2. 不限制 plan_type/payload 組合,先只防止不可歸因與明顯非法狀態。
-- 3. 若未來新增 caller需新增 migration 擴充白名單。
-- =============================================================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'chk_action_plans_source_marker'
) THEN
ALTER TABLE action_plans
ADD CONSTRAINT chk_action_plans_source_marker
CHECK (action_type IS NOT NULL OR created_by IS NOT NULL)
NOT VALID;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'chk_action_plans_action_type'
) THEN
ALTER TABLE action_plans
ADD CONSTRAINT chk_action_plans_action_type
CHECK (
action_type IS NULL
OR action_type IN ('auto', 'code_review_fix', 'openclaw_recommendation')
)
NOT VALID;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'chk_action_plans_created_by'
) THEN
ALTER TABLE action_plans
ADD CONSTRAINT chk_action_plans_created_by
CHECK (
created_by IS NULL
OR created_by IN (
'nemotron', 'openclaw', 'code_review_pipeline',
'ai_orchestrator', 'watcher_agent', 'agent_actions',
'elephant_alpha', 'manual', 'system'
)
)
NOT VALID;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'chk_action_plans_status'
) THEN
ALTER TABLE action_plans
ADD CONSTRAINT chk_action_plans_status
CHECK (
status IS NULL
OR status IN (
'pending', 'approved', 'rejected', 'executed',
'auto_pending', 'auto_disabled', 'pending_review'
)
)
NOT VALID;
END IF;
END $$;
DO $$
BEGIN
RAISE NOTICE 'Migration 037 done: action_plans guardrail CHECK constraints added as NOT VALID';
END $$;

View File

@@ -56,6 +56,19 @@ def test_incident_model_keeps_legacy_and_current_columns():
} <= columns
def test_action_plan_model_has_source_guardrail_constraints():
from database.manager import Base
constraints = {item.name for item in Base.metadata.tables["action_plans"].constraints}
assert {
"chk_action_plans_source_marker",
"chk_action_plans_action_type",
"chk_action_plans_created_by",
"chk_action_plans_status",
} <= constraints
def test_auto_heal_status_update_backfills_dual_playbook_columns(monkeypatch):
from services.auto_heal_service import AutoHealResult, AutoHealService