first commit

This commit is contained in:
strepsiades
2026-03-09 16:31:42 +08:00
commit 4a9c444b10
486 changed files with 52479 additions and 0 deletions
@@ -0,0 +1,357 @@
from __future__ import annotations
from typing import Any, Dict, List
import pytest
from pydantic import BaseModel
from sync_state_machine.common.collection import DataCollection
from sync_state_machine.common.sync_node import SyncNode
from sync_state_machine.domain.project.api_handler import ProjectApiHandler
from sync_state_machine.domain.project_detail.api_handler import ProjectDetailApiHandler
from sync_state_machine.domain.project_detail.sync_node import ProjectDetailSyncNode
from sync_state_machine.datasource.task_result import TaskStatus
class _ProjectSchema(BaseModel):
id: str
class _ProjectNode(SyncNode[_ProjectSchema]):
node_type = "project"
schema = _ProjectSchema
class _MockApiClient:
def __init__(self):
self.requests: List[Dict[str, Any]] = []
self.push_logs: Dict[str, Dict[str, Any]] = {}
self.all_info_calls: List[str] = []
async def request(self, method: str, endpoint: str, params: Dict[str, Any] | None = None, data: Dict[str, Any] | None = None, **kwargs):
if method == "GET" and endpoint == "/project/all_info":
project_id = (params or {}).get("project_id")
if project_id:
self.all_info_calls.append(str(project_id))
self.requests.append(
{
"method": method,
"endpoint": endpoint,
"params": params,
"data": data,
}
)
return {"code": 200, "data": {"biz_id": (data or {}).get("biz_id")}}
async def get_push_logs(self, task_ids: List[str]) -> Dict[str, Dict[str, Any]]:
return {task_id: self.push_logs.get(task_id, {}) for task_id in task_ids}
def _build_project_node(*, node_id: str, project_id: str, all_info: Dict[str, Any]) -> _ProjectNode:
return _ProjectNode(
node_id=node_id,
data_id=project_id,
data={"id": project_id},
context={"all_info": all_info},
)
def _build_project_detail_node(*, node_id: str, data_id: str, project_id: str, key: str, capacity: float = 12.3) -> ProjectDetailSyncNode:
payload = {
"id": data_id,
"project_id": project_id,
"key": key,
"value": [{"date": "2026-01-01", "capacity": capacity}],
}
return ProjectDetailSyncNode(node_id=node_id, data_id=data_id, data=payload)
def _patch_project_all_info_lookup(
monkeypatch: pytest.MonkeyPatch,
*,
collection: DataCollection,
call_recorder: List[str] | None = None,
) -> None:
async def _fake_get_all_info(cls, _client, project_id: str):
if call_recorder is not None:
call_recorder.append(project_id)
node = collection.get_by_data_id("project", project_id)
assert node is not None
return node.context.get("all_info", {})
monkeypatch.setattr(ProjectApiHandler, "get_all_info", classmethod(_fake_get_all_info))
@pytest.mark.asyncio
async def test_create_all_uses_project_update_endpoint_and_returns_in_progress() -> None:
collection = DataCollection("remote")
project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
await collection.add(project)
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
handler.api_client = _MockApiClient()
node = _build_project_detail_node(
node_id="detail-node-1",
data_id="local-detail-1",
project_id="project-1",
key="plan_construction",
)
results = await handler.create_all([node])
assert len(results) == 1
assert results[0].status == TaskStatus.IN_PROGRESS
assert isinstance(results[0].task_id, str) and results[0].task_id
req = handler.api_client.requests[-1]
assert req["method"] == "PUT"
assert req["endpoint"] == "/project/plan_construction"
@pytest.mark.asyncio
async def test_poll_tasks_backfills_real_data_id_from_project_all_info(monkeypatch: pytest.MonkeyPatch) -> None:
collection = DataCollection("remote")
project = _build_project_node(
node_id="p-node",
project_id="project-1",
all_info={
"project_detail": [
{
"id": "remote-detail-1",
"project_id": "project-1",
"key": "plan_construction",
"value": [{"date": "2026-01-01", "capacity": 13.0}],
}
]
},
)
await collection.add(project)
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
handler.api_client = _MockApiClient()
_patch_project_all_info_lookup(monkeypatch, collection=collection)
node = _build_project_detail_node(
node_id="detail-node-1",
data_id="local-detail-1",
project_id="project-1",
key="plan_construction",
)
create_result = (await handler.create_all([node]))[0]
assert create_result.task_id
handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}
polled = await handler.poll_tasks([create_result.task_id])
final = polled[create_result.task_id]
assert final.status == TaskStatus.SUCCESS
assert final.data_id == "remote-detail-1"
@pytest.mark.asyncio
async def test_poll_tasks_fails_when_all_info_missing_detail(monkeypatch: pytest.MonkeyPatch) -> None:
collection = DataCollection("remote")
project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
await collection.add(project)
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
handler.api_client = _MockApiClient()
_patch_project_all_info_lookup(monkeypatch, collection=collection)
node = _build_project_detail_node(
node_id="detail-node-1",
data_id="local-detail-1",
project_id="project-1",
key="plan_construction",
)
create_result = (await handler.create_all([node]))[0]
assert create_result.task_id
handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}
polled = await handler.poll_tasks([create_result.task_id])
final = polled[create_result.task_id]
assert final.status == TaskStatus.FAILED
assert "project_detail not found in all_info" in (final.error or "")
@pytest.mark.asyncio
async def test_poll_tasks_fails_when_project_detail_has_duplicate_same_key(monkeypatch: pytest.MonkeyPatch) -> None:
collection = DataCollection("remote")
project = _build_project_node(
node_id="p-node",
project_id="project-1",
all_info={
"project_detail": [
{
"id": "remote-detail-1",
"project_id": "project-1",
"key": "plan_construction",
"value": [{"date": "2026-01-01", "capacity": 13.0}],
},
{
"id": "remote-detail-2",
"project_id": "project-1",
"key": "plan_construction",
"value": [{"date": "2026-01-01", "capacity": 14.0}],
},
]
},
)
await collection.add(project)
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
handler.api_client = _MockApiClient()
_patch_project_all_info_lookup(monkeypatch, collection=collection)
node = _build_project_detail_node(
node_id="detail-node-1",
data_id="local-detail-1",
project_id="project-1",
key="plan_construction",
)
create_result = (await handler.create_all([node]))[0]
assert create_result.task_id
handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}
polled = await handler.poll_tasks([create_result.task_id])
final = polled[create_result.task_id]
assert final.status == TaskStatus.FAILED
assert "duplicated" in (final.error or "")
@pytest.mark.asyncio
async def test_create_all_marks_duplicate_project_key_in_same_batch_as_error() -> None:
collection = DataCollection("remote")
project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
await collection.add(project)
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
handler.api_client = _MockApiClient()
node1 = _build_project_detail_node(
node_id="detail-node-1",
data_id="local-detail-1",
project_id="project-1",
key="plan_construction",
capacity=10.0,
)
node2 = _build_project_detail_node(
node_id="detail-node-2",
data_id="local-detail-2",
project_id="project-1",
key="plan_construction",
capacity=20.0,
)
results = await handler.create_all([node1, node2])
assert results[0].status == TaskStatus.IN_PROGRESS
assert results[1].status == TaskStatus.FAILED
assert "Duplicate project_detail_data" in (results[1].error or "")
@pytest.mark.asyncio
async def test_create_node_reuses_existing_node_by_project_key_and_rebinds_data_id() -> None:
collection = DataCollection("remote")
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
handler.api_client = _MockApiClient()
existing = _build_project_detail_node(
node_id="detail-node-old",
data_id="virtual:project_detail_data:project-1:plan_construction",
project_id="project-1",
key="plan_construction",
)
await collection.add(existing)
created = handler._create_node(
{
"id": "remote-detail-99",
"project_id": "project-1",
"key": "plan_construction",
"value": [{"date": "2026-01-01", "capacity": 21.0}],
}
)
assert created.node_id == "detail-node-old"
assert created.data_id == "remote-detail-99"
@pytest.mark.asyncio
async def test_poll_tasks_queries_all_info_once_per_project_for_multiple_keys(monkeypatch: pytest.MonkeyPatch) -> None:
collection = DataCollection("remote")
project = _build_project_node(
node_id="p-node",
project_id="project-1",
all_info={"project_detail": []},
)
await collection.add(project)
handler = ProjectDetailApiHandler()
handler.set_collection(collection)
client = _MockApiClient()
handler.api_client = client
node1 = _build_project_detail_node(
node_id="detail-node-1",
data_id="local-detail-1",
project_id="project-1",
key="plan_construction",
)
node2 = _build_project_detail_node(
node_id="detail-node-2",
data_id="local-detail-2",
project_id="project-1",
key="actual_construction",
)
create_results = await handler.create_all([node1, node2])
task_ids = [r.task_id for r in create_results if r.task_id]
assert len(task_ids) == 2
client.push_logs[task_ids[0]] = {"code": 200, "data": {"biz_id": "project-1"}}
client.push_logs[task_ids[1]] = {"code": 200, "data": {"biz_id": "project-1"}}
project.context["all_info"] = {
"project_detail": [
{
"id": "remote-detail-1",
"project_id": "project-1",
"key": "plan_construction",
"value": [{"date": "2026-01-01", "capacity": 11.0}],
},
{
"id": "remote-detail-2",
"project_id": "project-1",
"key": "actual_construction",
"value": [{"date": "2026-01-01", "capacity": 22.0}],
},
]
}
_patch_project_all_info_lookup(
monkeypatch,
collection=collection,
call_recorder=client.all_info_calls,
)
polled = await handler.poll_tasks(task_ids)
assert polled[task_ids[0]].status == TaskStatus.SUCCESS
assert polled[task_ids[0]].data_id == "remote-detail-1"
assert polled[task_ids[1]].status == TaskStatus.SUCCESS
assert polled[task_ids[1]].data_id == "remote-detail-2"
assert client.all_info_calls == ["project-1"]