Files
query-payslip-default/tests/test_data_management_contract.py
skill-dev 3db196e49d feat: 工资查询助手首版 v1.0.105
基于 skill-template 脚手架开发,包含:
- payroll_config(费率配置,可编辑)+ payslip_snapshots(解析历史,只读)2 张业务表
- 全国默认 + 北上广 4 套费率(社保/公积金/个税起征点/7 级税率表)
- 5 个业务 Action:calculate-payslip-simple / calculate-payslip-full / explain-term / view-config / edit-config
- 个税累计预扣法计算,支持多月历史累加
- 20 个薪资名词库(社保/公积金/个税/全勤/加班费等)
- 完整市场文档(README/TUTORIAL/DEMO/CHANGELOG)
- 296 测试全部通过
2026-09-07 17:02:05 +08:00

480 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""通用数据管理契约测试(demo_items fixture,不含具体业务技能内容)。"""
from __future__ import annotations
import json
import sqlite3
import unittest
from _support import IsolatedDataRoot
DEMO_ITEMS_DDL = """
CREATE TABLE IF NOT EXISTS demo_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'draft',
generated_value TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE TRIGGER IF NOT EXISTS demo_items_set_updated_at
AFTER UPDATE ON demo_items
FOR EACH ROW
BEGIN
UPDATE demo_items
SET updated_at = unixepoch()
WHERE id = NEW.id AND updated_at = OLD.updated_at;
END;
"""
DEMO_ITEMS_TABLE_METADATA = {
"table_name": "demo_items",
"display_name": "示例记录",
"description": "通用数据管理契约演示表",
"visible": 1,
"readonly": 0,
}
DEMO_ITEMS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
{"column_name": "id", "display_name": "编号", "visible": 1, "searchable": 1, "editable": 0},
{"column_name": "title", "display_name": "标题", "visible": 1, "searchable": 1, "editable": 1},
{"column_name": "description", "display_name": "描述", "visible": 1, "searchable": 1, "editable": 1},
{
"column_name": "status",
"display_name": "状态",
"visible": 1,
"searchable": 1,
"editable": 1,
"options_json": json.dumps(
[
{"value": "draft", "label": "草稿"},
{"value": "published", "label": "已发布"},
{"value": "archived", "label": "已归档"},
],
ensure_ascii=False,
),
},
{"column_name": "generated_value", "display_name": "生成值", "visible": 1, "searchable": 0, "editable": 0},
{
"column_name": "created_at",
"display_name": "创建时间",
"visible": 1,
"searchable": 0,
"editable": 0,
"display_type": "datetime_unix_seconds",
},
{
"column_name": "updated_at",
"display_name": "更新时间",
"visible": 1,
"searchable": 0,
"editable": 0,
"display_type": "datetime_unix_seconds",
},
)
def _init_demo_items_fixture(conn: sqlite3.Connection, *, prune: bool = True) -> None:
from db.display_metadata import (
init_display_metadata_tables,
list_physical_column_names,
seed_column_metadata_batch,
upsert_table_metadata,
)
cur = conn.cursor()
init_display_metadata_tables(cur)
cur.executescript(DEMO_ITEMS_DDL)
meta = DEMO_ITEMS_TABLE_METADATA
upsert_table_metadata(
cur,
table_name=str(meta["table_name"]),
display_name=str(meta["display_name"]),
description=str(meta["description"]),
visible=int(meta["visible"]),
readonly=int(meta["readonly"]),
)
physical = list_physical_column_names(cur, "demo_items")
seed_column_metadata_batch(
cur,
table_name="demo_items",
columns=DEMO_ITEMS_COLUMN_METADATA,
valid_column_names=physical if prune else None,
)
conn.commit()
class TestDataManagementContract(unittest.TestCase):
def test_demo_items_metadata_permissions(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
from db.display_metadata import (
get_column_editable,
get_table_readonly,
list_editable_business_columns,
table_allows_manual_create,
)
cur = conn.cursor()
self.assertEqual(get_table_readonly(cur, "demo_items"), 0)
self.assertEqual(get_column_editable(cur, "demo_items", "id"), 0)
self.assertEqual(get_column_editable(cur, "demo_items", "title"), 1)
self.assertEqual(get_column_editable(cur, "demo_items", "generated_value"), 0)
self.assertEqual(get_column_editable(cur, "demo_items", "created_at"), 0)
self.assertEqual(get_column_editable(cur, "demo_items", "updated_at"), 0)
editable_cols = list_editable_business_columns(cur, "demo_items")
self.assertEqual(editable_cols, ["title", "description", "status"])
self.assertTrue(table_allows_manual_create(cur, "demo_items"))
finally:
conn.close()
def test_demo_items_init_idempotent_no_duplicates(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
_init_demo_items_fixture(conn)
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name = 'demo_items'")
self.assertEqual(int(cur.fetchone()[0]), 1)
cur.execute("SELECT COUNT(*) FROM _jiangchang_columns WHERE table_name = 'demo_items'")
self.assertEqual(int(cur.fetchone()[0]), 7)
finally:
conn.close()
def test_reinit_updates_stale_editable_value(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute(
"UPDATE _jiangchang_columns SET editable = 0 WHERE table_name = 'demo_items' AND column_name = 'title'"
)
conn.commit()
_init_demo_items_fixture(conn)
from db.display_metadata import get_column_editable
self.assertEqual(get_column_editable(conn.cursor(), "demo_items", "title"), 1)
finally:
conn.close()
def test_missing_table_metadata_detected(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute("DELETE FROM _jiangchang_tables WHERE table_name = 'demo_items'")
conn.commit()
from db.display_metadata_validator import validate_display_metadata
report = validate_display_metadata(conn)
self.assertFalse(report.ok)
self.assertTrue(any("demo_items" in err for err in report.errors))
finally:
conn.close()
def test_missing_column_metadata_detected(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute(
"DELETE FROM _jiangchang_columns WHERE table_name = 'demo_items' AND column_name = 'title'"
)
conn.commit()
from db.display_metadata_validator import validate_display_metadata
report = validate_display_metadata(conn)
self.assertFalse(report.ok)
self.assertTrue(any("demo_items.title" in err for err in report.errors))
finally:
conn.close()
def test_metadata_reference_to_missing_column_detected(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute(
"""
INSERT INTO _jiangchang_columns
(table_name, column_name, display_name, visible, searchable, editable)
VALUES ('demo_items', 'not_exists', '不存在', 1, 1, 0)
"""
)
conn.commit()
from db.display_metadata_validator import validate_display_metadata
report = validate_display_metadata(conn)
self.assertFalse(report.ok)
self.assertTrue(any("not_exists" in err for err in report.errors))
finally:
conn.close()
def test_empty_display_name_detected_for_visible_column(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute(
"UPDATE _jiangchang_columns SET display_name = '' WHERE table_name = 'demo_items' AND column_name = 'title'"
)
conn.commit()
from db.display_metadata_validator import validate_display_metadata
report = validate_display_metadata(conn)
self.assertFalse(report.ok)
self.assertTrue(any("display_name 为空" in err for err in report.errors))
finally:
conn.close()
def test_visible_columns_use_chinese_display_name(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
cur = conn.cursor()
cur.execute(
"""
SELECT column_name, display_name
FROM _jiangchang_columns
WHERE table_name = 'demo_items' AND visible = 1
"""
)
for column_name, display_name in cur.fetchall():
self.assertTrue(
any("\u4e00" <= ch <= "\u9fff" for ch in str(display_name)),
msg=f"{column_name} display_name should be Chinese: {display_name!r}",
)
finally:
conn.close()
def test_timestamp_fields_type_default_and_editable(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
cur = conn.cursor()
for column_name in ("created_at", "updated_at"):
cur.execute(f"PRAGMA table_info('demo_items')")
info = {row[1]: row for row in cur.fetchall()}
row = info[column_name]
self.assertIn("INT", str(row[2]).upper())
self.assertIn("unixepoch", str(row[4]).lower())
cur.execute(
"""
SELECT editable, display_type FROM _jiangchang_columns
WHERE table_name = 'demo_items' AND column_name = ?
""",
(column_name,),
)
editable, display_type = cur.fetchone()
self.assertEqual(int(editable), 0)
self.assertEqual(display_type, "datetime_unix_seconds")
finally:
conn.close()
def test_physical_order_matches_pragma_cid(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
from db.display_metadata import list_physical_column_names
cur = conn.cursor()
expected = [str(item["column_name"]) for item in DEMO_ITEMS_COLUMN_METADATA]
physical = list_physical_column_names(cur, "demo_items")
self.assertEqual(physical, expected)
cur.execute("PRAGMA table_info('demo_items')")
cid_order = [row[1] for row in sorted(cur.fetchall(), key=lambda row: int(row[0]))]
self.assertEqual(cid_order, expected)
finally:
conn.close()
def test_options_json_parses_for_enum_field(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
cur = conn.cursor()
cur.execute(
"""
SELECT options_json FROM _jiangchang_columns
WHERE table_name = 'demo_items' AND column_name = 'status'
"""
)
raw = cur.fetchone()[0]
from db.display_metadata_validator import validate_options_json
errors, _ = validate_options_json(raw)
self.assertEqual(errors, [])
parsed = json.loads(raw)
self.assertEqual(len(parsed), 3)
self.assertEqual(parsed[0]["value"], "draft")
finally:
conn.close()
def test_system_table_no_editable_fields_not_manual_create(self) -> None:
conn = sqlite3.connect(":memory:")
try:
from db.display_metadata import (
init_display_metadata_tables,
seed_column_metadata_batch,
table_allows_manual_create,
upsert_table_metadata,
)
cur = conn.cursor()
init_display_metadata_tables(cur)
cur.execute(
"""
CREATE TABLE source_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL,
synced_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
)
upsert_table_metadata(
cur,
table_name="source_records",
display_name="来源记录",
description="只由同步流程写入",
visible=1,
readonly=0,
)
seed_column_metadata_batch(
cur,
table_name="source_records",
columns=(
{"column_name": "id", "display_name": "编号", "editable": 0},
{"column_name": "item_id", "display_name": "记录编号", "editable": 0},
{"column_name": "synced_at", "display_name": "同步时间", "editable": 0, "display_type": "datetime_unix_seconds"},
),
)
conn.commit()
self.assertFalse(table_allows_manual_create(cur, "source_records"))
finally:
conn.close()
def test_table_readonly_blocks_manual_create_even_with_editable_fields(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute("UPDATE _jiangchang_tables SET readonly = 1 WHERE table_name = 'demo_items'")
conn.commit()
from db.display_metadata import table_allows_manual_create
self.assertFalse(table_allows_manual_create(conn.cursor(), "demo_items"))
finally:
conn.close()
def test_prune_stale_column_metadata(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
conn.execute(
"""
INSERT INTO _jiangchang_columns
(table_name, column_name, display_name, visible, searchable, editable)
VALUES ('demo_items', 'removed_field', '已删除', 0, 0, 0)
"""
)
conn.commit()
from db.display_metadata import list_physical_column_names, seed_column_metadata_batch
cur = conn.cursor()
physical = list_physical_column_names(cur, "demo_items")
seed_column_metadata_batch(
cur,
table_name="demo_items",
columns=DEMO_ITEMS_COLUMN_METADATA,
valid_column_names=physical,
)
conn.commit()
cur.execute(
"SELECT 1 FROM _jiangchang_columns WHERE table_name = 'demo_items' AND column_name = 'removed_field'"
)
self.assertIsNone(cur.fetchone())
finally:
conn.close()
def test_seed_batch_without_valid_column_names_does_not_prune(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn, prune=False)
conn.execute(
"""
INSERT INTO _jiangchang_columns
(table_name, column_name, display_name, visible, searchable, editable)
VALUES ('demo_items', 'removed_field', '已删除', 0, 0, 0)
"""
)
conn.commit()
from db.display_metadata import seed_column_metadata_batch
seed_column_metadata_batch(
conn.cursor(),
table_name="demo_items",
columns=DEMO_ITEMS_COLUMN_METADATA,
)
conn.commit()
cur = conn.cursor()
cur.execute(
"SELECT 1 FROM _jiangchang_columns WHERE table_name = 'demo_items' AND column_name = 'removed_field'"
)
self.assertIsNotNone(cur.fetchone())
finally:
conn.close()
def test_seed_batch_accepts_list_columns(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn, prune=False)
from db.display_metadata import get_column_editable, seed_column_metadata_batch
columns = list(DEMO_ITEMS_COLUMN_METADATA)
columns[1] = {**columns[1], "display_name": "标题(列表)"}
seed_column_metadata_batch(
conn.cursor(),
table_name="demo_items",
columns=columns,
valid_column_names=[str(item["column_name"]) for item in DEMO_ITEMS_COLUMN_METADATA],
)
conn.commit()
cur = conn.cursor()
cur.execute(
"SELECT display_name FROM _jiangchang_columns WHERE table_name = 'demo_items' AND column_name = 'title'"
)
self.assertEqual(cur.fetchone()[0], "标题(列表)")
self.assertEqual(get_column_editable(cur, "demo_items", "title"), 1)
finally:
conn.close()
def test_prune_does_not_remove_existing_column_metadata(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
from db.display_metadata import get_column_editable, seed_column_metadata_batch
cur = conn.cursor()
before = get_column_editable(cur, "demo_items", "title")
seed_column_metadata_batch(
cur,
table_name="demo_items",
columns=DEMO_ITEMS_COLUMN_METADATA,
valid_column_names=[str(item["column_name"]) for item in DEMO_ITEMS_COLUMN_METADATA],
)
conn.commit()
self.assertEqual(get_column_editable(cur, "demo_items", "title"), before)
finally:
conn.close()
def test_demo_items_validation_passes(self) -> None:
conn = sqlite3.connect(":memory:")
try:
_init_demo_items_fixture(conn)
from db.display_metadata_validator import validate_display_metadata
report = validate_display_metadata(conn)
self.assertTrue(report.ok, msg="; ".join(report.errors))
finally:
conn.close()
if __name__ == "__main__":
unittest.main()