This commit is contained in:
@@ -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'),
|
||||
|
||||
78
migrations/037_add_action_plans_guardrails.sql
Normal file
78
migrations/037_add_action_plans_guardrails.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- =============================================================================
|
||||
-- Migration 037: action_plans source/status guardrails
|
||||
-- 日期: 2026-05-12 台北
|
||||
-- =============================================================================
|
||||
-- 背景:
|
||||
-- action_plans 是 Group A(CodeReview/OpenClaw)與 Group B(NemoTron)
|
||||
-- 共用的 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 $$;
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user