@gotza02/sequential-thinking 2026.2.7 → 2026.2.10
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/README.md +34 -24
- package/dist/codestore.js +69 -0
- package/dist/codestore.test.js +59 -0
- package/dist/filesystem.test.js +48 -0
- package/dist/graph.js +13 -5
- package/dist/graph.test.js +53 -0
- package/dist/index.js +4 -0
- package/dist/integration.test.js +58 -0
- package/dist/lib.js +29 -4
- package/dist/notes.js +65 -43
- package/dist/notes.test.js +74 -0
- package/dist/registration.test.js +39 -0
- package/dist/repro_dollar.js +30 -0
- package/dist/repro_dollar_simple.js +22 -0
- package/dist/repro_history.js +41 -0
- package/dist/repro_path.js +17 -0
- package/dist/repro_ts_req.js +3 -0
- package/dist/server.test.js +32 -0
- package/dist/stress.test.js +68 -0
- package/dist/system_test.js +51 -0
- package/dist/test_ts_req.js +46 -0
- package/dist/tools/codestore.js +51 -0
- package/dist/tools/coding.js +15 -1
- package/dist/tools/filesystem.js +12 -9
- package/dist/tools/graph.js +3 -1
- package/dist/tools/notes.js +9 -6
- package/dist/tools/thinking.js +7 -0
- package/dist/tools/web.js +7 -2
- package/dist/utils.js +103 -2
- package/dist/web_read.test.js +60 -0
- package/package.json +1 -1
package/dist/tools/coding.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { validatePath } from "../utils.js";
|
|
4
5
|
export function registerCodingTools(server, graph) {
|
|
5
6
|
// 14. deep_code_analyze
|
|
6
7
|
server.tool("deep_code_analyze", "Generates a 'Codebase Context Document' for a specific file or task. This tool learns from the codebase structure and symbols to provide deep insights before coding.", {
|
|
@@ -8,6 +9,8 @@ export function registerCodingTools(server, graph) {
|
|
|
8
9
|
taskDescription: z.string().optional().describe("Optional description of what you intend to do")
|
|
9
10
|
}, async ({ filePath, taskDescription }) => {
|
|
10
11
|
try {
|
|
12
|
+
// Ensure path is safe before checking graph
|
|
13
|
+
validatePath(filePath);
|
|
11
14
|
const context = graph.getDeepContext(filePath);
|
|
12
15
|
if (!context) {
|
|
13
16
|
return { content: [{ type: "text", text: `Error: File '${filePath}' not found in graph. Run 'build_project_graph' first.` }], isError: true };
|
|
@@ -44,11 +47,22 @@ export function registerCodingTools(server, graph) {
|
|
|
44
47
|
reasoning: z.string().describe("The 'Deepest Thinking' reasoning behind this change")
|
|
45
48
|
}, async ({ path: filePath, oldText, newText, reasoning }) => {
|
|
46
49
|
try {
|
|
47
|
-
const absolutePath =
|
|
50
|
+
const absolutePath = validatePath(filePath);
|
|
48
51
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
49
52
|
if (!content.includes(oldText)) {
|
|
50
53
|
return { content: [{ type: "text", text: "Error: Target text not found. Ensure exact match including whitespace." }], isError: true };
|
|
51
54
|
}
|
|
55
|
+
// Safety Check: Ensure unique match
|
|
56
|
+
const occurrences = content.split(oldText).length - 1;
|
|
57
|
+
if (occurrences > 1) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Error: Ambiguous match. 'oldText' was found ${occurrences} times. Please provide more surrounding context (lines before/after) to identify the unique location.`
|
|
62
|
+
}],
|
|
63
|
+
isError: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
52
66
|
const newContent = content.replace(oldText, newText);
|
|
53
67
|
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
|
54
68
|
return {
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { execAsync } from "../utils.js";
|
|
4
|
+
import { execAsync, validatePath } from "../utils.js";
|
|
5
5
|
export function registerFileSystemTools(server) {
|
|
6
6
|
// 3. shell_execute
|
|
7
7
|
server.tool("shell_execute", "Execute a shell command. SECURITY WARNING: Use this ONLY for safe, non-destructive commands. Avoid 'rm -rf /', format, or destructive operations.", {
|
|
@@ -35,7 +35,8 @@ export function registerFileSystemTools(server) {
|
|
|
35
35
|
path: z.string().describe("Path to the file")
|
|
36
36
|
}, async ({ path }) => {
|
|
37
37
|
try {
|
|
38
|
-
const
|
|
38
|
+
const safePath = validatePath(path);
|
|
39
|
+
const content = await fs.readFile(safePath, 'utf-8');
|
|
39
40
|
return {
|
|
40
41
|
content: [{ type: "text", text: content }]
|
|
41
42
|
};
|
|
@@ -53,9 +54,10 @@ export function registerFileSystemTools(server) {
|
|
|
53
54
|
content: z.string().describe("Content to write")
|
|
54
55
|
}, async ({ path, content }) => {
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
+
const safePath = validatePath(path);
|
|
58
|
+
await fs.writeFile(safePath, content, 'utf-8');
|
|
57
59
|
return {
|
|
58
|
-
content: [{ type: "text", text: `Successfully wrote to ${
|
|
60
|
+
content: [{ type: "text", text: `Successfully wrote to ${safePath}` }]
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
catch (error) {
|
|
@@ -71,7 +73,7 @@ export function registerFileSystemTools(server) {
|
|
|
71
73
|
path: z.string().optional().default('.').describe("Root directory to search")
|
|
72
74
|
}, async ({ pattern, path: searchPath }) => {
|
|
73
75
|
try {
|
|
74
|
-
const resolvedPath =
|
|
76
|
+
const resolvedPath = validatePath(searchPath || '.');
|
|
75
77
|
const stats = await fs.stat(resolvedPath);
|
|
76
78
|
if (stats.isFile()) {
|
|
77
79
|
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
@@ -125,7 +127,8 @@ export function registerFileSystemTools(server) {
|
|
|
125
127
|
allowMultiple: z.boolean().optional().default(false).describe("Allow replacing multiple occurrences (default: false)")
|
|
126
128
|
}, async ({ path, oldText, newText, allowMultiple }) => {
|
|
127
129
|
try {
|
|
128
|
-
const
|
|
130
|
+
const safePath = validatePath(path);
|
|
131
|
+
const content = await fs.readFile(safePath, 'utf-8');
|
|
129
132
|
// Check occurrences
|
|
130
133
|
// Escape special regex characters in oldText to treat it as literal string
|
|
131
134
|
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
@@ -143,10 +146,10 @@ export function registerFileSystemTools(server) {
|
|
|
143
146
|
isError: true
|
|
144
147
|
};
|
|
145
148
|
}
|
|
146
|
-
const newContent = content.replace(allowMultiple ? regex : oldText, newText);
|
|
147
|
-
await fs.writeFile(
|
|
149
|
+
const newContent = content.replace(allowMultiple ? regex : oldText, () => newText);
|
|
150
|
+
await fs.writeFile(safePath, newContent, 'utf-8');
|
|
148
151
|
return {
|
|
149
|
-
content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${
|
|
152
|
+
content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${safePath}` }]
|
|
150
153
|
};
|
|
151
154
|
}
|
|
152
155
|
catch (error) {
|
package/dist/tools/graph.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { validatePath } from "../utils.js";
|
|
2
3
|
export function registerGraphTools(server, knowledgeGraph) {
|
|
3
4
|
// 6. build_project_graph
|
|
4
5
|
server.tool("build_project_graph", "Scan the directory and build a dependency graph of the project (Analyzing imports/exports).", {
|
|
5
6
|
path: z.string().optional().default('.').describe("Root directory path to scan (default: current dir)")
|
|
6
7
|
}, async ({ path }) => {
|
|
7
8
|
try {
|
|
8
|
-
const
|
|
9
|
+
const safePath = validatePath(path || '.');
|
|
10
|
+
const result = await knowledgeGraph.build(safePath);
|
|
9
11
|
return {
|
|
10
12
|
content: [{ type: "text", text: `Graph built successfully.\nNodes: ${result.nodeCount}\nTotal Scanned Files: ${result.totalFiles}` }]
|
|
11
13
|
};
|
package/dist/tools/notes.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerNoteTools(server, notesManager) {
|
|
3
3
|
// 15. manage_notes
|
|
4
|
-
server.tool("manage_notes", "Manage long-term memory/notes. Use this to save important information, rules, or learnings that should persist across sessions.", {
|
|
4
|
+
server.tool("manage_notes", "Manage long-term memory/notes. Use this to save important information, rules, or learnings that should persist across sessions. Supports priority levels and expiration dates.", {
|
|
5
5
|
action: z.enum(['add', 'list', 'search', 'update', 'delete']).describe("Action to perform"),
|
|
6
6
|
title: z.string().optional().describe("Title of the note (for add/update)"),
|
|
7
7
|
content: z.string().optional().describe("Content of the note (for add/update)"),
|
|
8
8
|
tags: z.array(z.string()).optional().describe("Tags for categorization (for add/update)"),
|
|
9
9
|
searchQuery: z.string().optional().describe("Query to search notes (for search)"),
|
|
10
|
-
noteId: z.string().optional().describe("ID of the note (for update/delete)")
|
|
11
|
-
|
|
10
|
+
noteId: z.string().optional().describe("ID of the note (for update/delete)"),
|
|
11
|
+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional().default('medium').describe("Priority level"),
|
|
12
|
+
expiresAt: z.string().optional().describe("Expiration date in ISO format (e.g., 2026-12-31)"),
|
|
13
|
+
includeExpired: z.boolean().optional().default(false).describe("Whether to include expired notes in list")
|
|
14
|
+
}, async ({ action, title, content, tags, searchQuery, noteId, priority, expiresAt, includeExpired }) => {
|
|
12
15
|
try {
|
|
13
16
|
switch (action) {
|
|
14
17
|
case 'add':
|
|
15
18
|
if (!title || !content) {
|
|
16
19
|
return { content: [{ type: "text", text: "Error: 'title' and 'content' are required for add action." }], isError: true };
|
|
17
20
|
}
|
|
18
|
-
const newNote = await notesManager.addNote(title, content, tags);
|
|
19
|
-
return { content: [{ type: "text", text: `Note added successfully.\nID: ${newNote.id}` }] };
|
|
21
|
+
const newNote = await notesManager.addNote(title, content, tags, priority, expiresAt);
|
|
22
|
+
return { content: [{ type: "text", text: `Note added successfully.\nID: ${newNote.id} (Priority: ${newNote.priority})` }] };
|
|
20
23
|
case 'list':
|
|
21
|
-
const notes = await notesManager.listNotes();
|
|
24
|
+
const notes = await notesManager.listNotes(undefined, includeExpired);
|
|
22
25
|
return { content: [{ type: "text", text: JSON.stringify(notes, null, 2) }] };
|
|
23
26
|
case 'search':
|
|
24
27
|
if (!searchQuery) {
|
package/dist/tools/thinking.js
CHANGED
|
@@ -27,6 +27,13 @@ Key features:
|
|
|
27
27
|
- Hypothesis Testing: Formulate and verify hypotheses based on Chain of Thought.
|
|
28
28
|
- Branch Merging: Combine insights from multiple divergent paths.
|
|
29
29
|
|
|
30
|
+
Troubleshooting & Stuck States:
|
|
31
|
+
- If you realize you are stuck in a loop or a bug persists after 2 attempts:
|
|
32
|
+
1. Do NOT continue linear thoughts.
|
|
33
|
+
2. Create a NEW BRANCH ('branchFromThought') from a point before the error.
|
|
34
|
+
3. Explicitly state "Stuck detected, branching to explore Approach B".
|
|
35
|
+
4. Change your fundamental assumptions or implementation strategy completely.
|
|
36
|
+
|
|
30
37
|
Parameters explained:
|
|
31
38
|
- thought: Your current thinking step (analysis, revision, question, hypothesis).
|
|
32
39
|
- nextThoughtNeeded: True if you need more thinking, even if at what seemed like the end.
|
package/dist/tools/web.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { fetchWithRetry } from "../utils.js";
|
|
2
|
+
import { fetchWithRetry, validatePublicUrl } from "../utils.js";
|
|
3
3
|
import { JSDOM } from 'jsdom';
|
|
4
4
|
import { Readability } from '@mozilla/readability';
|
|
5
5
|
import TurndownService from 'turndown';
|
|
@@ -102,6 +102,7 @@ export function registerWebTools(server) {
|
|
|
102
102
|
body: z.string().optional().describe("Request body (for POST/PUT)")
|
|
103
103
|
}, async ({ url, method, headers, body }) => {
|
|
104
104
|
try {
|
|
105
|
+
await validatePublicUrl(url);
|
|
105
106
|
const response = await fetchWithRetry(url, {
|
|
106
107
|
method,
|
|
107
108
|
headers: headers || {},
|
|
@@ -127,6 +128,7 @@ export function registerWebTools(server) {
|
|
|
127
128
|
url: z.string().url().describe("The URL to read")
|
|
128
129
|
}, async ({ url }) => {
|
|
129
130
|
try {
|
|
131
|
+
await validatePublicUrl(url);
|
|
130
132
|
const response = await fetchWithRetry(url);
|
|
131
133
|
const html = await response.text();
|
|
132
134
|
const doc = new JSDOM(html, { url });
|
|
@@ -135,7 +137,10 @@ export function registerWebTools(server) {
|
|
|
135
137
|
if (!article)
|
|
136
138
|
throw new Error("Could not parse article content");
|
|
137
139
|
const turndownService = new TurndownService();
|
|
138
|
-
|
|
140
|
+
let markdown = turndownService.turndown(article.content || "");
|
|
141
|
+
if (markdown.length > 20000) {
|
|
142
|
+
markdown = markdown.substring(0, 20000) + "\n...(truncated)";
|
|
143
|
+
}
|
|
139
144
|
return {
|
|
140
145
|
content: [{
|
|
141
146
|
type: "text",
|
package/dist/utils.js
CHANGED
|
@@ -1,7 +1,108 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
|
-
import
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as dns from 'dns/promises';
|
|
4
|
+
import { URL } from 'url';
|
|
3
5
|
import chalk from 'chalk';
|
|
4
|
-
export const execAsync = promisify(exec);
|
|
6
|
+
// export const execAsync = promisify(exec); // Removed simple promisify
|
|
7
|
+
export function execAsync(command, options = {}) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const timeout = options.timeout || 60000; // Default 60s timeout
|
|
10
|
+
const maxBuffer = options.maxBuffer || 1024 * 1024 * 5; // Default 5MB buffer
|
|
11
|
+
exec(command, { timeout, maxBuffer }, (error, stdout, stderr) => {
|
|
12
|
+
if (error) {
|
|
13
|
+
// Attach stdout/stderr to error for better debugging
|
|
14
|
+
error.stdout = stdout;
|
|
15
|
+
error.stderr = stderr;
|
|
16
|
+
reject(error);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
resolve({ stdout, stderr });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export class AsyncMutex {
|
|
24
|
+
mutex = Promise.resolve();
|
|
25
|
+
lock() {
|
|
26
|
+
let unlock = () => { };
|
|
27
|
+
const nextLock = new Promise(resolve => {
|
|
28
|
+
unlock = resolve;
|
|
29
|
+
});
|
|
30
|
+
// The caller waits for the previous lock to release
|
|
31
|
+
const willLock = this.mutex.then(() => unlock);
|
|
32
|
+
// The next caller will wait for this lock to release
|
|
33
|
+
this.mutex = nextLock;
|
|
34
|
+
return willLock;
|
|
35
|
+
}
|
|
36
|
+
async dispatch(fn) {
|
|
37
|
+
const unlock = await this.lock();
|
|
38
|
+
try {
|
|
39
|
+
return await Promise.resolve(fn());
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
unlock();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function validatePath(requestedPath) {
|
|
47
|
+
const absolutePath = path.resolve(requestedPath);
|
|
48
|
+
const rootDir = process.cwd();
|
|
49
|
+
if (!absolutePath.startsWith(rootDir)) {
|
|
50
|
+
throw new Error(`Access denied: Path '${requestedPath}' is outside the project root.`);
|
|
51
|
+
}
|
|
52
|
+
return absolutePath;
|
|
53
|
+
}
|
|
54
|
+
function isPrivateIP(ip) {
|
|
55
|
+
// IPv4 ranges
|
|
56
|
+
// 127.0.0.0/8
|
|
57
|
+
// 10.0.0.0/8
|
|
58
|
+
// 172.16.0.0/12
|
|
59
|
+
// 192.168.0.0/16
|
|
60
|
+
// 0.0.0.0/8
|
|
61
|
+
const parts = ip.split('.').map(Number);
|
|
62
|
+
if (parts.length === 4) {
|
|
63
|
+
if (parts[0] === 127)
|
|
64
|
+
return true;
|
|
65
|
+
if (parts[0] === 10)
|
|
66
|
+
return true;
|
|
67
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
68
|
+
return true;
|
|
69
|
+
if (parts[0] === 192 && parts[1] === 168)
|
|
70
|
+
return true;
|
|
71
|
+
if (parts[0] === 0)
|
|
72
|
+
return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// IPv6 checks (simple check for loopback/link-local)
|
|
76
|
+
if (ip === '::1' || ip === '::')
|
|
77
|
+
return true;
|
|
78
|
+
if (ip.startsWith('fc') || ip.startsWith('fd'))
|
|
79
|
+
return true; // Unique Local
|
|
80
|
+
if (ip.startsWith('fe80'))
|
|
81
|
+
return true; // Link Local
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
export async function validatePublicUrl(urlString) {
|
|
85
|
+
const parsed = new URL(urlString);
|
|
86
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
87
|
+
throw new Error('Invalid protocol. Only http and https are allowed.');
|
|
88
|
+
}
|
|
89
|
+
// Attempt to resolve hostname
|
|
90
|
+
try {
|
|
91
|
+
const addresses = await dns.lookup(parsed.hostname, { all: true });
|
|
92
|
+
for (const addr of addresses) {
|
|
93
|
+
if (isPrivateIP(addr.address)) {
|
|
94
|
+
throw new Error(`Access denied: Host '${parsed.hostname}' resolves to private IP ${addr.address}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// If it's the specific access denied error, rethrow
|
|
100
|
+
if (error instanceof Error && error.message.startsWith('Access denied')) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
// Ignore DNS errors here, let fetch handle them (or fail safely)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
5
106
|
class Logger {
|
|
6
107
|
level;
|
|
7
108
|
constructor() {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { registerWebTools } from './tools/web.js';
|
|
3
|
+
import * as utils from './utils.js';
|
|
4
|
+
vi.mock('./utils.js', async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal();
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
fetchWithRetry: vi.fn(),
|
|
9
|
+
validatePublicUrl: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
describe('read_webpage tool', () => {
|
|
13
|
+
let mockToolCallback;
|
|
14
|
+
const mockServer = {
|
|
15
|
+
tool: vi.fn((name, desc, schema, callback) => {
|
|
16
|
+
if (name === 'read_webpage') {
|
|
17
|
+
mockToolCallback = callback;
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
};
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
registerWebTools(mockServer);
|
|
24
|
+
});
|
|
25
|
+
it('should convert HTML to Markdown', async () => {
|
|
26
|
+
const mockHtml = `
|
|
27
|
+
<html>
|
|
28
|
+
<head><title>Test Article</title></head>
|
|
29
|
+
<body>
|
|
30
|
+
<h1>Main Header</h1>
|
|
31
|
+
<p>Paragraph <b>bold</b>.</p>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
`;
|
|
35
|
+
utils.fetchWithRetry.mockResolvedValue({
|
|
36
|
+
ok: true,
|
|
37
|
+
text: async () => mockHtml
|
|
38
|
+
});
|
|
39
|
+
utils.validatePublicUrl.mockResolvedValue(undefined);
|
|
40
|
+
const result = await mockToolCallback({ url: 'https://example.com' });
|
|
41
|
+
expect(result.isError).toBeUndefined();
|
|
42
|
+
const content = result.content[0].text;
|
|
43
|
+
expect(content).toContain("Title: Test Article");
|
|
44
|
+
expect(content).toContain("Main Header");
|
|
45
|
+
expect(content).toContain("**bold**"); // Markdown bold
|
|
46
|
+
});
|
|
47
|
+
it('should handle private URL validation error', async () => {
|
|
48
|
+
utils.validatePublicUrl.mockRejectedValue(new Error("Access denied: Private IP"));
|
|
49
|
+
const result = await mockToolCallback({ url: 'http://localhost' });
|
|
50
|
+
expect(result.isError).toBe(true);
|
|
51
|
+
expect(result.content[0].text).toContain("Access denied");
|
|
52
|
+
});
|
|
53
|
+
it('should handle fetch errors', async () => {
|
|
54
|
+
utils.validatePublicUrl.mockResolvedValue(undefined);
|
|
55
|
+
utils.fetchWithRetry.mockRejectedValue(new Error("Network Error"));
|
|
56
|
+
const result = await mockToolCallback({ url: 'https://example.com' });
|
|
57
|
+
expect(result.isError).toBe(true);
|
|
58
|
+
expect(result.content[0].text).toContain("Network Error");
|
|
59
|
+
});
|
|
60
|
+
});
|