feat: the extension adds Discord channels to an artist you pick, and its tests gate the XPI (milestone 429)
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 24s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m22s
CI and images / build-agent (push) Successful in 5s
CI and images / sign-extension (push) Successful in 3m13s
CI and images / build-web (push) Successful in 1m42s
CI and images / smoke-web (push) Successful in 54s
CI and images / promote (push) Successful in 1s
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 24s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m22s
CI and images / build-agent (push) Successful in 5s
CI and images / sign-extension (push) Successful in 3m13s
CI and images / build-web (push) Successful in 1m42s
CI and images / smoke-web (push) Successful in 54s
CI and images / promote (push) Successful in 1s
Server (#4420) - extension_service gains a Discord pattern (server or channel, jump links, ptb/canary; not DMs or threads), mirrored in platforms.js and pinned by the shared artist-url-samples.json. - probe on a Discord URL matches the source by ids under any artist, reports a whole-server source as covering the channel, suggests the artist who owns another source on the same server, and names server/channel via the stored token (best-effort, bounded, no rate-limit waits). - quick-add takes artist_id / artist_name; Discord URLs are stored canonical. Extension (#4421, #4422) - Content script on discord.com; SPA navigation by URL polling (the old pushState patch ran in the isolated world and never fired); stale probes are dropped. - Discord chip opens an Add panel: this channel or the whole server, and the suggested artist / a search / a new name. - Popup: sources show artist, platform and state; a Discord token export is verified by FC and the result shown. Token capture covers ptb/canary. - Pure logic in lib/chip.js and lib/popup-format.js, with specs. CI (#4423) - extension.yml's lane (web-ext lint, vitest, XPI contents) moves into build.yml as extension-test and joins the needs of sign-extension, build-web and build-agent. As a separate workflow it gated nothing: a red extension suite still signed and shipped the XPI (rule 177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -355,6 +355,177 @@ async def test_quick_add_source_attaches_to_existing_artist(client, ext_key, db,
|
||||
assert artist_count == 1 # no duplicate created
|
||||
|
||||
|
||||
# --- Discord: channels are added to a CHOSEN artist (milestone 429) ---
|
||||
|
||||
|
||||
_GUILD = "111111111111111111"
|
||||
_CHAN = "222222222222222222"
|
||||
_OTHER_CHAN = "333333333333333333"
|
||||
|
||||
|
||||
async def _discord_source(db, name, url):
|
||||
artist = Artist(name=name, slug=name.lower(), is_subscription=True)
|
||||
db.add(artist)
|
||||
await db.flush()
|
||||
src = Source(artist_id=artist.id, platform="discord", url=url,
|
||||
enabled=True, config_overrides={})
|
||||
db.add(src)
|
||||
await db.commit()
|
||||
return artist, src
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_discord_channel_is_added_to_the_artist_the_operator_picked(
|
||||
client, ext_key, db, db_sync,
|
||||
):
|
||||
artist = Artist(name="Tamada", slug="tamada", is_subscription=True)
|
||||
db.add(artist)
|
||||
await db.commit()
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
# A jump link on the ptb host still names the channel.
|
||||
json={"url": f"https://ptb.discord.com/channels/{_GUILD}/{_CHAN}/999",
|
||||
"artist_id": artist.id},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 201, await resp.get_json()
|
||||
body = await resp.get_json()
|
||||
assert body["artist"]["id"] == artist.id
|
||||
assert body["created_artist"] is False
|
||||
# Stored canonical, so the manual form and the ingester read the same URL.
|
||||
assert body["source"]["url"] == f"https://discord.com/channels/{_GUILD}/{_CHAN}"
|
||||
assert body["source"]["platform"] == "discord"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_discord_add_can_name_a_new_artist(client, ext_key):
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": f"https://discord.com/channels/{_GUILD}", "artist_name": "Studio Q"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = await resp.get_json()
|
||||
assert body["artist"]["name"] == "Studio Q"
|
||||
assert body["created_artist"] is True
|
||||
assert body["source"]["url"] == f"https://discord.com/channels/{_GUILD}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_discord_add_with_no_artist_and_no_token_names_the_server(client, ext_key):
|
||||
"""No stored token means no server name to read; the fallback still names
|
||||
a readable artist rather than slugifying the ids."""
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": f"https://discord.com/channels/{_GUILD}/{_CHAN}"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert (await resp.get_json())["artist"]["name"] == f"Discord {_GUILD}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_re_adding_a_discord_channel_keeps_its_artist(client, ext_key, db):
|
||||
"""Identity by source (#130), compared by ids: a row stored with a
|
||||
trailing slash is the same channel, and naming another artist does not
|
||||
move it."""
|
||||
owner, src = await _discord_source(
|
||||
db, "Owner", f"https://discord.com/channels/{_GUILD}/{_CHAN}/",
|
||||
)
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": f"https://discord.com/channels/{_GUILD}/{_CHAN}",
|
||||
"artist_name": "Someone Else"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["source"]["id"] == src.id
|
||||
assert body["artist"]["id"] == owner.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_with_a_missing_artist_id_is_404(client, ext_key):
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": f"https://discord.com/channels/{_GUILD}/{_CHAN}", "artist_id": 987654},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_rejects_a_non_integer_artist_id(client, ext_key):
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": f"https://discord.com/channels/{_GUILD}/{_CHAN}", "artist_id": "7"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
async def _probe(client, ext_key, url):
|
||||
resp = await client.get(
|
||||
"/api/extension/probe", query_string={"url": url},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
return await resp.get_json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_a_fresh_discord_channel_is_new_with_both_urls(client, ext_key):
|
||||
body = await _probe(client, ext_key, f"https://discord.com/channels/{_GUILD}/{_CHAN}")
|
||||
assert body["state"] == "new"
|
||||
assert body["platform"] == "discord"
|
||||
d = body["discord"]
|
||||
assert d["server_id"] == _GUILD and d["channel_id"] == _CHAN
|
||||
assert d["server_url"] == f"https://discord.com/channels/{_GUILD}"
|
||||
assert d["channel_url"] == f"https://discord.com/channels/{_GUILD}/{_CHAN}"
|
||||
# No token stored → no names, and no error.
|
||||
assert d["server_name"] is None and d["channel_name"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_suggests_the_artist_who_owns_another_channel_on_the_server(
|
||||
client, ext_key, db,
|
||||
):
|
||||
owner, _ = await _discord_source(
|
||||
db, "Owner", f"https://discord.com/channels/{_GUILD}/{_OTHER_CHAN}",
|
||||
)
|
||||
body = await _probe(client, ext_key, f"https://discord.com/channels/{_GUILD}/{_CHAN}")
|
||||
assert body["state"] == "artist_match"
|
||||
assert body["artist"]["id"] == owner.id
|
||||
assert "source" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_finds_the_channel_under_any_artist(client, ext_key, db):
|
||||
owner, src = await _discord_source(
|
||||
db, "Owner", f"https://discord.com/channels/{_GUILD}/{_CHAN}",
|
||||
)
|
||||
body = await _probe(client, ext_key, f"https://discord.com/channels/{_GUILD}/{_CHAN}/555")
|
||||
assert body["state"] == "source_match"
|
||||
assert body["source"]["id"] == src.id
|
||||
assert body["artist"]["id"] == owner.id
|
||||
assert body["covered_by_server"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_a_channel_is_covered_by_a_whole_server_source(client, ext_key, db):
|
||||
_, src = await _discord_source(db, "Owner", f"https://discord.com/channels/{_GUILD}")
|
||||
body = await _probe(client, ext_key, f"https://discord.com/channels/{_GUILD}/{_CHAN}")
|
||||
assert body["state"] == "source_match"
|
||||
assert body["source"]["id"] == src.id
|
||||
assert body["covered_by_server"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_a_discord_dm_is_not_a_source(client, ext_key):
|
||||
body = await _probe(client, ext_key, f"https://discord.com/channels/@me/{_CHAN}")
|
||||
assert body["state"] == "unknown_platform"
|
||||
|
||||
|
||||
# --- /api/extension/manifest ---------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -104,9 +104,8 @@ def test_the_sample_table_covers_every_platform_that_has_a_pattern():
|
||||
samples would make this file pass by testing less — the failure mode that
|
||||
makes absence-based tests untrustworthy (snippet #3352).
|
||||
|
||||
Discord is deliberately absent from both: it has no artist pattern on
|
||||
either side, because it is channel-based and has no creator page to put a
|
||||
button on.
|
||||
Discord is in the table since milestone 429: its "slug" is the
|
||||
server/channel pair, and the Add panel picks the artist.
|
||||
"""
|
||||
from backend.app.services.extension_service import _PLATFORM_PATTERNS
|
||||
|
||||
|
||||
Reference in New Issue
Block a user