This repository has been archived on 2026-10-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
hope-voice-ios/.artifacts/20260831-105824-plan-code-ui-audit/iterations/04-review.md
T
2026-08-31 19:50:46 -04:00

15 KiB
Raw Blame History

Review Report

  • Task: 20260831-105824-plan-code-ui-audit
  • Agent: reviewer
  • Model: openai-codex/gpt-5.6-sol
  • Effort: high
  • Reviewed: 2026-08-31T13:02:29Z
  • Verdict: changes-requested

Findings

1. High — 12 newly claimed tests are not in the generated Xcode project; the reported count is impossible

  • Evidence: Tests/PinnedMediaResourceLoaderTests.swift defines 7 tests and Tests/ConversationStateTests.swift defines 5, but neither filename appears anywhere in Voice.xcodeproj/project.pbxproj. The current test log starts only MediaAPITests, MediaReconcileTests, and TranscriptDedupeTests. XcodeBuildMCP therefore reports 16 tests: 1 API + 7 media reconcile + 8 transcript tests. It does not run either new file.
  • Contradicted claim: iterations/04-implementation.md:88-94 says 16 total while separately claiming 7 transcript + 7 resource-loader + 5 conversation-state tests plus existing media/API tests. Those components total 28, not 16. iterations/03-implementation.md:49-54 likewise claims the undiscovered resource tests ran.
  • Additional failures hidden by non-discovery:
    • PinnedMediaResourceLoaderTests.swift:67 expects https://192.168.86.2:9178/stream/track-123, but MediaAPI correctly builds under its /media/api base (Sources/Media/MediaAPI.swift:125-133,185-190), so the expected URL must be https://192.168.86.2:9178/media/api/stream/track-123.
    • ConversationStateTests.swift:18-26 expects listening after two zero ticks. EMA remains 0.02304 after those ticks, above the 0.02 agent threshold, so the test needs a third zero tick or a deterministic threshold-crossing loop.
  • Impact: the parser, stream URL, and new state-machine tests have never compiled or executed. Current “16 passed” does not verify the claimed fixes.
  • Correction: regenerate Voice.xcodeproj from project.yml, correct the two invalid expectations, rerun all expected 28 tests, and report suites/counts from the actual result bundle.

2. High — visible history duplicates every live final, while legitimate repeated finals are still lost

  • Evidence: every accepted final is added both to events and history.entries (Sources/LiveKit/TranscriptHandler.swift:45-61,64-89). TranscriptListView.visibleRows then concatenates all history entries with all live events (Sources/Features/Conversation/OrbView.swift:78-92). A live final therefore renders once as a “restored” history row and once as its live event.
  • Repeat semantics: dedupe state records only role/text/time, not source (TranscriptHandler.swift:31-35,45-60,82-88). Two distinct primary utterances with identical text inside five seconds are suppressed even though cross-source dedupe should suppress only the opposite-source copy. After five seconds, TranscriptHistory.add still permanently drops an adjacent same-role/same-text repeat (Sources/LiveKit/TranscriptHistory.swift:25-31), so it will not survive relaunch.
  • Test gap: TranscriptDedupeTests.swift:100-120 is named “legitimate repeat after dedupe window,” but does not advance time and explicitly asserts that the repeat is suppressed. There are no restoration/max-50/live-visible-row/clear integration tests requested by iteration 04.
  • Impact: current messages appear twice; valid repetitions such as “yes” twice can disappear from events within five seconds or from persisted history indefinitely.
  • Correction: snapshot restored entries separately from live-session history, or render a single canonical transcript collection; do not concatenate two stores containing the same finals. Track source in recent dedupe records and consume only an opposite-source duplicate. Remove permanent dedupe from persistence. Inject a clock and test before/after-window same-source repeats, cross-source pairs, restoration, live append, max-50, and clear.

3. High — server URL changes still write/apply per keystroke, active-session changes are dropped rather than deferred, and invalid values still fall back silently for media

  • Evidence: Settings continues assigning settings.serverURL from every urlText change (Sources/Features/Settings/SettingsView.swift:18-24), and SettingsStore persists each assignment. Conversation replacement fires for every intermediate value that parses as a valid URL (Sources/Features/Conversation/ConversationView.swift:71-83), so typing a valid hostname can launch repeated unstructured disconnect/replacement tasks.
  • The active-session guard simply returns (ConversationView.swift:75); there is no state observer or pending configuration, so the change is never applied when the session later becomes idle.
  • makeSession mutates an invalid value to the default before onAppear validation (ConversationView.swift:22-35,58-61), so the promised original-value error cannot appear. Invalid values entered later get no visible validation error. Media still silently uses the default host for invalid visible settings (Sources/Features/Media/MediaPanelView.swift:448-459).
  • Impact: the iteration-04 configuration contract is not met; conversation/media can use stale or fallback configuration, and valid typing can race multiple session replacements.
  • Correction: keep draft URL state local, validate on explicit Apply/Submit, show validation in Settings, persist only accepted values, serialize one replacement, and queue a pending accepted config until the current session reaches idle. Media must use the last accepted config, never derive a fallback from invalid draft text.

4. High — terminal cleanup can recursively react to its own disconnect, agent-timeout retry can be mistaken for terminal loss, and non-timeout connect failures still bypass cleanup

  • Evidence: teardownForFailure() calls teardownRoom() while session state remains .ready/.waitingAgent (Sources/LiveKit/VoiceSession.swift:182-212). teardownRoom() calls room.disconnect() before replacing room (:177-180). The resulting .disconnected callback sees the same room and active state and can invoke teardownForFailure() again (:515-533).
  • The intended first agent-timeout retry also calls teardownRoom() while state is .waitingAgent (:192-196), so its intentional disconnect can enter terminal-failure cleanup instead of retrying.
  • A non-timeout room.connect failure still only assigns .failed (:134-166) after media scope may already have started; it does not recreate the room or clean media state. A user retry can therefore reuse that room.
  • Impact: retries and terminal teardown are race-dependent and can recurse, abort the single retry, or leave stale media/room state.
  • Correction: introduce an idempotent teardown flag/state before awaiting disconnect; distinguish intentional disconnects from remote terminal disconnects; set the externally visible failure/transitional state before disconnect callbacks can run; and route every post-token/connect failure through fresh-room cleanup. Add a fake-room lifecycle seam or deterministic integration tests.

5. High — no-resume media interruption can still restart before the pause control completes

  • Evidence: on .ended, defer clears isInterruptionActive synchronously (Sources/Media/MediaPlayerController.swift:77-98), while the server pause is sent in an unstructured asynchronous task (:91-96). Until that request completes, polling may still receive desired == "playing"; reconciliation sees the interruption flag already false and calls player.play() (:188-200,237-252). If there is no room or the request fails, server desired remains playing indefinitely.
  • Impact: media can restart immediately after an interruption that explicitly did not authorize resume.
  • Correction: retain a separate resume-suppression state through successful server reconciliation (or synchronous local desired override plus acknowledged pause), and do not permit reconcile-driven play until a later explicit resume/play action. Add deterministic begin/end-without-resume/poll-before-ack/control-failure tests.

6. Medium — resource-loader cleanup is unused and task bookkeeping is data-racy

  • Evidence: removeLoader(_:) has no caller; repository search finds only its declaration (Sources/Media/PinnedMediaResourceLoader.swift:70-75). MediaPlayerController.wireSource and clearMedia never remove the previous asset mapping (Sources/Media/MediaPlayerController.swift:274-283,477-486), so loaderURLs remains unbounded.
  • activeTasks is written on the main resource-loader delegate queue (PinnedMediaResourceLoader.swift:134-135,139-149) and also from URLSession completion callbacks on another queue (:98-101) with no actor, lock, or serial queue. It uses a hashValue integer instead of ObjectIdentifier directly (:98,146), allowing collisions.
  • For 206 responses with missing/malformed/unknown total, code falls back to expectedContentLength, which is the returned range length, and incorrectly labels it as the full resource (:117-127,153-165). The authored tests endorse this fallback and, in any event, are not discovered. Status/cancellation behavior requested in iteration 04 has no test.
  • Impact: loader mappings leak across track changes, cancellation/completion can race dictionary mutation, and AVPlayer can receive an incorrect total length for partial content.
  • Correction: invoke removal when replacing/clearing an item, serialize all dictionaries on one queue/lock, key by ObjectIdentifier, and assign expectedContentLength only for 200; for 206 set content length only from a known Content-Range total. Add discovered status/cancellation/bookkeeping tests.

7. Medium — microphone interruption ownership is still set before the mute succeeds and mute/unmute tasks can cross

  • Evidence: .began sets micMutedByInterruption = true before launching asynchronous setMuted(true) (Sources/LiveKit/VoiceSession.swift:435-448). .ended can then launch setMuted(false) without awaiting/cancelling the begin task (:449-461). The flag therefore means “mute requested,” not “this interruption successfully muted the mic.”
  • Impact: rapid or malformed begin/end sequences can leave SDK mute operations completing out of order, including ending muted after a should-resume event. A failed begin-mute still causes an interruption-owned unmute attempt.
  • Correction: serialize interruption microphone operations under a generation/token, mark ownership only after successful mute, and cancel/ignore superseded completions. Device qualification remains required.

Acceptance Criteria

Prior finding disposition

Prior finding Iteration-04 disposition
1. Compile failure Fixed; current build succeeds
2. Failure/disconnect lifecycle Not fixed; Finding 4
3. Media interruption Not fixed; Finding 5
4. Text-mode mic privacy Fixed by API guards + disabled toggle; device-unverified
5. Server configuration Not fixed; Finding 3
6. Transcript dedupe/repeats Partial; symmetry improved, legitimate repeats still wrong; Finding 2
7. Visible history/Clear Partial; Clear is truthful, rendering duplicates live finals; Finding 2
8. Mic interruption ownership Partial; lifecycle resets added, async ownership race remains; Finding 7
9. Conversation state Source fix is correct; claimed tests are undiscovered and currently contain an invalid EMA expectation
10. Stale media publish Fixed by room/session/poll generation capture; device-unverified
11. Resource loader Partial; Finding 6
12. Generic-host TLS Fixed by host-scoped pin/default handling; network/device-unverified
13. UI/accessibility parity Correctly documented as pending separate redesign
14. Model picker copy Fixed by readiness check

Iteration 04 request disposition

Request item Verdict
1. Complete iteration 03 Partial: stream helper and parser compile; dedupe/interruption/resource tests and semantics remain incomplete
2. Terminal failure cleanup Changes required (Finding 4)
3. Text-mode mic invariant Implemented; device-unverified
4. Server config semantics Changes required (Finding 3)
5. Visible history/Clear Changes required (Finding 2)
6. Current-reference state inference Source implemented; test target/expectation correction required
7. Stale-safe media publish Implemented by inspection; device-unverified
8. Resource-loader hardening Changes required (Findings 1 and 6)
9. Configurable HTTPS trust Implemented by inspection; external-host/device test absent
10. Model picker copy Implemented
11. PLAN/README qualification Implemented; docs now classify redesign/device work as pending
12. Idle-timer wording Implemented correctly

Verification Evidence

  • Loaded the repository XcodeBuildMCP skill and confirmed defaults: project /Users/shane/dev/voice-ios/Voice.xcodeproj, scheme Voice_iOS, iPhone 17 Pro simulator AFDA137E-1657-428E-BF01-E5CD7989772C.
  • XcodeBuildMCP test_sim → 16 passed, 0 failed, 0 skipped in 9.6 s. Log: ~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/logs/test_sim_2026-08-31T12-58-36-052Z_pid94423_12510633.log. The log lists only MediaAPITests, MediaReconcileTests, and TranscriptDedupeTests; it proves the two new test files were omitted.
  • XcodeBuildMCP build_sim → Build succeeded in 2.8 s. Log: ~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/logs/build_sim_2026-08-31T12-58-53-892Z_pid94423_5f92c809.log.
  • git diff --check → clean.
  • Inspected the complete current diff, all five test files, generated project membership, iteration 03/04 reports, product sources, PLAN/README/device docs, and prior review findings.
  • Reviewer changed only artifact reports/manifest, not product implementation.

Residual Risks

  • Real-device microphone, interruption, LiveKit terminal failure, wake-lock, TLS alternate-host, and supported-media behavior remain unqualified.
  • Current YouTube library webm remains server-blocked for AVPlayer; a supported MP3/AAC stream is still needed for client qualification.
  • UI redesign/accessibility/app icon remain intentionally pending and out of this correction scope.
  • Plain LAN WebSocket signaling and rotating self-signed pin remain documented deployment risks.

Required Actions

  1. Regenerate the Xcode project, fix undiscovered test expectations, and prove the full expected 28-test suite runs.
  2. Correct Findings 2–7 and add deterministic tests or explicit integration seams for the high-risk lifecycle paths.
  3. Update implementation reports/test counts to match actual result-bundle evidence.
  4. Request another independent review before approval; retain device-only qualifications.