| Age | Commit message (Collapse) | Author |
|
1. tagService.AssignTag and RemoveTag now use verifyModifyAccess
(owner role required) instead of verifyAccess. Tags are global
state visible to every user with access to a media item, so a
viewer must not be able to add or remove them. Favorites and
notes stay on verifyAccess because they're per-user data
(favorites.user_id, media_notes.user_id) and don't affect anyone
else. Verified via curl: viewer POST /media/{id}/tags now 403,
admin still 200.
2. serveFileResult now emits a strong ETag header
("<size>-<mtime-nanos>") before calling http.ServeContent. Go's
ServeContent honours If-None-Match when ETag is set, so iOS
audio clients and podcast apps can revalidate cached downloads
with conditional GETs. Verified via curl: ETag present on
/stream; If-None-Match matching the ETag returns 304.
3. MediaFilter gains IncludeDeleted flag; ListMedia skips the
implicit `deleted_at IS NULL` predicate when it is set.
FSScanner.loadExistingMedia now passes IncludeDeleted=true so
the dedup map includes soft-deleted rows. Previously a re-scan
of a soft-deleted file tried to CreateMedia and hit the
UNIQUE(set_id, rel_path) constraint, failing the whole scan and
setting progress.last_error. Now the rescan skips the row
cleanly; soft-delete sticks.
4. FSScanner.reconcileOrphans soft-deletes media rows whose
underlying file disappeared between scans. The scanner used to
only walk files that exist and never compare against the DB,
leaving phantom rows in GET /api/v1/media that 404'd on stream.
Verified via curl: rm /testdata/.../orphan.mp3, rescan, row
now has deleted_at != NULL.
Scenarios updated to lock in the fixed behaviour:
S19 step 15 — viewer tag-add now asserts 403, not 200.
S20 step 14 — asserts ETag is present and If-None-Match → 304.
S24 step 13 — asserts clean rescan (no last_error from UNIQUE).
S24 step 20 — asserts orphan rows are soft-deleted by rescan.
Verified: full Go unit suite passes; 25/25 LLM e2e scenarios;
22/22 Playwright e2e-web.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
LLM e2e scenarios (S19-S25):
S19 — permissions matrix (viewer vs owner across two sets). Flags a
design mismatch: viewer role currently permits tags/favorites/
notes via verifyAccess instead of verifyModifyAccess, contrary
to the model.RoleViewer doc comment. Not fixed; documented.
S20 — HTTP Range and HEAD on /stream, /download, /thumbnail. Flags
no-ETag (cacheability gap) and locks in stdlib Range semantics
(single, suffix, open, 416, multi-range).
S21 — upload negatives (missing parts, bad extension, traversal,
404, 403, dedup collisions, 413 skip note).
S22 — share expiry (sqlite UPDATE on expires_at, then verify 410 on
all three /s/{token}/... routes) + 5-token uniqueness via
crypto/rand audit.
S23 — user deletion cascade with schema audit: every user FK has
ON DELETE CASCADE; tags are global by design.
S24 — soft-delete persistence across rescan. Surfaces TWO real bugs
in scanner: (1) re-INSERT of soft-deleted media hits UNIQUE
constraint and fails the scan; (2) files deleted from disk
leave orphan media rows that never get reconciled.
S25 — SQL injection + XSS + path-traversal probes. SQL surface
fully parameterised (audited repository/media.go); XSS storage
is API-correct (UI escapes); share path traversal blocked by
Go ServeMux path cleaning.
Playwright e2e-web extensions (Round 7, 10 new tests):
share-page.test.ts (4) — share metadata payload, audio/video stage
elements, invalid-token 404 page.
search-filter.test.ts (3) — search filter, like:1 favourites syntax,
clearing input restores full grid.
admin-panel.test.ts (3) — user list, permissions section, rescan
button + scan-progress UI.
Round 8 audit (Android): 24/24 Flutter widget tests pass; the app is
currently a stub with UnimplementedError-only API client, so no
additional test scaffolding is justified until production code lands.
Verified: 25/25 LLM scenarios pass; 22/22 Playwright tests pass; full
Go unit-test suite green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Add player-server/testdata/media/ with ten public-domain media files:
five LibriVox Aesop's-Fables mp3 chapters (audiobooks/), four NASA
image-library jpgs (images/), and one NASA mp4 short (videos/). Total
~11 MB; each file's source URL and license is documented in
testdata/LICENSES.md and README.md.
Switch every default reference in tests, scenarios, docs, and CI from
MEDIA_ROOT=./testmedia to MEDIA_ROOT=./testdata/media so a fresh clone
can run mage E2E and the Playwright smoke suite without supplying any
external media. The ./testmedia path remains gitignored — it's the
local-only personal library directory.
Verified: 12/12 Playwright e2e-web pass; 18/18 LLM e2e scenarios pass
against the new fixture set.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Bug fix: POST /api/v1/progress and POST /api/v1/progress/batch did not
verify that the supplied media_id belonged to a media row the caller
could see. Two failure modes:
- Missing media_id triggered an FK violation in UpsertProgress, which
fell through handleError to HTTP 500 instead of 404.
- Soft-deleted media_id (row still exists, deleted_at != nil) was
accepted silently with HTTP 200, recording progress on an item the
user could no longer reach.
Both now route through accessHelper.verifyAccess in progressService,
which returns ErrNotFound (404) for missing/soft-deleted rows and
ErrForbidden (403) for unauthorized sets. Verified via curl:
POST /progress media_id=999999999 → 404; POST /progress/batch with a
bad id → 404.
Tests: progress_test.go and no_rows_test.go now seed MediaRepo and
UserRepo so the verifyAccess branch finds a real (admin) caller and a
real media row. All other tests untouched.
S16 covers media list pagination, filtering, sort, and the parser's
intentional fail-open behaviour for malformed limit/offset/type.
S17 covers podcast list endpoints (GET /podcasts, GET /podcasts/{id}/
episodes) and the admin-only subscribe gate. S18 covers single
POST /progress + GET /in-progress, including the new 404 path for
missing/forbidden media. Full LLM e2e suite passes 18/18.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Three more handlers had the same pattern as the RevokeShare bug fixed
in 0848488: the service returned a plain errors.New(...) for an
expected condition, so handleError fell through to its default 500
branch instead of mapping to 404/400.
- browse.GetThumbnail → ErrNotFound (404 instead of 500) when the
media row has no thumbnail path.
- tag.RemoveTag → ErrNotFound (404 instead of 500) when the tag name
does not exist. DELETE /media/{id}/tags/{unknown} now returns 404.
- write.RegenerateSetCover → new sentinel ErrEmptySetForCover, mapped
to 400 in handleError, when the target set contains no media files
eligible to be promoted to a cover.
S15 (auth-boundary negatives) is tightened: the share-revoke step used
to accept {404, 500} as a workaround for the bug; it now requires 404.
A new section C2 locks in the three regressions above so they cannot
silently return 500 again.
Verified end-to-end via curl: DELETE /media/{id}/tags/unknown → 404,
DELETE /shares/unknown-token → 404. Full LLM e2e suite 15/15 still
passes against the rebuilt server.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
scenarios
Bug fix: shareService.RevokeShare returned errors.New("share not found")
instead of the sentinel ErrShareNotFound, so handleError fell through to
HTTP 500 instead of 404 for DELETE /api/v1/shares/{unknown-token}. The
sister method ValidateShareToken already used the sentinel; this aligns
them. Verified with curl: DELETE on a missing token now returns 404.
The bug surfaced while drafting S15 (auth-boundary negative tests),
which exercises 401 unauthenticated, 403 non-admin → admin routes, and
404 unknown resources. S14 covers admin rescan + scan-progress polling.
All 15 LLM e2e scenarios pass against the fixed server.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
precheckServer() polls /healthz once before iterating scenarios. When
the server is down, the runner now exits 2 with a clear startup hint
instead of spawning Playwright per scenario (each timing out and
filing a duplicate "did not become healthy" task via ask add).
Also corrects S10-progress-batch.md: the YAML db assertion referenced
`progress` but the actual table (per internal/repository/playback_progress.go)
is `playback_progress`, with `media_id` as the joinable column.
Verified: precheck cleanly exits 2 when the server is down (15 s timeout);
full LLM e2e suite passes 13/13 with the server up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Covers: set browse + cover (S06), favorites/tags/notes (S07),
media streaming/download/thumbnail (S08), soft-delete/trash/restore (S09),
progress batch sync (S10), shares management (S11),
admin user management (S12), and admin permissions (S13).
Each scenario follows the S01–S05 YAML front-matter + numbered
Markdown step format. Routes and response shapes were verified
against player-server/internal/api/* handlers. Full suite passes
13/13; Playwright e2e-web suite passes 12/12.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Covers the full API token flow: login as admin, create a token via
POST /api/v1/auth/tokens, list and confirm it appears, verify the token
authenticates a Bearer request, revoke via DELETE /api/v1/auth/tokens/{id},
confirm it disappears from the list and a Bearer request now returns 401.
Then exercise POST /api/v1/logout and confirm the session cookie is
invalidated server-side (a subsequent authenticated request returns 401).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Replaces the Anthropic SDK dependency in oracle.ts with direct fetch calls to
the Ollama cloud API (https://ollama.com/v1/chat/completions). Uses
qwen3-vl:235b-instruct — the strongest vision-language model available on the
Ollama cloud — for screenshot analysis. The model is configurable via
OLLAMA_MODEL and the endpoint via OLLAMA_BASE_URL so local Ollama instances
can be used during development. OLLAMA_API_KEY carries the cloud bearer token.
Smoke-tested: gate-off returns true without a network call; yes/no questions
against a test PNG return correct answers via the cloud API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The admin check test registered waitForResponse after page.goto(), so the
403 from API.users() (fired at SPA init) could arrive before the listener
was active — causing a 10 s timeout. Fix: inject the session cookie and
register the response listener before navigation, then await the promise
after goto(). Also corrects the URL filter: the SPA calls /api/admin/users
(not /api/v1/admin/users) so the pattern now matches the actual request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
S01: bootstrap fresh DB → first admin user (browser + API assertions).
S02: podcast subscribe → episode download → mark complete (uses mock RSS server fixture).
S03: upload via API with Bearer token → verify media card on web (Haiku visual check).
S04: share-link round-trip — unauthenticated access, root redirects to /login.html (Haiku visual check).
Includes fixtures/mock-rss-server.js: minimal Node.js HTTP server serving a
valid RSS 2.0 podcast feed at http://localhost:8888/feed.xml with two episodes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Implements oracle.ts (Layer 5 of the assertion stack) which sends a PNG
screenshot to Claude Haiku with a yes/no question and returns true when the
answer starts with "yes". Gated behind LLM_E2E_SCREENSHOTS=true so CI runs
that don't set this variable skip visual checks silently without burning API
credits. Adds @anthropic-ai/sdk dependency to the runner package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|