| 14 | // Load a FRESH copy of recallInject with selected sibling modules stubbed, so |
| 15 | // each test controls the Hub rows / local genes / logging without a network. |
| 16 | function loadCoreWithStubs({ hubRows, localGenes, hubUrl = 'https://hub.test', a2aResolvesEnv = false, onLog } = {}) { |
| 17 | for (const p of [CORE_PATH, HUBSEARCH_PATH, ASSETSTORE_PATH, A2A_PATH, ASSETLOG_PATH]) { |
| 18 | delete require.cache[p]; |
| 19 | } |
| 20 | const origLoad = Module._load; |
| 21 | const logged = []; |
| 22 | const stats = { hubCalls: 0 }; |
| 23 | Module._load = function (request, parent, isMain) { |
| 24 | const resolved = (() => { |
| 25 | try { return Module._resolveFilename(request, parent, isMain); } catch (_) { return null; } |
| 26 | })(); |
| 27 | if (resolved === HUBSEARCH_PATH) { |
| 28 | return { |
| 29 | getHubUrl: () => hubUrl, |
| 30 | isSemanticEnabled: () => { |
| 31 | const v = process.env.HUBSEARCH_SEMANTIC; |
| 32 | return !(v === 'false' || v === '0'); |
| 33 | }, |
| 34 | fetchSemanticResults: async () => { |
| 35 | stats.hubCalls++; |
| 36 | return (Array.isArray(hubRows) ? hubRows : []); |
| 37 | }, |
| 38 | }; |
| 39 | } |
| 40 | if (resolved === A2A_PATH) { |
| 41 | const a2a = { buildHubHeaders: () => ({}), getNodeId: () => 'node_selftest0001' }; |
| 42 | // When a2aResolvesEnv is set, mimic the REAL a2aProtocol.getHubUrl which |
| 43 | // honors A2A_HUB_URL || EVOMAP_HUB_URL — so we can assert recall resolves |
| 44 | // the legacy var (Bugbot #183 medium). |
| 45 | if (a2aResolvesEnv) { |
| 46 | a2a.getHubUrl = () => process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || ''; |
| 47 | } |
| 48 | return a2a; |
| 49 | } |
| 50 | if (resolved === ASSETSTORE_PATH) { |
| 51 | return { loadGenes: () => (Array.isArray(localGenes) ? localGenes : []) }; |
| 52 | } |
| 53 | if (resolved === ASSETLOG_PATH) { |
| 54 | return { logAssetCall: (e) => { logged.push(e); if (onLog) onLog(e); } }; |
| 55 | } |
| 56 | return origLoad.apply(this, arguments); |
| 57 | }; |
| 58 | try { |
| 59 | const core = require(CORE_PATH); |
| 60 | return { core, logged, stats, restore: () => { Module._load = origLoad; } }; |
| 61 | } catch (e) { |
| 62 | Module._load = origLoad; |
| 63 | throw e; |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | describe('recallInject — mode gating', () => { |
| 68 | let saved; |