Files
orca/src/main/git/status-diff-settled-cache.test.ts
T
NeilandNeil dff2ff0ec3 fix(git): read the diff working tree and stamp through the host path spelling (#17896)
Git can execute inside a WSL distro against a raw Linux worktree path while Node,
on the Windows side, reads the same files back through Win32. `path.join(
'/home/me/repo/feature', 'src/file.ts')` on win32 produces the drive-relative
`\home\me\repo\feature\src\file.ts`, which resolves against whatever the current
drive happens to be and almost always ENOENTs. The same mis-spelling hits the
drvfs form, where `/mnt/c/repo` should read as `C:\repo`.

Two consequences, both on the Node side only (git already works, because it gets
the Linux path as its cwd and resolves it inside the distro):

- getDiff's unstaged working-tree read missed, `readWorkingTreeFile` mapped ENOENT
  to `exists: false`, and an existing file rendered as DELETED in the diff view.
- `readWorktreeDiffStamp` could not find `.git`, so the stamp was null, the settled
  diff cache neither hit nor stored, and every diff respawned `git show` - two
  `wsl.exe` spawns the cache exists specifically to avoid.

Both now spell the worktree directory for the reading host first, via a new
`resolveWorktreeHostPath` wrapper around the resolver that landed in #17804.
The wrapper exists because `resolveGitMetadataPath` trims: a gitfile payload
carries a trailing newline, but a directory name may legally begin or end with
whitespace on POSIX, so the wrapper keeps the caller's spelling whenever the
resolver only trimmed it. The stamp's opaque `value` still embeds the caller's
original `worktreePath`, so settled-cache identity is byte-identical and no cache
key moves.

`readWorktreeDiffStamp` was already `Promise<WorktreeDiffStamp | null>` with one
caller that treats null as a cache miss, so no new nullability enters the type
system and the resolver's never-null-for-a-non-empty-pointer contract is
untouched. The only unspellable input is an empty worktree path, handled locally
as "not provably unchanged" in the stamp and as a read *failure* (not a proven
deletion) in file-diff.

What changes for users

| Platform | Delta |
|---|---|
| macOS | No change. An absolute POSIX path is returned verbatim, including one whose directory name carries leading or trailing whitespace. |
| Linux | No change. Same reason. |
| Native Windows (no WSL) | No change. A `C:\...` or `\\server\share\...` path is already absolute for win32 and passes through verbatim. |
| Windows + WSL, UNC worktree path (`\\wsl.localhost\Ubuntu\...`) | No change. Already absolute for win32; passes through verbatim. This is today's common case. |
| Windows + WSL, drvfs worktree path (`/mnt/c/repo`) | Fixed. Reads as `C:\repo` instead of the drive-relative `\mnt\c\repo`. Needs no distro name. |
| Windows + WSL, Linux worktree path with a named distro (`/home/me/repo`) | Fixed. Reads as `\\wsl.localhost\Ubuntu\home\me\repo`. The deleted-file misrender goes away and the diff cache starts hitting. |
| Windows, POSIX path, no distro and not a drvfs mount | No change. Passes through verbatim, same ENOENT, same existing fallback. |
| SSH | No change. `runtime-git-diff-commands.ts` and the `git:diff` IPC both route to `provider.getDiff` for a connection, so this local code is never reached. |
| Relay / remote | No change. No RPC param, wire field, stream opcode, or published content is touched; the relay host runs the same local code and gets the same fix. |
| Folder workspace (non-git) | No change. `.git` is absent either way, `resolveGitDir` returns the same fallback, and the stamp stays null exactly as today. |
| GitLab / other providers | Not applicable. No provider-specific or review code is touched. |

What this does NOT do

- It does not fix `resolveGitDir` itself. For a drvfs repo whose worktree Orca
  already spells `C:\repo\feature`, the gitfile payload `gitdir: /mnt/c/repo/.git/
  worktrees/feature` is still mis-resolved by `path.resolve` to
  `C:\mnt\c\repo\.git\...`, so the stamp still returns null in that shape. Separate
  change, separate PR; this one neither fixes nor regresses it.
- It does not touch submodule path resolution. `resolveSubmoduleWorktreePath` is
  the path-escape guard and has a near-identical twin in the relay; changing it
  without escape tests on both is out of scope.
- It does not change `readHeadComponent`'s `commondir` resolution. The relative
  `../..` git actually writes takes the identical `path.resolve` branch, and an
  absolute POSIX `commondir` under a WSL UNC `gitDir` already resolves correctly
  because the UNC root is `\\wsl.localhost\<distro>\`.
- It does not reorder drvfs-before-UNC inside the shared resolver. That changes the
  identity of returned strings and needs a real Windows+WSL box.
- It does not add any Git command, option, or version dependency.

Costs and residual risk

- One extra pure function call per diff read. No I/O added or removed on the
  unaffected paths.
- Translation still trims. `resolveWorktreeHostPath` preserves whitespace only when
  no translation happened; a guest directory named `/home/me/repo ` loses its
  trailing space on a Windows reader. Reachable only on win32, where such a name is
  not addressable anyway, and the previous behavior for that shape was a
  drive-relative miss.
- A relative worktree path (no caller passes one) is now resolved against the
  process cwd instead of joined relative to it. Same file in every case except a
  relative name that itself ends in whitespace.
- `UNSPELLABLE_WORKING_TREE_READ`'s `exists`/`failed` fields are correct but not
  observable today: the stamp is null for the same input, so nothing can be cached
  and `reusable` cannot be read back. They are there so the branch stays right if
  `loadDiff` ever gains a second caller. The test pins the observable part - that no
  read lands on a cwd-relative path.
- Every test here mocks `node:fs/promises` and spoofs `process.platform`. They prove
  which path string reaches `stat`/`readFile`, which is the right assertion, but
  none of this has executed against a real 9p mount on a Windows+WSL box and this
  repo's CI has no such runner.
- Honest framing of the trigger: I could not demonstrate a mainline path that hands
  `getDiff` an untranslated POSIX worktree path on Windows today -
  `translateWslOutputPaths` UNC-translates worktree paths whenever a distro is
  known, `getWslHome` returns the UNC spelling, and `resolveWslRepoWorktreeBasePath`
  normalizes a configured Linux base. The drvfs case is the most plausible live one.
  Treat this as defense-in-depth that is a strict no-op on every configuration above
  except the two marked Fixed.

Verification

- `npx vitest run src/main/git src/shared/git-metadata-path.test.ts` -> 196 files /
  2241 tests passed, 2 files and 5 tests skipped. One failure,
  `git-admission-storm-measurement.test.ts > reports bounded-concurrency before and
  after measurements` (ENOENT scandir on its own temp state dir), is pre-existing
  and environmental: it fails identically in isolation and spawns real git children
  without touching any changed module.
- `npx vitest run src/main/git/status-diff-settled-cache.test.ts` -> 21/21 (16
  pre-existing, 5 new). `npx vitest run src/shared/git-metadata-path.test.ts` ->
  25/25 (19 pre-existing, 6 new cases across 3 new tests).
- `npx oxfmt --write` then `npx oxlint` on all five changed files -> clean.

Mutation checks - all eight production substitutions were reverted one at a time
and the suite re-run. Each fails at least one test, and no new test survives its
own mutation:

| Reverted | Failing test |
|---|---|
| file-diff working-tree read -> `worktreePath` | reads the working tree through the host spelling instead of reporting a deletion; invalidates when the working tree file is edited under the host spelling |
| stamp working-tree component -> `worktreePath` | invalidates when the working tree file is edited under the host spelling |
| stamp `.gitmodules` stat -> `worktreePath` | invalidates when .gitmodules appears under the host spelling |
| stamp `resolveGitDir` -> `worktreePath` | stamps through the host spelling so the second read does not respawn git |
| `options` threading at the `readWorktreeDiffStamp` call | stamps through the host spelling...; invalidates when .gitmodules appears... |
| wrapper's untrimmed preservation -> return the resolver's value | keeps whitespace that belongs to the directory name (both cases) |
| `UNSPELLABLE_WORKING_TREE_READ` -> a cwd-relative `readWorkingTreeFile` | reads nothing relative to the cwd when the worktree path has no host spelling |
| stamp's null early return -> `hostWorktreePath ?? worktreePath` | reads nothing relative to the cwd when the worktree path has no host spelling |

The settled-cache tests seed the fake filesystem through the platform-bound `path`
module rather than `path.win32`, so they assert real behavior on a POSIX CI host as
well as on Windows and are not gated on the host platform.

Co-authored-by: Neil <79079362+brennanb2025@users.noreply.github.com>
2026-09-01 02:39:48 -07:00

413 lines
16 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import * as path from 'node:path'
import type * as BoundedFileReader from '../../shared/node-bounded-file-reader'
import { createBoundedFileReaderModuleMock, createGitRunnerModuleMock } from './status-test-harness'
const {
gitExecFileAsyncMock,
gitExecFileAsyncBufferMock,
gitStreamOptionsMock,
lstatMock,
realpathMock,
rmMock,
existsSyncMock
} = vi.hoisted(() => ({
gitExecFileAsyncMock: vi.fn(),
gitExecFileAsyncBufferMock: vi.fn(),
gitStreamOptionsMock: vi.fn(),
lstatMock: vi.fn(),
realpathMock: vi.fn(),
rmMock: vi.fn(),
existsSyncMock: vi.fn()
}))
/**
* A tiny in-memory filesystem, because every assertion here is about what the
* cache does when one specific path's mtime or bytes move. Sequenced
* `mockResolvedValueOnce` stacks cannot express that: the stamp and the diff
* read touch overlapping paths in an order the test should not have to know.
*/
type FakeFile = { content: Buffer; mtimeMs: number; ino: number }
const { files } = vi.hoisted(() => ({ files: new Map<string, FakeFile>() }))
const { readFileMock, statMock, accessMock } = vi.hoisted(() => {
const missing = (target: string): NodeJS.ErrnoException =>
Object.assign(new Error(`ENOENT: ${target}`), { code: 'ENOENT' })
return {
readFileMock: vi.fn(async (target: string, encoding?: BufferEncoding) => {
const file = files.get(target)
if (!file) {
throw missing(target)
}
return encoding ? file.content.toString(encoding) : file.content
}),
statMock: vi.fn(async (target: string) => {
const file = files.get(target)
if (!file) {
throw missing(target)
}
return {
isFile: () => true,
size: file.content.byteLength,
mtimeMs: file.mtimeMs,
ino: file.ino
}
}),
accessMock: vi.fn(async (target: string) => {
if (!files.has(target)) {
throw missing(target)
}
})
}
})
vi.mock('./runner', () =>
createGitRunnerModuleMock({
gitExecFileAsyncMock,
gitExecFileAsyncBufferMock,
gitStreamOptionsMock
})
)
vi.mock('fs/promises', () => ({
lstat: lstatMock,
realpath: realpathMock,
readFile: readFileMock,
stat: statMock,
rm: rmMock,
access: accessMock
}))
vi.mock('fs', () => ({ existsSync: existsSyncMock }))
vi.mock('../../shared/node-bounded-file-reader', async (importOriginal) =>
createBoundedFileReaderModuleMock(await importOriginal<typeof BoundedFileReader>(), {
readFileMock,
statMock
})
)
import { getDiff, getStatus, invalidateGitReadCaches, stageFile } from './status'
import { settledDiffCache } from './source-control/git-read-cache-invalidation'
const REPO = '/repo'
const FILE = 'src/file.ts'
const WORKING_TREE_PATH = `${REPO}/${FILE}`
const HEAD_PATH = `${REPO}/.git/HEAD`
const REF_PATH = `${REPO}/.git/refs/heads/main`
const INDEX_PATH = `${REPO}/.git/index`
const GITMODULES_PATH = `${REPO}/.gitmodules`
// Old enough that a further write is guaranteed to move the mtime, which is what
// lets the cache store at all.
const SETTLED_MTIME_MS = Date.now() - 60_000
let nextInode = 100
function writeFile(target: string, content: string, mtimeMs = SETTLED_MTIME_MS): void {
files.set(target, { content: Buffer.from(content), mtimeMs, ino: (nextInode += 1) })
}
function blobReadCount(): number {
return gitExecFileAsyncBufferMock.mock.calls.length
}
function seedRepo(): void {
files.clear()
// No `.git` file entry: `.git` is a directory, so reading it as a pointer misses.
writeFile(HEAD_PATH, 'ref: refs/heads/main\n')
writeFile(REF_PATH, `${'a'.repeat(40)}\n`)
writeFile(INDEX_PATH, 'index-bytes')
writeFile(WORKING_TREE_PATH, 'working-tree-content')
}
describe('settled diff cache', () => {
beforeEach(() => {
gitExecFileAsyncMock.mockReset()
gitExecFileAsyncBufferMock.mockReset()
gitStreamOptionsMock.mockReset()
readFileMock.mockClear()
statMock.mockClear()
accessMock.mockClear()
existsSyncMock.mockReset()
invalidateGitReadCaches()
settledDiffCache.resetStatsForTests()
seedRepo()
// `.gitmodules` is absent, so submodule routing resolves to "no submodules".
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
gitExecFileAsyncBufferMock.mockResolvedValue({ stdout: Buffer.from('index-content\n') })
})
it('serves the second read of an unchanged file without respawning git', async () => {
const first = await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
const second = await getDiff(REPO, FILE, false)
expect(spawnsAfterFirst).toBeGreaterThan(0)
expect(blobReadCount()).toBe(spawnsAfterFirst)
expect(second).toEqual(first)
expect(settledDiffCache.stats().hits).toBe(1)
})
// A WSL-routed read whose worktree path never went through UNC translation: git resolves
// `/home/...` inside the distro, but every Node read here has to be spelled for Win32.
describe('on a Windows host reading a raw Linux WSL worktree path', () => {
const WSL_WORKTREE = '/home/me/repo/feature'
const WSL_OPTIONS = { wslDistro: 'Ubuntu' }
const HOST_WORKTREE = String.raw`\\wsl.localhost\Ubuntu\home\me\repo\feature`
let platformDescriptor: PropertyDescriptor | undefined
beforeEach(() => {
platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
files.clear()
// `.git` is a directory here, so the gitdir is the worktree path plus a segment.
writeFile(path.join(HOST_WORKTREE, '.git/HEAD'), 'ref: refs/heads/main\n')
writeFile(path.join(HOST_WORKTREE, '.git/refs/heads/main'), `${'a'.repeat(40)}\n`)
writeFile(path.join(HOST_WORKTREE, '.git/index'), 'index-bytes')
writeFile(path.join(HOST_WORKTREE, FILE), 'working-tree-content')
})
afterEach(() => {
if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor)
}
})
it('reads the working tree through the host spelling instead of reporting a deletion', async () => {
const result = await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
expect(result.modifiedContent).toBe('working-tree-content')
expect(readFileMock).toHaveBeenCalledWith(path.join(HOST_WORKTREE, FILE))
expect(readFileMock).not.toHaveBeenCalledWith(path.join(WSL_WORKTREE, FILE))
})
it('stamps through the host spelling so the second read does not respawn git', async () => {
await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
const spawnsAfterFirst = blobReadCount()
await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
expect(spawnsAfterFirst).toBeGreaterThan(0)
expect(blobReadCount()).toBe(spawnsAfterFirst)
expect(settledDiffCache.stats().hits).toBe(1)
expect(statMock).toHaveBeenCalledWith(path.join(HOST_WORKTREE, '.git/index'))
})
// Each stamp component has to be stat'd under the host spelling too: one that permanently
// misses is a constant, so the stamp stops moving and the cache serves a stale diff.
it('invalidates when the working tree file is edited under the host spelling', async () => {
await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
const spawnsAfterFirst = blobReadCount()
writeFile(path.join(HOST_WORKTREE, FILE), 'edited-in-another-editor')
const second = await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(second.modifiedContent).toBe('edited-in-another-editor')
})
it('invalidates when .gitmodules appears under the host spelling', async () => {
await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
const spawnsAfterFirst = blobReadCount()
writeFile(path.join(HOST_WORKTREE, '.gitmodules'), '[submodule "vendor"]\n')
await getDiff(WSL_WORKTREE, FILE, false, false, WSL_OPTIONS)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(statMock).toHaveBeenCalledWith(path.join(HOST_WORKTREE, '.gitmodules'))
})
})
// An empty worktree path has no host spelling, and `path.join('', x)` is a *relative* path:
// every fs read would land in the process cwd, which in dev is Orca's own checkout.
it('reads nothing relative to the cwd when the worktree path has no host spelling', async () => {
await getDiff('', FILE, false)
expect(statMock).not.toHaveBeenCalledWith(FILE)
expect(readFileMock).not.toHaveBeenCalledWith('.git', 'utf-8')
expect(settledDiffCache.stats().entries).toBe(0)
})
// The four invalidation axes, one per diff input. Each proves the stale result is
// never served, which matters more than any of the hits above.
it.each([
[
'the working tree file is edited',
() => writeFile(WORKING_TREE_PATH, 'edited-in-another-editor')
],
['the index is rewritten by git add', () => writeFile(INDEX_PATH, 'index-bytes-after-add')],
['HEAD moves to a new commit', () => writeFile(REF_PATH, `${'b'.repeat(40)}\n`)],
['HEAD is detached onto another commit', () => writeFile(HEAD_PATH, `${'c'.repeat(40)}\n`)],
['.gitmodules appears', () => writeFile(GITMODULES_PATH, '[submodule "vendor"]\n')]
])('re-reads after %s', async (_name, mutate) => {
await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
mutate()
gitExecFileAsyncBufferMock.mockResolvedValue({ stdout: Buffer.from('fresh-index-content\n') })
const second = await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(second).toMatchObject({ originalContent: 'fresh-index-content\n' })
})
it('re-reads after a mutation runs through the shared invalidation point', async () => {
await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
await stageFile(REPO, FILE)
await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
})
// The dangerous ordering: the read observed pre-mutation state, so storing its
// result after the mutation would pin a diff that was already wrong.
it('refuses to store a result for a read that a mutation overtook', async () => {
let releaseBlob = (): void => {}
const blocked = new Promise<{ stdout: Buffer }>((resolve) => {
releaseBlob = () => resolve({ stdout: Buffer.from('pre-mutation\n') })
})
gitExecFileAsyncBufferMock.mockReturnValue(blocked)
const inFlight = getDiff(REPO, FILE, false)
await vi.waitFor(() => expect(blobReadCount()).toBeGreaterThan(0))
invalidateGitReadCaches()
releaseBlob()
await inFlight
expect(settledDiffCache.stats().invalidatedDuringRead).toBe(1)
expect(settledDiffCache.stats().entries).toBe(0)
const spawnsAfterFirst = blobReadCount()
gitExecFileAsyncBufferMock.mockResolvedValue({ stdout: Buffer.from('post-mutation\n') })
const second = await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(second).toMatchObject({ originalContent: 'post-mutation\n' })
})
// The stamp is itself several awaited stats, so a mutation can begin and end entirely
// inside it — leaving a stamp torn across the mutation that no later stamp can match.
it('refuses to store a result for a mutation that landed inside the stamp read', async () => {
const baseStat = statMock.getMockImplementation()
if (!baseStat) {
throw new Error('the fake filesystem lost its stat implementation')
}
let invalidated = false
statMock.mockImplementation(async (target: string) => {
if (!invalidated && target === INDEX_PATH) {
invalidated = true
invalidateGitReadCaches()
}
return baseStat(target)
})
try {
await getDiff(REPO, FILE, false)
} finally {
statMock.mockImplementation(baseStat)
}
expect(invalidated).toBe(true)
expect(settledDiffCache.stats().invalidatedDuringRead).toBe(1)
expect(settledDiffCache.stats().entries).toBe(0)
})
// A write inside the mtime granularity window could be overwritten again without
// moving the timestamp, so that read is not allowed to become a cache entry.
it('refuses to store a diff of a file written moments ago', async () => {
writeFile(WORKING_TREE_PATH, 'just-saved', Date.now())
await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(settledDiffCache.stats().racyWrites).toBeGreaterThan(0)
expect(settledDiffCache.stats().entries).toBe(0)
})
// A folder workspace, or any path that is not a git checkout, cannot be stamped.
it('never caches when the repo layout cannot be stamped', async () => {
files.delete(HEAD_PATH)
await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(settledDiffCache.stats().unprovable).toBeGreaterThan(0)
expect(settledDiffCache.stats().entries).toBe(0)
})
// A WSL relay that never reached git returns the same empty left side as a new
// file does, so persisting it would pin a wrong diff until something else moved.
it('refuses to store a diff whose blob read failed rather than proved absence', async () => {
gitExecFileAsyncBufferMock.mockRejectedValue(
Object.assign(new Error('wsl.exe failed'), { code: 1 })
)
await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterFirst)
expect(settledDiffCache.stats().entries).toBe(0)
})
it('caches a new file whose absence from the index git actually reported', async () => {
gitExecFileAsyncBufferMock.mockRejectedValue(
Object.assign(new Error("fatal: path 'src/file.ts' does not exist"), { code: 128 })
)
await getDiff(REPO, FILE, false)
const spawnsAfterFirst = blobReadCount()
await getDiff(REPO, FILE, false)
expect(blobReadCount()).toBe(spawnsAfterFirst)
expect(settledDiffCache.stats().hits).toBe(1)
})
it('keeps staged and unstaged diffs of one file in separate entries', async () => {
await getDiff(REPO, FILE, false)
const spawnsAfterUnstaged = blobReadCount()
await getDiff(REPO, FILE, true)
expect(blobReadCount()).toBeGreaterThan(spawnsAfterUnstaged)
})
// A staged diff compares HEAD to the index, so a working-tree edit must not evict it.
it('keeps a staged diff across a working-tree edit', async () => {
await getDiff(REPO, FILE, true)
const spawnsAfterFirst = blobReadCount()
writeFile(WORKING_TREE_PATH, 'edited-after-staging')
await getDiff(REPO, FILE, true)
expect(blobReadCount()).toBe(spawnsAfterFirst)
})
it('does not let a status poll drop the in-flight diff read', async () => {
let releaseBlob = (): void => {}
const blocked = new Promise<{ stdout: Buffer }>((resolve) => {
releaseBlob = () => resolve({ stdout: Buffer.from('index-content\n') })
})
gitExecFileAsyncBufferMock.mockReturnValue(blocked)
const first = getDiff(REPO, FILE, false)
await vi.waitFor(() => expect(blobReadCount()).toBeGreaterThan(0))
const spawnsBeforePoll = blobReadCount()
await getStatus(REPO)
const second = getDiff(REPO, FILE, false)
releaseBlob()
await Promise.all([first, second])
// Why exactly equal: the second read must join the first, not start its own spawns.
expect(blobReadCount()).toBe(spawnsBeforePoll)
})
})