fix(api): reconcile calendar_unsupported to 422/exit 2 and evidence preservation conformance #52
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-14-preservation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
Issue #014's preservation-safe patcher already exists and is in production use:
src/calendar/preservation.rs(a lossless line/component AST) driven bysrc/api/update.rs,src/api/creation.rsandsrc/api/lifecycle.rs. It landed with #011, a deviation recorded on both issues, and #014's spec explicitly permits it ("leveragingical::parser::ical::IcalParseror a dedicated lossless line/component AST").The issue stayed open for the conformance evidence its criteria require. The audit found the patcher sound but under-evidenced: folding had been asserted only on
"x".repeat(200)(pure ASCII), so nothing proved that a multi-octet character is never split, that continuation lines respect the 75-octet limit, or that a folded Unicode resource survives an edit byte for byte.The patcher itself needed no fix — "audited, no defect found" is the result — and no dependency, rewrite or switch to the
icalcrate is included. One production change is in scope, requested in review: reconciling thecalendar_unsupportedrefusal to HTTP 422 / exit 2 (finding 1 below).src/api/update.rsis untouched.Changes
src/calendar/preservation.rs— unit conformance tests plus the issue's illustrative fixture:SUMMARYis patched — the whole resource differs in exactly one physical line — with the client's folded UnicodeDESCRIPTION, itsX-APPLE-STRUCTURED-LOCATION/X-CUSTOM-VENDOR-PROPlines,VALARMandVTIMEZONEall intact, and the unfolded description still semantically identical;END, no component, unparseable, unknownUID, ambiguous identity) is aCalendarError::Preservationand never a partial resource;tests/update_tests.rs— the same evidence throughPATCH /v1/events/{id}andPATCH /v1/tasks/{id}, asserting on the exact bytesPUT, mirroring the suite's existing habit:c09_a_title_edit_preserves_a_folded_unicode_description_and_vendor_properties;c09_a_master_edit_leaves_the_sibling_detached_override_untouched;a_long_multibyte_title_is_folded_within_75_octets_and_reads_back_exactly;c09_an_ambiguous_target_is_refused_and_nothing_is_dispatched—calendar_unsupportedat HTTP 422 with CLI exit 2, zeroPUT, zero journalled operation, backend bytes untouched;a_calendar_unsupported_refusal_exits_two— the reallife task updateCLI against a 422 envelope, asserting process exit 2;a_custom_non_iana_tzid_is_refused_before_any_dispatch— the boundary recorded below, plus aMockState::resourceaccessor to prove no partial write.src/api/mod.rs/src/client.rs— the one production change (review request):ApiFailure::calendar_unsupportednow answers HTTP 422 / exit 2, and the CLI's error-code map answers 2 for the code.tests/update_tests.rsandtests/lifecycle_tests.rspins that expected 400 for this refusal were updated to 422.Audit against the acceptance criteria
X-*properties, parameters,VALARM,VTIMEZONEpreserved on aSUMMARY/STATUSeditdiffering_lines), theX-*/VALARM/VTIMEZONEregions are asserted byte-identical, and the semantic (unfolded) description is unchanged — unit and HTTP.folduses octet lengths withis_char_boundaryguards; new multibyte tests plus a boundary-straddling case, byte-exact reassembly, and an end-to-end multibyte title that reads back through the projection exactly.(UID, RECURRENCE-ID)is rewritten and nestedBEGIN/ENDregions are never descended into. New: the override block is asserted byte-identical viacomponent_block.src/calendar/ical.rsis referenced only fromsrc/api/creation.rs; updates callapply_editson the raw resource. Asserted by the byte-level preservation tests.calendar_unsupported(HTTP 422 / exit 2)CalendarError::Preservationand surface ascalendar_unsupportedwith zero dispatches. New: HTTP 422 asserted on the ambiguous-identity refusal, CLI exit 2 asserted both throughclient::exit_code_forand by running the real CLI against a 422 envelope.docs/technical/calendar.md) and Scenario C09 (docs/technical/verification.md)No defect was found in
foldor in the preservation path. Folding is octet-correct for every multi-octet case tried, including a dedicated search over 4,000 pseudo-random mixed-width strings (1/2/3/4-octet plus combining marks, values up to 400 characters): every physical line stayed within 75 octets, every continuation was marked correctly, and every reassembly matched the input byte for byte. That search was exploratory and is not committed.Findings recorded, deliberately not changed here
calendar_unsupportedmapped to HTTP 400 / exit 1 while the calendar layer answered exit 2 — resolved in review.ApiFailure::calendar_unsupportedwasBAD_REQUEST/exit_codes::GENERAL_ERROR,CalendarError::CalendarUnsupported::exit_code()isINVALID_INPUT(2),docs/technical/cli-api.mdmaps exit 2 to 400/413/422, and the CLI's error-code map had no arm for the code so it exited 1. The API now refuses with 422 / exit 2 and the CLI exits 2. The agenda partial path for an unsupported collection is deliberately untouched: it staysstatus: partial, coverageunsupported, exit 6.TZIDis refused asdependency_unavailable(503, retryable) instead ofcalendar_unsupported. The parser does produceCalendarError::CalendarUnsupported("Unsupported TZID …")as #013 requires, butscan_collectionscollapses the failure into "All configured CalDAV collections are unavailable" when no collection survives, and the update route reports that. The mutation is still refused with zero dispatches and no partial write (asserted), but the code and theretryableflag are wrong for a case that will never succeed. Same pattern insrc/api/agenda.rsandsrc/api/tasks.rs; no test covered it before this one. Worth a follow-up decision — it is read-path classification, not preservation.PropertyEdit::parameterizedexists for exactly this, e.g.VALUE=DATE), and untouched properties keep theirs verbatim. Pinned by a test so the interpretation is reviewable.Cargo.tomlauthors chore stays open. No dependency was needed, soCargo.tomlwas not touched;authorsis still unset.Verification
nix develop --command cargo test— all suites green: 35 lib tests (15 incalendar::preservation),update_tests33,lifecycle_tests27, and every other suite unchanged and passing (recall the 2 updated pins and the 2 new refusal tests).nix develop --command cargo clippy --all-targets -- -D warnings— clean.nix develop --command cargo fmt --check— clean.calendar_unsupportedrefusal,life::client::exit_code_for== 2 for that envelope, and a reallife task updateprocess exiting 2 against a 422 envelope — with zeroPUTs, zerooperationsrows and untouched backend bytes.PUT); the issue's fixture verbatim over HTTP, because itsCustom-TimezoneTZIDis refused by the read path before the patcher is consulted (finding 2) — it is exercised at the patcher level instead.Related Issues
VTIMEZONEhandling, finding 2), #15 (occurrence-scoped mutations, untouched here).Coordinator review — CHANGES REQUESTED (one item)
Thanks for correcting my brief rather than following it — that is exactly the behaviour I want. You were right on both points and I have verified them myself:
ApiFailure::calendar_unsupported(src/api/mod.rs:202) isHTTP 400/exit_codes::GENERAL_ERROR(1), not "422 / exit 2".CalendarError::CalendarUnsupported(_).exit_code()(src/calendar/mod.rs:70) isexit_codes::INVALID_INPUT(2).src/client.rshas nocalendar_unsupportedarm, so the CLI falls through to the default and exits 1.So the same condition carries exit 2 in the calendar layer and exit 1 in the API/CLI layer, and #14's criterion 5 asks for "HTTP 422 / Exit code 2 or 5".
Change needed: reconcile the mapping. Set
ApiFailure::calendar_unsupportedtohttp_status: StatusCode::UNPROCESSABLE_ENTITYandexit_code: exit_codes::INVALID_INPUT, which makes it agree withCalendarError::exit_code(), with the file's existingvalidation_failedprecedent (UNPROCESSABLE_ENTITY), and withdocs/technical/cli-api.md("Exit 2 … HTTP mapping 400/413/422"). Add assertions pinning HTTP 422 and CLI exit 2 on acalendar_unsupportedterminal refusal (the ambiguous-duplicate-identity case you already drive is a good vehicle), and update the existing test that currently pins HTTP 400.Scope note: keep this to the terminal-refusal mapping. The agenda's partial path for an unsupported collection is different behaviour and already correct (
status: partial, coverageunsupported, exit 6) — do not touch it. This is a shared constant, so run the full suite and fix any assertion that pinned the old 400/1.Everything else: approved as-is
The audit result ("audited, no defect found in fold or the preservation path") matches what I found reading the code, and the evidence you added is the right kind:
foldat octets withis_char_boundarybackoff, and the case where octet 75 lands inside a 4-octet emoji asserting the backoff to octet 73 — that is the exact failure mode criterion 2 exists to prevent, and nothing else in the suite covered it.VALARM/VTIMEZONE/X-APPLE-STRUCTURED-LOCATION/X-CUSTOM-VENDOR-PROPregions.calendar_unsupportedrefusal with zero PUTs and zerooperationsrows.ical::import being confined tosrc/api/creation.rs.No production change was needed, which is the correct outcome for an issue whose implementation landed with #11.
Cargo.tomlcorrectly untouched, so the deferred authors chore stays pending.Keep
cargo test/clippy -D warnings/fmt --checkgreen and do not merge.test(calendar): preservation conformance evidence for folded unicode and overrides (#14)to fix(api): reconcile calendar_unsupported to 422/exit 2 and evidence preservation conformance (#14)Coordinator re-review — APPROVED
Verified
af3213fmyself:ApiFailure::calendar_unsupportedis nowUNPROCESSABLE_ENTITY+exit_codes::INVALID_INPUT, with a doc comment that states why (same class of refusal asvalidation_failed; retrying cannot make an uninterpretable resource interpretable) and names both theCalendarError::exit_code()alignment and the cli-api table.src/client.rsgained the matching"calendar_unsupported" => INVALID_INPUTarm — that detail matters, because the CLI maps by error code rather than status, so without it the exit code would still have been 1.status: partial, exit 6) is untouched andagenda_tasks_testsis green as-is.tests/update_tests.rsoccurrence refusal, twotests/lifecycle_tests.rscases, plus the new C09 refusal test now asserting 422 + exit 2), anda_calendar_unsupported_refusal_exits_twodrives the real binary end to end to prove process exit 2.Independent verification on
af3213f:cargo test252 passed / 0 failed;cargo clippy --all-targets -- -D warnings0 errors;cargo fmt --checkclean.I have retitled the PR to reflect that it carries a production fix as well as the conformance evidence. Finding 2 (a custom non-IANA
TZIDon the read path classified asdependency_unavailable) is outside the patcher's scope —src/calendar/report.rs:374maps an unresolvableTZIDtoCalendarUnsupportedandsrc/api/items.rs::live_read_failuremaps that tocalendar_unsupported, so the claim needs a reproduction before anything is changed. Filing it as a follow-up rather than blocking this merge.Merging.
fix(api): reconcile calendar_unsupported to 422/exit 2 and evidence preservation conformance (#14)to fix(api): reconcile calendar_unsupported to 422/exit 2 and evidence preservation conformance