The workspace index cannot answer questions about an installed framework or SDK, and in plugin development that is where most of the reading goes: 64 of 80 tool calls in one real session went to the harness checkout under /usr/local/lib/node_modules to look up the API being implemented. A root is entered whatever it is called, and its dependencies are not: a path ending in node_modules/... is the normal way to name a package tree, while a node_modules below the root is somebody else's code. A result carries the label that says which tree it came from and an absolute sourcePath, the only form a follow-up read can resolve. The tool description now also claims the move this replaces - searching before reading files to find out what a module contains.
139 lines
6.4 KiB
JavaScript
139 lines
6.4 KiB
JavaScript
import { strict as assert } from 'node:assert'
|
|
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { test } from 'node:test'
|
|
|
|
import {
|
|
ROOT_SKIP_DIRS,
|
|
listRootFiles,
|
|
parseSourceRoot,
|
|
resolveSourceRoots,
|
|
rootRelativePath,
|
|
sanitizeRootName,
|
|
skipsRootDir,
|
|
} from '../lib/roots.js'
|
|
|
|
test('a root defaults its name to the directory basename', () => {
|
|
const parsed = parseSourceRoot('/usr/local/lib/node_modules/@deepseek-ai', '/repo')
|
|
assert.equal(parsed.name, '@deepseek-ai')
|
|
assert.equal(parsed.path, '/usr/local/lib/node_modules/@deepseek-ai')
|
|
assert.equal(parsed.workspaceRelative, false)
|
|
})
|
|
|
|
test('an explicit name is taken as written', () => {
|
|
const parsed = parseSourceRoot('dsh=/usr/local/lib/node_modules/@deepseek-ai', '/repo')
|
|
assert.equal(parsed.name, 'dsh')
|
|
assert.equal(parsed.path, '/usr/local/lib/node_modules/@deepseek-ai')
|
|
})
|
|
|
|
test('a relative root resolves against the workspace and says so', () => {
|
|
const parsed = parseSourceRoot('vendor/lib', '/repo')
|
|
assert.equal(parsed.path, '/repo/vendor/lib')
|
|
assert.equal(parsed.name, 'lib')
|
|
assert.equal(parsed.workspaceRelative, true)
|
|
})
|
|
|
|
test('a name containing a separator is refused, because it would forge a path', () => {
|
|
assert.throws(() => parseSourceRoot('a/b=/tmp/x', '/repo'), /path separator/)
|
|
})
|
|
|
|
test('an entry that names no path or yields no name is refused', () => {
|
|
assert.throws(() => parseSourceRoot('', '/repo'), /empty entry/)
|
|
assert.throws(() => parseSourceRoot('name=', '/repo'), /names no path/)
|
|
// A root that climbs to the filesystem root has no basename to be labelled
|
|
// with, and `resolve` collapsing the traversal is exactly why the check is on
|
|
// the resolved path rather than on the text of the entry.
|
|
assert.throws(() => parseSourceRoot('/../..', '/repo'), /does not yield a usable name/)
|
|
})
|
|
|
|
test('sanitizeRootName keeps the characters a path segment can hold', () => {
|
|
assert.equal(sanitizeRootName('@deepseek-ai'), '@deepseek-ai')
|
|
assert.equal(sanitizeRootName('my root'), 'my-root')
|
|
assert.equal(sanitizeRootName('..'), '')
|
|
assert.equal(sanitizeRootName('.'), '')
|
|
assert.equal(sanitizeRootName(' '), '')
|
|
})
|
|
|
|
test('rootRelativePath never emits a leading or trailing separator', () => {
|
|
assert.equal(rootRelativePath('dsh', 'lib/index.js'), 'dsh/lib/index.js')
|
|
assert.equal(rootRelativePath('dsh', ''), 'dsh')
|
|
})
|
|
|
|
test('resolveSourceRoots skips a bad entry loudly and keeps the rest', () => {
|
|
const result = resolveSourceRoots(['/good', 'missing=/nope', '/good'], {
|
|
workspace: '/repo',
|
|
isDirectory: (path) => path === '/good',
|
|
})
|
|
assert.deepEqual(result.roots, [{ name: 'good', path: '/good' }])
|
|
// A duplicate name is dropped rather than silently overwriting: two roots with
|
|
// one label would make every result from the second ambiguous.
|
|
assert.equal(result.problems.length, 2)
|
|
assert.match(result.problems[0], /not a directory/)
|
|
assert.match(result.problems[1], /duplicates the name/)
|
|
})
|
|
|
|
test('resolveSourceRoots tolerates no configuration at all', () => {
|
|
assert.deepEqual(resolveSourceRoots([], { workspace: '/repo' }), { roots: [], problems: [] })
|
|
assert.deepEqual(resolveSourceRoots(undefined, { workspace: '/repo' }), { roots: [], problems: [] })
|
|
})
|
|
|
|
test('the root itself is entered whatever it is called, and node_modules below it is not', () => {
|
|
// This asymmetry is the feature: a root is normally named `.../node_modules/...`,
|
|
// while a `node_modules` inside one of its packages is somebody else's code.
|
|
assert.equal(skipsRootDir(0, 'node_modules'), false, 'the root must not be skipped')
|
|
assert.equal(skipsRootDir(0, '.git'), false)
|
|
assert.equal(skipsRootDir(1, 'node_modules'), true, 'a nested dependency store must be skipped')
|
|
assert.equal(skipsRootDir(3, 'node_modules'), true)
|
|
assert.equal(skipsRootDir(1, 'dist'), true)
|
|
assert.equal(skipsRootDir(1, 'lib'), false)
|
|
assert.ok(ROOT_SKIP_DIRS.includes('node_modules'), 'the skip list must still name node_modules')
|
|
})
|
|
|
|
test('listRootFiles indexes a package tree and not its dependencies', async (t) => {
|
|
const root = await mkdtemp(join(tmpdir(), 'recall-root-'))
|
|
t.after(() => rm(root, { recursive: true, force: true }))
|
|
const packages = join(root, 'node_modules', '@scope')
|
|
await mkdir(join(packages, 'alpha', 'lib'), { recursive: true })
|
|
await mkdir(join(packages, 'alpha', 'node_modules', 'dep'), { recursive: true })
|
|
await mkdir(join(packages, 'beta', 'dist'), { recursive: true })
|
|
await writeFile(join(packages, 'alpha', 'lib', 'index.js'), 'export const a = 1\n')
|
|
await writeFile(join(packages, 'alpha', 'node_modules', 'dep', 'index.js'), 'module.exports = 0\n')
|
|
await writeFile(join(packages, 'beta', 'dist', 'bundle.js'), 'var x = 1\n')
|
|
await writeFile(join(packages, 'beta', 'index.js'), 'export const b = 2\n')
|
|
await writeFile(join(packages, 'README.md'), '# not code\n')
|
|
|
|
const walk = await listRootFiles({ name: 'dsh', path: packages })
|
|
assert.deepEqual(
|
|
walk.files.map((file) => file.path),
|
|
['dsh/alpha/lib/index.js', 'dsh/beta/index.js'],
|
|
'the root is entered, a dependency below it is not, and dist is not',
|
|
)
|
|
// Every path is labelled but resolvable: `read` needs the absolute one.
|
|
for (const file of walk.files) assert.ok(file.absolute.startsWith(packages), file.absolute)
|
|
})
|
|
|
|
test('listRootFiles does not follow a symlink out of the root', async (t) => {
|
|
const root = await mkdtemp(join(tmpdir(), 'recall-root-'))
|
|
const outside = await mkdtemp(join(tmpdir(), 'recall-outside-'))
|
|
t.after(async () => {
|
|
await rm(root, { recursive: true, force: true })
|
|
await rm(outside, { recursive: true, force: true })
|
|
})
|
|
await mkdir(join(root, 'pkg'), { recursive: true })
|
|
await writeFile(join(root, 'pkg', 'a.js'), 'export const a = 1\n')
|
|
await writeFile(join(outside, 'secret.js'), 'const secret = 1\n')
|
|
await symlink(outside, join(root, 'link'), 'dir')
|
|
|
|
const walk = await listRootFiles({ name: 'r', path: root })
|
|
assert.deepEqual(walk.files.map((file) => file.path), ['r/pkg/a.js'])
|
|
})
|
|
|
|
test('a file at the root of a root is labelled with the root name alone', async (t) => {
|
|
const root = await mkdtemp(join(tmpdir(), 'recall-root-'))
|
|
t.after(() => rm(root, { recursive: true, force: true }))
|
|
await writeFile(join(root, 'index.js'), 'export const root = 1\n')
|
|
const walk = await listRootFiles({ name: 'solo', path: root })
|
|
assert.deepEqual(walk.files.map((file) => file.path), ['solo/index.js'])
|
|
})
|