OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / test-frustum.mjs

277 lines · javascript ref:
/**
 * Test frustum: recursive sub-clustering across stacked dimensions.
 *
 * Uses a mock backend with controlled room/member/metadata data to verify:
 * 1. Single-dimension frustum = flat clustering (same as features mode)
 * 2. Two-dimension frustum produces nested tree
 * 3. Three-dimension frustum recurses correctly
 * 4. Unclustered rooms land in "other" node
 * 5. Tree flag is set in response
 * 6. Labels come from dominant tags
 */
import { LocalGraph } from '../lib/local-graph.js'

// --- Mock data: 8 rooms across 2 activity levels and 2 categories ---
//
// High activity (50+ msgs): rooms A, B, C, D
//   - Category "dev":     A, B
//   - Category "testing": C, D
//
// Low activity (5 msgs):  rooms E, F, G, H
//   - Category "social":  E, F
//   - Category "dev":     G, H

const ROOMS = [
  { roomId: 'A', title: 'Core Dev' },
  { roomId: 'B', title: 'API Dev' },
  { roomId: 'C', title: 'Unit Tests' },
  { roomId: 'D', title: 'Integration Tests' },
  { roomId: 'E', title: 'Hangout' },
  { roomId: 'F', title: 'Music Chat' },
  { roomId: 'G', title: 'Infra Dev' },
  { roomId: 'H', title: 'Tooling Dev' }
]

// Members: high-activity rooms share members, low-activity rooms share different members
const MEMBERS = {
  A: [{ memberId: 'u1' }, { memberId: 'u2' }, { memberId: 'u3' }],
  B: [{ memberId: 'u1' }, { memberId: 'u2' }, { memberId: 'u4' }],
  C: [{ memberId: 'u3' }, { memberId: 'u5' }, { memberId: 'u6' }],
  D: [{ memberId: 'u3' }, { memberId: 'u5' }, { memberId: 'u7' }],
  E: [{ memberId: 'u8' }, { memberId: 'u9' }],
  F: [{ memberId: 'u8' }, { memberId: 'u10' }],
  G: [{ memberId: 'u11' }, { memberId: 'u12' }],
  H: [{ memberId: 'u11' }, { memberId: 'u13' }]
}

// Activity: message counts control quantile bucketing
const MESSAGES = {
  A: 80, B: 70, C: 60, D: 50,
  E: 5, F: 5, G: 3, H: 2
}

// Metadata: category tags
const META = {
  A: { category: 'development' },
  B: { category: 'development' },
  C: { category: 'testing' },
  D: { category: 'testing' },
  E: { category: 'social' },
  F: { category: 'social' },
  G: { category: 'development' },
  H: { category: 'development' }
}

function makeMsgArray (count) {
  return Array.from({ length: count }, (_, i) => ({ id: `msg${i}`, text: 'hi' }))
}

const mockBackend = {
  api: {
    core: {
      getRecentRooms: async () => ROOMS,
      getMembers: async (roomId) => MEMBERS[roomId] || [],
      getIdentity: async () => ({ memberId: 'self-not-in-rooms' }),
      getChatMessages: async (roomId) => makeMsgArray(MESSAGES[roomId] || 0),
      getDmRequestsByStatus: async () => []
    }
  }
}

const mockRoomMeta = {
  get: async (roomId) => META[roomId] || null
}

const graph = new LocalGraph(mockBackend, mockRoomMeta)

// --- Helper: collect all leaf rooms from a tree ---
function collectLeafRooms (tree) {
  const result = []
  for (const node of tree) {
    if (node.children && node.children.length > 0) {
      result.push(...collectLeafRooms(node.children))
    } else {
      result.push(...node.rooms)
    }
  }
  return result
}

// --- Helper: find node by id in tree ---
function findNode (tree, id) {
  for (const node of tree) {
    if (node.id === id) return node
    if (node.children) {
      const found = findNode(node.children, id)
      if (found) return found
    }
  }
  return null
}

// --- Test 1: Single-dimension frustum → flat clusters ---
{
  console.log('\n--- Test 1: Single-dimension frustum ---')
  const result = await graph.compute({
    force: true,
    algo: {
      name: 'single-dim',
      frustum: [{ type: 'category', threshold: 0.1 }],
      minSize: 2
    }
  })

  if (!result.tree) throw new Error('Expected tree flag to be true')
  console.log(`  clusters: ${result.clusters.length}`)
  for (const c of result.clusters) {
    console.log(`  [${c.id}] dim=${c.dimension} label="${c.label}" rooms=${c.rooms.join(',')} children=${c.children.length}`)
  }

  // Should have clusters for development, testing, social (+ maybe other)
  const labels = result.clusters.map(c => c.label)
  if (!labels.includes('development')) throw new Error('Expected "development" cluster')
  // All children should be empty (single dimension = no sub-clustering)
  for (const c of result.clusters) {
    if (c.children.length > 0) throw new Error(`Expected no children for single-dimension, got ${c.children.length}`)
  }

  // All 8 rooms accounted for
  const allRooms = collectLeafRooms(result.clusters)
  if (allRooms.length !== 8) throw new Error(`Expected 8 rooms, got ${allRooms.length}`)

  console.log('  PASS: single-dimension frustum works')
}

// --- Test 2: Two-dimension frustum → nested tree ---
{
  console.log('\n--- Test 2: Two-dimension frustum (activity → category) ---')
  const result = await graph.compute({
    force: true,
    algo: {
      name: 'two-dim',
      frustum: [
        { type: 'activity', threshold: 0.1 },
        { type: 'category', threshold: 0.1 }
      ],
      minSize: 2
    }
  })

  if (!result.tree) throw new Error('Expected tree flag')
  console.log(`  top-level: ${result.clusters.length}`)
  for (const c of result.clusters) {
    console.log(`  [${c.id}] dim=${c.dimension} label="${c.label}" rooms=${c.rooms.join(',')}`)
    for (const ch of c.children) {
      console.log(`    [${ch.id}] dim=${ch.dimension} label="${ch.label}" rooms=${ch.rooms.join(',')}`)
    }
  }

  // Top-level should be activity dimension
  for (const c of result.clusters) {
    if (c.dimension !== 'activity') throw new Error(`Expected dimension=activity, got ${c.dimension}`)
  }

  // Should have at least 2 top-level clusters (high activity vs low activity)
  if (result.clusters.length < 2) throw new Error(`Expected ≥2 activity clusters, got ${result.clusters.length}`)

  // At least one cluster should have children (category sub-clusters)
  const withChildren = result.clusters.filter(c => c.children.length > 0)
  console.log(`  clusters with children: ${withChildren.length}`)

  // Children should have dimension = category
  for (const c of withChildren) {
    for (const ch of c.children) {
      if (ch.dimension !== 'category') throw new Error(`Expected child dimension=category, got ${ch.dimension}`)
    }
  }

  // All 8 rooms accounted for
  const allRooms = collectLeafRooms(result.clusters)
  if (allRooms.length !== 8) throw new Error(`Expected 8 rooms, got ${allRooms.length}`)

  console.log('  PASS: two-dimension frustum produces nested tree')
}

// --- Test 3: Three-dimension frustum ---
{
  console.log('\n--- Test 3: Three-dimension frustum (category → activity → members) ---')
  const result = await graph.compute({
    force: true,
    algo: {
      name: 'three-dim',
      frustum: [
        { type: 'category', threshold: 0.1 },
        { type: 'activity', threshold: 0.1 },
        { type: 'members', threshold: 0.1 }
      ],
      minSize: 2
    }
  })

  if (!result.tree) throw new Error('Expected tree flag')
  console.log(`  top-level: ${result.clusters.length}`)

  // Walk and print full tree
  function printTree (nodes, indent) {
    for (const n of nodes) {
      console.log(`${'  '.repeat(indent + 1)}[${n.id}] ${n.dimension}:"${n.label}" (${n.rooms.length} rooms)`)
      if (n.children.length > 0) printTree(n.children, indent + 1)
    }
  }
  printTree(result.clusters, 0)

  // Top level should be category
  for (const c of result.clusters) {
    if (c.dimension !== 'category') throw new Error(`Expected top=category, got ${c.dimension}`)
  }

  // All 8 rooms
  const allRooms = collectLeafRooms(result.clusters)
  if (allRooms.length !== 8) throw new Error(`Expected 8 rooms, got ${allRooms.length}`)

  console.log('  PASS: three-dimension frustum works')
}

// --- Test 4: Tree flag absent for non-frustum ---
{
  console.log('\n--- Test 4: Non-frustum mode has no tree flag ---')
  const result = await graph.compute({
    force: true,
    algo: {
      name: 'flat-category',
      features: [{ type: 'category', weight: 1.0 }],
      threshold: 0.1,
      minSize: 2
    }
  })

  if (result.tree) throw new Error('Expected no tree flag for flat mode')
  if (!Array.isArray(result.clusters)) throw new Error('Expected flat clusters array')
  console.log(`  flat clusters: ${result.clusters.length}`)
  console.log('  PASS: non-frustum returns flat clusters')
}

// --- Test 5: Legacy multi-feature → delegates to frustum ---
{
  console.log('\n--- Test 5: Legacy multi-feature converts to frustum ---')
  const result = await graph.compute({
    force: true,
    algo: {
      name: 'legacy-combined',
      features: [
        { type: 'activity', weight: 1.0 },
        { type: 'category', weight: 0.5 }
      ],
      threshold: 0.1,
      minSize: 2
    }
  })

  // Should now return tree (legacy hierarchical delegates to frustum)
  if (!result.tree) throw new Error('Expected tree flag from legacy hierarchical')
  console.log(`  top-level: ${result.clusters.length}`)
  console.log('  PASS: legacy multi-feature delegates to frustum')
}

console.log('\n=== All frustum tests passed! ===')