# QA1 journey E2E report Branch test/journey-e2e. Commit 501663c8b9c955e672b0f2ff70433b1e0544a1b4. Base origin/master 7fae058a000219ee7501721bdbc92e0de435b502. The real-Obsidian suite pins all 15 highest-risk R5 journeys before the v3 rewrite. Eighteen new tests join the 21 tests on origin/master at 7fae058. The change is confined to tests/e2e. No product behavior, dependency, CI, settings schema, API, or URI change is included. Tests use real media, trusted mouse and keyboard input, registered commands, Reading-view links through Obsidian's URI handling, and actual app restarts. Assertions read written notes, downloaded bytes, persisted subscriptions and progress, public API values, and advancing media time. UI selectors, legacy storage decoding, and fixture transport have separate flat owners. The Test Behavior, Not Implementation principle led to mutation experiments that remove real product effects. Model the Domain led to typed feed, playback, library, and workspace snapshots instead of a scenario interpreter. ## Journey mapping The file column omits the common tests/e2e prefix. Each journey ID appears in its named test. | Journey | File | User action and asserted outcome | | --- | --- | --- | | J-FEED-01 | journey-feeds.test.ts | Search Settings and click Add. Exact subscription and episode survive plugin reload. | | J-FEED-06 | journey-feeds.test.ts | Open podcast card. All 12 archive episodes render. Latest Episodes has the default limit of 10. | | J-LIST-08 | journey-playback.test.ts | Click episode row. Correct 60-second media plays and advances. | | J-PLAY-01 | journey-playback.test.ts | Pause and resume with the on-screen button. Paused position stays stationary, saves, and resumes. | | J-PLAY-11 | journey-playback.test.ts | Restart mid-playback. Both process IDs change. Episode and disk checkpoint survive. Opening Player preserves the position. Autoplay is guarded as #377. | | J-PLAY-09 | journey-playback.test.ts | Let a six-second episode end. It is marked played, removed from its playlist, and the next queued episode plays. | | J-NOTE-07 | journey-links-local.test.ts | Create the default episode note, switch episodes, and click Resume in Reading view. Literal generated link and saved position match. A finished episode restarts at zero through a legacy plus-encoded link. | | J-URI-03 | journey-links-local.test.ts | Click a timestamp link for an uncached podcast. Obsidian fetches the feed and plays the correct audio at 37 seconds. | | J-LOCAL-01 | journey-links-local.test.ts | Choose Play with PodNotes in the real file menu for a 30-second WAV. Player opens, time advances, and Local Files survives reload. | | J-EDGE-12 | journey-resilience.test.ts | Replace data.json atomically, wait for Obsidian's raw event, and edit skip-backward to 42 through Settings. Remote subscription loss is guarded as #357. | | J-FEED-08 | journey-feeds.test.ts | Click Import OPML and deliver a disk file to the product's real input. Two new subscriptions and the duplicate-skip summary persist. | | J-EDGE-05 | journey-feeds.test.ts | Browse sparse and empty feeds. Invalid items without both title and description and enclosureless items are omitted. Sparse episodes play. An empty feed is fetched and settles without a load-error Notice. Missing pubDate is guarded as #378. | | J-EDGE-03 | journey-resilience.test.ts | Download through the context menu, stop the fixture server, and restart. Exact downloaded bytes play offline. Cached episodes browse. An uncached podcast fails without a spinner. | | J-TR-02 | journey-resilience.test.ts | Run the transcription command with a fake SecretStorage key. One POST sends the correct authorization, model, full audio SHA-256, and byte count. The transcript note and public API agree. | | J-ONB-02 | journey-mobile.test.ts | Reload desktop mobile emulation with no saved PodNotes leaf. Startup remains dormant. Ribbon opens one usable view. | ## Known failures Each test asserts the correct outcome and accepts only the observed issue signature. Fixture failures, unrelated mismatches, and uncaught runtime errors remain failures. Correct behavior makes the test demand removal of the marker. | Issue | Exact accepted failure | | --- | --- | | [#357](https://github.com/chhoumann/PodNotes/issues/357) | Local skip-backward 42 saves, original local subscription stays, and the remote subscription is lost. | | [#358](https://github.com/chhoumann/PodNotes/issues/358) | Both subscriptions save, the artwork control card appears, and the artless card is absent. | | [#375](https://github.com/chhoumann/PodNotes/issues/375) | Feed loads into cache, but the Episode list tab has zero rows and shows No episodes found. | | [#377](https://github.com/chhoumann/PodNotes/issues/377) | After independent episode and checkpoint assertions pass, opening the restored Player auto-plays its media and advances over one second. | | [#378](https://github.com/chhoumann/PodNotes/issues/378) | The dated control loads, but the playable undated episode is omitted. | QA1 filed #375, #377, and #378 from real-runtime reproductions. These tests document defects, so five accepted known-failure signatures do not mean those five product outcomes work. ## Runtime and restoration Obsidian 1.13.7 and installer shell 1.13.7 run under Xvfb on headless Linux. The vault is podnotes-test-journey-e2e, isolated to this checkout. Node 22.23.2 runs every command through fnm exec --using 22. TMPDIR is /data/home/tmp/podnotes-QA1. Feeds and WAV enclosures are served from http://127.0.0.1.nip.io://feed.xml. The established verify fixture uses the same hostname to pass the product's literal-localhost policy. No source network seam was needed. iTunes search and the exact OpenAI transcription endpoint have test-only network responses. Real feed parsing, media fetch, provider request construction, and note writes run unchanged. The harness owns the vault lock before shared setup can enable or reload PodNotes. It retains the lock until the original suite state is restored. Each journey captures cache, live layout, recent-file history, mobile flag, window size, and both workspace files including absence. Cleanup flushes saves, reloads with PodNotes persistently disabled to end old callbacks, restores cache before original data loads, and restores workspace after shared teardown. Settings changed only through UI are included in the shared data snapshot. The rollback-validation-r3 probe passes from a desktop layout with no PodNotes leaf and a populated original cache and subscription. It compares both workspace files after the one-second save debounce, recent-file history, and parsed workspace state after an ordinary save. It also passes from original mobile mode true and checks prior window size and mobile layout. The probe uses direct Vitest against a warm app to isolate harness restoration. The pre-existing npm run test:e2e launcher reloads PodNotes before tests and can open a desktop leaf. The historical runtime-recovery-r2 fixture-failure probe passed before the direct-relaunch change. A failing whileClosed fixture still relaunches Obsidian, changes its renderer PID, and preserves the original error. Its post-CLI error probe proved native error persistence only. Review showed that Obsidian initializes its native CLI error buffer after plugins, so it cannot prove capture of early startup errors. A small test-only plugin now records error and unhandledrejection before PodNotes loads. A source mutation throws QA1 startup error sentinel during plugin startup only when the recorder loaded first. J-PLAY-11 rejects it as Startup runtime errors before known-failure handling. Source was restored, the clean bundle rebuilt, and the isolated app stopped. The recorder is removed after each suite with exact original community-plugins.json bytes and enabled order restored. The first startup probe detected a missing recorder after restart. The runner replaces the enabled-plugin list during provisioning. The test helper now directly relaunches the same isolated executable with its original main-process arguments, verifies the expected profile, and supplies private HOME plus a new Xvfb display on Linux. The second startup probe captures the sentinel. ## Mutation evidence One real source mutation per new test file reached the relevant failed outcome. Source was restored after each experiment. These experiments predate later lifecycle and comment changes, so they are evidence for those experiments rather than a rerun against final bytes. | File | Mutation | Observed failure | | --- | --- | --- | | journey-feeds | Add skips subscription save. | J-FEED-01 times out waiting for the saved subscription. | | journey-playback | Player ignores the saved position. | J-PLAY-11 reads about 0.196 seconds instead of at least 4.770. | | journey-links-local | URI handler replaces explicit time with zero. | J-URI-03 reads about 0.190 seconds instead of at least 37. | | journey-resilience | Player refuses downloaded audio. | Offline playback retains remote media, stays paused, and never advances. | | journey-mobile | Startup dormancy is disabled. | J-ONB-02 observes one leaf instead of zero. | Two extra guard experiments also fail correctly. Fixing the artless card makes #358 demand removal of its marker. Breaking the local setting save remains a hard failure outside #357 handling. ## Independent review Claude Opus 5.5 implemented the initial suite, then its task hit a rate limit. GPT Astra independently checked the implementation handoff and mutation artifacts. Parent owns all later fixes. Round 1 GPT Astra and GPT Sol reviews found seven gaps, all accepted. Round 2 found five further lifecycle and platform gaps, all accepted. Claude seats in both rounds hit API rate limits and received one other-family GPT retry each under standing order 22. Round 3 accepted two further gaps in startup-error capture and workspace save/history restoration. Both now have targeted runtime proof. Final review uses the coordinator-authorized separate Claude pool alongside GPT Astra. GPT Astra reviewed all nine files on 1316312 and checked their hashes against the final source manifest. It returned no findings and no correction-code acceptance blocker. It confirmed the startup recorder order, direct restart, lock lifetime, and delayed workspace restoration evidence. Claude pool also reported no Linux acceptance blocker, but the lead accepted its empty-feed false-positive gap, absent-secret rollback gap, macOS Select All command, cleanup-error reset gap, inaccurate test title, and late profile guard. The coordinator authorized one narrow fourth test-only fix round after the workflow cap, with no fifth round. The 19 added and three removed lines address those findings. Independent fourth-round reviewers inspect only that fix diff. Final results are reported separately. ## Limits and follow-ups - The v3 rewrite will need to adapt the centralized fixture seed, persisted-data decoder, and UI locators. Named journey assertions check the same user outcomes. - Mobile coverage is desktop tablet emulation. No iOS or Android device was tested. - Linux runtime evidence supports restart. macOS uses Electron app.quit because closing its last window leaves the app alive. That branch has static review only. - Electron's native GTK chooser blocks later links and ignores CDP interception. The helper temporarily holds back only the native dialog. DOM.setFileInputFiles delivers the actual OPML file and triggers the product's real change, FileReader, and import path. - The playback-rate slider bubble reads 1.50 while the label and API show 1x. This cosmetic observation is a follow-up. - Restart followed by an episode switch remains outside this suite. See #353. - A future policy that rejects resolved private addresses will require adapting nip.io fixtures. - Four review fix rounds and master advances required repeated acceptance. The coordinator explicitly authorized the fourth round. New review findings after it become PR follow-ups. ## Fourth-round evidence and review dispositions The current direct-relaunch recovery probe passes when the closed-app fixture throws. It preserves the exact fixture error, changes the renderer PID, and leaves the real PodNotes API available. This supersedes the historical recovery control-flow evidence. Both transcription cleanup cases pass on the warm isolated instance. An initially absent fake key returns null after the test, and a prior fake value returns exactly that value. The probe removes its synthetic prior value afterwards. The isolated test key contained an empty entry left by historical tests; the probe removed that fixture residue before testing the absent case. A real product mutation fetches the empty feed, then throws instead of returning its parsed empty episodes. J-EDGE-05 reaches the existing empty-list and No episodes found checks, then hard-fails on the new load-error Notice assertion. The mutation is restored and the clean bundle rebuilt. The proof driver initially used the wrong Vitest wording in its log check; its finally block still restored source, rebuilt and stopped the app. The recorded failed assertion confirms the proof, and the log check was corrected without a code change. The focused run passes J-EDGE-05, its #378 guard, J-EDGE-12, J-PLAY-11 and J-EDGE-03. A probe emits a recorder sentinel after ordinary test cleanup. The next beforeEach fails at the runtime check before diagnostic reset. This proves the delayed cleanup sentinel is rejected at the next test boundary. Final restoration checks runtime errors before removing the recorder, but its preceding cache-restoration reload can clear a late error. Astra identified that remaining gap as follow-up #382. A subsequent single-test run of the same delayed-cleanup probe returned green despite injecting the sentinel after its final per-test check, confirming the gap. This was a separate diagnostic probe, not an observed acceptance-run error. No fifth fix round is authorized. Claude's executable-duplication premise is dismissed. Runtime main-process argv contains only flags, including no-sandbox and user-data-dir. Slicing it would remove a real flag. Direct-launch consolidation is a maintenance follow-up. Dynamic imports and Vitest's default stack hook order remain unchanged. Changing that hook order requires revalidating rollback. The sparse feed's untitled item omits both title and description, which violates RSS 2.0. Its omission records current invalid-input behavior. #378 separately guards a valid titled audio item without optional pubDate. The #378 title now states episode-list coverage. [#380](https://github.com/chhoumann/PodNotes/issues/380) tracks recorder recovery after interruption. Normal teardown restores exact configuration, but SIGKILL can strand recorder folders and enabled entries. Isolated runner provisioning resets enabled plugins, and a full isolated-vault reset removes stranded folders. Shared macOS execution was not tested. Also follow up on macOS direct-launch logging, consolidation with runner relaunch, stale harness documentation, and lifecycle import organization. These are explicit limits, not additional fourth-round changes. Astra's fourth-round fix-diff review found no blocking regression. It checked every final-source manifest hash and accepted the empty-feed mutation, absent and existing secret rollback, direct restart recovery, profile-guard placement, and accurate test name. It identified one remaining correctness warning. [#382](https://github.com/chhoumann/PodNotes/issues/382) tracks late errors cleared by final suite restoration. The lead accepts the warning as a follow-up under the coordinator's explicit no-fifth-round decision. Claude also found no acceptance blocker. Its additional warnings concern repeated stale error reports and the exact negative Notice wording. The former is included in #382 and the latter is tracked in #383. The lead retains both as follow-ups under the no-fifth-round decision. Claude also noted that stricter runtime checks now cover the 21 baseline tests, which passed all three runs. Its repeated-profile-resolution nit is dismissed for this narrow patch because the coordinator explicitly retained the existing relaunch guard and forbade refactoring. ## Final verification The final six checks passed on 7fae058 after the CI-only #381 base merge. The final three complete npm run test:e2e runs passed 39 tests each in 91.64, 88.49, and 92.09 seconds. The earlier fourth-round sequence on 963d2d0 passed 92.03, 92.71, and 92.35 seconds and remains historical evidence. No tests were skipped. The unit/component check passed 1,263 tests in 81 files, and both Svelte checks reported zero errors and warnings. Final cleanup found no recorder or fixture library state and the stop dry-run reports no PIDs. One transcript from an earlier run had mtime 02:51:49 UTC, before the final code and acceptance sequence. It was removed by exact fixture path after verifying its fixture text. No content files remain in the isolated vault. Both independent fix-diff reviews are complete with no pending child runs. The final source manifest matches all test files. Earlier 37-test and 39-test sequences are historical evidence. Final verification uses current code on origin/master 7fae058, including #373, #376, the 2.21.0 version-only release commit and CI-only #381. All reviewed source bytes remain unchanged by the last base merge. Product source bytes are unchanged from 1316312. The final test hashes include the authorized fourth-round corrections. Wall times cover the complete npm process, including build, launch, suite and owned-instance teardown. They are stability-run durations, not a performance comparison. [#383](https://github.com/chhoumann/PodNotes/issues/383) tracks empty-feed failure detection independent of future Notice wording. Current wording is proven by mutation, but the exact negative match must adapt if v3 rewrites the Notice. ## Evidence links [Selected proof bundle](https://files.bagerbach.com/qa1-evidence-tar-e3qevnigfmd4.gz) contains final logs, source hashes, mutation diffs, runtime probes and independent review verdicts. It excludes vault contents and original-state snapshots. [Historical real-media fixture screenshot](https://files.bagerbach.com/j-list-08-playing-6573aogr78vj.png). It shows real fixture playback and predates final lifecycle corrections. ## Check summaries - npm run lint passed in 1.80 seconds. - npm run format:check passed in 1.05 seconds. - npm run typecheck passed in 3.61 seconds. - npm run build passed in 8.45 seconds. - npm run test passed in 13.33 seconds. - npm run docs:build passed in 0.47 seconds. ## Dispatch ledger - How exploration. claudeAgent/claude-opus-5-5. - Architecture candidate. claudeAgent/claude-opus-5-5. - Architecture candidate. codex_pool/gpt-6-astra. - Architecture cross-judge. codex_pool/gpt-6.1-sol. - Initial implementation. claudeAgent/claude-opus-5-5, later rate-limited. - Implementation recovery verification. codex_pool/gpt-6-astra. - Comment review. codex_pool/gpt-6.1-sol. - First interrogate round. codex_pool/gpt-6-astra. - First Claude review retry. codex_pool/gpt-6.1-sol after Claude rate limit. - Second interrogate round. codex_pool/gpt-6-astra. - Second Claude review retry. codex_pool/gpt-6.1-sol after Claude rate limit. - Third interrogate round. codex_pool/gpt-6-astra. - Third lifecycle verification. codex_pool/gpt-6-astra. - Third Claude review. claudeAgent_claude_poolclaudeAgent_pool/claude-opus-5-5 after regular-seat rate limit. - Fourth fix-diff review. codex_pool/gpt-6-astra, reasoningEffort xhigh. - Fourth fix-diff review. claudeAgent_claude_poolclaudeAgent_pool/claude-opus-5-5, effort xhigh. - Corrections and runtime proof. In-thread. Existing helpers and guards support the narrow changes; independent reviewers own judgment of the diff. - Final checks, evidence packaging, and PR. In-thread. One writer and one isolated app owner preserve the test vault boundary.