This repository has been archived on 2026-09-03. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
open-brainy/tests/vfs/vfs-search-path-scope.unit.test.ts
David Snelling 2c5e34748e test(gate): the coverage guard counts the perf lane's config as a gate
tests/configs/vitest.perf.config.ts (npm run test:perf) is a real gate,
not a manual-only slot, so inGate() now recognizes its include list
(tests/performance/** plus the four named files) directly. The 7 files
already correctly listed as perf move out of MANUAL_ONLY, which is now
reserved for files no automated lane covers.

That alone left the guard red: tests/vfs/vfs-search-path-scope.test.ts
was a genuine new orphan (added this cycle, named without the
.unit.test.ts suffix its siblings use) — it ran under the broad root
gate but silently missed test:unit. Renamed to match the sibling
convention in tests/vfs/, which puts it back in the unit gate.
2026-09-02 11:47:05 -07:00

165 lines
7.1 KiB
TypeScript

/**
* @module tests/vfs/vfs-search-path-scope.unit
* @description `vfs.search({ path })` scopes with a SERVED filter.
*
* The scope used to be emitted as `path: { $startsWith }` — an operator that is
* not in the filter vocabulary at all, and whose `$`-less spelling the metadata
* index refuses by the served-operator law (an equality/range posting index
* cannot evaluate a substring without reading every row). Every path-scoped VFS
* search threw; none has ever worked on this engine line.
*
* The scope is now a half-open range over `metadata.path`, which is the VFS's
* truth, is indexed on every VFS entity, and is served by the ordered range
* operators: `[dir + '/', dir + '0')` — '0' being the code point after '/', so
* membership in the range is EXACTLY "carries the prefix `dir/`". The
* non-recursive scope is the directory's own identity, `parent`, an equality.
*
* These pins hold the answer (descendants at every depth, siblings never — the
* `/scope-sibling` trap included), the shape (the operators the search emits
* are answered by the index's own door, never refused), and the law that the
* scope narrows the search BEFORE it runs rather than filtering an over-fetch.
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
import { Brainy } from '../../src/brainy.js'
import { VFSErrorCode } from '../../src/vfs/types.js'
/** A word every fixture file carries, so the text leg reaches all of them. */
const TOKEN = 'quasar'
describe('vfs.search({ path }) scopes with a served filter', () => {
let brain: Brainy
let vfs: VirtualFileSystem
/** In scope for '/scope', at three depths. */
const inScope = ['/scope/a.txt', '/scope/sub/b.txt', '/scope/sub/deep/c.txt']
/** Out of scope — including the two prefix traps a naive test misses. */
const outOfScope = ['/scope-sibling/d.txt', '/scope0/e.txt', '/elsewhere/f.txt', '/g.txt']
beforeAll(async () => {
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
await brain.init()
vfs = brain.vfs
await vfs.init()
await vfs.mkdir('/scope/sub/deep', { recursive: true })
await vfs.mkdir('/scope-sibling', { recursive: true })
await vfs.mkdir('/scope0', { recursive: true })
await vfs.mkdir('/elsewhere', { recursive: true })
for (const path of [...inScope, ...outOfScope]) {
await vfs.writeFile(path, `${TOKEN} content for ${path}`)
}
})
afterAll(async () => {
await vfs?.close()
await brain?.close()
})
it('includes every descendant depth and excludes every sibling', async () => {
const results = await vfs.search(TOKEN, { path: '/scope', limit: 50 })
const paths = results.map((r) => r.path).sort()
expect(paths).toEqual([...inScope].sort())
for (const path of outOfScope) expect(paths).not.toContain(path)
})
it('a trailing slash and a doubled slash name the same scope', async () => {
const plain = await vfs.search(TOKEN, { path: '/scope', limit: 50 })
const trailing = await vfs.search(TOKEN, { path: '/scope/', limit: 50 })
const doubled = await vfs.search(TOKEN, { path: '//scope//', limit: 50 })
const ids = (rs: Array<{ entityId: string }>) => rs.map((r) => r.entityId).sort()
expect(ids(trailing)).toEqual(ids(plain))
expect(ids(doubled)).toEqual(ids(plain))
})
it('the root scope is every VFS file — it adds no clause to narrow with', async () => {
const rooted = await vfs.search(TOKEN, { path: '/', limit: 50 })
const unscoped = await vfs.search(TOKEN, { limit: 50 })
const paths = rooted.map((r) => r.path).sort()
expect(paths).toEqual([...inScope, ...outOfScope].sort())
expect(paths).toEqual(unscoped.map((r) => r.path).sort())
})
it('recursive: false is the immediate children, not the subtree', async () => {
const results = await vfs.search(TOKEN, { path: '/scope', recursive: false, limit: 50 })
expect(results.map((r) => r.path)).toEqual(['/scope/a.txt'])
})
it('recursive: false on a path that does not exist refuses by name', async () => {
await expect(
vfs.search(TOKEN, { path: '/no-such-dir', recursive: false, limit: 50 })
).rejects.toMatchObject({ code: VFSErrorCode.ENOENT })
})
it('every operator the search emits is ANSWERED by the index door, never refused', async () => {
const index = (brain as any).metadataIndex
const emitted: any[] = []
const find = vi.spyOn(brain as any, 'find')
try {
await vfs.search(TOKEN, { path: '/scope', limit: 50 })
await vfs.search(TOKEN, { path: '/scope/sub', where: { mimeType: 'text/plain' }, limit: 50 })
await vfs.search(TOKEN, { path: '/scope', recursive: false, limit: 50 })
await vfs.search(TOKEN, { path: '/', limit: 50 })
for (const call of find.mock.calls) emitted.push((call[0] as any).where)
} finally {
find.mockRestore()
}
expect(emitted).toHaveLength(4)
for (const where of emitted) {
// The door itself is the judge: an operator outside the served set is
// REFUSED here (BrainyError INVALID_QUERY), never answered.
await expect(index.getIdsForFilter(where)).resolves.toBeInstanceOf(Array)
}
// And the scope really is a range on the path — the shape this fix chose.
expect(emitted[0].path).toEqual({ gte: '/scope/', lt: '/scope0' })
expect(emitted[3].path).toBeUndefined()
})
it('the scope narrows the search before it runs — no over-fetch to filter', async () => {
const index = (brain as any).metadataIndex
const filter = vi.spyOn(index, 'getIdsForFilter')
let universe: string[] = []
try {
await vfs.search(TOKEN, { path: '/scope', limit: 50 })
// The search's own call — the one carrying the scope. (Path resolution
// asks this same door for the root, before the search is built.)
const scoped = filter.mock.calls.findIndex(
(c) => (c[0] as any)?.path?.gte === '/scope/'
)
expect(scoped).toBeGreaterThanOrEqual(0)
universe = (await filter.mock.results[scoped].value) as string[]
} finally {
filter.mockRestore()
}
// The id universe the index resolved for the search is already the scope:
// three files, and not one row from outside it.
const rows = await brain.batchGet(universe)
const paths = [...rows.values()].map((e: any) => e.metadata.path).sort()
expect(paths).toEqual([...inScope].sort())
})
it('the range answers the same ids as walking the tree', async () => {
// The path is the truth and the Contains edges are its projection; a scope
// read from the truth must agree with one walked over the projection.
const walked: string[] = []
const walk = async (dir: string): Promise<void> => {
for (const name of await vfs.readdir(dir)) {
const child = dir === '/' ? `/${name}` : `${dir}/${name}`
const stat = await vfs.stat(child)
if (stat.isDirectory()) await walk(child)
else walked.push(child)
}
}
await walk('/scope')
const searched = await vfs.search(TOKEN, { path: '/scope', limit: 50 })
expect(searched.map((r) => r.path).sort()).toEqual(walked.sort())
})
})