chore: auto release commit (2026-07-14 11:56:45)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 两类样例文件
|
||||
## 样例文件
|
||||
|
||||
### 1. `test_service_contract.py.sample`
|
||||
|
||||
@@ -22,7 +22,17 @@
|
||||
- 需要验证:**成功路径**、**参数/校验错误**、**adapter 异常被转换为业务可读错误**。
|
||||
- 需要用 **`FakeAdapter`**(见 `tests/adapter_test_utils.py`)或自建 **stub** 替代 ERP、TMS、WMS、船司、报关、LLM、RPA 等,**不**发真实 HTTP、**不**开真实浏览器。
|
||||
|
||||
### 2. `test_golden_cases.py.sample`
|
||||
### 2. `test_sync_contract.py.sample`
|
||||
|
||||
**适合:** 外源 → 本地库的同步 / refresh / collect 能力。
|
||||
|
||||
至少应覆盖:首次插入、第二次幂等、更新、失效数据处理、不可信空结果保护 vs 完整空快照失效、遗漏 `snapshot_complete` 立即失败、部分写入后故障注入与回滚(`fail_after_writes`)、统计字段 `inserted/updated/unchanged/removed`、同步路径不调用 CSV/Excel 导出器。`snapshot_complete` 为调用方**必须显式传入**的 keyword-only 参数(无默认值),禁止默认把结果当成完整快照,也禁止靠条数猜测完整性。使用通用表名(如 `business_key`),不要写具体平台业务名。
|
||||
|
||||
### 3. `test_action_core_contract.py.sample`
|
||||
|
||||
**适合:** 校验「单业务内核、多入口」——task/CLI/兼容命令调用同一 domain service;不按 toolbar/cron/agent 来源改变业务结果。
|
||||
|
||||
### 4. `test_golden_cases.py.sample`
|
||||
|
||||
**适合:**
|
||||
|
||||
@@ -103,6 +113,8 @@
|
||||
| 用途 | 示例文件名 |
|
||||
|------|------------|
|
||||
| Service 契约 | `tests/test_service_contract.py` |
|
||||
| 同步契约 | `tests/test_sync_contract.py` |
|
||||
| 单内核多入口 | `tests/test_action_core_contract.py` |
|
||||
| Golden / fixture 回归 | `tests/test_golden_cases.py` |
|
||||
| 领域规则 | `tests/test_<domain>_rules.py` |
|
||||
| 校验逻辑 | `tests/test_<domain>_validation.py` |
|
||||
|
||||
116
tests/samples/test_action_core_contract.py.sample
Normal file
116
tests/samples/test_action_core_contract.py.sample
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 样例:复制到 ``tests/test_action_core_contract.py`` 后按实际模块改名运行。
|
||||
# 默认不会被 ``python tests/run_tests.py`` 收集(扩展名为 .sample)。
|
||||
"""
|
||||
单业务内核、多入口契约范式(通用)。
|
||||
|
||||
覆盖:
|
||||
- task / command adapter 调用唯一 domain service
|
||||
- 兼容命令复用同一个核心函数
|
||||
- CLI handler 不复制业务逻辑
|
||||
- 不根据 toolbar / cron / agent 来源改变业务处理
|
||||
|
||||
复制后把示意函数换成真实 import;禁止按入口维护多套实现。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class DomainService:
|
||||
"""唯一业务内核示意。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
def sync_records(self, *, limit: int = 100) -> dict[str, Any]:
|
||||
self.calls.append({"op": "sync_records", "limit": limit})
|
||||
return {"ok": True, "limit": limit, "inserted": 1}
|
||||
|
||||
|
||||
def cmd_sync_records(domain: DomainService, *, limit: int = 100) -> dict[str, Any]:
|
||||
"""task_service 编排层:可加 job_context,但不按 source 分叉业务。"""
|
||||
return domain.sync_records(limit=limit)
|
||||
|
||||
|
||||
def cmd_export_compat_refresh(domain: DomainService, *, limit: int = 100) -> dict[str, Any]:
|
||||
"""兼容旧 export:刷新必须复用同一 sync 内核,禁止另写采集逻辑。"""
|
||||
sync_result = domain.sync_records(limit=limit)
|
||||
return {"export": "delegated-to-host-or-report", "sync": sync_result}
|
||||
|
||||
|
||||
def cli_handle_sync(domain: DomainService, argv_limit: int) -> dict[str, Any]:
|
||||
"""CLI handler:只解析参数后转发,不内嵌业务。"""
|
||||
return cmd_sync_records(domain, limit=argv_limit)
|
||||
|
||||
|
||||
def run_from_source(
|
||||
domain: DomainService,
|
||||
*,
|
||||
source: str,
|
||||
limit: int = 10,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
宿主可能传入 source=toolbar|cron|agent|skill-detail。
|
||||
正确实现:忽略 source,业务结果一致。
|
||||
"""
|
||||
_ = source # 不得用于分支业务语义
|
||||
return cmd_sync_records(domain, limit=limit)
|
||||
|
||||
|
||||
class TestActionCoreContractSample(unittest.TestCase):
|
||||
def test_task_adapter_calls_single_domain_service(self) -> None:
|
||||
domain = DomainService()
|
||||
out = cmd_sync_records(domain, limit=5)
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(domain.calls, [{"op": "sync_records", "limit": 5}])
|
||||
|
||||
def test_compat_command_reuses_same_core(self) -> None:
|
||||
domain = DomainService()
|
||||
cmd_export_compat_refresh(domain, limit=3)
|
||||
self.assertEqual(len(domain.calls), 1)
|
||||
self.assertEqual(domain.calls[0]["op"], "sync_records")
|
||||
|
||||
def test_cli_handler_does_not_duplicate_business_logic(self) -> None:
|
||||
domain = DomainService()
|
||||
# CLI 与 task 编排应落到同一函数对象(示意)
|
||||
self.assertIs(cli_handle_sync.__wrapped__ if hasattr(cli_handle_sync, "__wrapped__") else None, None)
|
||||
out = cli_handle_sync(domain, 7)
|
||||
self.assertEqual(out["limit"], 7)
|
||||
self.assertEqual(domain.calls[-1], {"op": "sync_records", "limit": 7})
|
||||
|
||||
# 强化:CLI 路径与 cmd 路径结果一致
|
||||
domain2 = DomainService()
|
||||
a = cli_handle_sync(domain2, 2)
|
||||
domain3 = DomainService()
|
||||
b = cmd_sync_records(domain3, limit=2)
|
||||
self.assertEqual(a, b)
|
||||
|
||||
def test_business_result_independent_of_entry_source(self) -> None:
|
||||
domain = DomainService()
|
||||
results = [
|
||||
run_from_source(domain, source=src, limit=4)
|
||||
for src in ("toolbar", "cron", "agent", "skill-detail")
|
||||
]
|
||||
self.assertEqual(results[0], results[1])
|
||||
self.assertEqual(results[1], results[2])
|
||||
self.assertEqual(results[2], results[3])
|
||||
self.assertEqual(len(domain.calls), 4)
|
||||
self.assertTrue(all(c["op"] == "sync_records" and c["limit"] == 4 for c in domain.calls))
|
||||
|
||||
def test_entrypoint_wiring_points_to_same_callable(self) -> None:
|
||||
"""示意:manifest 多 placements 应对应同一 Python 入口函数。"""
|
||||
domain = DomainService()
|
||||
handlers: dict[str, Callable[..., dict[str, Any]]] = {
|
||||
"toolbar": lambda: cmd_sync_records(domain, limit=1),
|
||||
"cron": lambda: cmd_sync_records(domain, limit=1),
|
||||
"agent": lambda: cmd_sync_records(domain, limit=1),
|
||||
}
|
||||
outs = [handlers[k]() for k in handlers]
|
||||
self.assertEqual(outs[0], outs[1])
|
||||
self.assertEqual(outs[1], outs[2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
274
tests/samples/test_sync_contract.py.sample
Normal file
274
tests/samples/test_sync_contract.py.sample
Normal file
@@ -0,0 +1,274 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 样例:复制到 ``tests/test_sync_contract.py`` 后按实际 domain service 改名运行。
|
||||
# 默认不会被 ``python tests/run_tests.py`` 收集(扩展名为 .sample)。
|
||||
"""
|
||||
外源 → 本地库同步契约范式(通用,无具体平台业务名)。
|
||||
|
||||
覆盖:
|
||||
- 首次同步插入
|
||||
- 第二次同步幂等
|
||||
- 更新已有数据
|
||||
- 处理失效数据
|
||||
- 不可信空结果保护 vs 完整空快照失效(``snapshot_complete`` 必填,无默认值)
|
||||
- 遗漏 ``snapshot_complete`` 立即失败(TypeError)
|
||||
- 事务在部分写入后失败并回滚
|
||||
- 返回 inserted/updated/unchanged/removed
|
||||
- sync Action 不调用 CSV/Excel 文件导出器
|
||||
|
||||
复制后把内存仓库与 stub 外源换成真实 repository / reader;禁止访问真实网络或用户数据目录。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncStats:
|
||||
total: int = 0
|
||||
inserted: int = 0
|
||||
updated: int = 0
|
||||
unchanged: int = 0
|
||||
removed: int = 0
|
||||
|
||||
def as_dict(self) -> dict[str, int]:
|
||||
return {
|
||||
"total": self.total,
|
||||
"inserted": self.inserted,
|
||||
"updated": self.updated,
|
||||
"unchanged": self.unchanged,
|
||||
"removed": self.removed,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryRecordStore:
|
||||
"""内存库:演示幂等 upsert + 全量失效(is_active 标记)。"""
|
||||
|
||||
rows: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
in_transaction: bool = False
|
||||
_tx_snapshot: dict[str, dict[str, Any]] | None = None
|
||||
|
||||
def begin(self) -> None:
|
||||
self._tx_snapshot = {k: dict(v) for k, v in self.rows.items()}
|
||||
self.in_transaction = True
|
||||
|
||||
def commit(self) -> None:
|
||||
self._tx_snapshot = None
|
||||
self.in_transaction = False
|
||||
|
||||
def rollback(self) -> None:
|
||||
if self._tx_snapshot is not None:
|
||||
self.rows = {k: dict(v) for k, v in self._tx_snapshot.items()}
|
||||
self._tx_snapshot = None
|
||||
self.in_transaction = False
|
||||
|
||||
def upsert_snapshot(
|
||||
self,
|
||||
records: list[dict[str, Any]],
|
||||
*,
|
||||
fail_after_writes: int | None = None,
|
||||
) -> SyncStats:
|
||||
"""全量快照:按 business_key 对齐;缺失标 is_active=0。
|
||||
|
||||
``fail_after_writes``:在成功完成该次数的 insert/update 之后抛错,用于验证回滚。
|
||||
"""
|
||||
self.begin()
|
||||
try:
|
||||
stats = SyncStats(total=len(records))
|
||||
seen: set[str] = set()
|
||||
write_count = 0
|
||||
for rec in records:
|
||||
key = str(rec["business_key"])
|
||||
seen.add(key)
|
||||
existing = self.rows.get(key)
|
||||
payload = {
|
||||
"business_key": key,
|
||||
"title": rec["title"],
|
||||
"is_active": 1,
|
||||
}
|
||||
if existing is None:
|
||||
self.rows[key] = payload
|
||||
stats.inserted += 1
|
||||
write_count += 1
|
||||
elif existing.get("title") == payload["title"] and existing.get("is_active") == 1:
|
||||
stats.unchanged += 1
|
||||
else:
|
||||
self.rows[key] = payload
|
||||
stats.updated += 1
|
||||
write_count += 1
|
||||
if fail_after_writes is not None and write_count >= fail_after_writes:
|
||||
raise RuntimeError(
|
||||
f"simulated write failure after {write_count} insert/update write(s)"
|
||||
)
|
||||
for key, existing in list(self.rows.items()):
|
||||
if key not in seen and existing.get("is_active") == 1:
|
||||
existing["is_active"] = 0
|
||||
stats.removed += 1
|
||||
self.commit()
|
||||
return stats
|
||||
except Exception:
|
||||
self.rollback()
|
||||
raise
|
||||
|
||||
|
||||
class ExportSpy:
|
||||
"""若 sync 路径误调导出器,测试应失败。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[str] = []
|
||||
|
||||
def write_csv(self, *_a: Any, **_k: Any) -> None:
|
||||
self.calls.append("csv")
|
||||
|
||||
def write_excel(self, *_a: Any, **_k: Any) -> None:
|
||||
self.calls.append("excel")
|
||||
|
||||
|
||||
def sync_records(
|
||||
store: MemoryRecordStore,
|
||||
external_rows: list[dict[str, Any]],
|
||||
*,
|
||||
snapshot_complete: bool,
|
||||
exporter: ExportSpy | None = None,
|
||||
fail_after_writes: int | None = None,
|
||||
) -> dict[str, int]:
|
||||
"""领域同步内核示意:只写库,不导出文件。
|
||||
|
||||
``snapshot_complete`` 为 keyword-only **必填**参数(无默认值):
|
||||
- False:读取失败/分页不完整/不可信结果 → 不得清库或失效旧数据
|
||||
- True:外源完整快照(可为空)→ 允许按全量策略 upsert / 标记失效
|
||||
调用方必须显式传入,禁止默认当作完整快照,也禁止靠条数猜测完整性。
|
||||
"""
|
||||
if external_rows is None:
|
||||
raise ValueError("external_rows required")
|
||||
if not snapshot_complete:
|
||||
active = sum(1 for r in store.rows.values() if r.get("is_active") == 1)
|
||||
return SyncStats(total=0, unchanged=active).as_dict()
|
||||
stats = store.upsert_snapshot(external_rows, fail_after_writes=fail_after_writes)
|
||||
if exporter is not None:
|
||||
# 正确实现不得调用;本示意刻意不调用,供测试断言
|
||||
pass
|
||||
return stats.as_dict()
|
||||
|
||||
|
||||
class TestSyncContractSample(unittest.TestCase):
|
||||
def test_first_sync_inserts(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
stats = sync_records(
|
||||
store,
|
||||
[{"business_key": "a", "title": "A"}, {"business_key": "b", "title": "B"}],
|
||||
snapshot_complete=True,
|
||||
)
|
||||
self.assertEqual(stats["inserted"], 2)
|
||||
self.assertEqual(stats["total"], 2)
|
||||
self.assertEqual(len(store.rows), 2)
|
||||
|
||||
def test_second_sync_is_idempotent(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
rows = [{"business_key": "a", "title": "A"}]
|
||||
sync_records(store, rows, snapshot_complete=True)
|
||||
stats = sync_records(store, rows, snapshot_complete=True)
|
||||
self.assertEqual(stats["unchanged"], 1)
|
||||
self.assertEqual(stats["inserted"], 0)
|
||||
self.assertEqual(stats["updated"], 0)
|
||||
|
||||
def test_updates_existing_row(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
sync_records(store, [{"business_key": "a", "title": "A"}], snapshot_complete=True)
|
||||
stats = sync_records(
|
||||
store, [{"business_key": "a", "title": "A2"}], snapshot_complete=True
|
||||
)
|
||||
self.assertEqual(stats["updated"], 1)
|
||||
self.assertEqual(store.rows["a"]["title"], "A2")
|
||||
|
||||
def test_marks_removed_inactive_on_full_sync(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
sync_records(
|
||||
store,
|
||||
[{"business_key": "a", "title": "A"}, {"business_key": "b", "title": "B"}],
|
||||
snapshot_complete=True,
|
||||
)
|
||||
stats = sync_records(
|
||||
store, [{"business_key": "a", "title": "A"}], snapshot_complete=True
|
||||
)
|
||||
self.assertEqual(stats["removed"], 1)
|
||||
self.assertEqual(store.rows["b"]["is_active"], 0)
|
||||
self.assertEqual(store.rows["a"]["is_active"], 1)
|
||||
|
||||
def test_incomplete_empty_snapshot_does_not_wipe_existing(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
sync_records(store, [{"business_key": "a", "title": "A"}], snapshot_complete=True)
|
||||
stats = sync_records(store, [], snapshot_complete=False)
|
||||
self.assertEqual(store.rows["a"]["is_active"], 1)
|
||||
self.assertEqual(stats["removed"], 0)
|
||||
self.assertEqual(stats["unchanged"], 1)
|
||||
|
||||
def test_complete_empty_snapshot_marks_existing_inactive(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
sync_records(
|
||||
store,
|
||||
[{"business_key": "a", "title": "A"}, {"business_key": "b", "title": "B"}],
|
||||
snapshot_complete=True,
|
||||
)
|
||||
stats = sync_records(store, [], snapshot_complete=True)
|
||||
self.assertEqual(stats["removed"], 2)
|
||||
self.assertEqual(store.rows["a"]["is_active"], 0)
|
||||
self.assertEqual(store.rows["b"]["is_active"], 0)
|
||||
|
||||
def test_omitting_snapshot_complete_raises_type_error(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
with self.assertRaises(TypeError):
|
||||
sync_records(store, [{"business_key": "a", "title": "A"}]) # type: ignore[call-arg]
|
||||
|
||||
def test_transaction_failure_after_partial_writes_rolls_back(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
sync_records(store, [{"business_key": "a", "title": "A"}], snapshot_complete=True)
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
sync_records(
|
||||
store,
|
||||
[
|
||||
{"business_key": "a", "title": "A2"},
|
||||
{"business_key": "b", "title": "B"},
|
||||
],
|
||||
snapshot_complete=True,
|
||||
fail_after_writes=1,
|
||||
)
|
||||
self.assertIn("after 1", str(ctx.exception))
|
||||
self.assertEqual(store.rows["a"]["title"], "A")
|
||||
self.assertNotIn("b", store.rows)
|
||||
self.assertFalse(store.in_transaction)
|
||||
self.assertIsNone(store._tx_snapshot)
|
||||
|
||||
def test_stats_keys_cover_inserted_updated_unchanged_removed(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
sync_records(
|
||||
store,
|
||||
[{"business_key": "a", "title": "A"}, {"business_key": "b", "title": "B"}],
|
||||
snapshot_complete=True,
|
||||
)
|
||||
stats = sync_records(
|
||||
store,
|
||||
[{"business_key": "a", "title": "A2"}],
|
||||
snapshot_complete=True,
|
||||
)
|
||||
for key in ("total", "inserted", "updated", "unchanged", "removed"):
|
||||
self.assertIn(key, stats)
|
||||
self.assertEqual(stats["updated"], 1)
|
||||
self.assertEqual(stats["removed"], 1)
|
||||
|
||||
def test_sync_does_not_call_file_exporters(self) -> None:
|
||||
store = MemoryRecordStore()
|
||||
spy = ExportSpy()
|
||||
sync_records(
|
||||
store,
|
||||
[{"business_key": "a", "title": "A"}],
|
||||
snapshot_complete=True,
|
||||
exporter=spy,
|
||||
)
|
||||
self.assertEqual(spy.calls, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user