Skip to content

Follow Up to Deep Research Frontend + Chat Status Tool #1864

Description

@JSv4

Code Review: Deep Research Frontend + Chat Status Tool

Overview

This PR ships the full frontend surface for the deep-research feature that was backend-only in #1814: a /research/:slug route, a corpus Research tab, report detail view with polling + WebSocket completion, notification wiring, and a chat status tool so users can ask "is my research done?" This is a substantial, well-scoped feature completion.


Positives

  • Architecture is sound. Route resolution follows the same CentralRouteManager Phase-1/3 pattern as extracts, threads, and labelsets. The openedResearchReport reactive var, cache policies, and allowlist discipline test are all consistent with how existing entity routes work.
  • Security posture is good. researchReportBySlug uses filter_visible (creator-only), the resolver is @login_required, non-owner and unknown slug both return null (same response, prevents enumeration), and the anonymous rejection is pinned with a test.
  • Atomic finalize fix is correct. Wrapping the terminal save() and M2M set() calls in transaction.atomic() closes a real gap — a worker crash between them would have left a COMPLETED report with empty provenance.
  • Polling design is clean. isTerminalResearchStatus treats unrecognized statuses as terminal so polling never loops forever on an unknown state. The indirect flow of refetch → status update → useEffect stopping polling is correct.
  • Constants are in the right place. MAX_RESEARCH_PROMPT_CHARS, MAX_RESEARCH_TITLE_CHARS, RESEARCH_REPORT_POLL_INTERVAL_MS, RESEARCH_REPORT_UPDATE_PERMISSION are all in constants.ts, not inlined (CLAUDE.md rule 4).
  • relayStylePagination keyArgs are correct. ["corpusId", "status"] uses the GraphQL field-argument names, per CLAUDE.md pitfall Bump django-coverage-plugin from 2.0.2 to 2.0.3 #15.
  • Test coverage is solid. Backend slug resolver, status tool, and service helper are well-exercised. Frontend unit tests cover utilities, route parsing, and the routing write-discipline regression guard.

Issues

1. Detail test wrapper mock will drain on retries (test fragility risk)

File: frontend/tests/ResearchReportDetailTestWrapper.tsx

The mock array provides exactly one response. The component uses notifyOnNetworkStatusChange: true and calls refetch() inside onComplete, which means a second network trip will find an empty mock bucket and log a "No more mocked responses" error.

Since all tests in ResearchReportDetail.ct.tsx use terminal states (where polling is disabled and useResearchCompletionNotification is also disabled via !isTerminal), this is probably fine in practice. But it is a fragility: if test timing changes or a future test exercises a non-terminal state, the bucket drains and test behaviour becomes unpredictable.

Compare to CorpusResearchReportCardsTestWrapper.tsx which correctly uses maxUsageCount: Number.POSITIVE_INFINITY. Consider applying the same pattern to the detail mock.

2. Duration test relies on duration_seconds being a computed property

File: opencontractserver/tests/research/test_research_status_tool.py

test_completed_report_shows_duration sets started_at and completed_at directly, then asserts the formatted duration appears in the output. This works only if ResearchReport.duration_seconds is a @property computed from those timestamps at read-time — not a stored field.

If duration_seconds is a stored field set only by finalize(), the test creates a report where duration_seconds is None, and _summarize_recent_reports's if secs is not None guard would make detail = "" — causing assertIn("finished in 2m 5s", result) to fail. Since the PR says tests pass, duration_seconds is presumably a property, but a comment documenting this assumption would prevent a future refactor from silently breaking it.

3. Async error handling discards the exception

Files: frontend/src/views/ResearchReportDetail.tsx, frontend/src/components/widgets/modals/StartResearchModal.tsx

Both catch blocks swallow the underlying error without logging. Network errors, GraphQL parse failures, and unexpected exceptions all disappear silently. Add console.error(e) at minimum so production issues are diagnosable.

4. Snake_case local variable names in CorpusResearchReportCards

File: frontend/src/components/research/CorpusResearchReportCards.tsx

The reactive-var shadow locals opened_corpus, research_search_term, and auth_token use snake_case. The rest of the codebase uses camelCase for these. They should be openedCorpus, researchSearchTerm, authToken to match the convention.

5. CancelResearchReportOutput.obj.status typed as plain string

File: frontend/src/graphql/mutations.ts

The cancel output's obj.status is typed as string while ResearchReportType uses JobStatus | string. Minor inconsistency — the narrower union would catch typos if any caller compares against JobStatus values.

6. Slug index on ResearchReport — worth confirming

File: config/graphql/research_queries.py

The researchReportBySlug resolver runs .filter(slug=slug).first() on every route load. If the slug column lacks a DB index this will be a full-table scan as the table grows. AutoSlugField / SlugField(unique=True) both carry an implicit index, so this is likely already covered — just confirm 0001_initial.py declares the field with unique=True or db_index=True.


Minor nits

  • ResearchReportDetail.tsx is 881 lines (mostly styled components). A .styles.ts companion would match the Corpuses.styles.ts pattern — worth a follow-up issue.
  • RESEARCH_REPORT_PROGRESS is correctly absent from the notification union per the inline comment; add it when the backend emitter lands.

Summary

The implementation is clean, security-conscious, and follows project conventions throughout. Main actionable items: add maxUsageCount: Infinity to the detail test wrapper, add error logging to async catch blocks, fix the snake_case variable names, and confirm the slug column is indexed. None of these block the feature from working correctly, but addressing them before merge keeps the test suite resilient and the codebase consistent.

Originally posted by @claude in #1836 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions