* feat: add pushed_at field to ClaudeJob schema Nullable DateTime column to record when the agent's feature branch was pushed to origin. Enables the UI to show a 'pushed' state independently of DONE status. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: GitHub-link op DONE-card + pushed_at doorvoer - lib/job-status-url.ts: getBranchUrl(repoUrl, branch) → GitHub tree URL - JobState + ClaudeJobEvent: pushed_at? veld toegevoegd - realtime/solo/route.ts: pushed_at in Prisma-select, JobPayload en mapping - SoloBoardProps + TaskDetailDialog: repoUrl prop doorgevoerd - task-detail-dialog: "Open op GitHub"-link als done + pushed_at + branch + repoUrl - 3 unit-tests voor getBranchUrl; totaal 261 tests groen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add VerifyResult enum, verify_only on Task, verify_result on ClaudeJob Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add verify_result+pushed_at to JobState, VerifyResultApi type, SSE payload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: verify_only field on SoloTask, PATCH route saves verify_only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: TaskDetailDialog — verify_result display + verify_only checkbox Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: verify_only PATCH + verify_result dialog render + store fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: document VerifyResult enum, verify_only task field, pushed_at in architecture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days * feat(M13): add auto_pr field to Product schema + migration * feat(M13): auto_pr toggle in product settings — server action + UI component + tests * feat(M13): add pr_url to ClaudeJob schema + migration * feat(M13): UI — 'Open PR' link on DONE-card; pr_url in JobState + SSE + task-dialog * feat(M13): add retry_count migration + regen erd - Migration ALTER TABLE claude_jobs ADD COLUMN retry_count INT DEFAULT 0 (schema.prisma was reeds bijgewerkt in eerdere commits) - docs/erd.svg geregenereerd voor de complete M13-schema-wijzigingen (verify_result, verify_only, pushed_at, pr_url, auto_pr, retry_count) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
4.3 KiB
TypeScript
110 lines
4.3 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { useSoloStore } from '@/stores/solo-store'
|
|
import type { RealtimeEvent } from '@/stores/solo-store'
|
|
import type { SoloTask } from '@/components/solo/solo-board'
|
|
|
|
const baseTask = (id: string, overrides: Partial<SoloTask> = {}): SoloTask => ({
|
|
id,
|
|
title: `Task ${id}`,
|
|
description: null,
|
|
implementation_plan: null,
|
|
priority: 1,
|
|
sort_order: 1,
|
|
status: 'TO_DO',
|
|
verify_only: false,
|
|
story_id: 'story-1',
|
|
story_code: 'ST-100',
|
|
story_title: 'Original Story',
|
|
task_code: 'ST-100.1',
|
|
...overrides,
|
|
})
|
|
|
|
const taskEvent = (overrides: Partial<RealtimeEvent>): RealtimeEvent => ({
|
|
op: 'U',
|
|
entity: 'task',
|
|
id: 'task-1',
|
|
story_id: 'story-1',
|
|
product_id: 'prod-1',
|
|
sprint_id: 'sprint-1',
|
|
assignee_id: 'user-1',
|
|
...overrides,
|
|
})
|
|
|
|
const storyEvent = (overrides: Partial<RealtimeEvent>): RealtimeEvent => ({
|
|
op: 'U',
|
|
entity: 'story',
|
|
id: 'story-1',
|
|
product_id: 'prod-1',
|
|
sprint_id: 'sprint-1',
|
|
assignee_id: 'user-1',
|
|
...overrides,
|
|
})
|
|
|
|
describe('solo-store realtime', () => {
|
|
beforeEach(() => {
|
|
useSoloStore.setState({ tasks: {}, pendingOps: new Set() })
|
|
})
|
|
|
|
it('applies a task status update', () => {
|
|
useSoloStore.getState().initTasks([baseTask('task-1', { status: 'TO_DO' })])
|
|
useSoloStore.getState().handleRealtimeEvent(taskEvent({ task_status: 'IN_PROGRESS' }))
|
|
expect(useSoloStore.getState().tasks['task-1'].status).toBe('IN_PROGRESS')
|
|
})
|
|
|
|
it('skips a task update when pendingOps has the id', () => {
|
|
useSoloStore.getState().initTasks([baseTask('task-1', { status: 'TO_DO' })])
|
|
useSoloStore.getState().markPending('task-1')
|
|
useSoloStore.getState().handleRealtimeEvent(taskEvent({ task_status: 'DONE' }))
|
|
expect(useSoloStore.getState().tasks['task-1'].status).toBe('TO_DO')
|
|
})
|
|
|
|
it('applies the update again once pendingOps is cleared', () => {
|
|
useSoloStore.getState().initTasks([baseTask('task-1', { status: 'TO_DO' })])
|
|
useSoloStore.getState().markPending('task-1')
|
|
useSoloStore.getState().handleRealtimeEvent(taskEvent({ task_status: 'DONE' }))
|
|
expect(useSoloStore.getState().tasks['task-1'].status).toBe('TO_DO')
|
|
useSoloStore.getState().clearPending('task-1')
|
|
useSoloStore.getState().handleRealtimeEvent(taskEvent({ task_status: 'DONE' }))
|
|
expect(useSoloStore.getState().tasks['task-1'].status).toBe('DONE')
|
|
})
|
|
|
|
it('removes a task on D op', () => {
|
|
useSoloStore.getState().initTasks([baseTask('task-1'), baseTask('task-2')])
|
|
useSoloStore.getState().handleRealtimeEvent(taskEvent({ id: 'task-1', op: 'D' }))
|
|
expect(useSoloStore.getState().tasks['task-1']).toBeUndefined()
|
|
expect(useSoloStore.getState().tasks['task-2']).toBeDefined()
|
|
})
|
|
|
|
it('ignores task INSERT/UPDATE for tasks not in the store', () => {
|
|
useSoloStore.getState().initTasks([baseTask('task-1')])
|
|
useSoloStore.getState().handleRealtimeEvent(taskEvent({ id: 'task-other', task_status: 'DONE' }))
|
|
expect(Object.keys(useSoloStore.getState().tasks)).toEqual(['task-1'])
|
|
})
|
|
|
|
it('updates story_title/code on all child tasks via story UPDATE', () => {
|
|
useSoloStore.getState().initTasks([
|
|
baseTask('task-1', { story_id: 'story-1' }),
|
|
baseTask('task-2', { story_id: 'story-1' }),
|
|
baseTask('task-3', { story_id: 'story-other', story_title: 'Other' }),
|
|
])
|
|
useSoloStore.getState().handleRealtimeEvent(
|
|
storyEvent({ story_title: 'Renamed Story', story_code: 'ST-100B' }),
|
|
)
|
|
expect(useSoloStore.getState().tasks['task-1'].story_title).toBe('Renamed Story')
|
|
expect(useSoloStore.getState().tasks['task-1'].story_code).toBe('ST-100B')
|
|
expect(useSoloStore.getState().tasks['task-2'].story_title).toBe('Renamed Story')
|
|
expect(useSoloStore.getState().tasks['task-3'].story_title).toBe('Other')
|
|
})
|
|
|
|
it('removes all tasks of a story on story DELETE', () => {
|
|
useSoloStore.getState().initTasks([
|
|
baseTask('task-1', { story_id: 'story-1' }),
|
|
baseTask('task-2', { story_id: 'story-1' }),
|
|
baseTask('task-3', { story_id: 'story-other' }),
|
|
])
|
|
useSoloStore.getState().handleRealtimeEvent(storyEvent({ op: 'D' }))
|
|
expect(useSoloStore.getState().tasks['task-1']).toBeUndefined()
|
|
expect(useSoloStore.getState().tasks['task-2']).toBeUndefined()
|
|
expect(useSoloStore.getState().tasks['task-3']).toBeDefined()
|
|
})
|
|
})
|