# QA2 final real-artifact verification at b431ed0 Verdict: **PASS**. The user flow passed on the current bundle under default 1.14.4 app selection. It covered feed add, browse, play, UI pause, persisted position and plugin reload, plus all three Show player entry paths. The final 5+2 series logs show 7 of 7 runs green on the versions they name. The vault is byte-identical to its pre-flow snapshot, and no process I started is alive. Lane: T3 Review loop "Verify each fix". Driver: `.claude/skills/verify` (`ui.mjs` trusted input, `fixtures.mjs`). No tracked file changed, no child was spawned, nothing was committed or pushed. ## Revision and bundle - HEAD `b431ed00b4a5537353d5454519d6421b59f9891e`. `git status --porcelain` was empty at start, after the build, and at the end. - `git diff 7cd369b b431ed0` touches only `tests/e2e/harness-isolation.test.ts` and `tests/e2e/journey/library.ts`. `git diff HEAD -- src` is empty. - Root `main.js` was 282,491 bytes, sha256 `c5d3a6dff959abbd47ec66dc58adb952f1cc1db391f8bd1a177050752671e7fa`, before my build. `npm run build` at HEAD (typecheck plus vite, `build/main.js 282.49 kB, gzip 90.65 kB`) reproduced the same sha256 for `main.js` and `build/main.js`. - Root `manifest.json` sha256 `d58d070821e14b699fbc929db0d086232ebd13acf47804a28c1e3f35715508b0`, version 2.21.1, minAppVersion 1.13.0. - The vault links `.obsidian/plugins/podnotes/main.js` to `/home/christian/.t3/worktrees/PodNotes/test-e2e-hardening/main.js`, and `manifest.json` to the root manifest. Inside the running app, `fs.realpathSync` of the plugin's `main.js` resolved to the root file, and its renderer-computed sha256 was `c5d3a6df...71e7fa`. The instance was launched fresh after the build, so the loaded plugin came from these bytes. ## Environment (doctor, `state/doctor*.txt`) - `PODNOTES_OBSIDIAN_APP`, `PODNOTES_OBSIDIAN_BIN` and `OBSIDIAN_BIN` were unset. `DISPLAY` was unset. Host Node v22.23.2 via `fnm exec --using 22`. `TMPDIR=/data/home/tmp/podnotes-qa2`. - `npm run start:e2e-obsidian` printed `Obsidian app version 1.14.4 (installer shell 1.14.4)` and `Obsidian instance launched`. The app was `~/.local/share/obsidian-versions/obsidian-1.14.4/obsidian` under `xvfb-run`, Xvfb `:257` at 3840x3200x24, with a 1024x800 window. - `document.title` was `New tab - podnotes-test-e2e-hardening - Obsidian 1.14.4`. CLI `version` printed `1.14.4 (installer 1.14.4)`. - Platform linux x64, Electron 43.7.7, Chrome 150.0.7871.250, renderer Node 24.21.0, `isMobile` false, 1 window. - `--host-resolver-rules=MAP podnotes.test 127.0.0.1` was on the main and network-service processes. - Vault `podnotes-test-e2e-hardening`. Plugin 2.21.1, `api` true with its full getter and method set, `podnotes` URI handler true, enabled community plugins `["podnotes"]`. - `dev:errors` printed `No errors captured.` at the doctor, after the plugin reload, and before the stop. - The owned PIDs from the runner dry-run were 1444422 (xvfb-run), 1444446 (Xvfb), 1444464 (Obsidian main), 1444513 and 1444514 (zygotes), 1444609 (GPU), 1444615 (network service) and 1444633 (renderer). Their command lines are in `state/own-pids-start.txt`. ## Fixtures - `node .claude/skills/verify/fixtures.mjs feed` started this worktree's server, pid 1450452, on 127.0.0.1:35398. It printed `http://podnotes.test:35398/feed.xml`. - The feed is `PodNotes Fixture`, with `Fixture Episode 2` (Sat, 10 Oct 2026) above `Fixture Episode 1` (Fri, 09 Oct 2026). - Episode 1 is `http://podnotes.test:35398/episode-1.wav`: `audio/wav`, 960,044 bytes, sha256 `ecbf51e8f6b5692720c4a46e7e1b13592147d3f922469b30e01a01ea9d862e3c`. The header is RIFF PCM, mono, 8000 Hz, 16-bit, so 60 s. - No local WAV was written into the vault. ## User flow and observed state Every command, with its timestamp and output, is in `commands.log` (385 lines). `UI` means `node .claude/skills/verify/ui.mjs`, and `CLI` means `npm run obsidian:e2e --`. 1. **Initial state.** `api` reported episode `E2E Segment URI Episode` (local file), not playing, time 0. `data.json` sha256 `490e6427...` held savedFeeds `Journey Latest` only, `playedEpisodes` `{}`, and a queue of 1. 2. **Precondition.** `CLI plugin:enable id=command-palette filter=core` turned on the palette, which this vault had off. The original `core-plugins.json` was restored after the stop. 3. **Settings.** `UI key Ctrl+,` opened Settings. `UI click ... --text PodNotes` opened the PodNotes tab. `UI fill 'input[placeholder="Search or enter feed URL..."]' http://podnotes.test:35398/feed.xml` produced the card with `Add PodNotes Fixture podcast` (`shots/01`). 4. **Add.** `UI click '[aria-label="Add PodNotes Fixture podcast"]'` turned the button into `Remove PodNotes Fixture podcast` (`shots/02`). Within 1 s, the on-disk `savedFeeds["PodNotes Fixture"]` held url `http://podnotes.test:35398/feed.xml`, artwork, link, description and author `PodNotes` (`state/savedFeed-fixture.json`). Settings was closed with its header button. 5. **Entry paths.** Before each path I hid the view through `ui.mjs` clicks on the `All properties` tab and the right sidebar toggle (`bin/hide-player`). Each hidden state read `visible:false`, `rect [0,0,0,0]`, `rightCollapsed:true`, with 1 leaf. - Ribbon (`UI click '.side-dock-ribbon-action[aria-label="Show PodNotes"]'`) gave `leaves:1`, `visible:true`, rect `[724,40,300,760]`, `Podcast Player*` active (`shots/03`). - Palette (`UI key Ctrl+p`, `UI type .prompt-input 'Show player'`, `UI click .suggestion-item --text 'Show player'`) gave the same result (`shots/04` shows `PodNotes: Show player` over the collapsed sidebar). - Direct command (`CLI command id=podnotes:podnotes-show-leaf`, which printed `Executed: podnotes:podnotes-show-leaf`) gave the same result. No path created a second leaf. 6. **Browse.** I clicked the `Podcast grid` button, then the `PodNotes Fixture` card. The list read `10 OCTOBER 2026 Fixture Episode 2` above `09 OCTOBER 2026 Fixture Episode 1` (`state/episode-list.txt`, `shots/05`). 7. **Play.** `UI click '.podcast-view .podcast-episode-item' --text 'Fixture Episode 1'` started playback. Six samples about 1 s apart read `api.currentTime` 0.724507, 2.052643, 3.115139, 4.44338, 5.50576 and 6.568275, all with `isPlaying` true. The media element had `paused:false` and src `http://podnotes.test:35398/episode-1.wav`, and its clock tracked the API. At the screenshot, time was 14.005761. The `Player` tab was active and showed title `Fixture Episode 1`, with the seek slider at `00:00:14 of 00:01:00` (`shots/06`). 8. **Pause.** `UI click '.podcast-view [aria-label="Toggle playback"]'` paused playback. Four samples over 4 s read `isPlaying` false, `currentTime` 27.695497 and media `paused:true` at 27.695497, with no movement (`state/api-paused-samples.txt`). 9. **Independent disk read.** Python read `data.json` 5 s after the pause, without calling `flushSaves`. The file had mtime 20:55:08 (the second of the pause) and sha256 `e83d377f...`. It held `playedEpisodes["PodNotes Fixture::Fixture Episode 1"] = {time: 27.695497, duration: 60, finished: false}` and `currentEpisode` Fixture Episode 1 with the fixture stream URL. The queue was `[E2E Segment URI Episode, E2E Placeholder Episode]`, because autoQueue queued the episode the user switched away from (`state/disk-after-pause.json`, `shots/07`). 10. **Plugin reload.** `CLI plugin:reload id=podnotes` printed `Reloaded: podnotes`. The `data.json` sha256 was `e83d377f...` both before and after. Three samples after the reload read episode `Fixture Episode 1`, podcast `PodNotes Fixture`, `isPlaying` false, `currentTime` 27.695497 and length 60. The media element had an empty src and t 0, so nothing autoplayed or loaded. `dev:errors` was empty. The view came back on the grid tab (`shots/08`). One `UI click '.podcast-view [aria-label="Player"]'` showed `Fixture Episode 1` at `00:00:27 of 00:01:00`, with `aria-valuenow` 27.695497 (`shots/09`). ## Screenshots (all opened and inspected by me) All nine PNGs were taken in this session on bundle `c5d3a6df...`. No earlier or pre-PERF1 image is used as evidence. - `shots/01-settings-result-card.png` shows Settings > PodNotes with the URL in the field and the `PodNotes Fixture` card. It has purple artwork, author PodNotes and an `Add` button. The sidebar lists Canvas and Command palette under Core plugins. - `shots/02-settings-after-add.png` shows the same card with a red `Remove` button and the tooltip `Remove PodNotes Fixture podcast`. - `shots/03-ribbon-window.png` shows the full window after the ribbon click. The right sidebar is open on the PodNotes grid (Queue (1), Favorites (0), Local Files (1), Played (0), and the `JL` card), and the ribbon's podcast icon is highlighted. - `shots/04-palette-show-player.png` shows the palette with `Show player` typed and one suggestion, `PodNotes: Show player`. The right sidebar is collapsed. - `shots/05-episode-list.png` shows the PodNotes Fixture header and artwork above `Fixture Episode 2` (10 OCTOBER 2026) and `Fixture Episode 1` (09 OCTOBER 2026). - `shots/06-playing.png` shows the Player tab with the artwork, `Fixture Episode 1`, `00:00:13` elapsed with `00:00:46` left, skip buttons, volume 1.00, rate 1x, and Queue `E2E Segment URI Episod...`. - `shots/07-paused.png` shows the artwork dimmed with a play icon overlay, at `00:00:27` with `00:00:32` left. - `shots/08-after-reload.png` shows the view after the reload on the grid tab, with Queue (2) and Played (0). - `shots/09-after-reload-player-tab.png` shows the Player tab after the reload. It is paused with the overlay at `00:00:27` and `00:00:32` left, the same position as before the reload. UI observations, in src code this branch does not touch: - The player's `.episode-player` overflows sideways by 13 px (scrollWidth 277 against clientWidth 264). A horizontal scrollbar appears under the queue card in `shots/06`, `07` and `09`. - The queue card has a blank artwork area for the local-file episode, and its title is clipped (`E2E Segment URI Episod`). - `Volume: 100%` wraps onto two lines. - A 13x18 px `.status-bar` sliver sits at the window's bottom right, at (1011,782). These look pre-existing and are not caused by QA2. They are candidates for a follow-up, not blockers for this test-only PR. ## Final series audit `bin/audit-final-series.py` is a new script that reports what `run-log.json` and the raw logs say, without assuming an outcome. Its output is in `series-audit.json` and `series-audit.txt`. The old `audit-series.py` was not used. | Run | Exit | Version line | Files | Tests | Failed | Retry mentions | Vitest s | Controller s | Core preserved | Runtime after | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 1.14.4#1 | 0 | 1.14.4 (installer shell 1.14.4) | 12 passed (12) | 73 passed (73) | 0 | 0 | 237.72 | 240.114 | true | 1.14.4, explorer false, 0 leaves, 1 window | | 1.14.4#2 | 0 | 1.14.4 (installer shell 1.14.4) | 12 passed (12) | 73 passed (73) | 0 | 0 | 238.11 | 240.091 | true | same | | 1.14.4#3 | 0 | 1.14.4 (installer shell 1.14.4) | 12 passed (12) | 73 passed (73) | 0 | 0 | 274.57 | 277.307 | true | same | | 1.14.4#4 | 0 | 1.14.4 (installer shell 1.14.4) | 12 passed (12) | 73 passed (73) | 0 | 0 | 247.2 | 255.392 | true | same | | 1.14.4#5 | 0 | 1.14.4 (installer shell 1.14.4) | 12 passed (12) | 73 passed (73) | 0 | 0 | 318.34 | 321.407 | true | same | | 1.13.7#1 | 0 | 1.13.7 (installer shell 1.13.7) | 12 passed (12) | 73 passed (73) | 0 | 0 | 346.81 | 350.849 | true | 1.13.7, explorer false, 0 leaves, 1 window | | 1.13.7#2 | 0 | 1.13.7 (installer shell 1.13.7) | 12 passed (12) | 73 passed (73) | 0 | 0 | 348.84 | 353.935 | true | same | - Every row has head `b431ed0`. Each log has 73 unique passing lines, 0 failed and 0 skipped, and every run has the same set of 73 tests. The script found no disagreement between `run-log.json` and the raw logs. - All seven logs, and all setup, explorer-reset, stop and cleanup logs, are newer than the HEAD commit time (20:10:25). - The controller total is 2039.1 s. The slowest test in every run is J-PLAY-11 (restart keeps position), at 24.6 to 26.1 s on 1.14.4 and 28.0 to 32.2 s on 1.13.7. - Twelve selected cases passed in all seven runs. The harness tests are extra-window restart refusal, explorer and core-config restore, and leaked-window rollback. The local-files tests cover a disabled, an open and a closed explorer. The rest are offline TTL, offline after restart, Markdown punctuation, J-FEED-01, J-LIST-08 and J-PLAY-01. Per-run milliseconds are in `series-audit.json`. - Lifecycle per version: setup printed `Obsidian instance launched`, and explorer-reset returned `{enabled:false, leaves:0}`. Stop printed `terminated 8, killed 0, removed`, and the cleanup dry-run printed `pids: none`. - `core-plugins.json` is now sha256 `76ac83be09e8dc8f14e247a23cae8b3ad67c0dfc024bbcc8309bf2d44f37b047`, 713 bytes. It is exact `JSON.stringify(_, null, 2)` output with no trailing newline, and it equals my pre-flow snapshot. - The mutation log (`core-drain-mutation.log`, which removes the current-renderer drain) reads `Tests 1 failed | 72 skipped (73)`. Its assertion is `expected Buffer[...(609)] to deeply equal Buffer[...(610)]`, and the diff shows the expected trailing `10` (newline) missing. The corrected focused log (`core-drain-regression-final.log`) reads `Tests 1 passed | 72 skipped (73)`, with the core-config test at 4312 ms. - The six checks (`current-checks.json`, `core-timer-check-*.log`) all exit 0, and every log's sha256 matches the JSON. Format reports `All matched files use the correct format.` Build reports `build/main.js 282.49 kB`. Test reports svelte-check `0 errors and 0 warnings` twice, `Test Files 96 passed (96)` and `Tests 1587 passed (1587)`. Docs reports `All internal links are valid.` The lint and typecheck logs contain only their command headers, which is the silent success output of `oxlint --deny-warnings` and `tsc --noEmit`. - The sentinel `.audit/qa2-final-series-complete` reads `{"head": "b431ed0...", "all_green": true, "rows": 7}`, which agrees with the logs. ## Vault state, restoration and cleanup - Before any build or app start, the vault was copied with `cp -a` to `before/vault/`, and its manifest was written to `before/manifest.json` (25 entries, manifest sha256 `d26d853c...`). The copy matched the manifest with 0 differences. - After the flow, four files differed. `community-plugins.json` lost its tab indent but kept the same 16 bytes of size. `core-plugins.json` had `command-palette` set to true. `data.json` had the fixture feed, the played position and the queue. `workspace.json` had changed layout. No file was added or removed. Copies of these four post-flow files are in `state/after-flow-files/`. - Before the stop, I ran `flushSaves()` and canceled the workspace layout save and the core and community config saves. I then awaited the adapter. - The fixture server was stopped with `fixtures.mjs stop`. Port 35398 then had no listener, and pid 1450452 was gone. - `npm run stop:e2e-obsidian` printed `terminated 9, killed 0, removed`, and the dry-run printed `pids: none`. None of the eight recorded PIDs remained. The vault manifest did not change between the end of the flow and after the stop, so nothing wrote during the stop. - After the stop, the four files were restored with `cp -a` from `before/vault/`. The manifest diff against `before/manifest.json` shows 0 differences, and the two manifest files are byte-identical (sha256 `d26d853c...`). `diff -r --no-dereference before/vault ` reports no difference, and a final re-snapshot also shows 0 differences. - Leftovers that were present before I started stay as found and were not deleted: - two disabled error-recorder plugins, `podnotes-e2e-errors-2032567` (15:27) and `podnotes-e2e-errors-3104979` (17:25), neither listed in `community-plugins.json`; - `__obsidian_e2e__/journey-feeds-58340af5/subscriptions.opml` (15:28); - `__obsidian_e2e__/podnotes-runtime-9307734f/`, with two capture notes and a 4.8 MB `podnotes-e2e-silence.wav` (17:26); - an empty `.trash`; - `data.json` interrupted-fixture state, namely the `Journey Latest` feed on port 37297, current episode `E2E Segment URI Episode`, the queued `E2E Placeholder Episode`, and Local Files and downloaded entries pointing at the leftover WAV. - At the end, the dry-run printed `pids: none`, and port 35398 had 0 listeners. HEAD was unchanged and the tree was clean. - A process grep matched one `Xvfb :257`, pid 1509401, started at 20:59:02. That was after my stop at 20:56:45, and its parent `xvfb-run` belongs to worktree `feat-media-session` running `/opt/Obsidian/obsidian`. Another worktree's fixture server, pid 696381 from `feat-feed-transcripts`, was also running. I did not touch either. ## Limitations - I did not rerun the six checks as one series. Their logs were written 441 to 510 s before the HEAD commit, and the parent's note says the exact committed content was tested. Between 7cd369b and b431ed0, the commit touches only two `tests/e2e` files. My own reruns at HEAD passed lint (exit 0), format:check (`All matched files use the correct format.`), and typecheck plus build (inside `npm run build`). I did not rerun the unit `test` or `docs:build` myself. - The series ran with an explicit `PODNOTES_OBSIDIAN_APP` for both versions and `OBSIDIAN_BIN=/usr/bin/false`. Default app selection was therefore proven by my flow, not by the series. Each `test:e2e` log says `reused` because the controller started the instance first. The lifecycle logs show the launches. - The `retries` field in `run-log.json` is a constant 0 written by the controller, not a measurement. The raw logs support it, with no `retry` text and exactly one pass line per test. `core_config_preserved` is a hash comparison whose before and after hashes were not logged. Only today's file hash is recorded above. - Headless Obsidian plays to a null sink, so playback is proven by the API and media clocks, not by sound. - After the reload I did not press play again, so resuming from 27.7 s on play is shown only by the restored position and slider, not by a played sample. - The plugin reload was the direct CLI `plugin:reload`, not a toggle in Settings > Community plugins. Hiding the view used trusted clicks, but leaf counts and visibility were read with `eval`. - The command palette was enabled through CLI `plugin:enable` as a precondition, not through Settings. It was restored afterwards. ## Files - Commands: `commands.log`. - Wrappers: `bin/run`, `bin/cli`, `bin/ui`, `bin/state`, `bin/data` and `bin/note`, which existed before. `bin/view`, `bin/hide-player` and `bin/audit-final-series.py` are new. - State: `state/` holds doctor, API samples, disk reads, entry paths, manifests and diffs. - Screenshots: `shots/01` to `shots/09`. - Audit: `series-audit.json` and `series-audit.txt`. - Snapshot: `before/vault/` and `before/manifest.json`. - Todo: `todo-b431ed0.md`. The earlier `todo.md` belongs to the 7cd369b verifier and is left as found. PASS