OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / test / config.test.js

153 lines · javascript ref:
import { test } from 'brittle'
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'

import {
  loadConfig,
  applyCliOverrides,
  isExternalRef,
  isPlaceholder,
  getDefaults,
  DEFAULT_CONFIG
} from '../lib/config.js'

function mkFixture (name) {
  const dir = join(tmpdir(), 'codetree-test-' + name + '-' + Date.now())
  mkdirSync(dir, { recursive: true })
  return dir
}

// --- loadConfig ---

test('config: missing .codetree.json returns defaults', (t) => {
  const dir = mkFixture('missing')
  const config = loadConfig(dir)
  t.is(config.autoCrawl, false, 'default autoCrawl is false')
  t.ok(Array.isArray(config.externalSymbols), 'externalSymbols is an array')
  t.is(config.externalSymbols.length, 0, 'no externals by default')
  t.ok(Array.isArray(config.placeholderPatterns), 'placeholder patterns defaulted')
  t.ok(config.placeholderPatterns.length > 0, 'has some default placeholders')
  rmSync(dir, { recursive: true, force: true })
})

test('config: user values override defaults', (t) => {
  const dir = mkFixture('override')
  writeFileSync(join(dir, '.codetree.json'), JSON.stringify({
    autoCrawl: true,
    externalSymbols: ['an.sign', 'an.open'],
    externalPackages: ['anproto']
  }))

  const config = loadConfig(dir)
  t.is(config.autoCrawl, true, 'autoCrawl overridden')
  t.ok(config.externalSymbols.includes('an.sign'), 'external symbol loaded')
  t.ok(config.externalPackages.includes('anproto'), 'external package loaded')
  // Defaults still present
  t.ok(config.placeholderPatterns.length > 0, 'placeholders preserved')
  rmSync(dir, { recursive: true, force: true })
})

test('config: bad JSON logs but does not throw', (t) => {
  const dir = mkFixture('badjson')
  writeFileSync(join(dir, '.codetree.json'), '{ this is not valid json')
  // Silence the expected console.warn so the test log stays clean
  const origWarn = console.warn
  console.warn = () => {}
  try {
    const config = loadConfig(dir)
    t.ok(config, 'returns some config on bad JSON')
    t.is(config.autoCrawl, false, 'falls back to defaults')
  } finally {
    console.warn = origWarn
  }
  rmSync(dir, { recursive: true, force: true })
})

// --- applyCliOverrides ---

test('config: CLI overrides win over loaded config', (t) => {
  const base = { ...getDefaults(), autoCrawl: false, siblingDirs: ['../a'] }
  const merged = applyCliOverrides(base, { autoCrawl: true, siblingDirs: ['../b'] })
  t.is(merged.autoCrawl, true, 'CLI autoCrawl wins')
  t.ok(merged.siblingDirs.includes('../a'), 'original sibling preserved')
  t.ok(merged.siblingDirs.includes('../b'), 'CLI sibling merged in')
})

test('config: applyCliOverrides without fields is a no-op copy', (t) => {
  const base = { ...getDefaults(), autoCrawl: true }
  const merged = applyCliOverrides(base, {})
  t.is(merged.autoCrawl, true, 'no accidental reset')
  t.not(merged, base, 'returns a new object')
})

// --- isExternalRef ---

test('config: isExternalRef matches external symbols', (t) => {
  const config = { ...getDefaults(), externalSymbols: ['an.sign', 'IdentityKey'] }
  t.ok(isExternalRef('an.sign', config), 'symbol match')
  t.ok(isExternalRef('IdentityKey', config), 'class-like external')
  t.not(isExternalRef('SomethingElse', config), 'non-external not matched')
})

test('config: isExternalRef matches external packages', (t) => {
  const config = { ...getDefaults(), externalPackages: ['anproto', 'keet-identity-key'] }
  t.ok(isExternalRef('anproto', config), 'package name match')
  t.not(isExternalRef('something-else', config), 'non-package not matched')
})

test('config: isExternalRef matches external path prefixes', (t) => {
  const config = { ...getDefaults(), externalPaths: ['hyper-fleeky/anproto'] }
  t.ok(isExternalRef('hyper-fleeky/anproto', config), 'exact path match')
  t.ok(isExternalRef('hyper-fleeky/anproto/lib/an.js', config), 'subpath match')
  t.not(isExternalRef('hyper-fleeky/anproto-other', config), 'different prefix not matched')
})

// --- isPlaceholder (invariant I3) ---

test('config: isPlaceholder recognizes common placeholder forms', (t) => {
  const config = getDefaults()

  // Home paths
  t.ok(isPlaceholder('~/.hyperident/', config), 'home dir shorthand')
  t.ok(isPlaceholder('~/config/foo', config), 'home subdir')

  // Canonical "/path/to/" placeholder
  t.ok(isPlaceholder('/path/to/hyperident', config), '/path/to/ placeholder')

  // Angle-bracket placeholders
  t.ok(isPlaceholder('<placeholder>', config), 'angle brackets')
  t.ok(isPlaceholder('<name>', config), 'angle brackets short form')

  // Variable interpolation
  t.ok(isPlaceholder('${variable}', config), 'shell interpolation')
  t.ok(isPlaceholder('prefix${foo}suffix', config), 'embedded interpolation')

  // Path with embedded template token
  t.ok(isPlaceholder('/etc/<name>', config), 'path with embedded token')

  // Real paths should NOT match
  t.not(isPlaceholder('lib/auth/verify.js', config), 'real path not a placeholder')
  t.not(isPlaceholder('SomeClass', config), 'class name not a placeholder')
  t.not(isPlaceholder('foo()', config), 'function call not a placeholder')
})

test('config: custom placeholder patterns can be added', (t) => {
  const config = {
    ...getDefaults(),
    placeholderPatterns: [...DEFAULT_CONFIG.placeholderPatterns, '^@@internal/']
  }
  t.ok(isPlaceholder('@@internal/foo', config), 'custom pattern matched')
  t.ok(isPlaceholder('~/.config', config), 'default pattern still applied')
})

test('config: malformed regex in placeholderPatterns is tolerated', (t) => {
  const config = {
    ...getDefaults(),
    placeholderPatterns: ['(unclosed group', '^~/']
  }
  // Should not throw; bad regex just doesn't match
  const result = isPlaceholder('~/foo', config)
  t.ok(result, 'good pattern still works despite sibling bad regex')
})