diff --git a/tests/bottom-auto-terminal.spec.tsx b/tests/bottom-auto-terminal.spec.tsx index dfba81d..e9d4a5a 100644 --- a/tests/bottom-auto-terminal.spec.tsx +++ b/tests/bottom-auto-terminal.spec.tsx @@ -44,6 +44,9 @@ interface MountedSidebar { } /** Mount the real Sidebar shell against a minimal context (real store + service). */ +/** Unique per-test session ids (see the comment inside). */ +let sessionSeq = 0 + function mountSidebar(): MountedSidebar { vi.stubGlobal('WebSocket', FakeWebSocket) const container = document.createElement('div') @@ -52,14 +55,18 @@ function mountSidebar(): MountedSidebar { const service = createBetterSidebarService(store) // Fresh-session seed: the right panel starts OPEN, the bottom panel closed // (bottomOpen false → the first expansion is a false→true TRANSITION). - store.setSession('s1') + // Unique session per test — the store persists per-session state to + // localStorage (200ms debounce); a shared id lets a previous test's late + // write leak into this store's setSession restore. + const sessionId = `s1-${++sessionSeq}` + store.setSession(sessionId) // useSyncExternalStore requires STABLE snapshots across calls (the real DSH // services return stable objects) — a fresh object per call loops forever. const localeSnapshot = { active: 'en' } const sessionsSnapshot = { - current: 's1', + current: sessionId, // cwd present → api.sessionCwd is never called in these tests. - byId: { s1: { cwd: '/tmp' } }, + byId: { [sessionId]: { cwd: '/tmp' } }, } const ctx = { locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot }, @@ -81,6 +88,9 @@ function mountSidebar(): MountedSidebar { afterEach(() => { document.body.innerHTML = '' + // Belt and braces: drop any persisted layout a pending 200ms debounce + // write left behind between tests (unique session ids already isolate). + localStorage.clear() vi.unstubAllGlobals() }) diff --git a/tests/free-window.spec.tsx b/tests/free-window.spec.tsx index c90d498..568bfe2 100644 --- a/tests/free-window.spec.tsx +++ b/tests/free-window.spec.tsx @@ -60,7 +60,10 @@ interface Mounted { unmount: () => void } -function mountSidebar(): Mounted { +/** Unique per-test session ids (see the comment inside). */ +let sessionSeq = 0 + +function mountSidebar(sessionId: string = `s1-${++sessionSeq}`): Mounted { vi.stubGlobal('WebSocket', FakeWebSocket) if (!('ResizeObserver' in globalThis)) { vi.stubGlobal('ResizeObserver', class { @@ -83,9 +86,15 @@ function mountSidebar(): Mounted { component: ({ visible }) => createElement('div', { 'data-visible': String(visible) }, 'notes body'), }) store.setPrefs({ ...store.getPrefs(), openByDefault: true }) - store.setSession('s1') + // The default session id is unique per test: the store persists + // per-session state to localStorage on a 200ms debounce, and a shared id + // let a PREVIOUS test's late timer write leak into this store's + // setSession restore (the portaled-menu/resize flakes — a stale float at + // the old drop point won the querySelector race on slow runners). The + // persistence round-trip test passes its own fixed id instead. + store.setSession(sessionId) const localeSnapshot = { active: 'en' } - const sessionsSnapshot = { current: 's1', byId: { s1: { cwd: '/tmp' } } } + const sessionsSnapshot = { current: sessionId, byId: { [sessionId]: { cwd: '/tmp' } } } const ctx = { locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot }, sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } }, @@ -132,6 +141,10 @@ afterEach(() => { document.body.innerHTML = '' document.documentElement.style.cssText = '' vi.unstubAllGlobals() + // Belt and braces (after unstub, so a test's storage stub never sees it): + // drop any persisted layout a pending 200ms debounce write left behind + // between tests — unique session ids already isolate the stores. + localStorage.clear() }) describe('free windows: drag-out detection', () => { @@ -380,7 +393,7 @@ describe('free windows: the window', () => { const getItem = vi.fn((key: string): string | null => saved.get(key) ?? null) const setItem = vi.fn((key: string, value: string): void => { saved.set(key, value) }) vi.stubGlobal('localStorage', { getItem, setItem, removeItem: () => {} }) - const first = mountSidebar() + const first = mountSidebar('s1') try { act(() => { first.service.openTab({ type: 'notes', title: 'Notes' }) }) act(() => { first.store.reduce(s => floatTabReducer(s, 'notes', 512, 384)) }) @@ -390,7 +403,7 @@ describe('free windows: the window', () => { } finally { first.unmount() } - const second = mountSidebar() + const second = mountSidebar('s1') try { const state = second.store.getSnapshot().state! expect(state.floats).toHaveLength(1) diff --git a/tests/git-worktree.spec.ts b/tests/git-worktree.spec.ts index b92c6fd..2f4aca3 100644 --- a/tests/git-worktree.spec.ts +++ b/tests/git-worktree.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { spawnSync } from 'node:child_process' -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { parseWorktreeList, resolveWorktree, status, worktrees } from '../src/git.ts' @@ -67,13 +67,16 @@ describe('linked Git worktrees', () => { git(main, ['commit', '-q', '-m', 'base']) git(main, ['worktree', 'add', '-q', '-b', 'agent', agent]) writeFileSync(join(agent, 'tracked.txt'), 'changed by agent\n') + // macOS tmpdir() keeps the /var symlink while git reports the resolved + // /private/var prefix — canonicalize every path handed to git APIs. + const agentPath = realpathSync(agent) const listed = await worktrees(main) expect(listed).toHaveLength(2) expect(listed.find(entry => entry.current)).toMatchObject({ branch: 'main', changes: 0 }) expect(listed.find(entry => !entry.current)).toMatchObject({ branch: 'agent', changes: 1 }) - expect(resolve(await resolveWorktree(main, agent))).toBe(resolve(agent)) - expect((await status(await resolveWorktree(main, agent))).entries).toEqual([ + expect(resolve(await resolveWorktree(main, agentPath))).toBe(resolve(agentPath)) + expect((await status(await resolveWorktree(main, agentPath))).entries).toEqual([ { path: 'tracked.txt', xy: ' M' }, ]) await expect(resolveWorktree(main, root)).rejects.toThrow('unknown linked worktree') @@ -84,9 +87,9 @@ describe('linked Git worktrees', () => { rmSync(agent, { recursive: true, force: true }) const remaining = await worktrees(main) expect(remaining).toHaveLength(1) - expect(resolve(remaining[0]!.path)).toBe(resolve(main)) + expect(resolve(remaining[0]!.path)).toBe(resolve(realpathSync(main))) expect(remaining[0]!.current).toBe(true) - await expect(resolveWorktree(main, agent)).rejects.toThrow('unknown linked worktree') + await expect(resolveWorktree(main, agentPath)).rejects.toThrow('unknown linked worktree') } finally { try { git(main, ['worktree', 'remove', '--force', agent]) } catch { /* fixture may not be fully initialized */ } rmSync(root, { recursive: true, force: true }) diff --git a/tests/git.spec.ts b/tests/git.spec.ts index 655bde4..7d6e02d 100644 --- a/tests/git.spec.ts +++ b/tests/git.spec.ts @@ -1,5 +1,5 @@ import { execFile, execFileSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { mkdir, mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -10,6 +10,9 @@ import { parseLogLines, parsePorcelainZ, repoRoots, status } from '../src/git.ts const execFileAsync = promisify(execFile) const normalizePath = (path: string): string => path.replaceAll('\\', '/') +// macOS tmpdir() is the /var symlink while git reports the resolved +// /private/var prefix — canonicalize both sides before comparing. +const canonical = (path: string): string => normalizePath(realpathSync(path)) describe('git parsing', () => { it('discovers and selects direct child repositories under a workspace directory', async () => { @@ -23,11 +26,11 @@ describe('git parsing', () => { execFileAsync('git', ['-C', second, 'init']), ]) - await expect(repoRoots(workspace)).resolves.toEqual([normalizePath(first), normalizePath(second)]) - await expect(status(workspace, normalizePath(second))).resolves.toMatchObject({ + await expect(repoRoots(workspace)).resolves.toEqual([canonical(first), canonical(second)]) + await expect(status(workspace, canonical(second))).resolves.toMatchObject({ isRepo: true, - root: normalizePath(second), - repositories: [normalizePath(first), normalizePath(second)], + root: canonical(second), + repositories: [canonical(first), canonical(second)], }) } finally { await rm(workspace, { recursive: true, force: true }) diff --git a/tests/sidebar-crash.spec.tsx b/tests/sidebar-crash.spec.tsx index 095ef86..6e47654 100644 --- a/tests/sidebar-crash.spec.tsx +++ b/tests/sidebar-crash.spec.tsx @@ -47,6 +47,9 @@ interface MountedSidebar { } /** Mount the real Sidebar shell against a minimal context (real store + service). */ +/** Unique per-test session ids (see the comment inside). */ +let sessionSeq = 0 + function mountSidebar(): MountedSidebar { vi.stubGlobal('WebSocket', FakeWebSocket) const container = document.createElement('div') @@ -55,14 +58,18 @@ function mountSidebar(): MountedSidebar { const service = createBetterSidebarService(store) // Fresh-session seed: open the panel explicitly (openByDefault defaults off). store.setPrefs({ ...store.getPrefs(), openByDefault: true }) - store.setSession('s1') + // Unique session per test — the store persists per-session state to + // localStorage (200ms debounce); a shared id lets a previous test's late + // write leak into this store's setSession restore. + const sessionId = `s1-${++sessionSeq}` + store.setSession(sessionId) // useSyncExternalStore requires STABLE snapshots across calls (the real DSH // services return stable objects) — a fresh object per call loops forever. const localeSnapshot = { active: 'en' } const sessionsSnapshot = { - current: 's1', + current: sessionId, // cwd present → api.sessionCwd is never called in these tests. - byId: { s1: { cwd: '/tmp' } }, + byId: { [sessionId]: { cwd: '/tmp' } }, } const ctx = { locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot }, @@ -84,6 +91,9 @@ function mountSidebar(): MountedSidebar { afterEach(() => { document.body.innerHTML = '' + // Belt and braces: drop any persisted layout a pending 200ms debounce + // write left behind between tests (unique session ids already isolate). + localStorage.clear() vi.unstubAllGlobals() })