You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
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)
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
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.
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
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.
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/:slugroute, 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
openedResearchReportreactive var, cache policies, and allowlist discipline test are all consistent with how existing entity routes work.researchReportBySlugusesfilter_visible(creator-only), the resolver is@login_required, non-owner and unknown slug both returnnull(same response, prevents enumeration), and the anonymous rejection is pinned with a test.save()and M2Mset()calls intransaction.atomic()closes a real gap — a worker crash between them would have left a COMPLETED report with empty provenance.isTerminalResearchStatustreats unrecognized statuses as terminal so polling never loops forever on an unknown state. The indirect flow of refetch → status update →useEffectstopping polling is correct.MAX_RESEARCH_PROMPT_CHARS,MAX_RESEARCH_TITLE_CHARS,RESEARCH_REPORT_POLL_INTERVAL_MS,RESEARCH_REPORT_UPDATE_PERMISSIONare all inconstants.ts, not inlined (CLAUDE.md rule 4).relayStylePaginationkeyArgs 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.Issues
1. Detail test wrapper mock will drain on retries (test fragility risk)
File:
frontend/tests/ResearchReportDetailTestWrapper.tsxThe mock array provides exactly one response. The component uses
notifyOnNetworkStatusChange: trueand callsrefetch()insideonComplete, 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.tsxuse terminal states (where polling is disabled anduseResearchCompletionNotificationis 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.tsxwhich correctly usesmaxUsageCount: Number.POSITIVE_INFINITY. Consider applying the same pattern to the detail mock.2. Duration test relies on
duration_secondsbeing a computed propertyFile:
opencontractserver/tests/research/test_research_status_tool.pytest_completed_report_shows_durationsetsstarted_atandcompleted_atdirectly, then asserts the formatted duration appears in the output. This works only ifResearchReport.duration_secondsis a@propertycomputed from those timestamps at read-time — not a stored field.If
duration_secondsis a stored field set only byfinalize(), the test creates a report whereduration_secondsisNone, and_summarize_recent_reports'sif secs is not Noneguard would makedetail = ""— causingassertIn("finished in 2m 5s", result)to fail. Since the PR says tests pass,duration_secondsis 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.tsxBoth 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
CorpusResearchReportCardsFile:
frontend/src/components/research/CorpusResearchReportCards.tsxThe reactive-var shadow locals
opened_corpus,research_search_term, andauth_tokenuse snake_case. The rest of the codebase uses camelCase for these. They should beopenedCorpus,researchSearchTerm,authTokento match the convention.5.
CancelResearchReportOutput.obj.statustyped as plainstringFile:
frontend/src/graphql/mutations.tsThe cancel output's
obj.statusis typed asstringwhileResearchReportTypeusesJobStatus | string. Minor inconsistency — the narrower union would catch typos if any caller compares againstJobStatusvalues.6. Slug index on
ResearchReport— worth confirmingFile:
config/graphql/research_queries.pyThe
researchReportBySlugresolver runs.filter(slug=slug).first()on every route load. If theslugcolumn 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 confirm0001_initial.pydeclares the field withunique=Trueordb_index=True.Minor nits
ResearchReportDetail.tsxis 881 lines (mostly styled components). A.styles.tscompanion would match theCorpuses.styles.tspattern — worth a follow-up issue.RESEARCH_REPORT_PROGRESSis 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: Infinityto the detail test wrapper, add error logging to async catch blocks, fix the snake_case variable names, and confirm theslugcolumn 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)