| @@ -1,7 +1,6 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | | 2 | |
| 3 | import { | 3 | import { |
| 4 | integrationFaviconFallbackUrl, | | |
| 5 | integrationFaviconSrc, | 4 | integrationFaviconSrc, |
| 6 | integrationFaviconUrl, | 5 | integrationFaviconUrl, |
| 7 | integrationInferredUrl, | 6 | integrationInferredUrl, |
| @@ -10,22 +9,15 @@ import { · 2 unmodified lines |
| 10 | } from "./integration-favicon"; | 9 | } from "./integration-favicon"; |
| 11 | | 10 | |
| 12 | describe("IntegrationFavicon", () => { | 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 | expect(integrationFaviconUrl("https://api.github.com/graphql", 20)).toBe( | 13 | expect(integrationFaviconUrl("https://api.github.com/graphql", 20)).toBe( |
| 15 | "https://integrations.sh/logo/github.com?sz=40", | 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 | it("does not request favicons for local URLs", () => { | 18 | it("does not request favicons for local URLs", () => { |
| 26 | expect(integrationFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); | 19 | expect(integrationFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); |
| 27 | expect(integrationFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull(); | 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 | it("sends only the registrable domain to the logo proxy", () => { | 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 | const url = "https://api.github.com/graphql"; | 30 | const url = "https://api.github.com/graphql"; |
| 39 | const primary = integrationFaviconSrc({ url, size: 20 }); | 31 | const primary = integrationFaviconSrc({ url, size: 20 }); |
| 40 | expect(primary).toBe("https://integrations.sh/logo/github.com?sz=40"); | 32 | expect(primary).toBe("https://integrations.sh/logo/github.com?sz=40"); |
| 41 | expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBe( | 33 | expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBeNull(); |
| 42 | "https://www.google.com/s2/favicons?domain=github.com&sz=40", | | |
| 43 | ); | | |
| 44 | }); | 34 | }); |
| 45 | | 35 | |
| 46 | it("uses the Executor favicon for the built-in executor integration", () => { | 36 | it("uses the Executor favicon for the built-in executor integration", () => { |
| @@ -102,63 +92,11 @@ describe("IntegrationFavicon", () => { · 55 unmodified lines |
| 102 | ).toBe("https://example.com/sheets.svg"); | 92 | ).toBe("https://example.com/sheets.svg"); |
| 103 | }); | 93 | }); |
| 104 | | 94 | |
| 105 | it("finds preset icons from display names with suffixes", () => { | 95 | it("does not fuzzy-match preset icons from names or slugs", () => { |
| 106 | expect( | 96 | // Name/slug token matching is gone: without an exact defaultSlug or URL |
| 107 | integrationPresetIconUrl( | 97 | // match, the preset matcher declines and the cascade resolves through the |
| 108 | { | 98 | // integrations.sh favicon instead, which is derived from the integration's |
| 109 | id: "google_search_console_api", | 99 | // own domain and therefore cannot render the wrong brand. |
| 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", () => { | | |
| 162 | const presets = [ | 100 | const presets = [ |
| 163 | { | 101 | { |
| 164 | key: "mcp", | 102 | key: "mcp", |
| @@ -167,63 +105,25 @@ describe("IntegrationFavicon", () => { · 2 unmodified lines |
| 167 | edit: () => null, | 105 | edit: () => null, |
| 168 | presets: [ | 106 | presets: [ |
| 169 | { | 107 | { |
| 170 | id: "posthog", | 108 | id: "sentry", |
| 171 | name: "PostHog", | 109 | name: "Sentry", |
| 172 | summary: "Analytics.", | 110 | summary: "Errors.", |
| 173 | icon: "https://example.com/posthog.png", | 111 | icon: "https://example.com/sentry.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", | | |
| 186 | }, | 112 | }, |
| 187 | ], | 113 | ], |
| 188 | }, | 114 | }, |
| 189 | ]; | 115 | ]; |
| 190 | | 116 | |
| 191 | expect( | 117 | expect( |
| 192 | integrationPresetIconUrl( | 118 | integrationPresetIconUrl({ id: "sentry", kind: "mcp", name: "Sentry MCP" }, presets), |
| 193 | { id: "mcp_posthog_com", kind: "mcp", name: "mcp.posthog.com" }, | 119 | ).toBeNull(); |
| 194 | presets, | 120 | |
| 195 | ), | 121 | // Migrated host-shaped integrations resolve through the inferred URL: |
| 196 | ).toBe("https://example.com/posthog.png"); | 122 | const inferred = integrationInferredUrl({ id: "mcp_posthog_com", name: "mcp.posthog.com" }); |
| 197 | expect( | 123 | expect(inferred).toBe("https://mcp.posthog.com"); |
| 198 | integrationPresetIconUrl( | 124 | expect(integrationFaviconSrc({ url: inferred ?? undefined, size: 16 })).toBe( |
| 199 | { id: "mcp_linear_app", kind: "mcp", name: "mcp.linear.app" }, | 125 | "https://integrations.sh/logo/posthog.com?sz=32", |
| 200 | presets, | 126 | ); |
| 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"); | | |
| 227 | }); | 127 | }); |
| 228 | | 128 | |
| 229 | it("prefers exact catalog defaultSlug icons for OpenAPI provider services", () => { | 129 | it("prefers exact catalog defaultSlug icons for OpenAPI provider services", () => { |
| @@ -248,7 +148,25 @@ describe("IntegrationFavicon", () => { · 18 unmodified lines |
| 248 | ).toBe("https://example.com/gmail.png"); | 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 | expect( | 170 | expect( |
| 253 | integrationPresetIconUrl( | 171 | integrationPresetIconUrl( |
| 254 | { | 172 | { |
| @@ -257,30 +175,43 @@ describe("IntegrationFavicon", () => { · 2 unmodified lines |
| 257 | name: "Spotify Web API", | 175 | name: "Spotify Web API", |
| 258 | url: "https://api.spotify.com/v1", | 176 | url: "https://api.spotify.com/v1", |
| 259 | }, | 177 | }, |
| 260 | [ | 178 | presets, |
| 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 | ], | | |
| 282 | ), | 179 | ), |
| 283 | ).toBe("https://example.com/spotify.png"); | 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 | it("infers favicon URLs from migrated host-shaped MCP names and slugs", () => { | 217 | it("infers favicon URLs from migrated host-shaped MCP names and slugs", () => { |