mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 02:56:17 +00:00
Adopts the layout feedback landed in cb49dd67: secondary adapters live under `app/adapters/<context>/`, one file per port, the file named after the port in snake_case with the technology carried by the class name. `app/infra/` is now gone entirely. The rename also separates two meanings of "run" that shared one filename space: `run_sql.py` held `ScheduledRun` (an execution record), while the `run_launcher.py` still to come deals in Gateway runs. task_sql.py -> scheduled_task_repository.py run_sql.py -> scheduled_run_repository.py spec_mapping.py -> spec_mapping.py (implements no port, so no rename) Both SQL adapters now carry the `Secondary adapter (owned persistence)` docstring marker -- this context owns both tables and writes its own queries. `spec_mapping` says instead that it is a boundary mapping and names its two callers. The package `__init__.py` is empty, so imports go through the full path and a class's home file stays unambiguous. Pure move: every top-level symbol was compared against its pre-move original by AST dump (docstrings excluded), plus a separate per-class method-name comparison, since a whole-class dump reports a docstring edit and a renamed method the same way. Also repoints three docstrings and one diagram that still named the deleted `app/infra/` path.
79 lines
3.7 KiB
Python
79 lines
3.7 KiB
Python
"""Boundary tests for the schedule_spec mapping.
|
|
|
|
This is where a `Mapping[str, Any]` is allowed to exist. The split it enforces
|
|
is the point: **structural** problems (key missing, wrong type, unknown
|
|
schedule type) are caught here, **value** problems (5-field cron, resolvable
|
|
timezone) are left to `ScheduleSpec.__post_init__` -- and both surface as the
|
|
same domain error, so the router maps one family.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
|
|
from app.adapters.schedule.spec_mapping import spec_to_domain, spec_to_wire
|
|
from deerflow.domain.schedule.model import InvalidScheduleError, ScheduleSpec, ScheduleType
|
|
|
|
|
|
class TestStructuralChecks:
|
|
def test_unknown_schedule_type_is_rejected(self):
|
|
"""The one rule the domain cannot state: ScheduleSpec only accepts the
|
|
enum, so a bad string has to be caught at the boundary."""
|
|
with pytest.raises(InvalidScheduleError, match="Unsupported schedule_type"):
|
|
spec_to_domain("teleport", {"cron": "0 9 * * *"}, "UTC")
|
|
|
|
@pytest.mark.parametrize("spec", [{}, None, {"cron": 5}, {"run_at": "..."}])
|
|
def test_cron_without_a_string_cron_is_rejected(self, spec):
|
|
with pytest.raises(InvalidScheduleError, match="requires schedule_spec"):
|
|
spec_to_domain("cron", spec, "UTC")
|
|
|
|
@pytest.mark.parametrize("spec", [{}, None, {"run_at": 5}, {"cron": "0 9 * * *"}])
|
|
def test_once_without_a_string_run_at_is_rejected(self, spec):
|
|
with pytest.raises(InvalidScheduleError, match="requires run_at"):
|
|
spec_to_domain("once", spec, "UTC")
|
|
|
|
def test_an_unparseable_run_at_is_rejected(self):
|
|
with pytest.raises(InvalidScheduleError, match="unparseable run_at"):
|
|
spec_to_domain("once", {"run_at": "next tuesday"}, "UTC")
|
|
|
|
|
|
class TestValueChecksStayInTheDomain:
|
|
"""These are not re-implemented here -- they arrive from __post_init__,
|
|
which is why the boundary can stay thin."""
|
|
|
|
def test_a_bad_cron_expression_still_raises(self):
|
|
with pytest.raises(InvalidScheduleError, match="exactly 5 fields"):
|
|
spec_to_domain("cron", {"cron": "0 9 * *"}, "UTC")
|
|
|
|
def test_a_bad_timezone_still_raises(self):
|
|
with pytest.raises(InvalidScheduleError, match="Unknown timezone"):
|
|
spec_to_domain("cron", {"cron": "0 9 * * *"}, "Mars/Olympus_Mons")
|
|
|
|
|
|
class TestRoundTrip:
|
|
def test_cron_round_trips_normalized(self):
|
|
spec = spec_to_domain("cron", {"cron": " 0 9 * * * "}, "Asia/Shanghai")
|
|
assert spec.schedule_type is ScheduleType.CRON
|
|
assert spec_to_wire(spec) == {"cron": "0 9 * * *"}
|
|
assert spec_to_domain("cron", spec_to_wire(spec), "Asia/Shanghai") == spec
|
|
|
|
def test_once_round_trips_to_the_same_instant(self):
|
|
spec = spec_to_domain("once", {"run_at": "2026-08-01T09:00:00"}, "Asia/Shanghai")
|
|
assert spec.run_at == datetime(2026, 8, 1, 1, 0, tzinfo=UTC)
|
|
assert spec_to_domain("once", spec_to_wire(spec), "Asia/Shanghai") == spec
|
|
|
|
def test_a_trailing_z_input_parses_and_re_emits_as_an_offset(self):
|
|
"""The shape the frontend actually submits (`zonedLocalToUtcIso`).
|
|
Both spellings parse on either side, so normalizing is deliberate."""
|
|
spec = spec_to_domain("once", {"run_at": "2026-08-01T01:00:00Z"}, "Asia/Shanghai")
|
|
emitted = spec_to_wire(spec)["run_at"]
|
|
assert emitted.endswith("+00:00")
|
|
assert spec_to_domain("once", {"run_at": emitted}, "Asia/Shanghai") == spec
|
|
|
|
def test_wire_output_is_idempotent(self):
|
|
spec = ScheduleSpec.once_at(datetime(2026, 8, 1, 9, 0, tzinfo=UTC), "UTC")
|
|
once = spec_to_wire(spec)
|
|
assert spec_to_wire(spec_to_domain("once", once, "UTC")) == once
|