상담자 발화 판정(A)·감정(B)·표현(C) 20문항 질문 세트, 감쇠 없는 이번 턴 반응과 비대칭 기분 전이, 개방도 게이트로 생성 지시를 만들고 ccd.coping_strategy 전달 누락을 고친다. trace v2와 고정 문구 속마음 요약(migration 24, AI 경로 차단 RLS)을 같은 트랜잭션에 저장하고 피드백 정책이 켜진 경우에만 done·TurnResponse·음성 reply·리뷰로 노출한다. 회기 화면 속마음 보기 토글, 리뷰 접힘 블록, 관리자 감정 관측 v2 표시를 추가한다.
132 lines
5.1 KiB
Python
132 lines
5.1 KiB
Python
"""앱의 런타임 준비 계약과 infra SQL SSOT 사이의 회귀 검사."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from .runtime_schema import (
|
|
CALIBRATION_TRANSFER_SCHEMA_CONTRACT,
|
|
CLIENT_AFFECT_TRACE_SCHEMA_CONTRACT,
|
|
CLIENT_INNER_REACTION_SCHEMA_CONTRACT,
|
|
CONTINUOUS_IMPROVEMENT_SCHEMA_CONTRACT,
|
|
DELIBERATE_PRACTICE_SCHEMA_CONTRACT,
|
|
MEASUREMENT_SCHEMA_CONTRACT,
|
|
MULTIMODAL_ALLIANCE_SCHEMA_CONTRACT,
|
|
NOTIFICATION_SCHEMA_CONTRACT,
|
|
OUTCOME_TRAJECTORY_SCHEMA_CONTRACT,
|
|
REVIEW_SCHEMA_CONTRACT,
|
|
RUPTURE_REPAIR_SCHEMA_CONTRACT,
|
|
SUPERVISION_RESEARCH_SCHEMA_CONTRACT,
|
|
)
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
INFRA_SQL = "\n".join(
|
|
path.read_text(encoding="utf-8")
|
|
for path in sorted((REPO_ROOT / "infra" / "db" / "init").glob("*.sql"))
|
|
)
|
|
|
|
|
|
class RuntimeSchemaSsotTest(unittest.TestCase):
|
|
def test_runtime_contract_objects_are_owned_by_infra_sql(self) -> None:
|
|
for contract in (
|
|
CLIENT_AFFECT_TRACE_SCHEMA_CONTRACT,
|
|
CLIENT_INNER_REACTION_SCHEMA_CONTRACT,
|
|
REVIEW_SCHEMA_CONTRACT,
|
|
NOTIFICATION_SCHEMA_CONTRACT,
|
|
MEASUREMENT_SCHEMA_CONTRACT,
|
|
OUTCOME_TRAJECTORY_SCHEMA_CONTRACT,
|
|
RUPTURE_REPAIR_SCHEMA_CONTRACT,
|
|
DELIBERATE_PRACTICE_SCHEMA_CONTRACT,
|
|
CALIBRATION_TRANSFER_SCHEMA_CONTRACT,
|
|
SUPERVISION_RESEARCH_SCHEMA_CONTRACT,
|
|
CONTINUOUS_IMPROVEMENT_SCHEMA_CONTRACT,
|
|
MULTIMODAL_ALLIANCE_SCHEMA_CONTRACT,
|
|
):
|
|
for relation in contract.relations:
|
|
schema, table = relation.split(".")
|
|
pattern = rf"CREATE TABLE IF NOT EXISTS\s+{re.escape(schema)}\.{re.escape(table)}\b"
|
|
self.assertRegex(
|
|
INFRA_SQL, pattern, msg=f"infra SQL missing relation: {relation}"
|
|
)
|
|
|
|
for qualified_name in contract.columns:
|
|
schema, table, column = qualified_name.split(".")
|
|
table_mentions = [
|
|
block
|
|
for block in re.split(
|
|
r"(?=CREATE TABLE IF NOT EXISTS|ALTER TABLE)", INFRA_SQL
|
|
)
|
|
if re.search(rf"\b{re.escape(schema)}\.{re.escape(table)}\b", block)
|
|
]
|
|
self.assertTrue(
|
|
any(
|
|
re.search(rf"\b{re.escape(column)}\b", block)
|
|
for block in table_mentions
|
|
),
|
|
msg=f"infra SQL missing column: {qualified_name}",
|
|
)
|
|
|
|
for qualified_name in contract.policies:
|
|
schema, table, policy = qualified_name.split(".")
|
|
pattern = (
|
|
rf"CREATE POLICY\s+{re.escape(policy)}\s+ON\s+"
|
|
rf"{re.escape(schema)}\.{re.escape(table)}\b"
|
|
)
|
|
self.assertRegex(
|
|
INFRA_SQL,
|
|
pattern,
|
|
msg=f"infra SQL missing policy: {qualified_name}",
|
|
)
|
|
|
|
for qualified_name in contract.triggers:
|
|
schema, table, trigger = qualified_name.split(".")
|
|
pattern = (
|
|
rf"CREATE TRIGGER\s+{re.escape(trigger)}\s+"
|
|
rf"(?:BEFORE|AFTER|INSTEAD OF)[\s\S]*?ON\s+"
|
|
rf"{re.escape(schema)}\.{re.escape(table)}\b"
|
|
)
|
|
self.assertRegex(
|
|
INFRA_SQL,
|
|
pattern,
|
|
msg=f"infra SQL missing trigger: {qualified_name}",
|
|
)
|
|
|
|
for qualified_name in contract.indexes:
|
|
schema, table, index = qualified_name.split(".")
|
|
pattern = (
|
|
rf"CREATE (?:UNIQUE )?INDEX IF NOT EXISTS\s+{re.escape(index)}"
|
|
rf"\s+ON\s+{re.escape(schema)}\.{re.escape(table)}\b"
|
|
)
|
|
self.assertRegex(
|
|
INFRA_SQL,
|
|
pattern,
|
|
msg=f"infra SQL missing index: {qualified_name}",
|
|
)
|
|
|
|
def test_alliance_pulse_contract_keeps_lock_and_history_guards(self) -> None:
|
|
required_fragments = (
|
|
"CONSTRAINT measurement_event_pulse_session_fkey",
|
|
"CONSTRAINT self_assessment_pulse_session_fkey",
|
|
"CONSTRAINT ck_alliance_pulse_lock_before_reveal",
|
|
"CONSTRAINT ck_alliance_pulse_time_order",
|
|
"CONSTRAINT ck_alliance_pulse_terminal_state",
|
|
"CREATE TABLE IF NOT EXISTS audit.alliance_pulse_status_event",
|
|
"SECURITY DEFINER",
|
|
"CREATE POLICY p_alliance_pulse_status_event_select",
|
|
)
|
|
for fragment in required_fragments:
|
|
with self.subTest(fragment=fragment):
|
|
self.assertIn(fragment, INFRA_SQL)
|
|
|
|
self.assertNotIn(
|
|
"CREATE POLICY p_alliance_pulse_status_event_insert",
|
|
INFRA_SQL,
|
|
msg="status audit rows must only be produced by the pulse trigger",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|