from pathlib import Path ROOT = Path(__file__).resolve().parents[2] FRONTEND = ROOT / "studio" / "frontend" / "src" def source(path: str) -> str: return (FRONTEND / path).read_text(encoding = "utf-8") def test_research_api_is_isolated_and_cursor_based() -> None: api = source("features/chat/api/research-api.ts") store = source("features/chat/stores/research-run-store.ts") assert 'authFetch("/api/chat/research-runs"' in api assert "authFetch(`/api/chat/research-runs/active?${query}`)" in api assert "const { runs, hasRun }" in api assert "runs.at(-1) ?? null" in api assert "getResearchThreadState" in api assert "/events?after=${Math.max(0, after)}" in api assert 'headers: { accept: "text/event-stream" }' in api assert "export async function* followResearchRun" in api assert "Math.min(8_000, 500 * 2 ** (failures - 1))" in api assert "for await (const event of streamResearchEvents" in api assert 'source: "event"' in api assert "fresh.report !== currentRun.report" in api assert "await waitForReconnect(" in api assert "while (!(run || signal?.aborted))" in api assert "isPermanentResearchError(error)" in api assert 'yield { run, source: "snapshot" }' in api assert "event.id <= pending.event.id" in store for action in ("cancel", "retry"): assert f'mutate(id, "{action}")' in api assert 'mutate(id, "approve", { planRevision, planHash })' in api assert "JSON.stringify({ plan, expectedRevision })" in api def test_research_mode_is_single_chat_and_detaches_without_cancel() -> None: adapter = source("features/chat/api/chat-adapter.ts") thread = source("components/assistant-ui/thread.tsx") assert "runtime.deepResearchEnabled" in adapter assert "!options.pairId" in adapter assert 'options.modelType === "base"' in adapter assert "cancelResearchRun(run.id)" not in adapter assert "createResearchRun" in adapter assert "await saveStoredChatMessage({" in adapter assert "unstable_assistantMessageId," in adapter assert "if (!unstable_assistantMessageId)" in adapter assert "assistantMessageId: unstable_assistantMessageId" in adapter assert "followResearchRun(createdRun.id" in adapter assert "inferenceRequest" in adapter assert "Number.isFinite(params.temperature)" in adapter assert "Number.isFinite(params.topP)" in adapter assert "Number.isFinite(params.maxTokens)" in adapter assert "Math.min(8192, Math.floor(params.maxTokens))" in adapter assert 'update.event?.event === "report.updated"' in adapter assert 'update.event?.event === "reasoning.updated"' in adapter assert "The activity store coalesces these high-frequency events" in adapter assert '{ type: "text" as const, text: report }' in adapter assert "if (abortSignal.aborted) return" in adapter assert "await autoLoadSmallestModel()" in adapter assert "signal: researchFollowController.signal" in adapter assert "beginExternalResearchFollow(" in adapter assert "ragScope" in adapter assert "const projectRagEnabled = researchProjectId" in adapter assert "runtime.ragEnabled || projectRagEnabled" in adapter submit = thread.split("const handleSubmit = useCallback", 1)[1].split("const stopQueue", 1)[0] assert "if (isResearchActive)" in submit assert "event.preventDefault()" in submit assert "runtime.ragEnabled\n ? { thread_id: resolvedThreadId }" in adapter message_error = thread.split("const MessageError: FC = () =>", 1)[1].split( "const GeneratingIndicator", 1 )[0] assert "useThreadResearchActive()" in message_error assert "!researchRunId && !researchActive" in message_error create_block = adapter.split("createdRun = await createResearchRun({", 1)[1].split("});", 1)[0] assert "modelId:" not in create_block assert "prompt," not in create_block assert "instructions: researchInstructions" in create_block assert "resolveChatInstructions" in adapter def test_research_metadata_and_server_merge_are_persisted() -> None: adapter = source("features/chat/api/chat-adapter.ts") runtime = source("features/chat/runtime-provider.tsx") assert "researchRunId: run.id" in adapter assert "serverManaged: true" in adapter assert "getResearchThreadState(remoteId)" in runtime assert "preserveServerManaged" in runtime assert "sameResearchRun" in runtime assert "existingRevision > incomingRevision" in runtime assert "const userMessage = [...messages]" in runtime assert '.find((message) => message.role === "user")' in runtime assert "pendingRunStartReadyByMessageId.get(userMessage.id)" in runtime def test_research_presentation_is_integrated() -> None: thread = source("components/assistant-ui/thread.tsx") page = source("features/chat/chat-page.tsx") chat_index = source("features/chat/index.ts") store = source("features/chat/stores/chat-runtime-store.ts") activity = source("features/chat/components/research-activity-panel.tsx") message = source("features/chat/components/research-message.tsx") markdown_preview = source("components/markdown/markdown-preview.tsx") safe_markdown_url = source("lib/safe-markdown-url.ts") coordinator = source("features/chat/stores/research-run-store.ts") assert "DeepResearchComposerButton" in thread assert "Deep research" in thread research_gate = thread.split("const researchDisabled =", 1)[1].split(";", 1)[0] assert "!modelLoaded" not in research_gate assert "" in thread assert "if (researchRunId) return null" in thread assert "!researchRunId &&" in thread assert "if (researchRunId || ownsResearchMessage)" in thread assert "parentId === messageId && Boolean(getResearchRunId(message.metadata))" in thread user_actions = thread.split("const UserActionBar: FC = () =>", 1)[1].split( "const EditComposer:", 1 )[0] assert "!ownsResearchMessage &&" in user_actions assert "", 1)[1].split( "const GeneratingIndicator:", 1 )[0] assert "!researchRunId &&" in message_error assert "ResearchActivityPanel" in page assert "ResearchActivitySheet" in page assert "ResearchActivityPanel" in chat_index assert 'role="log"' in activity assert "Review the research plan" in activity assert "Start research" in activity assert "cancelResearchRun" in thread assert "Stop research" not in activity assert "retryResearchRun" in activity assert "Deep research completed" in message assert "", 1)[1].split( "setActiveThreadId:", 1 )[0] assert "saveBool(CHAT_DEEP_RESEARCH_ENABLED_KEY, false)" in checkpoint_update assert "const permissionMode = loadPermissionMode();" in store assert "permissionMode," in store def test_research_plan_and_status_contract() -> None: types = source("features/chat/types/research.ts") assert '| "queued"' in types assert '| "cancelling"' in types assert "title: string;" in types assert "query: string;" in types assert "position: number;" in types assert "createdAt: number;" in types assert "planRevision: number;" in types assert "planHash: string | null;" in types def test_research_website_limits_are_configurable_and_sent_with_each_run() -> None: component = source("features/chat/components/deep-research-composer-button.tsx") thread = source("components/assistant-ui/thread.tsx") store = source("features/chat/stores/chat-runtime-store.ts") adapter = source("features/chat/api/chat-adapter.ts") assert 'label="Allow only"' in component assert 'label="Always block"' in component assert "their subdomains" in component assert ">Websites" in component assert "DeepResearchWebsiteAccessDialog" in thread assert "researchWebsitePolicy" in store assert "CHAT_DEEP_RESEARCH_WEBSITE_POLICY_KEY" in store assert "websitePolicy:" in adapter assert "allowedDomains" in adapter and "blockedDomains" in adapter def test_research_is_one_shot_per_thread_without_disabling_normal_chat() -> None: adapter = source("features/chat/api/chat-adapter.ts") runtime = source("features/chat/runtime-provider.tsx") thread = source("components/assistant-ui/thread.tsx") coordinator = source("features/chat/stores/research-run-store.ts") assert "claimedThreadIds" in coordinator assert "setThreadClaimed" in coordinator assert "researchThreadState.hasRun" in runtime assert "threadAlreadyResearched" in adapter assert "runtime.setDeepResearchEnabled(false)" in adapter assert "effectiveDeepResearchEnabled" in thread assert "researchAvailable={!researchUsed}" in thread assert "{researchAvailable ? (" in thread assert "setToolsEnabled" in thread assert "Web search" in thread def test_settled_terminal_research_never_stays_disconnected() -> None: coordinator = source("features/chat/stores/research-run-store.ts") activity = source("features/chat/components/research-activity-panel.tsx") assert "function isSettledResearchRun" in coordinator assert 'connection: settled ? "idle"' in coordinator assert "error: settled ? null" in coordinator assert 'state.setFollowing(runId, false, "idle")' in coordinator assert "!isSettledResearchRun(run, session.lastAppliedSeq)" in activity def test_research_stop_is_prompt_only_and_deduplicated() -> None: adapter = source("features/chat/api/chat-adapter.ts") thread = source("components/assistant-ui/thread.tsx") activity = source("features/chat/components/research-activity-panel.tsx") assert "stoppingResearchRunIdRef" in thread assert 'activeResearchRun.status === "cancelling"' in thread assert 'aria-label={researchStopping ? "Stopping research"' in thread assert "cancelResearchRun" not in activity assert "Stop research" not in activity assert "abortSignal.reason as { detach?: boolean }" in adapter assert "await cancelResearchRun(createdRun.id)" in adapter