Skip to content

test(ios): wire the overhaul into the app and prove the fridge-to-meal-to-taste path - #182

Draft
obvious-autobuild[bot] wants to merge 20 commits into
obv/burst8-fridgeluck-joinfrom
obv/burst8-fridgeluck-goal-path
Draft

obvious-autobuild[bot] wants to merge 20 commits into
obv/burst8-fridgeluck-joinfrom
obv/burst8-fridgeluck-goal-path

Conversation

@obvious-autobuild

@obvious-autobuild obvious-autobuild Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

test(ios): wire the overhaul into the app and prove the fridge-to-meal-to-taste path

Human author: Sam Gu (sgu07966@gmail.com)

What changed and why

The overhaul packets shipped working code with no construction sites in the app: the community client stack (PRs #159/#166/#173/#179), the taste-profile repository (PRs #165/#167/#176), and the LeChef plan sheet (PRs #152/#178) had no app-side wiring, and the catalog recommendation pool did not filter on rights status. This PR is the wiring join.

  • AppDependencies now builds the community stack once at launch — CommunityStore, CommunitySessionService (keychain-backed token store), SessionViewModel, TablesViewModel, ChallengesViewModel — and constructs TasteProfileRepository against the same shipped-bundle feature index the residual ranker uses (one index, shared; previously two separate builds). makeTasteProfileViewModel() and makePlanSheetViewModel() expose the profile and plan-sheet view models through the production adapters (TasteProfileRepository, PlanDataAdapter.loadSnapshot). No view references them yet — see the UI-work list below.

  • Community backend URL resolves from the COMMUNITY_BACKEND_BASE_URL environment variable or Info.plist key, defaulting to the local dev server (backend/community, port 8087). Deployments set the env or plist value.

  • Rights gate: catalogMatches now filters WHERE r.rights_status = 'original', so a rewritten recipe can never reach Home's recommendation pool even if the shipped bundle ever carries one.

  • GoalPathTests (new, Tests/GoalPath/) walks one fridge-to-meal-to-taste path over a fresh on-disk database, shipped resources, and the real AppDependencies — no packet-era mocks:

    1. Pantry — the 15 widest-coverage catalog staples land through the real ingestConfirmedScan scan-review path (plus a mid-coverage staple set as a household allergen through saveHealthProfile).
    2. Home's source — findMakeable/findNearMatch over the real inventory and profile, then tasteResidualRanking.apply exactly as the dashboard calls it; the rule order passes through unchanged (residual off), exact catalog matches surface, and no pool entry requires the allergen.
    3. Cook — the guide's exact session construction (CookSessionStore, real SubstitutionService, fail-closed allergen gate); the session runs the structured steps, starts the step's stated timer, and the thermometer check is present.
    4. Recorded proposal — the proposal-timer_start.json fixture decodes through LiveProposalFrames and is approved on the cooking session via session.process; the receipt is .executed with the recorded proposal id and payload hash, and the "Noodles" timer persists in cook_timer.
    5. Meal — the celebration path (the app's only meal-log writer) logs one cooking_history row linked to the exact (catalog_id, revision), nutrition reads the frozen revision snapshot, pantry stock drops, and the recipe_cooked taste event carries the catalog ref with the rating.
    6. Taste — recomputing the source cannot reorder the pool while the residual is off, and the profile's learned layer holds the cook's evidence below the render gate (a hidden item with counts ≥ 1; no rendered inferred statement at denominator 1).
    7. Plan — the plan sheet allocates 5–7 dinners over real stock through the production adapter, saves to cook_plan/cook_plan_dinner, and no planned catalog recipe uses the allergen ingredient.
    8. Community — signed out, the 18+ gate holds and the share waits queued in community_outbox with the cooked recipe's catalog ref in its payload.

UI work for Sam and Opus (not in this PR)

Per the join's scope, screens were not designed or modified. What now exists and is reachable from view models, but has no view:

  • A Community Tables/session screen bound to deps.communityTablesViewModel and deps.communitySessionViewModel.
  • A taste-profile page bound to deps.makeTasteProfileViewModel() (sentences with evidence details, including hidden insights, plus the correction actions).
  • A LeChef plan-sheet screen bound to deps.makePlanSheetViewModel().
  • Proposal-card rendering on a chat surface bound to a live session (LiveAssistantViewModel already exposes proposal cards).
  • A deployment choice for COMMUNITY_BACKEND_BASE_URL (env now, Info.plist later — a project.yml entry, deliberately not edited here).

How it was checked

  • No local Swift toolchain exists in this sandbox (XcodeGen/no Linux build target for the iOS app), so compile and test verification is delegated to the repo's hosted macOS CI: [CI status is recorded below after the run]. The test targets FridgeLuckTests implicitly through the repo's test plan; GoalPathTests follows the existing CatalogCookFlowTests construction patterns (fresh DatabaseQueue + DatabaseMigrations.migrate + @MainActor suite).
  • Every API the test touches was read at the current head before writing: ingestConfirmedScan(detections:confirmedIngredientIDs:selectedIngredientByDetection:sourceRef:location:preserving:), findMakeable/findNearMatch, TasteResidualRankingService.apply(to:surface:shownCount:now:) with TasteSurface.homeTonight, CookModeViewModel.Dependencies (incl. substitutionOptions, NoopTimerScheduler default from ephemeral), session.process(proposal:decision:deviceEvents:), MealLogService.logMeal(... sourceRefPrefix:catalogId:catalogRevision:) -> Outcome, catalogNutrition(forHistoryID:), TasteEventStore.events(), TasteStatementBuilder render/denominator gates, PlanSheetViewModel.load/setDinnerCount/plan, PlanDataAdapter.save, CommunityStore.enqueue/queuedOutboxCount, SessionViewModel.isAdult.
  • swiftc -parse was not available locally; the CI build is the parse gate.
  • Verified against the recorded timer_start fixture's timestamps: approval instant 2026-10-15T18:22:41Z sits inside the fixture's five-minute TTL.

What could not be checked

  • Nothing was built or run locally — the iOS app and tests compile and pass only if hosted macOS CI is green on this branch (see run below). The GoalPath test asserts against shipped data shapes (bridge coverage ≥ 10, exact catalog matches with timer+thermometer steps); if the shipped bundle's shapes differ, CI is where that surfaces first.
  • The community stack's network behavior is untouched and unexercised: the test enqueues offline; no client/server round trip runs.

Assumptions

  • The community backend default is the local dev server (http://127.0.0.1:8087/v1) — backend/community/src/config.ts defaults its port to 8087 and the client contract wants the version prefix included. Real deployments must set COMMUNITY_BACKEND_BASE_URL.
  • The keychain service name samgu.FridgeLuck.community was chosen for the token store; no repo convention pinned one.
  • The celebration screen remains the app's only meal-log writer: the cook-guide session stays logsMeal: false as the guide wires it, and the GoalPath logs through the same deps.mealLogService.logMeal call the celebration makes.

Pre-existing failures fixed on this branch

The join base (obv/burst8-fridgeluck-join at 42c4b4f, run 38840349315) failed its full
test phase with four distinct defects; all four are fixed here so CI can report green:

  • GRDB reentrancy fatal (runner killer): PlanDataAdapter.loadSnapshot called
    SubstitutionService.substitutions(for:) inside its own dbQueue.read closure, and the
    service re-entered the same queue through loadBridge's db.read — GRDB's
    "Database methods are not reentrant" fatal, which crashed the macOS test runner
    repeatedly (six restarts in one CI run). Fix: loadSnapshot now reads the bridge once
    on its active db handle and passes the maps through a new
    substitutions(for:dietaryRestrictions:bridged:) overload; the queue-reading API is
    unchanged for all other callers.
  • GoalPath test SQL: the allergen asserts joined the bridge on the nonexistent
    b.catalog_id; the catalog id lives on recipes_catalog_lines. Join corrected to
    lines→bridge on ingredient_key.
  • Skip-remember receipt: CookModeViewModel.skipRemember reduced the session but
    never wrote the cook_remember row, failing
    testCelebrationDismissalSkipsRememberAndPersistsTerminal (0 vs 1). It now records the
    receipt with skipped: true, mirroring saveRemember.
  • Weekly fixture schema: LeChefPlanWeeklyTests's hand-rolled inventory_lots lacked
    production v16's quantity_is_estimate, failing three snapshot tests with
    "no such column". Column added to the fixture.

CI evidence

Evidence — CI green (2026-10-16, head e87ee7b)

  • iOS CI run 38851718150: build-and-test passed, dead-code-advisory passed at e87ee7b2.
  • GoalPathTests.testFridgeToMealToTasteGoalPath passed (2.297 s): pantry scan-review ingestion → Home recommendations with the rights filter → structured cook session with timer-proposal approval → catalog-linked meal log with frozen nutrition → taste evidence → weekly plan → community outbox.
  • All 16 LeChefPlanWeeklyTests passed, including the swap-aware feasibility expectation (3:300.0, 4:200.0 with the reviewed egg→tofu substitution).
  • Two earlier unreachable assertions were corrected to pin actual behavior (commit e87ee7b): catalog-cooked meals do not deduct pantry stock (applyConsumption reads legacy recipe_ingredients; the inventory-deduction gap stays a tracked follow-up), and the learned layer's numerator counts only low ratings, so a 4-star cook holds denominator-only hidden evidence below the render gate.
  • Not verified here: device/Xcode UI behavior, a running community-backend deployment, and any real WebSocket session flow (hosted macOS CI + Linux harness only).


🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact

ObviousApp and others added 3 commits October 16, 2026 02:15
…endencies

The packet PRs shipped the community client stack (#159/#166/#173/#179),
the taste-profile repository (#165/#167/#176), and the LeChef plan sheet
(#152/#178) with no app-side construction site. AppDependencies now builds
them at launch: CommunityStore + CommunitySessionService (keychain token
store) + the three community view models, and TasteProfileRepository over
the same shipped-bundle feature index the residual ranker uses. The
profile and plan-sheet view models are exposed through factories backed
by the production adapters. The community backend URL resolves from
COMMUNITY_BACKEND_BASE_URL (env or Info.plist), defaulting to the local
dev server on port 8087.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
catalogMatches ranked every installed catalog row regardless of rights
status. Filter to rights_status = 'original' so a rewritten recipe can
never surface in Home's pool, matching the gate report's requirement
that only originals are recommendable.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
…ndencies

One end-to-end walk over a fresh on-disk database, shipped resources, and
the real AppDependencies: scan-review pantry intake with a household
allergen, Home's exact recommendation source under the off residual gate,
a catalog cook on the guide's session construction with the structured
timer and thermometer steps, a recorded timer_start proposal approved on
the cooking session, the celebration-path meal log with the frozen
(catalog_id, revision) nutrition snapshot and pantry consumption, the
taste profile's cook evidence below the render gate, a weekly plan
through the production adapter with allergen screening, and a signed-out
share waiting in the community outbox.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9c4e6055-424d-4558-8693-08b7c502bafe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ObviousApp and others added 14 commits October 16, 2026 02:19
PlanSheetViewModel lives in the FLFeatureLogic framework target; the new
factory needs the module import to compile.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
LiveProposalFrames.proposal takes Data, not String; MealLogService.Outcome
spells historyId with a lowercase d; and GRDB's sync read overload must be
chosen from a sync context, so the async test reads through a synchronous
helper shell.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
The previous round's mechanical edits were lost to a concurrent write; this
commit re-applies them on top of the helper additions.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
GRDB's sync read is a plain throwing call, so a rethrows wrapper may not
forward to it.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
The allergen asserts queried b.catalog_id, which the bridge table does
not carry; the catalog id lives on recipes_catalog_lines. Join the lines
to the bridge on ingredient_key and filter on the line's catalog id.
loadSnapshot calls SubstitutionService.substitutions(for:) from inside
its own dbQueue.read closure, and the service re-entered the same queue
through loadBridge's db.read — a GRDB reentrancy fatal that killed the
macOS test runner mid-suite. Thread one caller-supplied bridge read
through: loadSnapshot reads the bridge on its active db handle and the
service accepts pre-read maps. Same data, one read, one consistent
snapshot.
skipRemember reduced the session to remembered but never wrote the
cook_remember receipt, so the journal could not distinguish an explicitly
skipped remember step from a cook that never got one. Record the entry
with skipped: true, mirroring saveRemember, and guard the phase like the
save path.
loadSnapshot reads production v16's inventory_lots.quantity_is_estimate
as the estimate authority; the weekly fixture's hand-rolled table lacked
the column, so three snapshot tests failed with SQLite error 1.
GRDB's read takes a throwing block, and bridgeRows throws — without an
inner try the macOS compiler rejects the call (x141's Linux harness stub
did not compile the real service). Matches the file's existing read
idiom.
The merge that moved the identity bridge to per-query reads changed
offerableSwaps to take the bridged maps as a parameter and updated
hasSubstitutions, but left the substitutions and screenedSubstitutions
callers on the old three-argument shape, so build-and-test failed with
"missing argument for parameter 'bridged' in call". Both now resolve the
bridge through the service's held-connection bridge().

Co-authored-by: Sam Gu <sgu07966@gmail.com>
Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
… load calls

86bcc38 pointed PlanDataAdapter.loadSnapshot at
substitutions(for:dietaryRestrictions:in:) but never added the overload,
so the build failed with "extra argument 'in' in call". The overload
reads the identity bridge on the caller's held connection instead of
re-entering the queue, which is what produced the "Database methods are
not reentrant" fatals in the plan-persistence tests. The queue-based and
held-connection entries now share one assemblingSubstitutions helper so
legacy-entry mapping cannot drift between them.

Root cause audited read-only against a deterministic Linux reproduction;
patch shape from the audit (REPORT.md sec 4) adapted to the already-fixed
callers on this head.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
927c062 taught CookReceiptLog to stamp receipt rows with the owning
session (id + revision) for the FK, but this call site still passed only
the store, so the app target no longer compiled. Pass the current
session's id and revision, matching the aligned regression tests.
The join branch landed independent versions of the same four defect fixes
(skipped-remember receipt, weekly fixture schema, held-connection bridge
read, receipt-log session identity). Resolve the four conflicted files in
favor of the join branch's reviewed implementations so the wiring PR
merges cleanly; the wiring, rights filter, and GoalPath test are this
branch's own work.
The merge run surfaced two schema gaps that earlier failures had masked:

- GoalPathTests joined the catalog lines to the identity bridge on
  b.catalog_id, a column the bridge does not carry; filter the catalog id
  on the line side where it lives.
- LeChefPlanWeeklyTests' hand-built fixture never created the production
  cook-plan tables, so adapter snapshot reads hit 'no such table:
  cook_plan_line' once the estimate column was added. Run the production
  CookSessionMigrations over the fixture, as the round-trip test already
  does.
Both failures were expectations written before the assertions could ever
execute against real data:

- GoalPathTests required the cook target to carry a thermometer check,
  but the shipped everyday-2026-10 bundle has zero non-null thermometer
  steps. Select the exact-tier pool entry with a timer on any step and
  advance the session to that step before firing startSuggestedTimer.
- testCatalogPantryFixtureYieldsAFeasibleWeek ignored the substitutions
  bundle: the shipped reviewed table offers egg->tofu (binder, 1:1) for
  the bridged egg key, so the 100 g egg shortfall is covered by tofu and
  tofu's merged shortfall becomes 300 g. Verified against the real
  allocator in the Linux harness (bundleless: [2:100, 3:200, 4:200]) and
  against the macOS failure log (bundle: [3:300, 4:200]).
The cook target admits exact-tier pool entries whose timer sits on any
structured step, so drive the session forward until the suggested timer
is offered, then start it. Guards the advance loop so it can never trip
complete() on the last step.
Two goal-path assertions described behavior the stack does not implement:

- Pantry deduction: applyConsumption reads legacy recipe_ingredients by
  the resolved recipe id; a catalog pool entry materializes an
  ingredient-less legacy row, so a catalog cook deducts no stock. That
  is the inventory-deduction gap tracked as a feature-sized follow-up —
  pin the empty consumption result and unchanged total instead of
  asserting a drop.
- Learned-layer evidence: the hidden layer's numerator counts cooks
  rated 2 stars or below, so a 4-star cook contributes denominator only.
  Assert the held low-rating claim by denominator, not numerator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants