mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
aa26c4d9b2
* feat: scope AI session browser storage to the logged-in user Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: address review nits in user-scoped session storage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: store AI session list in per-user IndexedDB via shared userScopedDb Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: cover legacy chat-history migration; address review nits Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: harden chat-DB writes and session-key cleanup per PR review - route HistoryManager writes through whenReady() so they can't land in a previous user's DB after an in-place user switch (drop cached this.indexDB) - dedup the legacy chat-DB claim to one session-wide promise so racing manager instances can't issue blocking concurrent deleteDB calls - delete bare windmill_sessions keys unconditionally even when the user DB is already populated, closing a partial-migration leak window Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: trim package-lock to only the fake-indexeddb addition npm install -D had also stripped "dev": true from ~34 unrelated optional native-binding packages (npm graph recomputation). Restore main's lockfile and graft in only the fake-indexeddb node, so the lock diff is exactly the intended dev dependency and prod-install classification of those bindings is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
|
|
// The userStore subscription is gated on BROWSER; the vitest "server" env
|
|
// reports BROWSER=false.
|
|
vi.mock('esm-env', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('esm-env')>()),
|
|
BROWSER: true
|
|
}))
|
|
|
|
import { userStore, type UserExt } from '$lib/stores'
|
|
import {
|
|
scopedKey,
|
|
getCurrentUserEmail,
|
|
onUserChange,
|
|
migrateLegacyLocalStorage
|
|
} from './userScopedStorage'
|
|
|
|
function asUser(email: string): UserExt {
|
|
return { email, username: email.split('@')[0] } as unknown as UserExt
|
|
}
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
userStore.set(undefined)
|
|
})
|
|
|
|
describe('scopedKey / getCurrentUserEmail', () => {
|
|
it('returns undefined while no user is logged in', () => {
|
|
expect(getCurrentUserEmail()).toBeUndefined()
|
|
expect(scopedKey('windmill_sessions')).toBeUndefined()
|
|
})
|
|
|
|
it('namespaces by email and isolates distinct users', () => {
|
|
userStore.set(asUser('a@x.com'))
|
|
expect(scopedKey('windmill_sessions')).toBe('windmill_sessions::a@x.com')
|
|
userStore.set(asUser('b@y.com'))
|
|
expect(scopedKey('windmill_sessions')).toBe('windmill_sessions::b@y.com')
|
|
})
|
|
})
|
|
|
|
describe('onUserChange', () => {
|
|
it('fires immediately with the current email, then on every change', () => {
|
|
userStore.set(asUser('a@x.com'))
|
|
const calls: Array<[string | undefined, string | undefined]> = []
|
|
onUserChange((email, prev) => calls.push([email, prev]))
|
|
// Immediate fire with current email, prev undefined.
|
|
expect(calls).toEqual([['a@x.com', undefined]])
|
|
userStore.set(asUser('b@y.com'))
|
|
expect(calls).toEqual([
|
|
['a@x.com', undefined],
|
|
['b@y.com', 'a@x.com']
|
|
])
|
|
// No-op when the email is unchanged.
|
|
userStore.set(asUser('b@y.com'))
|
|
expect(calls).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
describe('migrateLegacyLocalStorage', () => {
|
|
it('claims a legacy key into the target and deletes the legacy copy', () => {
|
|
localStorage.setItem('ai-chat-autonomy-mode', 'yolo')
|
|
migrateLegacyLocalStorage('ai-chat-autonomy-mode', 'ai-chat-autonomy-mode::a@x.com')
|
|
expect(localStorage.getItem('ai-chat-autonomy-mode::a@x.com')).toBe('yolo')
|
|
expect(localStorage.getItem('ai-chat-autonomy-mode')).toBeNull()
|
|
})
|
|
|
|
it('does not overwrite an existing target', () => {
|
|
localStorage.setItem('ai-chat-autonomy-mode', 'yolo')
|
|
localStorage.setItem('ai-chat-autonomy-mode::a@x.com', 'acceptedit')
|
|
migrateLegacyLocalStorage('ai-chat-autonomy-mode', 'ai-chat-autonomy-mode::a@x.com')
|
|
expect(localStorage.getItem('ai-chat-autonomy-mode::a@x.com')).toBe('acceptedit')
|
|
// Legacy left untouched since the target was already populated.
|
|
expect(localStorage.getItem('ai-chat-autonomy-mode')).toBe('yolo')
|
|
})
|
|
|
|
it('is a no-op when the target key is undefined (no user)', () => {
|
|
localStorage.setItem('ai-chat-autonomy-mode', 'yolo')
|
|
migrateLegacyLocalStorage('ai-chat-autonomy-mode', undefined)
|
|
expect(localStorage.getItem('ai-chat-autonomy-mode')).toBe('yolo')
|
|
})
|
|
})
|