cc-haha/desktop/src/stores/workspacePanelStore.test.ts
程序员阿江(Relakkes) f5cf5932b4 fix: keep workspace changes and session checkpoints reliable
The desktop workspace panel and chat transcript were mixing project-level file state with per-turn session history, which made rewind, file attachment, and changed-file previews brittle across multi-turn and historical sessions. This keeps turn checkpoints durable in the transcript, makes workspace refreshes happen at the right lifecycle points, and hardens long file previews without blocking the UI.

Constraint: Right-side workspace changes are project working-tree state, while chat turn cards are session checkpoint state.
Rejected: Treat every changed-file panel entry as session-local | new sessions must still reveal existing dirty project files.
Confidence: high
Scope-risk: moderate
Directive: Do not couple global workspace status to session checkpoint cards without preserving both product meanings.
Tested: cd desktop && bun run test src/components/workspace/WorkspacePanel.test.tsx
Tested: cd desktop && bun run lint
Tested: git diff --check
Tested: cd desktop && bun run test
Not-tested: Packaged Tauri runtime smoke test
2026-05-01 20:53:04 +08:00

696 lines
25 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
type HoistedVi = typeof vi & {
hoisted?: <T>(factory: () => T) => T
}
if (typeof (vi as HoistedVi).hoisted !== 'function') {
;(vi as HoistedVi).hoisted = <T>(factory: () => T) => factory()
}
const mocks = vi.hoisted(() => ({
getWorkspaceStatusMock: vi.fn(),
getWorkspaceTreeMock: vi.fn(),
getWorkspaceFileMock: vi.fn(),
getWorkspaceDiffMock: vi.fn(),
}))
vi.mock('../api/sessions', () => ({
sessionsApi: {
getWorkspaceStatus: mocks.getWorkspaceStatusMock,
getWorkspaceTree: mocks.getWorkspaceTreeMock,
getWorkspaceFile: mocks.getWorkspaceFileMock,
getWorkspaceDiff: mocks.getWorkspaceDiffMock,
},
}))
import { sessionsApi } from '../api/sessions'
import {
WORKSPACE_PANEL_DEFAULT_WIDTH,
WORKSPACE_PANEL_MAX_WIDTH,
WORKSPACE_PANEL_MIN_WIDTH,
getWorkspacePreviewTabId,
useWorkspacePanelStore,
} from './workspacePanelStore'
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
describe('workspacePanelStore', () => {
const initialState = useWorkspacePanelStore.getInitialState()
beforeEach(() => {
vi.clearAllMocks()
useWorkspacePanelStore.setState(initialState, true)
})
afterEach(() => {
useWorkspacePanelStore.setState(initialState, true)
vi.restoreAllMocks()
})
it('uses hoisted session api mocks', () => {
expect(sessionsApi.getWorkspaceStatus).toBe(mocks.getWorkspaceStatusMock)
expect(sessionsApi.getWorkspaceTree).toBe(mocks.getWorkspaceTreeMock)
expect(sessionsApi.getWorkspaceFile).toBe(mocks.getWorkspaceFileMock)
expect(sessionsApi.getWorkspaceDiff).toBe(mocks.getWorkspaceDiffMock)
})
it('keeps panel open state and active view isolated per session', () => {
const store = useWorkspacePanelStore.getState()
expect(store.isPanelOpen('session-a')).toBe(false)
expect(store.getActiveView('session-a')).toBe('changed')
expect(store.width).toBe(WORKSPACE_PANEL_DEFAULT_WIDTH)
store.openPanel('session-a')
store.setActiveView('session-a', 'all')
expect(useWorkspacePanelStore.getState().isPanelOpen('session-a')).toBe(true)
expect(useWorkspacePanelStore.getState().getActiveView('session-a')).toBe('all')
expect(useWorkspacePanelStore.getState().isPanelOpen('session-b')).toBe(false)
expect(useWorkspacePanelStore.getState().getActiveView('session-b')).toBe('changed')
store.togglePanel('session-b')
expect(useWorkspacePanelStore.getState().isPanelOpen('session-b')).toBe(true)
expect(useWorkspacePanelStore.getState().isPanelOpen('session-a')).toBe(true)
store.closePanel('session-a')
expect(useWorkspacePanelStore.getState().isPanelOpen('session-a')).toBe(false)
expect(useWorkspacePanelStore.getState().getActiveView('session-a')).toBe('all')
store.setWidth(120)
expect(useWorkspacePanelStore.getState().width).toBe(WORKSPACE_PANEL_MIN_WIDTH)
store.setWidth(1200)
expect(useWorkspacePanelStore.getState().width).toBe(WORKSPACE_PANEL_MAX_WIDTH)
})
it('loads workspace status successfully', async () => {
mocks.getWorkspaceStatusMock.mockResolvedValue({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [
{
path: 'src/a.ts',
status: 'modified',
additions: 3,
deletions: 1,
},
],
})
await useWorkspacePanelStore.getState().loadStatus('session-1')
expect(mocks.getWorkspaceStatusMock).toHaveBeenCalledWith('session-1')
expect(useWorkspacePanelStore.getState().statusBySession['session-1']).toMatchObject({
branch: 'main',
changedFiles: [{ path: 'src/a.ts' }],
})
expect(useWorkspacePanelStore.getState().loading.statusBySession['session-1']).toBe(false)
expect(useWorkspacePanelStore.getState().errors.statusBySession['session-1']).toBeNull()
})
it('defaults an empty changed-files status to the all-files view', async () => {
mocks.getWorkspaceStatusMock.mockResolvedValue({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [],
})
useWorkspacePanelStore.getState().openPanel('session-empty-changes')
expect(useWorkspacePanelStore.getState().getActiveView('session-empty-changes')).toBe('changed')
await useWorkspacePanelStore.getState().loadStatus('session-empty-changes')
expect(useWorkspacePanelStore.getState().statusBySession['session-empty-changes']?.changedFiles).toEqual([])
expect(useWorkspacePanelStore.getState().getActiveView('session-empty-changes')).toBe('all')
})
it('keeps the changed-files view when status contains changes', async () => {
mocks.getWorkspaceStatusMock.mockResolvedValue({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [
{
path: 'src/app.ts',
status: 'modified',
additions: 2,
deletions: 1,
},
],
})
useWorkspacePanelStore.getState().openPanel('session-has-changes')
await useWorkspacePanelStore.getState().loadStatus('session-has-changes')
expect(useWorkspacePanelStore.getState().getActiveView('session-has-changes')).toBe('changed')
})
it('returns to changed-files when a refreshed default all-files view now has changes', async () => {
mocks.getWorkspaceStatusMock
.mockResolvedValueOnce({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [],
})
.mockResolvedValueOnce({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [
{
path: 'src/app.ts',
status: 'modified',
additions: 2,
deletions: 1,
},
],
})
useWorkspacePanelStore.getState().openPanel('session-refresh-changes')
await useWorkspacePanelStore.getState().loadStatus('session-refresh-changes')
expect(useWorkspacePanelStore.getState().getActiveView('session-refresh-changes')).toBe('all')
await useWorkspacePanelStore.getState().loadStatus('session-refresh-changes')
expect(useWorkspacePanelStore.getState().getActiveView('session-refresh-changes')).toBe('changed')
})
it('does not override an explicit all-files selection when refreshed status has changes', async () => {
mocks.getWorkspaceStatusMock.mockResolvedValue({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [
{
path: 'src/app.ts',
status: 'modified',
additions: 2,
deletions: 1,
},
],
})
useWorkspacePanelStore.getState().openPanel('session-explicit-all')
useWorkspacePanelStore.getState().setActiveView('session-explicit-all', 'all')
await useWorkspacePanelStore.getState().loadStatus('session-explicit-all')
expect(useWorkspacePanelStore.getState().getActiveView('session-explicit-all')).toBe('all')
})
it('does not override an explicit changed-files selection when status is empty', async () => {
mocks.getWorkspaceStatusMock.mockResolvedValue({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [],
})
useWorkspacePanelStore.getState().openPanel('session-explicit-changed')
useWorkspacePanelStore.getState().setActiveView('session-explicit-changed', 'changed')
await useWorkspacePanelStore.getState().loadStatus('session-explicit-changed')
expect(useWorkspacePanelStore.getState().getActiveView('session-explicit-changed')).toBe('changed')
})
it('captures workspace status request errors', async () => {
mocks.getWorkspaceStatusMock.mockRejectedValue(new Error('status failed'))
await useWorkspacePanelStore.getState().loadStatus('session-err')
expect(useWorkspacePanelStore.getState().statusBySession['session-err']).toBeUndefined()
expect(useWorkspacePanelStore.getState().loading.statusBySession['session-err']).toBe(false)
expect(useWorkspacePanelStore.getState().errors.statusBySession['session-err']).toBe('status failed')
})
it('lazily loads tree nodes when expanding and reuses cached results', async () => {
mocks.getWorkspaceTreeMock.mockResolvedValue({
state: 'ok',
path: '',
entries: [
{ name: 'src', path: 'src', isDirectory: true },
{ name: 'README.md', path: 'README.md', isDirectory: false },
],
})
await useWorkspacePanelStore.getState().toggleTreeNode('session-tree', '')
expect(mocks.getWorkspaceTreeMock).toHaveBeenCalledTimes(1)
expect(mocks.getWorkspaceTreeMock).toHaveBeenCalledWith('session-tree', '')
expect(useWorkspacePanelStore.getState().expandedPathsBySession['session-tree']).toEqual([''])
expect(useWorkspacePanelStore.getState().treeBySessionPath['session-tree']?.['']).toMatchObject({
entries: [{ path: 'src' }, { path: 'README.md' }],
})
await useWorkspacePanelStore.getState().toggleTreeNode('session-tree', '')
expect(useWorkspacePanelStore.getState().expandedPathsBySession['session-tree']).toEqual([])
await useWorkspacePanelStore.getState().toggleTreeNode('session-tree', '')
expect(mocks.getWorkspaceTreeMock).toHaveBeenCalledTimes(1)
expect(useWorkspacePanelStore.getState().expandedPathsBySession['session-tree']).toEqual([''])
})
it('ignores stale tree responses for the same session path', async () => {
const first = deferred<{ state: 'ok'; path: string; entries: Array<{ name: string; path: string; isDirectory: boolean }> }>()
const second = deferred<{ state: 'ok'; path: string; entries: Array<{ name: string; path: string; isDirectory: boolean }> }>()
mocks.getWorkspaceTreeMock
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise)
const firstLoad = useWorkspacePanelStore.getState().loadTree('session-tree-race', 'src')
const secondLoad = useWorkspacePanelStore.getState().loadTree('session-tree-race', 'src')
second.resolve({
state: 'ok',
path: 'src',
entries: [{ name: 'new.ts', path: 'src/new.ts', isDirectory: false }],
})
await secondLoad
first.resolve({
state: 'ok',
path: 'src',
entries: [{ name: 'old.ts', path: 'src/old.ts', isDirectory: false }],
})
await firstLoad
expect(useWorkspacePanelStore.getState().treeBySessionPath['session-tree-race']?.src?.entries).toEqual([
{ name: 'new.ts', path: 'src/new.ts', isDirectory: false },
])
})
it('opens preview tabs, supports multiple kinds, and reuses duplicates without persistence', async () => {
const storage = typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage
const setItemSpy = storage ? vi.spyOn(storage, 'setItem') : null
mocks.getWorkspaceFileMock.mockResolvedValue({
state: 'ok',
path: 'src/a.ts',
content: 'export const a = 1',
language: 'typescript',
size: 18,
})
mocks.getWorkspaceDiffMock.mockResolvedValue({
state: 'ok',
path: 'src/a.ts',
diff: '@@ -1 +1 @@',
})
useWorkspacePanelStore.getState().openPanel('session-preview')
await useWorkspacePanelStore.getState().openPreview('session-preview', 'src/a.ts', 'file')
await useWorkspacePanelStore.getState().openPreview('session-preview', 'src/a.ts', 'diff')
await useWorkspacePanelStore.getState().openPreview('session-preview', 'src/a.ts', 'file')
expect(mocks.getWorkspaceFileMock).toHaveBeenCalledTimes(1)
expect(mocks.getWorkspaceDiffMock).toHaveBeenCalledTimes(1)
const tabs = useWorkspacePanelStore.getState().previewTabsBySession['session-preview']
expect(tabs).toBeDefined()
expect(tabs).toHaveLength(2)
expect(tabs![0]).toMatchObject({
id: 'file:src/a.ts',
kind: 'file',
path: 'src/a.ts',
content: 'export const a = 1',
language: 'typescript',
})
expect(tabs![1]).toMatchObject({
id: 'diff:src/a.ts',
kind: 'diff',
path: 'src/a.ts',
diff: '@@ -1 +1 @@',
})
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-preview']).toBe('file:src/a.ts')
if (setItemSpy) {
expect(setItemSpy).not.toHaveBeenCalled()
} else {
expect(storage).toBeNull()
}
})
it('closes exact tab id and preserves sibling preview for the same path', async () => {
mocks.getWorkspaceFileMock.mockResolvedValue({
state: 'ok',
path: 'src/a.ts',
content: 'export const a = 1',
language: 'typescript',
size: 18,
})
mocks.getWorkspaceDiffMock.mockResolvedValue({
state: 'ok',
path: 'src/a.ts',
diff: '@@ -1 +1 @@',
})
await useWorkspacePanelStore.getState().openPreview('session-close-path', 'src/a.ts', 'file')
await useWorkspacePanelStore.getState().openPreview('session-close-path', 'src/a.ts', 'diff')
useWorkspacePanelStore.getState().closePreview('session-close-path', 'diff:src/a.ts')
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-close-path']).toEqual([
expect.objectContaining({ id: 'file:src/a.ts' }),
])
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-close-path']).toBe('file:src/a.ts')
})
it('chooses the next sensible active preview when closing the current tab', async () => {
mocks.getWorkspaceFileMock
.mockResolvedValueOnce({
state: 'ok',
path: 'src/a.ts',
content: 'a',
language: 'typescript',
size: 1,
})
.mockResolvedValueOnce({
state: 'ok',
path: 'src/b.ts',
content: 'b',
language: 'typescript',
size: 1,
})
.mockResolvedValueOnce({
state: 'ok',
path: 'src/c.ts',
content: 'c',
language: 'typescript',
size: 1,
})
await useWorkspacePanelStore.getState().openPreview('session-close', 'src/a.ts', 'file')
await useWorkspacePanelStore.getState().openPreview('session-close', 'src/b.ts', 'file')
await useWorkspacePanelStore.getState().openPreview('session-close', 'src/c.ts', 'file')
useWorkspacePanelStore.getState().closePreview('session-close', 'file:src/b.ts')
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-close']).toMatchObject([
{ id: 'file:src/a.ts' },
{ id: 'file:src/c.ts' },
])
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-close']).toBe('file:src/c.ts')
useWorkspacePanelStore.getState().closePreview('session-close', 'file:src/c.ts')
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-close']).toBe('file:src/a.ts')
})
it('closes preview tabs by context-menu scope', () => {
useWorkspacePanelStore.setState((state) => ({
...state,
previewTabsBySession: {
...state.previewTabsBySession,
'session-preview-scope': [
{ id: 'file:a.ts', path: 'a.ts', kind: 'file', title: 'a.ts', state: 'ok', language: 'typescript', size: 1 },
{ id: 'file:b.ts', path: 'b.ts', kind: 'file', title: 'b.ts', state: 'ok', language: 'typescript', size: 1 },
{ id: 'file:c.ts', path: 'c.ts', kind: 'file', title: 'c.ts', state: 'ok', language: 'typescript', size: 1 },
{ id: 'file:d.ts', path: 'd.ts', kind: 'file', title: 'd.ts', state: 'ok', language: 'typescript', size: 1 },
],
},
activePreviewTabIdBySession: {
...state.activePreviewTabIdBySession,
'session-preview-scope': 'file:d.ts',
},
loading: {
...state.loading,
previewByTabId: {
...state.loading.previewByTabId,
'session-preview-scope::file:c.ts': true,
'session-preview-scope::file:d.ts': true,
},
},
errors: {
...state.errors,
previewByTabId: {
...state.errors.previewByTabId,
'session-preview-scope::file:c.ts': 'loading',
'session-preview-scope::file:d.ts': 'loading',
},
},
}))
useWorkspacePanelStore.getState().closePreviewTabs('session-preview-scope', 'file:b.ts', 'right')
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-preview-scope']).toMatchObject([
{ id: 'file:a.ts' },
{ id: 'file:b.ts' },
])
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-preview-scope']).toBe('file:b.ts')
expect(useWorkspacePanelStore.getState().loading.previewByTabId['session-preview-scope::file:c.ts']).toBeUndefined()
expect(useWorkspacePanelStore.getState().errors.previewByTabId['session-preview-scope::file:d.ts']).toBeUndefined()
useWorkspacePanelStore.getState().closePreviewTabs('session-preview-scope', 'file:b.ts', 'others')
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-preview-scope']).toMatchObject([
{ id: 'file:b.ts' },
])
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-preview-scope']).toBe('file:b.ts')
useWorkspacePanelStore.getState().closePreviewTabs('session-preview-scope', 'file:b.ts', 'all')
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-preview-scope']).toBeUndefined()
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-preview-scope']).toBeNull()
})
it('ignores stale status responses for the same session', async () => {
const first = deferred<{
state: 'ok'
workDir: string
repoName: string | null
branch: string | null
isGitRepo: boolean
changedFiles: []
error?: string
}>()
const second = deferred<{
state: 'ok'
workDir: string
repoName: string | null
branch: string | null
isGitRepo: boolean
changedFiles: []
error?: string
}>()
mocks.getWorkspaceStatusMock
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise)
const firstLoad = useWorkspacePanelStore.getState().loadStatus('session-race')
const secondLoad = useWorkspacePanelStore.getState().loadStatus('session-race')
second.resolve({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'new',
isGitRepo: true,
changedFiles: [],
})
await secondLoad
first.resolve({
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'old',
isGitRepo: true,
changedFiles: [],
})
await firstLoad
expect(useWorkspacePanelStore.getState().statusBySession['session-race']?.branch).toBe('new')
})
it('ignores stale preview responses after close and reopen', async () => {
const first = deferred<{ state: 'ok'; path: string; content: string; language: string; size: number }>()
const second = deferred<{ state: 'ok'; path: string; content: string; language: string; size: number }>()
mocks.getWorkspaceFileMock
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise)
const firstOpen = useWorkspacePanelStore.getState().openPreview('session-preview-race', 'src/a.ts', 'file')
useWorkspacePanelStore.getState().closePreview('session-preview-race', 'file:src/a.ts')
const secondOpen = useWorkspacePanelStore.getState().openPreview('session-preview-race', 'src/a.ts', 'file')
second.resolve({
state: 'ok',
path: 'src/a.ts',
content: 'new',
language: 'typescript',
size: 3,
})
await secondOpen
first.resolve({
state: 'ok',
path: 'src/a.ts',
content: 'old',
language: 'typescript',
size: 3,
})
await firstOpen
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-preview-race']).toEqual([
expect.objectContaining({ id: 'file:src/a.ts', content: 'new' }),
])
})
it('does not recreate loading or error state when a loading preview is closed before completion', async () => {
const pending = deferred<{ state: 'ok'; path: string; diff: string }>()
const tabId = getWorkspacePreviewTabId('src/a.ts', 'diff')
const previewKey = `session-preview-close::${tabId}`
mocks.getWorkspaceDiffMock.mockReturnValueOnce(pending.promise)
const openPromise = useWorkspacePanelStore.getState().openPreview('session-preview-close', 'src/a.ts', 'diff')
useWorkspacePanelStore.getState().closePreview('session-preview-close', tabId)
pending.resolve({
state: 'ok',
path: 'src/a.ts',
diff: '@@ -1 +1 @@',
})
await openPromise
expect(useWorkspacePanelStore.getState().previewTabsBySession['session-preview-close']).toBeUndefined()
expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-preview-close']).toBeNull()
expect(useWorkspacePanelStore.getState().loading.previewByTabId[previewKey]).toBeUndefined()
expect(useWorkspacePanelStore.getState().errors.previewByTabId[previewKey]).toBeUndefined()
})
it('clears session UI and cached data for clearSession and resetSessionUi', () => {
useWorkspacePanelStore.setState((state) => ({
...state,
panelBySession: {
'session-clear': { isOpen: true, activeView: 'all' },
'session-reset': { isOpen: true, activeView: 'all' },
},
statusBySession: {
'session-clear': {
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [],
},
'session-reset': {
state: 'ok',
workDir: '/repo',
repoName: 'repo',
branch: 'main',
isGitRepo: true,
changedFiles: [],
},
},
expandedPathsBySession: {
'session-clear': ['src'],
'session-reset': ['src'],
},
treeBySessionPath: {
'session-clear': {
src: { state: 'ok', path: 'src', entries: [] },
},
'session-reset': {
src: { state: 'ok', path: 'src', entries: [] },
},
},
previewTabsBySession: {
'session-clear': [
{ id: 'file:src/a.ts', path: 'src/a.ts', kind: 'file', title: 'a.ts' },
],
'session-reset': [
{ id: 'file:src/b.ts', path: 'src/b.ts', kind: 'file', title: 'b.ts' },
],
},
activePreviewTabIdBySession: {
'session-clear': 'file:src/a.ts',
'session-reset': 'file:src/b.ts',
},
loading: {
statusBySession: {
'session-clear': true,
'session-reset': true,
},
treeBySessionPath: {
'session-clear::src': true,
'session-reset::src': true,
},
previewByTabId: {
'session-clear::file:src/a.ts': true,
'session-reset::file:src/b.ts': true,
},
},
errors: {
statusBySession: {
'session-clear': 'bad',
'session-reset': 'bad',
},
treeBySessionPath: {
'session-clear::src': 'bad',
'session-reset::src': 'bad',
},
previewByTabId: {
'session-clear::file:src/a.ts': 'bad',
'session-reset::file:src/b.ts': 'bad',
},
},
}))
useWorkspacePanelStore.getState().clearSession('session-clear')
useWorkspacePanelStore.getState().resetSessionUi('session-reset')
const state = useWorkspacePanelStore.getState()
expect(state.panelBySession['session-clear']).toBeUndefined()
expect(state.panelBySession['session-reset']).toBeUndefined()
expect(state.statusBySession['session-clear']).toBeUndefined()
expect(state.statusBySession['session-reset']).toBeUndefined()
expect(state.expandedPathsBySession['session-clear']).toBeUndefined()
expect(state.expandedPathsBySession['session-reset']).toBeUndefined()
expect(state.treeBySessionPath['session-clear']).toBeUndefined()
expect(state.treeBySessionPath['session-reset']).toBeUndefined()
expect(state.previewTabsBySession['session-clear']).toBeUndefined()
expect(state.previewTabsBySession['session-reset']).toBeUndefined()
expect(state.activePreviewTabIdBySession['session-clear']).toBeUndefined()
expect(state.activePreviewTabIdBySession['session-reset']).toBeUndefined()
expect(state.loading.statusBySession['session-clear']).toBeUndefined()
expect(state.loading.statusBySession['session-reset']).toBeUndefined()
expect(state.loading.treeBySessionPath['session-clear::src']).toBeUndefined()
expect(state.loading.treeBySessionPath['session-reset::src']).toBeUndefined()
expect(state.loading.previewByTabId['session-clear::file:src/a.ts']).toBeUndefined()
expect(state.loading.previewByTabId['session-reset::file:src/b.ts']).toBeUndefined()
expect(state.errors.statusBySession['session-clear']).toBeUndefined()
expect(state.errors.statusBySession['session-reset']).toBeUndefined()
expect(state.errors.treeBySessionPath['session-clear::src']).toBeUndefined()
expect(state.errors.treeBySessionPath['session-reset::src']).toBeUndefined()
expect(state.errors.previewByTabId['session-clear::file:src/a.ts']).toBeUndefined()
expect(state.errors.previewByTabId['session-reset::file:src/b.ts']).toBeUndefined()
})
})