247 lines
7.3 KiB
TypeScript
247 lines
7.3 KiB
TypeScript
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 }) => (
|
|
<div>workspace shell {jobId}</div>
|
|
),
|
|
}));
|
|
|
|
describe('component test harness', () => {
|
|
it('renders the Spoon landing headline', () => {
|
|
render(<Hero />);
|
|
expect(
|
|
screen.getByRole('heading', {
|
|
name: /fork freely & keep them all intimately close to upstream\./i,
|
|
}),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders the new Spoon form fields', () => {
|
|
render(<NewSpoonForm />);
|
|
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(
|
|
<AgentThread
|
|
jobId='job-1'
|
|
messages={[]}
|
|
events={[]}
|
|
interactions={[]}
|
|
workspaceChanges={[
|
|
{
|
|
_id: 'change-1',
|
|
_creationTime: 1,
|
|
jobId: 'job-1',
|
|
spoonId: 'spoon-1',
|
|
ownerId: 'user-1',
|
|
path: 'apps/web/auth.ts',
|
|
source: 'agent',
|
|
changeType: 'modified',
|
|
diff: 'diff --git a/apps/web/auth.ts b/apps/web/auth.ts\n+authentik',
|
|
createdAt: 1,
|
|
} as never,
|
|
]}
|
|
disabled={false}
|
|
agentTurnActive={false}
|
|
onOpenFile={onOpenFile}
|
|
onOpenDiff={onOpenDiff}
|
|
/>,
|
|
);
|
|
|
|
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('keeps the workspace thread focused on user, agent, and tool content', () => {
|
|
render(
|
|
<AgentThread
|
|
jobId='job-1'
|
|
messages={[
|
|
{
|
|
_id: 'message-system',
|
|
_creationTime: 1,
|
|
jobId: 'job-1',
|
|
spoonId: 'spoon-1',
|
|
ownerId: 'user-1',
|
|
role: 'system',
|
|
content: 'Workspace is ready.',
|
|
status: 'completed',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
} as never,
|
|
{
|
|
_id: 'message-empty-assistant',
|
|
_creationTime: 2,
|
|
jobId: 'job-1',
|
|
spoonId: 'spoon-1',
|
|
ownerId: 'user-1',
|
|
role: 'assistant',
|
|
content: '',
|
|
status: 'completed',
|
|
createdAt: 2,
|
|
updatedAt: 2,
|
|
} as never,
|
|
{
|
|
_id: 'message-user',
|
|
_creationTime: 3,
|
|
jobId: 'job-1',
|
|
spoonId: 'spoon-1',
|
|
ownerId: 'user-1',
|
|
role: 'user',
|
|
content: 'Use Authentik as the only provider.',
|
|
status: 'completed',
|
|
createdAt: 3,
|
|
updatedAt: 3,
|
|
} as never,
|
|
{
|
|
_id: 'message-assistant',
|
|
_creationTime: 4,
|
|
jobId: 'job-1',
|
|
spoonId: 'spoon-1',
|
|
ownerId: 'user-1',
|
|
role: 'assistant',
|
|
content: 'I found the Auth.js provider configuration.',
|
|
status: 'completed',
|
|
createdAt: 4,
|
|
updatedAt: 4,
|
|
} as never,
|
|
{
|
|
_id: 'message-tool',
|
|
_creationTime: 5,
|
|
jobId: 'job-1',
|
|
spoonId: 'spoon-1',
|
|
ownerId: 'user-1',
|
|
role: 'tool',
|
|
content: 'rg Authentik',
|
|
status: 'completed',
|
|
createdAt: 5,
|
|
updatedAt: 5,
|
|
} as never,
|
|
]}
|
|
events={[
|
|
{
|
|
_id: 'event-info',
|
|
_creationTime: 1,
|
|
jobId: 'job-1',
|
|
level: 'info',
|
|
phase: 'plan',
|
|
message: 'Sending message to agent.',
|
|
createdAt: 1,
|
|
} as never,
|
|
]}
|
|
interactions={[]}
|
|
workspaceChanges={[]}
|
|
disabled={false}
|
|
agentTurnActive={false}
|
|
onOpenFile={vi.fn()}
|
|
onOpenDiff={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByText('Workspace is ready.')).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText('Sending message to agent.'),
|
|
).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Assistant')).not.toBeInTheDocument();
|
|
expect(
|
|
screen.getByText('Use Authentik as the only provider.'),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText('I found the Auth.js provider configuration.'),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText('rg Authentik')).toBeInTheDocument();
|
|
});
|
|
|
|
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(<ThreadDetailPage />);
|
|
|
|
expect(screen.getByText('workspace shell job-1')).toBeInTheDocument();
|
|
expect(screen.queryByText('Thread state')).not.toBeInTheDocument();
|
|
});
|
|
});
|