| @@ -1,7 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | | − integrationFaviconFallbackUrl, |
| 5 | 4 | integrationFaviconSrc, |
| 6 | 5 | integrationFaviconUrl, |
| 7 | 6 | integrationInferredUrl, |
| @@ -10,22 +9,15 @@ import { · 2 unmodified lines |
| 10 | 9 | } from "./integration-favicon"; |
| 11 | 10 | |
| 12 | 11 | describe("IntegrationFavicon", () => { |
| 13 | | − it("uses the integrations.sh logo proxy as the primary favicon source", () => { |
| 12 | + it("resolves favicons only through the integrations.sh logo proxy", () => { |
| 14 | 13 | expect(integrationFaviconUrl("https://api.github.com/graphql", 20)).toBe( |
| 15 | 14 | "https://integrations.sh/logo/github.com?sz=40", |
| 16 | 15 | ); |
| 17 | 16 | }); |
| 18 | 17 | |
| 19 | | − it("falls back to the Google favicon service", () => { |
| 20 | | − expect(integrationFaviconFallbackUrl("https://api.github.com/graphql", 20)).toBe( |
| 21 | | − "https://www.google.com/s2/favicons?domain=github.com&sz=40", |
| 22 | | − ); |
| 23 | | − }); |
| 24 | | − |
| 25 | 18 | it("does not request favicons for local URLs", () => { |
| 26 | 19 | expect(integrationFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); |
| 27 | 20 | expect(integrationFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull(); |
| 28 | | − expect(integrationFaviconFallbackUrl("http://localhost:3000/private", 20)).toBeNull(); |
| 29 | 21 | }); |
| 30 | 22 | |
| 31 | 23 | it("sends only the registrable domain to the logo proxy", () => { |
| @@ -34,13 +26,11 @@ describe("IntegrationFavicon", () => { · 2 unmodified lines |
| 34 | 26 | ); |
| 35 | 27 | }); |
| 36 | 28 | |
| 37 | | − it("walks the cascade from the proxy to the favicon service on failure", () => { |
| 29 | + it("falls through to the placeholder when the proxy fails", () => { |
| 38 | 30 | const url = "https://api.github.com/graphql"; |
| 39 | 31 | const primary = integrationFaviconSrc({ url, size: 20 }); |
| 40 | 32 | expect(primary).toBe("https://integrations.sh/logo/github.com?sz=40"); |
| 41 | | − expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBe( |
| 42 | | − "https://www.google.com/s2/favicons?domain=github.com&sz=40", |
| 43 | | − ); |
| 33 | + expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBeNull(); |
| 44 | 34 | }); |
| 45 | 35 | |
| 46 | 36 | it("uses the Executor favicon for the built-in executor integration", () => { |
| @@ -102,63 +92,11 @@ describe("IntegrationFavicon", () => { · 55 unmodified lines |
| 102 | 92 | ).toBe("https://example.com/sheets.svg"); |
| 103 | 93 | }); |
| 104 | 94 | |
| 105 | | − it("finds preset icons from display names with suffixes", () => { |
| 106 | | − expect( |
| 107 | | − integrationPresetIconUrl( |
| 108 | | − { |
| 109 | | − id: "google_search_console_api", |
| 110 | | − kind: "googleDiscovery", |
| 111 | | − name: "Google Search Console API", |
| 112 | | − }, |
| 113 | | − [ |
| 114 | | − { |
| 115 | | − key: "google", |
| 116 | | − label: "Google", |
| 117 | | − add: () => null, |
| 118 | | − edit: () => null, |
| 119 | | − presets: [ |
| 120 | | − { |
| 121 | | − id: "google-search-console", |
| 122 | | − name: "Google Search Console", |
| 123 | | − summary: "Search performance.", |
| 124 | | − icon: "https://example.com/google.svg", |
| 125 | | − }, |
| 126 | | − ], |
| 127 | | − }, |
| 128 | | − ], |
| 129 | | − ), |
| 130 | | − ).toBe("https://example.com/google.svg"); |
| 131 | | − }); |
| 132 | | − |
| 133 | | − it("finds preset icons from an integration id when the URL is missing", () => { |
| 134 | | − expect( |
| 135 | | − integrationPresetIconUrl( |
| 136 | | − { |
| 137 | | − id: "sentry", |
| 138 | | − kind: "mcp", |
| 139 | | − name: "Sentry MCP", |
| 140 | | − }, |
| 141 | | − [ |
| 142 | | − { |
| 143 | | − key: "mcp", |
| 144 | | − label: "MCP", |
| 145 | | − add: () => null, |
| 146 | | − edit: () => null, |
| 147 | | − presets: [ |
| 148 | | − { |
| 149 | | − id: "sentry", |
| 150 | | − name: "Sentry", |
| 151 | | − summary: "Errors.", |
| 152 | | − icon: "https://example.com/sentry.png", |
| 153 | | − }, |
| 154 | | − ], |
| 155 | | − }, |
| 156 | | − ], |
| 157 | | − ), |
| 158 | | − ).toBe("https://example.com/sentry.png"); |
| 159 | | − }); |
| 160 | | − |
| 161 | | − it("matches migrated MCP slugs with host/suffix noise", () => { |
| 95 | + it("does not fuzzy-match preset icons from names or slugs", () => { |
| 96 | + // Name/slug token matching is gone: without an exact defaultSlug or URL |
| 97 | + // match, the preset matcher declines and the cascade resolves through the |
| 98 | + // integrations.sh favicon instead, which is derived from the integration's |
| 99 | + // own domain and therefore cannot render the wrong brand. |
| 162 | 100 | const presets = [ |
| 163 | 101 | { |
| 164 | 102 | key: "mcp", |
| @@ -167,63 +105,25 @@ describe("IntegrationFavicon", () => { · 2 unmodified lines |
| 167 | 105 | edit: () => null, |
| 168 | 106 | presets: [ |
| 169 | 107 | { |
| 170 | | − id: "posthog", |
| 171 | | − name: "PostHog", |
| 172 | | − summary: "Analytics.", |
| 173 | | − icon: "https://example.com/posthog.png", |
| 174 | | − }, |
| 175 | | − { |
| 176 | | − id: "linear", |
| 177 | | − name: "Linear", |
| 178 | | − summary: "Issues.", |
| 179 | | − icon: "https://example.com/linear.png", |
| 180 | | − }, |
| 181 | | − { |
| 182 | | − id: "planetscale", |
| 183 | | − name: "PlanetScale", |
| 184 | | − summary: "Databases.", |
| 185 | | − icon: "https://example.com/pscale.png", |
| 108 | + id: "sentry", |
| 109 | + name: "Sentry", |
| 110 | + summary: "Errors.", |
| 111 | + icon: "https://example.com/sentry.png", |
| 186 | 112 | }, |
| 187 | 113 | ], |
| 188 | 114 | }, |
| 189 | 115 | ]; |
| 190 | 116 | |
| 191 | 117 | expect( |
| 192 | | − integrationPresetIconUrl( |
| 193 | | − { id: "mcp_posthog_com", kind: "mcp", name: "mcp.posthog.com" }, |
| 194 | | − presets, |
| 195 | | − ), |
| 196 | | − ).toBe("https://example.com/posthog.png"); |
| 197 | | − expect( |
| 198 | | − integrationPresetIconUrl( |
| 199 | | − { id: "mcp_linear_app", kind: "mcp", name: "mcp.linear.app" }, |
| 200 | | − presets, |
| 201 | | − ), |
| 202 | | − ).toBe("https://example.com/linear.png"); |
| 203 | | − expect( |
| 204 | | − integrationPresetIconUrl({ id: "pscale_mcp", kind: "mcp", name: "Pscale MCP" }, presets), |
| 205 | | − ).toBe("https://example.com/pscale.png"); |
| 206 | | − }); |
| 207 | | − |
| 208 | | − it("matches migrated OpenAPI slugs with API/REST suffixes", () => { |
| 209 | | − expect( |
| 210 | | − integrationPresetIconUrl({ id: "stripe_api", kind: "openapi", name: "Stripe API" }, [ |
| 211 | | − { |
| 212 | | − key: "openapi", |
| 213 | | − label: "OpenAPI", |
| 214 | | − add: () => null, |
| 215 | | − edit: () => null, |
| 216 | | − presets: [ |
| 217 | | − { |
| 218 | | − id: "stripe", |
| 219 | | − name: "Stripe", |
| 220 | | − summary: "Payments.", |
| 221 | | − icon: "https://example.com/stripe.png", |
| 222 | | − }, |
| 223 | | − ], |
| 224 | | − }, |
| 225 | | − ]), |
| 226 | | − ).toBe("https://example.com/stripe.png"); |
| 118 | + integrationPresetIconUrl({ id: "sentry", kind: "mcp", name: "Sentry MCP" }, presets), |
| 119 | + ).toBeNull(); |
| 120 | + |
| 121 | + // Migrated host-shaped integrations resolve through the inferred URL: |
| 122 | + const inferred = integrationInferredUrl({ id: "mcp_posthog_com", name: "mcp.posthog.com" }); |
| 123 | + expect(inferred).toBe("https://mcp.posthog.com"); |
| 124 | + expect(integrationFaviconSrc({ url: inferred ?? undefined, size: 16 })).toBe( |
| 125 | + "https://integrations.sh/logo/posthog.com?sz=32", |
| 126 | + ); |
| 227 | 127 | }); |
| 228 | 128 | |
| 229 | 129 | it("prefers exact catalog defaultSlug icons for OpenAPI provider services", () => { |
| @@ -248,7 +148,25 @@ describe("IntegrationFavicon", () => { · 18 unmodified lines |
| 248 | 148 | ).toBe("https://example.com/gmail.png"); |
| 249 | 149 | }); |
| 250 | 150 | |
| 251 | | − it("does not split generic words into brand matches", () => { |
| 151 | + it("matches presets on exact URL equality, not names", () => { |
| 152 | + const presets = [ |
| 153 | + { |
| 154 | + key: "openapi", |
| 155 | + label: "OpenAPI", |
| 156 | + add: () => null, |
| 157 | + edit: () => null, |
| 158 | + presets: [ |
| 159 | + { |
| 160 | + id: "spotify", |
| 161 | + name: "Spotify", |
| 162 | + summary: "Music.", |
| 163 | + url: "https://api.spotify.com/v1", |
| 164 | + icon: "https://example.com/spotify.png", |
| 165 | + }, |
| 166 | + ], |
| 167 | + }, |
| 168 | + ]; |
| 169 | + |
| 252 | 170 | expect( |
| 253 | 171 | integrationPresetIconUrl( |
| 254 | 172 | { |
| @@ -257,30 +175,43 @@ describe("IntegrationFavicon", () => { · 2 unmodified lines |
| 257 | 175 | name: "Spotify Web API", |
| 258 | 176 | url: "https://api.spotify.com/v1", |
| 259 | 177 | }, |
| 260 | | − [ |
| 261 | | − { |
| 262 | | − key: "openapi", |
| 263 | | − label: "OpenAPI", |
| 264 | | − add: () => null, |
| 265 | | − edit: () => null, |
| 266 | | − presets: [ |
| 267 | | − { |
| 268 | | − id: "exa-websets", |
| 269 | | − name: "Exa Websets", |
| 270 | | − summary: "Web data.", |
| 271 | | − icon: "https://example.com/exa.png", |
| 272 | | − }, |
| 273 | | − { |
| 274 | | − id: "spotify", |
| 275 | | − name: "Spotify", |
| 276 | | − summary: "Music.", |
| 277 | | − icon: "https://example.com/spotify.png", |
| 278 | | − }, |
| 279 | | − ], |
| 280 | | − }, |
| 281 | | − ], |
| 178 | + presets, |
| 282 | 179 | ), |
| 283 | 180 | ).toBe("https://example.com/spotify.png"); |
| 181 | + |
| 182 | + // Same name, different URL: no match — the URL-derived favicon takes over. |
| 183 | + expect( |
| 184 | + integrationPresetIconUrl( |
| 185 | + { |
| 186 | + id: "spotify_web_api", |
| 187 | + kind: "openapi", |
| 188 | + name: "Spotify Web API", |
| 189 | + url: "https://api.spotify.com/v2", |
| 190 | + }, |
| 191 | + presets, |
| 192 | + ), |
| 193 | + ).toBeNull(); |
| 194 | + }); |
| 195 | + |
| 196 | + it("does not match a different brand sharing a word fragment (ClickHouse Cloud vs Cloudflare)", () => { |
| 197 | + expect( |
| 198 | + integrationPresetIconUrl({ id: "clickhouse", kind: "mcp", name: "ClickHouse Cloud" }, [ |
| 199 | + { |
| 200 | + key: "mcp", |
| 201 | + label: "MCP", |
| 202 | + add: () => null, |
| 203 | + edit: () => null, |
| 204 | + presets: [ |
| 205 | + { |
| 206 | + id: "cloudflare", |
| 207 | + name: "Cloudflare", |
| 208 | + summary: "Workers, KV, D1, R2, and DNS management via MCP.", |
| 209 | + icon: "https://integrations.sh/logo/cloudflare.com", |
| 210 | + }, |
| 211 | + ], |
| 212 | + }, |
| 213 | + ]), |
| 214 | + ).toBeNull(); |
| 284 | 215 | }); |
| 285 | 216 | |
| 286 | 217 | it("infers favicon URLs from migrated host-shaped MCP names and slugs", () => { |