fix: refresh gap suggestions from the live wardrobe and actually save from them - #52
Merged
Conversation
FakeWardrobeRepository.addItem dropped the three optional fields, so no test could assert that adding an item covers a suggestion's subcategory, and getGapRecommendations was hardcoded to an empty list with no call counter — leaving no way to prove a wardrobe write does not trigger a new paid AI call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The filter lived inline in GapsViewModel and was applied to the capsule fallback only. Pulling it into the domain model lets the AI list reuse it and makes it testable on its own; GapRecommendationParsingTest re-implemented the expression instead of calling it, so it would have kept passing even if production stopped filtering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suggestions were computed once in init and on the LoadGaps intent, which is wired only to the error-state retry button — so adding a polo shirt on the Wardrobe tab left "Polo shirt" on the Gaps tab until the ViewModel happened to be recreated. The screen stays alive on both platforms (HorizontalPager on Android, @StateObject on iOS), so that could be a long while. State is now derived from observeAll(), and the ownership filter applies to the AI list as well as the capsule fallback: a wardrobe write re-filters both for free, without spending another Claude request. The AI list is still fetched only on init, on an availability change, or on an explicit retry. Also default the AI failure message — a null Throwable.message left state.error null, which rendered the "your wardrobe looks complete" screen on a failure. Fixes part of #42 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Add to Wardrobe" on a gap opened the add sheet with
`onSave = { _,_,_,_,_,_,_,_ -> dismiss }` on both platforms: the photo, the
name and every tag the user filled in were dropped on the floor, so the
suggestion they had just satisfied stayed on the list.
GapsViewModel now owns the save — it already had the WardrobeRepository — so
the item goes through the same path as the Wardrobe tab and the derived state
drops its suggestion on the resulting DB emission. Resolving a second
WardrobeViewModel here would have leaked an uncancelled observeAll() collector
on iOS, where it is a Koin factory, and coupled the two screens on Android,
where it is not.
The pre-fill also moves off `existingItem` onto a new `prefillItem`: seeding
through `existingItem` put the sheet in editing mode, so it read "Edit item" /
"Save changes" and hid the AI badge for an item that did not exist yet.
Neither screen collected its effects before, so errors were silent; both do now.
Fixes #42
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Android resets it (AddItemSheet.kt) but iOS did not, so picking Tops → Polo and then switching to Bottoms saved a BOTTOM item carrying Subcategory.POLO. Gap suggestions match on subcategory alone, so that item silently suppressed the "Polo shirt" suggestion forever — the same symptom as #42, from a different cause. Reset in the picker action rather than .onChange(of: selectedCategory), which also fires while seeding the form from existingItem/prefillItem and would wipe a legitimately pre-filled subcategory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers the prefill sheet reading "Add new item" / "Save to wardrobe" rather than the editing copy, and stops before the photo, which is external input. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`:shared:allTests` is an aggregate lifecycle task and rejects `--tests`; the per-target `:shared:testAndroidHostTest` accepts it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #42 — "added a polo shirt but it is still displayed in the suggestions".
Two independent defects each reproduced that report:
GapsViewModelcomputed the list once ininit(and on theLoadGapsintent, which is wired only to the error-state Retry button), reading the wardrobe through the one-shotgetAll()rather thanobserveAll(). The Gaps screen stays alive on both platforms —HorizontalPageron Android,@StateObjecton iOS — so adding an item on the Wardrobe tab left the suggestion on screen until the ViewModel happened to be recreated. That is the "updated later" in the issue thread.AddItemSheetwithonSave = { _,_,_,_,_,_,_,_ -> dismiss }— no repository call, no photo written. A user who tapped the Polo shirt suggestion, filled the form and saved got nothing stored, so the suggestion legitimately stayed.Suggestions are now derived from the live wardrobe rather than fetched-and-cached, and the ownership filter applies to AI recommendations as well as the local capsule fallback — so both lists re-filter the instant an item is added or deleted anywhere, without spending another Claude request. The AI list itself is still fetched only on init, on an AI-availability change, or on an explicit retry.
Changes
excludingOwnedextracted intoGapRecommendation.ktand applied to both the AI list and the capsule fallback. Matches onSubcategoryalone (mappedCategorydisagrees by design) and keeps recommendations the AI left untagged, since a null subcategory has nothing to compare against.GapsViewModelrestructured —combine(observeAll().catch { … }, _uiState).stateIn(viewModelScope, Eagerly), mirroringWardrobeViewModel. A wardrobe emission never triggers an AI call; a test pins the call count.GapsIntent.AddItemonGapsViewModel, which already hadWardrobeRepositoryinjected. Resolving a secondWardrobeViewModelwas rejected: it is a KoinfactoryOf, so on iOS that would leak an uncancelledobserveAll()collector per screen, and on Android it would share the Wardrobe tab's instance and close its sheet too.prefillItemparam onAddItemSheet(both platforms). Seeding throughexistingItemput the sheet in editing mode — "Edit item", "Save changes", AI badge hidden — for an item that did not exist yet.GapsEffect.ShowErrorwas dropped on the floor everywhere.Throwable.messageleftstate.errornull, which rendered the "your wardrobe looks complete" screen on a failure.selectedSubcategorywhen the category changes, mirroring Android. Without it an iOS user could save aBOTTOMitem carryingSubcategory.POLO, which permanently suppressed the Polo shirt suggestion — the same symptom as Check fallback suggestions logic #42 from a different cause. Reset in the picker action rather than.onChange(of: selectedCategory), which would also fire while seeding the form and wipe a legitimate prefill.FakeWardrobeRepository.addItemsilently droppedsubcategory/fit/material, andgetGapRecommendationswas hardcoded to an empty list — neither the regression test nor the no-extra-AI-call assertion was expressible before.GapRecommendationParsingTestre-implemented the filter expression inline instead of calling production code, so it would have kept passing if production stopped filtering; moved toGapRecommendationFilterTestincommonTest.CLAUDE.mdnow requires branching offmain, and the documented single-test command is corrected —:shared:allTestsis an aggregate task that rejects--tests.Test plan
./gradlew :shared:allTests— 160 tests green, including 18 new inGapsViewModelTestand 7 inGapRecommendationFilterTest.adding an item to the wardrobe removes its suggestion without any intent,adding an item re-filters the AI list without a new AI call,adding the recommended item removes it from the suggestions../gradlew :composeApp:assembleDebugand./gradlew detektclean.journeys/gaps-add-suggestion.xmlcovers the prefill sheet reading "Add new item" / "Save to wardrobe" rather than the editing copy, stopping before the photo. Existingjourneys/gaps-common-suggestions.xmlis untouched and must still pass.Not verified: the Swift changes are not compile-checked —
linkDebugFrameworkIosSimulatorArm64is skipped on Linux and there is no Xcode in this environment. The KotliniosSimulatorArm64compilation does pass, so the exportedGapsIntentAddItem/GapsEffectItemAddedsymbols exist, but the iOS app needs a build on macOS before merge.Related
Filed #51 while working on this:
Subcategory.HOODIEsits underCategory.TOPwhile the capsule hoodie gap isCategory.OUTERWEAR, so the pre-filled sheet shows a subcategory its own dropdown cannot re-select. Not blocking — the filter matches on subcategory alone precisely because those two disagree.Checklist
./gradlew detektpasses🤖 Generated with Claude Code