Files
hapi/web/src/lib/sessionLastSeen.test.ts
T
f399aa0222 feat(web): transient Unread-only session list filter (#1400)
* feat(web): opt-in needs-attention session list filter

Add a default-off sidebar filter (and Settings → Display toggle) that
keeps permission/input/unread rows while excluding background-only busy
work, mirroring the active-only preference pattern from #903.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): make needs-attention filter a transient sidebar toggle

Drop the Settings → Display preference and localStorage persistence.
This is an inbox lens: toggle in the session list, rows fall away as
they're seen, and a reload clears the filter.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): unread-only sidebar lens, not needs-attention

Rename the transient toggle to Unread only and filter solely on
updatedAt > lastSeenAt. Permission/input attention stays on row dots;
this is not Overseer review inbox.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): use unread-dot shape for unread filter toggle

Match the session-row unread indicator instead of an inbox tray so
Overseer can own inbox chrome later without competing metaphors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): apply unread filter after machine scope

Keep machineFilters derived from the unfiltered sidebar set so a
persisted machine selection is not cleared when that machine has no
unread rows. Unread intersects with the selected machine instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): snapshot last-seen once for unread filter pass

Avoid N localStorage read/parse calls while the unread lens is on by
loading the store once per memoized filter pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 09:47:39 +08:00

94 lines
3.1 KiB
TypeScript

import { describe, expect, it, beforeEach, vi } from 'vitest'
import {
getSessionLastSeenAt,
getSessionLastSeenSnapshot,
initializeSessionLastSeen,
markSessionSeen,
} from './sessionLastSeen'
describe('sessionLastSeen', () => {
beforeEach(() => {
localStorage.clear()
})
it('stores the latest seen timestamp for a session', () => {
markSessionSeen('session-a', 1000)
markSessionSeen('session-a', 2500)
expect(getSessionLastSeenAt('session-a')).toBe(2500)
})
it('snapshots the store once for bulk unread filtering', () => {
markSessionSeen('session-a', 1000)
markSessionSeen('session-b', 2500)
expect(getSessionLastSeenSnapshot()).toEqual({
'session-a': 1000,
'session-b': 2500,
})
})
it('does not move the watermark backwards', () => {
markSessionSeen('session-a', 5000)
markSessionSeen('session-a', 2000)
expect(getSessionLastSeenAt('session-a')).toBe(5000)
})
it('uses the first session list as the unread baseline', () => {
initializeSessionLastSeen('hub-a', [
{ id: 'session-a', updatedAt: 1000 },
{ id: 'session-b', updatedAt: 2500 },
])
expect(getSessionLastSeenAt('session-a')).toBe(1000)
expect(getSessionLastSeenAt('session-b')).toBe(2500)
})
it('preserves existing watermarks while completing a legacy partial baseline', () => {
markSessionSeen('session-a', 1000)
initializeSessionLastSeen('hub-a', [
{ id: 'session-a', updatedAt: 2500 },
{ id: 'session-b', updatedAt: 2500 },
])
expect(getSessionLastSeenAt('session-a')).toBe(1000)
expect(getSessionLastSeenAt('session-b')).toBe(2500)
initializeSessionLastSeen('hub-a', [{ id: 'session-c', updatedAt: 3000 }])
expect(getSessionLastSeenAt('session-c')).toBe(0)
})
it('initializes each hub independently', () => {
initializeSessionLastSeen('hub-a', [{ id: 'session-a', updatedAt: 1000 }])
initializeSessionLastSeen('hub-b', [{ id: 'session-b', updatedAt: 2000 }])
expect(getSessionLastSeenAt('session-b')).toBe(2000)
})
it('ignores localStorage write failures', () => {
const setItem = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new Error('quota exceeded')
})
expect(() => markSessionSeen('session-a', 1000)).not.toThrow()
setItem.mockRestore()
})
it('returns zero when localStorage getter throws', () => {
const localStorageDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage')
Object.defineProperty(window, 'localStorage', {
configurable: true,
get() {
throw new Error('storage denied')
},
})
expect(getSessionLastSeenAt('session-a')).toBe(0)
expect(() => markSessionSeen('session-a', 1000)).not.toThrow()
if (localStorageDescriptor) {
Object.defineProperty(window, 'localStorage', localStorageDescriptor)
}
})
})