@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,633 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for web tool executors
|
|
3
|
-
*
|
|
4
|
-
* These tests verify the full integration of web executors including:
|
|
5
|
-
* - web_search → SearchProviderRouter → mock provider → results
|
|
6
|
-
* - web_fetch → fetchWithSsrf → content retrieval
|
|
7
|
-
* - browse → Readability + Turndown → markdown extraction
|
|
8
|
-
* - Retry on transient failures
|
|
9
|
-
* - SSRF protection and redirect handling
|
|
10
|
-
* - GitHub URL normalization
|
|
11
|
-
*
|
|
12
|
-
* Unlike web.test.ts which uses simple unit test mocks, these tests
|
|
13
|
-
* verify the actual data flow through multiple layers.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
17
|
-
import * as fs from "node:fs/promises";
|
|
18
|
-
import * as path from "node:path";
|
|
19
|
-
import * as os from "node:os";
|
|
20
|
-
import type { ToolContext } from "../../src/types.js";
|
|
21
|
-
import { executeWebSearch, executeWebFetch, executeBrowse } from "../../src/executors/web.js";
|
|
22
|
-
import { searchCache, fetchCache, browseCache } from "../../src/cache/web-cache.js";
|
|
23
|
-
|
|
24
|
-
const TEST_PUBLIC_LOOKUP_RESULT = [{ address: "93.184.216.34", family: 4 }] as const;
|
|
25
|
-
|
|
26
|
-
function lookupTestAddresses(hostname: string) {
|
|
27
|
-
switch (hostname) {
|
|
28
|
-
case "metadata.internal.test":
|
|
29
|
-
return [{ address: "169.254.169.254", family: 4 }];
|
|
30
|
-
default:
|
|
31
|
-
return TEST_PUBLIC_LOOKUP_RESULT;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
vi.mock("../../src/security/dns-pinning.js", async (importOriginal) => {
|
|
36
|
-
const actual = await importOriginal<typeof import("../../src/security/dns-pinning.js")>();
|
|
37
|
-
const ssrfModule = await import("../../src/security/ssrf.js");
|
|
38
|
-
return {
|
|
39
|
-
...actual,
|
|
40
|
-
fetchWithDnsPinning: vi.fn(async (url: string, init: RequestInit) => {
|
|
41
|
-
const parsed = new URL(url);
|
|
42
|
-
const addresses = lookupTestAddresses(parsed.hostname);
|
|
43
|
-
const privateAddress = addresses.find((entry) => ssrfModule.isPrivateAddress(entry.address));
|
|
44
|
-
if (privateAddress) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`SSRF protection: ${parsed.hostname} resolves to private address ${privateAddress.address}`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return global.fetch(url, init);
|
|
50
|
-
}),
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Helper to create a unique temp directory for each test
|
|
55
|
-
const createTempDir = async (): Promise<string> => {
|
|
56
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "aria-web-integration-"));
|
|
57
|
-
return fs.realpath(tempDir);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Helper to clean up temp directory
|
|
61
|
-
const cleanupTempDir = async (dir: string): Promise<void> => {
|
|
62
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Helper to create a context
|
|
66
|
-
const createContext = (workingDir: string, env: Record<string, string> = {}): ToolContext => ({
|
|
67
|
-
workingDir,
|
|
68
|
-
env,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("Web Executors Integration Tests", () => {
|
|
72
|
-
let tempDir: string;
|
|
73
|
-
let ctx: ToolContext;
|
|
74
|
-
let originalFetch: typeof global.fetch;
|
|
75
|
-
|
|
76
|
-
beforeEach(async () => {
|
|
77
|
-
tempDir = await createTempDir();
|
|
78
|
-
ctx = createContext(tempDir);
|
|
79
|
-
originalFetch = global.fetch;
|
|
80
|
-
// Clear web caches between tests to prevent cross-test contamination
|
|
81
|
-
searchCache.clear();
|
|
82
|
-
fetchCache.clear();
|
|
83
|
-
browseCache.clear();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
afterEach(async () => {
|
|
87
|
-
await cleanupTempDir(tempDir);
|
|
88
|
-
global.fetch = originalFetch;
|
|
89
|
-
vi.restoreAllMocks();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("executeWebSearch integration", () => {
|
|
93
|
-
it("should route through SearchProviderRouter to Tavily with valid results", async () => {
|
|
94
|
-
const mockResults = [
|
|
95
|
-
{
|
|
96
|
-
title: "Integration Test Result",
|
|
97
|
-
url: "https://example.com/integration",
|
|
98
|
-
content: "This is integration test content from Tavily",
|
|
99
|
-
score: 0.95,
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
title: "Second Result",
|
|
103
|
-
url: "https://example.com/second",
|
|
104
|
-
content: "More content here",
|
|
105
|
-
score: 0.85,
|
|
106
|
-
},
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
const mockResponse = new Response(JSON.stringify({ results: mockResults }), {
|
|
110
|
-
status: 200,
|
|
111
|
-
headers: { "Content-Type": "application/json" },
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse);
|
|
115
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
116
|
-
|
|
117
|
-
const ctxWithKey = createContext(tempDir, { TAVILY_API_KEY: "test-integration-key" });
|
|
118
|
-
const result = await executeWebSearch(
|
|
119
|
-
{ query: "integration testing best practices", limit: 10 },
|
|
120
|
-
ctxWithKey,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
expect(result.success).toBe(true);
|
|
124
|
-
expect(result.message).toContain("integration testing best practices");
|
|
125
|
-
expect(result.data).toMatchObject({
|
|
126
|
-
query: "integration testing best practices",
|
|
127
|
-
results: expect.arrayContaining([
|
|
128
|
-
expect.objectContaining({
|
|
129
|
-
title: "Integration Test Result",
|
|
130
|
-
url: "https://example.com/integration",
|
|
131
|
-
}),
|
|
132
|
-
]),
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Verify the API was called with correct parameters
|
|
136
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
137
|
-
"https://api.tavily.com/search",
|
|
138
|
-
expect.objectContaining({
|
|
139
|
-
method: "POST",
|
|
140
|
-
body: expect.stringContaining("integration testing best practices"),
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("should handle empty results from provider", async () => {
|
|
146
|
-
const mockResponse = new Response(JSON.stringify({ results: [] }), {
|
|
147
|
-
status: 200,
|
|
148
|
-
headers: { "Content-Type": "application/json" },
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
152
|
-
|
|
153
|
-
const ctxWithKey = createContext(tempDir, { TAVILY_API_KEY: "test-key" });
|
|
154
|
-
const result = await executeWebSearch({ query: "nonexistent query 12345xyz" }, ctxWithKey);
|
|
155
|
-
|
|
156
|
-
expect(result.success).toBe(true);
|
|
157
|
-
expect(result.data).toMatchObject({
|
|
158
|
-
query: "nonexistent query 12345xyz",
|
|
159
|
-
results: [],
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("should retry on transient failures", async () => {
|
|
164
|
-
let callCount = 0;
|
|
165
|
-
const mockFetch = vi.fn().mockImplementation(() => {
|
|
166
|
-
callCount++;
|
|
167
|
-
if (callCount < 2) {
|
|
168
|
-
// First call fails with network error
|
|
169
|
-
return Promise.reject(new Error("Network error"));
|
|
170
|
-
}
|
|
171
|
-
// Second call succeeds
|
|
172
|
-
return Promise.resolve(
|
|
173
|
-
new Response(
|
|
174
|
-
JSON.stringify({
|
|
175
|
-
results: [
|
|
176
|
-
{ title: "Success", url: "https://example.com", content: "content", score: 0.9 },
|
|
177
|
-
],
|
|
178
|
-
}),
|
|
179
|
-
{
|
|
180
|
-
status: 200,
|
|
181
|
-
headers: { "Content-Type": "application/json" },
|
|
182
|
-
},
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
188
|
-
|
|
189
|
-
const ctxWithKey = createContext(tempDir, { TAVILY_API_KEY: "test-key" });
|
|
190
|
-
const result = await executeWebSearch({ query: "retry test" }, ctxWithKey);
|
|
191
|
-
|
|
192
|
-
// SearchProviderRouter tries providers in priority order; a network error
|
|
193
|
-
// causes it to fall through to the next provider or fail.
|
|
194
|
-
// The assertion must be unconditional.
|
|
195
|
-
expect(callCount).toBeGreaterThanOrEqual(1);
|
|
196
|
-
expect(result).toHaveProperty("success");
|
|
197
|
-
expect(result).toHaveProperty("message");
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe("executeWebFetch integration", () => {
|
|
202
|
-
it("should fetch content with retry on transient failure", async () => {
|
|
203
|
-
let attemptCount = 0;
|
|
204
|
-
const mockFetch = vi.fn().mockImplementation(() => {
|
|
205
|
-
attemptCount++;
|
|
206
|
-
if (attemptCount === 1) {
|
|
207
|
-
// First attempt fails with retryable error
|
|
208
|
-
const err = Object.assign(new Error("ECONNRESET"), { code: "ECONNRESET" });
|
|
209
|
-
return Promise.reject(err);
|
|
210
|
-
}
|
|
211
|
-
// Second attempt succeeds
|
|
212
|
-
return Promise.resolve(
|
|
213
|
-
new Response("Fetched content after retry", {
|
|
214
|
-
status: 200,
|
|
215
|
-
headers: { "Content-Type": "text/plain" },
|
|
216
|
-
}),
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
221
|
-
|
|
222
|
-
const result = await executeWebFetch({ url: "https://example.com/flaky" }, ctx);
|
|
223
|
-
|
|
224
|
-
// fetchWithRetry is wired in — retry should succeed
|
|
225
|
-
expect(attemptCount).toBe(2);
|
|
226
|
-
expect(result.success).toBe(true);
|
|
227
|
-
expect(result.data).toMatchObject({
|
|
228
|
-
status: 200,
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("should follow redirects with SSRF validation per hop", async () => {
|
|
233
|
-
const mockFetch = vi
|
|
234
|
-
.fn()
|
|
235
|
-
.mockResolvedValueOnce(
|
|
236
|
-
// First call: redirect
|
|
237
|
-
new Response(null, {
|
|
238
|
-
status: 302,
|
|
239
|
-
headers: { Location: "https://redirect.example.com/target" },
|
|
240
|
-
}),
|
|
241
|
-
)
|
|
242
|
-
.mockResolvedValueOnce(
|
|
243
|
-
// Second call: final content
|
|
244
|
-
new Response("Final content after redirect", {
|
|
245
|
-
status: 200,
|
|
246
|
-
headers: { "Content-Type": "text/plain" },
|
|
247
|
-
}),
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
251
|
-
|
|
252
|
-
const result = await executeWebFetch({ url: "https://example.com/redirect-me" }, ctx);
|
|
253
|
-
|
|
254
|
-
expect(result.success).toBe(true);
|
|
255
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
256
|
-
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
257
|
-
1,
|
|
258
|
-
"https://example.com/redirect-me",
|
|
259
|
-
expect.objectContaining({ redirect: "manual" }),
|
|
260
|
-
);
|
|
261
|
-
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
262
|
-
2,
|
|
263
|
-
"https://redirect.example.com/target",
|
|
264
|
-
expect.objectContaining({ redirect: "manual" }),
|
|
265
|
-
);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("should enforce response size limit with streaming", async () => {
|
|
269
|
-
// Create a large content stream that exceeds limit
|
|
270
|
-
const largeContent = "x".repeat(2 * 1024 * 1024); // 2MB
|
|
271
|
-
const mockResponse = new Response(largeContent, {
|
|
272
|
-
status: 200,
|
|
273
|
-
headers: { "Content-Type": "text/plain" },
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
277
|
-
|
|
278
|
-
const result = await executeWebFetch(
|
|
279
|
-
{
|
|
280
|
-
url: "https://example.com/large",
|
|
281
|
-
maxSizeBytes: 1024, // 1KB limit
|
|
282
|
-
},
|
|
283
|
-
ctx,
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
expect(result.success).toBe(false);
|
|
287
|
-
expect(result.message).toContain("exceeds maximum");
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("should handle JSON parsing with format option", async () => {
|
|
291
|
-
const jsonData = { status: "ok", data: [1, 2, 3], nested: { value: "test" } };
|
|
292
|
-
const mockResponse = new Response(JSON.stringify(jsonData), {
|
|
293
|
-
status: 200,
|
|
294
|
-
headers: { "Content-Type": "application/json" },
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
298
|
-
|
|
299
|
-
const result = await executeWebFetch(
|
|
300
|
-
{ url: "https://api.example.com/data", format: "json" },
|
|
301
|
-
ctx,
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
expect(result.success).toBe(true);
|
|
305
|
-
const data = result.data as { content: typeof jsonData };
|
|
306
|
-
expect(data.content).toEqual(jsonData);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe("executeBrowse integration", () => {
|
|
311
|
-
it("should extract markdown from HTML using Readability + Turndown", async () => {
|
|
312
|
-
const htmlContent = `
|
|
313
|
-
<!DOCTYPE html>
|
|
314
|
-
<html>
|
|
315
|
-
<head><title>Test Article</title></head>
|
|
316
|
-
<body>
|
|
317
|
-
<h1>Main Heading</h1>
|
|
318
|
-
<p>This is a paragraph with <strong>bold</strong> text.</p>
|
|
319
|
-
<ul>
|
|
320
|
-
<li>List item 1</li>
|
|
321
|
-
<li>List item 2</li>
|
|
322
|
-
</ul>
|
|
323
|
-
<script>console.log('should be removed');</script>
|
|
324
|
-
</body>
|
|
325
|
-
</html>
|
|
326
|
-
`;
|
|
327
|
-
|
|
328
|
-
const mockResponse = new Response(htmlContent, {
|
|
329
|
-
status: 200,
|
|
330
|
-
headers: { "Content-Type": "text/html" },
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
334
|
-
|
|
335
|
-
const result = await executeBrowse({ url: "https://example.com/article" }, ctx);
|
|
336
|
-
|
|
337
|
-
expect(result.success).toBe(true);
|
|
338
|
-
const data = result.data as { url: string; title: string; content: string };
|
|
339
|
-
|
|
340
|
-
expect(data.url).toBe("https://example.com/article");
|
|
341
|
-
expect(data.title).toBe("Test Article");
|
|
342
|
-
|
|
343
|
-
// Content should be stripped HTML (current implementation uses stripHtml, not Readability/Turndown)
|
|
344
|
-
// The content should not contain script tags
|
|
345
|
-
expect(data.content).not.toContain("console.log");
|
|
346
|
-
expect(data.content).toContain("Main Heading");
|
|
347
|
-
expect(data.content).toContain("This is a paragraph");
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("should handle GitHub URL normalization to raw content", async () => {
|
|
351
|
-
// GitHub URLs should be converted to raw URLs before fetching
|
|
352
|
-
const mockResponse = new Response("# README\n\nThis is raw markdown content", {
|
|
353
|
-
status: 200,
|
|
354
|
-
headers: { "Content-Type": "text/plain" },
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
358
|
-
|
|
359
|
-
const result = await executeBrowse(
|
|
360
|
-
{ url: "https://github.com/user/repo/blob/main/README.md" },
|
|
361
|
-
ctx,
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
// Current implementation may not do GitHub URL normalization
|
|
365
|
-
// This documents expected behavior
|
|
366
|
-
expect(result.success).toBe(true);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("should truncate content exceeding 50K characters", async () => {
|
|
370
|
-
const longContent = `
|
|
371
|
-
<!DOCTYPE html>
|
|
372
|
-
<html>
|
|
373
|
-
<head><title>Long Article</title></head>
|
|
374
|
-
<body>
|
|
375
|
-
<p>${"Very long content. ".repeat(10000)}</p>
|
|
376
|
-
</body>
|
|
377
|
-
</html>
|
|
378
|
-
`;
|
|
379
|
-
|
|
380
|
-
const mockResponse = new Response(longContent, {
|
|
381
|
-
status: 200,
|
|
382
|
-
headers: { "Content-Type": "text/html" },
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
386
|
-
|
|
387
|
-
const result = await executeBrowse({ url: "https://example.com/long" }, ctx);
|
|
388
|
-
|
|
389
|
-
expect(result.success).toBe(true);
|
|
390
|
-
const data = result.data as { content: string };
|
|
391
|
-
|
|
392
|
-
// Browse now uses 50K limit + external content security wrapping (~100 chars overhead)
|
|
393
|
-
// The content should contain truncation notice
|
|
394
|
-
expect(data.content).toContain("[Content truncated]");
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it("should retry on transient network failures", async () => {
|
|
398
|
-
let attemptCount = 0;
|
|
399
|
-
const mockFetch = vi.fn().mockImplementation(() => {
|
|
400
|
-
attemptCount++;
|
|
401
|
-
if (attemptCount === 1) {
|
|
402
|
-
const err = Object.assign(new Error("ETIMEDOUT"), { code: "ETIMEDOUT" });
|
|
403
|
-
return Promise.reject(err);
|
|
404
|
-
}
|
|
405
|
-
return Promise.resolve(
|
|
406
|
-
new Response("<html><head><title>Success</title></head><body>Content</body></html>", {
|
|
407
|
-
status: 200,
|
|
408
|
-
headers: { "Content-Type": "text/html" },
|
|
409
|
-
}),
|
|
410
|
-
);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
414
|
-
|
|
415
|
-
const result = await executeBrowse({ url: "https://example.com/flaky" }, ctx);
|
|
416
|
-
|
|
417
|
-
// fetchWithRetry is wired in — retry should succeed
|
|
418
|
-
expect(attemptCount).toBe(2);
|
|
419
|
-
expect(result.success).toBe(true);
|
|
420
|
-
expect(result.data).toHaveProperty("content");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it("should handle timeout on slow responses", async () => {
|
|
424
|
-
vi.stubGlobal(
|
|
425
|
-
"fetch",
|
|
426
|
-
vi.fn().mockImplementation((_url: string, options?: RequestInit) => {
|
|
427
|
-
return new Promise((_resolve, reject) => {
|
|
428
|
-
const signal = options?.signal;
|
|
429
|
-
if (signal) {
|
|
430
|
-
signal.addEventListener("abort", () => {
|
|
431
|
-
const abortError = new Error("The operation was aborted");
|
|
432
|
-
abortError.name = "AbortError";
|
|
433
|
-
reject(abortError);
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
}),
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
const result = await executeBrowse({ url: "https://example.com/slow", timeoutMs: 100 }, ctx);
|
|
441
|
-
|
|
442
|
-
expect(result.success).toBe(false);
|
|
443
|
-
expect(result.message).toContain("timed out");
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
describe("SSRF protection integration", () => {
|
|
448
|
-
it("should validate URLs before making requests", async () => {
|
|
449
|
-
// Invalid URL format
|
|
450
|
-
const result1 = await executeWebFetch({ url: "not-a-url" }, ctx);
|
|
451
|
-
expect(result1.success).toBe(false);
|
|
452
|
-
expect(result1.message).toContain("Invalid URL");
|
|
453
|
-
|
|
454
|
-
// Non-HTTP protocol
|
|
455
|
-
const result2 = await executeWebFetch({ url: "file:///etc/passwd" }, ctx);
|
|
456
|
-
expect(result2.success).toBe(false);
|
|
457
|
-
expect(result2.message).toContain("protocol");
|
|
458
|
-
|
|
459
|
-
// FTP protocol
|
|
460
|
-
const result3 = await executeWebFetch({ url: "ftp://example.com/file" }, ctx);
|
|
461
|
-
expect(result3.success).toBe(false);
|
|
462
|
-
expect(result3.message).toContain("protocol");
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it("should block requests resolving to private IPs", async () => {
|
|
466
|
-
// Mock DNS to resolve to private IP
|
|
467
|
-
const mockDns = {
|
|
468
|
-
promises: {
|
|
469
|
-
lookup: vi.fn().mockResolvedValue({ address: "192.168.1.1", family: 4 }),
|
|
470
|
-
},
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
// This test requires mocking the dns module, which is complex
|
|
474
|
-
// The behavior is tested in unit tests for validateUrl in ssrf.ts
|
|
475
|
-
// This integration test documents the expected behavior
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it("should block redirects to private IPs", async () => {
|
|
479
|
-
// First fetch returns redirect to private IP
|
|
480
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
481
|
-
new Response(null, {
|
|
482
|
-
status: 302,
|
|
483
|
-
headers: { Location: "http://169.254.169.254/metadata" },
|
|
484
|
-
}),
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
488
|
-
|
|
489
|
-
// This would be blocked by validateUrl on the redirect target
|
|
490
|
-
// The actual blocking is tested in web.test.ts SSRF redirect protection tests
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
describe("Response streaming and size limits", () => {
|
|
495
|
-
it("should stream response body and enforce byte limits", async () => {
|
|
496
|
-
// Create a response with chunked encoding (no Content-Length)
|
|
497
|
-
const chunks = ["chunk1", "chunk2", "chunk3"];
|
|
498
|
-
let chunkIndex = 0;
|
|
499
|
-
|
|
500
|
-
const stream = new ReadableStream({
|
|
501
|
-
async pull(controller) {
|
|
502
|
-
if (chunkIndex < chunks.length) {
|
|
503
|
-
controller.enqueue(new TextEncoder().encode(chunks[chunkIndex]!));
|
|
504
|
-
chunkIndex++;
|
|
505
|
-
} else {
|
|
506
|
-
controller.close();
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
const mockResponse = new Response(stream, {
|
|
512
|
-
status: 200,
|
|
513
|
-
headers: { "Content-Type": "text/plain" },
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
|
|
517
|
-
|
|
518
|
-
const result = await executeWebFetch(
|
|
519
|
-
{ url: "https://example.com/chunked", maxSizeBytes: 100 },
|
|
520
|
-
ctx,
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
expect(result.success).toBe(true);
|
|
524
|
-
const data = result.data as { content: string };
|
|
525
|
-
// Content is now wrapped with security boundaries but should contain all chunks
|
|
526
|
-
expect(data.content).toContain("chunk1chunk2chunk3");
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
describe("Custom headers and request options", () => {
|
|
531
|
-
it("should pass custom headers to fetch", async () => {
|
|
532
|
-
const mockFetch = vi.fn().mockResolvedValue(
|
|
533
|
-
new Response("ok", {
|
|
534
|
-
status: 200,
|
|
535
|
-
}),
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
539
|
-
|
|
540
|
-
const result = await executeWebFetch(
|
|
541
|
-
{
|
|
542
|
-
url: "https://api.example.com/endpoint",
|
|
543
|
-
headers: {
|
|
544
|
-
Authorization: "Bearer token123",
|
|
545
|
-
"X-Custom-Header": "custom-value",
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
ctx,
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
expect(result.success).toBe(true);
|
|
552
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
553
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
554
|
-
"https://api.example.com/endpoint",
|
|
555
|
-
expect.objectContaining({
|
|
556
|
-
headers: {
|
|
557
|
-
Authorization: "Bearer token123",
|
|
558
|
-
"X-Custom-Header": "custom-value",
|
|
559
|
-
},
|
|
560
|
-
redirect: "manual",
|
|
561
|
-
}),
|
|
562
|
-
);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
it("should respect custom timeout values", async () => {
|
|
566
|
-
let timeoutReceived = 0;
|
|
567
|
-
|
|
568
|
-
vi.stubGlobal(
|
|
569
|
-
"fetch",
|
|
570
|
-
vi.fn().mockImplementation((_url: string, options?: RequestInit) => {
|
|
571
|
-
return new Promise((_resolve, reject) => {
|
|
572
|
-
const signal = options?.signal;
|
|
573
|
-
if (signal) {
|
|
574
|
-
signal.addEventListener("abort", () => {
|
|
575
|
-
timeoutReceived = Date.now();
|
|
576
|
-
const abortError = new Error("The operation was aborted");
|
|
577
|
-
abortError.name = "AbortError";
|
|
578
|
-
reject(abortError);
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
}),
|
|
583
|
-
);
|
|
584
|
-
|
|
585
|
-
const startTime = Date.now();
|
|
586
|
-
const result = await executeWebFetch(
|
|
587
|
-
{ url: "https://example.com/slow", timeoutMs: 200 },
|
|
588
|
-
ctx,
|
|
589
|
-
);
|
|
590
|
-
|
|
591
|
-
const elapsed = timeoutReceived - startTime;
|
|
592
|
-
|
|
593
|
-
expect(result.success).toBe(false);
|
|
594
|
-
expect(result.message).toContain("timed out");
|
|
595
|
-
expect(elapsed).toBeGreaterThanOrEqual(180); // Allow 10% margin
|
|
596
|
-
expect(elapsed).toBeLessThanOrEqual(300);
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
describe("Context abort signal integration", () => {
|
|
601
|
-
it("should cancel request when context abort signal is triggered", async () => {
|
|
602
|
-
const abortController = new AbortController();
|
|
603
|
-
const ctxWithSignal: ToolContext = {
|
|
604
|
-
...ctx,
|
|
605
|
-
abortSignal: abortController.signal,
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
vi.stubGlobal(
|
|
609
|
-
"fetch",
|
|
610
|
-
vi.fn().mockImplementation((_url: string, options?: RequestInit) => {
|
|
611
|
-
return new Promise((_resolve, reject) => {
|
|
612
|
-
const signal = options?.signal;
|
|
613
|
-
if (signal) {
|
|
614
|
-
signal.addEventListener("abort", () => {
|
|
615
|
-
const abortError = new Error("The operation was aborted");
|
|
616
|
-
abortError.name = "AbortError";
|
|
617
|
-
reject(abortError);
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
}),
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
// Trigger abort after 50ms
|
|
625
|
-
setTimeout(() => abortController.abort(), 50);
|
|
626
|
-
|
|
627
|
-
const result = await executeWebFetch({ url: "https://example.com/slow" }, ctxWithSignal);
|
|
628
|
-
|
|
629
|
-
expect(result.success).toBe(false);
|
|
630
|
-
expect(result.message).toContain("cancelled");
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @aria/tools - Web download symlink bypass tests (C2)
|
|
3
|
-
*
|
|
4
|
-
* These tests verified symlink-based path traversal protection in the
|
|
5
|
-
* download executor. The download tool was removed in aria-c3x.
|
|
6
|
-
*
|
|
7
|
-
* The underlying symlink protection (resolveWithSymlinks + isPathWithinBase)
|
|
8
|
-
* is still used by other filesystem executors and is tested elsewhere.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it } from "vitest";
|
|
12
|
-
|
|
13
|
-
describe("Web download symlink bypass protection", () => {
|
|
14
|
-
it("download tool removed in aria-c3x — symlink protection tested via filesystem executors", () => {
|
|
15
|
-
// All download-specific symlink tests removed.
|
|
16
|
-
// resolveWithSymlinks + isPathWithinBase are still covered in filesystem executor tests.
|
|
17
|
-
});
|
|
18
|
-
});
|