diff --git a/__tests__/components/shared/sprint-switcher.test.tsx b/__tests__/components/shared/sprint-switcher.test.tsx
new file mode 100644
index 0000000..8b1e2ec
--- /dev/null
+++ b/__tests__/components/shared/sprint-switcher.test.tsx
@@ -0,0 +1,125 @@
+// @vitest-environment jsdom
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import React from 'react'
+
+const pushMock = vi.fn()
+const refreshMock = vi.fn()
+const pathnameMock = vi.fn(() => '/products/p1/sprint')
+
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({ push: pushMock, refresh: refreshMock }),
+ usePathname: () => pathnameMock(),
+}))
+
+vi.mock('@/actions/active-sprint', () => ({
+ setActiveSprintAction: vi.fn(),
+}))
+
+vi.mock('sonner', () => ({
+ toast: { error: vi.fn(), success: vi.fn() },
+}))
+
+const isDemoMock = { value: false }
+vi.mock('@/stores/user-settings/store', () => ({
+ useUserSettingsStore: (selector: (s: { context: { isDemo: boolean } }) => unknown) =>
+ selector({ context: { isDemo: isDemoMock.value } }),
+}))
+
+vi.mock('@/components/ui/dropdown-menu', () => {
+ type Props = { children?: React.ReactNode; onClick?: () => void; className?: string }
+ const PassThrough = ({ children }: Props) => <>{children}>
+ return {
+ DropdownMenu: PassThrough,
+ DropdownMenuTrigger: PassThrough,
+ DropdownMenuContent: PassThrough,
+ DropdownMenuItem: ({ children, onClick, className }: Props) => (
+
+ ),
+ DropdownMenuSeparator: () => null,
+ }
+})
+
+vi.mock('@/components/ui/tooltip', () => {
+ type Props = { children?: React.ReactNode }
+ const PassThrough = ({ children }: Props) => <>{children}>
+ return {
+ Tooltip: PassThrough,
+ TooltipContent: PassThrough,
+ TooltipProvider: PassThrough,
+ TooltipTrigger: PassThrough,
+ }
+})
+
+import { setActiveSprintAction } from '@/actions/active-sprint'
+import { toast } from 'sonner'
+import { SprintSwitcher } from '@/components/shared/sprint-switcher'
+
+const actionMock = setActiveSprintAction as unknown as ReturnType
+const toastError = toast.error as unknown as ReturnType
+const toastSuccess = toast.success as unknown as ReturnType
+
+const sprints = [
+ { id: 's1', code: 'SP-1', sprint_goal: 'Goal 1', status: 'open' as const },
+ { id: 's2', code: 'SP-2', sprint_goal: 'Goal 2', status: 'open' as const },
+]
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ isDemoMock.value = false
+ actionMock.mockResolvedValue({ success: true })
+ pathnameMock.mockReturnValue('/products/p1/sprint')
+})
+
+describe('SprintSwitcher', () => {
+ it('demo: clicking another sprint navigates via router.push without calling the action', () => {
+ isDemoMock.value = true
+ render(
+ ,
+ )
+ fireEvent.click(screen.getByText('Goal 2'))
+ expect(pushMock).toHaveBeenCalledWith('/products/p1/sprint/s2')
+ expect(actionMock).not.toHaveBeenCalled()
+ expect(toastError).not.toHaveBeenCalled()
+ expect(toastSuccess).not.toHaveBeenCalled()
+ })
+
+ it('non-demo: clicking another sprint calls setActiveSprintAction', async () => {
+ isDemoMock.value = false
+ render(
+ ,
+ )
+ fireEvent.click(screen.getByText('Goal 2'))
+ // Wait microtask for the transition to flush.
+ await Promise.resolve()
+ expect(actionMock).toHaveBeenCalledWith('p1', 's2')
+ })
+
+ it('clicking the already-active sprint does nothing', () => {
+ isDemoMock.value = true
+ render(
+ ,
+ )
+ fireEvent.click(screen.getByText('Goal 1'))
+ expect(pushMock).not.toHaveBeenCalled()
+ expect(actionMock).not.toHaveBeenCalled()
+ })
+})
diff --git a/components/shared/sprint-switcher.tsx b/components/shared/sprint-switcher.tsx
index 4377742..e1a041c 100644
--- a/components/shared/sprint-switcher.tsx
+++ b/components/shared/sprint-switcher.tsx
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { setActiveSprintAction } from '@/actions/active-sprint'
+import { useUserSettingsStore } from '@/stores/user-settings/store'
import type { SprintStatusApi } from '@/lib/task-status'
import { debugProps } from '@/lib/debug'
@@ -44,6 +45,7 @@ export function SprintSwitcher({
const [isPending, startTransition] = useTransition()
const [showClosed, setShowClosed] = useState(false)
const buildingSet = new Set(buildingSprintIds)
+ const isDemo = useUserSettingsStore(s => s.context.isDemo)
const visibleSprints = sprints.filter(s => {
if (showClosed) return true
@@ -53,6 +55,10 @@ export function SprintSwitcher({
function handleSwitchSprint(sprintId: string) {
if (sprintId === activeSprint?.id) return
+ if (isDemo) {
+ router.push(`/products/${productId}/sprint/${sprintId}`)
+ return
+ }
startTransition(async () => {
const result = await setActiveSprintAction(productId, sprintId)
if (result?.error) {