@esankhan3/anvil-agent-core 0.1.2 → 0.3.0
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/dist/__tests__/fetch-pool.integration.test.d.ts +15 -0
- package/dist/__tests__/fetch-pool.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-pool.integration.test.js +109 -0
- package/dist/__tests__/fetch-pool.integration.test.js.map +1 -0
- package/dist/__tests__/fetch-pool.test.d.ts +15 -0
- package/dist/__tests__/fetch-pool.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-pool.test.js +115 -0
- package/dist/__tests__/fetch-pool.test.js.map +1 -0
- package/dist/__tests__/mcp-merged-executor.test.d.ts +2 -0
- package/dist/__tests__/mcp-merged-executor.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-merged-executor.test.js +221 -0
- package/dist/__tests__/mcp-merged-executor.test.js.map +1 -0
- package/dist/__tests__/mcp.test.js +21 -15
- package/dist/__tests__/mcp.test.js.map +1 -1
- package/dist/__tests__/model-catalog.test.d.ts +11 -0
- package/dist/__tests__/model-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/model-catalog.test.js +102 -0
- package/dist/__tests__/model-catalog.test.js.map +1 -0
- package/dist/__tests__/model-tier-resolver.test.d.ts +19 -0
- package/dist/__tests__/model-tier-resolver.test.d.ts.map +1 -0
- package/dist/__tests__/model-tier-resolver.test.js +97 -0
- package/dist/__tests__/model-tier-resolver.test.js.map +1 -0
- package/dist/adk-adapter.d.ts.map +1 -1
- package/dist/adk-adapter.js +11 -0
- package/dist/adk-adapter.js.map +1 -1
- package/dist/adk-anthropic-llm.d.ts.map +1 -1
- package/dist/adk-anthropic-llm.js +23 -10
- package/dist/adk-anthropic-llm.js.map +1 -1
- package/dist/agent/session/__tests__/spawn-telemetry.test.js +39 -4
- package/dist/agent/session/__tests__/spawn-telemetry.test.js.map +1 -1
- package/dist/agent/session/adapter.d.ts +44 -0
- package/dist/agent/session/adapter.d.ts.map +1 -1
- package/dist/agent/session/adapter.js +1 -0
- package/dist/agent/session/adapter.js.map +1 -1
- package/dist/agent/session/default-adapter-factory.d.ts.map +1 -1
- package/dist/agent/session/default-adapter-factory.js +66 -2
- package/dist/agent/session/default-adapter-factory.js.map +1 -1
- package/dist/agent/session/language-model-bridge.d.ts.map +1 -1
- package/dist/agent/session/language-model-bridge.js +59 -2
- package/dist/agent/session/language-model-bridge.js.map +1 -1
- package/dist/agent/session/session.d.ts +16 -0
- package/dist/agent/session/session.d.ts.map +1 -1
- package/dist/agent/session/session.js +86 -2
- package/dist/agent/session/session.js.map +1 -1
- package/dist/agent/session/types.d.ts +12 -0
- package/dist/agent/session/types.d.ts.map +1 -1
- package/dist/agent/session/types.js.map +1 -1
- package/dist/fetch-pool.d.ts +47 -0
- package/dist/fetch-pool.d.ts.map +1 -0
- package/dist/fetch-pool.js +132 -0
- package/dist/fetch-pool.js.map +1 -0
- package/dist/gemini-adapter.d.ts.map +1 -1
- package/dist/gemini-adapter.js +30 -6
- package/dist/gemini-adapter.js.map +1 -1
- package/dist/gemini-cli-adapter.d.ts.map +1 -1
- package/dist/gemini-cli-adapter.js +10 -0
- package/dist/gemini-cli-adapter.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +64 -18
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +190 -26
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +3 -8
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/merged-executor.d.ts +74 -0
- package/dist/mcp/merged-executor.d.ts.map +1 -0
- package/dist/mcp/merged-executor.js +198 -0
- package/dist/mcp/merged-executor.js.map +1 -0
- package/dist/mcp/pool.d.ts +71 -0
- package/dist/mcp/pool.d.ts.map +1 -0
- package/dist/mcp/pool.js +171 -0
- package/dist/mcp/pool.js.map +1 -0
- package/dist/mcp/tool-merger.d.ts +8 -5
- package/dist/mcp/tool-merger.d.ts.map +1 -1
- package/dist/mcp/tool-merger.js +8 -16
- package/dist/mcp/tool-merger.js.map +1 -1
- package/dist/model-catalog.d.ts +36 -0
- package/dist/model-catalog.d.ts.map +1 -0
- package/dist/model-catalog.js +102 -0
- package/dist/model-catalog.js.map +1 -0
- package/dist/model-tier-resolver.d.ts +61 -0
- package/dist/model-tier-resolver.d.ts.map +1 -0
- package/dist/model-tier-resolver.js +170 -0
- package/dist/model-tier-resolver.js.map +1 -0
- package/dist/ollama-adapter.d.ts.map +1 -1
- package/dist/ollama-adapter.js +33 -8
- package/dist/ollama-adapter.js.map +1 -1
- package/dist/opencode-adapter.d.ts +11 -0
- package/dist/opencode-adapter.d.ts.map +1 -1
- package/dist/opencode-adapter.js +13 -0
- package/dist/opencode-adapter.js.map +1 -1
- package/dist/openrouter-adapter.d.ts +15 -0
- package/dist/openrouter-adapter.d.ts.map +1 -1
- package/dist/openrouter-adapter.js +76 -19
- package/dist/openrouter-adapter.js.map +1 -1
- package/dist/provider-liveness.d.ts +5 -0
- package/dist/provider-liveness.d.ts.map +1 -1
- package/dist/provider-liveness.js +28 -21
- package/dist/provider-liveness.js.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/router/local-executor.d.ts.map +1 -1
- package/dist/router/local-executor.js +19 -6
- package/dist/router/local-executor.js.map +1 -1
- package/dist/single-shot.d.ts.map +1 -1
- package/dist/single-shot.js +26 -1
- package/dist/single-shot.js.map +1 -1
- package/dist/tools/builtin.d.ts +19 -0
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +51 -0
- package/dist/tools/builtin.js.map +1 -1
- package/dist/upstream-error.d.ts.map +1 -1
- package/dist/upstream-error.js +11 -1
- package/dist/upstream-error.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test — proves the heal-on-failure fix actually works.
|
|
3
|
+
*
|
|
4
|
+
* Without the fix, a poisoned pool fails every subsequent fetch with the
|
|
5
|
+
* same TypeError until the process restarts. With the fix:
|
|
6
|
+
* - call 1 succeeds against a live server
|
|
7
|
+
* - server dies → call 2 fails (TypeError: fetch failed)
|
|
8
|
+
* - recycleFetchPoolOnFailure() runs
|
|
9
|
+
* - server comes back → call 3 succeeds against the healed pool
|
|
10
|
+
*
|
|
11
|
+
* This is the test the plan in docs/FETCH-POOL-MANAGEMENT-PLAN.md §6.2
|
|
12
|
+
* specifies as the canary for the entire fix.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=fetch-pool.integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test — proves the heal-on-failure fix actually works.
|
|
3
|
+
*
|
|
4
|
+
* Without the fix, a poisoned pool fails every subsequent fetch with the
|
|
5
|
+
* same TypeError until the process restarts. With the fix:
|
|
6
|
+
* - call 1 succeeds against a live server
|
|
7
|
+
* - server dies → call 2 fails (TypeError: fetch failed)
|
|
8
|
+
* - recycleFetchPoolOnFailure() runs
|
|
9
|
+
* - server comes back → call 3 succeeds against the healed pool
|
|
10
|
+
*
|
|
11
|
+
* This is the test the plan in docs/FETCH-POOL-MANAGEMENT-PLAN.md §6.2
|
|
12
|
+
* specifies as the canary for the entire fix.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import { getFetchPool, recycleFetchPoolOnFailure, getPoolMetrics, resetAllPools, } from '../fetch-pool.js';
|
|
18
|
+
describe('fetch-pool integration — heal-on-failure end-to-end', () => {
|
|
19
|
+
let server = null;
|
|
20
|
+
let port = 0;
|
|
21
|
+
async function startServer() {
|
|
22
|
+
server = http.createServer((req, res) => {
|
|
23
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
24
|
+
res.end(JSON.stringify({ ok: true, path: req.url }));
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolve) => {
|
|
27
|
+
server.listen(port, '127.0.0.1', () => {
|
|
28
|
+
port = server.address().port;
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function stopServer() {
|
|
34
|
+
if (!server)
|
|
35
|
+
return;
|
|
36
|
+
await new Promise((resolve) => {
|
|
37
|
+
server.close(() => resolve());
|
|
38
|
+
// Force-close any sockets the pool may be holding so the close()
|
|
39
|
+
// callback fires promptly.
|
|
40
|
+
server.closeAllConnections?.();
|
|
41
|
+
});
|
|
42
|
+
server = null;
|
|
43
|
+
}
|
|
44
|
+
before(async () => {
|
|
45
|
+
await startServer();
|
|
46
|
+
});
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
await resetAllPools();
|
|
49
|
+
});
|
|
50
|
+
after(async () => {
|
|
51
|
+
await stopServer();
|
|
52
|
+
await resetAllPools();
|
|
53
|
+
});
|
|
54
|
+
it('healed pool succeeds after a network failure poisons the old pool', async () => {
|
|
55
|
+
const url = `http://127.0.0.1:${port}/v1/messages`;
|
|
56
|
+
// 1. First call succeeds against a live server.
|
|
57
|
+
const r1 = await fetch(url, {
|
|
58
|
+
// @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
|
|
59
|
+
dispatcher: getFetchPool('anthropic'),
|
|
60
|
+
});
|
|
61
|
+
assert.strictEqual(r1.status, 200);
|
|
62
|
+
await r1.text();
|
|
63
|
+
// 2. Kill the server. The pool now holds zombie sockets pointed at a
|
|
64
|
+
// dead listener — every subsequent fetch through the SAME dispatcher
|
|
65
|
+
// fails with `TypeError: fetch failed`.
|
|
66
|
+
await stopServer();
|
|
67
|
+
let caught = null;
|
|
68
|
+
try {
|
|
69
|
+
await fetch(url, {
|
|
70
|
+
// @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
|
|
71
|
+
dispatcher: getFetchPool('anthropic'),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
caught = err;
|
|
76
|
+
}
|
|
77
|
+
assert.ok(caught instanceof Error, 'expected fetch to throw with server down');
|
|
78
|
+
assert.match(caught.message + ' ' + (caught.cause instanceof Error ? caught.cause.message : ''), /fetch failed|ECONNREFUSED|ECONNRESET|socket/i);
|
|
79
|
+
// 3. Recycle the pool — this is what the adapter's catch block does
|
|
80
|
+
// in production.
|
|
81
|
+
await recycleFetchPoolOnFailure('anthropic', caught);
|
|
82
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'anthropic');
|
|
83
|
+
assert.strictEqual(metrics?.recycleCount, 1, 'pool should have been recycled');
|
|
84
|
+
// 4. Bring the server back on the same port.
|
|
85
|
+
await startServer();
|
|
86
|
+
// 5. The next fetch must succeed. Without the recycle, the same zombie
|
|
87
|
+
// socket would be re-used and this would fail again.
|
|
88
|
+
const r3 = await fetch(url, {
|
|
89
|
+
// @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
|
|
90
|
+
dispatcher: getFetchPool('anthropic'),
|
|
91
|
+
});
|
|
92
|
+
assert.strictEqual(r3.status, 200, 'healed pool must serve the next request');
|
|
93
|
+
const body = (await r3.json());
|
|
94
|
+
assert.strictEqual(body.ok, true);
|
|
95
|
+
});
|
|
96
|
+
it('one provider failing does not recycle a sibling provider', async () => {
|
|
97
|
+
// Touch both pools so they exist.
|
|
98
|
+
getFetchPool('anthropic');
|
|
99
|
+
getFetchPool('gemini');
|
|
100
|
+
// Poison only one.
|
|
101
|
+
await recycleFetchPoolOnFailure('anthropic', new TypeError('fetch failed'));
|
|
102
|
+
const metrics = getPoolMetrics();
|
|
103
|
+
const anthropic = metrics.find((m) => m.provider === 'anthropic');
|
|
104
|
+
const gemini = metrics.find((m) => m.provider === 'gemini');
|
|
105
|
+
assert.strictEqual(anthropic?.recycleCount, 1);
|
|
106
|
+
assert.strictEqual(gemini?.recycleCount, 0, 'sibling provider must be untouched');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=fetch-pool.integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.integration.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for fetch-pool — per-provider dispatcher with heal-on-failure.
|
|
3
|
+
*
|
|
4
|
+
* Covers the eight contracts from FETCH-POOL-MANAGEMENT-PLAN.md §6.1:
|
|
5
|
+
* a) Lazy construction returns a stable instance per provider
|
|
6
|
+
* b) Per-provider isolation — different providers get different agents
|
|
7
|
+
* c) Bounded options applied at construction
|
|
8
|
+
* d) Recycle on poisoned-error pattern swaps the agent
|
|
9
|
+
* e) No recycle on non-poisoned errors (e.g. 400, 401, 503)
|
|
10
|
+
* f) Concurrent recycles coalesce to one bump
|
|
11
|
+
* g) Metrics shape is well-formed
|
|
12
|
+
* h) resetAllPools clears state cleanly
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=fetch-pool.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for fetch-pool — per-provider dispatcher with heal-on-failure.
|
|
3
|
+
*
|
|
4
|
+
* Covers the eight contracts from FETCH-POOL-MANAGEMENT-PLAN.md §6.1:
|
|
5
|
+
* a) Lazy construction returns a stable instance per provider
|
|
6
|
+
* b) Per-provider isolation — different providers get different agents
|
|
7
|
+
* c) Bounded options applied at construction
|
|
8
|
+
* d) Recycle on poisoned-error pattern swaps the agent
|
|
9
|
+
* e) No recycle on non-poisoned errors (e.g. 400, 401, 503)
|
|
10
|
+
* f) Concurrent recycles coalesce to one bump
|
|
11
|
+
* g) Metrics shape is well-formed
|
|
12
|
+
* h) resetAllPools clears state cleanly
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { Agent } from 'undici';
|
|
17
|
+
import { getFetchPool, recycleFetchPoolOnFailure, getPoolMetrics, resetAllPools, } from '../fetch-pool.js';
|
|
18
|
+
describe('fetch-pool', () => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await resetAllPools();
|
|
21
|
+
});
|
|
22
|
+
it('a) lazy construction returns the same Agent on repeat calls', () => {
|
|
23
|
+
const a1 = getFetchPool('anthropic');
|
|
24
|
+
const a2 = getFetchPool('anthropic');
|
|
25
|
+
assert.ok(a1 instanceof Agent);
|
|
26
|
+
assert.strictEqual(a1, a2, 'second call should return same instance');
|
|
27
|
+
});
|
|
28
|
+
it('b) per-provider isolation — separate Agents per provider', () => {
|
|
29
|
+
const anthropic = getFetchPool('anthropic');
|
|
30
|
+
const gemini = getFetchPool('gemini');
|
|
31
|
+
const opencode = getFetchPool('opencode');
|
|
32
|
+
assert.notStrictEqual(anthropic, gemini);
|
|
33
|
+
assert.notStrictEqual(anthropic, opencode);
|
|
34
|
+
assert.notStrictEqual(gemini, opencode);
|
|
35
|
+
});
|
|
36
|
+
it('c) constructed Agent is bounded — close() resolves without hanging', async () => {
|
|
37
|
+
// Indirect bounded-keep-alive test: a fresh pool with no in-flight work
|
|
38
|
+
// closes synchronously-ish. If construction picked up unbounded defaults,
|
|
39
|
+
// tests for d/f would intermittently hang on the close() inside recycle.
|
|
40
|
+
const agent = getFetchPool('openai');
|
|
41
|
+
const closed = agent.close();
|
|
42
|
+
await Promise.race([
|
|
43
|
+
closed,
|
|
44
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('close hung beyond 500ms')), 500)),
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
it('d) recycle swaps the Agent when the error matches a poisoned pattern', async () => {
|
|
48
|
+
const before = getFetchPool('opencode');
|
|
49
|
+
await recycleFetchPoolOnFailure('opencode', new TypeError('fetch failed'));
|
|
50
|
+
const after = getFetchPool('opencode');
|
|
51
|
+
assert.notStrictEqual(after, before, 'agent should have been swapped');
|
|
52
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'opencode');
|
|
53
|
+
assert.ok(metrics);
|
|
54
|
+
assert.strictEqual(metrics.recycleCount, 1);
|
|
55
|
+
assert.ok(metrics.lastRecycleAt);
|
|
56
|
+
assert.match(metrics.lastRecycleReason ?? '', /fetch failed/);
|
|
57
|
+
});
|
|
58
|
+
it('e) does not recycle on non-poisoned errors (real upstream status)', async () => {
|
|
59
|
+
const before = getFetchPool('gemini');
|
|
60
|
+
await recycleFetchPoolOnFailure('gemini', new Error('400 Bad Request'));
|
|
61
|
+
await recycleFetchPoolOnFailure('gemini', new Error('503 Service Unavailable'));
|
|
62
|
+
await recycleFetchPoolOnFailure('gemini', new Error('quota exceeded'));
|
|
63
|
+
const after = getFetchPool('gemini');
|
|
64
|
+
assert.strictEqual(after, before, 'agent must not change on non-network errors');
|
|
65
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'gemini');
|
|
66
|
+
assert.strictEqual(metrics?.recycleCount, 0);
|
|
67
|
+
});
|
|
68
|
+
it('f) concurrent recycles coalesce — recycleCount increments once', async () => {
|
|
69
|
+
getFetchPool('ollama');
|
|
70
|
+
const err = new TypeError('fetch failed');
|
|
71
|
+
const all = await Promise.all(Array.from({ length: 10 }, () => recycleFetchPoolOnFailure('ollama', err)));
|
|
72
|
+
assert.strictEqual(all.length, 10);
|
|
73
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'ollama');
|
|
74
|
+
assert.strictEqual(metrics?.recycleCount, 1, 'must coalesce to one recycle');
|
|
75
|
+
});
|
|
76
|
+
it('g) getPoolMetrics returns well-shaped entries', async () => {
|
|
77
|
+
getFetchPool('anthropic');
|
|
78
|
+
getFetchPool('openrouter');
|
|
79
|
+
const metrics = getPoolMetrics();
|
|
80
|
+
assert.ok(Array.isArray(metrics));
|
|
81
|
+
assert.strictEqual(metrics.length, 2);
|
|
82
|
+
for (const m of metrics) {
|
|
83
|
+
assert.ok(['anthropic', 'openrouter'].includes(m.provider));
|
|
84
|
+
assert.strictEqual(typeof m.createdAt, 'string');
|
|
85
|
+
assert.match(m.createdAt, /\dT\d/, 'ISO timestamp');
|
|
86
|
+
assert.strictEqual(typeof m.recycleCount, 'number');
|
|
87
|
+
assert.strictEqual(typeof m.active, 'boolean');
|
|
88
|
+
assert.strictEqual(m.active, true, 'idle pool is active');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it('h) resetAllPools drops all state', async () => {
|
|
92
|
+
getFetchPool('anthropic');
|
|
93
|
+
getFetchPool('gemini');
|
|
94
|
+
assert.strictEqual(getPoolMetrics().length, 2);
|
|
95
|
+
await resetAllPools();
|
|
96
|
+
assert.strictEqual(getPoolMetrics().length, 0);
|
|
97
|
+
// Subsequent get reconstructs cleanly
|
|
98
|
+
const fresh = getFetchPool('anthropic');
|
|
99
|
+
assert.ok(fresh instanceof Agent);
|
|
100
|
+
assert.strictEqual(getPoolMetrics().length, 1);
|
|
101
|
+
});
|
|
102
|
+
it('recycle reads ECONNRESET from err.cause', async () => {
|
|
103
|
+
const before = getFetchPool('anthropic');
|
|
104
|
+
// Node's fetch wraps the underlying socket error in `.cause`.
|
|
105
|
+
const err = new TypeError('fetch failed', {
|
|
106
|
+
cause: new Error('read ECONNRESET'),
|
|
107
|
+
});
|
|
108
|
+
await recycleFetchPoolOnFailure('anthropic', err);
|
|
109
|
+
const after = getFetchPool('anthropic');
|
|
110
|
+
assert.notStrictEqual(after, before);
|
|
111
|
+
const m = getPoolMetrics().find((p) => p.provider === 'anthropic');
|
|
112
|
+
assert.strictEqual(m?.recycleCount, 1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=fetch-pool.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-merged-executor.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mcp-merged-executor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for `MergedToolExecutor` driving the official MCP TS SDK
|
|
3
|
+
* over an in-process transport. Validates:
|
|
4
|
+
* - tool merging (builtin + MCP)
|
|
5
|
+
* - exact-name + per-server glob allowlists
|
|
6
|
+
* - destructive-tool guard under glob
|
|
7
|
+
* - dispatch routing builtin vs MCP
|
|
8
|
+
* - flattening of `content[]` array results
|
|
9
|
+
* - cancellation propagation
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
18
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
+
import { McpAgentClient } from '../mcp/client.js';
|
|
20
|
+
import { McpClientPool } from '../mcp/pool.js';
|
|
21
|
+
import { MergedToolExecutor } from '../mcp/merged-executor.js';
|
|
22
|
+
import { BuiltinToolExecutor } from '../tools/builtin.js';
|
|
23
|
+
/**
|
|
24
|
+
* Spin up an MCP server in-process and return a connected `McpAgentClient`
|
|
25
|
+
* wired through `InMemoryTransport`. The official SDK handles the protocol;
|
|
26
|
+
* the test only cares about the tool surface.
|
|
27
|
+
*/
|
|
28
|
+
async function makeInMemoryMcpClient(opts) {
|
|
29
|
+
const server = new Server({ name: `test-${opts.name}`, version: '0.0.1' }, { capabilities: { tools: {} } });
|
|
30
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
31
|
+
tools: opts.tools.map((t) => ({
|
|
32
|
+
name: t.name,
|
|
33
|
+
description: t.description,
|
|
34
|
+
inputSchema: t.inputSchema ?? { type: 'object' },
|
|
35
|
+
...(t.annotations ? { annotations: t.annotations } : {}),
|
|
36
|
+
})),
|
|
37
|
+
}));
|
|
38
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
39
|
+
const tool = opts.tools.find((t) => t.name === req.params.name);
|
|
40
|
+
if (!tool) {
|
|
41
|
+
return {
|
|
42
|
+
isError: true,
|
|
43
|
+
content: [{ type: 'text', text: `unknown tool ${req.params.name}` }],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const result = tool.onCall?.(req.params.arguments ?? {}) ?? {
|
|
47
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
48
|
+
};
|
|
49
|
+
return result;
|
|
50
|
+
});
|
|
51
|
+
const [clientT, serverT] = InMemoryTransport.createLinkedPair();
|
|
52
|
+
await server.connect(serverT);
|
|
53
|
+
// Construct a client and inject the linked transport, bypassing connect().
|
|
54
|
+
const client = new McpAgentClient({ name: opts.name, transport: 'stdio', command: 'noop' });
|
|
55
|
+
// Test seam: swap the SDK Client's transport for our in-memory one.
|
|
56
|
+
// The McpAgentClient holds the SDK Client in a private field; we
|
|
57
|
+
// bypass the type with a controlled cast.
|
|
58
|
+
await client.client.connect(clientT);
|
|
59
|
+
client.connected = true;
|
|
60
|
+
return client;
|
|
61
|
+
}
|
|
62
|
+
function workdir() {
|
|
63
|
+
const dir = mkdtempSync(join(tmpdir(), 'merged-exec-'));
|
|
64
|
+
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
65
|
+
}
|
|
66
|
+
// ── Tests ───────────────────────────────────────────────────────────────
|
|
67
|
+
describe('MergedToolExecutor — schema merging', () => {
|
|
68
|
+
it('merges builtin + MCP tools and namespaces MCP entries as mcp__<server>__<tool>', async () => {
|
|
69
|
+
const client = await makeInMemoryMcpClient({
|
|
70
|
+
name: 'gh',
|
|
71
|
+
tools: [{ name: 'list_issues', description: 'list', annotations: { readOnlyHint: true } }],
|
|
72
|
+
});
|
|
73
|
+
const pool = new McpClientPool({ servers: [client.config] });
|
|
74
|
+
// Bypass pool.discoverTools' loader by stuffing the client directly.
|
|
75
|
+
pool.clients.set('gh', client);
|
|
76
|
+
const builtin = new BuiltinToolExecutor({
|
|
77
|
+
allowedTools: ['read_file', 'mcp__gh__list_issues'],
|
|
78
|
+
});
|
|
79
|
+
const exec = new MergedToolExecutor({
|
|
80
|
+
builtin,
|
|
81
|
+
pool,
|
|
82
|
+
allowedTools: ['read_file', 'mcp__gh__list_issues'],
|
|
83
|
+
});
|
|
84
|
+
await exec.prime();
|
|
85
|
+
const names = exec.listSchemas().map((s) => s.name).sort();
|
|
86
|
+
assert.deepEqual(names, ['mcp__gh__list_issues', 'read_file']);
|
|
87
|
+
await client.close();
|
|
88
|
+
await pool.close();
|
|
89
|
+
});
|
|
90
|
+
it('per-server glob allows read-only MCP tools but hides destructive ones', async () => {
|
|
91
|
+
const client = await makeInMemoryMcpClient({
|
|
92
|
+
name: 'fs',
|
|
93
|
+
tools: [
|
|
94
|
+
{ name: 'read', description: 'read', annotations: { readOnlyHint: true } },
|
|
95
|
+
{ name: 'delete', description: 'delete', annotations: { destructiveHint: true } },
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
const pool = new McpClientPool({ servers: [client.config] });
|
|
99
|
+
pool.clients.set('fs', client);
|
|
100
|
+
const builtin = new BuiltinToolExecutor({ allowedTools: ['read_file', 'mcp__fs__*'] });
|
|
101
|
+
const exec = new MergedToolExecutor({
|
|
102
|
+
builtin,
|
|
103
|
+
pool,
|
|
104
|
+
allowedTools: ['read_file', 'mcp__fs__*'],
|
|
105
|
+
});
|
|
106
|
+
await exec.prime();
|
|
107
|
+
const names = exec.listSchemas().map((s) => s.name).sort();
|
|
108
|
+
assert.ok(names.includes('mcp__fs__read'), 'glob admits read-only MCP tool');
|
|
109
|
+
assert.ok(!names.includes('mcp__fs__delete'), 'glob hides destructive MCP tool');
|
|
110
|
+
await client.close();
|
|
111
|
+
await pool.close();
|
|
112
|
+
});
|
|
113
|
+
it('exact-name allowance admits destructive MCP tools', async () => {
|
|
114
|
+
const client = await makeInMemoryMcpClient({
|
|
115
|
+
name: 'fs',
|
|
116
|
+
tools: [{ name: 'delete', description: 'd', annotations: { destructiveHint: true } }],
|
|
117
|
+
});
|
|
118
|
+
const pool = new McpClientPool({ servers: [client.config] });
|
|
119
|
+
pool.clients.set('fs', client);
|
|
120
|
+
const exec = new MergedToolExecutor({
|
|
121
|
+
builtin: new BuiltinToolExecutor({ allowedTools: ['mcp__fs__delete'] }),
|
|
122
|
+
pool,
|
|
123
|
+
allowedTools: ['mcp__fs__delete'],
|
|
124
|
+
});
|
|
125
|
+
await exec.prime();
|
|
126
|
+
const names = exec.listSchemas().map((s) => s.name);
|
|
127
|
+
assert.deepEqual(names, ['mcp__fs__delete']);
|
|
128
|
+
await client.close();
|
|
129
|
+
await pool.close();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('MergedToolExecutor — dispatch', () => {
|
|
133
|
+
it('routes mcp__* calls to the pool and flattens text content[] into a string', async () => {
|
|
134
|
+
const client = await makeInMemoryMcpClient({
|
|
135
|
+
name: 'echo',
|
|
136
|
+
tools: [{
|
|
137
|
+
name: 'say',
|
|
138
|
+
description: 'echo',
|
|
139
|
+
onCall: (args) => ({
|
|
140
|
+
content: [
|
|
141
|
+
{ type: 'text', text: `you said: ${args.msg ?? '?'}` },
|
|
142
|
+
{ type: 'text', text: 'and a second line' },
|
|
143
|
+
],
|
|
144
|
+
}),
|
|
145
|
+
}],
|
|
146
|
+
});
|
|
147
|
+
const pool = new McpClientPool({ servers: [client.config] });
|
|
148
|
+
pool.clients.set('echo', client);
|
|
149
|
+
const exec = new MergedToolExecutor({
|
|
150
|
+
builtin: new BuiltinToolExecutor({ allowedTools: ['mcp__echo__say'] }),
|
|
151
|
+
pool,
|
|
152
|
+
allowedTools: ['mcp__echo__say'],
|
|
153
|
+
});
|
|
154
|
+
await exec.prime();
|
|
155
|
+
const { dir, cleanup } = workdir();
|
|
156
|
+
try {
|
|
157
|
+
const ctrl = new AbortController();
|
|
158
|
+
const result = await exec.execute({ id: '1', name: 'mcp__echo__say', arguments: { msg: 'hi' } }, { workingDir: dir, abortSignal: ctrl.signal });
|
|
159
|
+
assert.equal(result.isError, false);
|
|
160
|
+
assert.match(result.content, /you said: hi/);
|
|
161
|
+
assert.match(result.content, /second line/);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
cleanup();
|
|
165
|
+
await client.close();
|
|
166
|
+
await pool.close();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
it('routes builtin names to BuiltinToolExecutor untouched', async () => {
|
|
170
|
+
const pool = new McpClientPool({ servers: [] });
|
|
171
|
+
const exec = new MergedToolExecutor({
|
|
172
|
+
builtin: new BuiltinToolExecutor({ allowedTools: ['list'] }),
|
|
173
|
+
pool,
|
|
174
|
+
allowedTools: ['list'],
|
|
175
|
+
});
|
|
176
|
+
const { dir, cleanup } = workdir();
|
|
177
|
+
try {
|
|
178
|
+
const ctrl = new AbortController();
|
|
179
|
+
const result = await exec.execute({ id: '1', name: 'list', arguments: {} }, { workingDir: dir, abortSignal: ctrl.signal });
|
|
180
|
+
assert.equal(result.isError, false);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
cleanup();
|
|
184
|
+
await pool.close();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
it('rejects disallowed MCP names at exec with isError=true', async () => {
|
|
188
|
+
const pool = new McpClientPool({ servers: [] });
|
|
189
|
+
const exec = new MergedToolExecutor({
|
|
190
|
+
builtin: new BuiltinToolExecutor({ allowedTools: ['read_file'] }),
|
|
191
|
+
pool,
|
|
192
|
+
allowedTools: ['read_file'],
|
|
193
|
+
});
|
|
194
|
+
const { dir, cleanup } = workdir();
|
|
195
|
+
try {
|
|
196
|
+
const ctrl = new AbortController();
|
|
197
|
+
const result = await exec.execute({ id: '1', name: 'mcp__rogue__do_anything', arguments: {} }, { workingDir: dir, abortSignal: ctrl.signal });
|
|
198
|
+
assert.equal(result.isError, true);
|
|
199
|
+
assert.match(result.content, /not permitted/);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
cleanup();
|
|
203
|
+
await pool.close();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('McpClientPool — failure isolation', () => {
|
|
208
|
+
it('records connect failures in `failures` without crashing the pool', async () => {
|
|
209
|
+
// Construct a pool with one bogus server config that will fail at connect.
|
|
210
|
+
const pool = new McpClientPool({
|
|
211
|
+
servers: [{ name: 'broken', transport: 'stdio', command: '/no/such/command' }],
|
|
212
|
+
connectTimeoutMs: 1000,
|
|
213
|
+
});
|
|
214
|
+
const tools = await pool.discoverTools();
|
|
215
|
+
assert.deepEqual(tools, []);
|
|
216
|
+
assert.equal(pool.failures.length, 1);
|
|
217
|
+
assert.equal(pool.failures[0].server, 'broken');
|
|
218
|
+
await pool.close();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
//# sourceMappingURL=mcp-merged-executor.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-merged-executor.test.js","sourceRoot":"","sources":["../../src/__tests__/mcp-merged-executor.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -270,7 +270,13 @@ class FakeMcpClient {
|
|
|
270
270
|
async listTools() {
|
|
271
271
|
if (this.throwOnList)
|
|
272
272
|
throw new Error('boom');
|
|
273
|
-
return this.tools
|
|
273
|
+
return this.tools.map((t) => ({
|
|
274
|
+
name: t.name,
|
|
275
|
+
bareName: t.name.includes('__') ? t.name.split('__').slice(2).join('__') : t.name,
|
|
276
|
+
serverName: this.config.name,
|
|
277
|
+
schema: t,
|
|
278
|
+
annotations: {},
|
|
279
|
+
}));
|
|
274
280
|
}
|
|
275
281
|
}
|
|
276
282
|
function asClient(c) {
|
|
@@ -282,13 +288,13 @@ describe('buildAgentToolset', () => {
|
|
|
282
288
|
{ name: 'fs.read', description: 'read', inputSchema: {} },
|
|
283
289
|
];
|
|
284
290
|
const ghClient = new FakeMcpClient('github', [
|
|
285
|
-
{ name: '
|
|
291
|
+
{ name: 'mcp__github__list_issues', description: 'list', inputSchema: {} },
|
|
286
292
|
]);
|
|
287
293
|
const ts = await buildAgentToolset(builtIn, [asClient(ghClient)]);
|
|
288
294
|
assert.equal(ts.tools.length, 2);
|
|
289
|
-
assert.deepEqual(ts.tools.map((t) => t.name).sort(), ['fs.read', '
|
|
295
|
+
assert.deepEqual(ts.tools.map((t) => t.name).sort(), ['fs.read', 'mcp__github__list_issues']);
|
|
290
296
|
assert.equal(ts.mcpDispatch.size, 1);
|
|
291
|
-
assert.equal(ts.mcpDispatch.get('
|
|
297
|
+
assert.equal(ts.mcpDispatch.get('mcp__github__list_issues'), asClient(ghClient));
|
|
292
298
|
assert.equal(ts.mcpDispatch.has('fs.read'), false);
|
|
293
299
|
});
|
|
294
300
|
it('drops collisions with built-ins (warns)', async () => {
|
|
@@ -297,22 +303,22 @@ describe('buildAgentToolset', () => {
|
|
|
297
303
|
];
|
|
298
304
|
const dup = new FakeMcpClient('weird', [
|
|
299
305
|
{ name: 'fs.read', description: 'mcp dup', inputSchema: {} },
|
|
300
|
-
{ name: '
|
|
306
|
+
{ name: 'mcp__weird__ok', description: 'fine', inputSchema: {} },
|
|
301
307
|
]);
|
|
302
308
|
const { result, captured } = await captureStderrAsync(() => buildAgentToolset(builtIn, [asClient(dup)]));
|
|
303
309
|
assert.equal(result.tools.length, 2);
|
|
304
310
|
assert.equal(result.mcpDispatch.has('fs.read'), false);
|
|
305
|
-
assert.equal(result.mcpDispatch.has('
|
|
311
|
+
assert.equal(result.mcpDispatch.has('mcp__weird__ok'), true);
|
|
306
312
|
assert.ok(captured.includes('collision "fs.read"'));
|
|
307
313
|
});
|
|
308
314
|
it('isolates listTools failures: one bad server does not kill others', async () => {
|
|
309
315
|
const good = new FakeMcpClient('good', [
|
|
310
|
-
{ name: '
|
|
316
|
+
{ name: 'mcp__good__ping', description: 'p', inputSchema: {} },
|
|
311
317
|
]);
|
|
312
318
|
const bad = new FakeMcpClient('bad', [], /* throwOnList */ true);
|
|
313
319
|
const { result, captured } = await captureStderrAsync(() => buildAgentToolset([], [asClient(bad), asClient(good)]));
|
|
314
320
|
assert.equal(result.tools.length, 1);
|
|
315
|
-
assert.equal(result.tools[0].name, '
|
|
321
|
+
assert.equal(result.tools[0].name, 'mcp__good__ping');
|
|
316
322
|
assert.ok(captured.includes('"bad" listTools failed'));
|
|
317
323
|
});
|
|
318
324
|
});
|
|
@@ -348,21 +354,21 @@ describe('McpAgentClient routing', () => {
|
|
|
348
354
|
c.client = fake;
|
|
349
355
|
c.connected = true;
|
|
350
356
|
}
|
|
351
|
-
it('namespaces tools as
|
|
357
|
+
it('namespaces tools as `mcp__<server>__<tool>` in listTools()', async () => {
|
|
352
358
|
const client = new McpAgentClient({ name: 'fs', transport: 'stdio', command: 'noop' });
|
|
353
359
|
const fake = makeFakeSdkClient();
|
|
354
360
|
injectFakeClient(client, fake);
|
|
355
361
|
const tools = await client.listTools();
|
|
356
|
-
assert.deepEqual(tools.map((t) => t.name), ['
|
|
357
|
-
// descriptions
|
|
358
|
-
assert.
|
|
359
|
-
assert.deepEqual(tools[0].inputSchema, { type: 'object' });
|
|
362
|
+
assert.deepEqual(tools.map((t) => t.name), ['mcp__fs__read_file', 'mcp__fs__write_file']);
|
|
363
|
+
// descriptions decorated with server tag + read-only heuristic; schema preserved
|
|
364
|
+
assert.match(tools[0].schema.description, /\(via fs MCP/);
|
|
365
|
+
assert.deepEqual(tools[0].schema.inputSchema, { type: 'object' });
|
|
360
366
|
});
|
|
361
|
-
it('callTool strips the
|
|
367
|
+
it('callTool strips the namespacing prefix before forwarding to SDK', async () => {
|
|
362
368
|
const client = new McpAgentClient({ name: 'gh', transport: 'stdio', command: 'noop' });
|
|
363
369
|
const fake = makeFakeSdkClient();
|
|
364
370
|
injectFakeClient(client, fake);
|
|
365
|
-
await client.callTool('
|
|
371
|
+
await client.callTool('mcp__gh__list_issues', { repo: 'owner/repo' });
|
|
366
372
|
assert.equal(fake.calls.length, 1);
|
|
367
373
|
assert.equal(fake.calls[0].params.name, 'list_issues');
|
|
368
374
|
assert.deepEqual(fake.calls[0].params.arguments, { repo: 'owner/repo' });
|