mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
Two main-process `resolveGitDir` call sites dropped the WSL distro their caller
already held, so they could only resolve a gitdir pointer whose spelling carries
its own translation: a `//wsl.localhost/<Distro>/...` base, or a `/mnt/<letter>`
drvfs pointer that maps to a drive letter on its own.
The layout that needs the third case is a repo inside the distro's filesystem
with its worktrees on the Windows drive. `git worktree list` reports the worktree
as `/mnt/c/wt/x`, which the listing translates to `C:\wt\x` — a base that no
longer names a distro — while the `.git` gitfile beside it points at
`/home/me/repo/.git/worktrees/x`, which has no drive to derive. Win32 then treats
that pointer as absolute and reads a path that names nothing:
- `detectSparseCheckout` stats `info/sparse-checkout` under the fabricated path,
always misses, and reports the worktree as non-sparse — no sparse badge, and
the file list claims files that are not on disk.
- `readWorktreeDiffStamp` reads HEAD under the same path, gets nothing, and
returns null. Null is the safe answer ("cannot prove unchanged"), but it
retires the settled-diff cache for every file in that worktree, so each diff
respawns Git.
Both callers already have the distro: the listing threads its
`GitWorktreeExecOptions` to `annotateSparseCheckoutStatus`, on through
`detectSparseCheckoutCached` (the annotation cache added by #17859) and its
background revalidation probe, and finally to `detectSparseCheckout`; and
`file-diff` already forwards its `GitRuntimeOptions` to `readWorktreeDiffStamp`,
which now forwards it to `resolveGitDir` as well.
`resolveGitMetadataPath` still prefers a UNC base's distro and still tries drvfs
before the caller-named distro, so nothing that resolved before resolves
differently.
The cache hop matters twice over. It is the only remaining caller of
`detectSparseCheckout`, so without threading it the fix would not reach the
probe at all. And the cache is where the bug turns sticky. #17859 keyed entries
on `repoPath` + `worktreePath` alone, on the reasoning that the distro is a
property of the repo and so every read for a given `repoPath` carries the same
one. That invariant does not hold. `listRepoWorktrees(repo)` is called with no
options at all from the filesystem-auth root rebuild
(`registered-worktree-roots-cache.ts`, reached from `ensureAuthorizedRootsCache`
on any auth check with a dirty cache) and from the local worktree-ownership
check in `filesystem-worktree-helpers.ts`. Both land on the *same* key as the
distro-carrying listing, because `translateWslOutputPaths` derives the distro
from the cwd spelling before falling back to `options.wslDistro`, so a
UNC-spelled repo path yields the identical `C:\...` worktree row either way.
Measured on Windows in one process, branch build: a distro-less read followed by
a distro-carrying read reported the sparse worktree as non-sparse both times.
So `wslDistro` now joins the cache key -- trimmed and lowercased, matching how
the rest of the codebase compares distro names, and appended last so the
repo-scoped prefix delete still matches every variant. The per-path invalidate
becomes a prefix delete for the same reason, dropping every distro variant of a
removed or moved worktree.
Keying on it closes both halves of the defect. A correct caller can no longer be
served an answer derived without the distro it supplied. And because the entry a
reader reaches is now selected by the same distro it would re-probe with,
`revalidateInBackground` can no longer re-derive a warm entry under weaker
options -- which mattered on its own: a distro-less reader crossing the
five-minute window would otherwise flip a correct `true` to `false`, and the
resulting change notification runs the registered invalidator, clearing the
whole repo's cache and re-probing every worktree cold, on a five-minute loop.
Cost of the extra key dimension is bounded by the number of distinct distros a
given repo is actually read under: one where a distro is threaded everywhere,
two while the distro-less callers above still exist. Entries are still
repo-scoped, and both clears already sweep by prefix.
Per-platform delta:
- macOS/Linux: no change. Guest-pointer translation is gated to win32 and a
caller-named distro is ignored off Windows; no caller supplies one there, so
the cache keys and probes exactly as before.
- native Windows, no WSL: no change. `wslDistro` is undefined, so the resolver
takes exactly the branches it took before and every read keys on the same
empty distro component, so the cache behaves exactly as it did.
- Windows + WSL, UNC-spelled worktree: no change. The base already names the
distro and outranks the caller's.
- Windows + WSL, drvfs-spelled worktree with a drvfs pointer: no change. The
drive-letter derivation still runs first.
- Windows + WSL, drvfs-spelled worktree with a non-drvfs pointer: the sparse
badge appears and the settled-diff cache starts hitting. Both previously
failed toward "not sparse" / "do not cache", so neither can now serve a stale
answer, and the distro-less listings no longer share the badge's cache entry.
- SSH/relay: none. Those paths return through the provider branch before
reaching either function.
- folder workspaces, GitLab: none. Neither is on these code paths.
Not in this change:
- `readRepoCommonDirFromDisk` (worktree-listing). Passing the distro there is
inert: a repo root's `.git` is a directory, so the gitfile-pointer branch never
runs, and when `repoPath` itself is guest-spelled the preceding `stat` already
fails — which no `resolveGitDir` option can fix.
- The two `findExistingWorktreeSymlinkPaths` calls on the removal paths. Both
receive `registeredWorktree.path` from `listWorktreesStrict`, which already
translates every row out of the guest namespace, so the distro would be a
no-op. The `removeWorktreeLinkedPaths` unlink beside them is untranslated too,
so a half-threaded fix would only move the refusal from Orca's preflight to
`git worktree remove`.
- An absolute `commondir` payload, which `resolveGitCommonDir` still resolves
untranslated. Git writes that file relative in the layouts above, and the
failure direction is unchanged.
- Giving the two distro-less `listRepoWorktrees(repo)` callers a distro. Neither
reads `isSparse` -- both use only `worktree.path` -- so the distro would buy
them nothing they consume, while resolving a project runtime inside the
filesystem-auth rebuild would put a call that throws on `repair-required`
behind a catch that skips the whole repo's authorized roots. The cache key
makes their reads harmless; skipping the annotation for callers that never
read it is a separate, larger change. The third no-options call in
`hosted-review.ts` is inside the `repo.connectionId` branch and returns
through the SSH provider, so it never reaches this cache.
357 lines
15 KiB
TypeScript
357 lines
15 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { detectSparseCheckoutMock } = vi.hoisted(() => ({
|
|
detectSparseCheckoutMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./worktree-sparse-state', () => ({
|
|
detectSparseCheckout: detectSparseCheckoutMock,
|
|
resolveGitCommonDir: vi.fn()
|
|
}))
|
|
|
|
import {
|
|
__getSparseCheckoutStateCacheSizeForTests,
|
|
__resetSparseCheckoutStateCacheForTests,
|
|
clearSparseCheckoutStateCache,
|
|
clearSparseCheckoutStateCacheForRepo,
|
|
detectSparseCheckoutCached,
|
|
invalidateSparseCheckoutState,
|
|
onSparseCheckoutStateChanged
|
|
} from './worktree-sparse-checkout-cache'
|
|
|
|
const RECONCILE_WINDOW_MS = 5 * 60_000
|
|
|
|
// A real setTimeout tick, not a faked one, to flush the microtask chain a background
|
|
// stale-while-revalidate detect runs on without needing vi.useFakeTimers() (which would also
|
|
// have to fake Date.now(), the thing these tests drive manually via the Date.now spy below).
|
|
async function flushBackgroundRevalidation(): Promise<void> {
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
}
|
|
|
|
beforeEach(() => {
|
|
detectSparseCheckoutMock.mockReset()
|
|
__resetSparseCheckoutStateCacheForTests()
|
|
})
|
|
|
|
describe('detectSparseCheckoutCached', () => {
|
|
it('caches a detection result across repeated calls for the same repo+path', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('detects each distinct worktree path independently', async () => {
|
|
detectSparseCheckoutMock.mockImplementation(
|
|
async (worktreePath: string) => worktreePath === '/repo/wt-sparse'
|
|
)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-sparse')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-full')).toBe(false)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('scopes the cache by repo, so the same path under two repos is detected independently', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo-a', '/shared-mount/wt')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo-b', '/shared-mount/wt')).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('treats an equivalent path spelling (trailing slash, redundant segment) as the same cache entry', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/./wt-a/')).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('caches a false result too, so a worktree that stays non-sparse costs one detect', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(false)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('serves the stale value immediately past the reconcile window and corrects it in the background', async () => {
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
|
|
// Just under the window: still trusts the cached value, no re-detect.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS - 1)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
|
|
// Past the window: both calls return the (stale) cached value, and only one kicks a
|
|
// background re-detect. Read both without awaiting in between — the underlying function
|
|
// never suspends before returning the stale value, so two calls issued back-to-back in the
|
|
// same tick are the only reliable way to observe "both concurrent callers stay stale" without
|
|
// racing the background revalidation's own microtask.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(true)
|
|
const firstPastWindow = detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
const secondPastWindow = detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
expect(await firstPastWindow).toBe(false)
|
|
expect(await secondPastWindow).toBe(false)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
|
|
await flushBackgroundRevalidation()
|
|
|
|
// The corrected value is now served without needing another window to elapse.
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('does not resurrect an entry that was explicitly invalidated while a background revalidation was in flight', async () => {
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
let resolveDetect: (isSparse: boolean) => void = () => {}
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// Past the window: kicks a background re-detect that we hold open.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockImplementationOnce(
|
|
() => new Promise<boolean>((resolve) => (resolveDetect = resolve))
|
|
)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// The worktree is removed (or the repo cache is cleared) while the detect above is in flight.
|
|
invalidateSparseCheckoutState('/repo', '/repo/wt-a')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(0)
|
|
|
|
// The in-flight detect now resolves; it must not write the entry back.
|
|
resolveDetect(true)
|
|
await flushBackgroundRevalidation()
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(0)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('does not let a stale in-flight revalidation clobber a fresh value written after remove+recreate at the same path', async () => {
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
let resolveStaleDetect: (isSparse: boolean) => void = () => {}
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// Past the window: kicks a background re-detect that we hold open (simulates a slow probe
|
|
// racing a worktree removal + recreation at the same path).
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockImplementationOnce(
|
|
() => new Promise<boolean>((resolve) => (resolveStaleDetect = resolve))
|
|
)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// The worktree is removed (invalidate) and a new one is recreated at the exact same path,
|
|
// repopulating the key with a fresh, different value via a normal cold read.
|
|
invalidateSparseCheckoutState('/repo', '/repo/wt-a')
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
|
|
// The stale in-flight detect from before the remove+recreate now resolves with the old
|
|
// answer. A presence-only guard would let this overwrite the fresh entry above; the fix
|
|
// must compare entry identity and refuse to write back over a value it didn't produce.
|
|
resolveStaleDetect(false)
|
|
await flushBackgroundRevalidation()
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('notifies the registered change listener only when a background revalidation flips the answer', async () => {
|
|
const listener = vi.fn()
|
|
onSparseCheckoutStateChanged(listener)
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await flushBackgroundRevalidation()
|
|
expect(listener).not.toHaveBeenCalled()
|
|
|
|
nowSpy.mockReturnValue(1_000 + 2 * RECONCILE_WINDOW_MS + 2)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(true)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await flushBackgroundRevalidation()
|
|
expect(listener).toHaveBeenCalledTimes(1)
|
|
expect(listener).toHaveBeenCalledWith('/repo', '/repo/wt-a', true)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
onSparseCheckoutStateChanged(undefined)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('invalidateSparseCheckoutState', () => {
|
|
it('drops only the named repo+path, leaving other cached paths untouched', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-b')
|
|
|
|
invalidateSparseCheckoutState('/repo', '/repo/wt-a')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
|
|
detectSparseCheckoutMock.mockClear()
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-b')
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledWith('/repo/wt-a', {})
|
|
})
|
|
})
|
|
|
|
describe('clearSparseCheckoutStateCacheForRepo', () => {
|
|
it('drops only the named repo`s entries, leaving a sibling repo`s warm cache intact', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('/repo-a', '/repo-a/wt-1')
|
|
await detectSparseCheckoutCached('/repo-b', '/repo-b/wt-1')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(2)
|
|
|
|
clearSparseCheckoutStateCacheForRepo('/repo-a')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
|
|
detectSparseCheckoutMock.mockClear()
|
|
await detectSparseCheckoutCached('/repo-a', '/repo-a/wt-1')
|
|
await detectSparseCheckoutCached('/repo-b', '/repo-b/wt-1')
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('clearSparseCheckoutStateCache', () => {
|
|
it('drops every cached path across every repo, matching the fallback used when a repo cannot be resolved', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('/repo-a', '/repo-a/wt-1')
|
|
await detectSparseCheckoutCached('/repo-b', '/repo-b/wt-1')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(2)
|
|
|
|
clearSparseCheckoutStateCache()
|
|
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(0)
|
|
})
|
|
})
|
|
|
|
// Regression coverage for the live Windows+WSL sequence: a distro-less listing (filesystem-auth
|
|
// root rebuild, worktree ownership checks) racing the real distro-carrying listing for the same
|
|
// repo. Before the distro joined the cache key they shared one entry, so whichever ran first
|
|
// decided the sparse badge for the whole reconcile window.
|
|
describe('detectSparseCheckoutCached with a WSL distro', () => {
|
|
// Mirrors the real probe: without the distro the gitdir pointer resolves to a fabricated Win32
|
|
// path, the sparse-checkout stat misses, and the worktree reads as non-sparse.
|
|
function detectOnlyWithDistro(distro: string): void {
|
|
detectSparseCheckoutMock.mockImplementation(
|
|
async (_worktreePath: string, options?: { wslDistro?: string }) =>
|
|
options?.wslDistro === distro
|
|
)
|
|
}
|
|
|
|
it('does not serve a distro-carrying read an answer derived without that distro', async () => {
|
|
detectOnlyWithDistro('Ubuntu')
|
|
|
|
expect(await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')).toBe(false)
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('keeps a distro-carrying answer correct when a distro-less read follows it', async () => {
|
|
detectOnlyWithDistro('Ubuntu')
|
|
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
expect(await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')).toBe(false)
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('treats distro spellings that name the same distro as one entry', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: ' ubuntu ' })
|
|
).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does not let a distro-less reader past the window revalidate a distro-carrying entry', async () => {
|
|
const listener = vi.fn()
|
|
onSparseCheckoutStateChanged(listener)
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
try {
|
|
detectOnlyWithDistro('Ubuntu')
|
|
nowSpy.mockReturnValue(1_000)
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')
|
|
|
|
// Past the window the distro-less caller re-probes its own entry, not the sparse one, so the
|
|
// badge cannot blink off and fire the change listener that clears the whole repo's cache.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
expect(await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')).toBe(false)
|
|
await flushBackgroundRevalidation()
|
|
|
|
expect(listener).not.toHaveBeenCalled()
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
onSparseCheckoutStateChanged(undefined)
|
|
}
|
|
})
|
|
|
|
it('drops every distro variant of a path on invalidate, so a removed worktree leaves nothing behind', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\other')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(3)
|
|
|
|
invalidateSparseCheckoutState('C:\\repo', 'C:\\repo\\wt')
|
|
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
})
|
|
|
|
it('still caches normally with no distro anywhere, as on macOS/Linux and native Windows', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a', {})).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a', { wslDistro: undefined })).toBe(
|
|
true
|
|
)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
})
|
|
})
|