From b3026f64763f879ba7a7bd031aaaca3f9553bf75 Mon Sep 17 00:00:00 2001 From: Beth-2709 Date: Sun, 18 Oct 2026 04:49:22 -0400 Subject: [PATCH] fix(workspace): glob and grep start where their pattern does On a mount larger than its walk cap, glob and grep walked the whole mount, or their path, and could reach the cap before the directory a pattern names, so a glob for one repository's sources in a mount of several checkouts answered with no matches. glob now starts its pass at the directory its pattern's segments name before the first wildcard, beneath its path, and grep at the deeper of its path and its glob filter's directory, since the filter matches mount-relative paths. Every path a pattern matches lies beneath that directory. The start goes down only through directories disk lists with exactly the pattern's spelling, and never through a link or into a directory the mount passes over by design, so on a case-insensitive disk a pattern spelled in another case walks from above and matches nothing, as before, rather than taking files in under its spelling. It lists only directories it has checked lie inside the mount. Within the cap the matches are the same. A pattern into a directory the mount passes over by design names that directory under incomplete, as a search given it as its path does, and a search's incomplete names only what lies beneath the directory its walk starts at. A grep whose path and filter don't overlap searches nothing. A search that starts below the root takes in, and announces, only what changed beneath its start, and no longer marks the mount's first whole sync done. The docs of walkCap and initialSyncDone say so. Co-Authored-By: Claude Opus 5.5 --- .../search-starts-at-pattern-dir.fixed.md | 1 + src/modules/workspace/index.ts | 82 ++++++- src/modules/workspace/types.ts | 15 +- test/workspace-three-way.test.ts | 209 ++++++++++++++++++ 4 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 changelog.d/search-starts-at-pattern-dir.fixed.md diff --git a/changelog.d/search-starts-at-pattern-dir.fixed.md b/changelog.d/search-starts-at-pattern-dir.fixed.md new file mode 100644 index 000000000..9840f3354 --- /dev/null +++ b/changelog.d/search-starts-at-pattern-dir.fixed.md @@ -0,0 +1 @@ +- The workspace's `glob` starts its walk at the directory its pattern names before its first wildcard, and `grep` at the deeper of its `path` and the directory its `glob` filter names that way. On a mount larger than its walk cap, a search for one directory's files then finds them, as long as that directory's own walk fits the cap. A walk of the whole mount could reach the cap before it got there and answer with no matches. The walk starts only as deep as disk lists each segment with the pattern's spelling, so on a case-insensitive disk a pattern spelled in another case finds nothing, as before. A search's `incomplete` now names only what lies beneath the directory its walk starts at, and a pattern into a directory the mount passes over by design (its ignore list, or another mount's directory) names that directory there. The start never goes through a link, as a walk of the whole mount doesn't. A `grep` whose `path` and filter name directories that don't overlap searches nothing. A search that starts below the mount root takes in, and announces, only the disk changes beneath its start, and no longer marks the mount's first whole sync done in `status`. diff --git a/src/modules/workspace/index.ts b/src/modules/workspace/index.ts index fe2c7e0ed..2541dcd77 100644 --- a/src/modules/workspace/index.ts +++ b/src/modules/workspace/index.ts @@ -8,7 +8,7 @@ import { isUtf8 } from 'node:buffer'; import { constants as fsConstants } from 'node:fs'; import type { Stats } from 'node:fs'; -import { open, readFile, stat, lstat, realpath } from 'node:fs/promises'; +import { open, readFile, readdir, stat, lstat, realpath } from 'node:fs/promises'; import { isAbsolute, join, resolve, relative, sep } from 'node:path'; import type { JsStore } from '@animalabs/chronicle'; import type { Module, ModuleContext, ProcessState, EventResponse } from '../../types/module.js'; @@ -40,7 +40,7 @@ import { MountWatcher, type FsChange } from './watcher.js'; import { hashContent, DEFAULT_MAX_FILE_SIZE } from './sync.js'; import { DiskAgreement, sameRootIdentity } from './disk-agreement.js'; import { BranchIntents } from './branch-intent.js'; -import { IGNORED, OTHER_MOUNT, filesystemTrustsCtime, inOtherMount, locateRoot, passedOverAt, readPrefix } from './observe.js'; +import { IGNORED, OTHER_MOUNT, checkDir, filesystemTrustsCtime, inOtherMount, locateRoot, passedOverAt, readPrefix } from './observe.js'; import { TOOL_BATCH_HOOK_TIMEOUT_MS, ensureStateRegistered } from '../../module-registry.js'; import { type MountRuntime, @@ -853,6 +853,33 @@ export class WorkspaceModule implements Module { }; } + /** + * Where a search's walk starts: `base`, its path as given, then each + * segment of `beneath` while disk lists it, in the directory reached so + * far, as a directory with exactly that spelling. The walk names what it + * finds from its start's spelling, and a case-insensitive disk would let + * it go on under a spelling disk doesn't list, taking files in under that + * spelling beside their own. So the start stops before a segment spelled + * any other way, not there, or not a directory, and before a link, which + * a walk of the whole mount doesn't go through either. It lists only + * directories it has checked lie inside the mount, and none the mount + * passes over by design, which the walk then answers for. + */ + private async walkStart(mount: MountState, base: string, beneath: string): Promise { + if (beneath === '') return base; + const { view, rootReal } = await this.runtime(mount); + let start = base; + for (const segment of beneath.split('/')) { + if (start !== '' && passedOverAt(start, view.ignore, view.others) !== undefined) break; + const here = await checkDir(view, rootReal, start); + if (here.kind !== 'checked') break; + const entry = (await readdir(here.dir.real, { withFileTypes: true }).catch(() => [])).find((e) => e.name === segment); + if (entry === undefined || !entry.isDirectory()) break; + start = start ? `${start}/${segment}` : segment; + } + return start; + } + /** One disk→store pass; the caller holds the mount's turn. Emits its events. */ private async passUnlocked(mount: MountState, scope: Scope, opts: PassOptions = {}): Promise { const result = await reconcilePass(this.getStore(), this.agreementOrThrow(), await this.runtime(mount), scope, opts); @@ -2680,9 +2707,14 @@ export class WorkspaceModule implements Module { ? [this.parsePath(input.path)] : [...this.mounts.values()].map(m => ({ mount: m, relativePath: '' })); + // Every match lies beneath the pattern's literal directory, so the walk + // starts there: past the walk's cap, a walk of the whole scope can stop + // before it gets there. + const patternDir = literalDirOf(input.pattern); for (const { mount, relativePath } of mountsToSearch) { + const start = await this.walkStart(mount, relativePath, patternDir); const pass = await this.withMount(mount, () => - this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true })); + this.passUnlocked(mount, { kind: 'dir', dir: start, recursive: true })); for (const report of [...pass.reports.values()].sort((a, b) => a.path.localeCompare(b.path))) { const testPath = relativePath ? report.path.slice(relativePath.length + 1) : report.path; if (regex.test(testPath)) { @@ -2716,6 +2748,10 @@ export class WorkspaceModule implements Module { } const fileGlob = input.glob ? globToRegex(input.glob) : null; + // The filter matches mount-relative paths, so every file it lets through + // lies beneath its literal directory, and the walk starts at the deeper of + // that and `path`. + const filterDir = input.glob ? literalDirOf(input.glob) : ''; const contextBefore = input.contextBefore ?? 0; const contextAfter = input.contextAfter ?? 0; @@ -2753,6 +2789,11 @@ export class WorkspaceModule implements Module { incomplete.push({ path: `${mount.config.name}/${relativePath}`, reason: this.otherMountNote(mount.config.name, relativePath, 'list') }); continue; } + // A `path` and a filter whose directories don't overlap leave no file + // the filter can let through, so nothing is searched. + const deeper = deeperOf(relativePath, filterDir); + if (deeper === null) continue; + const start = await this.walkStart(mount, relativePath, deeper === relativePath ? '' : deeper.slice(relativePath ? relativePath.length + 1 : 0)); await this.withMount(mount, async () => { // A `path` naming a single file greps just that file; otherwise it is a // directory prefix. (A file path once produced the prefix "notes.md/" @@ -2770,7 +2811,7 @@ export class WorkspaceModule implements Module { for (let attempt = 0; attempt < 3 && pass === null; attempt++) { const next = await this.passUnlocked(mount, fileScope ? { kind: 'paths', paths: [relativePath] } - : { kind: 'dir', dir: relativePath, recursive: true }); + : { kind: 'dir', dir: start, recursive: true }); if (next.branchId === undefined || next.branchId === store.currentBranch().id) pass = next; } if (pass === null) { @@ -3401,10 +3442,10 @@ export class WorkspaceModule implements Module { const scan: AgentActionScan = { late: false, settled: Promise.resolve() }; const pass = this.withMount(mount, () => { if (mount.agentActionScanWaiting === scan) mount.agentActionScanWaiting = undefined; - // From the mount's start, as a listing walks, keeping no place: on a - // mount over its walkCap, every batch's scan takes the first walkCap - // entries in the walk's order, and the place that syncs and catch-up - // scans resume from stays theirs. + // From the mount's root, keeping no place, as a listing keeps none: + // on a mount over its walkCap, every batch's scan takes the first + // walkCap entries in the walk's order, and the place that syncs and + // catch-up scans resume from stays theirs. return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }); }); scan.settled = pass.then( @@ -3649,6 +3690,31 @@ function globToRegex(pattern: string): RegExp { return new RegExp(`^${regex}$`); } +/** + * The directory a glob pattern names before its first wildcard, beneath + * which every path it matches lies: its leading segments that hold none of + * globToRegex's wildcards (`*`, `?`, `{`), short of the last segment, which + * names the files. '' when the pattern starts with a wildcard or has a + * single segment. A segment that's empty, `.` or `..`, which no path + * matched holds, is one no directory listing holds either, so walkStart + * stops before it. + */ +function literalDirOf(pattern: string): string { + const dir: string[] = []; + for (const segment of pattern.split('/').slice(0, -1)) { + if (/[*?{]/.test(segment)) break; + dir.push(segment); + } + return dir.join('/'); +} + +/** The deeper of two mount-relative directories, '' being the root, or null when neither holds the other. */ +function deeperOf(a: string, b: string): string | null { + if (a === '' || b === a || b.startsWith(a + '/')) return b; + if (b === '' || a.startsWith(b + '/')) return a; + return null; +} + /** A glob's refusal of a brace form it can't read, with the way to match a literal "{". */ function braceRefusal(pattern: string, problem: string): string { return `glob pattern ${JSON.stringify(pattern)} ${problem}. To match a "{" in a name, put "?" in its place.`; diff --git a/src/modules/workspace/types.ts b/src/modules/workspace/types.ts index 9ce59bd63..75dd13de4 100644 --- a/src/modules/workspace/types.ts +++ b/src/modules/workspace/types.ts @@ -63,9 +63,13 @@ export interface MountConfig { * kept with the mount's disk evidence, so a restart resumes there, unless * the machine went down before the store made the latest place durable. * An agent-action scan (`watch: 'on-agent-action'`) and a listing (`ls`, - * `glob`, `grep`) take up to it from their scope's start every time. Each - * directory's listing is read whole, so a deletion is seen wherever the - * walk lists, whatever it took. + * `glob`, `grep`) take up to it from their scope's start every time: + * `glob`'s is the directory its pattern's segments before the first + * wildcard name beneath its `path`, and `grep`'s the deeper of its `path` + * and its `glob` filter's directory, as far as disk lists each segment + * with that spelling as a directory, not a link. Each directory's listing + * is read whole, so a deletion is seen wherever the walk lists, whatever + * it took. * * It bounds what a walk observes, not the walk's own work: directories * don't count against it, and each one the walk visits is read whole, so @@ -143,7 +147,10 @@ export interface MountState { lastMaterializedSeq: number; /** Paths currently suppressed from watcher (recently materialized) */ suppressedPaths: Set; - /** Whether a full scan of the mount has completed this session */ + /** + * Whether a pass over the whole mount has run this session. On a mount + * over its walkCap, that pass took the first walkCap entries. + */ initialSyncDone: boolean; /** Branch ID that was active when this mount last materialized */ lastMaterializedBranchId: string | null; diff --git a/test/workspace-three-way.test.ts b/test/workspace-three-way.test.ts index 73580b8cf..f293660eb 100644 --- a/test/workspace-three-way.test.ts +++ b/test/workspace-three-way.test.ts @@ -5542,6 +5542,215 @@ describe('a mount larger than its walk cap (agent-framework #272)', () => { }); }); +describe('a search starts at the directory its pattern names', () => { + const matchesOf = async (m: WorkspaceModule, input: Record): Promise => + ((await call(m, 'glob', input)).matches as Array<{ path: string }>).map((match) => match.path); + const grepped = async (m: WorkspaceModule, input: Record): Promise => + ((await call(m, 'grep', input)).results as Array<{ file: string }>).map((result) => result.file); + /** + * At a walk cap of 2, a walk of the whole mount takes a's first two files + * and stops before z, and a walk of z takes its own two and stops before + * z/deep. + */ + const pastTheCap = (env: Env): void => { + for (const f of ['a/1.txt', 'a/2.txt', 'a/3.txt', 'z/1.txt', 'z/2.txt']) env.writeDisk(f, 'filler'); + env.writeDisk('z/deep/x.ts', 'a needle'); + env.writeDisk('z/deep/y.ts', 'nothing here'); + }; + + // Each search runs on a mount of its own: one before it would have taken + // z/deep's files in, and a walk reports what the store holds wherever it + // stopped. + test('past the walk cap, glob finds the files in the directory its pattern names', async (t) => { + const cases: Array<[Record, string[]]> = [ + [{ pattern: '**/*.ts' }, []], + [{ pattern: 'z/deep/*.ts' }, ['work/z/deep/x.ts', 'work/z/deep/y.ts']], + [{ path: 'work/z', pattern: 'deep/*.ts' }, ['work/z/deep/x.ts', 'work/z/deep/y.ts']], + // A pattern with no literal directory starts at the path. + [{ path: 'work/z/deep', pattern: '*.ts' }, ['work/z/deep/x.ts', 'work/z/deep/y.ts']], + ]; + for (const [input, want] of cases) { + const env = new Env(t); + pastTheCap(env); + const m = await env.open({ walkCap: 2 }); + assert.deepEqual(await matchesOf(m, input), want, JSON.stringify(input)); + } + }); + + test('past the walk cap, grep searches what its filter names, from the deeper of the filter\'s directory and its path', async (t) => { + const cases: Array<[Record, string[]]> = [ + [{ pattern: 'needle' }, []], + [{ pattern: 'needle', glob: 'z/deep/*.ts' }, ['work/z/deep/x.ts']], + [{ pattern: 'needle', path: 'work/z', glob: 'z/deep/**' }, ['work/z/deep/x.ts']], + [{ pattern: 'needle', path: 'work/z/deep', glob: 'z/**' }, ['work/z/deep/x.ts']], + [{ pattern: 'needle', path: 'work/z/deep', glob: 'z/deep/*.ts' }, ['work/z/deep/x.ts']], + ]; + for (const [input, want] of cases) { + const env = new Env(t); + pastTheCap(env); + const m = await env.open({ walkCap: 2 }); + assert.deepEqual(await grepped(m, input), want, JSON.stringify(input)); + } + }); + + // Each list is what a walk of the whole mount finds: the test passes on + // the code before searches started at their pattern's directory too. + test('within the cap, each pattern finds what a walk of the whole mount finds, and leaves nothing incomplete', async (t) => { + const env = new Env(t); + for (const f of ['README.md', 'src/a.ts', 'src/lib/b.ts', 'src/lib/c.js', 'test/a.test.ts']) env.writeDisk(f, 'a needle'); + const m = await env.open(); + const cases: Array<[string, string[]]> = [ + ['**/*.ts', ['src/a.ts', 'src/lib/b.ts', 'test/a.test.ts']], + ['src/*.ts', ['src/a.ts']], + ['src/lib/b.ts', ['src/lib/b.ts']], + ['src/{lib,x}/*.js', ['src/lib/c.js']], + ['src/l?b/*', ['src/lib/b.ts', 'src/lib/c.js']], + ['README.md', ['README.md']], + // A segment that's empty, `.` or `..` ends the directory, and none matches. + ['./src/*.ts', []], + ['../work/src/*.ts', []], + ['src//a.ts', []], + // A directory that isn't there, and one beneath a file. + ['nowhere/*.ts', []], + ['README.md/*.ts', []], + ]; + for (const [pattern, expected] of cases) { + const want = expected.map((p) => `work/${p}`); + const found = await call(m, 'glob', { pattern }); + assert.deepEqual(found.matches.map((match: { path: string }) => match.path), want, `glob ${pattern}`); + assert.deepEqual(found.incomplete ?? [], [], `glob ${pattern}`); + const searched = await call(m, 'grep', { pattern: 'needle', glob: pattern }); + assert.deepEqual(searched.results.map((result: { file: string }) => result.file), want, `grep's filter ${pattern}`); + assert.deepEqual(searched.incomplete ?? [], [], `grep's filter ${pattern}`); + } + }); + + test('a wildcard segment walks from above it, even where a directory has its name', async (t) => { + const env = new Env(t); + for (const dir of ['lib', 'l?b', 'a', 'b', '{a,b}', '*']) env.writeDisk(`${dir}/x.ts`, 'a needle'); + const m = await env.open(); + const cases: Array<[string, string[]]> = [ + ['l?b/x.ts', ['l?b', 'lib']], + ['{a,b}/x.ts', ['a', 'b']], + ['*/x.ts', ['lib', 'l?b', 'a', 'b', '{a,b}', '*']], + ]; + for (const [pattern, dirs] of cases) { + const want = dirs.map((dir) => `work/${dir}/x.ts`).sort(); + assert.deepEqual((await matchesOf(m, { pattern })).sort(), want, `glob ${pattern}`); + assert.deepEqual((await grepped(m, { pattern: 'needle', glob: pattern })).sort(), want, `grep's filter ${pattern}`); + } + }); + + test('grep with a path and a filter whose directories don\'t overlap searches nothing, and takes nothing in', async (t) => { + const env = new Env(t); + env.writeDisk('src/a.ts', 'a needle'); + env.writeDisk('test/a.test.ts', 'a needle'); + env.writeDisk('testing/a.ts', 'a needle'); + const m = await env.open(); + assert.deepEqual(await grepped(m, { pattern: 'needle', path: 'work/test', glob: 'src/**' }), []); + // A sibling whose name starts with the path's lies outside it. + assert.deepEqual(await grepped(m, { pattern: 'needle', path: 'work/test', glob: 'testing/**' }), []); + for (const path of ['test/a.test.ts', 'src/a.ts', 'testing/a.ts']) { + assert.equal(env.store.treeGet(TREE, path), null, `no pass took in ${path}`); + } + }); + + // On a case-insensitive disk, as macOS's is by default, a walk started at + // `SRC` would name src's files `SRC/…` and take them in beside their own. + // Elsewhere there's no `SRC` to walk, and this holds either way. + test('a pattern that spells its directory in another case walks from where disk lists it, finds nothing, and takes nothing in under that spelling', async (t) => { + const env = new Env(t); + env.writeDisk('src/a.ts', 'a needle'); + const m = await env.open(); + assert.deepEqual(await matchesOf(m, { pattern: 'SRC/*.ts' }), []); + assert.deepEqual(await grepped(m, { pattern: 'needle', glob: 'SRC/*.ts' }), []); + assert.equal(env.store.treeGet(TREE, 'SRC/a.ts'), null); + assert.ok(!env.eventsOf('workspace:created').flatMap((e) => e.paths).includes('work/SRC/a.ts')); + // The walk started from the root, where disk lists `src`, and took the + // file in under its own spelling. + assert.notEqual(env.store.treeGet(TREE, 'src/a.ts'), null); + }); + + test('a pattern whose directory disk doesn\'t list walks from the deepest directory it does', async (t) => { + const env = new Env(t); + pastTheCap(env); + const m = await env.open({ walkCap: 2 }); + // From z, the walk stops before z/deep. + const found = await call(m, 'glob', { pattern: 'z/nowhere/*.ts' }); + assert.deepEqual(found.matches, []); + assert.deepEqual(found.incomplete?.map((r: { path: string }) => r.path), ['work/z/deep'], JSON.stringify(found)); + const searched = await call(m, 'grep', { pattern: 'needle', glob: 'z/nowhere/*.ts' }); + assert.deepEqual(searched.results, []); + assert.deepEqual(searched.incomplete?.map((r: { path: string }) => r.path), ['work/z/deep'], JSON.stringify(searched)); + }); + + test('a search that starts lower takes in and announces only what lies beneath its start, and doesn\'t count as the first whole sync', async (t) => { + const env = new Env(t); + env.writeDisk('src/a.ts', 'one'); + env.writeDisk('docs/b.md', 'two'); + const m = await env.open(); + await call(m, 'glob', { pattern: 'src/*.ts' }); + assert.notEqual(env.store.treeGet(TREE, 'src/a.ts'), null, 'beneath the start: taken in'); + assert.equal(env.store.treeGet(TREE, 'docs/b.md'), null, 'elsewhere: left to a pass that covers it'); + assert.deepEqual(env.eventsOf('workspace:created').flatMap((e) => e.paths), ['work/src/a.ts']); + assert.equal((await call(m, 'status', {})).work.initialSyncDone, false); + await call(m, 'grep', { pattern: 'one', glob: 'src/*.ts' }); + assert.equal((await call(m, 'status', {})).work.initialSyncDone, false, 'nor a grep whose filter names a directory'); + await call(m, 'glob', { pattern: '**/*.md' }); + assert.notEqual(env.store.treeGet(TREE, 'docs/b.md'), null); + assert.equal((await call(m, 'status', {})).work.initialSyncDone, true, 'a pattern with no literal directory walks the whole mount'); + }); + + test('a pattern into a directory the mount passes over by design names that directory, as a search given it as its path does', async (t) => { + const env = new Env(t); + env.writeDisk('vendor/lib/x.js', 'x'); + env.writeDisk('a/b/sub/y.js', 'y'); + const m = await env.open({}, { mounts: [ + { name: 'work', path: env.dir, mode: 'read-write', watch: 'never', ignore: ['vendor'] }, + { name: 'deep', path: env.disk('a/b'), mode: 'read-write', watch: 'never' }, + ] }); + // An ignored directory, and another mount's: nothing beneath either is + // listed to choose the start. + for (const [dir, beneath] of [['vendor', 'lib'], ['a/b', 'sub']]) { + const viaPath = await call(m, 'glob', { path: `work/${dir}`, pattern: '**' }); + assert.equal(viaPath.incomplete?.length, 1, JSON.stringify(viaPath)); + assert.equal(viaPath.incomplete[0].path, `work/${dir}`); + const found = await call(m, 'glob', { pattern: `${dir}/${beneath}/*.js` }); + assert.deepEqual(found.matches, [], dir); + assert.deepEqual(found.incomplete, viaPath.incomplete, `glob ${dir}`); + const searched = await call(m, 'grep', { pattern: '.', glob: `${dir}/${beneath}/*.js` }); + assert.deepEqual(searched.results, [], dir); + assert.deepEqual(searched.incomplete, viaPath.incomplete, `grep's filter ${dir}`); + } + }); + + test('a pattern through a symlinked directory reaches nothing past the link, as a walk of the whole mount does', async (t) => { + const env = new Env(t); + env.writeDisk('real/x.ts', 'x'); + symlinkSync('real', env.disk('link')); + const outside = mkdtempSync(join(tmpdir(), 'ws-outside-')); + t.after(() => rmSync(outside, { recursive: true, force: true })); + mkdirSync(join(outside, 'sub')); + writeFileSync(join(outside, 'sub', 'y.ts'), 'y'); + symlinkSync(outside, env.disk('out')); + const m = await env.open(); + // A link inside the mount, one leading out of it, and a directory + // beyond one: each is passed over, as a walk from the root passes it. + for (const pattern of ['link/*.ts', 'out/sub/*.ts']) { + assert.deepEqual(await matchesOf(m, { pattern }), [], `glob ${pattern}`); + assert.deepEqual(await grepped(m, { pattern: '.', glob: pattern }), [], `grep's filter ${pattern}`); + } + for (const path of ['link/x.ts', 'out/sub/y.ts']) { + assert.equal(env.store.treeGet(TREE, path), null, `nothing taken in at ${path}`); + } + // A path through the link that leads out is refused, naming the link, + // and nothing past it is listed to choose a deeper start. + const viaPath = await call(m, 'glob', { path: 'work/out', pattern: 'sub/*.ts' }); + assert.deepEqual(viaPath.matches, []); + assert.deepEqual(viaPath.incomplete?.map((r: { path: string }) => r.path), ['work/out'], JSON.stringify(viaPath)); + }); +}); + describe('the mount boundary', () => { test('a root replaced during an absence proof proves nothing: the replacement lacks what the original still holds', async (t) => { const env = new Env(t);