import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import ThreadDetailPage from '../../src/app/(app)/threads/[threadId]/page'; import { AgentThread } from '../../src/components/agent-workspace/agent-thread'; import { extractFileDiff } from '../../src/components/agent-workspace/diff-utils'; import { Hero } from '../../src/components/landing'; import { NewSpoonForm } from '../../src/components/spoons/new-spoon-form'; const { mockUseMutation, mockUseParams, mockUseQuery } = vi.hoisted(() => ({ mockUseMutation: vi.fn(), mockUseParams: vi.fn(), mockUseQuery: vi.fn(), })); vi.mock('convex/react', () => ({ useConvexAuth: () => ({ isAuthenticated: false }), useMutation: mockUseMutation, useQuery: mockUseQuery, })); vi.mock('next/navigation', () => ({ useParams: mockUseParams, useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), })); vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn(), }, })); vi.mock('@/components/agent-workspace/agent-workspace-shell', () => ({ AgentWorkspaceShell: ({ jobId }: { jobId: string }) => (
workspace shell {jobId}
), })); describe('component test harness', () => { it('renders the Spoon landing headline', () => { render(); expect( screen.getByRole('heading', { name: /fork freely & keep them all intimately close to upstream\./i, }), ).toBeInTheDocument(); }); it('renders the new Spoon form fields', () => { render(); expect(screen.getByLabelText(/spoon name/i)).toBeInTheDocument(); expect(screen.getByLabelText(/upstream owner/i)).toBeInTheDocument(); expect(screen.getByLabelText(/upstream repository/i)).toBeInTheDocument(); }); it('extracts a single file diff from a workspace diff', () => { const diff = [ 'diff --git a/apps/web/auth.ts b/apps/web/auth.ts', 'index 123..456 100644', '--- a/apps/web/auth.ts', '+++ b/apps/web/auth.ts', '@@ -1 +1 @@', '-github', '+authentik', 'diff --git a/README.md b/README.md', '--- a/README.md', '+++ b/README.md', '@@ -1 +1 @@', '-old', '+new', ].join('\n'); expect(extractFileDiff(diff, 'apps/web/auth.ts')).toContain('+authentik'); expect(extractFileDiff(diff, 'apps/web/auth.ts')).not.toContain( 'README.md', ); }); it('renders workspace file activity and opens changed files', () => { const onOpenFile = vi.fn(); const onOpenDiff = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: 'Files' })); expect(screen.getByText('apps/web/auth.ts')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'View diff' })); expect(onOpenDiff).toHaveBeenCalledWith('apps/web/auth.ts'); fireEvent.click(screen.getByRole('button', { name: 'Open' })); expect(onOpenFile).toHaveBeenCalledWith('apps/web/auth.ts'); }); it('renders thread workspaces on the canonical thread route', () => { mockUseParams.mockReturnValue({ threadId: 'thread-1' }); mockUseQuery.mockReturnValue({ thread: { _id: 'thread-1', title: 'Update auth', status: 'running', source: 'user_request', priority: 'normal', summary: 'Use Authentik', createdAt: 1, updatedAt: 1, }, spoon: { _id: 'spoon-1', name: 'useSend' }, latestJob: { _id: 'job-1', spoonId: 'spoon-1', status: 'running', workspaceStatus: 'active', }, }); mockUseMutation.mockReturnValue(vi.fn()); render(); expect(screen.getByText('workspace shell job-1')).toBeInTheDocument(); expect(screen.queryByText('Thread state')).not.toBeInTheDocument(); }); });