ai_nav.test.js 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. const fs = require('fs');
  2. const path = require('path');
  3. const { eq, ok } = require('../../helpers/assert');
  4. const CACHE_FILE = path.join(__dirname, '..', '..', '..', 'src', 'AI', 'embeddings', 'routes_cache.json');
  5. const removeCache = () => { try { if (fs.existsSync(CACHE_FILE)) fs.unlinkSync(CACHE_FILE); } catch (_) {} };
  6. removeCache();
  7. process.on('exit', removeCache);
  8. const routesIndexPath = require.resolve('../../../src/AI/routes_index');
  9. delete require.cache[routesIndexPath];
  10. const routesIndex = require('../../../src/AI/routes_index');
  11. const fakeEmbed = async (text) => {
  12. const t = String(text || '').toLowerCase();
  13. return [
  14. /(market|shop|buy|sell|product|store|vendor)/.test(t) ? 1 : 0,
  15. /(tribe|group|community|room|sub-tribe)/.test(t) ? 1 : 0,
  16. /(chat|message|messaging|pm|encrypted)/.test(t) ? 1 : 0,
  17. /(transfer|payment|wallet|money|eco|banking)/.test(t) ? 1 : 0,
  18. /(video|audio|image|document|file)/.test(t) ? 1 : 0
  19. ];
  20. };
  21. describe('ai: routes_index.resolveTopK', (t) => {
  22. t('returns an array sorted by descending score, capped at K', async () => {
  23. const q = await fakeEmbed('I want to buy something at a shop');
  24. const top = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0 }, 3);
  25. ok(Array.isArray(top));
  26. ok(top.length <= 3);
  27. ok(top.length > 0);
  28. for (let i = 1; i < top.length; i++) ok(top[i].score <= top[i - 1].score, 'descending');
  29. });
  30. t('items expose path, score, description, mod', async () => {
  31. const q = await fakeEmbed('buy shop product');
  32. const top = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0 }, 5);
  33. ok(top.length > 0);
  34. const it = top[0];
  35. ok(typeof it.path === 'string' && it.path.startsWith('/'));
  36. ok(typeof it.score === 'number');
  37. ok(typeof it.description === 'string' && it.description.length > 0);
  38. ok('mod' in it, 'mod key present (may be null)');
  39. });
  40. t('isModuleEnabled filter excludes disabled mods', async () => {
  41. const q = await fakeEmbed('shop store vendor');
  42. const allEnabled = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0, isModuleEnabled: () => true }, 60);
  43. ok(allEnabled.find(r => r.path === '/shops'));
  44. const shopsDisabled = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0, isModuleEnabled: (m) => m !== 'shopMod' }, 60);
  45. ok(!shopsDisabled.find(r => r.path === '/shops'));
  46. });
  47. t('threshold filters out low scores', async () => {
  48. const q = await fakeEmbed('zzz nonsensical query no matches');
  49. const top = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0.5 }, 10);
  50. for (const r of top) ok(r.score >= 0.5, `score ${r.score} >= 0.5`);
  51. });
  52. t('K=0 returns at least 1 entry (lower-bounded to 1)', async () => {
  53. const q = await fakeEmbed('shop');
  54. const top = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0 }, 0);
  55. ok(top.length <= 1);
  56. });
  57. t('shop-related query ranks /shops or /market near the top', async () => {
  58. const q = await fakeEmbed('I want to buy a product');
  59. const top = await routesIndex.resolveTopK(q, { embed: fakeEmbed, threshold: 0 }, 5);
  60. const paths = top.map(r => r.path);
  61. ok(paths.includes('/shops') || paths.includes('/market'), `expected /shops or /market in top 5, got ${paths.join(',')}`);
  62. });
  63. });
  64. describe('ai: routes_index.resolveBest backward compat', (t) => {
  65. t('returns single best entry above threshold', async () => {
  66. const q = await fakeEmbed('encrypted chat room');
  67. const best = await routesIndex.resolveBest(q, { embed: fakeEmbed, threshold: 0 });
  68. ok(best);
  69. ok(typeof best.path === 'string' && best.path.startsWith('/'));
  70. ok(typeof best.score === 'number');
  71. });
  72. t('returns null when no entry meets threshold', async () => {
  73. const q = await fakeEmbed('zzz nonsensical query no matches');
  74. const best = await routesIndex.resolveBest(q, { embed: fakeEmbed, threshold: 0.99 });
  75. eq(best, null);
  76. });
  77. });