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();
});
});