mirror of
https://github.com/EvoMap/evolver.git
synced 2026-09-18 21:47:53 +08:00
230 lines
8.4 KiB
JavaScript
230 lines
8.4 KiB
JavaScript
// Regression guard for #460 Bug 2: when EVOMAP_PROXY=1 and a proxy is
|
|
// running, src/atp/hubClient.js MUST route /atp/* calls through the local
|
|
// proxy (127.0.0.1) and NOT make a direct call to the Hub. The proxy is the
|
|
// single egress point for the mailbox/task/session/ATP surface.
|
|
//
|
|
// We spin up an in-memory HTTP server that impersonates both the proxy and
|
|
// the hub on the same loopback interface. The proxy endpoint increments
|
|
// `proxyHits`; the hub endpoint increments `hubHits`. We then assert which
|
|
// counter moves under each env/settings combination.
|
|
|
|
'use strict';
|
|
|
|
const { describe, it, before, after, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
function listen(handler) {
|
|
return new Promise((resolve) => {
|
|
const server = http.createServer(handler);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const { port } = server.address();
|
|
resolve({ server, url: `http://127.0.0.1:${port}` });
|
|
});
|
|
});
|
|
}
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve) => {
|
|
const chunks = [];
|
|
req.on('data', (c) => chunks.push(c));
|
|
req.on('end', () => {
|
|
const raw = Buffer.concat(chunks).toString();
|
|
try { resolve(raw ? JSON.parse(raw) : {}); }
|
|
catch { resolve({ raw }); }
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('ATP hubClient proxy routing (regression #460 Bug 2)', () => {
|
|
let proxyServer;
|
|
let hubServer;
|
|
let proxyUrl;
|
|
let hubUrl;
|
|
let proxyHits;
|
|
let hubHits;
|
|
let lastProxyBody;
|
|
let origHome;
|
|
let origUserProfile;
|
|
let tmpHome;
|
|
let origEnv;
|
|
|
|
const ENV_KEYS = [
|
|
'EVOMAP_PROXY', 'A2A_TRANSPORT', 'A2A_HUB_URL', 'A2A_NODE_ID',
|
|
'A2A_NODE_SECRET', 'EVOMAP_PROXY_PORT', 'EVOMAP_HUB_ALLOW_INSECURE',
|
|
];
|
|
|
|
before(async () => {
|
|
proxyServer = await listen(async (req, res) => {
|
|
proxyHits.push({ method: req.method, path: req.url });
|
|
if (req.method === 'POST') lastProxyBody = await readBody(req);
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ via: 'proxy', path: req.url }));
|
|
});
|
|
proxyUrl = proxyServer.url;
|
|
|
|
hubServer = await listen(async (req, res) => {
|
|
hubHits.push({ method: req.method, path: req.url });
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ via: 'hub', path: req.url }));
|
|
});
|
|
hubUrl = hubServer.url;
|
|
});
|
|
|
|
after(async () => {
|
|
await new Promise((r) => proxyServer.server.close(r));
|
|
await new Promise((r) => hubServer.server.close(r));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
proxyHits = [];
|
|
hubHits = [];
|
|
lastProxyBody = null;
|
|
|
|
origEnv = {};
|
|
for (const k of ENV_KEYS) { origEnv[k] = process.env[k]; delete process.env[k]; }
|
|
|
|
origHome = process.env.HOME;
|
|
origUserProfile = process.env.USERPROFILE;
|
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'evomap-atp-route-'));
|
|
process.env.HOME = tmpHome;
|
|
process.env.USERPROFILE = tmpHome;
|
|
|
|
// Seed A2A env so hubClient can call getNodeId / buildHubHeaders without
|
|
// touching the filesystem persist path. We mint a fresh 64-hex secret
|
|
// per test so module caches (if any) don't leak between tests.
|
|
process.env.A2A_HUB_URL = hubUrl;
|
|
process.env.A2A_NODE_ID = 'node_' + crypto.randomBytes(8).toString('hex');
|
|
process.env.A2A_NODE_SECRET = crypto.randomBytes(32).toString('hex');
|
|
// The mock hub above is plain http://127.0.0.1:<port>. hubClient's
|
|
// _hubPost / _hubGet now enforce the same https-only posture as
|
|
// hubFetch, so we set the documented escape hatch to keep these mock
|
|
// servers usable.
|
|
process.env.EVOMAP_HUB_ALLOW_INSECURE = '1';
|
|
|
|
// Bust the require cache so our env changes take effect for every test.
|
|
for (const key of Object.keys(require.cache)) {
|
|
if (/src[\\/](atp[\\/]hubClient|proxy[\\/]server[\\/]settings|gep[\\/]a2aProtocol)\.js$/.test(key)) {
|
|
delete require.cache[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (origHome === undefined) delete process.env.HOME;
|
|
else process.env.HOME = origHome;
|
|
if (origUserProfile === undefined) delete process.env.USERPROFILE;
|
|
else process.env.USERPROFILE = origUserProfile;
|
|
|
|
for (const k of ENV_KEYS) {
|
|
if (origEnv[k] === undefined) delete process.env[k];
|
|
else process.env[k] = origEnv[k];
|
|
}
|
|
|
|
try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
});
|
|
|
|
function writeProxySettings(url) {
|
|
// settings.js uses os.homedir()/.evolver/settings.json. os.homedir()
|
|
// on linux reads $HOME which we've swapped to tmpHome above.
|
|
const settingsDir = path.join(tmpHome, '.evolver');
|
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({
|
|
proxy: { url, pid: process.pid, started_at: new Date().toISOString() },
|
|
}));
|
|
}
|
|
|
|
it('routes placeOrder through proxy when EVOMAP_PROXY=1 and proxy is running', async () => {
|
|
process.env.EVOMAP_PROXY = '1';
|
|
writeProxySettings(proxyUrl);
|
|
|
|
const hubClient = require('../src/atp/hubClient');
|
|
const result = await hubClient.placeOrder({
|
|
capabilities: ['test'],
|
|
budget: 5,
|
|
question: 'does proxy see this?',
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.data.via, 'proxy', 'response should come from proxy, not hub');
|
|
assert.equal(hubHits.length, 0, 'hub MUST NOT be called directly when proxy is running');
|
|
assert.equal(proxyHits.length, 1);
|
|
assert.equal(proxyHits[0].method, 'POST');
|
|
assert.equal(proxyHits[0].path, '/atp/order');
|
|
assert.equal(lastProxyBody.sender_id, process.env.A2A_NODE_ID);
|
|
assert.equal(lastProxyBody.budget, 5);
|
|
});
|
|
|
|
it('routes getOrderStatus (GET) through proxy when EVOMAP_PROXY=1', async () => {
|
|
process.env.EVOMAP_PROXY = '1';
|
|
writeProxySettings(proxyUrl);
|
|
|
|
const hubClient = require('../src/atp/hubClient');
|
|
const result = await hubClient.getOrderStatus('ord_abc123');
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.data.via, 'proxy');
|
|
assert.equal(hubHits.length, 0);
|
|
assert.equal(proxyHits.length, 1);
|
|
assert.equal(proxyHits[0].method, 'GET');
|
|
assert.equal(proxyHits[0].path, '/atp/order/ord_abc123');
|
|
});
|
|
|
|
it('routes listProofs with query string through proxy when EVOMAP_PROXY=1', async () => {
|
|
process.env.EVOMAP_PROXY = '1';
|
|
writeProxySettings(proxyUrl);
|
|
|
|
const hubClient = require('../src/atp/hubClient');
|
|
const result = await hubClient.listProofs({ role: 'merchant', limit: 10 });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(hubHits.length, 0);
|
|
assert.equal(proxyHits.length, 1);
|
|
assert.match(proxyHits[0].path, /^\/atp\/proofs\?/);
|
|
assert.match(proxyHits[0].path, /role=merchant/);
|
|
assert.match(proxyHits[0].path, /limit=10/);
|
|
});
|
|
|
|
it('falls back to hub direct when EVOMAP_PROXY is unset', async () => {
|
|
// EVOMAP_PROXY not set at all -- legacy behavior.
|
|
const hubClient = require('../src/atp/hubClient');
|
|
const result = await hubClient.placeOrder({ capabilities: ['test'], budget: 1 });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.data.via, 'hub');
|
|
assert.equal(hubHits.length, 1);
|
|
assert.equal(hubHits[0].path, '/a2a/atp/order');
|
|
assert.equal(proxyHits.length, 0, 'proxy MUST NOT be called when EVOMAP_PROXY is unset');
|
|
});
|
|
|
|
it('falls back to hub direct when EVOMAP_PROXY=1 but proxy is not running', async () => {
|
|
process.env.EVOMAP_PROXY = '1';
|
|
// intentionally NOT calling writeProxySettings -- simulates "user set the
|
|
// env flag but the proxy never started / crashed". We must not strand
|
|
// the call; falling back to hub keeps the CLI usable.
|
|
const hubClient = require('../src/atp/hubClient');
|
|
const result = await hubClient.placeOrder({ capabilities: ['test'], budget: 1 });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.data.via, 'hub');
|
|
assert.equal(hubHits.length, 1);
|
|
assert.equal(proxyHits.length, 0);
|
|
});
|
|
|
|
it('A2A_TRANSPORT=mailbox is equivalent to EVOMAP_PROXY=1', async () => {
|
|
process.env.A2A_TRANSPORT = 'mailbox';
|
|
writeProxySettings(proxyUrl);
|
|
|
|
const hubClient = require('../src/atp/hubClient');
|
|
const result = await hubClient.placeOrder({ capabilities: ['test'], budget: 1 });
|
|
|
|
assert.equal(result.data.via, 'proxy');
|
|
assert.equal(hubHits.length, 0);
|
|
assert.equal(proxyHits.length, 1);
|
|
});
|
|
});
|