test(settings): settingsGuard unit tests — tab gating scenarios

Extract guard logic to settingsGuard.ts for testability.
Router beforeEach keeps async config.load() wrapper, delegates to sync guard.
14 test cases cover system/fine-tune/developer gates across cloud/self-hosted/tier/GPU profile combos.
This commit is contained in:
pyr0ball 2026-03-22 16:27:45 -07:00
parent feea057463
commit 3e41dbf030
3 changed files with 168 additions and 12 deletions

View file

@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useAppConfigStore } from '../stores/appConfig' import { useAppConfigStore } from '../stores/appConfig'
import { settingsGuard } from './settingsGuard'
export const router = createRouter({ export const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@ -39,16 +40,5 @@ router.beforeEach(async (to, _from, next) => {
if (!to.path.startsWith('/settings/')) return next() if (!to.path.startsWith('/settings/')) return next()
const config = useAppConfigStore() const config = useAppConfigStore()
if (!config.loaded) await config.load() if (!config.loaded) await config.load()
const tab = to.path.replace('/settings/', '') settingsGuard(to, _from, next)
const devOverride = config.devTierOverride
const gpuProfiles = ['single-gpu', 'dual-gpu']
if (tab === 'system' && config.isCloud) return next('/settings/my-profile')
if (tab === 'fine-tune') {
const cloudBlocked = config.isCloud && config.tier !== 'premium'
const selfHostedBlocked = !config.isCloud && !gpuProfiles.includes(config.inferenceProfile)
if (cloudBlocked || selfHostedBlocked) return next('/settings/my-profile')
}
if (tab === 'developer' && !config.isDevMode && !devOverride) return next('/settings/my-profile')
next()
}) })

View file

@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAppConfigStore } from '../stores/appConfig'
import { settingsGuard } from './settingsGuard'
vi.mock('../composables/useApi', () => ({ useApiFetch: vi.fn() }))
describe('settingsGuard', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
})
afterEach(() => {
localStorage.clear()
})
it('passes through non-settings routes immediately', () => {
const next = vi.fn()
settingsGuard({ path: '/review' }, {}, next)
// Guard only handles /settings/* — for non-settings routes the router
// calls next() before reaching settingsGuard, but the guard itself
// will still call next() with no redirect since no tab matches
expect(next).toHaveBeenCalledWith()
})
it('redirects /settings/system in cloud mode', () => {
const store = useAppConfigStore()
store.isCloud = true
const next = vi.fn()
settingsGuard({ path: '/settings/system' }, {}, next)
expect(next).toHaveBeenCalledWith('/settings/my-profile')
})
it('allows /settings/system in self-hosted mode', () => {
const store = useAppConfigStore()
store.isCloud = false
const next = vi.fn()
settingsGuard({ path: '/settings/system' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('redirects /settings/fine-tune for non-GPU self-hosted', () => {
const store = useAppConfigStore()
store.isCloud = false
store.inferenceProfile = 'cpu'
const next = vi.fn()
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
expect(next).toHaveBeenCalledWith('/settings/my-profile')
})
it('allows /settings/fine-tune for single-gpu self-hosted', () => {
const store = useAppConfigStore()
store.isCloud = false
store.inferenceProfile = 'single-gpu'
const next = vi.fn()
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('allows /settings/fine-tune for dual-gpu self-hosted', () => {
const store = useAppConfigStore()
store.isCloud = false
store.inferenceProfile = 'dual-gpu'
const next = vi.fn()
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('redirects /settings/fine-tune on cloud when tier is not premium', () => {
const store = useAppConfigStore()
store.isCloud = true
store.tier = 'paid'
const next = vi.fn()
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
expect(next).toHaveBeenCalledWith('/settings/my-profile')
})
it('allows /settings/fine-tune on cloud when tier is premium', () => {
const store = useAppConfigStore()
store.isCloud = true
store.tier = 'premium'
const next = vi.fn()
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('redirects /settings/developer when not dev mode and no override', () => {
const store = useAppConfigStore()
store.isDevMode = false
const next = vi.fn()
settingsGuard({ path: '/settings/developer' }, {}, next)
expect(next).toHaveBeenCalledWith('/settings/my-profile')
})
it('allows /settings/developer when isDevMode is true', () => {
const store = useAppConfigStore()
store.isDevMode = true
const next = vi.fn()
settingsGuard({ path: '/settings/developer' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('allows /settings/developer when dev_tier_override set in localStorage', () => {
const store = useAppConfigStore()
store.isDevMode = false
localStorage.setItem('dev_tier_override', 'premium')
const next = vi.fn()
settingsGuard({ path: '/settings/developer' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('allows /settings/privacy in cloud mode', () => {
const store = useAppConfigStore()
store.isCloud = true
const next = vi.fn()
settingsGuard({ path: '/settings/privacy' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('allows /settings/privacy in self-hosted mode', () => {
const store = useAppConfigStore()
store.isCloud = false
const next = vi.fn()
settingsGuard({ path: '/settings/privacy' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
it('allows /settings/license in both modes', () => {
const store = useAppConfigStore()
store.isCloud = true
const next = vi.fn()
settingsGuard({ path: '/settings/license' }, {}, next)
expect(next).toHaveBeenCalledWith()
})
})

View file

@ -0,0 +1,31 @@
import { useAppConfigStore } from '../stores/appConfig'
const GPU_PROFILES = ['single-gpu', 'dual-gpu']
/**
* Synchronous tab-gating logic for /settings/* routes.
* Called by the async router.beforeEach after config.load() has resolved.
* Reading devTierOverride from localStorage here (not only the store ref) ensures
* the guard reflects overrides set externally before the store hydrates.
*/
export function settingsGuard(
to: { path: string },
_from: unknown,
next: (to?: string) => void,
): void {
const config = useAppConfigStore()
const tab = to.path.replace('/settings/', '')
const devOverride = config.devTierOverride || localStorage.getItem('dev_tier_override')
if (tab === 'system' && config.isCloud) return next('/settings/my-profile')
if (tab === 'fine-tune') {
const cloudBlocked = config.isCloud && config.tier !== 'premium'
const selfHostedBlocked = !config.isCloud && !GPU_PROFILES.includes(config.inferenceProfile)
if (cloudBlocked || selfHostedBlocked) return next('/settings/my-profile')
}
if (tab === 'developer' && !config.isDevMode && !devOverride) return next('/settings/my-profile')
next()
}