diff --git a/__tests__/api/tasks.test.ts b/__tests__/api/tasks.test.ts
index ed0616e..3b08da7 100644
--- a/__tests__/api/tasks.test.ts
+++ b/__tests__/api/tasks.test.ts
@@ -200,6 +200,35 @@ describe('PATCH /api/tasks/:id', () => {
})
})
+ // TC-T-12
+ it('updates verify_only alone and returns 200 with verify_only in response', async () => {
+ mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'TO_DO', implementation_plan: null, verify_only: true })
+
+ const res = await patchTask(...makeRequest({ verify_only: true }))
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.verify_only).toBe(true)
+ expect(mockPrisma.task.update).toHaveBeenCalledWith(
+ expect.objectContaining({ data: { verify_only: true } }),
+ )
+ })
+
+ it('combines verify_only and implementation_plan into one update call', async () => {
+ const plan = 'Verify only: check test results.'
+ mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'TO_DO', implementation_plan: plan, verify_only: true })
+
+ const res = await patchTask(...makeRequest({ implementation_plan: plan, verify_only: true }))
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data).toMatchObject({ implementation_plan: plan, verify_only: true })
+ expect(mockPrisma.task.update).toHaveBeenCalledTimes(1)
+ expect(mockPrisma.task.update).toHaveBeenCalledWith(
+ expect.objectContaining({ data: { implementation_plan: plan, verify_only: true } }),
+ )
+ })
+
it('returns 400 for malformed JSON', async () => {
const req = new Request('http://localhost/api/tasks/task-1', {
method: 'PATCH',
diff --git a/__tests__/components/solo/task-detail-dialog.test.tsx b/__tests__/components/solo/task-detail-dialog.test.tsx
new file mode 100644
index 0000000..a1ef88b
--- /dev/null
+++ b/__tests__/components/solo/task-detail-dialog.test.tsx
@@ -0,0 +1,180 @@
+// @vitest-environment jsdom
+import '@testing-library/jest-dom'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import type { SoloTask } from '@/components/solo/solo-board'
+
+// Mock heavy UI primitives to avoid portal/JSDOM issues
+vi.mock('@/components/ui/dialog', () => ({
+ Dialog: ({ open, children }: { open: boolean; onOpenChange?: () => void; children: React.ReactNode }) =>
+ open ?
{children}
: null,
+ DialogContent: ({ children }: { children: React.ReactNode }) => {children}
,
+ DialogHeader: ({ children }: { children: React.ReactNode }) => {children}
,
+ DialogTitle: ({ children }: { children: React.ReactNode }) => {children}
,
+}))
+vi.mock('@/components/ui/tooltip', () => ({
+ TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ TooltipTrigger: ({ render: r, children }: { render?: React.ReactElement; children?: React.ReactNode }) =>
+ r ? <>{r}> : <>{children}>,
+ TooltipContent: ({ children }: { children: React.ReactNode }) => {children},
+}))
+vi.mock('@/components/ui/textarea', () => ({
+ Textarea: (props: React.TextareaHTMLAttributes) => ,
+}))
+vi.mock('@/components/ui/badge', () => ({
+ Badge: ({ children, className }: { children: React.ReactNode; className?: string }) =>
+ {children},
+}))
+vi.mock('@/components/ui/button', () => ({
+ Button: ({ children, onClick, disabled }: { children?: React.ReactNode; onClick?: () => void; disabled?: boolean }) =>
+ ,
+}))
+vi.mock('@/components/markdown', () => ({
+ Markdown: ({ children }: { children: React.ReactNode }) => {children}
,
+}))
+vi.mock('@/components/shared/demo-tooltip', () => ({
+ DemoTooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}))
+vi.mock('next/link', () => ({
+ default: ({ children, href }: { children: React.ReactNode; href: string }) => {children},
+}))
+vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
+vi.mock('@/actions/claude-jobs', () => ({
+ enqueueClaudeJobAction: vi.fn(),
+ cancelClaudeJobAction: vi.fn(),
+}))
+vi.mock('@/lib/job-status-url', () => ({
+ getBranchUrl: (repoUrl: string, branch: string) => `${repoUrl}/tree/${branch}`,
+}))
+
+import { useSoloStore } from '@/stores/solo-store'
+import { TaskDetailDialog } from '@/components/solo/task-detail-dialog'
+
+const baseTask: SoloTask = {
+ id: 'task-1',
+ title: 'Test taak',
+ description: null,
+ implementation_plan: null,
+ priority: 2,
+ sort_order: 1,
+ status: 'TO_DO',
+ verify_only: false,
+ story_id: 'story-1',
+ story_code: 'ST-100',
+ story_title: 'Test Story',
+ task_code: 'ST-100.1',
+}
+
+const DEFAULT_PROPS = {
+ productId: 'prod-1',
+ isDemo: false,
+ repoUrl: 'https://github.com/user/repo',
+ onClose: vi.fn(),
+}
+
+function jobDone(verify_result?: string) {
+ return {
+ 'task-1': {
+ job_id: 'j1',
+ task_id: 'task-1',
+ status: 'done' as const,
+ branch: 'feat/job-abc',
+ pushed_at: '2026-01-01T00:00:00Z',
+ ...(verify_result && { verify_result: verify_result as import('@/stores/solo-store').VerifyResultApi }),
+ },
+ }
+}
+
+describe('TaskDetailDialog — verify_result display', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ useSoloStore.setState({
+ tasks: { 'task-1': baseTask },
+ claudeJobsByTaskId: {},
+ connectedWorkers: 0,
+ })
+ global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) })
+ })
+
+ it('shows no verify label when job has no verify_result', () => {
+ useSoloStore.setState({ claudeJobsByTaskId: jobDone() })
+ render()
+ expect(screen.queryByText(/Aligned|Gedeeltelijk|Divergent|Geen wijzigingen/)).toBeNull()
+ })
+
+ it('shows Aligned label with text-status-done class for verify_result=aligned', () => {
+ useSoloStore.setState({ claudeJobsByTaskId: jobDone('aligned') })
+ render()
+ const label = screen.getByText(/Aligned/)
+ expect(label).toHaveClass('text-status-done')
+ })
+
+ it('shows Gedeeltelijk label with text-warning class for verify_result=partial', () => {
+ useSoloStore.setState({ claudeJobsByTaskId: jobDone('partial') })
+ render()
+ const label = screen.getByText(/Gedeeltelijk/)
+ expect(label).toHaveClass('text-warning')
+ })
+
+ it('shows Divergent label with text-error class for verify_result=divergent', () => {
+ useSoloStore.setState({ claudeJobsByTaskId: jobDone('divergent') })
+ render()
+ const label = screen.getByText(/Divergent/)
+ expect(label).toHaveClass('text-error')
+ })
+
+ it('shows Geen wijzigingen label with text-muted-foreground class for verify_result=empty', () => {
+ useSoloStore.setState({ claudeJobsByTaskId: jobDone('empty') })
+ render()
+ const label = screen.getByText(/Geen wijzigingen/)
+ expect(label).toHaveClass('text-muted-foreground')
+ })
+})
+
+describe('TaskDetailDialog — verify_only checkbox', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ useSoloStore.setState({
+ tasks: { 'task-1': baseTask },
+ claudeJobsByTaskId: {},
+ connectedWorkers: 0,
+ })
+ global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) })
+ })
+
+ it('renders verify_only checkbox unchecked when task.verify_only is false', () => {
+ render()
+ const checkbox = screen.getByRole('checkbox')
+ expect(checkbox).toHaveAttribute('aria-checked', 'false')
+ })
+
+ it('renders verify_only checkbox checked when task.verify_only is true', () => {
+ render()
+ const checkbox = screen.getByRole('checkbox')
+ expect(checkbox).toHaveAttribute('aria-checked', 'true')
+ })
+
+ it('calls PATCH with verify_only toggled on click', async () => {
+ render()
+ fireEvent.click(screen.getByRole('checkbox'))
+ await waitFor(() => {
+ expect(global.fetch).toHaveBeenCalledWith(
+ '/api/tasks/task-1',
+ expect.objectContaining({
+ method: 'PATCH',
+ body: JSON.stringify({ verify_only: true }),
+ }),
+ )
+ })
+ })
+
+ it('reverts optimistic toggle when PATCH fails', async () => {
+ global.fetch = vi.fn().mockResolvedValue({ ok: false })
+ render()
+ fireEvent.click(screen.getByRole('checkbox'))
+ await waitFor(() => {
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-checked', 'false')
+ })
+ })
+})
diff --git a/__tests__/stores/solo-store-realtime.test.ts b/__tests__/stores/solo-store-realtime.test.ts
index 589b961..b2f42cf 100644
--- a/__tests__/stores/solo-store-realtime.test.ts
+++ b/__tests__/stores/solo-store-realtime.test.ts
@@ -11,6 +11,7 @@ const baseTask = (id: string, overrides: Partial = {}): SoloTask => ({
priority: 1,
sort_order: 1,
status: 'TO_DO',
+ verify_only: false,
story_id: 'story-1',
story_code: 'ST-100',
story_title: 'Original Story',