fodda-mcp 1.46.31 → 1.46.97

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.
Files changed (289) hide show
  1. package/README.md +14 -94
  2. package/dist/agents/fodda-researcher/skills.d.ts +1 -1
  3. package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
  4. package/dist/agents/fodda-researcher/skills.js +1 -1
  5. package/dist/brandTemplate.d.ts +1 -1
  6. package/dist/brandTemplate.d.ts.map +1 -1
  7. package/dist/brandTemplate.js +162 -74
  8. package/dist/brandTemplate.js.map +1 -1
  9. package/dist/catalogCache.d.ts +29 -1
  10. package/dist/catalogCache.d.ts.map +1 -1
  11. package/dist/catalogCache.js +267 -16
  12. package/dist/catalogCache.js.map +1 -1
  13. package/dist/coverageRelevance.d.ts +97 -2
  14. package/dist/coverageRelevance.d.ts.map +1 -1
  15. package/dist/coverageRelevance.js +959 -164
  16. package/dist/coverageRelevance.js.map +1 -1
  17. package/dist/editorialFill.d.ts +5 -0
  18. package/dist/editorialFill.d.ts.map +1 -1
  19. package/dist/editorialFill.js +1 -1
  20. package/dist/editorialFill.js.map +1 -1
  21. package/dist/enrichment.d.ts +26 -0
  22. package/dist/enrichment.d.ts.map +1 -1
  23. package/dist/enrichment.js +394 -31
  24. package/dist/enrichment.js.map +1 -1
  25. package/dist/errorHandling.d.ts +35 -7
  26. package/dist/errorHandling.d.ts.map +1 -1
  27. package/dist/errorHandling.js +225 -155
  28. package/dist/errorHandling.js.map +1 -1
  29. package/dist/index.d.ts +25 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +351 -143
  32. package/dist/index.js.map +1 -1
  33. package/dist/jobs/jlens_quarterly_sweep.d.ts +62 -0
  34. package/dist/jobs/jlens_quarterly_sweep.d.ts.map +1 -0
  35. package/dist/jobs/jlens_quarterly_sweep.js +160 -0
  36. package/dist/jobs/jlens_quarterly_sweep.js.map +1 -0
  37. package/dist/pricingCache.d.ts.map +1 -1
  38. package/dist/pricingCache.js +41 -1
  39. package/dist/pricingCache.js.map +1 -1
  40. package/dist/queryCache.d.ts.map +1 -1
  41. package/dist/queryCache.js +2 -0
  42. package/dist/queryCache.js.map +1 -1
  43. package/dist/reportBriefing.d.ts +89 -0
  44. package/dist/reportBriefing.d.ts.map +1 -0
  45. package/dist/reportBriefing.js +662 -0
  46. package/dist/reportBriefing.js.map +1 -0
  47. package/dist/searchTemplate.d.ts.map +1 -1
  48. package/dist/searchTemplate.js +65 -53
  49. package/dist/searchTemplate.js.map +1 -1
  50. package/dist/services/graphRouter.d.ts +7 -0
  51. package/dist/services/graphRouter.d.ts.map +1 -0
  52. package/dist/services/graphRouter.js +7 -0
  53. package/dist/services/graphRouter.js.map +1 -0
  54. package/dist/services/notifications.d.ts +49 -0
  55. package/dist/services/notifications.d.ts.map +1 -0
  56. package/dist/services/notifications.js +206 -0
  57. package/dist/services/notifications.js.map +1 -0
  58. package/dist/sessionTracker.d.ts +19 -2
  59. package/dist/sessionTracker.d.ts.map +1 -1
  60. package/dist/sessionTracker.js +69 -16
  61. package/dist/sessionTracker.js.map +1 -1
  62. package/dist/skillClient.d.ts.map +1 -1
  63. package/dist/skillClient.js +11 -6
  64. package/dist/skillClient.js.map +1 -1
  65. package/dist/svgVisuals.d.ts +17 -19
  66. package/dist/svgVisuals.d.ts.map +1 -1
  67. package/dist/svgVisuals.js +189 -233
  68. package/dist/svgVisuals.js.map +1 -1
  69. package/dist/systemPrompt.d.ts +7 -2
  70. package/dist/systemPrompt.d.ts.map +1 -1
  71. package/dist/systemPrompt.js +110 -68
  72. package/dist/systemPrompt.js.map +1 -1
  73. package/dist/telemetry.d.ts.map +1 -1
  74. package/dist/telemetry.js +15 -0
  75. package/dist/telemetry.js.map +1 -1
  76. package/dist/test_adjacent_trends_polymorphic.d.ts +2 -0
  77. package/dist/test_adjacent_trends_polymorphic.d.ts.map +1 -0
  78. package/dist/test_adjacent_trends_polymorphic.js +154 -0
  79. package/dist/test_adjacent_trends_polymorphic.js.map +1 -0
  80. package/dist/test_book_a_call_live.js +18 -5
  81. package/dist/test_book_a_call_live.js.map +1 -1
  82. package/dist/test_byo_mcp_submission_clarity.d.ts +2 -0
  83. package/dist/test_byo_mcp_submission_clarity.d.ts.map +1 -0
  84. package/dist/test_byo_mcp_submission_clarity.js +142 -0
  85. package/dist/test_byo_mcp_submission_clarity.js.map +1 -0
  86. package/dist/test_capabilities_guidance.d.ts +2 -0
  87. package/dist/test_capabilities_guidance.d.ts.map +1 -0
  88. package/dist/test_capabilities_guidance.js +372 -0
  89. package/dist/test_capabilities_guidance.js.map +1 -0
  90. package/dist/test_client_provenance.d.ts +2 -0
  91. package/dist/test_client_provenance.d.ts.map +1 -0
  92. package/dist/test_client_provenance.js +77 -0
  93. package/dist/test_client_provenance.js.map +1 -0
  94. package/dist/test_connection_surface_cleanup.d.ts +2 -0
  95. package/dist/test_connection_surface_cleanup.d.ts.map +1 -0
  96. package/dist/test_connection_surface_cleanup.js +203 -0
  97. package/dist/test_connection_surface_cleanup.js.map +1 -0
  98. package/dist/test_consult_routing.d.ts +2 -0
  99. package/dist/test_consult_routing.d.ts.map +1 -0
  100. package/dist/test_consult_routing.js +86 -0
  101. package/dist/test_consult_routing.js.map +1 -0
  102. package/dist/test_consult_split.d.ts +2 -0
  103. package/dist/test_consult_split.d.ts.map +1 -0
  104. package/dist/test_consult_split.js +30 -0
  105. package/dist/test_consult_split.js.map +1 -0
  106. package/dist/test_coverage_relevance.js +111 -70
  107. package/dist/test_coverage_relevance.js.map +1 -1
  108. package/dist/test_coverage_replay.d.ts +2 -0
  109. package/dist/test_coverage_replay.d.ts.map +1 -0
  110. package/dist/test_coverage_replay.js +170 -0
  111. package/dist/test_coverage_replay.js.map +1 -0
  112. package/dist/test_dcr_and_legacy_deprecation.js +45 -50
  113. package/dist/test_dcr_and_legacy_deprecation.js.map +1 -1
  114. package/dist/test_demand_signal_logging.d.ts +2 -0
  115. package/dist/test_demand_signal_logging.d.ts.map +1 -0
  116. package/dist/test_demand_signal_logging.js +92 -0
  117. package/dist/test_demand_signal_logging.js.map +1 -0
  118. package/dist/test_earnings_activity_scope.d.ts +5 -0
  119. package/dist/test_earnings_activity_scope.d.ts.map +1 -0
  120. package/dist/test_earnings_activity_scope.js +101 -0
  121. package/dist/test_earnings_activity_scope.js.map +1 -0
  122. package/dist/test_earnings_divergence_live.d.ts +5 -0
  123. package/dist/test_earnings_divergence_live.d.ts.map +1 -0
  124. package/dist/test_earnings_divergence_live.js +135 -0
  125. package/dist/test_earnings_divergence_live.js.map +1 -0
  126. package/dist/test_earnings_hygiene_and_binding.d.ts +5 -0
  127. package/dist/test_earnings_hygiene_and_binding.d.ts.map +1 -0
  128. package/dist/test_earnings_hygiene_and_binding.js +104 -0
  129. package/dist/test_earnings_hygiene_and_binding.js.map +1 -0
  130. package/dist/test_exec_quotes_render.d.ts +2 -0
  131. package/dist/test_exec_quotes_render.d.ts.map +1 -0
  132. package/dist/test_exec_quotes_render.js +86 -0
  133. package/dist/test_exec_quotes_render.js.map +1 -0
  134. package/dist/test_expert_intro.d.ts +2 -0
  135. package/dist/test_expert_intro.d.ts.map +1 -0
  136. package/dist/test_expert_intro.js +57 -0
  137. package/dist/test_expert_intro.js.map +1 -0
  138. package/dist/test_expert_layer.d.ts +11 -0
  139. package/dist/test_expert_layer.d.ts.map +1 -0
  140. package/dist/test_expert_layer.js +356 -0
  141. package/dist/test_expert_layer.js.map +1 -0
  142. package/dist/test_gap_alert.js +8 -0
  143. package/dist/test_gap_alert.js.map +1 -1
  144. package/dist/test_grok_offering_surfaces.d.ts +2 -0
  145. package/dist/test_grok_offering_surfaces.d.ts.map +1 -0
  146. package/dist/test_grok_offering_surfaces.js +155 -0
  147. package/dist/test_grok_offering_surfaces.js.map +1 -0
  148. package/dist/test_human_agent_live.d.ts +2 -0
  149. package/dist/test_human_agent_live.d.ts.map +1 -0
  150. package/dist/test_human_agent_live.js +48 -0
  151. package/dist/test_human_agent_live.js.map +1 -0
  152. package/dist/test_identity_gap.d.ts +2 -0
  153. package/dist/test_identity_gap.d.ts.map +1 -0
  154. package/dist/test_identity_gap.js +171 -0
  155. package/dist/test_identity_gap.js.map +1 -0
  156. package/dist/test_intelligence_and_verifier.d.ts +17 -0
  157. package/dist/test_intelligence_and_verifier.d.ts.map +1 -0
  158. package/dist/test_intelligence_and_verifier.js +162 -0
  159. package/dist/test_intelligence_and_verifier.js.map +1 -0
  160. package/dist/test_latency_mcp_brief5.d.ts +12 -0
  161. package/dist/test_latency_mcp_brief5.d.ts.map +1 -0
  162. package/dist/test_latency_mcp_brief5.js +200 -0
  163. package/dist/test_latency_mcp_brief5.js.map +1 -0
  164. package/dist/test_limit_and_credit_card_capture.d.ts +21 -0
  165. package/dist/test_limit_and_credit_card_capture.d.ts.map +1 -0
  166. package/dist/test_limit_and_credit_card_capture.js +216 -0
  167. package/dist/test_limit_and_credit_card_capture.js.map +1 -0
  168. package/dist/test_list_graphs_topics.js +1 -1
  169. package/dist/test_list_graphs_topics.js.map +1 -1
  170. package/dist/test_live_deployed_mcp_search.d.ts +2 -0
  171. package/dist/test_live_deployed_mcp_search.d.ts.map +1 -0
  172. package/dist/test_live_deployed_mcp_search.js +85 -0
  173. package/dist/test_live_deployed_mcp_search.js.map +1 -0
  174. package/dist/test_live_deployed_split.d.ts +2 -0
  175. package/dist/test_live_deployed_split.d.ts.map +1 -0
  176. package/dist/test_live_deployed_split.js +72 -0
  177. package/dist/test_live_deployed_split.js.map +1 -0
  178. package/dist/test_live_endpoint_runner.js +2 -2
  179. package/dist/test_live_endpoint_runner.js.map +1 -1
  180. package/dist/test_live_mcp.js +1 -1
  181. package/dist/test_live_mcp.js.map +1 -1
  182. package/dist/test_live_neil_widget.d.ts +2 -0
  183. package/dist/test_live_neil_widget.d.ts.map +1 -0
  184. package/dist/test_live_neil_widget.js +84 -0
  185. package/dist/test_live_neil_widget.js.map +1 -0
  186. package/dist/test_longpoll_status.d.ts +2 -0
  187. package/dist/test_longpoll_status.d.ts.map +1 -0
  188. package/dist/test_longpoll_status.js +117 -0
  189. package/dist/test_longpoll_status.js.map +1 -0
  190. package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts +2 -0
  191. package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts.map +1 -0
  192. package/dist/test_mcp_find_expert_and_vocabulary_routing.js +130 -0
  193. package/dist/test_mcp_find_expert_and_vocabulary_routing.js.map +1 -0
  194. package/dist/test_next_moves.js +644 -279
  195. package/dist/test_next_moves.js.map +1 -1
  196. package/dist/test_next_moves_transcripts.js +122 -40
  197. package/dist/test_next_moves_transcripts.js.map +1 -1
  198. package/dist/test_on_request_caller_identity.d.ts +13 -0
  199. package/dist/test_on_request_caller_identity.d.ts.map +1 -0
  200. package/dist/test_on_request_caller_identity.js +131 -0
  201. package/dist/test_on_request_caller_identity.js.map +1 -0
  202. package/dist/test_on_request_intercept.d.ts +10 -0
  203. package/dist/test_on_request_intercept.d.ts.map +1 -0
  204. package/dist/test_on_request_intercept.js +95 -0
  205. package/dist/test_on_request_intercept.js.map +1 -0
  206. package/dist/test_onboarding_consent_and_errors.d.ts +2 -0
  207. package/dist/test_onboarding_consent_and_errors.d.ts.map +1 -0
  208. package/dist/test_onboarding_consent_and_errors.js +110 -0
  209. package/dist/test_onboarding_consent_and_errors.js.map +1 -0
  210. package/dist/test_onboarding_live_scenarios.d.ts +2 -0
  211. package/dist/test_onboarding_live_scenarios.d.ts.map +1 -0
  212. package/dist/test_onboarding_live_scenarios.js +189 -0
  213. package/dist/test_onboarding_live_scenarios.js.map +1 -0
  214. package/dist/test_referral_live.d.ts +2 -0
  215. package/dist/test_referral_live.d.ts.map +1 -0
  216. package/dist/test_referral_live.js +33 -0
  217. package/dist/test_referral_live.js.map +1 -0
  218. package/dist/test_referral_piers.d.ts +2 -0
  219. package/dist/test_referral_piers.d.ts.map +1 -0
  220. package/dist/test_referral_piers.js +33 -0
  221. package/dist/test_referral_piers.js.map +1 -0
  222. package/dist/test_report_editorial_briefing.d.ts +21 -0
  223. package/dist/test_report_editorial_briefing.d.ts.map +1 -0
  224. package/dist/test_report_editorial_briefing.js +356 -0
  225. package/dist/test_report_editorial_briefing.js.map +1 -0
  226. package/dist/test_search_graph_quality.d.ts +2 -0
  227. package/dist/test_search_graph_quality.d.ts.map +1 -0
  228. package/dist/test_search_graph_quality.js +493 -0
  229. package/dist/test_search_graph_quality.js.map +1 -0
  230. package/dist/test_service_to_service_auth.d.ts +2 -0
  231. package/dist/test_service_to_service_auth.d.ts.map +1 -0
  232. package/dist/test_service_to_service_auth.js +199 -0
  233. package/dist/test_service_to_service_auth.js.map +1 -0
  234. package/dist/test_sources_live.d.ts +2 -0
  235. package/dist/test_sources_live.d.ts.map +1 -0
  236. package/dist/test_sources_live.js +30 -0
  237. package/dist/test_sources_live.js.map +1 -0
  238. package/dist/test_specialist_and_analyst_tiers.d.ts +2 -0
  239. package/dist/test_specialist_and_analyst_tiers.d.ts.map +1 -0
  240. package/dist/test_specialist_and_analyst_tiers.js +155 -0
  241. package/dist/test_specialist_and_analyst_tiers.js.map +1 -0
  242. package/dist/test_v2_2_visual_hardening.d.ts +5 -0
  243. package/dist/test_v2_2_visual_hardening.d.ts.map +1 -0
  244. package/dist/test_v2_2_visual_hardening.js +333 -0
  245. package/dist/test_v2_2_visual_hardening.js.map +1 -0
  246. package/dist/test_verify_deprecate_render_spec.d.ts +2 -0
  247. package/dist/test_verify_deprecate_render_spec.d.ts.map +1 -0
  248. package/dist/test_verify_deprecate_render_spec.js +217 -0
  249. package/dist/test_verify_deprecate_render_spec.js.map +1 -0
  250. package/dist/test_verify_neil_demo.d.ts +2 -0
  251. package/dist/test_verify_neil_demo.d.ts.map +1 -0
  252. package/dist/test_verify_neil_demo.js +149 -0
  253. package/dist/test_verify_neil_demo.js.map +1 -0
  254. package/dist/test_waitable_research_and_grounded_links.d.ts +2 -0
  255. package/dist/test_waitable_research_and_grounded_links.d.ts.map +1 -0
  256. package/dist/test_waitable_research_and_grounded_links.js +129 -0
  257. package/dist/test_waitable_research_and_grounded_links.js.map +1 -0
  258. package/dist/test_widget_guards.d.ts +2 -0
  259. package/dist/test_widget_guards.d.ts.map +1 -0
  260. package/dist/test_widget_guards.js +229 -0
  261. package/dist/test_widget_guards.js.map +1 -0
  262. package/dist/test_zero_retention_alerts.d.ts +14 -0
  263. package/dist/test_zero_retention_alerts.d.ts.map +1 -0
  264. package/dist/test_zero_retention_alerts.js +76 -0
  265. package/dist/test_zero_retention_alerts.js.map +1 -0
  266. package/dist/toolHandlers.d.ts +38 -1
  267. package/dist/toolHandlers.d.ts.map +1 -1
  268. package/dist/toolHandlers.js +3856 -938
  269. package/dist/toolHandlers.js.map +1 -1
  270. package/dist/tools.d.ts +20 -0
  271. package/dist/tools.d.ts.map +1 -1
  272. package/dist/tools.js +25 -4
  273. package/dist/tools.js.map +1 -1
  274. package/dist/verify_discoverability.d.ts +2 -0
  275. package/dist/verify_discoverability.d.ts.map +1 -0
  276. package/dist/verify_discoverability.js +53 -0
  277. package/dist/verify_discoverability.js.map +1 -0
  278. package/dist/verify_tools_endpoint.js +16 -3
  279. package/dist/verify_tools_endpoint.js.map +1 -1
  280. package/dist/widgetShell.d.ts +2 -2
  281. package/dist/widgetShell.d.ts.map +1 -1
  282. package/dist/widgetShell.js +1 -1
  283. package/dist/widgetShell.js.map +1 -1
  284. package/package.json +1 -1
  285. package/server.json +4 -32
  286. package/dist/oauthRegisterShim.d.ts +0 -13
  287. package/dist/oauthRegisterShim.d.ts.map +0 -1
  288. package/dist/oauthRegisterShim.js +0 -83
  289. package/dist/oauthRegisterShim.js.map +0 -1
@@ -0,0 +1,203 @@
1
+ import axios from 'axios';
2
+ import assert from 'assert';
3
+ const TEST_PORT = 8994;
4
+ process.env.PORT = String(TEST_PORT);
5
+ process.env.NODE_ENV = 'development';
6
+ process.env.FODDA_SERVICE_URL = `http://localhost:${TEST_PORT}`;
7
+ // Boot MCP server on TEST_PORT
8
+ await import('./index.js');
9
+ async function runCleanupTests() {
10
+ console.log('=== Connection Surface Cleanup Verification Suite ===\n');
11
+ const baseUrl = `http://localhost:${TEST_PORT}`;
12
+ // Wait for server /health to respond
13
+ let ready = false;
14
+ for (let i = 0; i < 30; i++) {
15
+ try {
16
+ const h = await axios.get(`${baseUrl}/health`);
17
+ if (h.status === 200) {
18
+ ready = true;
19
+ break;
20
+ }
21
+ }
22
+ catch {
23
+ await new Promise((r) => setTimeout(r, 200));
24
+ }
25
+ }
26
+ assert(ready, 'Server failed to start on TEST_PORT');
27
+ console.log(`[test-server] Verified server healthy on port ${TEST_PORT}\n`);
28
+ try {
29
+ // ── TEST 1: GET /sse -> 410 Gone ──
30
+ console.log('--- Test 1: GET /sse returns 410 Gone ---');
31
+ try {
32
+ await axios.get(`${baseUrl}/sse`);
33
+ assert.fail('Expected 410 for GET /sse');
34
+ }
35
+ catch (err) {
36
+ assert.strictEqual(err.response?.status, 410, `Expected 410, got ${err.response?.status}`);
37
+ assert.strictEqual(err.response?.data?.error, 'sse_retired');
38
+ assert.strictEqual(err.response?.data?.message, 'Use Streamable HTTP at https://mcp.fodda.ai/mcp');
39
+ assert.strictEqual(err.response?.data?.source, 'fodda-mcp');
40
+ console.log('✅ PASS: GET /sse returns 410 with sse_retired JSON\n');
41
+ }
42
+ // ── TEST 2: POST /messages -> 410 Gone ──
43
+ console.log('--- Test 2: POST /messages returns 410 Gone ---');
44
+ try {
45
+ await axios.post(`${baseUrl}/messages`, { test: 'message' });
46
+ assert.fail('Expected 410 for POST /messages');
47
+ }
48
+ catch (err) {
49
+ assert.strictEqual(err.response?.status, 410, `Expected 410, got ${err.response?.status}`);
50
+ assert.strictEqual(err.response?.data?.error, 'sse_retired');
51
+ assert.strictEqual(err.response?.data?.message, 'Use Streamable HTTP at https://mcp.fodda.ai/mcp');
52
+ assert.strictEqual(err.response?.data?.source, 'fodda-mcp');
53
+ console.log('✅ PASS: POST /messages returns 410 with sse_retired JSON\n');
54
+ }
55
+ // ── TEST 3: Invalid /c/:token -> 401 + WWW-Authenticate pointing at /mcp ──
56
+ console.log('--- Test 3: POST /c/invalid returns 401 + WWW-Authenticate pointing to /mcp ---');
57
+ try {
58
+ await axios.post(`${baseUrl}/c/invalid_token_xyz`, {
59
+ jsonrpc: '2.0',
60
+ id: 101,
61
+ method: 'initialize',
62
+ params: {
63
+ protocolVersion: '2024-11-05',
64
+ capabilities: {},
65
+ clientInfo: { name: 'test-client', version: '1.0.0' }
66
+ }
67
+ }, {
68
+ headers: { 'Content-Type': 'application/json' }
69
+ });
70
+ assert.fail('Expected 401 for invalid /c/ token');
71
+ }
72
+ catch (err) {
73
+ assert.strictEqual(err.response?.status, 401, `Expected 401, got ${err.response?.status}`);
74
+ const authHeader = err.response?.headers['www-authenticate'] || '';
75
+ console.log('WWW-Authenticate header:', authHeader);
76
+ assert(authHeader.includes('/.well-known/oauth-protected-resource/mcp'), `Expected header to point to /mcp resource_metadata, got: ${authHeader}`);
77
+ assert(!authHeader.includes('/.well-known/oauth-protected-resource/c'), `Header must not point to /c: ${authHeader}`);
78
+ assert(authHeader.includes('error="invalid_token"'), `Expected error="invalid_token" in WWW-Authenticate: ${authHeader}`);
79
+ const body = err.response?.data;
80
+ console.log('Response body:', body);
81
+ assert.strictEqual(body?.jsonrpc, '2.0');
82
+ assert.strictEqual(body?.id, 101);
83
+ assert.strictEqual(body?.error?.code, -32000);
84
+ assert(body?.error?.error === 'invalid_token' || body?.error?.data?.error === 'invalid_token', 'Expected error="invalid_token" in error body');
85
+ console.log('✅ PASS: Invalid /c/ token returns 401 + WWW-Authenticate pointing at /mcp with invalid_token JSON-RPC\n');
86
+ }
87
+ // ── TEST 4: Invalid Clerk OAuth Bearer on /mcp -> 401 + WWW-Authenticate pointing at /mcp ──
88
+ console.log('--- Test 4: POST /mcp with invalid bearer returns 401 + WWW-Authenticate ---');
89
+ try {
90
+ await axios.post(`${baseUrl}/mcp`, {
91
+ jsonrpc: '2.0',
92
+ id: 102,
93
+ method: 'initialize',
94
+ params: {
95
+ protocolVersion: '2024-11-05',
96
+ capabilities: {},
97
+ clientInfo: { name: 'test-client', version: '1.0.0' }
98
+ }
99
+ }, {
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ 'Authorization': 'Bearer invalid_clerk_token_jwt'
103
+ }
104
+ });
105
+ assert.fail('Expected 401 for invalid bearer');
106
+ }
107
+ catch (err) {
108
+ assert.strictEqual(err.response?.status, 401, `Expected 401, got ${err.response?.status}`);
109
+ const authHeader = err.response?.headers['www-authenticate'] || '';
110
+ console.log('WWW-Authenticate header:', authHeader);
111
+ assert(authHeader.includes('/.well-known/oauth-protected-resource/mcp'), `Expected header to point to /mcp: ${authHeader}`);
112
+ assert(authHeader.includes('error="invalid_token"'), `Expected error="invalid_token": ${authHeader}`);
113
+ const body = err.response?.data;
114
+ console.log('Response body:', body);
115
+ assert.strictEqual(body?.jsonrpc, '2.0');
116
+ assert.strictEqual(body?.id, 102);
117
+ assert.strictEqual(body?.error?.code, -32000);
118
+ assert(body?.error?.error === 'invalid_token' || body?.error?.data?.error === 'invalid_token');
119
+ console.log('✅ PASS: Invalid bearer returns 401 + WWW-Authenticate + invalid_token JSON-RPC\n');
120
+ }
121
+ // ── TEST 5: Deprecation on /c/:token?api_key=junk and /grok-brand-context?api_key=junk ──
122
+ console.log('--- Test 5: Outdated ?api_key= on /c/:token and /grok-brand-context returns 401 ---');
123
+ try {
124
+ await axios.post(`${baseUrl}/c/sometoken?api_key=junk_key`, {
125
+ jsonrpc: '2.0',
126
+ id: 103,
127
+ method: 'tools/list'
128
+ }, {
129
+ headers: { 'Content-Type': 'application/json' }
130
+ });
131
+ assert.fail('Expected 401 for /c/:token?api_key=junk');
132
+ }
133
+ catch (err) {
134
+ assert.strictEqual(err.response?.status, 401);
135
+ assert.strictEqual(err.response?.data?.error?.code, -32001);
136
+ assert(err.response?.data?.error?.message?.includes('Fodda: this connection URL is outdated'));
137
+ assert(err.response?.data?.error?.message?.includes('Connections'));
138
+ assert(!err.response?.data?.error?.message?.includes('MCP Integration'));
139
+ assert.strictEqual(err.response?.data?.error?.data?.docs, 'https://www.fodda.ai/connect');
140
+ assert(err.response?.headers['www-authenticate']?.includes('/.well-known/oauth-protected-resource/mcp'));
141
+ console.log('✅ PASS: /c/:token?api_key=junk returns deprecation 401 with Connections navigation');
142
+ }
143
+ try {
144
+ await axios.post(`${baseUrl}/grok-brand-context?api_key=junk_key`, {
145
+ jsonrpc: '2.0',
146
+ id: 104,
147
+ method: 'tools/list'
148
+ }, {
149
+ headers: { 'Content-Type': 'application/json' }
150
+ });
151
+ assert.fail('Expected 401 for /grok-brand-context?api_key=junk');
152
+ }
153
+ catch (err) {
154
+ assert.strictEqual(err.response?.status, 401);
155
+ assert.strictEqual(err.response?.data?.error?.code, -32001);
156
+ assert(err.response?.data?.error?.message?.includes('Fodda: this connection URL is outdated'));
157
+ assert(err.response?.data?.error?.message?.includes('Connections'));
158
+ console.log('✅ PASS: /grok-brand-context?api_key=junk returns deprecation 401 with Connections navigation\n');
159
+ }
160
+ // ── TEST 6: Unauthenticated handshake on /mcp -> 401 + WWW-Authenticate (without invalid_token) ──
161
+ console.log('--- Test 6: Unauthenticated initialize returns 401 + WWW-Authenticate ---');
162
+ try {
163
+ await axios.post(`${baseUrl}/mcp`, {
164
+ jsonrpc: '2.0',
165
+ id: 105,
166
+ method: 'initialize',
167
+ params: {
168
+ protocolVersion: '2024-11-05',
169
+ capabilities: {},
170
+ clientInfo: { name: 'test-client', version: '1.0.0' }
171
+ }
172
+ }, {
173
+ headers: { 'Content-Type': 'application/json' }
174
+ });
175
+ assert.fail('Expected 401 for unauthenticated initialize');
176
+ }
177
+ catch (err) {
178
+ assert.strictEqual(err.response?.status, 401);
179
+ const authHeader = err.response?.headers['www-authenticate'] || '';
180
+ assert(authHeader.includes('/.well-known/oauth-protected-resource/mcp'));
181
+ assert(!authHeader.includes('error="invalid_token"'), 'Unauthenticated should not set invalid_token');
182
+ assert.strictEqual(err.response?.data?.error?.code, -32000);
183
+ assert.strictEqual(err.response?.data?.id, 105);
184
+ console.log('✅ PASS: Unauthenticated initialize returns 401 + WWW-Authenticate without invalid_token\n');
185
+ }
186
+ // ── TEST 7: Server card metadata does NOT contain mcpSse ──
187
+ console.log('--- Test 7: Server cards omit mcpSse ---');
188
+ const cardResp = await axios.get(`${baseUrl}/.well-known/mcp/server.json`);
189
+ assert.strictEqual(cardResp.status, 200);
190
+ assert(!('mcpSse' in (cardResp.data?.endpoints || {})), 'Server card must not have mcpSse');
191
+ assert('mcpStreamableHttp' in (cardResp.data?.endpoints || {}), 'Server card must have mcpStreamableHttp');
192
+ console.log('✅ PASS: Server card endpoints contains mcpStreamableHttp and omits mcpSse\n');
193
+ console.log('=== ALL CONNECTION SURFACE CLEANUP TESTS PASSED ===');
194
+ }
195
+ finally {
196
+ process.exit(0);
197
+ }
198
+ }
199
+ runCleanupTests().catch((err) => {
200
+ console.error('Test suite failed:', err);
201
+ process.exit(1);
202
+ });
203
+ //# sourceMappingURL=test_connection_surface_cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_connection_surface_cleanup.js","sourceRoot":"","sources":["../src/test_connection_surface_cleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test_consult_routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_consult_routing.d.ts","sourceRoot":"","sources":["../src/test_consult_routing.ts"],"names":[],"mappings":""}
@@ -0,0 +1,86 @@
1
+ import assert from 'assert';
2
+ import { setCachedCatalogForTesting } from './catalogCache.js';
3
+ import { createServer } from './toolHandlers.js';
4
+ async function runRoutingTests() {
5
+ console.log('================================================================');
6
+ console.log(' Running Unit Tests: Consult Routing & Dispatch Verification');
7
+ console.log('================================================================\n');
8
+ // 1. Setup mock catalog with Digital Twin and Synthetic Expert
9
+ setCachedCatalogForTesting({ version: '2.0.0', generated_at: new Date().toISOString(), graph_count: 2, graphs: [] }, [
10
+ { id: 'ben-dietz-sic', name: 'Ben Dietz', graphSubType: 'Digital Twin' },
11
+ { id: 'jane-austen', name: 'Jane Austen', graphSubType: 'Classic Digital Twin' },
12
+ { id: 'retail-strategy-innovation', name: 'Retail Strategy & Innovation Lead', graphSubType: 'Synthetic Expert' },
13
+ { id: 'marketing-media-strategy', name: 'Marketing & Media Strategy Lead', graphSubType: 'Synthetic Expert' }
14
+ ]);
15
+ let humanConsultCalls = [];
16
+ let syntheticConsultCalls = [];
17
+ const mockFoddaRequest = async (method, path, apiKey, userId, body) => {
18
+ if (path.includes('/v1/human-agents/consult')) {
19
+ humanConsultCalls.push({ analyst_id: body.analyst_id, query: body.query });
20
+ return {
21
+ result: `Human Agent response for ${body.analyst_id}`,
22
+ coverage: 'IN',
23
+ sources_used: []
24
+ };
25
+ }
26
+ if (path.includes('/v1/analysts/consult')) {
27
+ syntheticConsultCalls.push({ analyst_id: body.analyst_id, query: body.query });
28
+ return {
29
+ result: `Synthetic Analyst response for ${body.analyst_id}`,
30
+ coverage: 'IN',
31
+ sources_used: []
32
+ };
33
+ }
34
+ if (path === '/v1/graphs') {
35
+ return { graphs: [], disabled_graphs: [] };
36
+ }
37
+ return {};
38
+ };
39
+ const server = await createServer('dummy_key', 'test_user', mockFoddaRequest, async () => ({}), () => '', () => '');
40
+ const registeredTools = server._registeredTools;
41
+ // Test 1: consult_analyst with Digital Twin ID (ben-dietz-sic) routes directly to Human Agent endpoint
42
+ console.log('Test 1: consult_analyst with Digital Twin ID (ben-dietz-sic)...');
43
+ humanConsultCalls = [];
44
+ syntheticConsultCalls = [];
45
+ const consultAnalystHandler = registeredTools['consult_analyst'].handler;
46
+ const res1 = await consultAnalystHandler({ analyst_id: 'ben-dietz-sic', query: 'Hype trends' });
47
+ assert.strictEqual(humanConsultCalls.length, 1, 'Must invoke human-agents endpoint once');
48
+ assert.strictEqual(syntheticConsultCalls.length, 0, 'Must NOT invoke synthetic endpoint');
49
+ assert.strictEqual(humanConsultCalls[0]?.analyst_id, 'ben-dietz-sic');
50
+ assert.ok(res1.content[0].text.includes('Human Agent response for ben-dietz-sic'));
51
+ console.log('✅ Test 1 Passed: consult_analyst proactively routes Digital Twin to Human Agent endpoint.\n');
52
+ // Test 2: consult_analyst with Classic Digital Twin ID (jane-austen) routes to Human Agent endpoint
53
+ console.log('Test 2: consult_analyst with Classic Digital Twin ID (jane-austen)...');
54
+ humanConsultCalls = [];
55
+ syntheticConsultCalls = [];
56
+ const res2 = await consultAnalystHandler({ analyst_id: 'jane-austen', query: 'Regency etiquette' });
57
+ assert.strictEqual(humanConsultCalls.length, 1, 'Must invoke human-agents endpoint for Classic Digital Twin');
58
+ assert.strictEqual(syntheticConsultCalls.length, 0, 'Must NOT invoke synthetic endpoint');
59
+ assert.strictEqual(humanConsultCalls[0]?.analyst_id, 'jane-austen');
60
+ console.log('✅ Test 2 Passed: consult_analyst proactively routes Classic Digital Twin to Human Agent endpoint.\n');
61
+ // Test 3: consult_analyst with Synthetic Expert (retail-strategy-innovation) stays on Analyst endpoint
62
+ console.log('Test 3: consult_analyst with Synthetic Expert (retail-strategy-innovation)...');
63
+ humanConsultCalls = [];
64
+ syntheticConsultCalls = [];
65
+ const res3 = await consultAnalystHandler({ analyst_id: 'retail-strategy-innovation', query: 'Omnichannel retail' });
66
+ assert.strictEqual(syntheticConsultCalls.length, 1, 'Must invoke synthetic analyst endpoint once');
67
+ assert.strictEqual(humanConsultCalls.length, 0, 'Must NOT invoke human endpoint');
68
+ assert.strictEqual(syntheticConsultCalls[0]?.analyst_id, 'retail-strategy-innovation');
69
+ console.log('✅ Test 3 Passed: consult_analyst maintains standard Synthetic Analyst execution.\n');
70
+ // Test 4: consult_human_agent with Synthetic Expert (marketing-media-strategy) routes to Analyst endpoint
71
+ console.log('Test 4: consult_human_agent with Synthetic Expert (marketing-media-strategy)...');
72
+ humanConsultCalls = [];
73
+ syntheticConsultCalls = [];
74
+ const consultHumanHandler = registeredTools['consult_human_agent'].handler;
75
+ const res4 = await consultHumanHandler({ analyst_id: 'marketing-media-strategy', query: 'CTV spend' });
76
+ assert.strictEqual(syntheticConsultCalls.length, 1, 'Must invoke synthetic endpoint once');
77
+ assert.strictEqual(humanConsultCalls.length, 0, 'Must NOT invoke human endpoint');
78
+ assert.strictEqual(syntheticConsultCalls[0]?.analyst_id, 'marketing-media-strategy');
79
+ console.log('✅ Test 4 Passed: consult_human_agent proactively routes Synthetic Expert to Analyst endpoint.\n');
80
+ console.log('All 4 consult routing tests passed cleanly.');
81
+ }
82
+ runRoutingTests().catch(err => {
83
+ console.error('Test failure:', err);
84
+ process.exit(1);
85
+ });
86
+ //# sourceMappingURL=test_consult_routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_consult_routing.js","sourceRoot":"","sources":["../src/test_consult_routing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test_consult_split.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_consult_split.d.ts","sourceRoot":"","sources":["../src/test_consult_split.ts"],"names":[],"mappings":""}
@@ -0,0 +1,30 @@
1
+ import { initPricingCache, getQueryPricing } from './pricingCache.js';
2
+ import fs from 'fs';
3
+ import assert from 'assert';
4
+ console.log("Running verification for Split Human Agent Consult Tool...");
5
+ await initPricingCache();
6
+ // 1. Verify pricingCache entries
7
+ const humanPricing = getQueryPricing('human_agent_consult');
8
+ assert.ok(humanPricing, 'human_agent_consult pricing entry missing');
9
+ assert.strictEqual(humanPricing?.mcpToolName, 'consult_human_agent');
10
+ assert.strictEqual(humanPricing?.apiCallsCharged, 1);
11
+ console.log("✅ PASS: pricingCache contains human_agent_consult entry");
12
+ const syntheticPricing = getQueryPricing('expert_agent');
13
+ assert.ok(syntheticPricing, 'expert_agent pricing entry missing');
14
+ assert.strictEqual(syntheticPricing?.mcpToolName, 'consult_analyst');
15
+ console.log("✅ PASS: pricingCache contains expert_agent entry with mcpToolName='consult_analyst'");
16
+ // 2. Verify tools-manifest.json contents
17
+ const manifestRaw = fs.readFileSync(new URL('../tools-manifest.json', import.meta.url), 'utf8');
18
+ const manifest = JSON.parse(manifestRaw);
19
+ const consultHumanTool = manifest.tools.find((t) => t.name === 'consult_human_agent');
20
+ assert.ok(consultHumanTool, 'consult_human_agent missing from tools-manifest.json');
21
+ assert.strictEqual(consultHumanTool.bills_as, 'human_agent_consult');
22
+ assert.strictEqual(consultHumanTool.category, 'Expert');
23
+ console.log("✅ PASS: tools-manifest.json contains consult_human_agent registered under Expert category");
24
+ const consultAnalystTool = manifest.tools.find((t) => t.name === 'consult_analyst');
25
+ assert.ok(consultAnalystTool, 'consult_analyst missing from tools-manifest.json');
26
+ assert.strictEqual(consultAnalystTool.bills_as, 'expert_agent');
27
+ assert.ok(!consultAnalystTool.description.includes('$15 per turn'), 'consult_analyst description still contains hardcoded price string!');
28
+ console.log("✅ PASS: consult_analyst description has zero hardcoded '$15 per turn' price strings");
29
+ console.log("\nAll Split Human Agent Consult verification tests passed successfully!");
30
+ //# sourceMappingURL=test_consult_split.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_consult_split.js","sourceRoot":"","sources":["../src/test_consult_split.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,CAAC,GAAG,CAAC,4DAA4D,CAAC,CAAC;AAE1E,MAAM,gBAAgB,EAAE,CAAC;AAEzB,iCAAiC;AACjC,MAAM,YAAY,GAAG,eAAe,CAAC,qBAAqB,CAAC,CAAC;AAC5D,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,2CAA2C,CAAC,CAAC;AACrE,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,WAAW,EAAE,qBAAqB,CAAC,CAAC;AACrE,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC;AACrD,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;AAEvE,MAAM,gBAAgB,GAAG,eAAe,CAAC,cAAc,CAAC,CAAC;AACzD,MAAM,CAAC,EAAE,CAAC,gBAAgB,EAAE,oCAAoC,CAAC,CAAC;AAClE,MAAM,CAAC,WAAW,CAAC,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACrE,OAAO,CAAC,GAAG,CAAC,qFAAqF,CAAC,CAAC;AAEnG,yCAAyC;AACzC,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;AAChG,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AAEzC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAC;AAC3F,MAAM,CAAC,EAAE,CAAC,gBAAgB,EAAE,sDAAsD,CAAC,CAAC;AACpF,MAAM,CAAC,WAAW,CAAC,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAC;AACrE,MAAM,CAAC,WAAW,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACxD,OAAO,CAAC,GAAG,CAAC,2FAA2F,CAAC,CAAC;AAEzG,MAAM,kBAAkB,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC;AACzF,MAAM,CAAC,EAAE,CAAC,kBAAkB,EAAE,kDAAkD,CAAC,CAAC;AAClF,MAAM,CAAC,WAAW,CAAC,kBAAkB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AAChE,MAAM,CAAC,EAAE,CAAC,CAAC,kBAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,oEAAoE,CAAC,CAAC;AAC1I,OAAO,CAAC,GAAG,CAAC,qFAAqF,CAAC,CAAC;AAEnG,OAAO,CAAC,GAAG,CAAC,yEAAyE,CAAC,CAAC"}
@@ -48,74 +48,115 @@ function check(label, actual, expected) {
48
48
  failures++;
49
49
  console.log(`${pass ? 'PASS' : 'FAIL'} ${label} (got ${JSON.stringify(actual)}, want ${JSON.stringify(expected)})`);
50
50
  }
51
- // ── Tokenizer strips generic research vocabulary ──
52
- check('tokens keep only specific terms', specificQueryTokens(QA_QUERY), ['chinese', 'automotive', 'ev', 'china', 'car']);
53
- // ── Case A: QA replica — 10 rows, 2 on-topic → thin + suggested_action ──
54
- const a = addCoverageAnnotation({ rows: [...qaRows] }, QA_QUERY, [], 10, false, CATALOG);
55
- check('A: status', a.coverage.status, 'thin');
56
- check('A: results_returned', a.coverage.results_returned, 10);
57
- check('A: results_on_topic', a.coverage.results_on_topic, 2);
58
- check('A: suggested_action tool', a.coverage.suggested_action?.tool, 'get_supplemental_context');
59
- check('A: reason names the on-topic share', a.coverage.suggested_action?.reason?.includes('only 2 of 10 results are on-topic'), true);
60
- check('A: presentation is internal', a.coverage.presentation, 'internal');
61
- check('A: escalation offers Deep Dive', a.coverage.escalation?.options?.[0]?.tool, 'deep_research_topic');
62
- check('A: escalation offers web/LLM research', a.coverage.escalation?.options?.[1]?.action, 'web_llm_research');
63
- check('A: escalation gated on failed recovery', a.coverage.escalation?.when?.includes('suggested_action'), true);
64
- check('A: escalation phrasing', a.coverage.escalation?.say, 'This is what we have on this right now.');
65
- // ── Case B: healthy — 6 on-topic rows among the same noise → ok, no nudge ──
66
- const onTopicSix = [
67
- row('retail', 'EV Brand Loyalty in China', 'Chinese consumers switching to domestic EV marques', 2.1),
68
- row('retail', 'Chinese Auto Retail Formats', 'Direct-to-consumer car showrooms in China', 1.9),
69
- row('retail', 'BYD Global Push', 'Dealership expansion strategy', 1.8), // no literal token; rescued by domain score
70
- row('automotive-color-trends', 'China Car Exterior Palettes', 'Color direction for the Chinese automotive market', 0.82),
71
- row('automotive-color-trends', 'EV Interior Materials', 'Cabin material direction for electric vehicles', 0.78),
72
- row('retail', 'NEV Charging Retail', 'Charging-station adjacent commerce in Chinese cities', 1.7),
73
- ];
74
- const b = addCoverageAnnotation({ rows: [...onTopicSix, ...qaRows.slice(2, 6)] }, QA_QUERY, [], 10, false, CATALOG);
75
- check('B: status', b.coverage.status, 'ok');
76
- check('B: results_on_topic', b.coverage.results_on_topic, 6);
77
- check('B: no suggested_action', b.coverage.suggested_action === undefined, true);
78
- check('B: no escalation on ok', b.coverage.escalation === undefined, true);
79
- // ── Case F: empty result set → escalation ladder also present ──
80
- const f = addCoverageAnnotation({ rows: [] }, QA_QUERY, [], 10, false, CATALOG);
81
- check('F: status', f.coverage.status, 'empty');
82
- check('F: suggested_action tool', f.coverage.suggested_action?.tool, 'get_supplemental_context');
83
- check('F: escalation offers Deep Dive', f.coverage.escalation?.options?.[0]?.tool, 'deep_research_topic');
84
- // ── Case C: generic query — relevance not judgeable, legacy behavior only ──
85
- const c = addCoverageAnnotation({ rows: [...qaRows] }, 'top emerging trends', [], 10, false, CATALOG);
86
- check('C: status', c.coverage.status, 'ok');
87
- check('C: results_on_topic absent', c.coverage.results_on_topic === undefined, true);
88
- // ── Case D: score-less rows need a token match to count on-topic ──
89
- // (2026-08-20 fix: a row with no score AND no lexical overlap is off-topic;
90
- // previously score-less rows were counted on-topic unconditionally, which let
91
- // fully off-topic result sets report status "ok".)
92
- const statsRows = qaRows.slice(2).map(r => ({ ...r, relevance_score: null }));
93
- const d = addCoverageAnnotation({ rows: statsRows }, QA_QUERY, [], 10, true, CATALOG);
94
- check('D: status', d.coverage.status, 'thin');
95
- check('D: score-less off-topic rows not counted', d.coverage.results_on_topic, 0);
96
- // Score-less rows WITH a token match still count on-topic.
97
- const statsOnTopic = onTopicSix.slice(0, 2).map(r => ({ ...r, relevance_score: null }));
98
- const d2 = addCoverageAnnotation({ rows: [...statsOnTopic, ...statsRows] }, QA_QUERY, [], 10, true, CATALOG);
99
- check('D2: matching score-less rows counted', d2.coverage.results_on_topic, 2);
100
- // ── Case E: limit < 3 with all rows on-topic — no false thin ──
101
- const e = addCoverageAnnotation({ rows: onTopicSix.slice(0, 2) }, QA_QUERY, [], 2, false, CATALOG);
102
- check('E: status', e.coverage.status, 'ok');
103
- // ── Case G: Jeff Squires QA replica (2026-08-20) — deliberately off-topic
104
- // query against retail/skincare fan-out rows must NOT report ok with
105
- // results_on_topic == raw count ──
106
- const JEFF_QUERY = 'financial planning reinvention retirement Easter egg hunt';
107
- const jeffRows = [
108
- row('retail', 'Retailer-Operated Value-Recovery Programs', 'Trade-in and resale programs run by retailers', 1.45),
109
- row('retail', 'Ambient Commerce Storefronts', 'Sensor-based checkout-free formats', 1.4),
110
- row('womens-health-expert', 'Hormonal Wellness Platforms', 'Femtech products for menopause support', 0.7),
111
- row('beauty-expert', 'Clinical Skinimalism', 'Derm-approved minimal routines', 0.66),
112
- row('social-report', 'Deinfluencing 2.0', 'Creators monetizing anti-haul content', 0.5),
113
- ];
114
- const g = addCoverageAnnotation({ rows: jeffRows }, JEFF_QUERY, [], 10, false, CATALOG);
115
- check('G: status not ok for fully off-topic set', g.coverage.status, 'thin');
116
- check('G: results_on_topic is 0', g.coverage.results_on_topic, 0);
117
- // ── Direct unit check on the counter ──
118
- check('countOnTopicRows on QA replica', countOnTopicRows(qaRows, QA_QUERY, [], CATALOG), { onTopic: 2, evaluated: true });
119
- console.log(failures === 0 ? '\nALL CHECKS PASSED' : `\n${failures} CHECK(S) FAILED`);
120
- process.exit(failures === 0 ? 0 : 1);
51
+ async function runTests() {
52
+ // ── Tokenizer strips generic research vocabulary ──
53
+ check('tokens keep only specific terms', specificQueryTokens(QA_QUERY), ['chinese', 'automotive', 'ev', 'china', 'car']);
54
+ // ── Case A: QA replica — 10 rows, 2 on-topic → thin + suggested_action ──
55
+ const a = await addCoverageAnnotation({ rows: [...qaRows] }, QA_QUERY, [], 10, false, CATALOG);
56
+ check('A: status', a.coverage.status, 'thin');
57
+ check('A: results_returned', a.coverage.results_returned, 10);
58
+ check('A: results_on_topic', a.coverage.results_on_topic, 2);
59
+ check('A: suggested_action tool', a.coverage.suggested_action?.tool, 'get_supplemental_context');
60
+ check('A: reason names the on-topic share', a.coverage.suggested_action?.reason?.includes('only 2 of 10 results are on-topic'), true);
61
+ check('A: presentation is internal', a.coverage.presentation, 'internal');
62
+ check('A: escalation offers Deep Dive', a.coverage.escalation?.options?.[0]?.tool, 'deep_research_topic');
63
+ check('A: escalation offers web/LLM research', a.coverage.escalation?.options?.[1]?.action, 'web_llm_research');
64
+ check('A: escalation gated on failed recovery', a.coverage.escalation?.when?.includes('suggested_action'), true);
65
+ check('A: escalation phrasing', a.coverage.escalation?.say, 'This is what we have on this right now.');
66
+ // ── Case B: healthy — 6 on-topic rows among the same noise → ok, no nudge ──
67
+ const onTopicSix = [
68
+ row('retail', 'EV Brand Loyalty in China', 'Chinese consumers switching to domestic EV marques', 2.1),
69
+ row('retail', 'Chinese Auto Retail Formats', 'Direct-to-consumer car showrooms in China', 1.9),
70
+ row('retail', 'BYD Global Push', 'Dealership expansion strategy', 1.8), // no literal token; rescued by domain score
71
+ row('automotive-color-trends', 'China Car Exterior Palettes', 'Color direction for the Chinese automotive market', 0.82),
72
+ row('automotive-color-trends', 'EV Interior Materials', 'Cabin material direction for electric vehicles', 0.78),
73
+ row('retail', 'NEV Charging Retail', 'Charging-station adjacent commerce in Chinese cities', 1.7),
74
+ ];
75
+ const b = await addCoverageAnnotation({ rows: [...onTopicSix, ...qaRows.slice(2, 6)] }, QA_QUERY, [], 10, false, CATALOG);
76
+ check('B: status', b.coverage.status, 'ok');
77
+ check('B: results_on_topic', b.coverage.results_on_topic, 6);
78
+ check('B: no suggested_action', b.coverage.suggested_action === undefined, true);
79
+ check('B: no escalation on ok', b.coverage.escalation === undefined, true);
80
+ // ── Case F: empty result set → escalation ladder also present ──
81
+ const f = await addCoverageAnnotation({ rows: [] }, QA_QUERY, [], 10, false, CATALOG);
82
+ check('F: status', f.coverage.status, 'empty');
83
+ check('F: suggested_action tool', f.coverage.suggested_action?.tool, 'get_supplemental_context');
84
+ check('F: escalation offers Deep Dive', f.coverage.escalation?.options?.[0]?.tool, 'deep_research_topic');
85
+ // ── Case C: generic query — relevance not judgeable, legacy behavior only ──
86
+ const c = await addCoverageAnnotation({ rows: [...qaRows] }, 'top emerging trends', [], 10, false, CATALOG);
87
+ check('C: status', c.coverage.status, 'ok');
88
+ check('C: results_on_topic absent', c.coverage.results_on_topic === undefined, true);
89
+ // ── Case D: score-less rows need a token match to count on-topic ──
90
+ // (2026-08-20 fix: a row with no score AND no lexical overlap is off-topic;
91
+ // previously score-less rows were counted on-topic unconditionally, which let
92
+ // fully off-topic result sets report status "ok".)
93
+ const statsRows = qaRows.slice(2).map(r => ({ ...r, relevance_score: null }));
94
+ const d = await addCoverageAnnotation({ rows: statsRows }, QA_QUERY, [], 10, true, CATALOG);
95
+ check('D: status', d.coverage.status, 'thin');
96
+ check('D: score-less off-topic rows not counted', d.coverage.results_on_topic, 0);
97
+ // Score-less rows WITH a token match still count on-topic.
98
+ const statsOnTopic = onTopicSix.slice(0, 2).map(r => ({ ...r, relevance_score: null }));
99
+ const d2 = await addCoverageAnnotation({ rows: [...statsOnTopic, ...statsRows] }, QA_QUERY, [], 10, true, CATALOG);
100
+ check('D2: matching score-less rows counted', d2.coverage.results_on_topic, 2);
101
+ // ── Case E: limit < 3 with all rows on-topic — no false thin ──
102
+ const e = await addCoverageAnnotation({ rows: onTopicSix.slice(0, 2) }, QA_QUERY, [], 2, false, CATALOG);
103
+ check('E: status', e.coverage.status, 'ok');
104
+ // ── Case G: Jeff Squires QA replica (2026-08-20) — deliberately off-topic
105
+ // query against retail/skincare fan-out rows must NOT report ok with
106
+ // results_on_topic == raw count ──
107
+ const JEFF_QUERY = 'financial planning reinvention retirement Easter egg hunt';
108
+ const jeffRows = [
109
+ row('retail', 'Retailer-Operated Value-Recovery Programs', 'Trade-in and resale programs run by retailers', 1.45),
110
+ row('retail', 'Ambient Commerce Storefronts', 'Sensor-based checkout-free formats', 1.4),
111
+ row('womens-health-expert', 'Hormonal Wellness Platforms', 'Femtech products for menopause support', 0.7),
112
+ row('beauty-expert', 'Clinical Skinimalism', 'Derm-approved minimal routines', 0.66),
113
+ row('social-report', 'Deinfluencing 2.0', 'Creators monetizing anti-haul content', 0.5),
114
+ ];
115
+ const g = await addCoverageAnnotation({ rows: jeffRows }, JEFF_QUERY, [], 10, false, CATALOG);
116
+ check('G: status not ok for fully off-topic set', g.coverage.status, 'thin');
117
+ check('G: results_on_topic is 0', g.coverage.results_on_topic, 0);
118
+ // ── Direct unit check on the counter ──
119
+ check('countOnTopicRows on QA replica', countOnTopicRows(qaRows, QA_QUERY, [], CATALOG), { onTopic: 2, evaluated: true });
120
+ // ── Case H: Backend dataStatus: 'TREND_MATCH' with 10 rows without evidence arrays ──
121
+ // Specialist search (e.g. Peter Abraham cycling search) returns 10 trends confirmed
122
+ // by backend vector search. Should NOT be downgraded to 'thin' or attach suggested_action.
123
+ const cyclingRows = [
124
+ { _use_this_graphId: 'peter-abraham-bicycles-cycling', title: 'digital wall', summary: 'Chinese cycling market', relevance_score: 0.726, evidence: [] },
125
+ { _use_this_graphId: 'peter-abraham-bicycles-cycling', title: 'eventization of the weekly ride', summary: 'Group rides', relevance_score: 0.569, evidence: [] },
126
+ { _use_this_graphId: 'peter-abraham-bicycles-cycling', title: 'post-Justin generation', summary: 'Young cyclists', relevance_score: 0.566, evidence: [] },
127
+ { _use_this_graphId: 'peter-abraham-bicycles-cycling', title: 'Ride LA Together', summary: 'LA bike infrastructure', relevance_score: 0.56, evidence: [] },
128
+ { _use_this_graphId: 'havas-marketing', title: 'Gaming Culture', summary: 'Niche fandoms', relevance_score: 0.632, evidence: [] },
129
+ { _use_this_graphId: 'pinterest-hobbies-trend-report-2026', title: 'Collecting As Identity', summary: 'Hobby collection', relevance_score: 0.615, evidence: [] },
130
+ { _use_this_graphId: 'collectibles-alt-assets', title: 'Live Commerce', summary: 'Discovery layer', relevance_score: 0.516, evidence: [] },
131
+ { _use_this_graphId: '2026-macro-trend-graph', title: 'BIOADAPTIVE BALANCE', summary: 'Health balance', relevance_score: 0.496, evidence: [] },
132
+ { _use_this_graphId: '2026-macro-trend-graph', title: 'ANALOG SOUL', summary: 'Physical touchpoints', relevance_score: 0.493, evidence: [] },
133
+ { _use_this_graphId: 'edelman-marketing', title: 'Finance Eats Culture', summary: 'Financialization', relevance_score: 0.448, evidence: [] },
134
+ ];
135
+ const h = await addCoverageAnnotation({
136
+ dataStatus: 'TREND_MATCH',
137
+ on_topic_total: 4,
138
+ rows: cyclingRows
139
+ }, 'cycling and bicycle culture trends', [], 10, false, CATALOG);
140
+ check('H: backend TREND_MATCH status', h.coverage.status, 'ok');
141
+ check('H: results_returned', h.coverage.results_returned, 10);
142
+ check('H: no suggested_action on TREND_MATCH', h.coverage.suggested_action, undefined);
143
+ // ── Case I: Multi-graph fanout with on_topic_total: 4 and 10 rows ──
144
+ const i = await addCoverageAnnotation({
145
+ dataStatus: 'ok',
146
+ on_topic_total: 4,
147
+ rows: cyclingRows
148
+ }, 'cycling and bicycle culture trends', [], 10, false, CATALOG, { onTopicTotal: 4 });
149
+ check('I: on_topic_total >= 3 status', i.coverage.status, 'ok');
150
+ check('I: no suggested_action when on_topic_total >= 3', i.coverage.suggested_action, undefined);
151
+ // ── Case J: include_evidence: false with 10 rows does not trigger isThinEvidence ──
152
+ const j = await addCoverageAnnotation({
153
+ dataStatus: 'ok',
154
+ rows: cyclingRows
155
+ }, 'top emerging trends', [], 10, false, CATALOG, { include_evidence: false });
156
+ check('J: include_evidence: false does not flag thin on generic query', j.coverage.status, 'ok');
157
+ check('J: no suggested_action on healthy results without evidence', j.coverage.suggested_action, undefined);
158
+ console.log(failures === 0 ? '\nALL CHECKS PASSED' : `\n${failures} CHECK(S) FAILED`);
159
+ process.exit(failures === 0 ? 0 : 1);
160
+ }
161
+ runTests();
121
162
  //# sourceMappingURL=test_coverage_relevance.js.map