OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / test / floor-upper-levels.test.js

327 lines · javascript ref:
import { test } from 'brittle'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { scan } from '../lib/scanner.js'
import {
  buildSystemLevel, buildModuleLevel, bundleLinks, dependencyClosure, depthColor,
  nodesAtPath, categoryOf, categoriesOf, categoryColor, moduleStats
} from '../lib/floor/cards.js'

const HERE = dirname(fileURLToPath(import.meta.url))
const WORKSPACE = resolve(HERE, 'fixtures/workspace')

// The top two rungs, against the fixture workspace: four modules, one shared
// dep, one real dependency edge, and a module whose files nest two directories
// deep. Categories and directories are CONTAINERS here — the same machinery
// zones use at the symbol level — which is why they cannot overlap.

let tree = null
test('upper levels: scan the fixture workspace', async (t) => {
  tree = await scan(WORKSPACE, { maxDepth: 1 })
  t.is(tree.system.modules.length, 4, 'four modules')
})

test('system level: one card per module, categories as containers', (t) => {
  const { cards, containers, links } = buildSystemLevel(tree)

  t.is(cards.length, 4, 'four cards')
  const byTitle = {}
  for (const c of cards) byTitle[c.title] = c
  t.ok(byTitle['widget-store'], 'widget-store is there')
  t.is(byTitle['widget-store'].kind, 'module', 'kind is module')
  t.is(byTitle['widget-store'].badge, '0.2.0', 'badged with its version')
  t.is(byTitle['widget-store'].openTo, 'widget-store', 'and opens into itself')
  t.ok(byTitle['widget-store'].sub.includes('files'), 'counts its files: ' + byTitle['widget-store'].sub)

  // Every card belongs to the container its category names.
  for (const c of cards) {
    const category = tree.moduleCategories[c.title]
    t.is(c.container, category ? 'cat:' + category : null, c.title + ' is in its category')
  }
  const ids = new Set(containers.map(c => c.id))
  for (const c of cards) {
    if (c.container) t.ok(ids.has(c.container), 'the container it names exists')
  }
  t.ok(links.length >= 0, 'links computed')
})

test('system level: only edges between modules the scan HAS', (t) => {
  const { cards, links } = buildSystemLevel(tree)
  const ids = new Set(cards.map(c => c.id))

  const named = links.map(l => l.from.replace('module:', '') + '->' + l.to.replace('module:', ''))
  t.ok(named.includes('widget-rooms->widget-store'), 'the real dependency edge is drawn')
  // b4a is a shared dependency but is NOT a module in the scan, so it is not a
  // card and must not be an edge endpoint pointing at nothing.
  for (const l of links) {
    t.ok(ids.has(l.from), 'edge source is a card')
    t.ok(ids.has(l.to), 'edge target is a card')
  }
  t.absent(named.some(n => n.includes('b4a')), 'no edge to an external dependency')
})

test('module level: one card per file, directories as NESTED containers', (t) => {
  const { cards, containers } = buildModuleLevel(tree, 'widget-store')

  const titles = cards.map(c => c.title).sort()
  t.alike(titles, ['index.js', 'schema.js', 'store.test.js', 'widget-store.js', 'widgets.js'],
    'every file is a card')

  const byId = {}
  for (const c of containers) byId[c.id] = c
  t.ok(byId['dir:lib'], 'lib/ is a container')
  t.ok(byId['dir:lib/handlers'], 'lib/handlers/ is a container')
  t.ok(byId['dir:test'], 'test/ is a container')
  t.is(byId['dir:lib/handlers'].parent, 'dir:lib', 'and it nests inside lib')
  t.is(byId['dir:lib'].parent, null, 'while lib is top level')

  const handlers = cards.find(c => c.title === 'widgets.js')
  t.is(handlers.container, 'dir:lib/handlers', 'a nested file names the deepest directory')
  const entry = cards.find(c => c.title === 'index.js')
  t.is(entry.container, null, 'a root file has no directory container')
  t.is(entry.badge, 'entry', 'and keeps its role as its badge')
})

test('module level: links read supplier-first, like every other level', (t) => {
  const { cards, links } = buildModuleLevel(tree, 'widget-store')
  const byId = {}
  for (const c of cards) byId[c.id] = c.title

  const named = links.map(l => byId[l.from] + '->' + byId[l.to])
  t.ok(named.includes('widget-store.js->index.js'),
    'the barrel is fed by what it re-exports: ' + named.join(', '))
  t.ok(named.includes('schema.js->widget-store.js'), 'and schema feeds the class')
  t.is(new Set(named).size, named.length, 'deduped')
})

test('module level: opening a file gives a path the file level can load', (t) => {
  const { cards } = buildModuleLevel(tree, 'widget-store')
  for (const c of cards) {
    t.ok(typeof c.openTo === 'string' && c.openTo.length > 0, c.title + ' opens to a path')
    t.ok(c.openTo.endsWith(c.title), 'which ends in the file name: ' + c.openTo)
  }
})

test('upper levels: a module that does not exist yields an empty level, not a throw', (t) => {
  const out = buildModuleLevel(tree, 'nope')
  t.alike(out, { cards: [], links: [], containers: [] }, 'empty')
})

test('bundling: crossing edges collapse to one per container pair, with a count', (t) => {
  const cards = [
    { id: 'a1', container: 'A' }, { id: 'a2', container: 'A' },
    { id: 'b1', container: 'B' }, { id: 'b2', container: 'B' },
    { id: 'loose', container: null }
  ]
  const links = [
    { from: 'a1', to: 'b1' },
    { from: 'a1', to: 'b2' },
    { from: 'a2', to: 'b1' },
    { from: 'a1', to: 'a2' },      // inside A — says nothing bundled
    { from: 'loose', to: 'b1' }
  ]
  const bundled = bundleLinks(cards, links)

  const ab = bundled.find(b => b.from === 'A' && b.to === 'B')
  t.ok(ab, 'A -> B exists')
  t.is(ab.count, 3, 'carrying all three crossing edges')
  t.is(ab.members.length, 3, 'and remembering which they were')

  t.absent(bundled.find(b => b.from === 'A' && b.to === 'A'), 'no self-edge for the internal one')

  const lb = bundled.find(b => b.from === 'loose')
  t.ok(lb, 'a card with no container stands for itself')
  t.is(lb.to, 'B', 'and still reaches the group it points at')
})

test('bundling: direction is kept, and the order is stable', (t) => {
  const cards = [{ id: 'x', container: 'A' }, { id: 'y', container: 'B' }]
  const links = [{ from: 'x', to: 'y' }, { from: 'y', to: 'x' }]
  const bundled = bundleLinks(cards, links)

  t.is(bundled.length, 2, 'A->B and B->A are different edges')
  t.alike(bundleLinks(cards, links), bundled, 'and the order does not wander')
})

test('bundling: on a real level it is a large reduction', (t) => {
  const sys = buildSystemLevel(tree)
  const bundled = bundleLinks(sys.cards, sys.links)
  t.ok(bundled.length <= sys.links.length, 'never more edges than it started with')
  t.ok(bundled.every(b => b.count >= 1), 'every bundle carries at least one')
  const total = bundled.reduce((a, b) => a + b.count, 0)
  const crossing = sys.links.filter(l => {
    const g = (id) => (sys.cards.find(c => c.id === id) || {}).container
    return g(l.from) !== g(l.to)
  }).length
  t.is(total, crossing, 'and every crossing edge is accounted for exactly once')
})

test('bundling: crossing edges collapse to one per container pair, with a count', (t) => {
  const cards = [
    { id: 'a1', container: 'A' }, { id: 'a2', container: 'A' },
    { id: 'b1', container: 'B' }, { id: 'b2', container: 'B' },
    { id: 'loose', container: null }
  ]
  const links = [
    { from: 'a1', to: 'b1' },
    { from: 'a1', to: 'b2' },
    { from: 'a2', to: 'b1' },
    { from: 'a1', to: 'a2' },      // inside A - says nothing once bundled
    { from: 'loose', to: 'b1' }
  ]
  const bundled = bundleLinks(cards, links)

  const ab = bundled.find(b => b.from === 'A' && b.to === 'B')
  t.ok(ab, 'A -> B exists')
  t.is(ab.count, 3, 'carrying all three crossing edges')
  t.is(ab.members.length, 3, 'and remembering which they were')
  t.absent(bundled.find(b => b.from === 'A' && b.to === 'A'), 'no self-edge for the internal one')

  const lb = bundled.find(b => b.from === 'loose')
  t.ok(lb, 'a card with no container stands for itself')
  t.is(lb.to, 'B', 'and still reaches the group it points at')
})

test('bundling: direction is kept, and the order is stable', (t) => {
  const cards = [{ id: 'x', container: 'A' }, { id: 'y', container: 'B' }]
  const links = [{ from: 'x', to: 'y' }, { from: 'y', to: 'x' }]
  const bundled = bundleLinks(cards, links)

  t.is(bundled.length, 2, 'A->B and B->A are different edges')
  t.alike(bundleLinks(cards, links), bundled, 'and the order does not wander')
})

test('bundling: on a real level every crossing edge is accounted for once', (t) => {
  const sys = buildSystemLevel(tree)
  const bundled = bundleLinks(sys.cards, sys.links)
  t.ok(bundled.length <= sys.links.length, 'never more edges than it started with')

  const groupOf = (id) => {
    const card = sys.cards.find(c => c.id === id)
    return card ? (card.container || card.id) : undefined
  }
  const crossing = sys.links.filter(l => groupOf(l.from) !== groupOf(l.to)).length
  t.is(bundled.reduce((a, b) => a + b.count, 0), crossing, 'exactly once')
})

test('closure: dependencies are found by walking backward, with shortest depth', (t) => {
  // a -> b -> c -> d  (supplier -> consumer), so d depends on c, b and a.
  const links = [
    { from: 'a', to: 'b' }, { from: 'b', to: 'c' }, { from: 'c', to: 'd' }
  ]
  const out = dependencyClosure(links, 'd')

  t.is(out.nodes.get('d'), 0, 'the start is depth 0')
  t.is(out.nodes.get('c'), 1, 'its direct dependency is 1')
  t.is(out.nodes.get('b'), 2, 'then 2')
  t.is(out.nodes.get('a'), 3, 'then 3')
  t.is(out.maxDepth, 3, 'and it reports how deep it went')

  // The other direction finds nothing: d supplies nobody.
  t.is(dependencyClosure(links, 'd', 'dependents').nodes.size, 1, 'no dependents')
  t.is(dependencyClosure(links, 'a', 'dependents').nodes.get('d'), 3, 'but a has d at 3')
})

test('closure: depth is the SHORTEST path, not the first one walked', (t) => {
  //   a -> b -> c -> e
  //   a -------------> e     (a is both 3 hops and 1 hop away)
  const links = [
    { from: 'a', to: 'b' }, { from: 'b', to: 'c' }, { from: 'c', to: 'e' },
    { from: 'a', to: 'e' }
  ]
  const out = dependencyClosure(links, 'e')
  t.is(out.nodes.get('a'), 1, 'a is one hop away by the short edge')
})

test('closure: a cycle terminates', (t) => {
  const links = [{ from: 'a', to: 'b' }, { from: 'b', to: 'c' }, { from: 'c', to: 'a' }]
  const out = dependencyClosure(links, 'a')
  t.is(out.nodes.size, 3, 'all three reached')
  t.ok(out.maxDepth <= 3, 'and it stopped')
})

test('closure: unrelated nodes are not in it', (t) => {
  const links = [{ from: 'a', to: 'b' }, { from: 'x', to: 'y' }]
  const out = dependencyClosure(links, 'b')
  t.absent(out.nodes.has('x'), 'a different component is excluded')
  t.absent(out.nodes.has('y'), 'entirely')
})

test('closure: on a real level it finds the real dependency', (t) => {
  // This fixture is deliberately one hop deep — widget-rooms feeds widget-store
  // and nothing else chains — so the claim here is that the closure finds the
  // right module at the right distance, not that it goes deep. Depth is proven
  // on the synthetic chains above, where the shape is controlled.
  const sys = buildSystemLevel(tree)
  const store = sys.cards.find(c => c.title === 'widget-store')
  const out = dependencyClosure(sys.links, store.id)

  t.is(out.nodes.get('module:widget-rooms'), 1, 'widget-rooms is one hop away')
  t.is(out.maxDepth, 1, 'and this fixture is exactly one hop deep')
  t.absent(out.nodes.has('module:legacy-sync'), 'an unrelated module is not pulled in')
})

test('gradient: colour moves with depth and is stable at the ends', (t) => {
  t.is(depthColor(0, 5), '#ffa657', 'the selected node itself is the accent colour')
  const near = depthColor(1, 5)
  const far = depthColor(5, 5)
  t.not(near, far, 'near and far differ')
  t.ok(near.startsWith('hsl('), 'and they are hsl ramps: ' + near + ' -> ' + far)
  t.is(depthColor(9, 1), depthColor(9, 1), 'deterministic')
})

test('shared: nodesAtPath walks the dirTree, one implementation for both views', (t) => {
  const roots = nodesAtPath(tree, '')
  t.ok(roots.length > 0, 'the workspace root has entries')
  const names = roots.map(r => r.name).sort()
  t.alike(names, ['legacy-sync', 'mapped-io', 'widget-rooms', 'widget-store'], 'the four modules')

  for (const r of roots) {
    t.is(r.type, 'module', r.name + ' is a module, not a bare directory')
    t.ok(r.mod, 'and carries its module entry')
    t.ok(r.dirEntry, 'and its dirTree entry')
  }
  t.alike(nodesAtPath(tree, 'does/not/exist'), [], 'a missing path is empty, not a throw')
})

test('shared: categoryOf uses the scanner`s single resolution', (t) => {
  for (const name of tree.system.modules) {
    t.is(categoryOf(tree, name), tree.moduleCategories[name],
      name + ' agrees with the scanner')
  }
  t.is(categoryOf(tree, 'not-a-module'), 'Other', 'an unknown module lands in Other')
})

test('shared: every category in use gets a colour, derived ones included', (t) => {
  const cats = categoriesOf(tree)
  t.ok(cats.length > 0, 'categories: ' + cats.join(', '))

  const derived = cats.filter(c => c.endsWith(' users'))
  t.ok(derived.length > 0, 'this workspace has a DERIVED category: ' + derived.join(', '))

  const colors = new Set()
  for (const c of cats) {
    const col = categoryColor(tree, c)
    t.ok(/^#[0-9a-f]{6}$/i.test(col), c + ' has a colour: ' + col)
    colors.add(col)
  }
  t.is(colors.size, cats.length, 'and distinct categories get distinct colours')
  t.is(categoryColor(tree, cats[0]), categoryColor(tree, cats[0]), 'stable')
})

test('shared: moduleStats counts what both views show', (t) => {
  const mod = tree.modules['widget-store']
  const stats = moduleStats(mod)
  t.is(stats.fileCount, Object.keys(mod.files).length, 'files')
  t.is(stats.exportCount, mod.publicAPI.length, 'exports')
  // Pin the definition: functions + class methods + getters, across every file.
  let expected = 0
  for (const f of Object.values(mod.files)) {
    expected += (f.functions || []).length
    for (const c of f.classes || []) expected += (c.methods || []).length + (c.getters || []).length
  }
  t.is(stats.memberCount, expected, 'members are functions + methods + getters: ' + expected)
  t.ok(expected > 0, 'and this fixture has some')
  t.alike(moduleStats(null), { fileCount: 0, memberCount: 0, exportCount: 0 }, 'no module is zeros')
})