fix(app): handle nullable detail consumers

This commit is contained in:
Gabriel Brown
2026-07-10 17:24:32 -04:00
parent ed21126eea
commit 2beeeb1732
4 changed files with 103 additions and 2 deletions
@@ -1,3 +1,4 @@
import type { FunctionReturnType } from 'convex/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { proxyWorker } from '@/lib/agent-worker-proxy'; import { proxyWorker } from '@/lib/agent-worker-proxy';
import { convexAuthNextjsToken } from '@convex-dev/auth/nextjs/server'; import { convexAuthNextjsToken } from '@convex-dev/auth/nextjs/server';
@@ -35,7 +36,14 @@ export const POST = async (
{ status: 400 }, { status: 400 },
); );
} }
const details = await fetchQuery(api.threads.get, { threadId }, { token }); const details = (await fetchQuery(
api.threads.get,
{ threadId },
{ token },
)) as FunctionReturnType<typeof api.threads.get> | null;
if (details === null) {
return NextResponse.json({ error: 'Thread not found.' }, { status: 404 });
}
const latestJob = details.latestJob; const latestJob = details.latestJob;
const canSendToWorker = const canSendToWorker =
latestJob && latestJob &&
@@ -1,5 +1,6 @@
'use client'; 'use client';
import type { FunctionReturnType } from 'convex/server';
import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react'; import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useMutation, useQuery } from 'convex/react'; import { useMutation, useQuery } from 'convex/react';
@@ -59,7 +60,10 @@ type PendingOverwrite = {
}; };
export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => { export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
const job = useQuery(api.agentJobs.get, { jobId }); const job = useQuery(api.agentJobs.get, { jobId }) as
| FunctionReturnType<typeof api.agentJobs.get>
| null
| undefined;
const messages = const messages =
useQuery(api.agentJobs.listMessages, { jobId, limit: 200 }) ?? []; useQuery(api.agentJobs.listMessages, { jobId, limit: 200 }) ?? [];
const events = const events =
@@ -323,6 +327,14 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
); );
} }
if (job === null) {
return (
<main className='text-muted-foreground p-6'>
This workspace was not found.
</main>
);
}
const activeFile = activeFilePath ? files[activeFilePath] : undefined; const activeFile = activeFilePath ? files[activeFilePath] : undefined;
const recoverWorkspace = async () => { const recoverWorkspace = async () => {
if (!job.threadId) return; if (!job.threadId) return;
@@ -0,0 +1,31 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { AgentWorkspaceShell } from '../../src/components/agent-workspace/agent-workspace-shell';
const { mockUseMutation, mockUseQuery } = vi.hoisted(() => ({
mockUseMutation: vi.fn(() => vi.fn()),
mockUseQuery: vi.fn(() => null),
}));
vi.mock('convex/react', () => ({
useMutation: mockUseMutation,
useQuery: mockUseQuery,
}));
vi.mock('sonner', () => ({
toast: {
error: vi.fn(),
success: vi.fn(),
},
}));
describe('AgentWorkspaceShell', () => {
it('renders a missing state when the job query returns null', () => {
render(<AgentWorkspaceShell jobId={'job-1' as never} />);
expect(
screen.getByText('This workspace was not found.'),
).toBeInTheDocument();
});
});
@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { POST } from '../../src/app/api/threads/[threadId]/message/route';
const { mockAuthToken, mockFetchMutation, mockFetchQuery, mockProxyWorker } =
vi.hoisted(() => ({
mockAuthToken: vi.fn(),
mockFetchMutation: vi.fn(),
mockFetchQuery: vi.fn(),
mockProxyWorker: vi.fn(),
}));
vi.mock('@convex-dev/auth/nextjs/server', () => ({
convexAuthNextjsToken: mockAuthToken,
}));
vi.mock('convex/nextjs', () => ({
fetchMutation: mockFetchMutation,
fetchQuery: mockFetchQuery,
}));
vi.mock('@/lib/agent-worker-proxy', () => ({
proxyWorker: mockProxyWorker,
}));
describe('thread message route', () => {
beforeEach(() => {
vi.clearAllMocks();
mockAuthToken.mockResolvedValue('token');
});
it('returns 404 when the thread detail query returns null', async () => {
mockFetchQuery.mockResolvedValue(null);
const response = await POST(
new Request('http://localhost/api/threads/thread-1/message', {
method: 'POST',
body: JSON.stringify({ content: 'Hello' }),
}),
{ params: Promise.resolve({ threadId: 'thread-1' }) },
);
expect(response.status).toBe(404);
await expect(response.json()).resolves.toEqual({
error: 'Thread not found.',
});
expect(mockFetchMutation).not.toHaveBeenCalled();
expect(mockProxyWorker).not.toHaveBeenCalled();
});
});