// conflict-detail — classify WHAT differs in a BUILD_SKEW so a publish can
// be triaged (code-differs = real; docs-only/noise = benign) instead of
// accepted blind. Deliberately conservative: only a pure-META diff is
// 'noise'; anything not positively doc/meta counts as significant.
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import path from '#node:path'
import { classifyFile, diffTrees, classifyConflicts, summarize, bumpRecommendations } from '../lib/conflict-detail.js'
function tmp () {
return fs.mkdtempSync(path.join(os.tmpdir(), 'pgh-cd-'))
}
function write (dir, rel, content) {
const p = path.join(dir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, content)
}
test('classifyFile buckets', (t) => {
t.is(classifyFile('index.js'), 'CODE')
t.is(classifyFile('lib/foo.cjs'), 'CODE')
t.is(classifyFile('package.json'), 'CODE')
t.is(classifyFile('prebuilds/x.node'), 'CODE')
t.is(classifyFile('README.md'), 'DOC')
t.is(classifyFile('LICENSE'), 'DOC')
t.is(classifyFile('CHANGELOG.md'), 'DOC')
t.is(classifyFile('.npmignore'), 'META')
t.is(classifyFile('.gitattributes'), 'META')
t.is(classifyFile('.prettierrc'), 'META')
// Top-level dev/test/CI dirs are DEV (benign, not runtime code)
t.is(classifyFile('.github/workflows/test.yml'), 'DEV')
t.is(classifyFile('test/all.js'), 'DEV')
t.is(classifyFile('example/index.js'), 'DEV')
t.is(classifyFile('bench/run.js'), 'DEV')
// ...but a nested helper under runtime code stays CODE (first segment only)
t.is(classifyFile('lib/test-helper.js'), 'CODE')
// Genuinely-unknown files at the root are OTHER → still reviewed
t.is(classifyFile('binding.gyp'), 'OTHER')
})
test('diffTrees — only a .npmignore differs ⇒ noise', (t) => {
const a = tmp(); const b = tmp()
write(a, 'index.js', 'module.exports = 1\n')
write(a, 'package.json', '{"name":"x","version":"1.0.0"}')
write(a, '.npmignore', 'test\n')
write(b, 'index.js', 'module.exports = 1\n')
write(b, 'package.json', '{"name":"x","version":"1.0.0"}')
const d = diffTrees(a, b)
t.is(d.verdict, 'noise')
t.alike(d.files.map(f => f.path), ['.npmignore'])
})
test('diffTrees — README differs ⇒ docs-only', (t) => {
const a = tmp(); const b = tmp()
write(a, 'index.js', 'X\n'); write(b, 'index.js', 'X\n')
write(a, 'README.md', 'v1\n'); write(b, 'README.md', 'v2 different\n')
const d = diffTrees(a, b)
t.is(d.verdict, 'docs-only')
})
test('diffTrees — index.js content differs ⇒ code-differs (REVIEW)', (t) => {
const a = tmp(); const b = tmp()
write(a, 'index.js', 'module.exports = 1\n')
write(b, 'index.js', 'module.exports = 2\n')
write(a, '.npmignore', 'noise\n') // present too, but code dominates
const d = diffTrees(a, b)
t.is(d.verdict, 'code-differs')
t.ok(d.files.some(f => f.path === 'index.js' && f.kind === 'CODE' && f.status === 'content'))
})
test('diffTrees — a top-level test/ file differs ⇒ docs-only (not a runtime conflict)', (t) => {
const a = tmp(); const b = tmp()
write(a, 'index.js', 'X\n'); write(b, 'index.js', 'X\n')
write(a, 'test/all.js', 'tests\n') // only in A; test/ is not runtime
const d = diffTrees(a, b)
t.is(d.verdict, 'docs-only')
})
test('diffTrees — a NESTED test helper under lib/ differs ⇒ code-differs (still runtime)', (t) => {
const a = tmp(); const b = tmp()
write(a, 'lib/util.js', '1\n'); write(b, 'lib/util.js', '2\n')
const d = diffTrees(a, b)
t.is(d.verdict, 'code-differs')
})
test('diffTrees — byte-identical trees ⇒ identical (no file diff)', (t) => {
const a = tmp(); const b = tmp()
write(a, 'index.js', 'same\n'); write(b, 'index.js', 'same\n')
const d = diffTrees(a, b)
t.is(d.verdict, 'identical')
t.is(d.files.length, 0)
})
test('bumpRecommendations (B) — names workspace deps that skewed, skips npm deps', (t) => {
const lockfile = {
deps: [
{ name: 'hyper-overlay', version: '0.2.5', workspace: true, _sourcePath: '/fleet/hyper-overlay' },
{ name: 'bogon', version: '1.3.0', workspace: false } // third-party, not ours
]
}
const conflicts = [
{ type: 'BUILD_SKEW', dep: 'hyper-overlay', newVersion: '0.2.5' },
{ type: 'BUILD_SKEW', dep: 'bogon', newVersion: '1.3.0' },
{ type: 'VERSION_CONFLICT', dep: 'hyper-overlay', newVersion: '0.2.5' } // not a skew
]
const recs = bumpRecommendations(conflicts, lockfile)
t.is(recs.length, 1, 'only the workspace BUILD_SKEW is recommended')
t.is(recs[0].pkg, 'hyper-overlay')
t.is(recs[0].sourcePath, '/fleet/hyper-overlay')
})
test('classifyConflicts + summarize — end to end with a resolver', (t) => {
const codeA = tmp(); const codeB = tmp()
write(codeA, 'index.js', '1\n'); write(codeB, 'index.js', '2\n')
const noiseA = tmp(); const noiseB = tmp()
write(noiseA, 'index.js', 'k\n'); write(noiseB, 'index.js', 'k\n'); write(noiseA, '.npmignore', 'x\n')
const resolve = (h) => ({ ca: codeA, cb: codeB, na: noiseA, nb: noiseB }[h] || null)
const conflicts = [
{ type: 'VERSION_CONFLICT', dep: 'foo', newVersion: '2.0.0', existing: [{ appName: 'other', version: '1.0.0' }] },
{ type: 'BUILD_SKEW', dep: 'bar', newVersion: '1.0.0', newTree: 'ca', existing: [{ appName: 'other', version: '1.0.0', tree: 'cb' }] },
{ type: 'BUILD_SKEW', dep: 'baz', newVersion: '1.0.0', newTree: 'na', existing: [{ appName: 'other', version: '1.0.0', tree: 'nb' }] },
{ type: 'BUILD_SKEW', dep: 'qux', newVersion: '1.0.0', newTree: 'missing', existing: [{ appName: 'other', version: '1.0.0', tree: 'alsomissing' }] }
]
classifyConflicts(conflicts, resolve)
t.is(conflicts[1].detail.verdict, 'code-differs')
t.is(conflicts[2].detail.verdict, 'noise')
t.is(conflicts[3].detail.verdict, 'unknown', 'missing tree ⇒ unknown, not guessed')
const s = summarize(conflicts)
t.is(s.version, 1)
t.is(s.codeDiffers, 1)
t.is(s.noise, 1)
t.is(s.unknown, 1)
})