T-15 D28 部分成交专项:
- sqlite +1 终态单幂等 skipped 口径裁定(ConfirmConflict 仅限读后状态才变窗口)
- 真库 confirm +3 组 19 项(E2 占用释放双向 / G2 actual_qty=0 → rejected / H2 撤单后确认)→ 102/102
- CONVERT_STRESS Barrier(2) 窄窗争批:恰 1 confirmed + 1 rejected
- 突变实证防御纵深:④可用量复核 / ⑤plan_lots / 事务内哨兵三道防线拆任一道仍收敛 rejected
T-16 D29 资金流中转:
- _insert_cash_flows 全仓唯一写入点(out/redeem 费前 + in/subscribe 净转入,remark=convert:{gid} 同事务)
- test_confirm_writes_no_cash_flow 反转为恰 2 条 + 金额对账(T-7 登记闭环)
- 真库 apply A2 组 + B 组 0 残留 → 30/30;confirm 反转断言 → 103/103
- FK 坑(core_cash_flow.fk_cf_customer 挡客户删除):5 处清理段补删
T-17 D30 share_class + A/C 互转:
- _validate_products 纯新增拦截分支:两端 share_class 非空且不同才查 allow_ac_convert(任一端=1 两向放行)
- 口径裁定 2 条(R-12 字面偏差,待用户追认):NULL 不参与 A/C 判定 / 同类走一般转换规则
- 联网核实(用户铁律):A/C 互转需管理人开通(摩根 2025-11 公告)= 开关真实载体;持有期重新起算与本仓一致
- 种子 ⑥ 段(双开对/双关对照/同类对照);sqlite +4 → 842 passed / 8 skipped;真库 accept H 组 → 43/43
- 突变:分支失效 → closed 红,还原绿
文档:开发计划 T-15~T-17 DoD 全勾 + 执行记录 · 基线 842 · TODO/MEMORY/AGENTS/交接文档 v4.5 终态(第 5 步收官 → 下一步第 6 步集成测试)
350 lines
18 KiB
Python
350 lines
18 KiB
Python
"""sqlite 测试 DDL 单一事实源(B8 · B4 评审 P3-12 收敛 / B5 评审 P3-4 时区)。
|
||
|
||
以 VARCHAR/TEXT/INTEGER/REAL 近似 MySQL 的 ENUM/SMALLINT/JSON/DECIMAL,
|
||
枚举防线不在测试 DB 层(由 service 白名单保证,见 test_profile_l3 口径注释)。
|
||
时间默认值统一 `(datetime('now','localtime'))`:sqlite CURRENT_TIMESTAMP 为 UTC,
|
||
本地 23:00~24:00 跑测试时与 MySQL localtime 口径错位一个日界(B5 评审 P3-4)。
|
||
列集为各测试所需列的并集;新表/新列只改此处,禁止再回测试文件手写。
|
||
"""
|
||
|
||
from sqlalchemy import create_engine, text
|
||
from sqlalchemy.pool import StaticPool
|
||
|
||
_TS = "TIMESTAMP DEFAULT (datetime('now','localtime'))"
|
||
|
||
SQLITE_TABLES: dict[str, str] = {
|
||
# ---- Core 模拟库(只读侧) ----
|
||
"core_customer": """
|
||
CREATE TABLE core_customer (
|
||
customer_id VARCHAR(64) PRIMARY KEY, display_name VARCHAR(128), age INTEGER,
|
||
occupation VARCHAR(64), open_date DATE, is_active TINYINT DEFAULT 1,
|
||
is_hnw TINYINT DEFAULT 0, service_tier VARCHAR(16) DEFAULT 'normal')
|
||
""",
|
||
"core_customer_risk": """
|
||
CREATE TABLE core_customer_risk (
|
||
customer_id VARCHAR(64), risk_code VARCHAR(8), evaluated_at TIMESTAMP,
|
||
expires_at TIMESTAMP, is_authoritative TINYINT DEFAULT 1,
|
||
source VARCHAR(32) DEFAULT 'risk_questionnaire',
|
||
investor_category VARCHAR(32) DEFAULT 'ordinary',
|
||
questionnaire_score INTEGER, max_loss_tolerance_pct REAL,
|
||
investment_goal VARCHAR(128), investment_horizon VARCHAR(16),
|
||
professional_approved_at TIMESTAMP)
|
||
""",
|
||
"core_customer_advisor": """
|
||
CREATE TABLE core_customer_advisor (
|
||
advisor_id VARCHAR(64), customer_id VARCHAR(64), rel_status VARCHAR(16))
|
||
""",
|
||
"core_suitability_rule": """
|
||
CREATE TABLE core_suitability_rule (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
customer_risk_code VARCHAR(8), product_risk_code VARCHAR(8),
|
||
match_result VARCHAR(32), rule_ref VARCHAR(32) DEFAULT 'JR-AST-012')
|
||
""",
|
||
"core_product": """
|
||
CREATE TABLE core_product (
|
||
product_id VARCHAR(64) PRIMARY KEY, product_name VARCHAR(128),
|
||
min_risk_code VARCHAR(8), product_type VARCHAR(32),
|
||
min_subscribe_amount DECIMAL DEFAULT 1.00, term_days INTEGER,
|
||
requires_disclosure TINYINT DEFAULT 0,
|
||
can_subscribe TINYINT DEFAULT 1, can_redeem TINYINT DEFAULT 1,
|
||
min_hold_qty DECIMAL DEFAULT 0, min_redeem_qty DECIMAL DEFAULT 0,
|
||
min_hold_action VARCHAR(32) DEFAULT 'force_transfer',
|
||
subscribe_fee_rate DECIMAL DEFAULT 0,
|
||
fund_company VARCHAR(64), ta_code VARCHAR(32),
|
||
share_class VARCHAR(16), allow_ac_convert TINYINT DEFAULT 0,
|
||
rounding_mode VARCHAR(16) DEFAULT 'half_up',
|
||
share_digits TINYINT DEFAULT 2)
|
||
""",
|
||
# T-0(R1 门禁):以 scripts/core/01-ddl.sql:132-145 为权威逐列对齐 ——
|
||
# id PK / customer_id / product_id / qty / cost_amount / market_value /
|
||
# pnl_pct / as_of + UNIQUE(customer_id, product_id)。
|
||
# 原列名 quantity 已废弃(MySQL 恒为 qty,列名失配是 convert 首个阻断项)。
|
||
# 不建外键:sqlite 测试库既有表均无 FK,保持一致(避免插入顺序约束)。
|
||
"core_holding": """
|
||
CREATE TABLE core_holding (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
customer_id VARCHAR(64) NOT NULL, product_id VARCHAR(64) NOT NULL,
|
||
qty DECIMAL NOT NULL, cost_amount DECIMAL NOT NULL,
|
||
market_value DECIMAL NOT NULL, pnl_pct DECIMAL NOT NULL, as_of DATE NOT NULL,
|
||
UNIQUE (customer_id, product_id))
|
||
""",
|
||
"core_trade": """
|
||
CREATE TABLE core_trade (
|
||
trade_id VARCHAR(64) PRIMARY KEY, customer_id VARCHAR(64), product_id VARCHAR(64),
|
||
trade_type VARCHAR(16), amount DECIMAL, qty DECIMAL,
|
||
convert_group_id VARCHAR(64), trade_status VARCHAR(16),
|
||
traded_at TIMESTAMP)
|
||
""",
|
||
# ---- convert 新增 4 表(T-1):MySQL 侧落点为
|
||
# scripts/core/01-ddl.sql(前 3 张)与 docs/项目框架设计/表设计/02-mysql-agent专用.sql
|
||
# (risk_convert_detail)。列集为测试所需列的并集,新列只改此处。
|
||
"core_fee_rule": """
|
||
CREATE TABLE core_fee_rule (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, product_id VARCHAR(64) NOT NULL,
|
||
fee_type VARCHAR(16) NOT NULL, min_hold_days INTEGER, max_hold_days INTEGER,
|
||
min_amount DECIMAL, max_amount DECIMAL, rate DECIMAL NOT NULL,
|
||
to_fund_ratio DECIMAL NOT NULL DEFAULT 1.0)
|
||
""",
|
||
"core_share_lot": f"""
|
||
CREATE TABLE core_share_lot (
|
||
lot_id VARCHAR(64) PRIMARY KEY, customer_id VARCHAR(64) NOT NULL,
|
||
product_id VARCHAR(64) NOT NULL, qty DECIMAL NOT NULL, remain_qty DECIMAL NOT NULL,
|
||
nav DECIMAL NOT NULL, confirmed_at TIMESTAMP NOT NULL,
|
||
source_trade_id VARCHAR(64), created_at {_TS})
|
||
""",
|
||
"core_convert_lot_detail": f"""
|
||
CREATE TABLE core_convert_lot_detail (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, convert_group_id VARCHAR(64) NOT NULL,
|
||
lot_id VARCHAR(64) NOT NULL, qty DECIMAL NOT NULL, hold_days INTEGER NOT NULL,
|
||
amount DECIMAL NOT NULL, fee_rate DECIMAL NOT NULL, fee_amount DECIMAL NOT NULL,
|
||
nav DECIMAL NOT NULL, nav_date DATE NOT NULL, created_at {_TS})
|
||
""",
|
||
# T+1 受理/确认分离模型新增 3 表(T-1 · 开发计划 v2.0):
|
||
# core_convert_request(6 态受理单权威状态机)
|
||
# core_trade_calendar(交易日历,Q11)
|
||
# core_share_rule(D27 产品舍入规则)
|
||
# 列集为测试所需列的并集;MySQL 侧落点 scripts/core/01-ddl.sql,双份一致性
|
||
# 由 REQUIRED_CONVERT_TABLES + test_db.py 列清单断言守护(R-g)。
|
||
"core_convert_request": f"""
|
||
CREATE TABLE core_convert_request (
|
||
convert_group_id VARCHAR(64) PRIMARY KEY,
|
||
client_request_id VARCHAR(64),
|
||
customer_id VARCHAR(64) NOT NULL,
|
||
from_product_id VARCHAR(64) NOT NULL,
|
||
to_product_id VARCHAR(64) NOT NULL,
|
||
qty DECIMAL NOT NULL,
|
||
actual_qty DECIMAL,
|
||
status VARCHAR(16) NOT NULL DEFAULT 'accepted',
|
||
cancel_before TIMESTAMP,
|
||
requested_at TIMESTAMP NOT NULL,
|
||
confirmed_at TIMESTAMP,
|
||
coupon DECIMAL,
|
||
remark VARCHAR(128),
|
||
created_at {_TS}, updated_at {_TS},
|
||
UNIQUE (client_request_id))
|
||
""",
|
||
"core_trade_calendar": """
|
||
CREATE TABLE core_trade_calendar (
|
||
cal_date DATE NOT NULL PRIMARY KEY,
|
||
is_open TINYINT DEFAULT 1, remark VARCHAR(64))
|
||
""",
|
||
"core_share_rule": """
|
||
CREATE TABLE core_share_rule (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
product_id VARCHAR(64) NOT NULL,
|
||
business_type VARCHAR(16) NOT NULL,
|
||
share_digits TINYINT DEFAULT 2,
|
||
rounding_mode VARCHAR(16) DEFAULT 'half_up',
|
||
UNIQUE (product_id, business_type))
|
||
""",
|
||
# T-16(R-11/D29):core_cash_flow 与 MySQL scripts/core/01-ddl.sql 对齐 ——
|
||
# 确认事务内写 2 条中转资金流(转出 out/redeem + 转入 in/subscribe)。
|
||
# MySQL 的 ENUM 在 sqlite 侧按既有惯例降为 VARCHAR(值域由写入方约束)。
|
||
"core_cash_flow": """
|
||
CREATE TABLE core_cash_flow (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
customer_id VARCHAR(64) NOT NULL,
|
||
flow_type VARCHAR(8) NOT NULL,
|
||
flow_subtype VARCHAR(16) NOT NULL DEFAULT 'other',
|
||
amount DECIMAL NOT NULL,
|
||
channel VARCHAR(16) NOT NULL DEFAULT 'online',
|
||
counterparty_account_mask VARCHAR(32),
|
||
counterparty_name VARCHAR(64),
|
||
remark VARCHAR(128),
|
||
occurred_at TIMESTAMP NOT NULL)
|
||
""",
|
||
"risk_convert_detail": f"""
|
||
CREATE TABLE risk_convert_detail (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, convert_group_id VARCHAR(64) NOT NULL,
|
||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||
out_trade_id VARCHAR(64), in_trade_id VARCHAR(64), related_trade_id VARCHAR(64),
|
||
nav DECIMAL, nav_date DATE, fee_amount DECIMAL,
|
||
hold_days_min INTEGER, hold_days_max INTEGER,
|
||
estimated TINYINT NOT NULL DEFAULT 1, nav_stale TINYINT NOT NULL DEFAULT 0,
|
||
client_request_id VARCHAR(64), cancelled_at TIMESTAMP,
|
||
created_at {_TS},
|
||
UNIQUE (convert_group_id), UNIQUE (client_request_id))
|
||
""",
|
||
# T-0(R-f):core_product_nav 与 MySQL scripts/core/01-ddl.sql:186-194 对齐
|
||
# (get_latest_nav 及新增 get_as_of 的读取目标;此前 sqlite 侧整表缺失)。
|
||
"core_product_nav": """
|
||
CREATE TABLE core_product_nav (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, product_id VARCHAR(64) NOT NULL,
|
||
nav DECIMAL NOT NULL, daily_chg_pct DECIMAL NOT NULL, nav_date DATE NOT NULL,
|
||
UNIQUE (product_id, nav_date))
|
||
""",
|
||
# ---- agent 库 ----
|
||
"audit_log": f"""
|
||
CREATE TABLE audit_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id VARCHAR(64), event_type VARCHAR(64),
|
||
agent_type VARCHAR(16), actor_id VARCHAR(64), customer_id VARCHAR(64),
|
||
rule_id VARCHAR(64), input_summary TEXT, decision VARCHAR(64), risk_score INTEGER,
|
||
handler_id VARCHAR(64), handler_result VARCHAR(64), handler_comment VARCHAR(512),
|
||
created_at {_TS})
|
||
""",
|
||
"input_guard_log": f"""
|
||
CREATE TABLE input_guard_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id VARCHAR(64), session_id VARCHAR(64),
|
||
agent_type VARCHAR(16), actor_id VARCHAR(64), guard_type VARCHAR(24),
|
||
raw_excerpt VARCHAR(1024), action VARCHAR(16),
|
||
created_at {_TS})
|
||
""",
|
||
"agent_session": f"""
|
||
CREATE TABLE agent_session (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, session_id VARCHAR(64) UNIQUE,
|
||
trace_id VARCHAR(64), agent_type VARCHAR(16), actor_id VARCHAR(64),
|
||
actor_role VARCHAR(32), customer_id VARCHAR(64), advisor_id VARCHAR(64),
|
||
title VARCHAR(256), status VARCHAR(16) DEFAULT 'active',
|
||
created_at {_TS}, closed_at TIMESTAMP)
|
||
""",
|
||
"agent_message": f"""
|
||
CREATE TABLE agent_message (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, session_id VARCHAR(64),
|
||
trace_id VARCHAR(64), seq_no INTEGER, role VARCHAR(16),
|
||
content TEXT, has_disclaimer TINYINT DEFAULT 0, token_est INTEGER,
|
||
created_at {_TS})
|
||
""",
|
||
"agent_tool_call": f"""
|
||
CREATE TABLE agent_tool_call (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, session_id VARCHAR(64),
|
||
trace_id VARCHAR(64), message_id INTEGER, tool_name VARCHAR(128),
|
||
tool_input TEXT, tool_output TEXT, status VARCHAR(16),
|
||
error_code VARCHAR(64), latency_ms INTEGER,
|
||
created_at {_TS})
|
||
""",
|
||
"risk_alert": f"""
|
||
CREATE TABLE risk_alert (
|
||
alert_id VARCHAR(64) PRIMARY KEY, trace_id VARCHAR(64), customer_id VARCHAR(64),
|
||
trade_id VARCHAR(64), alert_type VARCHAR(16), triggered_rules TEXT,
|
||
risk_score INTEGER, status VARCHAR(24) DEFAULT 'pending_review', payload TEXT,
|
||
handler_id VARCHAR(64), handler_result VARCHAR(64), handler_comment VARCHAR(512),
|
||
created_at {_TS}, handled_at TIMESTAMP)
|
||
""",
|
||
"risk_suitability_log": f"""
|
||
CREATE TABLE risk_suitability_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id VARCHAR(64) NOT NULL,
|
||
customer_id VARCHAR(64) NOT NULL, product_id VARCHAR(64) NOT NULL,
|
||
product_name VARCHAR(128),
|
||
customer_risk_level VARCHAR(8) NOT NULL, product_risk_level VARCHAR(8) NOT NULL,
|
||
investor_category VARCHAR(32) DEFAULT 'ordinary',
|
||
match_result VARCHAR(32) NOT NULL, mismatch_type VARCHAR(32) DEFAULT 'none',
|
||
is_matched TINYINT, is_blocked TINYINT DEFAULT 0,
|
||
requires_disclosure TINYINT DEFAULT 0, needs_branch_confirm TINYINT DEFAULT 0,
|
||
risk_was_expired TINYINT DEFAULT 0,
|
||
block_reason VARCHAR(512), block_response_code VARCHAR(32),
|
||
check_source VARCHAR(16) DEFAULT 'r02_trade', actor_id VARCHAR(64),
|
||
request_ref VARCHAR(64), profile_l1_version VARCHAR(32),
|
||
rule_refs TEXT, created_at {_TS})
|
||
""",
|
||
"customer_profile_l3": f"""
|
||
CREATE TABLE customer_profile_l3 (
|
||
customer_id VARCHAR(64) PRIMARY KEY, monitor_tier VARCHAR(16) NOT NULL,
|
||
risk_score INTEGER, score_dimensions TEXT, monitor_tags TEXT,
|
||
last_alert_id VARCHAR(64), computed_at TIMESTAMP NOT NULL,
|
||
updated_at {_TS})
|
||
""",
|
||
"risk_aml_list": f"""
|
||
CREATE TABLE risk_aml_list (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, list_id VARCHAR(64), list_type VARCHAR(16),
|
||
full_name VARCHAR(128), id_no VARCHAR(32), bank_card_no VARCHAR(32),
|
||
match_threshold REAL, source VARCHAR(64), list_version VARCHAR(16),
|
||
effective_date DATE, is_active TINYINT DEFAULT 1, created_at {_TS})
|
||
""",
|
||
}
|
||
|
||
|
||
# ---------- T-0 建库自校验(R1 门禁)----------
|
||
# 与 scripts/core/01-ddl.sql 人工同步的权威结构清单(逐列对照,见上表注释):
|
||
# core_holding id BIGINT PK / customer_id / product_id / qty / cost_amount /
|
||
# market_value / pnl_pct / as_of / UNIQUE KEY uk_cust_product
|
||
# core_product_nav id BIGINT PK / product_id / nav / daily_chg_pct / nav_date /
|
||
# UNIQUE KEY uk_product_date
|
||
# 失配在建库那一刻即 AssertionError,不留到集成测试期。
|
||
EXPECTED_CORE_HOLDING_COLUMNS = {
|
||
"id", "customer_id", "product_id", "qty",
|
||
"cost_amount", "market_value", "pnl_pct", "as_of",
|
||
}
|
||
REQUIRED_CORE_TABLES = {"core_holding", "core_product_nav", "core_trade", "core_product"}
|
||
# T-1:convert 新增 4 表同样纳入存在性门禁。落点:前 3 张在 scripts/core/01-ddl.sql,
|
||
# risk_convert_detail 在 docs/项目框架设计/表设计/02-mysql-agent专用.sql。
|
||
REQUIRED_CONVERT_TABLES = {
|
||
"core_fee_rule", "core_share_lot", "core_convert_lot_detail", "risk_convert_detail",
|
||
# T+1 模型新增 3 表(T-1)
|
||
"core_convert_request", "core_trade_calendar", "core_share_rule",
|
||
# T-16(R-11/D29):确认事务的中转资金流载体(MySQL 侧已建,F-14)
|
||
"core_cash_flow",
|
||
}
|
||
|
||
|
||
def _assert_ddl_aligned(engine) -> None:
|
||
"""建库后自校验:缺列/缺表立即 AssertionError(R1 门禁)。
|
||
|
||
触发场景(真实踩过的坑):sqlite 列名曾为 quantity,而 MySQL 是 qty ——
|
||
单测全绿但 convert 首次 UPDATE core_holding.qty 才炸;本校验把失败点前移。
|
||
"""
|
||
with engine.connect() as conn:
|
||
tables = {
|
||
row[0]
|
||
for row in conn.execute(text("SELECT name FROM sqlite_master WHERE type = 'table'"))
|
||
}
|
||
missing_tables = REQUIRED_CORE_TABLES - tables
|
||
assert not missing_tables, (
|
||
f"sqlite DDL 缺表:{sorted(missing_tables)}(以 scripts/core/01-ddl.sql 为准)"
|
||
)
|
||
missing_convert = REQUIRED_CONVERT_TABLES - tables
|
||
assert not missing_convert, (
|
||
f"sqlite DDL 缺 convert 表:{sorted(missing_convert)}"
|
||
"(以 scripts/core/01-ddl.sql 与 docs/项目框架设计/表设计/02-mysql-agent专用.sql 为准)"
|
||
)
|
||
columns = {row[1] for row in conn.execute(text("PRAGMA table_info(core_holding)"))}
|
||
missing_columns = EXPECTED_CORE_HOLDING_COLUMNS - columns
|
||
assert not missing_columns, (
|
||
f"core_holding 缺列:{sorted(missing_columns)}(以 scripts/core/01-ddl.sql 为准)"
|
||
)
|
||
|
||
|
||
def create_sqlite_engine():
|
||
"""内存 sqlite + 全部表(StaticPool 单连接共享,同现有测试模式)。
|
||
|
||
建表后经 `_assert_ddl_aligned` 自校验(T-0 门禁):结构失配在建库即失败,
|
||
而不是留到集成测试期。
|
||
"""
|
||
engine = create_engine(
|
||
"sqlite://", poolclass=StaticPool, connect_args={"check_same_thread": False}
|
||
)
|
||
with engine.begin() as conn:
|
||
for ddl in SQLITE_TABLES.values():
|
||
conn.execute(text(ddl))
|
||
_assert_ddl_aligned(engine)
|
||
return engine
|
||
|
||
|
||
# C×R 适当性矩阵测试种子(与 scripts/core/02-seed-base.sql 25 行逐格一致;
|
||
# AL-05 换核后 check_suitability 以矩阵表为 L0 权威,缺失即全量 forbidden)
|
||
SUITABILITY_MATRIX_SEED: list[tuple[str, str, str]] = [
|
||
("C1", "R1", "allowed"), ("C1", "R2", "allowed"), ("C1", "R3", "forbidden"),
|
||
("C1", "R4", "forbidden"), ("C1", "R5", "forbidden"),
|
||
("C2", "R1", "allowed"), ("C2", "R2", "allowed"), ("C2", "R3", "allowed"),
|
||
("C2", "R4", "forbidden"), ("C2", "R5", "forbidden"),
|
||
("C3", "R1", "allowed"), ("C3", "R2", "allowed"), ("C3", "R3", "allowed"),
|
||
("C3", "R4", "allowed_with_disclosure"), ("C3", "R5", "forbidden"),
|
||
("C4", "R1", "allowed"), ("C4", "R2", "allowed"), ("C4", "R3", "allowed"),
|
||
("C4", "R4", "allowed"), ("C4", "R5", "allowed_with_disclosure"),
|
||
("C5", "R1", "allowed"), ("C5", "R2", "allowed"), ("C5", "R3", "allowed"),
|
||
("C5", "R4", "allowed"), ("C5", "R5", "allowed"),
|
||
]
|
||
|
||
|
||
def seed_suitability_matrix(engine) -> None:
|
||
"""给 sqlite 测试库灌 25 行 C×R 矩阵(各涉及适当性的测试 fixture 调用)。"""
|
||
with engine.begin() as conn:
|
||
for c, r, m in SUITABILITY_MATRIX_SEED:
|
||
conn.execute(
|
||
text(
|
||
"INSERT INTO core_suitability_rule "
|
||
"(customer_risk_code, product_risk_code, match_result) "
|
||
"VALUES (:c, :r, :m)"
|
||
),
|
||
{"c": c, "r": r, "m": m},
|
||
)
|