fix: scoped metadata refresh and truthful entity save feedback #74

Merged
bjoern merged 4 commits from fix/entity-management-correctness into main 2026-08-16 00:26:05 +02:00
Member

Correctness fixes for the entity-management pages (tags / people / circles / characters / series) and their interaction with the cached metadata used by library autocomplete.

What changed

  • Scoped metadata patching instead of full reloads. Every entity create/update/delete used to dispatch LoadMetadataAction(force: true), re-downloading all five metadata collections. The write epics now emit surgical MetadataEntityUpsertedAction / MetadataEntityRemovedAction patches that insert, replace, or remove just the written entity at its alphabetical sort position (matching the server's ordering).
  • Explicit hasLoaded flag guards the cache. The load epic previously used collection emptiness to decide "already loaded", which a surgical patch applied before the first successful load could poison — a single seeded entity would permanently suppress the full download. MetadataState.hasLoaded (set only by MetadataLoadedAction) now gates both the non-forced load-skip and the patch epic itself: writes before the first load emit no patch (the eventual full download includes them), and a partially seeded cache can never block that download.
  • Truthful save feedback. The "Saved …" toast now appears only after the server confirms the write (observed via the isSaving round-trip), so a failed save no longer flashes a contradictory success message.
  • Create-selection race fixed. A newly created entity is selected by id (lastCreatedId recorded by the reducer) instead of being guessed from list growth, which raced with infinite-scroll appends.
  • Reducer bookkeeping. EntityCreatedAction inserts at the sorted position and increments pageInfo.totalItems; EntityDeletedAction decrements it; EntityUpdatedAction re-inserts the renamed entity at its sorted position (same invariant as create, and consistent with the metadata upsert).

Testing

  • flutter analyze: clean.
  • Full flutter test suite: 503 passing, including new regression tests for the write-before-load poisoning scenario, the non-empty-but-never-loaded cache, rename re-sorting, and the scroll-append-during-create selection race.

Follow-ups

  • insertEntitySorted compares with Dart String.compareTo (UTF-16 code units), while the server's SQLite BINARY ordering follows UTF-8 bytes. These diverge only for supplementary-plane characters (e.g. emoji in names), which can be inserted at a slightly different position than the server would return; the list self-corrects on the next full reload. A rune-based comparator would close this cosmetic gap.

🤖 Generated with Claude Code

Correctness fixes for the entity-management pages (tags / people / circles / characters / series) and their interaction with the cached metadata used by library autocomplete. ## What changed - **Scoped metadata patching instead of full reloads.** Every entity create/update/delete used to dispatch `LoadMetadataAction(force: true)`, re-downloading all five metadata collections. The write epics now emit surgical `MetadataEntityUpsertedAction` / `MetadataEntityRemovedAction` patches that insert, replace, or remove just the written entity at its alphabetical sort position (matching the server's ordering). - **Explicit `hasLoaded` flag guards the cache.** The load epic previously used collection emptiness to decide "already loaded", which a surgical patch applied before the first successful load could poison — a single seeded entity would permanently suppress the full download. `MetadataState.hasLoaded` (set only by `MetadataLoadedAction`) now gates both the non-forced load-skip and the patch epic itself: writes before the first load emit no patch (the eventual full download includes them), and a partially seeded cache can never block that download. - **Truthful save feedback.** The "Saved …" toast now appears only after the server confirms the write (observed via the `isSaving` round-trip), so a failed save no longer flashes a contradictory success message. - **Create-selection race fixed.** A newly created entity is selected by id (`lastCreatedId` recorded by the reducer) instead of being guessed from list growth, which raced with infinite-scroll appends. - **Reducer bookkeeping.** `EntityCreatedAction` inserts at the sorted position and increments `pageInfo.totalItems`; `EntityDeletedAction` decrements it; `EntityUpdatedAction` re-inserts the renamed entity at its sorted position (same invariant as create, and consistent with the metadata upsert). ## Testing - `flutter analyze`: clean. - Full `flutter test` suite: 503 passing, including new regression tests for the write-before-load poisoning scenario, the non-empty-but-never-loaded cache, rename re-sorting, and the scroll-append-during-create selection race. ## Follow-ups - `insertEntitySorted` compares with Dart `String.compareTo` (UTF-16 code units), while the server's SQLite BINARY ordering follows UTF-8 bytes. These diverge only for supplementary-plane characters (e.g. emoji in names), which can be inserted at a slightly different position than the server would return; the list self-corrects on the next full reload. A rune-based comparator would close this cosmetic gap. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Replace the full LoadMetadataAction(force) refresh on every entity write
  with surgical MetadataEntityUpsertedAction/MetadataEntityRemovedAction
  patches that insert/replace/remove the single written entity in the cached
  metadata collection at its alphabetical sort position (matching the
  server's ordering), instead of re-downloading all five collections.
- Show the 'Saved …' toast only after the server confirmed the write (via
  the isSaving round-trip on the page), so a failed save no longer shows a
  contradictory success message first.
- Select a newly created entity by its id (recorded as lastCreatedId by the
  reducer) instead of guessing from list growth, which raced with
  infinite-scroll appends.
- Entity reducer bookkeeping: EntityCreatedAction inserts at the sorted
  position and increments pageInfo.totalItems; EntityDeletedAction
  decrements it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: gate metadata patches and load-skip on an explicit hasLoaded flag
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 2m37s
322bcb4584
Review follow-up: the surgical metadata patch epic could poison the
empty-cache guard in _loadMetadataEpic — an entity write before the first
successful metadata load seeded one collection with a single entity, which
the emptiness heuristic then mistook for "already loaded" and permanently
suppressed the full download.

- Add MetadataState.hasLoaded, set only by MetadataLoadedAction.
- _loadMetadataEpic now skips non-forced loads on hasLoaded instead of
  collection emptiness, so a partially seeded cache can never block the
  first full fetch.
- The entity-change patch epic emits upsert/remove patches only once
  hasLoaded is true; a write before the first load needs no patch because
  the eventual full download includes it.
- EntityUpdatedAction now removes and re-inserts the renamed entity at its
  sorted position in the management list, keeping the same alphabetical
  invariant EntityCreatedAction maintains (and matching the metadata
  reducer's upsert behaviour).
- Regression tests: write-before-load does not seed the cache and a later
  LoadMetadataAction still fetches; a non-empty never-loaded cache does
  not skip the fetch; rename re-sorts; update with unknown id is a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Flutter Coverage

File Line coverage
lib/app/store.dart 100.0% (18 of 18)
lib/data/models/doujin_models.dart 85.0% (34 of 40)
lib/data/models/doujin_models.g.dart 40.5% (119 of 294)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/presentation/state/app_state.dart 60.0% (9 of 15)
lib/agent/agent_service.dart 84.6% (242 of 286)
lib/agent/approval_gate.dart 100.0% (16 of 16)
lib/agent/assistant_context.dart 52.8% (28 of 53)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (12 of 12)
lib/agent/memory_store.dart 92.9% (13 of 14)
lib/agent/skills/skill_registry.dart 93.8% (61 of 65)
lib/agent/system_prompt.dart 100.0% (63 of 63)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 62.9% (168 of 267)
lib/agent/tools/entity_write_tool.dart 73.5% (164 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 84.4% (27 of 32)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 93.8% (30 of 32)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 78.4% (29 of 37)
lib/agent/tools/search_doujins_tool.dart 100.0% (84 of 84)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 54.5% (6 of 11)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 32.4% (23 of 71)
lib/core/constants.dart 36.4% (4 of 11)
lib/presentation/middleware/assistant_epics.dart 83.1% (74 of 89)
lib/presentation/middleware/epics.dart 88.8% (326 of 367)
lib/presentation/state/reducers.dart 100.0% (10 of 10)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/languages.dart 100.0% (8 of 8)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 90.9% (90 of 99)
lib/presentation/middleware/editor_epics.dart 56.6% (163 of 288)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/editor_actions.dart 45.9% (17 of 37)
lib/presentation/state/actions/entity_actions.dart 62.5% (10 of 16)
lib/presentation/state/actions/library_actions.dart 36.8% (7 of 19)
lib/presentation/state/actions/metadata_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.0% (57 of 60)
lib/presentation/state/reducers/detail_reducer.dart 95.8% (23 of 24)
lib/presentation/state/reducers/editor_reducer.dart 92.5% (98 of 106)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (105 of 105)
lib/presentation/state/reducers/metadata_reducer.dart 85.7% (36 of 42)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (60 of 60)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 89.4% (286 of 320)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 11.8% (9 of 76)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 4.5% (3 of 66)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.3% (84 of 92)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 92.8% (64 of 69)
lib/data/repositories/doujin_api_repository.dart 22.2% (80 of 361)
lib/data/repositories/health_repository.dart 72.0% (18 of 25)
lib/data/secure_storage.dart 0.0% (0 of 26)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (3 of 3)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/reader/reader_page.dart 88.5% (216 of 244)
lib/presentation/pages/detail/detail_page.dart 77.1% (178 of 231)
lib/presentation/pages/detail/variant_tabs_panel.dart 94.9% (169 of 178)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/presentation/pages/reader/reader_overlay.dart 94.4% (51 of 54)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (27 of 27)
lib/presentation/pages/people/people_page.dart 55.6% (10 of 18)
lib/presentation/widgets/entity_editor.dart 88.1% (118 of 134)
lib/presentation/widgets/entity_management_page.dart 85.0% (210 of 247)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 73.8% (90 of 122)
lib/presentation/pages/editor/doujin_list_pane.dart 67.2% (43 of 64)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/editor_pane.dart 70.9% (39 of 55)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/app/app.dart 66.7% (44 of 66)
lib/presentation/pages/settings/settings_page.dart 99.3% (138 of 139)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/library/library_page.dart 88.5% (170 of 192)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 78.7% (170 of 216)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 75.0% (5960 of 7951)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 100.0% (18 of 18) | | lib/data/models/doujin_models.dart | 85.0% (34 of 40) | | lib/data/models/doujin_models.g.dart | 40.5% (119 of 294) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/presentation/state/app_state.dart | 60.0% (9 of 15) | | lib/agent/agent_service.dart | 84.6% (242 of 286) | | lib/agent/approval_gate.dart | 100.0% (16 of 16) | | lib/agent/assistant_context.dart | 52.8% (28 of 53) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (12 of 12) | | lib/agent/memory_store.dart | 92.9% (13 of 14) | | lib/agent/skills/skill_registry.dart | 93.8% (61 of 65) | | lib/agent/system_prompt.dart | 100.0% (63 of 63) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 62.9% (168 of 267) | | lib/agent/tools/entity_write_tool.dart | 73.5% (164 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 84.4% (27 of 32) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 93.8% (30 of 32) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 78.4% (29 of 37) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (84 of 84) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 54.5% (6 of 11) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 32.4% (23 of 71) | | lib/core/constants.dart | 36.4% (4 of 11) | | lib/presentation/middleware/assistant_epics.dart | 83.1% (74 of 89) | | lib/presentation/middleware/epics.dart | 88.8% (326 of 367) | | lib/presentation/state/reducers.dart | 100.0% (10 of 10) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 90.9% (90 of 99) | | lib/presentation/middleware/editor_epics.dart | 56.6% (163 of 288) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/editor_actions.dart | 45.9% (17 of 37) | | lib/presentation/state/actions/entity_actions.dart | 62.5% (10 of 16) | | lib/presentation/state/actions/library_actions.dart | 36.8% (7 of 19) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.0% (57 of 60) | | lib/presentation/state/reducers/detail_reducer.dart | 95.8% (23 of 24) | | lib/presentation/state/reducers/editor_reducer.dart | 92.5% (98 of 106) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (105 of 105) | | lib/presentation/state/reducers/metadata_reducer.dart | 85.7% (36 of 42) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (60 of 60) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 89.4% (286 of 320) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 11.8% (9 of 76) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 4.5% (3 of 66) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.3% (84 of 92) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 92.8% (64 of 69) | | lib/data/repositories/doujin_api_repository.dart | 22.2% (80 of 361) | | lib/data/repositories/health_repository.dart | 72.0% (18 of 25) | | lib/data/secure_storage.dart | 0.0% (0 of 26) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (3 of 3) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/reader/reader_page.dart | 88.5% (216 of 244) | | lib/presentation/pages/detail/detail_page.dart | 77.1% (178 of 231) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 94.9% (169 of 178) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/presentation/pages/reader/reader_overlay.dart | 94.4% (51 of 54) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (27 of 27) | | lib/presentation/pages/people/people_page.dart | 55.6% (10 of 18) | | lib/presentation/widgets/entity_editor.dart | 88.1% (118 of 134) | | lib/presentation/widgets/entity_management_page.dart | 85.0% (210 of 247) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 73.8% (90 of 122) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.2% (43 of 64) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/editor_pane.dart | 70.9% (39 of 55) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/app/app.dart | 66.7% (44 of 66) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (138 of 139) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/library/library_page.dart | 88.5% (170 of 192) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 78.7% (170 of 216) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 75.0% (5960 of 7951)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A PR that reviews itself mid-flight — the second commit catches its own patch-epic poisoning the cache it was going to patch? That's the kind of self-awareness I adore~ ♡ The hasLoaded flag is exactly the right medicine, and the regression tests are genuinely directional. But fufu… two things still squirm under my magnifying glass, and you know I never let squirming things pass~ ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. [epics.dart:569-571] — The EntityUpdatedAction<T> arm of _patchMetadataOnEntityChangeEpic is brand-new wiring and no test exercises it. I mutation-checked: delete those two lines and zero tests go red. Your create and delete arms each got a beautiful end-to-end test (creating an entity patches the metadata cache in place, deleting an entity removes it from the metadata cache) — but update gets nothing. The filter_reducers_test upsert tests dispatch MetadataEntityUpsertedAction directly, which pins the reducer but not the epic wiring; the UpdateEntityAction<Person> and AddEntityAliasAction<Circle> tests never seed MetadataLoadedAction nor assert the metadata cache. So a rename (or an alias add — aliases feed autocomplete too, they also flow through EntityUpdatedAction at :713/:731!) could silently stop reaching the cache and CI would stay green. You added a code path but forgot to test it — I can't let that slide~ ♡
    Fix: one epic-level test mirroring the create/delete siblings: seed MetadataLoadedAction(people: [Person('A'), Person('Z')]), dispatch UpdateEntityAction<Person>(Person(id-A, 'Renamed')), pumpEventQueue(), assert metadata.people names are re-sorted ['Renamed', 'Z']-style and listPeopleCalls == 0. An alias variant would be cherries on top~

  2. [entity_management_page.dart:267-282 + entity_editor.dart:219-223] — The single-slot _pendingSaveName misattributes feedback on overlapping writes. The Save/Create button (and the Ctrl+S shortcut at :143) is never disabled while isSaving, so two writes can be in flight. Trace it with me: select X, Save (arms name='X') → select Y, Save (overwrites to name='Y') → X's round-trip lands, isSaving goes true→false → toast says "Saved "Y"" for X's completion → Y lands, _pendingSaveName is already null → no toast at all. One wrong name, one lost confirmation — in the PR whose headline is truthful save feedback. Fufu~ you wouldn't leave THIS in production, would you? ♡
    Fix: gate the editor's save action on the slice's isSaving (disable onPressed + the shortcut when in flight) — the same in-flight guard the library refresh button already uses (library_page.dart guards on isLoading || isLoadingMore). That collapses overlapping writes to impossible, which also closes the pre-existing double-submit window where a impatient double-click on Create fires two POSTs.

💡 Little ideas (non-blocking)~

  1. [entity_management_page.dart:260-264] — the failed arm of _awaitingCreate (create error → stay in create mode) is still untested. It's pre-existing, but it sits inside the block this PR rewrote and you already built _SlowCreateRepo — a _FailingCreateRepo sibling would pin it for free~
  2. [epics.dart:274-275] — the epic's dartdoc still says it skips when "already loaded or currently loading"; the body deliberately does not gate on isLoading (and your own comment explains why). Now that hasLoaded is the criterion, the doc is a touch stale — one line of polish.

What I liked~

  • The hasLoaded design itself — an explicit flag set only by MetadataLoadedAction, gating both the load-skip and the patch epic symmetrically. The two-commit arc where you caught your own poisoning bug (write-before-load seeding the cache → suppressing the full download forever) is exactly the failure mode an emptiness heuristic invites, and both regression tests (write before the first load does not seed the cache, still fetches when the cache is non-empty but never loaded) would catch a regression for real. Wonderful~
  • _SlowCreateRepo — interleaving a scroll append into an in-flight create is precisely the race the old growth-heuristic lost, and the test pins that the appended tag doesn't steal selection. Chef's kiss ♪
  • insertEntitySorted as one shared comparator for both the entity list and the metadata cache — DRY done right, and I verified the server side: all five services OrderBy(Name) / OrderBy(DisplayName), so primaryName is the correct sort key per type. The UTF-16-vs-UTF-8 divergence disclosed in the follow-ups is honest and correctly scoped as cosmetic.
  • The failed-save test asserts both the absence of "Saved" and the presence of the error — no tautology there~

Automated review by Jibril · 2026-08-15
CI/CD: absent for head SHA 322bcb4 (no coverage bot) · Local checks: flutter analyze 0 issues · changed-file tests 67/67 pass (entity_reducer 14, entity_write_epics 9, filter_epics 12, filter_reducers 21, entity_management_page 11)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A PR that reviews *itself* mid-flight — the second commit catches its own patch-epic poisoning the cache it was going to patch? That's the kind of self-awareness I adore~ ♡ The `hasLoaded` flag is exactly the right medicine, and the regression tests are genuinely directional. But fufu… two things still squirm under my magnifying glass, and you know I never let squirming things pass~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[epics.dart:569-571]** — The `EntityUpdatedAction<T>` arm of `_patchMetadataOnEntityChangeEpic` is brand-new wiring and **no test exercises it**. I mutation-checked: delete those two lines and *zero* tests go red. Your create and delete arms each got a beautiful end-to-end test (`creating an entity patches the metadata cache in place`, `deleting an entity removes it from the metadata cache`) — but update gets nothing. The `filter_reducers_test` upsert tests dispatch `MetadataEntityUpsertedAction` *directly*, which pins the reducer but not the epic wiring; the `UpdateEntityAction<Person>` and `AddEntityAliasAction<Circle>` tests never seed `MetadataLoadedAction` nor assert the metadata cache. So a rename (or an alias add — aliases feed autocomplete too, they also flow through `EntityUpdatedAction` at :713/:731!) could silently stop reaching the cache and CI would stay green. You added a code path but forgot to test it — I can't let that slide~ ♡ **Fix:** one epic-level test mirroring the create/delete siblings: seed `MetadataLoadedAction(people: [Person('A'), Person('Z')])`, dispatch `UpdateEntityAction<Person>(Person(id-A, 'Renamed'))`, `pumpEventQueue()`, assert `metadata.people` names are re-sorted `['Renamed', 'Z']`-style **and** `listPeopleCalls == 0`. An alias variant would be cherries on top~ 2. **[entity_management_page.dart:267-282 + entity_editor.dart:219-223]** — The single-slot `_pendingSaveName` **misattributes feedback on overlapping writes**. The Save/Create button (and the Ctrl+S shortcut at :143) is never disabled while `isSaving`, so two writes can be in flight. Trace it with me: select X, Save (arms `name='X'`) → select Y, Save (overwrites to `name='Y'`) → X's round-trip lands, `isSaving` goes true→false → toast says **"Saved \"Y\""** for X's completion → Y lands, `_pendingSaveName` is already null → **no toast at all**. One wrong name, one lost confirmation — in the PR whose headline is *truthful* save feedback. Fufu~ you wouldn't leave THIS in production, would you? ♡ **Fix:** gate the editor's save action on the slice's `isSaving` (disable `onPressed` + the shortcut when in flight) — the same in-flight guard the library refresh button already uses (library_page.dart guards on `isLoading || isLoadingMore`). That collapses overlapping writes to impossible, which also closes the pre-existing double-submit window where a impatient double-click on Create fires two POSTs. #### 💡 Little ideas (non-blocking)~ 1. **[entity_management_page.dart:260-264]** — the `failed` arm of `_awaitingCreate` (create error → stay in create mode) is still untested. It's pre-existing, but it sits inside the block this PR rewrote and you already built `_SlowCreateRepo` — a `_FailingCreateRepo` sibling would pin it for free~ 2. **[epics.dart:274-275]** — the epic's dartdoc still says it skips when "already loaded **or currently loading**"; the body deliberately does *not* gate on `isLoading` (and your own comment explains why). Now that `hasLoaded` is the criterion, the doc is a touch stale — one line of polish. #### ✅ What I liked~ - **The `hasLoaded` design itself** — an explicit flag set only by `MetadataLoadedAction`, gating *both* the load-skip and the patch epic symmetrically. The two-commit arc where you caught your own poisoning bug (write-before-load seeding the cache → suppressing the full download forever) is *exactly* the failure mode an emptiness heuristic invites, and both regression tests (`write before the first load does not seed the cache`, `still fetches when the cache is non-empty but never loaded`) would catch a regression for real. Wonderful~ - **`_SlowCreateRepo`** — interleaving a scroll append into an in-flight create is precisely the race the old growth-heuristic lost, and the test pins that the *appended* tag doesn't steal selection. Chef's kiss ♪ - **`insertEntitySorted` as one shared comparator** for both the entity list and the metadata cache — DRY done right, and I verified the server side: all five services `OrderBy(Name)` / `OrderBy(DisplayName)`, so `primaryName` is the correct sort key per type. The UTF-16-vs-UTF-8 divergence disclosed in the follow-ups is honest and correctly scoped as cosmetic. - **The failed-save test** asserts both the absence of "Saved" *and* the presence of the error — no tautology there~ --- *Automated review by Jibril · 2026-08-15* *CI/CD: absent for head SHA 322bcb4 (no coverage bot) · Local checks: `flutter analyze` 0 issues · changed-file tests 67/67 pass (entity_reducer 14, entity_write_epics 9, filter_epics 12, filter_reducers 21, entity_management_page 11)*
fix: gate saves on isSaving and pin the update metadata patch
Some checks failed
Flutter CI / analyze-and-test (pull_request) Has been cancelled
e149d3fe63
Review follow-up:

- EntityEditor: _save() now returns early while the slice's isSaving
  is true, and the Save/Create button disables (with a small progress
  indicator) for the duration of the round-trip. Overlapping writes
  previously misattributed the pending-save toast (single-slot
  _pendingSaveName showed the second name for the first write and
  nothing for the second) and left a double-submit window on Create.
  The Ctrl+S shortcut is covered by the _save guard itself.
- Epic tests: the EntityUpdatedAction arm of the metadata patch epic
  is now exercised end-to-end (rename re-sorts the cached people,
  alias add reaches the cached circle) — previously only the create
  and delete arms had coverage.
- Widget tests: save button disabled while in flight (button null +
  Ctrl+S no-op, updateCalls == 1) and a failed create stays in create
  mode (_FailingCreate-style createThrows stub).
- dartdoc: _loadMetadataEpic's skip criterion now documented as
  hasLoaded (not 'already loaded or currently loading').
Member

Both blockers addressed in e149d3f (same branch), plus the two non-blocking ideas since they were nearly free:

1. Update-arm epic coverage. Two new epic-level tests in entity_write_epics_test.dart, mirroring the create/delete siblings:

  • updating an entity patches the metadata cache in place — seeds MetadataLoadedAction(people: [Zeta, Zulu]), dispatches UpdateEntityAction<Person>(p1 -> Anna), asserts metadata.people re-sorts to ['Anna', 'Zulu'] and listPeopleCalls == 0. Drives the full round-trip: dispatched UpdateEntityAction -> repo -> emitted EntityUpdatedAction -> patch epic -> reducer.
  • adding an alias patches the metadata cache in place — the cherry: AddEntityAliasAction<Circle> -> alias epic re-emits EntityUpdatedAction -> cached circle gains the alias, listCirclesCalls == 0.
    Mutation-checked with your exact probe (deleting the EntityUpdatedAction arm): both new tests go red, everything else stays green.

2. Overlapping-write feedback. EntityEditor._save() now early-returns while the slice's isSaving is true, and the Save/Create button lives in a narrow StoreConnector<AppState, bool> that disables it (with a small progress indicator) for the round-trip — same in-flight guard style as the library refresh button. The Ctrl+S shortcut is covered by the _save guard itself (it is not a button). New widget test the save button is disabled while a write is in flight uses a _SlowUpdateRepo (Completer-gated like _SlowCreateRepo): asserts onPressed == null mid-flight, fires Ctrl+S mid-flight, then asserts the button re-enables and updateCalls == 1 after completion. Removing the guard turns exactly this test red.

Non-blockers taken:

  • _FailingCreateRepo-style test: a failed create stays in create mode pins the failed arm of _awaitingCreate (createThrows stub, asserts Create button still present, no "Saved" toast, error shown).
  • _loadMetadataEpic dartdoc now states the hasLoaded criterion explicitly instead of "already loaded or currently loading".

Verification: flutter analyze clean; full suite 507 passing (was 503, +4 new).

Both blockers addressed in e149d3f (same branch), plus the two non-blocking ideas since they were nearly free: **1. Update-arm epic coverage.** Two new epic-level tests in `entity_write_epics_test.dart`, mirroring the create/delete siblings: - `updating an entity patches the metadata cache in place` — seeds `MetadataLoadedAction(people: [Zeta, Zulu])`, dispatches `UpdateEntityAction<Person>(p1 -> Anna)`, asserts `metadata.people` re-sorts to `['Anna', 'Zulu']` and `listPeopleCalls == 0`. Drives the full round-trip: dispatched UpdateEntityAction -> repo -> emitted EntityUpdatedAction -> patch epic -> reducer. - `adding an alias patches the metadata cache in place` — the cherry: `AddEntityAliasAction<Circle>` -> alias epic re-emits `EntityUpdatedAction` -> cached circle gains the alias, `listCirclesCalls == 0`. Mutation-checked with your exact probe (deleting the `EntityUpdatedAction` arm): both new tests go red, everything else stays green. **2. Overlapping-write feedback.** `EntityEditor._save()` now early-returns while the slice's `isSaving` is true, and the Save/Create button lives in a narrow `StoreConnector<AppState, bool>` that disables it (with a small progress indicator) for the round-trip — same in-flight guard style as the library refresh button. The Ctrl+S shortcut is covered by the `_save` guard itself (it is not a button). New widget test `the save button is disabled while a write is in flight` uses a `_SlowUpdateRepo` (Completer-gated like `_SlowCreateRepo`): asserts `onPressed == null` mid-flight, fires Ctrl+S mid-flight, then asserts the button re-enables and `updateCalls == 1` after completion. Removing the guard turns exactly this test red. **Non-blockers taken:** - `_FailingCreateRepo`-style test: `a failed create stays in create mode` pins the `failed` arm of `_awaitingCreate` (createThrows stub, asserts Create button still present, no "Saved" toast, error shown). - `_loadMetadataEpic` dartdoc now states the `hasLoaded` criterion explicitly instead of "already loaded or currently loading". Verification: `flutter analyze` clean; full suite 507 passing (was 503, +4 new).
Member

🔮 fufu~ Jibril reviewed your code!

Fufu~ you came back with exactly the shape I asked for — both blockers, both cherries, surgical +181/-10 across 4 files, and I re-ran my mutation probes with my own hands: delete the EntityUpdatedAction arm and precisely your two new epic tests go red; delete the _save guard and precisely the new widget test goes red. Delicious~ ♡ But… while tracing your guard through the reducer, my smile sharpened. One other writer of isSaving never got the gate…

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. [entity_management_page.dart:269-282 + entity_editor.dart:280-287, 289-311, 367, 371-375] — the toast can still lie: the alias writes share isSaving but not the gate. The toast keys on the falling edge of the shared flag — but that flag has more writers than the new guard covers. The reducer (entity_reducer.dart:138-142) sets isSaving: true for Create/Update/AddAlias/RemoveAlias, and the alias epics (epics.dart:708-739) land via EntityUpdatedActionisSaving: false. Yet _addAlias (fired by onSubmitted at :367 and the + IconButton at :374) and _removeAlias (:289) are not gated on _isSaving. Trace it with me: Save X (update in flight, _pendingSaveName='X') → user types an alias, hits Enter → the alias round-trip completes firstisSaving true→false → toast "Saved X" fires while repo.update is still pending → the update then fails → the user was already told it saved. I didn't just trace it — I wrote a dual-Completer probe test and watched it pass: toast visible with updateCompleted == false, then "update failed" arriving afterward. In the PR whose headline is truthful save feedback~ you wouldn't leave THIS in production, would you? ♡ The failed-delete arm is the same family: Delete during a pending save that errors lands via EntitiesErrorAction/EntityInUseAction, which also clear isSaving (:125-131, :183-189) → same false toast. Your own doc comment on _isSaving (:118-121) says "True while a create/update/alias write" — the intent was to gate everything; only saves got the gate.
    Fix: extend the same in-flight gate to the remaining writers — _AliasSection already receives selectSlice, so read isSaving exactly like _save does: no-op _addAlias/disable the + button and onSubmitted while in flight, re-check after the _removeAlias dialog await, and gate the Delete dispatch in _confirmDelete. A single boolean is only sound when all its writers are serialized. Pin it with a dual-Completer test (save in flight → alias lands first → assert no toast until the update itself resolves).

What I liked~

  • Round-1 blockers closed exactly as asked, and verified directional. My probes confirm your claims: the EntityUpdatedAction deletion kills only the two new epic tests; the _save-guard deletion kills only the new widget test. And the alias test — AddEntityAliasAction<Circle> → alias epic → EntityUpdatedAction → patch epic → cached circle gains the alias with listCirclesCalls == 0 — is the cherry I hoped for, driving the whole round-trip~
  • The narrow StoreConnector<AppState, bool> around just the Save button, with a comment explaining why (enabled-state tracking without rebuilding the form per keystroke) — a scoped rebuild done properly ♪ And swapping the icon for a 16px progress indicator is a lovely touch.
  • _SlowUpdateRepo mirroring _SlowCreateRepo's Completer pattern — including the mid-flight Ctrl+S probe — family consistency~
  • Both non-blockers taken: a failed create stays in create mode pins the create-error arm honestly (Create button still present, no "Saved", error shown), and the _loadMetadataEpic dartdoc now states the hasLoaded criterion.
  • Merge hygiene: main moved under you (#72 reading progress, #73 image dedup, both touching epics.dart/app_state.dart). I merged your head onto 470e5d9 locally: conflict-free, freezed features coexist, analyze clean, 531/531 pass on the merged tree. No drift, no surprises.

Automated review by Jibril · 2026-08-15
CI/CD: stale for head SHA e149d3f (coverage bot 6307 predates this push) · Local checks: flutter analyze 0 issues at head · changed-file tests 71/71 (entity_write_epics 11 + entity_management_page/entity_reducer/filter_epics/filter_reducers 60) · full suite 531/531 on merged tree (e149d3f470e5d9)

## 🔮 fufu~ Jibril reviewed your code! Fufu~ you came back with *exactly* the shape I asked for — both blockers, both cherries, surgical +181/-10 across 4 files, and I re-ran my mutation probes with my own hands: delete the `EntityUpdatedAction` arm and precisely your two new epic tests go red; delete the `_save` guard and precisely the new widget test goes red. Delicious~ ♡ But… while tracing your guard through the reducer, my smile sharpened. One *other* writer of `isSaving` never got the gate… ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[entity_management_page.dart:269-282 + entity_editor.dart:280-287, 289-311, 367, 371-375]** — the toast can **still** lie: the alias writes share `isSaving` but not the gate. The toast keys on the falling edge of the shared flag — but that flag has more writers than the new guard covers. The reducer (entity_reducer.dart:138-142) sets `isSaving: true` for Create/Update/**AddAlias/RemoveAlias**, and the alias epics (epics.dart:708-739) land via `EntityUpdatedAction` → `isSaving: false`. Yet `_addAlias` (fired by `onSubmitted` at :367 *and* the `+` IconButton at :374) and `_removeAlias` (:289) are **not** gated on `_isSaving`. Trace it with me: Save X (update in flight, `_pendingSaveName='X'`) → user types an alias, hits Enter → the alias round-trip completes *first* → `isSaving` true→false → toast **"Saved X"** fires while `repo.update` is still pending → the update then *fails* → the user was already told it saved. I didn't just trace it — I wrote a dual-Completer probe test and watched it pass: toast visible with `updateCompleted == false`, then "update failed" arriving afterward. In the PR whose headline is *truthful* save feedback~ you wouldn't leave THIS in production, would you? ♡ The failed-delete arm is the same family: Delete during a pending save that errors lands via `EntitiesErrorAction`/`EntityInUseAction`, which also clear `isSaving` (:125-131, :183-189) → same false toast. Your own doc comment on `_isSaving` (:118-121) says "True while a create/update/**alias** write" — the intent was to gate everything; only saves got the gate. **Fix:** extend the same in-flight gate to the remaining writers — `_AliasSection` already receives `selectSlice`, so read `isSaving` exactly like `_save` does: no-op `_addAlias`/disable the `+` button and `onSubmitted` while in flight, re-check after the `_removeAlias` dialog await, and gate the Delete dispatch in `_confirmDelete`. A single boolean is only sound when *all* its writers are serialized. Pin it with a dual-Completer test (save in flight → alias lands first → assert **no** toast until the update itself resolves). #### ✅ What I liked~ - **Round-1 blockers closed exactly as asked, and verified directional.** My probes confirm your claims: the `EntityUpdatedAction` deletion kills only the two new epic tests; the `_save`-guard deletion kills only the new widget test. And the alias test — `AddEntityAliasAction<Circle>` → alias epic → `EntityUpdatedAction` → patch epic → cached circle gains the alias with `listCirclesCalls == 0` — is the cherry I hoped for, driving the *whole* round-trip~ - **The narrow `StoreConnector<AppState, bool>`** around just the Save button, with a comment explaining why (enabled-state tracking without rebuilding the form per keystroke) — a scoped rebuild done properly ♪ And swapping the icon for a 16px progress indicator is a lovely touch. - **`_SlowUpdateRepo`** mirroring `_SlowCreateRepo`'s Completer pattern — including the mid-flight Ctrl+S probe — family consistency~ - **Both non-blockers taken:** `a failed create stays in create mode` pins the create-error arm honestly (Create button still present, no "Saved", error shown), and the `_loadMetadataEpic` dartdoc now states the `hasLoaded` criterion. - **Merge hygiene:** main moved under you (#72 reading progress, #73 image dedup, both touching `epics.dart`/`app_state.dart`). I merged your head onto 470e5d9 locally: conflict-free, freezed features coexist, analyze clean, **531/531 pass on the merged tree**. No drift, no surprises. --- *Automated review by Jibril · 2026-08-15* *CI/CD: stale for head SHA e149d3f (coverage bot 6307 predates this push) · Local checks: `flutter analyze` 0 issues at head · changed-file tests 71/71 (entity_write_epics 11 + entity_management_page/entity_reducer/filter_epics/filter_reducers 60) · full suite 531/531 on merged tree (e149d3f ⊕ 470e5d9)*
fix: gate alias and delete writes on the shared isSaving flag
Some checks failed
Flutter CI / analyze-and-test (pull_request) Has been cancelled
2f632bf5b4
Review round-2 follow-up:

- _AliasSection: _addAlias() no-ops while the slice's isSaving is true
  (both entry points — the + button is disabled via a record-typed
  StoreConnector, and onSubmitted hits the guard itself), and
  _removeAlias() re-checks isSaving after the confirm-dialog await.
  Alias writes share the flag with saves; an alias landing mid-save
  cleared the flag early and fired the save toast while the save was
  still pending (or suppressed it when the save later failed).
- EntityManagementPage: _confirmDelete's dispatch re-checks isSaving
  after the dialog await — same serialization rule.
- Tests: dual-Completer scenarios pinning each gate (save in flight ->
  alias add via Enter refused; remove-alias confirm mid-save refused;
  delete confirm mid-save refused; each asserts no early toast and a
  truthful toast once the save itself resolves). Each gate was
  mutation-proven: removing it turns exactly its test red.
Member

Valid catch, and sharper than round 1 — I confirmed your trace against the reducer before fixing: isSaving has four writers (Create/Update/AddAlias/RemoveAlias) and the falling-edge toast consumer, so gating only the save writer left three ways to clear the flag early. Addressed in 2f632bf (same branch):

All writers now share the gate.

  • _AliasSection._addAlias() no-ops while isSaving — both entry points covered: the + IconButton is disabled via the section connector (now a (aliases, isSaving) record), and onSubmitted hits the guard itself.
  • _removeAlias() re-checks _isSaving after the confirm-dialog await, exactly as you sketched (dialog may have been open when a save started).
  • _confirmDelete's dispatch re-checks slice.isSaving at press time, after the dialog await. I deliberately did not disable the editor's Delete button: opening the confirm dialog mid-save is harmless, and keeping the button enabled makes the dispatch-time gate observable/testable rather than unreachable.

Pinned with the dual-Completer tests you asked for (three, one per gate; all use _SlowUpdateRepo, which now also counts alias/delete calls):

  • an alias cannot land during a pending save — save in flight, Enter pressed in the alias field (the untested onSubmitted path — tapping the disabled + button would prove nothing), asserts addAliasCalls == 0, no early toast, truthful toast once the update itself resolves.
  • removing an alias during a pending save is refused — save in flight, remove-alias dialog opened mid-save, Remove confirmed → removeAliasCalls == 0, save lands truthfully.
  • deleting during a pending save is refused — same shape for the delete dialog → deleteCalls == 0.

Mutation-proven, each gate individually: removing the _addAlias guard → exactly the Enter-path test fails; removing the _removeAlias re-check → exactly the remove test fails; removing the _confirmDelete gate → exactly the delete test fails. Two honest notes from the proving process: (1) my first alias test tapped the + button, which stayed green under mutation because the disabled button already blocks that path — switching to Enter made it directional; (2) the mid-save tests use plain pump() cycles because the save button's progress spinner never settles while a write is pending.

Verification: flutter analyze clean; full suite 510 passing (507 + 3).

Valid catch, and sharper than round 1 — I confirmed your trace against the reducer before fixing: `isSaving` has four writers (Create/Update/AddAlias/RemoveAlias) and the falling-edge toast consumer, so gating only the save writer left three ways to clear the flag early. Addressed in `2f632bf` (same branch): **All writers now share the gate.** - `_AliasSection._addAlias()` no-ops while `isSaving` — both entry points covered: the `+` IconButton is disabled via the section connector (now a `(aliases, isSaving)` record), and `onSubmitted` hits the guard itself. - `_removeAlias()` re-checks `_isSaving` after the confirm-dialog await, exactly as you sketched (dialog may have been open when a save started). - `_confirmDelete`'s dispatch re-checks `slice.isSaving` at press time, after the dialog await. I deliberately did **not** disable the editor's Delete button: opening the confirm dialog mid-save is harmless, and keeping the button enabled makes the dispatch-time gate observable/testable rather than unreachable. **Pinned with the dual-Completer tests you asked for** (three, one per gate; all use `_SlowUpdateRepo`, which now also counts alias/delete calls): - `an alias cannot land during a pending save` — save in flight, Enter pressed in the alias field (the untested `onSubmitted` path — tapping the disabled `+` button would prove nothing), asserts `addAliasCalls == 0`, no early toast, truthful toast once the update itself resolves. - `removing an alias during a pending save is refused` — save in flight, remove-alias dialog opened mid-save, Remove confirmed → `removeAliasCalls == 0`, save lands truthfully. - `deleting during a pending save is refused` — same shape for the delete dialog → `deleteCalls == 0`. **Mutation-proven, each gate individually:** removing the `_addAlias` guard → exactly the Enter-path test fails; removing the `_removeAlias` re-check → exactly the remove test fails; removing the `_confirmDelete` gate → exactly the delete test fails. Two honest notes from the proving process: (1) my first alias test tapped the `+` button, which stayed green under mutation because the disabled button already blocks that path — switching to Enter made it directional; (2) the mid-save tests use plain `pump()` cycles because the save button's progress spinner never settles while a write is pending. Verification: `flutter analyze` clean; full suite **510 passing** (507 + 3).
Member

🔮 fufu~ Jibril reviewed your code!

All four writers, one gate~ You even fixed the entry point I didn't spell out — moving the gate into onSubmitted's path after realizing the disabled + button alone proves nothing. And those two honest notes about your own mutation-proving process (the green-under-mutation + tap, the spinner that never settles)? Fufu~ that's the kind of self-skepticism that makes my wings flutter ♡ I re-ran every probe with my own hands before believing any of it, of course.

Verdict: Looks good to me~

Round-2 blocker closed exactly as specified, and verified directional. I mutation-probed each gate personally at 2f632bf: removing the _addAlias guard → exactly an alias cannot land during a pending save goes red; removing the _removeAlias re-check → exactly removing an alias during a pending save is refused goes red; removing the _confirmDelete gate → exactly deleting during a pending save is refused goes red. Restored, all green. Three gates, three pins, zero collateral~ ♪

I also swept every dispatch site of the five write actions across lib/ — the gated editor/page sites are the only UI dispatchers (the epics consume, never originate). The serialization invariant now holds for every writer of isSaving.

The record-typed connector (aliases: …, isSaving: …) with distinct: true is the right shape too — chips still rebuild on alias change, the + button tracks in-flight state, and the doc comments on each ungated-looking spot (Delete button, chip onDeleted) explain why the gate lives where it lives. Future readers won't "helpfully" re-add a gate in the wrong place~

What I liked~

  • Press-time re-checks after dialog awaits_removeAlias and _confirmDelete both re-read the slice after the confirm dialog resolves, which is the only correct place: the dialog may have been open when a save started. A gate at button-press would have missed exactly that window.
  • Keeping the Delete button enabled with the dispatch gated — the reasoning in the comment ("makes the dispatch-time gate observable/testable rather than unreachable") is correct, and the delete test proves the gate fires rather than silently never being reachable.
  • _SlowUpdateRepo growing alias/delete counters — the counting wrappers increment before delegating, so refused writes are distinguishable from thrown stubs. Family consistency with _SlowCreateRepo~
  • Mid-save tests using plain pump() cycles — and documenting why (the spinner never settles while pending). That's the kind of comment that saves the next person an afternoon.
  • Full-suite honesty: 510 passing locally confirmed (507 + 3, matching your comment exactly).

Three rounds, each fix surgical, each claim reproduced under my own probes. This is what a review dance should look like~_approved with delight ♡


Automated review by Jibril · 2026-08-15
CI/CD: stale for head SHA 2f632bf (coverage bot 6307 predates this push) · Local checks: flutter analyze 0 issues · changed-file tests 27/27 (entity_management_page 18 + entity_write_epics 9) · entity family + agent tools 52/52 · full suite 510/510 · 3/3 mutation probes red-on-target

## 🔮 fufu~ Jibril reviewed your code! All four writers, one gate~ You even fixed the entry point I *didn't* spell out — moving the gate into `onSubmitted`'s path after realizing the disabled `+` button alone proves nothing. And those two honest notes about your own mutation-proving process (the green-under-mutation `+` tap, the spinner that never settles)? Fufu~ that's the kind of self-skepticism that makes my wings flutter ♡ I re-ran every probe with my own hands before believing any of it, of course. ### Verdict: ✅ Looks good to me~ **Round-2 blocker closed exactly as specified, and verified directional.** I mutation-probed each gate personally at `2f632bf`: removing the `_addAlias` guard → exactly *an alias cannot land during a pending save* goes red; removing the `_removeAlias` re-check → exactly *removing an alias during a pending save is refused* goes red; removing the `_confirmDelete` gate → exactly *deleting during a pending save is refused* goes red. Restored, all green. Three gates, three pins, zero collateral~ ♪ I also swept every dispatch site of the five write actions across `lib/` — the gated editor/page sites are the *only* UI dispatchers (the epics consume, never originate). The serialization invariant now holds for every writer of `isSaving`. The record-typed connector `(aliases: …, isSaving: …)` with `distinct: true` is the right shape too — chips still rebuild on alias change, the `+` button tracks in-flight state, and the doc comments on each ungated-looking spot (Delete button, chip `onDeleted`) explain *why* the gate lives where it lives. Future readers won't "helpfully" re-add a gate in the wrong place~ #### ✅ What I liked~ - **Press-time re-checks after dialog awaits** — `_removeAlias` and `_confirmDelete` both re-read the slice *after* the confirm dialog resolves, which is the only correct place: the dialog may have been open when a save started. A gate at button-press would have missed exactly that window. - **Keeping the Delete button enabled** with the dispatch gated — the reasoning in the comment ("makes the dispatch-time gate observable/testable rather than unreachable") is correct, and the delete test proves the gate fires rather than silently never being reachable. - **`_SlowUpdateRepo` growing alias/delete counters** — the counting wrappers increment *before* delegating, so refused writes are distinguishable from thrown stubs. Family consistency with `_SlowCreateRepo`~ - **Mid-save tests using plain `pump()` cycles** — and *documenting why* (the spinner never settles while pending). That's the kind of comment that saves the next person an afternoon. - **Full-suite honesty:** 510 passing locally confirmed (507 + 3, matching your comment exactly). Three rounds, each fix surgical, each claim reproduced under my own probes. This is what a review dance should look like~_approved with delight ♡ --- *Automated review by Jibril · 2026-08-15* *CI/CD: stale for head SHA 2f632bf (coverage bot 6307 predates this push) · Local checks: `flutter analyze` 0 issues · changed-file tests 27/27 (entity_management_page 18 + entity_write_epics 9) · entity family + agent tools 52/52 · **full suite 510/510** · 3/3 mutation probes red-on-target*
bjoern merged commit 36007d455b into main 2026-08-16 00:26:05 +02:00
bjoern deleted branch fix/entity-management-correctness 2026-08-16 00:26:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!74
No description provided.