@vellumai/assistant 0.12.2-dev.202609190043.ee5ba34 → 0.12.2-dev.202609191222.757d196

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.yaml CHANGED
@@ -21217,6 +21217,7 @@ paths:
21217
21217
  - firecrawl
21218
21218
  - fastcrw
21219
21219
  - searxng
21220
+ - tinyfish
21220
21221
  resultCount:
21221
21222
  type: number
21222
21223
  durationMs:
@@ -21270,6 +21271,7 @@ paths:
21270
21271
  - default
21271
21272
  - firecrawl
21272
21273
  - fastcrw
21274
+ - tinyfish
21273
21275
  status:
21274
21276
  type: number
21275
21277
  contentType:
@@ -21735,6 +21737,7 @@ paths:
21735
21737
  - firecrawl
21736
21738
  - fastcrw
21737
21739
  - searxng
21740
+ - tinyfish
21738
21741
  resultCount:
21739
21742
  type: number
21740
21743
  durationMs:
@@ -21788,6 +21791,7 @@ paths:
21788
21791
  - default
21789
21792
  - firecrawl
21790
21793
  - fastcrw
21794
+ - tinyfish
21791
21795
  status:
21792
21796
  type: number
21793
21797
  contentType:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.2-dev.202609190043.ee5ba34",
3
+ "version": "0.12.2-dev.202609191222.757d196",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -53,6 +53,7 @@ export const WebSearchProviderIdSchema = z.enum([
53
53
  "firecrawl",
54
54
  "fastcrw",
55
55
  "searxng",
56
+ "tinyfish",
56
57
  ]);
57
58
 
58
59
  export type WebSearchProviderId = z.infer<typeof WebSearchProviderIdSchema>;
@@ -61,6 +62,7 @@ export const WebFetchProviderIdSchema = z.enum([
61
62
  "default",
62
63
  "firecrawl",
63
64
  "fastcrw",
65
+ "tinyfish",
64
66
  ]);
65
67
 
66
68
  export type WebFetchProviderId = z.infer<typeof WebFetchProviderIdSchema>;
@@ -115,7 +115,7 @@ export async function signBundle(
115
115
  const { signature, keyId } = await requestSignature(canonicalPayload);
116
116
 
117
117
  // 5. Build SignatureJson
118
- const signatureJson: SignatureJson = {
118
+ return {
119
119
  algorithm: "ed25519",
120
120
  signer: {
121
121
  key_id: keyId,
@@ -124,6 +124,4 @@ export async function signBundle(
124
124
  content_hashes: contentHashes,
125
125
  signature,
126
126
  };
127
-
128
- return signatureJson;
129
127
  }
@@ -68,9 +68,7 @@ function linearToMulaw(sample: number): number {
68
68
  // Determine segment
69
69
  const exponent = segmentSearch(sample);
70
70
  const mantissa = (sample >> (exponent + 3)) & 0x0f;
71
- const mulawByte = ~(sign | (exponent << 4) | mantissa) & 0xff;
72
-
73
- return mulawByte;
71
+ return ~(sign | (exponent << 4) | mantissa) & 0xff;
74
72
  }
75
73
 
76
74
  function segmentSearch(val: number): number {
@@ -337,8 +337,7 @@ export async function postPublishRequest(
337
337
  },
338
338
  );
339
339
 
340
- const data = (await resp.json()) as PublishResult;
341
- return data;
340
+ return (await resp.json()) as PublishResult;
342
341
  }
343
342
 
344
343
  /**
@@ -83,8 +83,9 @@ const WebSearchServiceSchema = z.object({
83
83
  .enum(VALID_WEB_SEARCH_PROVIDERS)
84
84
  .default("inference-provider-native"),
85
85
  // Origin for providers that support a custom API base (e.g. fastCRW,
86
- // SearXNG). Empty / omitted uses the provider's cloud default when one
87
- // exists. SearXNG has no cloud default and requires a user-supplied URL.
86
+ // SearXNG, TinyFish). Empty / omitted uses the provider's cloud default
87
+ // when one exists. SearXNG has no cloud default and requires a user-supplied
88
+ // URL.
88
89
  apiBase: z.string().optional(),
89
90
  });
90
91
 
@@ -98,7 +99,8 @@ const WebFetchServiceSchema = z.object({
98
99
  // `firecrawl`) scrape via their hosted API and reuse the same stored key as
99
100
  // their web-search counterpart.
100
101
  provider: z.enum(VALID_WEB_FETCH_PROVIDERS).default("default"),
101
- // Origin for providers that support a custom API base (e.g. fastCRW).
102
+ // Origin for providers that support a custom API base (e.g. fastCRW,
103
+ // TinyFish).
102
104
  // Empty / omitted uses the provider's cloud default.
103
105
  apiBase: z.string().optional(),
104
106
  });
@@ -13,10 +13,15 @@ export type WebSearchProviderId =
13
13
  | "keenable"
14
14
  | "firecrawl"
15
15
  | "fastcrw"
16
- | "searxng";
16
+ | "searxng"
17
+ | "tinyfish";
17
18
 
18
19
  /** Provider that backed a `web_fetch` call. `default` is the built-in fetcher. */
19
- export type WebFetchProviderId = "default" | "firecrawl" | "fastcrw";
20
+ export type WebFetchProviderId =
21
+ | "default"
22
+ | "firecrawl"
23
+ | "fastcrw"
24
+ | "tinyfish";
20
25
 
21
26
  export interface WebSearchResultItem {
22
27
  rank: number; // 1-indexed
@@ -69,28 +69,22 @@ async function request<T>(
69
69
  const canRetry = isIdempotent(method);
70
70
 
71
71
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
72
- let resp: OAuthConnectionResponse;
73
- try {
74
- const extraHeaders =
75
- options?.headers &&
76
- typeof options.headers === "object" &&
77
- !Array.isArray(options.headers)
78
- ? (options.headers as Record<string, string>)
79
- : {};
80
- resp = await connection.request({
81
- method,
82
- path,
83
- query,
84
- headers: {
85
- "Content-Type": "application/json",
86
- ...extraHeaders,
87
- },
88
- body: options?.body ? JSON.parse(options.body as string) : undefined,
89
- });
90
- } catch (err) {
91
- // Network-level errors from connection.request() are not retryable
92
- throw err;
93
- }
72
+ const extraHeaders =
73
+ options?.headers &&
74
+ typeof options.headers === "object" &&
75
+ !Array.isArray(options.headers)
76
+ ? (options.headers as Record<string, string>)
77
+ : {};
78
+ const resp: OAuthConnectionResponse = await connection.request({
79
+ method,
80
+ path,
81
+ query,
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ ...extraHeaders,
85
+ },
86
+ body: options?.body ? JSON.parse(options.body as string) : undefined,
87
+ });
94
88
 
95
89
  if (resp.status < 200 || resp.status >= 300) {
96
90
  if (canRetry && isRetryable(resp.status) && attempt < MAX_RETRIES) {
@@ -467,12 +467,11 @@ async function fetchPlatformConnections(options: {
467
467
  }
468
468
 
469
469
  const body = (await response.json()) as unknown;
470
- const connections = (
470
+ return (
471
471
  Array.isArray(body)
472
472
  ? body
473
473
  : ((body as Record<string, unknown>).results ?? [])
474
474
  ) as PlatformConnectionEntry[];
475
- return connections;
476
475
  }
477
476
 
478
477
  /**
@@ -1440,9 +1440,9 @@ function buildExcerptFromText(
1440
1440
  text.length,
1441
1441
  match.index + match.length + EXCERPT_WINDOW,
1442
1442
  );
1443
- const excerpt =
1443
+ return (
1444
1444
  (start > 0 ? "\u2026" : "") +
1445
1445
  text.slice(start, end).replace(/\s+/g, " ").trim() +
1446
- (end < text.length ? "\u2026" : "");
1447
- return excerpt;
1446
+ (end < text.length ? "\u2026" : "")
1447
+ );
1448
1448
  }
@@ -23,7 +23,7 @@ import type { RouteDefinition } from "../../../../runtime/routes/types.js";
23
23
  import { getLogger } from "../logging.js";
24
24
  import { getWorkspaceDir } from "../paths.js";
25
25
  import { runMemoryEval } from "../v3-eval/eval-packets.js";
26
- import { type TallyResult, tallyVerdicts } from "../v3-eval/eval-tally.js";
26
+ import { tallyVerdicts } from "../v3-eval/eval-tally.js";
27
27
 
28
28
  const log = getLogger("memory-eval-routes");
29
29
 
@@ -138,12 +138,11 @@ export type MemoryEvalTallyResult = z.infer<typeof MemoryEvalTallyResultSchema>;
138
138
  */
139
139
  export function handleMemoryEvalTally(body: unknown): MemoryEvalTallyResult {
140
140
  const params = MemoryEvalTallyParamsSchema.parse(body ?? {});
141
- const result: TallyResult = tallyVerdicts(
141
+ return tallyVerdicts(
142
142
  params.verdicts,
143
143
  params.key,
144
144
  params.alpha !== undefined ? { alpha: params.alpha } : {},
145
145
  );
146
- return result;
147
146
  }
148
147
 
149
148
  const WRITE_POLICY: RoutePolicy = {
@@ -95,4 +95,13 @@ describe("search-provider-catalog", () => {
95
95
  expect(searxng?.defaultApiBase).toBeUndefined();
96
96
  expect(searxng?.envVar).toBe("SEARXNG_API_KEY");
97
97
  });
98
+
99
+ test("TinyFish is a keyed BYOK provider with a cloud API base", () => {
100
+ const tinyfish = getSearchProvider("tinyfish");
101
+ expect(tinyfish?.kind).toBe("byok");
102
+ expect(tinyfish?.keyless).toBeUndefined();
103
+ expect(tinyfish?.supportsApiBase).toBe(true);
104
+ expect(tinyfish?.defaultApiBase).toBe("https://api.search.tinyfish.ai");
105
+ expect(tinyfish?.envVar).toBe("TINYFISH_API_KEY");
106
+ });
98
107
  });
@@ -84,6 +84,17 @@ export const FETCH_PROVIDER_CATALOG: readonly FetchProviderCatalogEntry[] = [
84
84
  supportsApiBase: true,
85
85
  defaultApiBase: "https://api.fastcrw.com",
86
86
  },
87
+ {
88
+ id: "tinyfish",
89
+ displayName: "TinyFish",
90
+ kind: "byok",
91
+ apiKeyPrefix: "TinyFish API key...",
92
+ envVar: "TINYFISH_API_KEY",
93
+ secretKey: "tinyfish",
94
+ privacyPolicyUrl: "https://www.tinyfish.ai/privacy-policy",
95
+ supportsApiBase: true,
96
+ defaultApiBase: "https://api.fetch.tinyfish.ai",
97
+ },
87
98
  ];
88
99
 
89
100
  /** Provider ids accepted by the web-fetch config schema. */
@@ -62,9 +62,7 @@ export class RateLimitProvider implements Provider {
62
62
  // calls from bypassing the rate limit during the async gap.
63
63
  this.recordRequest();
64
64
 
65
- const response = await this.inner.sendMessage(messages, options);
66
-
67
- return response;
65
+ return this.inner.sendMessage(messages, options);
68
66
  }
69
67
 
70
68
  private enforceRequestRate(): void {
@@ -175,6 +175,18 @@ export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [
175
175
  privacyPolicyUrl: "https://docs.searxng.org/own-instance.html",
176
176
  supportsApiBase: true,
177
177
  },
178
+ {
179
+ id: "tinyfish",
180
+ displayName: "TinyFish",
181
+ kind: "byok",
182
+ apiKeyPrefix: "TinyFish API key...",
183
+ envVar: "TINYFISH_API_KEY",
184
+ secretKey: "tinyfish",
185
+ fallbackOrder: 8,
186
+ privacyPolicyUrl: "https://www.tinyfish.ai/privacy-policy",
187
+ supportsApiBase: true,
188
+ defaultApiBase: "https://api.search.tinyfish.ai",
189
+ },
178
190
  ];
179
191
 
180
192
  /** Provider ids accepted by the web-search config schema. */
@@ -607,8 +607,7 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
607
607
  }
608
608
 
609
609
  async function handleCredentialsStatus() {
610
- const info = await getActiveBackendInfoAsync();
611
- return info;
610
+ return getActiveBackendInfoAsync();
612
611
  }
613
612
 
614
613
  // ---------------------------------------------------------------------------
@@ -169,7 +169,7 @@ async function handleEmailStatus(_args: RouteHandlerArgs) {
169
169
  );
170
170
  }
171
171
 
172
- const statusData = (await statusResponse.json()) as {
172
+ return (await statusResponse.json()) as {
173
173
  address: string;
174
174
  status: string;
175
175
  created_at: string;
@@ -181,8 +181,6 @@ async function handleEmailStatus(_args: RouteHandlerArgs) {
181
181
  received_this_month: number;
182
182
  };
183
183
  };
184
-
185
- return statusData;
186
184
  }
187
185
 
188
186
  async function handleEmailList({ queryParams = {} }: RouteHandlerArgs) {
@@ -239,8 +237,7 @@ async function handleEmailDownload({ queryParams = {} }: RouteHandlerArgs) {
239
237
  throw new RouteError(String(detail), "DOWNLOAD_FAILED", response.status);
240
238
  }
241
239
 
242
- const msg = await response.json();
243
- return msg;
240
+ return response.json();
244
241
  }
245
242
 
246
243
  async function handleEmailSend({ body = {} }: RouteHandlerArgs) {
@@ -323,11 +320,10 @@ async function handleEmailSend({ body = {} }: RouteHandlerArgs) {
323
320
  throw new BadRequestError(String(detail));
324
321
  }
325
322
 
326
- const data = (await response.json()) as {
323
+ return (await response.json()) as {
327
324
  delivery_id: string;
328
325
  status: string;
329
326
  };
330
- return data;
331
327
  }
332
328
 
333
329
  // ── Attachment handlers ───────────────────────────────────────────────
@@ -86,7 +86,7 @@ function handleWatcherCreate({ body = {} }: RouteHandlerArgs) {
86
86
  const credentialService =
87
87
  credentialServiceOverride ?? provider.requiredCredentialService;
88
88
 
89
- const watcher = createWatcher({
89
+ return createWatcher({
90
90
  name,
91
91
  providerId,
92
92
  actionPrompt,
@@ -94,8 +94,6 @@ function handleWatcherCreate({ body = {} }: RouteHandlerArgs) {
94
94
  pollIntervalMs,
95
95
  configJson: config ? JSON.stringify(config) : null,
96
96
  });
97
-
98
- return watcher;
99
97
  }
100
98
 
101
99
  function handleWatcherList({ body = {} }: RouteHandlerArgs) {
@@ -406,14 +406,7 @@ export class CdpInspectClient implements ScopedCdpClient {
406
406
  httpError: unknown,
407
407
  signal: AbortSignal,
408
408
  ): Promise<AttachedSession> {
409
- let wsUrl: string;
410
- try {
411
- wsUrl = this.helpers.buildBrowserWsUrl(host, port);
412
- } catch (err) {
413
- // buildBrowserWsUrl enforces loopback — if it throws, the host
414
- // is non-loopback and we should not attempt any WS connection.
415
- throw err;
416
- }
409
+ const wsUrl = this.helpers.buildBrowserWsUrl(host, port);
417
410
 
418
411
  let transport: CdpWsTransport;
419
412
  try {
@@ -0,0 +1,250 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { setConfig } from "../../../__tests__/helpers/set-config.js";
4
+
5
+ let mockTinyfishSecureKey: string | undefined;
6
+ let mockResolveAddresses: string[] = [];
7
+
8
+ function seedWebFetch(provider: string, apiBase?: string): void {
9
+ const entry: Record<string, string> = { provider };
10
+ if (apiBase !== undefined) {
11
+ entry.apiBase = apiBase;
12
+ }
13
+ setConfig("services", { "web-fetch": entry });
14
+ }
15
+
16
+ mock.module("../../../security/secure-keys.js", () => ({
17
+ getProviderKeyAsync: async (provider: string) =>
18
+ provider === "tinyfish" ? mockTinyfishSecureKey : undefined,
19
+ }));
20
+
21
+ const realLogger = await import("../../../util/logger.js");
22
+ mock.module("../../../util/logger.js", () => ({
23
+ ...realLogger,
24
+ getLogger: () =>
25
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
26
+ }));
27
+
28
+ mock.module("../../../permissions/types.js", () => ({
29
+ RiskLevel: { Low: "low", Medium: "medium", High: "high" },
30
+ }));
31
+
32
+ const realUrlSafety = await import("../url-safety.js");
33
+ mock.module("../url-safety.js", () => ({
34
+ ...realUrlSafety,
35
+ resolveHostAddresses: async () => mockResolveAddresses,
36
+ }));
37
+
38
+ const { executeTinyfishFetch, webFetchTool } = await import("../web-fetch.js");
39
+
40
+ function fetchResponse(body: unknown, status = 200): Response {
41
+ return new Response(JSON.stringify(body), {
42
+ status,
43
+ headers: { "content-type": "application/json" },
44
+ });
45
+ }
46
+
47
+ describe("executeTinyfishFetch", () => {
48
+ let originalFetch: typeof globalThis.fetch;
49
+
50
+ beforeEach(() => {
51
+ originalFetch = globalThis.fetch;
52
+ seedWebFetch("tinyfish");
53
+ });
54
+
55
+ afterEach(() => {
56
+ globalThis.fetch = originalFetch;
57
+ });
58
+
59
+ test("posts the URL and returns markdown with metadata", async () => {
60
+ let capturedUrl = "";
61
+ let capturedBody: Record<string, unknown> = {};
62
+ let capturedApiKey = "";
63
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
64
+ capturedUrl = url;
65
+ capturedBody = JSON.parse(String(init?.body));
66
+ capturedApiKey = new Headers(init?.headers).get("x-api-key") ?? "";
67
+ return fetchResponse({
68
+ results: [
69
+ {
70
+ url: "https://example.com/",
71
+ final_url: "https://www.example.com/",
72
+ title: "Example",
73
+ description: "An example page",
74
+ text: "# Example\n\nHello from TinyFish.",
75
+ format: "markdown",
76
+ latency_ms: 120,
77
+ },
78
+ ],
79
+ errors: [],
80
+ });
81
+ }) as typeof fetch;
82
+
83
+ const result = await executeTinyfishFetch(
84
+ { url: "https://example.com", timeout_seconds: 45 },
85
+ { apiKey: "tf_test" },
86
+ );
87
+
88
+ expect(result.isError).toBe(false);
89
+ expect(capturedUrl).toBe("https://api.fetch.tinyfish.ai/");
90
+ expect(capturedBody).toEqual({
91
+ urls: ["https://example.com/"],
92
+ format: "markdown",
93
+ per_url_timeout_ms: 45_000,
94
+ });
95
+ expect(capturedApiKey).toBe("tf_test");
96
+ expect(result.content).toContain("Hello from TinyFish.");
97
+ expect(result.content).toContain("Title: Example");
98
+ expect(result.activityMetadata?.webFetch?.provider).toBe("tinyfish");
99
+ expect(result.activityMetadata?.webFetch?.finalUrl).toBe(
100
+ "https://www.example.com/",
101
+ );
102
+ });
103
+
104
+ test("uses a custom API base and applies character windows", async () => {
105
+ seedWebFetch("tinyfish", "https://fetch.example.com/api/");
106
+ let capturedUrl = "";
107
+ globalThis.fetch = (async (url: string) => {
108
+ capturedUrl = url;
109
+ return fetchResponse({
110
+ results: [
111
+ {
112
+ url: "https://example.com/",
113
+ final_url: "https://example.com/",
114
+ text: "0123456789",
115
+ format: "markdown",
116
+ },
117
+ ],
118
+ errors: [],
119
+ });
120
+ }) as typeof fetch;
121
+
122
+ const result = await executeTinyfishFetch(
123
+ { url: "https://example.com", start_index: 2, max_chars: 4 },
124
+ { apiKey: "tf_test" },
125
+ );
126
+
127
+ expect(capturedUrl).toBe("https://fetch.example.com/api/");
128
+ expect(result.content).toContain("Character Window: 2-6 of 10");
129
+ expect(result.content).toContain("2345");
130
+ expect(result.activityMetadata?.webFetch?.truncated).toBe(true);
131
+ });
132
+
133
+ test("surfaces per-URL errors from a successful HTTP response", async () => {
134
+ globalThis.fetch = (async () =>
135
+ fetchResponse({
136
+ results: [],
137
+ errors: [
138
+ {
139
+ url: "https://example.com/",
140
+ error: "bot_blocked",
141
+ status: 403,
142
+ },
143
+ ],
144
+ })) as unknown as typeof fetch;
145
+
146
+ const result = await executeTinyfishFetch(
147
+ { url: "https://example.com" },
148
+ { apiKey: "tf_test" },
149
+ );
150
+
151
+ expect(result.isError).toBe(true);
152
+ expect(result.content).toContain("bot_blocked");
153
+ expect(result.content).toContain("HTTP 403");
154
+ expect(result.activityMetadata?.webFetch?.provider).toBe("tinyfish");
155
+ expect(result.activityMetadata?.webFetch?.status).toBe(403);
156
+ });
157
+
158
+ test("rejects URL-embedded credentials before calling TinyFish", async () => {
159
+ let fetchCalls = 0;
160
+ globalThis.fetch = (async () => {
161
+ fetchCalls += 1;
162
+ return fetchResponse({ results: [], errors: [] });
163
+ }) as unknown as typeof fetch;
164
+
165
+ const result = await executeTinyfishFetch(
166
+ { url: "https://user:pass@example.com/private" },
167
+ { apiKey: "tf_test" },
168
+ );
169
+
170
+ expect(result.isError).toBe(true);
171
+ expect(result.content).toContain("embedded credentials");
172
+ expect(fetchCalls).toBe(0);
173
+ });
174
+ });
175
+
176
+ describe("webFetchTool TinyFish dispatch", () => {
177
+ let originalFetch: typeof globalThis.fetch;
178
+
179
+ const execute = (input: Record<string, unknown>, context: any = {}) =>
180
+ webFetchTool.execute(input, context);
181
+
182
+ beforeEach(() => {
183
+ originalFetch = globalThis.fetch;
184
+ seedWebFetch("tinyfish");
185
+ mockTinyfishSecureKey = "tf_test";
186
+ mockResolveAddresses = ["93.184.216.34"];
187
+ });
188
+
189
+ afterEach(() => {
190
+ globalThis.fetch = originalFetch;
191
+ });
192
+
193
+ test("routes public URLs to TinyFish when a key is stored", async () => {
194
+ let tinyfishHit = false;
195
+ globalThis.fetch = (async (url: string) => {
196
+ if (String(url).includes("api.fetch.tinyfish.ai")) {
197
+ tinyfishHit = true;
198
+ }
199
+ return fetchResponse({
200
+ results: [
201
+ {
202
+ url: "https://example.com/",
203
+ final_url: "https://example.com/",
204
+ text: "routed",
205
+ format: "markdown",
206
+ },
207
+ ],
208
+ errors: [],
209
+ });
210
+ }) as typeof fetch;
211
+
212
+ const result = await execute({ url: "https://example.com" });
213
+
214
+ expect(tinyfishHit).toBe(true);
215
+ expect(result.activityMetadata?.webFetch?.provider).toBe("tinyfish");
216
+ });
217
+
218
+ test("does not send private targets to TinyFish", async () => {
219
+ let tinyfishHit = false;
220
+ globalThis.fetch = (async (url: string) => {
221
+ if (String(url).includes("api.fetch.tinyfish.ai")) {
222
+ tinyfishHit = true;
223
+ }
224
+ return new Response("", { status: 200 });
225
+ }) as typeof fetch;
226
+
227
+ const result = await execute({ url: "http://localhost:8080/admin" });
228
+
229
+ expect(tinyfishHit).toBe(false);
230
+ expect(result.isError).toBe(true);
231
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
232
+ });
233
+
234
+ test("falls back to the built-in fetcher when no TinyFish key is stored", async () => {
235
+ mockTinyfishSecureKey = undefined;
236
+ mockResolveAddresses = [];
237
+ let tinyfishHit = false;
238
+ globalThis.fetch = (async (url: string) => {
239
+ if (String(url).includes("api.fetch.tinyfish.ai")) {
240
+ tinyfishHit = true;
241
+ }
242
+ return new Response("", { status: 200 });
243
+ }) as typeof fetch;
244
+
245
+ const result = await execute({ url: "https://example.com" });
246
+
247
+ expect(tinyfishHit).toBe(false);
248
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
249
+ });
250
+ });