mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-04 11:58:36 +00:00
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fa0d709f29 |
refactor(schedule): turn the write use cases into commands
Add domain/schedule/commands.py with one frozen dataclass per HTTP-driven write use case -- CreateScheduledTask, UpdateScheduledTask, PauseTask, ResumeTask, DeleteTask, TriggerTask -- and make the service methods their handlers, keeping the naming chain aligned across all three spellings (command / handler / <Command>Request). The clock stays an explicit now= handler parameter: it is a rule input, not part of the client's intent. UpdateScheduledTask expresses partial updates with an UNSET sentinel, so absence is unambiguous; the wire keeps its historical None-means-omitted convention and the request model's to_command owns the translation. The former ContextChange moves from service.py into commands.py unchanged. The request models are renamed to <Command>Request and own transformation ① (to_command): identity is injected server-side and pinned by a test to never appear on the wire models. Clock- and callback-driven writes (run_once, dispatch_task, handle_run_completion, reconcile_on_startup) deliberately stay plain methods -- those drivers have no wire shape to translate, their inputs are already domain vocabulary. The context package now also exports the commands and the service, which completes its public API and retires the stale 'service not landed' note. |
||
|
|
680f044e58 |
refactor(schedule): move schedule_spec translation onto its owners
`spec_column.py` and `spec_wire.py` were the same 60 lines twice, kept in sync by a parity suite. Both are gone; what they did is now split along the line that actually separates the two boundaries. Why two files existed --------------------- They were one module until the slice split it, and the reason given for the split holds: a primary adapter must not import a secondary one, and the two shapes are equal only by coincidence. But that argument only requires the two *shapes* to be independent -- it does not require the parsing *rule* to be written twice, and writing it twice is what needed `test_schedule_spec_parity` to assert the two agreed, down to identical error text. Splitting it properly --------------------- `ScheduleSpec.from_primitives(schedule_type, *, cron, run_at, timezone)` takes four strings, not a `Mapping[str, Any]` -- the mapping was the thing that kept this out of the domain, and four strings carry no transport or storage format with them. It owns the whole rule: unknown type, missing or non-string field, unparseable `run_at`, and (via __post_init__, unchanged) 5-field cron and resolvable timezone. Values are checked rather than trusted, since both callers read data a client can influence. Each adapter keeps only what is genuinely its own -- which two keys its format uses -- as private methods on the class that owns the boundary, matching how `SqlFeedbackRepository` and AWS's own ports-and-adapters sample put the conversion inside the adapter rather than beside it: SqlScheduledTaskRepository._spec_from_row / _spec_to_column models.ScheduledTaskCreateRequest.to_schedule / models._spec_to_wire The emit direction stays duplicated, deliberately: it is three lines per side with no rule in it, and the two are *allowed* to diverge -- one is an HTTP contract, the other a storage format. Asserting they stay byte-identical was a constraint neither side asked for, so that suite is not replaced. The router stops building value objects --------------------------------------- `create` passes `body.to_schedule()`. `update` passes `body.to_schedule(current.schedule)`, replacing eight lines that re-emitted the current spec to the wire shape purely to read defaults back out of it; omitted parts now come off the value object directly. Tests ----- `test_schedule_spec_parity.py` is deleted (162 lines). Its structural and value cases moved to `TestFromPrimitives` in the domain suite -- stated once now instead of parametrized over two implementations. Emit coverage was already elsewhere: wire in `test_schedule_response_models`, column via the repository round-trip in `test_schedule_fakes`. One gap found while removing it: the `once` half of the update fallback had no coverage on either side (both existing cases use cron), and it is exactly the branch this commit rewrites -- `spec_to_wire(current)` round-trip to `current.run_at.isoformat()`. Added `test_a_timezone_change_on_a_once_task_keeps_the_same_instant`, and confirmed by mutation that it is the only case that catches that branch breaking. Behaviour is unchanged: same wire shapes, same normalizations (whitespace in cron, trailing Z re-emitted as +00:00), same error messages, so the 422 details clients see do not move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eb37df0779 |
refactor(schedule): wire the slice through a composition root
Switches production onto the hexagonal path. The legacy modules still compile and still have tests, but nothing assembles them any more; deleting them is the next commit, kept separate so it stays reviewable. Composition root ---------------- `app/composition.py::build_domain_services()` is now the only place an adapter is instantiated. It was extracted from `deps.py::langgraph_runtime` rather than added to it: wiring there was tangled with engine startup, orphan recovery and shutdown, so the one rule that governs it -- no SQL backend means no service and the routes answer 503 -- could not be tested without booting the whole application, and was held up by a single comment. It is a pure function of already-built infrastructure, so that rule is now an assertion. Feedback moved with it; doing this while adding schedule's five objects costs one change instead of two. Primary adapter --------------- The router is protocol translation only. What is gone is the giveaway: cron normalisation, `next_run_at` arithmetic, the re-arm rule and hand-written ownership checks all now live in the aggregate. Domain errors map to status codes through one table, so a new error surfaces as a 500 to be classified rather than being swallowed by whichever `except` was nearest. `spec_mapping` split in two (AWS's own layout puts the wire model under the entrypoint that owns it, and a primary adapter must not import a secondary one): `adapters/schedule/spec_column.py` for the JSON column, `routers/schedule/spec_wire.py` for the HTTP body. The two shapes are equal only by coincidence, so `test_schedule_spec_parity.py` runs every case against both and compares their outputs and messages directly. Function names differ per side so an import from the wrong one is visible. Explicit responses ------------------ Routes returned the ORM row's `to_dict()`, leaking `user_id`, `assistant_id`, `overlap_policy` and the two lease columns. The response models publish exactly the field set the frontend declares -- asserted in both directions, since an extra field is a leak and a missing one breaks a client. One wire detail was nearly changed by accident: Pydantic v2 serializes a UTC datetime as `...Z`, while the legacy `coerce_iso` path emitted `+00:00`. `UtcTimestamp` pins `isoformat()` so adopting a model does not silently alter the wire format for every client parsing these. Tests ----- 73 new cases: router behaviour driven through a real `ScheduleService` over in-memory fakes (a mocked service would let the error mapping pass without a domain error ever being raised), response shape, and the composition root. Router mappings verified by mutation -- a wrong status code or a dropped timezone fallback turns 9, 2 and 1 cases red respectively. Two lifespan tests carried a `SimpleNamespace` config that predates this change; `langgraph_runtime` now reads `config.scheduler`, so they were given one. Tolerating the gap with `getattr` was rejected: `AppConfig.scheduler` always exists, so the fallback would be unreachable in production and exist purely to excuse an incomplete test double. Full suite is back to its 24 pre-existing failures. |