fix(snapshot): report a refused cron merge to the import caller - #8315
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix at the right layer: the helper returns its outcome, both callers gate reporting on it, and the audit trail follows an existing Suggestions
[DESIGN-REVIEWED] 5b3182d |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All claims verified. Composing the review. First-Principles-Verdict: CONCERNS The fix earns its place at both callers, but What this change shipsIntent: stop a snapshot import from reporting a refused cron merge as a success — a FIX.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 5b3182d |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
7c51776 to
bf3a112
Compare
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
_merge_crons could refuse the merge (unreadable source, unreadable destination, or an unusable cron shape on either side) and return with only a print to show for it. apply_import_zip then appended "crons (merged)" to the summary unconditionally, and the dashboard handler logged outcome=ok -- so an import that brought back ZERO cron jobs was reported to the caller as a success, with the audit trail agreeing. - _merge_crons now returns True when it wrote the merged store and False on every refusal path (diagnostics stay on stdout). - apply_import_zip gates the summary item on that return: a refusal appends "crons (skipped: unreadable or invalid cron store)" and a machine-readable refused_merges entry instead of "crons (merged)". - The dashboard import handler logs outcome=partial (naming the refused component in resources) instead of a flat ok. - The CLI restore path prints an explicit skip line instead of a success checkmark over a refused merge. Tests lock all three refusal paths at the merger, the summary shape through apply_import_zip for every live-store refusal reachable there, the genuine-merge summary, and the handler's partial-vs-ok outcome. Closes #8217
bf3a112 to
5b3182d
Compare
bolichen97
left a comment
There was a problem hiding this comment.
Correct root-cause fix for a genuine false-success: _merge_crons returns bool (False on all three refusal paths, True only after the merged store is written), and both production callers now gate on it — apply_import_zip emits a skip item plus machine-readable refused_merges instead of an unconditional "crons (merged)", and the dashboard handler logs outcome="partial" naming the component instead of a flat ok. The distinction I checked most closely is the one the docstring makes explicit and the code honours: a failed WRITE still raises OSError rather than being flattened into False, so a loud failure stays loud. _do_merge's no-crons-in-archive path also still prints the checkmark, so the skip line only appears over a real refusal. Tests cover all three refusal paths, both callers, the handler outcome, and the success path.
Problem / Motivation
A snapshot import whose cron merge was refused is reported to the caller as a successful one.
_merge_crons(src/kiro_crew/snapshot.py) has three refusal paths -- unreadable source JSON, unreadable destination JSON, and an unusable cron shape on either side -- and each returnedNonewith only aprintto show for it.apply_import_zip(src/kiro_crew/portability.py) then appended"crons (merged)"to the summary unconditionally, and the dashboard handler logged the import withoutcome="ok". Net effect: an import that restored ZERO cron jobs answered{ok: true}with a summary listing"crons (merged)", and the SEL audit trail agreed. The only trace of the refusal was a print a dashboard import has no terminal to show.Why it matters
A user restoring a backup expecting their scheduled jobs back is told it worked when no job was imported. They discover the loss later, when a job silently never fires -- and the audit trail, the one place that should contradict the false success, confirms it instead.
What changed (motivation -> approach -> change)
Symptom: false "crons (merged)" over a refused merge. Root cause: the merge's outcome existed only on stdout, so no caller could distinguish a merge from a refusal. Fix: make the refusal observable at each caller, minimally:
_merge_cronsnow returnsbool:Trueonly after the merged store is written,Falseon all three refusal paths. Diagnostics stay on stdout; a failing write still raises (OSErrorpropagates to the handler's error path, which is the correct loud behavior).apply_import_zipgates the summary item on that return: a refusal appends"crons (skipped: unreadable or invalid cron store)"plus a machine-readablerefused_merges: ["crons"], instead of"crons (merged)".src/kiro_crew/dashboard/handlers/portability.py) logsoutcome="partial"naming the refused component inresources, instead of a flatok."partial"follows the established convention (connections.py,security.py,hooks_integration.py)._do_merge) prints an explicitcrons: merge skippedline instead of"OK checkmark" crons status lineover a refusal.Known, deliberately out-of-scope siblings (this PR stops the cron-merge false positive at its root, per the issue; each of these is the same shape but a different surface):
PortabilityTab.tsxrenders onlysummary.items.lengthwith a green check, so the human-visible toast is unchanged; the API payload and audit trail are now truthful, andrefused_mergesis in the response for the UI to render. Precedent:rejected_crons/paused_cronsare equally unrendered today. Frontend surfacing is a small follow-up (this change is backend-only by design)._sanitize_imported_cronsbefore the merge, so that case still reports"crons (merged)"over zero restored jobs -- the truth for that path lives inrejected_crons, which the sanitizer already reports. Pre-existing, not introduced or widened here._merge_memoryand_merge_notificationsappend"(merged)"under the same unconditional pattern; left alone deliberately to keep this root-cause scoped to the component the issue names.state_restored) does not yet reflect a refused component;_do_mergereturnsNonetoday and propagating it is a second contract change.Tests
test_merge_crons_returns_the_outcome_on_every_path: all three refusal paths answerFalseand write nothing; a genuine merge answersTrueand writes the merged store.test_a_refused_cron_merge_is_not_reported_as_merged(parametrized x3): every live-store refusal reachable throughapply_import_zip(unreadable bytes, non-object top level, unusable job list) yields the skip item +refused_merges, never"crons (merged)", and leaves the live store byte-identical.test_an_archive_side_refusal_is_not_reported_as_merged: the one archive-side refusal that survives the sanitizer end-to-end (a lone-surrogate job name) is reported as a skip.test_a_genuine_cron_merge_still_reports_merged: no regression on the success path.test_import_handler_outcome_reflects_a_refused_merge(parametrized x2): the handler logspartial+refused=cronsfor a refused merge andokfor a clean one.TestARefusedCronMergeIsVisibleInTheRestoreStatus(2 tests):_do_mergeprints the skip line, not"OK checkmark" crons status line, over a refusal -- and still prints the checkmark on a genuine merge.Local gates: isort / flake8 / mypy clean;
test/test_portability.py+test/test_snapshot.py132 passed; full backend suite run -- the only failures are pre-existing host-environment classes (NFS/local/homeownership,AF_UNIX path too long) that reproduce identically on a pristineorigin/mainworktree on this host.Manual verification
N/A -- unit coverage sufficient: both production callers of
_merge_cronsand the handler outcome are exercised directly by the new tests, and the change has no UI surface.Closes #8217
Pattern harvest
Rule candidate: a helper that can refuse its work (merge/restore/import) must return the outcome, and a caller MUST NOT append a success item to a user-facing summary (or log outcome=ok) unless that return says the work happened. Two sibling instances of the same shape exist today (
_merge_memory,_merge_notificationsappend "(merged)" unconditionally) and are named in the out-of-scope list above -- candidate for an AUTOSDE reviewer rule onsummary["items"].append(...(merged)...)sites adjacent to an ungated helper call; a semgrep rule is likely too syntactic to avoid false positives.