/**
* 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! ===')