@aria-cli/tools 1.0.9 → 1.0.11
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/package.json +9 -5
- package/src/__tests__/web-fetch-download.test.ts +0 -433
- package/src/__tests__/web-tools.test.ts +0 -619
- package/src/ask-user-interaction.ts +0 -33
- package/src/cache/web-cache.ts +0 -110
- package/src/definitions/arion.ts +0 -118
- package/src/definitions/browser/browser.ts +0 -502
- package/src/definitions/browser/index.ts +0 -5
- package/src/definitions/browser/pw-downloads.ts +0 -142
- package/src/definitions/browser/pw-interactions.ts +0 -282
- package/src/definitions/browser/pw-responses.ts +0 -98
- package/src/definitions/browser/pw-session.ts +0 -405
- package/src/definitions/browser/pw-shared.ts +0 -85
- package/src/definitions/browser/pw-snapshot.ts +0 -383
- package/src/definitions/browser/pw-state.ts +0 -101
- package/src/definitions/browser/types.ts +0 -203
- package/src/definitions/code-intelligence.ts +0 -526
- package/src/definitions/core.ts +0 -118
- package/src/definitions/delegation.ts +0 -567
- package/src/definitions/deploy.ts +0 -73
- package/src/definitions/filesystem.ts +0 -217
- package/src/definitions/frg.ts +0 -67
- package/src/definitions/index.ts +0 -28
- package/src/definitions/memory.ts +0 -150
- package/src/definitions/messaging.ts +0 -734
- package/src/definitions/meta.ts +0 -392
- package/src/definitions/network.ts +0 -179
- package/src/definitions/outlook.ts +0 -318
- package/src/definitions/patch/apply-patch.ts +0 -235
- package/src/definitions/patch/fuzzy-match.ts +0 -217
- package/src/definitions/patch/index.ts +0 -1
- package/src/definitions/patch/patch-parser.ts +0 -297
- package/src/definitions/patch/sandbox-paths.ts +0 -129
- package/src/definitions/process/index.ts +0 -5
- package/src/definitions/process/process-registry.ts +0 -303
- package/src/definitions/process/process.ts +0 -456
- package/src/definitions/process/pty-keys.ts +0 -298
- package/src/definitions/process/session-slug.ts +0 -147
- package/src/definitions/quip.ts +0 -225
- package/src/definitions/search.ts +0 -67
- package/src/definitions/session-history.ts +0 -79
- package/src/definitions/shell.ts +0 -202
- package/src/definitions/slack.ts +0 -211
- package/src/definitions/web.ts +0 -119
- package/src/executors/apply-patch.ts +0 -1035
- package/src/executors/arion.ts +0 -199
- package/src/executors/code-intelligence.ts +0 -1179
- package/src/executors/deploy.ts +0 -1066
- package/src/executors/filesystem.ts +0 -1428
- package/src/executors/frg-freshness.ts +0 -743
- package/src/executors/frg.ts +0 -394
- package/src/executors/index.ts +0 -280
- package/src/executors/learning-meta.ts +0 -1367
- package/src/executors/lsp-client.ts +0 -355
- package/src/executors/memory.ts +0 -978
- package/src/executors/meta.ts +0 -293
- package/src/executors/process-registry.ts +0 -570
- package/src/executors/pty-session-store.ts +0 -43
- package/src/executors/pty.ts +0 -342
- package/src/executors/restart.ts +0 -133
- package/src/executors/search-freshness.ts +0 -249
- package/src/executors/search-types.ts +0 -98
- package/src/executors/search.ts +0 -89
- package/src/executors/self-diagnose.ts +0 -552
- package/src/executors/session-history.ts +0 -435
- package/src/executors/shell-safety.ts +0 -519
- package/src/executors/shell.ts +0 -1243
- package/src/executors/utils.ts +0 -40
- package/src/executors/web.ts +0 -786
- package/src/extraction/content-extraction.ts +0 -281
- package/src/extraction/index.ts +0 -5
- package/src/headless-control-contract.ts +0 -1149
- package/src/index.ts +0 -788
- package/src/local-control-http-auth.ts +0 -2
- package/src/mcp/client.ts +0 -218
- package/src/mcp/connection.ts +0 -568
- package/src/mcp/index.ts +0 -11
- package/src/mcp/jsonrpc.ts +0 -195
- package/src/mcp/types.ts +0 -199
- package/src/network-control-adapter.ts +0 -88
- package/src/network-runtime/address-types.ts +0 -218
- package/src/network-runtime/db-owner-fencing.ts +0 -91
- package/src/network-runtime/delivery-receipts.ts +0 -372
- package/src/network-runtime/direct-endpoint-authority.ts +0 -35
- package/src/network-runtime/index.ts +0 -316
- package/src/network-runtime/local-control-contract.ts +0 -784
- package/src/network-runtime/node-store-contract.ts +0 -46
- package/src/network-runtime/pair-route-contract.ts +0 -97
- package/src/network-runtime/peer-capabilities.ts +0 -48
- package/src/network-runtime/peer-principal-ref.ts +0 -20
- package/src/network-runtime/peer-state-machine.ts +0 -160
- package/src/network-runtime/protocol-schemas.ts +0 -265
- package/src/network-runtime/runtime-bootstrap-contract.ts +0 -83
- package/src/outlook/desktop-session.ts +0 -409
- package/src/policy.ts +0 -171
- package/src/providers/brave.ts +0 -80
- package/src/providers/duckduckgo.ts +0 -199
- package/src/providers/exa.ts +0 -85
- package/src/providers/firecrawl.ts +0 -77
- package/src/providers/index.ts +0 -8
- package/src/providers/jina.ts +0 -70
- package/src/providers/router.ts +0 -121
- package/src/providers/search-provider.ts +0 -74
- package/src/providers/tavily.ts +0 -74
- package/src/quip/desktop-session.ts +0 -435
- package/src/registry/index.ts +0 -1
- package/src/registry/registry.ts +0 -905
- package/src/runtime-socket-local-control-client.ts +0 -632
- package/src/security/dns-normalization.ts +0 -34
- package/src/security/dns-pinning.ts +0 -138
- package/src/security/external-content.ts +0 -129
- package/src/security/ssrf.ts +0 -207
- package/src/slack/desktop-session.ts +0 -493
- package/src/tool-factory.ts +0 -91
- package/src/types.ts +0 -1341
- package/src/utils/retry.ts +0 -163
- package/src/utils/safe-parse-json.ts +0 -176
- package/src/utils/url.ts +0 -20
- package/tests/benchmarks/registry.bench.ts +0 -57
- package/tests/cache/web-cache.test.ts +0 -147
- package/tests/critical-integration.test.ts +0 -1465
- package/tests/definitions/apply-patch.test.ts +0 -586
- package/tests/definitions/browser.test.ts +0 -495
- package/tests/definitions/delegation-pause-resume.test.ts +0 -758
- package/tests/definitions/execution.test.ts +0 -671
- package/tests/definitions/messaging-inbox-scope.test.ts +0 -229
- package/tests/definitions/messaging.test.ts +0 -1468
- package/tests/definitions/outlook.test.ts +0 -30
- package/tests/definitions/process.test.ts +0 -469
- package/tests/definitions/slack.test.ts +0 -28
- package/tests/definitions/tool-inventory.test.ts +0 -218
- package/tests/e2e/delegation-quest-orchestration.e2e.test.ts +0 -433
- package/tests/e2e/memory-tool-discovery-contract.e2e.test.ts +0 -81
- package/tests/executors/apply-patch.test.ts +0 -538
- package/tests/executors/arion.test.ts +0 -309
- package/tests/executors/conversation-primitives.test.ts +0 -250
- package/tests/executors/deploy.test.ts +0 -746
- package/tests/executors/filesystem-tools.test.ts +0 -357
- package/tests/executors/filesystem.test.ts +0 -959
- package/tests/executors/frg-freshness.test.ts +0 -136
- package/tests/executors/frg-merge.test.ts +0 -70
- package/tests/executors/frg-session-content.test.ts +0 -40
- package/tests/executors/frg.test.ts +0 -56
- package/tests/executors/memory-bugfixes.test.ts +0 -257
- package/tests/executors/memory-real-memoria.integration.test.ts +0 -316
- package/tests/executors/memory.test.ts +0 -853
- package/tests/executors/meta-tools.test.ts +0 -411
- package/tests/executors/meta.test.ts +0 -683
- package/tests/executors/path-containment.test.ts +0 -51
- package/tests/executors/process-registry.test.ts +0 -505
- package/tests/executors/pty.test.ts +0 -664
- package/tests/executors/quest-security.test.ts +0 -249
- package/tests/executors/read-file-media.test.ts +0 -230
- package/tests/executors/recall-knowledge-schema.test.ts +0 -209
- package/tests/executors/recall-tags.test.ts +0 -278
- package/tests/executors/remember-null-safety.contract.test.ts +0 -41
- package/tests/executors/restart.test.ts +0 -67
- package/tests/executors/search-unified.test.ts +0 -381
- package/tests/executors/session-history.test.ts +0 -340
- package/tests/executors/session-transcript.test.ts +0 -561
- package/tests/executors/shell-abort.test.ts +0 -416
- package/tests/executors/shell-env-blocklist.test.ts +0 -648
- package/tests/executors/shell-env-process.test.ts +0 -245
- package/tests/executors/shell-process-registry.test.ts +0 -334
- package/tests/executors/shell-tools.test.ts +0 -393
- package/tests/executors/shell.test.ts +0 -690
- package/tests/executors/web-abort-vs-timeout.test.ts +0 -213
- package/tests/executors/web-integration.test.ts +0 -633
- package/tests/executors/web-symlink.test.ts +0 -18
- package/tests/executors/web.test.ts +0 -1400
- package/tests/executors/write-stdin.test.ts +0 -145
- package/tests/extraction/content-extraction.test.ts +0 -153
- package/tests/guards/tools-default-test-lane.integration.test.ts +0 -21
- package/tests/guards/tools-package-test-commands.e2e.test.ts +0 -43
- package/tests/guards/tools-test-lane-manifest.contract.test.ts +0 -76
- package/tests/guards/tools-vitest-workspace-alias.contract.test.ts +0 -63
- package/tests/helpers/async-waits.ts +0 -53
- package/tests/integration/headless-control-contract.integration.test.ts +0 -153
- package/tests/integration/memory-tool-schema-parity.integration.test.ts +0 -67
- package/tests/integration/meta-tools-round-trip.integration.test.ts +0 -506
- package/tests/integration/quest-round-trip.test.ts +0 -303
- package/tests/integration/registry-executor-flow.test.ts +0 -85
- package/tests/integration.test.ts +0 -177
- package/tests/loading-tier.test.ts +0 -126
- package/tests/mcp/client-reconnect.test.ts +0 -267
- package/tests/mcp/connection.test.ts +0 -846
- package/tests/mcp/injectable-logger.test.ts +0 -83
- package/tests/mcp/jsonrpc.test.ts +0 -109
- package/tests/mcp/lifecycle.test.ts +0 -879
- package/tests/network-runtime/address-types.contract.test.ts +0 -143
- package/tests/network-runtime/continuity-bind-schema.contract.test.ts +0 -203
- package/tests/network-runtime/local-control-contract.test.ts +0 -869
- package/tests/network-runtime/local-control-invite-token.contract.test.ts +0 -146
- package/tests/network-runtime/node-store-contract.test.ts +0 -11
- package/tests/network-runtime/pair-protocol-nodeid.contract.test.ts +0 -15
- package/tests/network-runtime/peer-state-machine.contract.test.ts +0 -148
- package/tests/network-runtime/protocol-schemas.contract.test.ts +0 -512
- package/tests/network-runtime/relay-pending-nodeid.contract.test.ts +0 -62
- package/tests/network-runtime/runtime-bootstrap-contract.test.ts +0 -227
- package/tests/network-runtime/runtime-socket-local-control-client.test.ts +0 -621
- package/tests/network-runtime/wait-for-message-script.test.ts +0 -288
- package/tests/parallel.test.ts +0 -71
- package/tests/policy.test.ts +0 -184
- package/tests/print-default-test-lane.ts +0 -14
- package/tests/print-test-lane-manifest.ts +0 -22
- package/tests/providers/brave.test.ts +0 -159
- package/tests/providers/duckduckgo.test.ts +0 -207
- package/tests/providers/exa.test.ts +0 -175
- package/tests/providers/firecrawl.test.ts +0 -168
- package/tests/providers/jina.test.ts +0 -144
- package/tests/providers/router.test.ts +0 -328
- package/tests/providers/tavily.test.ts +0 -165
- package/tests/registry/discovery.test.ts +0 -154
- package/tests/registry/injectable-logger.test.ts +0 -230
- package/tests/registry/input-validation.test.ts +0 -361
- package/tests/registry/interface-completeness.test.ts +0 -85
- package/tests/registry/mcp-integration.test.ts +0 -103
- package/tests/registry/mcp-read-only-hint.test.ts +0 -60
- package/tests/registry/memoria-discovery.test.ts +0 -390
- package/tests/registry/nested-validation.test.ts +0 -283
- package/tests/registry/pseudo-tool-filtering.test.ts +0 -258
- package/tests/registry/registration-lifecycle.test.ts +0 -133
- package/tests/registry-validation.test.ts +0 -424
- package/tests/registry.test.ts +0 -460
- package/tests/security/dns-pinning.test.ts +0 -162
- package/tests/security/external-content.test.ts +0 -144
- package/tests/security/ssrf.test.ts +0 -118
- package/tests/shell-safety-integration.test.ts +0 -32
- package/tests/shell-safety.test.ts +0 -365
- package/tests/slack/desktop-session.test.ts +0 -50
- package/tests/test-lane-manifest.ts +0 -440
- package/tests/test-utils.ts +0 -27
- package/tests/tool-factory.test.ts +0 -188
- package/tests/utils/retry.test.ts +0 -231
- package/tests/utils/url.test.ts +0 -63
- package/tsconfig.cjs.json +0 -24
- package/tsconfig.json +0 -12
- package/vitest.config.ts +0 -55
- package/vitest.e2e.config.ts +0 -24
- package/vitest.integration.config.ts +0 -24
- package/vitest.native.config.ts +0 -24
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @aria/tools - Web tool tests (web_search + browse)
|
|
3
|
-
*
|
|
4
|
-
* Tests for the web_search (SearchProviderRouter) and browse (Readability+Turndown) tool executors.
|
|
5
|
-
* All external calls (search providers, fetch) are mocked -- no real network traffic.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as dns from "node:dns";
|
|
9
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
10
|
-
import type { ToolContext } from "../types.js";
|
|
11
|
-
import { executeWebSearch, executeBrowse } from "../executors/web.js";
|
|
12
|
-
import { isPrivateAddress } from "../security/ssrf.js";
|
|
13
|
-
import { searchCache, browseCache } from "../cache/web-cache.js";
|
|
14
|
-
|
|
15
|
-
vi.mock("node:dns", () => ({
|
|
16
|
-
promises: {
|
|
17
|
-
lookup: vi.fn(),
|
|
18
|
-
},
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// Helper to create a minimal ToolContext
|
|
22
|
-
const createContext = (env: Record<string, string> = {}): ToolContext => ({
|
|
23
|
-
workingDir: "/tmp/test",
|
|
24
|
-
env,
|
|
25
|
-
confirm: async () => true,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("web_search", () => {
|
|
29
|
-
let originalFetch: typeof global.fetch;
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
originalFetch = global.fetch;
|
|
33
|
-
searchCache.clear();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
global.fetch = originalFetch;
|
|
38
|
-
vi.restoreAllMocks();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("uses DuckDuckGo fallback when no API keys are set", async () => {
|
|
42
|
-
// With SearchProviderRouter, DuckDuckGo is always available as fallback.
|
|
43
|
-
// No longer requires TAVILY_API_KEY to be set.
|
|
44
|
-
const savedKey = process.env.TAVILY_API_KEY;
|
|
45
|
-
delete process.env.TAVILY_API_KEY;
|
|
46
|
-
|
|
47
|
-
// Mock fetch to respond as DuckDuckGo would
|
|
48
|
-
const mockFetch = vi.fn().mockResolvedValue(
|
|
49
|
-
new Response(JSON.stringify({ results: [] }), {
|
|
50
|
-
status: 200,
|
|
51
|
-
headers: { "Content-Type": "application/json" },
|
|
52
|
-
}),
|
|
53
|
-
);
|
|
54
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const ctx = createContext({});
|
|
58
|
-
const result = await executeWebSearch({ query: "test" }, ctx);
|
|
59
|
-
|
|
60
|
-
// Should succeed via DuckDuckGo fallback, or fail with search error (not missing key)
|
|
61
|
-
if (result.success) {
|
|
62
|
-
expect(result.data).toHaveProperty("query", "test");
|
|
63
|
-
} else {
|
|
64
|
-
expect(result.message).toContain("Web search failed");
|
|
65
|
-
}
|
|
66
|
-
} finally {
|
|
67
|
-
if (savedKey !== undefined) {
|
|
68
|
-
process.env.TAVILY_API_KEY = savedKey;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("returns formatted results from mocked Tavily provider", async () => {
|
|
74
|
-
const tavilyResponse = {
|
|
75
|
-
results: [
|
|
76
|
-
{
|
|
77
|
-
title: "TypeScript Handbook",
|
|
78
|
-
url: "https://www.typescriptlang.org/docs/handbook",
|
|
79
|
-
content: "The TypeScript Handbook is the official guide to TypeScript.",
|
|
80
|
-
score: 0.95,
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
title: "TypeScript Tutorial",
|
|
84
|
-
url: "https://www.typescriptlang.org/docs/tutorial",
|
|
85
|
-
content: "Learn TypeScript from scratch with this tutorial.",
|
|
86
|
-
score: 0.88,
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const mockFetch = vi.fn().mockResolvedValue(
|
|
92
|
-
new Response(JSON.stringify(tavilyResponse), {
|
|
93
|
-
status: 200,
|
|
94
|
-
headers: { "Content-Type": "application/json" },
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
98
|
-
|
|
99
|
-
const ctx = createContext({ TAVILY_API_KEY: "tvly-test-key-123" });
|
|
100
|
-
const result = await executeWebSearch({ query: "typescript guide" }, ctx);
|
|
101
|
-
|
|
102
|
-
expect(result.success).toBe(true);
|
|
103
|
-
expect(result.message).toContain("2 results");
|
|
104
|
-
expect(result.message).toContain("typescript guide");
|
|
105
|
-
|
|
106
|
-
const data = result.data as {
|
|
107
|
-
query: string;
|
|
108
|
-
results: Array<{ title: string; url: string; content: string }>;
|
|
109
|
-
};
|
|
110
|
-
expect(data.query).toBe("typescript guide");
|
|
111
|
-
expect(data.results).toHaveLength(2);
|
|
112
|
-
expect(data.results[0]).toMatchObject({
|
|
113
|
-
title: "TypeScript Handbook",
|
|
114
|
-
url: "https://www.typescriptlang.org/docs/handbook",
|
|
115
|
-
content: expect.stringContaining("TypeScript Handbook"),
|
|
116
|
-
});
|
|
117
|
-
expect(data.results[1]).toMatchObject({
|
|
118
|
-
title: "TypeScript Tutorial",
|
|
119
|
-
url: "https://www.typescriptlang.org/docs/tutorial",
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("routes through Tavily provider when TAVILY_API_KEY is set", async () => {
|
|
124
|
-
const mockFetch = vi.fn().mockResolvedValue(
|
|
125
|
-
new Response(JSON.stringify({ results: [] }), {
|
|
126
|
-
status: 200,
|
|
127
|
-
headers: { "Content-Type": "application/json" },
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
131
|
-
|
|
132
|
-
const ctx = createContext({ TAVILY_API_KEY: "tvly-key" });
|
|
133
|
-
await executeWebSearch({ query: "test query", limit: 3 }, ctx);
|
|
134
|
-
|
|
135
|
-
// Verify the Tavily API was called (through the provider)
|
|
136
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
137
|
-
"https://api.tavily.com/search",
|
|
138
|
-
expect.objectContaining({
|
|
139
|
-
method: "POST",
|
|
140
|
-
body: expect.any(String),
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
145
|
-
expect(body.api_key).toBe("tvly-key");
|
|
146
|
-
expect(body.query).toBe("test query");
|
|
147
|
-
expect(body.max_results).toBe(3);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("defaults limit to 10 when not specified", async () => {
|
|
151
|
-
const mockFetch = vi
|
|
152
|
-
.fn()
|
|
153
|
-
.mockResolvedValue(new Response(JSON.stringify({ results: [] }), { status: 200 }));
|
|
154
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
155
|
-
|
|
156
|
-
const ctx = createContext({ TAVILY_API_KEY: "tvly-key" });
|
|
157
|
-
await executeWebSearch({ query: "test" }, ctx);
|
|
158
|
-
|
|
159
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
160
|
-
expect(body.max_results).toBe(10);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("handles provider API error response", async () => {
|
|
164
|
-
vi.stubGlobal(
|
|
165
|
-
"fetch",
|
|
166
|
-
vi.fn().mockResolvedValue(
|
|
167
|
-
new Response("Unauthorized", {
|
|
168
|
-
status: 401,
|
|
169
|
-
statusText: "Unauthorized",
|
|
170
|
-
}),
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const ctx = createContext({ TAVILY_API_KEY: "tvly-bad-key" });
|
|
175
|
-
const result = await executeWebSearch({ query: "test" }, ctx);
|
|
176
|
-
|
|
177
|
-
// With SearchProviderRouter, a 401 from Tavily triggers fallback to DuckDuckGo.
|
|
178
|
-
// DuckDuckGo also gets the 401 mock, so all providers fail.
|
|
179
|
-
expect(result.success).toBe(false);
|
|
180
|
-
expect(result.message).toContain("Web search failed");
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("handles network error from all providers", async () => {
|
|
184
|
-
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network unreachable")));
|
|
185
|
-
|
|
186
|
-
const ctx = createContext({ TAVILY_API_KEY: "tvly-key" });
|
|
187
|
-
const result = await executeWebSearch({ query: "test" }, ctx);
|
|
188
|
-
|
|
189
|
-
expect(result.success).toBe(false);
|
|
190
|
-
expect(result.message).toContain("Web search failed");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("merges ctx.env API keys into process.env for providers", async () => {
|
|
194
|
-
const mockFetch = vi
|
|
195
|
-
.fn()
|
|
196
|
-
.mockResolvedValue(new Response(JSON.stringify({ results: [] }), { status: 200 }));
|
|
197
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
198
|
-
|
|
199
|
-
// ctx.env should be preferred — SearchProviderRouter reads from process.env,
|
|
200
|
-
// but executeWebSearch merges ctx.env into process.env temporarily
|
|
201
|
-
const ctx = createContext({ TAVILY_API_KEY: "ctx-key" });
|
|
202
|
-
await executeWebSearch({ query: "test" }, ctx);
|
|
203
|
-
|
|
204
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
205
|
-
expect(body.api_key).toBe("ctx-key");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("handles context abort as cancellation", async () => {
|
|
209
|
-
vi.stubGlobal(
|
|
210
|
-
"fetch",
|
|
211
|
-
vi.fn().mockImplementation((_url: string, options?: RequestInit) => {
|
|
212
|
-
return new Promise((_resolve, reject) => {
|
|
213
|
-
const signal = options?.signal;
|
|
214
|
-
if (signal) {
|
|
215
|
-
signal.addEventListener("abort", () => {
|
|
216
|
-
const abortError = new Error("The operation was aborted");
|
|
217
|
-
abortError.name = "AbortError";
|
|
218
|
-
reject(abortError);
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
const ctx = createContext({ TAVILY_API_KEY: "tvly-key" });
|
|
226
|
-
const abortController = new AbortController();
|
|
227
|
-
const ctxWithAbort = { ...ctx, abortSignal: abortController.signal };
|
|
228
|
-
|
|
229
|
-
// Abort immediately to trigger the cancellation path
|
|
230
|
-
setTimeout(() => abortController.abort(), 10);
|
|
231
|
-
const result = await executeWebSearch({ query: "test" }, ctxWithAbort);
|
|
232
|
-
|
|
233
|
-
expect(result.success).toBe(false);
|
|
234
|
-
expect(result.message).toContain("cancelled");
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe("browse", () => {
|
|
239
|
-
let originalFetch: typeof global.fetch;
|
|
240
|
-
|
|
241
|
-
beforeEach(() => {
|
|
242
|
-
originalFetch = global.fetch;
|
|
243
|
-
browseCache.clear();
|
|
244
|
-
// Default DNS mock: resolve to a public IP
|
|
245
|
-
vi.mocked(dns.promises.lookup).mockResolvedValue({
|
|
246
|
-
address: "93.184.216.34",
|
|
247
|
-
family: 4,
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
afterEach(() => {
|
|
252
|
-
global.fetch = originalFetch;
|
|
253
|
-
vi.restoreAllMocks();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("returns text content from a valid HTML page", async () => {
|
|
257
|
-
const html = `
|
|
258
|
-
<html>
|
|
259
|
-
<head><title>Example Page</title></head>
|
|
260
|
-
<body>
|
|
261
|
-
<h1>Hello World</h1>
|
|
262
|
-
<p>This is a test page with some content.</p>
|
|
263
|
-
</body>
|
|
264
|
-
</html>
|
|
265
|
-
`;
|
|
266
|
-
|
|
267
|
-
vi.stubGlobal(
|
|
268
|
-
"fetch",
|
|
269
|
-
vi.fn().mockResolvedValue(
|
|
270
|
-
new Response(html, {
|
|
271
|
-
status: 200,
|
|
272
|
-
headers: { "Content-Type": "text/html" },
|
|
273
|
-
}),
|
|
274
|
-
),
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const ctx = createContext();
|
|
278
|
-
const result = await executeBrowse({ url: "https://example.com" }, ctx);
|
|
279
|
-
|
|
280
|
-
expect(result.success).toBe(true);
|
|
281
|
-
expect(result.message).toContain("Browsed");
|
|
282
|
-
|
|
283
|
-
const data = result.data as { url: string; title: string; content: string };
|
|
284
|
-
expect(data.url).toBe("https://example.com");
|
|
285
|
-
expect(data.title).toBe("Example Page");
|
|
286
|
-
expect(data.content).toContain("Hello World");
|
|
287
|
-
expect(data.content).toContain("test page");
|
|
288
|
-
// Should not contain HTML tags
|
|
289
|
-
expect(data.content).not.toContain("<h1>");
|
|
290
|
-
expect(data.content).not.toContain("<p>");
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("strips script and style blocks from HTML via Readability+Turndown", async () => {
|
|
294
|
-
const html = `
|
|
295
|
-
<html>
|
|
296
|
-
<head>
|
|
297
|
-
<title>Test</title>
|
|
298
|
-
<style>body { color: red; }</style>
|
|
299
|
-
</head>
|
|
300
|
-
<body>
|
|
301
|
-
<script>alert('xss');</script>
|
|
302
|
-
<p>Visible content only</p>
|
|
303
|
-
<script type="text/javascript">console.log('hidden');</script>
|
|
304
|
-
</body>
|
|
305
|
-
</html>
|
|
306
|
-
`;
|
|
307
|
-
|
|
308
|
-
vi.stubGlobal(
|
|
309
|
-
"fetch",
|
|
310
|
-
vi.fn().mockResolvedValue(
|
|
311
|
-
new Response(html, {
|
|
312
|
-
status: 200,
|
|
313
|
-
headers: { "Content-Type": "text/html" },
|
|
314
|
-
}),
|
|
315
|
-
),
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const ctx = createContext();
|
|
319
|
-
const result = await executeBrowse({ url: "https://example.com" }, ctx);
|
|
320
|
-
|
|
321
|
-
expect(result.success).toBe(true);
|
|
322
|
-
const data = result.data as { content: string };
|
|
323
|
-
// Content is now wrapped with external content boundary markers
|
|
324
|
-
expect(data.content).toContain("Visible content only");
|
|
325
|
-
expect(data.content).not.toContain("alert(");
|
|
326
|
-
expect(data.content).not.toContain("console.log(");
|
|
327
|
-
expect(data.content).not.toContain("color: red");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("returns error for invalid URL", async () => {
|
|
331
|
-
const ctx = createContext();
|
|
332
|
-
const result = await executeBrowse({ url: "not-a-valid-url" }, ctx);
|
|
333
|
-
|
|
334
|
-
expect(result.success).toBe(false);
|
|
335
|
-
expect(result.message).toContain("Invalid URL");
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("returns error for missing URL", async () => {
|
|
339
|
-
const ctx = createContext();
|
|
340
|
-
const result = await executeBrowse({ url: "" }, ctx);
|
|
341
|
-
|
|
342
|
-
expect(result.success).toBe(false);
|
|
343
|
-
expect(result.message).toContain("URL is required");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("returns error for non-http protocols", async () => {
|
|
347
|
-
const ctx = createContext();
|
|
348
|
-
const result = await executeBrowse({ url: "ftp://files.example.com/doc" }, ctx);
|
|
349
|
-
|
|
350
|
-
expect(result.success).toBe(false);
|
|
351
|
-
expect(result.message).toContain("protocol");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("returns error on HTTP error response", async () => {
|
|
355
|
-
vi.stubGlobal(
|
|
356
|
-
"fetch",
|
|
357
|
-
vi.fn().mockResolvedValue(
|
|
358
|
-
new Response("Not Found", {
|
|
359
|
-
status: 404,
|
|
360
|
-
statusText: "Not Found",
|
|
361
|
-
}),
|
|
362
|
-
),
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
const ctx = createContext();
|
|
366
|
-
const result = await executeBrowse({ url: "https://example.com/missing" }, ctx);
|
|
367
|
-
|
|
368
|
-
expect(result.success).toBe(false);
|
|
369
|
-
expect(result.message).toContain("404");
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it("handles timeout", async () => {
|
|
373
|
-
vi.stubGlobal(
|
|
374
|
-
"fetch",
|
|
375
|
-
vi.fn().mockImplementation((_url: string, options?: RequestInit) => {
|
|
376
|
-
return new Promise((_resolve, reject) => {
|
|
377
|
-
const signal = options?.signal;
|
|
378
|
-
if (signal) {
|
|
379
|
-
signal.addEventListener("abort", () => {
|
|
380
|
-
const abortError = new Error("The operation was aborted");
|
|
381
|
-
abortError.name = "AbortError";
|
|
382
|
-
reject(abortError);
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
}),
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
const ctx = createContext();
|
|
390
|
-
const result = await executeBrowse({ url: "https://slow.example.com", timeoutMs: 50 }, ctx);
|
|
391
|
-
|
|
392
|
-
expect(result.success).toBe(false);
|
|
393
|
-
expect(result.message).toContain("timed out");
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("handles network error", async () => {
|
|
397
|
-
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Connection refused")));
|
|
398
|
-
|
|
399
|
-
const ctx = createContext();
|
|
400
|
-
const result = await executeBrowse({ url: "https://unreachable.example.com" }, ctx);
|
|
401
|
-
|
|
402
|
-
expect(result.success).toBe(false);
|
|
403
|
-
expect(result.message).toContain("Connection refused");
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it("truncates long content to 50000 characters", async () => {
|
|
407
|
-
const longText = "A".repeat(60_000);
|
|
408
|
-
const html = `<html><head><title>Big</title></head><body><p>${longText}</p></body></html>`;
|
|
409
|
-
|
|
410
|
-
vi.stubGlobal(
|
|
411
|
-
"fetch",
|
|
412
|
-
vi.fn().mockResolvedValue(
|
|
413
|
-
new Response(html, {
|
|
414
|
-
status: 200,
|
|
415
|
-
headers: { "Content-Type": "text/html" },
|
|
416
|
-
}),
|
|
417
|
-
),
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
const ctx = createContext();
|
|
421
|
-
const result = await executeBrowse({ url: "https://example.com/long" }, ctx);
|
|
422
|
-
|
|
423
|
-
expect(result.success).toBe(true);
|
|
424
|
-
const data = result.data as { content: string };
|
|
425
|
-
// Content is now truncated at 50K + wrapped with external content boundary
|
|
426
|
-
// The boundary markers add ~100 chars, truncation notice adds ~20 chars
|
|
427
|
-
expect(data.content).toContain("[Content truncated]");
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it("marks content as truncated when response exceeds byte limit", async () => {
|
|
431
|
-
const longText = "A".repeat(2 * 1024 * 1024 + 4096);
|
|
432
|
-
const html = `<html><head><title>Huge</title></head><body><p>${longText}</p></body></html>`;
|
|
433
|
-
|
|
434
|
-
vi.stubGlobal(
|
|
435
|
-
"fetch",
|
|
436
|
-
vi.fn().mockResolvedValue(
|
|
437
|
-
new Response(html, {
|
|
438
|
-
status: 200,
|
|
439
|
-
headers: { "Content-Type": "text/html" },
|
|
440
|
-
}),
|
|
441
|
-
),
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
const ctx = createContext();
|
|
445
|
-
const result = await executeBrowse({ url: "https://example.com/huge-truncated" }, ctx);
|
|
446
|
-
|
|
447
|
-
expect(result.success).toBe(true);
|
|
448
|
-
const data = result.data as { content: string };
|
|
449
|
-
expect(data.content).toContain("[Content truncated]");
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it("decodes HTML entities", async () => {
|
|
453
|
-
const html = `
|
|
454
|
-
<html>
|
|
455
|
-
<head><title>Entities</title></head>
|
|
456
|
-
<body>
|
|
457
|
-
<p>Tom & Jerry <3 > "friends" 'forever'</p>
|
|
458
|
-
</body>
|
|
459
|
-
</html>
|
|
460
|
-
`;
|
|
461
|
-
|
|
462
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(html, { status: 200 })));
|
|
463
|
-
|
|
464
|
-
const ctx = createContext();
|
|
465
|
-
const result = await executeBrowse({ url: "https://example.com/entities" }, ctx);
|
|
466
|
-
|
|
467
|
-
expect(result.success).toBe(true);
|
|
468
|
-
const data = result.data as { content: string };
|
|
469
|
-
// Depending on extraction path, content may be decoded text or raw HTML fallback.
|
|
470
|
-
expect(data.content).toMatch(/Tom\s+(?:&|&)\s+Jerry/);
|
|
471
|
-
expect(data.content).toMatch(/(?:<3|<3)/);
|
|
472
|
-
expect(data.content).toMatch(/(?:"friends"|"friends")/);
|
|
473
|
-
expect(data.content).toMatch(/(?:'forever'|'forever')/);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("returns empty title when no <title> tag present", async () => {
|
|
477
|
-
const html = "<html><body><p>No title here</p></body></html>";
|
|
478
|
-
|
|
479
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(html, { status: 200 })));
|
|
480
|
-
|
|
481
|
-
const ctx = createContext();
|
|
482
|
-
const result = await executeBrowse({ url: "https://example.com/notitle" }, ctx);
|
|
483
|
-
|
|
484
|
-
expect(result.success).toBe(true);
|
|
485
|
-
const data = result.data as { title: string };
|
|
486
|
-
expect(data.title).toBe("");
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it("rejects URLs that resolve to private IP addresses", async () => {
|
|
490
|
-
vi.mocked(dns.promises.lookup).mockResolvedValue({
|
|
491
|
-
address: "127.0.0.1",
|
|
492
|
-
family: 4,
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
const ctx = createContext();
|
|
496
|
-
const result = await executeBrowse({ url: "https://internal.example.com" }, ctx);
|
|
497
|
-
|
|
498
|
-
expect(result.success).toBe(false);
|
|
499
|
-
expect(result.message).toContain("private network");
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
describe("isPrivateAddress", () => {
|
|
504
|
-
it("blocks IPv4 loopback (127.x.x.x)", () => {
|
|
505
|
-
expect(isPrivateAddress("127.0.0.1")).toBe(true);
|
|
506
|
-
expect(isPrivateAddress("127.255.255.255")).toBe(true);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("blocks AWS metadata endpoint (169.254.169.254)", () => {
|
|
510
|
-
expect(isPrivateAddress("169.254.169.254")).toBe(true);
|
|
511
|
-
expect(isPrivateAddress("169.254.0.1")).toBe(true);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it("blocks RFC 1918 10.x.x.x", () => {
|
|
515
|
-
expect(isPrivateAddress("10.0.0.1")).toBe(true);
|
|
516
|
-
expect(isPrivateAddress("10.255.255.255")).toBe(true);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it("blocks RFC 1918 172.16-31.x.x", () => {
|
|
520
|
-
expect(isPrivateAddress("172.16.0.1")).toBe(true);
|
|
521
|
-
expect(isPrivateAddress("172.31.255.255")).toBe(true);
|
|
522
|
-
// 172.15.x and 172.32.x are NOT private
|
|
523
|
-
expect(isPrivateAddress("172.15.0.1")).toBe(false);
|
|
524
|
-
expect(isPrivateAddress("172.32.0.1")).toBe(false);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
it("blocks RFC 1918 192.168.x.x", () => {
|
|
528
|
-
expect(isPrivateAddress("192.168.1.1")).toBe(true);
|
|
529
|
-
expect(isPrivateAddress("192.168.0.0")).toBe(true);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it("blocks IPv6 loopback (::1)", () => {
|
|
533
|
-
expect(isPrivateAddress("::1")).toBe(true);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it("blocks IPv6 private (fc00::/7)", () => {
|
|
537
|
-
expect(isPrivateAddress("fc00::1")).toBe(true);
|
|
538
|
-
expect(isPrivateAddress("fd12:3456:789a::1")).toBe(true);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it("blocks unspecified addresses", () => {
|
|
542
|
-
expect(isPrivateAddress("0.0.0.0")).toBe(true);
|
|
543
|
-
expect(isPrivateAddress("::")).toBe(true);
|
|
544
|
-
expect(isPrivateAddress("[::]")).toBe(true);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it("allows public IP addresses", () => {
|
|
548
|
-
expect(isPrivateAddress("8.8.8.8")).toBe(false);
|
|
549
|
-
expect(isPrivateAddress("93.184.216.34")).toBe(false);
|
|
550
|
-
expect(isPrivateAddress("1.1.1.1")).toBe(false);
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
describe("SSRF protection", () => {
|
|
555
|
-
let originalFetch: typeof global.fetch;
|
|
556
|
-
|
|
557
|
-
beforeEach(() => {
|
|
558
|
-
originalFetch = global.fetch;
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
afterEach(() => {
|
|
562
|
-
global.fetch = originalFetch;
|
|
563
|
-
vi.restoreAllMocks();
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
const privateIPs = [
|
|
567
|
-
{ ip: "127.0.0.1", label: "loopback", family: 4 },
|
|
568
|
-
{ ip: "169.254.169.254", label: "AWS metadata", family: 4 },
|
|
569
|
-
{ ip: "10.0.0.1", label: "RFC 1918 (10.x)", family: 4 },
|
|
570
|
-
{ ip: "192.168.1.1", label: "RFC 1918 (192.168.x)", family: 4 },
|
|
571
|
-
{ ip: "172.16.0.1", label: "RFC 1918 (172.16.x)", family: 4 },
|
|
572
|
-
{ ip: "::1", label: "IPv6 loopback", family: 6 },
|
|
573
|
-
];
|
|
574
|
-
|
|
575
|
-
for (const { ip, label, family } of privateIPs) {
|
|
576
|
-
it(`browse rejects hostname resolving to ${label} (${ip})`, async () => {
|
|
577
|
-
vi.mocked(dns.promises.lookup).mockResolvedValue({
|
|
578
|
-
address: ip,
|
|
579
|
-
family,
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
const ctx = createContext();
|
|
583
|
-
const result = await executeBrowse({ url: "https://attacker.example.com" }, ctx);
|
|
584
|
-
|
|
585
|
-
expect(result.success).toBe(false);
|
|
586
|
-
expect(result.message).toContain("private network");
|
|
587
|
-
expect(result.message).toContain(ip);
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
it("browse allows hostname resolving to public IP", async () => {
|
|
592
|
-
vi.mocked(dns.promises.lookup).mockResolvedValue({
|
|
593
|
-
address: "8.8.8.8",
|
|
594
|
-
family: 4,
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
vi.stubGlobal(
|
|
598
|
-
"fetch",
|
|
599
|
-
vi.fn().mockResolvedValue(new Response("<html><body>OK</body></html>", { status: 200 })),
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
const ctx = createContext();
|
|
603
|
-
const result = await executeBrowse({ url: "https://safe.example.com" }, ctx);
|
|
604
|
-
|
|
605
|
-
expect(result.success).toBe(true);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it("browse returns error when DNS resolution fails", async () => {
|
|
609
|
-
vi.mocked(dns.promises.lookup).mockRejectedValue(
|
|
610
|
-
new Error("getaddrinfo ENOTFOUND bad.example.com"),
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
const ctx = createContext();
|
|
614
|
-
const result = await executeBrowse({ url: "https://bad.example.com" }, ctx);
|
|
615
|
-
|
|
616
|
-
expect(result.success).toBe(false);
|
|
617
|
-
expect(result.message).toContain("DNS resolution failed");
|
|
618
|
-
});
|
|
619
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export interface AskUserQuestion {
|
|
2
|
-
question: string;
|
|
3
|
-
options?: string[];
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const ASK_USER_PAUSE_REQUIRED_CODE = "ASK_USER_PAUSE_REQUIRED" as const;
|
|
7
|
-
|
|
8
|
-
export class AskUserPauseRequiredError extends Error {
|
|
9
|
-
readonly code = ASK_USER_PAUSE_REQUIRED_CODE;
|
|
10
|
-
readonly questions: AskUserQuestion[];
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
questions: AskUserQuestion[],
|
|
14
|
-
message = "ask_user requires additional answers before this run can continue.",
|
|
15
|
-
) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = "AskUserPauseRequiredError";
|
|
18
|
-
this.questions = questions.map((question) => ({
|
|
19
|
-
question: question.question,
|
|
20
|
-
...(Array.isArray(question.options) ? { options: [...question.options] } : {}),
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function isAskUserPauseRequiredError(error: unknown): error is AskUserPauseRequiredError {
|
|
26
|
-
if (error instanceof AskUserPauseRequiredError) return true;
|
|
27
|
-
if (!error || typeof error !== "object") return false;
|
|
28
|
-
const candidate = error as { code?: unknown; name?: unknown; questions?: unknown };
|
|
29
|
-
return (
|
|
30
|
-
candidate.code === ASK_USER_PAUSE_REQUIRED_CODE ||
|
|
31
|
-
(candidate.name === "AskUserPauseRequiredError" && Array.isArray(candidate.questions))
|
|
32
|
-
);
|
|
33
|
-
}
|