OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / test / cruft-external.test.js

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

import { scan } from '../lib/scanner.js'

/**
 * End-to-end checks for Phase 0a invariants.
 *
 * I2 — external refs resolve as 'external', not 'missing'.
 * I3 — placeholder paths are not classified as references at all.
 * I4 — deprecated modules don't emit undocumented-X warnings.
 *
 * Uses minimal in-tempdir fixtures so the test is self-contained and
 * doesn't couple to pear-git-v3's current spec state (invariant I1).
 */

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

function cleanup (dir) {
  rmSync(dir, { recursive: true, force: true })
}

// --- I2: external symbols resolve, don't flag as missing ---

test('I2: external symbols declared in .codetree.json resolve as external', async (t) => {
  const dir = mkFixture('i2-externals')
  try {
    // Minimal fixture: a fake "project" with a spec that references an external symbol
    writeFileSync(join(dir, 'package.json'), JSON.stringify({
      name: 'myproject', version: '0.1.0', main: 'index.js'
    }))
    writeFileSync(join(dir, 'index.js'), 'export function hello () { return "hi" }\n')
    writeFileSync(join(dir, '.codetree.json'), JSON.stringify({
      externalSymbols: ['an.sign', 'an.open'],
      externalPackages: ['anproto']
    }))
    writeFileSync(join(dir, 'README.md'),
      '# myproject\n\n' +
      'Uses `an.sign()` and `an.open()` from `anproto`.\n' +
      'Our own function: `hello()`.\n'
    )

    const tree = await scan(dir)
    t.ok(tree.docCruft, 'docCruft produced')
    const readme = tree.docCruft.docs.find(d => d.path === 'README.md')
    t.ok(readme, 'README.md found in report')

    // hello() is local and should resolve as valid
    const helloDetail = readme.references.details.find(d => d.ref === 'hello()')
    t.ok(helloDetail, 'hello() reference extracted')
    t.is(helloDetail.status, 'valid', 'hello() resolves as valid')

    // an.sign() and an.open() should be external, not missing
    const signDetail = readme.references.details.find(d => d.ref === 'an.sign()')
    t.ok(signDetail, 'an.sign() reference extracted')
    t.is(signDetail.status, 'external', 'an.sign() resolves as external, not missing')

    const openDetail = readme.references.details.find(d => d.ref === 'an.open()')
    t.ok(openDetail, 'an.open() reference extracted')
    t.is(openDetail.status, 'external', 'an.open() resolves as external')

    // Summary counts separate external from missing
    t.ok(tree.docCruft.summary.externalReferences >= 2, 'external refs counted separately')
  } finally {
    cleanup(dir)
  }
})

test('I2: external refs do NOT inflate staleness score', async (t) => {
  // Same fixture; confirm a doc with only external refs has low/zero staleness.
  const dir = mkFixture('i2-staleness')
  try {
    writeFileSync(join(dir, 'package.json'), JSON.stringify({
      name: 'myproject', version: '0.1.0', main: 'index.js'
    }))
    writeFileSync(join(dir, 'index.js'), 'export const x = 1\n')
    writeFileSync(join(dir, '.codetree.json'), JSON.stringify({
      externalSymbols: ['external_thing']
    }))
    writeFileSync(join(dir, 'docs.md'),
      '# docs\n\n' +
      'References `external_thing` (should not penalize staleness).\n'
    )

    const tree = await scan(dir)
    const doc = tree.docCruft.docs.find(d => d.path === 'docs.md')
    t.ok(doc, 'docs.md found')
    t.is(doc.references.missing, 0, 'external ref not counted as missing')
    t.ok(doc.references.external >= 1, 'external ref counted as external')
    t.ok(doc.staleness.missingRatio === 0, 'missing ratio is 0 for externals-only doc')
  } finally {
    cleanup(dir)
  }
})

// --- I3: placeholder paths filtered out ---

test('I3: placeholder paths are not classified as references', async (t) => {
  const dir = mkFixture('i3-placeholders')
  try {
    writeFileSync(join(dir, 'package.json'), JSON.stringify({
      name: 'myproj', version: '0.1.0', main: 'index.js'
    }))
    writeFileSync(join(dir, 'index.js'), 'export function run () {}\n')
    writeFileSync(join(dir, 'README.md'),
      '# myproj\n\n' +
      'Config lives at `~/.myproj/config.json`.\n' +
      'Run `run()` from `<install-dir>/bin/myproj`.\n' +
      'Paths like `/path/to/install` are examples.\n' +
      'Env variable: `${MY_PROJ_HOME}`.\n'
    )

    const tree = await scan(dir)
    const readme = tree.docCruft.docs.find(d => d.path === 'README.md')
    t.ok(readme, 'README.md found')

    // run() should be found
    const runDetail = readme.references.details.find(d => d.ref === 'run()')
    t.ok(runDetail, 'run() reference extracted')

    // Placeholders should NOT appear in the details at all
    const placeholderDetails = readme.references.details.filter(d =>
      d.ref.startsWith('~/') ||
      d.ref.startsWith('/path/to/') ||
      d.ref.startsWith('<') ||
      d.ref.includes('${')
    )
    t.is(placeholderDetails.length, 0, 'placeholder refs not in details')
  } finally {
    cleanup(dir)
  }
})

// --- I4: deprecated modules don't warn about undocumented items ---
// (Validator-side test; uses a minimal spec fixture.)

test('I4: deprecated module skips undocumented-export/method warnings', async (t) => {
  const dir = mkFixture('i4-deprecated')
  try {
    // Fixture: a project with one module (the "legacy" module) that
    // exposes methods but is marked deprecated in its spec.
    const modDir = join(dir, 'legacy')
    mkdirSync(modDir, { recursive: true })
    writeFileSync(join(modDir, 'package.json'), JSON.stringify({
      name: 'legacy', version: '0.1.0', main: 'index.js'
    }))
    writeFileSync(join(modDir, 'index.js'),
      'export class Legacy {\n' +
      '  foo () {}\n' +
      '  bar () {}\n' +
      '  baz () {}\n' +
      '}\n'
    )

    // Spec dir at scan root
    const specDir = join(dir, 'spec')
    const modulesDir = join(specDir, 'modules')
    mkdirSync(modulesDir, { recursive: true })
    writeFileSync(join(specDir, 'SYSTEM.md'),
      '# System\n\n' +
      '**Last verified:** 2026-04-16\n\n' +
      '### Legacy modules\n\n' +
      '| Module | Location | Purpose |\n' +
      '|---|---|---|\n' +
      '| **legacy** | `legacy/` | Deprecated module |\n'
    )
    writeFileSync(join(modulesDir, 'legacy.md'),
      '# legacy -- Module Spec\n\n' +
      '**Location:** `legacy/`\n' +
      '**Status:** deprecated\n' +
      '**Replaced by:** `NewClass`\n\n' +
      '## What it is\n\nOld module, being removed.\n\n' +
      '## API\n\n```js\nimport { Legacy } from \'legacy\'\nconst l = new Legacy()\n// (methods no longer documented)\n```\n'
    )

    const tree = await scan(dir)
    t.ok(tree.validation, 'validation produced')

    const modReport = tree.validation.modules.legacy
    t.ok(modReport, 'legacy module validated')

    // There should be NO warn-level "Method X is documented" checks for
    // legacy's methods, because the module is deprecated.
    const undocumentedMethodWarnings = modReport.checks.filter(c =>
      c.status === 'warn' && /Method ".*" is documented/.test(c.claim)
    )
    t.is(undocumentedMethodWarnings.length, 0, 'no undocumented-method warnings for deprecated module')

    const undocumentedExportWarnings = modReport.checks.filter(c =>
      c.status === 'warn' && /Export ".*" is documented/.test(c.claim)
    )
    t.is(undocumentedExportWarnings.length, 0, 'no undocumented-export warnings for deprecated module')
  } finally {
    cleanup(dir)
  }
})

test('I4: non-deprecated module still emits undocumented warnings', async (t) => {
  // Contrast fixture — same shape, WITHOUT the deprecated marker.
  const dir = mkFixture('i4-active')
  try {
    const modDir = join(dir, 'active')
    mkdirSync(modDir, { recursive: true })
    writeFileSync(join(modDir, 'package.json'), JSON.stringify({
      name: 'active', version: '0.1.0', main: 'index.js'
    }))
    writeFileSync(join(modDir, 'index.js'),
      'export class Active {\n' +
      '  foo () {}\n' +
      '  bar () {}\n' +
      '}\n'
    )

    const specDir = join(dir, 'spec')
    const modulesDir = join(specDir, 'modules')
    mkdirSync(modulesDir, { recursive: true })
    writeFileSync(join(specDir, 'SYSTEM.md'),
      '# System\n\n**Last verified:** 2026-04-16\n\n' +
      '### Active\n\n| Module | Location | Purpose |\n|---|---|---|\n' +
      '| **active** | `active/` | Active module |\n'
    )
    writeFileSync(join(modulesDir, 'active.md'),
      '# active -- Module Spec\n\n' +
      '**Location:** `active/`\n\n' +
      '## What it is\n\nActive module.\n\n' +
      '## API\n\n```js\nimport { Active } from \'active\'\nconst a = new Active()\n// foo documented, bar is NOT\na.foo()\n```\n'
    )

    const tree = await scan(dir)
    const modReport = tree.validation.modules.active
    t.ok(modReport, 'active module validated')

    // Should warn about `bar` being undocumented
    const undocumentedMethodWarnings = modReport.checks.filter(c =>
      c.status === 'warn' && c.claim.includes('Method "bar"')
    )
    t.ok(undocumentedMethodWarnings.length > 0, 'undocumented method "bar" emits warning when not deprecated')
  } finally {
    cleanup(dir)
  }
})