diff --git a/docs/plans/integration-readiness-matrix.md b/docs/plans/integration-readiness-matrix.md index cebee40bac..63a40d6b5f 100644 --- a/docs/plans/integration-readiness-matrix.md +++ b/docs/plans/integration-readiness-matrix.md @@ -49,7 +49,7 @@ The matrix below is the definition of "ready". A row is ready when every cell is |---|---|---| | Telegram | yes | Re-walked live 2026-09-27 on `c941626b` with a real account (below). **C1 green**: the code typed exactly as the page shows it, spaces and a capitalised group included, binds on the first try (#1931 TASK-153, #1932 TASK-157); on `7ccc6ac2` the same form was refused. **More than one pod, green outbound**: a second pod switched on under "Pods that reach this channel" relayed its agent's line to the chat within the minute, and a line typed in the chat went only to the active pod, as the connector is built (one private chat binds one Commonly user, pods behind it as gates; TASK-154). **C3 green** (an agent line relayed) and **C5 inbound green** with sender identity (" (via Telegram)"). C10 green: both refusals are named, and since #1878 a dead chat is a named failure. Shipped since the re-walk: relayed lines now carry their pod's name (#1935, TASK-156; seen live on `19d1d3e2` as "[Connector walk 0926] Scout: PONG-TAG"), and a quote-reply routes to the quoted line's pod or is refused by name (#1935; unit-witnessed, not walked live). The catalogue row names the multi-pod model (#1957, TASK-158) | | Slack | yes | Walked live 2026-09-26/27 on `7ccc6ac2` with a real workspace (below). **C1, C3 and C5 green**: connect through consent, callback and confirm; a hosted agent's reply relays to Slack; a Slack mention reaches the agent and its answer comes back. C5 inbound was green only after two Slack app settings were fixed during the walk. Red for customers until the app was publicly distributed, which is now done, and the distribution attestation is now true of the running backend: #1929 (TASK-151) removed every `SLACK_BOT_TOKEN`/`SLACK_APP_TOKEN` read and injection, live on `f5eb5563`. A second authorize on a connected row is refused with 409 `slack_already_authorized` (measured through the API); the refusal copy on the page (#1890, Row C) is still unwalked in the UI. Before #1875 (09-04 to 09-25), every new install was refused at Authorize | -| Discord | partly | **C0 listed, not offered**: #1826 (approved 2026-09-29) gives Discord its own row, live since `96cc9711` (2026-09-29 12:11Z). `/api/installables` returns `discord` under `channels` with `available: true, offered: false`. `offered` is the roster decision (`installableCatalogService.ts` `offeredByRoster`: an active builtin Installable row exists), and commonly.me has no `discord` Installable row, so it is not offered. `available` is the manifest readiness (`manifests.ts`: missing required keys would make it `false`), and it reads `true`. A stranger sees "Not enabled on this instance · ask your operator" rather than a merged "not yet" row. **red** in C1: not connectable (TASK-104) | +| Discord | partly | **Not listed since 2026-10-31 (D215).** The Connectors page no longer shows a provider this instance does not offer, unless the person already installed it, so a stranger sees no Discord row at all. The API still reports the entry with `offered: false`. Before that, **C0 listed, not offered**: #1826 (approved 2026-09-29) gives Discord its own row, live since `96cc9711` (2026-09-29 12:11Z). `/api/installables` returns `discord` under `channels` with `available: true, offered: false`. `offered` is the roster decision (`installableCatalogService.ts` `offeredByRoster`: an active builtin Installable row exists), and commonly.me has no `discord` Installable row, so it is not offered. `available` is the manifest readiness (`manifests.ts`: missing required keys would make it `false`), and it reads `true`. A stranger saw "Unavailable here." (the not-connectable line) rather than a merged "not yet" row. **red** in C1: not connectable (TASK-104) | | GroupMe | yes | **red**: TASK-101 | | X | yes (admin OAuth callback + feed) | unverified | | GitHub (app) | yes | Walked 2026-09-25 and re-walked 2026-09-26 on `5561a1dd` (both below). Live on our own repository since 09-18. **red** in C1 until per-person GitHub: a team cannot connect its own repository. C2 at 390 is green since #1874, shown on `5561a1dd`. C3 (a hosted agent gets the broker, #1880) shipped and has not been walked. C8 was green on `9a32fca5`. A new grant from the connection's owner, `grant_3c4d6ad5` (2026-09-29), has the same scope as the lapsed 09-18 one: pod "C4 run 2026-09-18"; `list_issues`, `create_issue` and `close_issue`; write-with-confirm; audience the owner, `c3stranger0912` and `c4-smoke`. It runs to 2026-10-06, so C3, C4 and C8 are walkable again. None has been walked on the current build | diff --git a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx index a8a1fc65c6..7706a03e34 100644 --- a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx @@ -1234,16 +1234,32 @@ describe('V2ConnectorsPage', () => { expect(screen.queryByText(/Tell us which channel/)).toBeNull(); }); - it('reads a usable-but-unrostered provider as not connectable, with no Add anywhere', async () => { - // The ruled middle state: this instance can use Discord - // and there is no builtin Installable row to install, so the row states the - // state and offers no action. Before this, the row rendered an Add whose - // click ended in 404 installable_not_found. + it('does not list a provider this instance does not offer (D215)', async () => { + // Discord on commonly.me: usable keys, no builtin Installable row, never + // walked. A stranger must not see it, not even in the inactive fold. mockCatalog([ + entry(), entry({ installableId: 'discord', label: 'Discord', available: true, offered: false }), ]); renderPage(); + await screen.findByText('Telegram'); + // The fold only exists when a dead row does, so its absence is the + // assertion; a collapsed fold would hide a listed Discord from queryByText. + expect(screen.queryByRole('button', { name: /^inactive · \d+$/ })).toBeNull(); + expect(screen.queryByText('Discord')).toBeNull(); + }); + + it('keeps an installed but no-longer-offered provider as not connectable, with no Add anywhere', async () => { + // The ruled middle state, now reachable only by someone who installed it + // before the roster row went away: the row states the state and offers no + // action. Before #1826 the row rendered an Add whose click ended in 404 + // installable_not_found. + mockCatalog([ + entry({ installableId: 'discord', label: 'Discord', available: true, offered: false, installation: { status: 'active', boundPodId: 'p1' } }), + ]); + renderPage(); + await openInactiveFold(); const roster = (await screen.findByText('Unavailable here.')).closest('.v2-connector-row') as HTMLElement; expect(roster).toHaveClass('v2-connector-row--not-enabled'); @@ -1272,6 +1288,20 @@ describe('V2ConnectorsPage', () => { expect(screen.queryByText(/Tell us which channel/)).toBeNull(); }); + it('keeps a no-longer-offered provider listed when the person has only an integration row', async () => { + // The third arm of the D215 filter: a channel connected before the roster + // row went away can leave an integration with no installation. Dropping it + // from the list would hide a live connection from its owner. + mockCatalog([ + entry({ installableId: 'discord', label: 'Discord', available: true, offered: false, integration: liveIntegration() }), + ]); + renderPage(); + + await openInactiveFold(); + const row = (await screen.findByText('Unavailable here.')).closest('.v2-connector-row') as HTMLElement; + expect(row.textContent).toContain('Discord'); + }); + it('renders an unavailable provider with a state line and an available one with Add', async () => { mockCatalog([ entry(), diff --git a/frontend/src/v2/components/V2ConnectorsPage.tsx b/frontend/src/v2/components/V2ConnectorsPage.tsx index f4032ee912..2a94d69062 100644 --- a/frontend/src/v2/components/V2ConnectorsPage.tsx +++ b/frontend/src/v2/components/V2ConnectorsPage.tsx @@ -1114,8 +1114,15 @@ const V2ConnectorsPage: React.FC = () => { }; const catalogTypes = new Set((catalog || []).map((entry) => entry.installableId)); + // D215: a provider this instance does not offer is not listed, so a connector + // nobody here has walked is never shown. The API still reports it (the + // offerability contract), and someone who already installed it keeps their + // row, which renders in the not-connectable state below. + const listedCatalog = (catalog || []).filter((entry) => ( + entry.offered !== false || Boolean(entry.installation) || Boolean(entry.integration) + )); const allItems: ListItem[] = [ - ...(catalog || []).map((entry): ListItem => ({ + ...listedCatalog.map((entry): ListItem => ({ kind: 'catalog', key: `catalog:${entry.installableId}`, entry,