Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
commit
03634b1ca3
11340 files changed
+3442369
No files matched your search
@@ -0,0 +1,230 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import { makeSessionInfo } from '../test/session-info'
|
||||
|
||||
import { $activeGatewayProfile } from './profile'
|
||||
import {
|
||||
$cronSessions,
|
||||
$messagingSessions,
|
||||
$selectedStoredSessionId,
|
||||
$sessions,
|
||||
$unreadFinishedSessionIds,
|
||||
setSelectedStoredSessionId,
|
||||
setSessions
|
||||
} from './session'
|
||||
import { clearAllSessionStates } from './session-states'
|
||||
import {
|
||||
$sessionSeenCounts,
|
||||
$unreadFinishedMarkers,
|
||||
forgetSessionUnread,
|
||||
markSessionUnreadFinished
|
||||
} from './session-unread'
|
||||
|
||||
const session = (over: Partial<SessionInfo>): SessionInfo => makeSessionInfo({ id: 'live', ...over })
|
||||
|
||||
function resetAll() {
|
||||
clearAllSessionStates()
|
||||
// Lists first: their listeners recompute the unread atom, so wiping the
|
||||
// atom afterwards leaves a genuinely clean slate.
|
||||
$sessions.set([])
|
||||
$cronSessions.set([])
|
||||
$messagingSessions.set([])
|
||||
$selectedStoredSessionId.set(null)
|
||||
$activeGatewayProfile.set('default')
|
||||
$sessionSeenCounts.set({})
|
||||
$unreadFinishedMarkers.set({})
|
||||
$unreadFinishedSessionIds.set([])
|
||||
}
|
||||
|
||||
describe('persisted unread (session-unread)', () => {
|
||||
beforeEach(resetAll)
|
||||
|
||||
it('reconstructs the green dot from a seen watermark after a cold start', () => {
|
||||
// "Restart": persisted watermark says 3 messages were seen; the freshly
|
||||
// loaded list says the session now has 5 — it finished while we were away.
|
||||
$sessionSeenCounts.set({ default: { s1: 3 } })
|
||||
|
||||
setSessions([session({ id: 's1', message_count: 5 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
|
||||
})
|
||||
|
||||
it('seeds the watermark on first sight instead of painting every row green', () => {
|
||||
setSessions([session({ id: 's1', message_count: 5 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
expect($sessionSeenCounts.get()).toEqual({ default: { s1: 5 } })
|
||||
})
|
||||
|
||||
it('keeps a live-edge marker across a transient wipe (gateway/profile switch)', () => {
|
||||
setSessions([session({ id: 's1', message_count: 4 })])
|
||||
markSessionUnreadFinished('s1')
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
|
||||
|
||||
// Gateway switch: transient paint layer wiped, lists drained…
|
||||
setSessions([])
|
||||
$unreadFinishedSessionIds.set([])
|
||||
|
||||
// …then the profile's lists load again: the persisted marker repaints.
|
||||
setSessions([session({ id: 's1', message_count: 4 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
|
||||
})
|
||||
|
||||
it('acks watermark + marker when the user opens the session', () => {
|
||||
$sessionSeenCounts.set({ default: { s1: 3 } })
|
||||
setSessions([session({ id: 's1', message_count: 5 })])
|
||||
markSessionUnreadFinished('s1')
|
||||
|
||||
setSelectedStoredSessionId('s1')
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
expect($unreadFinishedMarkers.get()).toEqual({})
|
||||
expect($sessionSeenCounts.get()).toEqual({ default: { s1: 5 } })
|
||||
|
||||
// A later refresh with the same count stays read.
|
||||
setSessions([session({ id: 's1', message_count: 5 })])
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
})
|
||||
|
||||
it('tracks the selected session’s count so on-screen activity never reads as unread', () => {
|
||||
$selectedStoredSessionId.set('s1')
|
||||
setSessions([session({ id: 's1', message_count: 2 })])
|
||||
|
||||
// The turn completes while the user is looking at it.
|
||||
setSessions([session({ id: 's1', message_count: 7 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
expect($sessionSeenCounts.get()).toEqual({ default: { s1: 7 } })
|
||||
|
||||
// Navigating away must not retroactively flag it.
|
||||
setSelectedStoredSessionId(null)
|
||||
setSessions([session({ id: 's1', message_count: 7 })])
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
})
|
||||
|
||||
it('follows auto-compression id rotation via the durable lineage id', () => {
|
||||
$sessionSeenCounts.set({ default: { root: 3 } })
|
||||
|
||||
setSessions([session({ _lineage_root_id: 'root', id: 'tip-2', message_count: 6 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['tip-2'])
|
||||
|
||||
// Opening the rotated tip acks the lineage watermark.
|
||||
setSelectedStoredSessionId('tip-2')
|
||||
expect($sessionSeenCounts.get()).toEqual({ default: { root: 6 } })
|
||||
})
|
||||
|
||||
it('reconstructs unread for cron rows too', () => {
|
||||
$sessionSeenCounts.set({ default: { cron1: 1 } })
|
||||
|
||||
$cronSessions.set([session({ id: 'cron1', message_count: 3 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['cron1'])
|
||||
})
|
||||
|
||||
it('never paints messaging rows from count growth, but honors live-edge markers', () => {
|
||||
// First sight seeds…
|
||||
$messagingSessions.set([session({ id: 'tg1', message_count: 10 })])
|
||||
// …then inbound messages bump the count: NOT "finished — unread".
|
||||
$messagingSessions.set([session({ id: 'tg1', message_count: 14 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
|
||||
// A real background turn edge still marks it, durably.
|
||||
markSessionUnreadFinished('tg1')
|
||||
$unreadFinishedSessionIds.set([])
|
||||
$messagingSessions.set([session({ id: 'tg1', message_count: 14 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['tg1'])
|
||||
})
|
||||
|
||||
it('preserves a live-edge id whose row is not in any loaded list yet', () => {
|
||||
// A brand-new session's first turn isn't flushed to the sidebar list until
|
||||
// persisted — the flag must survive list refreshes in the meantime.
|
||||
markSessionUnreadFinished('fresh')
|
||||
|
||||
setSessions([session({ id: 'other', message_count: 2 })])
|
||||
|
||||
expect($unreadFinishedSessionIds.get()).toContain('fresh')
|
||||
})
|
||||
|
||||
it('keeps same-id sessions in different profiles independent', () => {
|
||||
// Session ids are caller-supplied and every profile backend is its own
|
||||
// namespace, so "s1" in alpha and "s1" in beta are different sessions —
|
||||
// and cron/messaging lists always mix profiles into one array.
|
||||
$sessionSeenCounts.set({ alpha: { s1: 3 }, beta: { s1: 5 } })
|
||||
$unreadFinishedMarkers.set({ alpha: ['s1'] })
|
||||
|
||||
setSessions([
|
||||
session({ id: 's1', message_count: 5, profile: 'beta' }),
|
||||
session({ id: 's1', message_count: 5, profile: 'alpha' })
|
||||
])
|
||||
|
||||
// Only alpha's row is past its watermark; beta's sits exactly on it.
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
|
||||
expect($sessionSeenCounts.get()).toEqual({ alpha: { s1: 3 }, beta: { s1: 5 } })
|
||||
|
||||
// Opening beta's row (the gateway is on beta) acks beta only.
|
||||
$activeGatewayProfile.set('beta')
|
||||
setSelectedStoredSessionId('s1')
|
||||
|
||||
expect($sessionSeenCounts.get()).toEqual({ alpha: { s1: 3 }, beta: { s1: 5 } })
|
||||
// alpha's marker survives an ack of beta's identically-named session.
|
||||
expect($unreadFinishedMarkers.get()).toEqual({ alpha: ['s1'] })
|
||||
|
||||
// A later refresh must not launder alpha's gap through beta's selection.
|
||||
setSessions([
|
||||
session({ id: 's1', message_count: 6, profile: 'beta' }),
|
||||
session({ id: 's1', message_count: 5, profile: 'alpha' })
|
||||
])
|
||||
|
||||
expect($sessionSeenCounts.get()).toEqual({ alpha: { s1: 3 }, beta: { s1: 6 } })
|
||||
expect($unreadFinishedMarkers.get()).toEqual({ alpha: ['s1'] })
|
||||
})
|
||||
|
||||
it('files a live-edge marker with no loaded row under the active gateway profile', () => {
|
||||
$activeGatewayProfile.set('work')
|
||||
|
||||
markSessionUnreadFinished('fresh')
|
||||
|
||||
expect($unreadFinishedMarkers.get()).toEqual({ work: ['fresh'] })
|
||||
expect($unreadFinishedSessionIds.get()).toEqual(['fresh'])
|
||||
})
|
||||
|
||||
it('forgets a deleted session’s persisted unread, per profile', () => {
|
||||
$sessionSeenCounts.set({ alpha: { root: 3, other: 1 }, beta: { root: 9 } })
|
||||
$unreadFinishedMarkers.set({ alpha: ['root', 'keep'], beta: ['root'] })
|
||||
$unreadFinishedSessionIds.set(['tip-2'])
|
||||
|
||||
forgetSessionUnread(['tip-2', 'root', null, undefined], 'alpha')
|
||||
|
||||
expect($sessionSeenCounts.get()).toEqual({ alpha: { other: 1 }, beta: { root: 9 } })
|
||||
expect($unreadFinishedMarkers.get()).toEqual({ alpha: ['keep'], beta: ['root'] })
|
||||
expect($unreadFinishedSessionIds.get()).toEqual([])
|
||||
})
|
||||
|
||||
it('drops an emptied profile bucket instead of persisting an empty shell', () => {
|
||||
$sessionSeenCounts.set({ alpha: { s1: 3 } })
|
||||
$unreadFinishedMarkers.set({ alpha: ['s1'] })
|
||||
|
||||
forgetSessionUnread(['s1'], 'alpha')
|
||||
|
||||
expect($sessionSeenCounts.get()).toEqual({})
|
||||
expect($unreadFinishedMarkers.get()).toEqual({})
|
||||
})
|
||||
|
||||
it('caps markers per profile, evicting the oldest', () => {
|
||||
for (let i = 0; i < 201; i += 1) {
|
||||
markSessionUnreadFinished(`s${i}`)
|
||||
}
|
||||
|
||||
const markers = $unreadFinishedMarkers.get().default
|
||||
|
||||
expect(markers).toHaveLength(200)
|
||||
expect(markers[0]).toBe('s1')
|
||||
expect(markers.at(-1)).toBe('s200')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user