Merge branch 'worktree-make-planner-explaining-itself'
Clicking a planned day raises the calendar toast with a plain-language
reason (sv + en) taken from the live in-memory proposal; the backend
field-parity walk now covers ProposedDay.
Conflicts in Calendar.tsx / Calendar.test.tsx against the day-assertions
restructure and the two earlier merges; both intents kept:
- usePlannedDayExclusions stays replaced by useDayAssertions; the branch's
usePlanProposal watch, reasonProposal state, reasonByDay map and the
handleBandClick reason toast are added alongside main's band logic.
- runSync keeps the security branch's background refresh and also records
the sync's proposal for the reason toast. main's applyWishSyncResult
(every wish save on this page) records result.sync.proposal the same way,
so an edit's fresh reasons reach the toast — the stale-response guards
in usePlanSync/usePlanProposal only gate hook state, never the returned
response the reason rides in on.
- One main test ("no success toast when sync-with-wish throws") now
dismisses the click's own reason toast first; its assertion is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
This commit is contained in:
@@ -37,7 +37,7 @@ from app.repositories_impl.sqlite.parental_leave_repository import SQLiteParenta
|
||||
from app.repositories_impl.sqlite.personal_data_repository import SQLitePersonalDataRepository
|
||||
from app.repositories_impl.sqlite.vacation_days_repository import SQLiteVacationDaysRepository
|
||||
from app.repositories_impl.sqlite.workspace_repository import SQLiteWorkspaceRepository
|
||||
from app.services.allocation_service import PlanProposal, WindowOutcome
|
||||
from app.services.allocation_service import PlanProposal, ProposedDay, WindowOutcome
|
||||
|
||||
# Fixed identity for the authenticated caller in every test in this file.
|
||||
# Each test creates its own workspace + "parent" PersonalData row — that row
|
||||
@@ -889,6 +889,16 @@ _PLAN_PROPOSAL_ALLOWLIST: dict[str, str] = {}
|
||||
_WINDOW_OUTCOME_RENAMES: dict[str, tuple[str, ...]] = {}
|
||||
_WINDOW_OUTCOME_ALLOWLIST: dict[str, str] = {}
|
||||
|
||||
# Same idea again for `ProposedDay` -> `ProposedDayResponse` -- added
|
||||
# specifically because `reason` (the machine-readable code the frontend's
|
||||
# calendar toast now reads, see `frontend/src/utils/planReasons.ts`) was
|
||||
# never walked by this mechanism: nothing would have failed here if
|
||||
# `from_domain()` had silently dropped it, the same class of bug this file's
|
||||
# `_assert_domain_fields_reach_the_wire` exists to catch for `PlanProposal`/
|
||||
# `WindowOutcome`. Every field is a straight same-name passthrough today.
|
||||
_PROPOSED_DAY_RENAMES: dict[str, tuple[str, ...]] = {}
|
||||
_PROPOSED_DAY_ALLOWLIST: dict[str, str] = {}
|
||||
|
||||
|
||||
def _wire_keys_for(domain_field: str, renames: dict[str, tuple[str, ...]]) -> tuple[str, ...]:
|
||||
"""The wire key(s) `domain_field` must appear as, absent an explicit rename."""
|
||||
@@ -1226,6 +1236,19 @@ class TestPlanProposalFieldParityWithDomain:
|
||||
check_nonempty=False,
|
||||
)
|
||||
|
||||
# `ProposedDay` -> `ProposedDayResponse`: was never walked before
|
||||
# (see `_PROPOSED_DAY_RENAMES`'s docstring) -- this is what would have
|
||||
# caught `reason` being silently dropped from the wire.
|
||||
for day in body["days"]:
|
||||
_assert_domain_fields_reach_the_wire(
|
||||
ProposedDay,
|
||||
_PROPOSED_DAY_RENAMES,
|
||||
_PROPOSED_DAY_ALLOWLIST,
|
||||
day,
|
||||
context="ProposedDay -> ProposedDayResponse",
|
||||
check_nonempty=False,
|
||||
)
|
||||
|
||||
# `remedies` is a list-of-dataclass field inside WindowOutcome too —
|
||||
# confirm the shortfall outcome specifically has a non-empty one
|
||||
# (the generic walk above only guarantees each outcome's own
|
||||
|
||||
Reference in New Issue
Block a user