feat(extension): probe shows current state before click; v1.0.6
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 20s
extension / lint (push) Successful in 13s
CI / intimp (push) Successful in 3m41s
CI / intapi (push) Successful in 7m11s
CI / intcore (push) Successful in 7m41s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 20s
extension / lint (push) Successful in 13s
CI / intimp (push) Successful in 3m41s
CI / intapi (push) Successful in 7m11s
CI / intcore (push) Successful in 7m41s
Operator-asked 2026-05-31 (during sidecar synthetic anchor cleanup): "the add source/subscription button idea to the firefox extension so it can tell me if a source/artist is added or not and offer an option to add it if it isn't." Plan tracked in Scribe task #507. ## Backend - `ExtensionService.probe(url)` — read-only resolution. Reuses `_derive` for platform+slug, then 2 SELECTs. Returns one of: - `source_match` (exact (artist, platform, url) Source exists) - `artist_match` (artist exists, this URL isn't a Source yet; collapses the sidecar-synthetic-only case from v26.06.01.0) - `new` (neither exists) - `unknown_platform` (URL didn't match any artist-page regex) - `GET /api/extension/probe?url=...` route with `X-Extension-Key` auth posture matching `/quick-add-source`. Read-only, side-effect free. - 6 backend tests in tests/test_api_extension.py covering each state + auth + invalid URL. ## Extension - `api.js`: `probeSource(url)` mirroring `quickAddSource` shape. - `background.js`: `PROBE_SOURCE` + `OPEN_ARTIST_PAGE` handlers. The latter strips the `/api` suffix from configured `apiUrl` (placeholder format per options.html) and opens `${base}/artist/{slug}` in a new tab via `browser.tabs.create`. - `content-script.js`: probe-first render — on page-load and SPA navigation, asks the backend for the URL's state and renders the chip in the matching color/copy on FIRST paint instead of flashing generic "Add" and updating after. Click handler branches: `source_match` → OPEN_ARTIST_PAGE; `artist_match`/`new` → existing ADD_AS_SOURCE flow (then re-probes so the chip flips green immediately, no wait for next nav). - `content-script.css`: three state-color modifiers (--new, --artist-match, --source-match) on the FC parchment-on-slate palette. Sage for already-added, amber for artist-exists, accent orange for new. ## Versioning - `extension/manifest.json` + `extension/package.json` → 1.0.6. build.yml's sign-extension job will fire on push to main since no `ext-1.0.6` Forgejo/Gitea release exists yet — exercises the regenerated AMO keys end-to-end. ## Behavior on the sidecar-synthetic case Filesystem-imported "Dymkens"-style artist with only a sidecar synthetic Source: probe returns `artist_match` (not `new`), so the chip reads "+ Add Patreon source to Dymkens" rather than offering to recreate the artist. Clicking adds the real Source; existing `_source_for_sidecar` preference logic (v26.06.01.0) routes future gallery-dl Posts to the real one.
This commit is contained in:
@@ -135,6 +135,114 @@ async def test_quick_add_source_wrong_key_401(client, ext_key):
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# --- /api/extension/probe ---------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_returns_new_when_nothing_exists(client, ext_key):
|
||||
resp = await client.get(
|
||||
"/api/extension/probe",
|
||||
query_string={"url": "https://www.patreon.com/freshcreator"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["state"] == "new"
|
||||
assert body["platform"] == "patreon"
|
||||
assert body["slug"] == "freshcreator"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_returns_source_match_for_already_added(client, ext_key, db):
|
||||
artist = Artist(name="Alice", slug="alice", is_subscription=True)
|
||||
db.add(artist)
|
||||
await db.flush()
|
||||
src = Source(
|
||||
artist_id=artist.id, platform="patreon",
|
||||
url="https://www.patreon.com/alice", enabled=True, config_overrides={},
|
||||
)
|
||||
db.add(src)
|
||||
await db.commit()
|
||||
|
||||
resp = await client.get(
|
||||
"/api/extension/probe",
|
||||
query_string={"url": "https://www.patreon.com/alice"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["state"] == "source_match"
|
||||
assert body["artist"]["slug"] == "alice"
|
||||
assert body["source"]["url"] == "https://www.patreon.com/alice"
|
||||
assert body["source"]["platform"] == "patreon"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_returns_artist_match_when_only_synthetic_anchor_exists(
|
||||
client, ext_key, db,
|
||||
):
|
||||
"""Filesystem-imported artist with only a sidecar synthetic Source
|
||||
for the (artist, platform) — the URL the operator's browsing isn't
|
||||
yet a real Source. The probe should collapse this into artist_match
|
||||
so the chip says '+ Add Patreon source to Dymkens' rather than
|
||||
'+ Add to FabledCurator' (which would re-create the artist)."""
|
||||
artist = Artist(name="Dymkens", slug="dymkens", is_subscription=False)
|
||||
db.add(artist)
|
||||
await db.flush()
|
||||
synthetic = Source(
|
||||
artist_id=artist.id, platform="patreon",
|
||||
url="sidecar:patreon:dymkens", enabled=False, config_overrides={},
|
||||
)
|
||||
db.add(synthetic)
|
||||
await db.commit()
|
||||
|
||||
resp = await client.get(
|
||||
"/api/extension/probe",
|
||||
query_string={"url": "https://www.patreon.com/dymkens"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["state"] == "artist_match"
|
||||
assert body["artist"]["slug"] == "dymkens"
|
||||
assert "source" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_returns_unknown_platform_for_non_artist_url(client, ext_key):
|
||||
"""A patreon URL that isn't an artist page (e.g. /home, /posts/N)
|
||||
shouldn't trigger the button. Sentinel 'unknown_platform' state
|
||||
tells the content script to skip injection."""
|
||||
resp = await client.get(
|
||||
"/api/extension/probe",
|
||||
query_string={"url": "https://www.patreon.com/posts/12345"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["state"] == "unknown_platform"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_missing_key_401(client):
|
||||
resp = await client.get(
|
||||
"/api/extension/probe",
|
||||
query_string={"url": "https://www.patreon.com/maewix"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_missing_url_400(client, ext_key):
|
||||
resp = await client.get(
|
||||
"/api/extension/probe",
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
body = await resp.get_json()
|
||||
assert body["error"] == "invalid_body"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_source_missing_body_400(client, ext_key):
|
||||
resp = await client.post(
|
||||
|
||||
Reference in New Issue
Block a user