From d0592d63a6b5aed6399fb6e2aa46cd1823d23e53 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Wed, 15 Apr 2026 18:51:43 +0700 Subject: [PATCH] test(frontend): extract resolveSessionDryRun to helper + add coverage - Extract session dry_run resolution logic from useChatSession.ts into resolveSessionDryRun() helper in helpers.ts for testability - Add 6 unit tests covering null/undefined/non-200/false/missing/true branches of resolveSessionDryRun in __tests__/helpers.test.ts - Remove unused isDryRun destructure from CopilotPage.tsx (now only sessionDryRun is used for the session-scoped test mode banner) - Fix patch coverage gap: new sessionDryRun logic is now fully covered --- .../app/(platform)/copilot/CopilotPage.tsx | 3 +- .../copilot/__tests__/helpers.test.ts | 38 ++++++++++++++++++- .../src/app/(platform)/copilot/helpers.ts | 18 +++++++++ .../app/(platform)/copilot/useChatSession.ts | 9 +++-- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx index 6b825ba4c8..88f70c75d8 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx @@ -113,8 +113,7 @@ export function CopilotPage() { // Rate limit reset rateLimitMessage, dismissRateLimit, - // Dry run dev toggle - isDryRun, + // Dry run session state sessionDryRun, } = useCopilotPage(); diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/__tests__/helpers.test.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/__tests__/helpers.test.ts index 712aaaf508..d8eb53d661 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/__tests__/helpers.test.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/__tests__/helpers.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { IMPERSONATION_HEADER_NAME } from "@/lib/constants"; -import { getCopilotAuthHeaders } from "../helpers"; +import { getCopilotAuthHeaders, resolveSessionDryRun } from "../helpers"; vi.mock("@/lib/supabase/actions", () => ({ getWebSocketToken: vi.fn(), @@ -16,6 +16,42 @@ import { getSystemHeaders } from "@/lib/impersonation"; const mockGetWebSocketToken = vi.mocked(getWebSocketToken); const mockGetSystemHeaders = vi.mocked(getSystemHeaders); +describe("resolveSessionDryRun", () => { + it("returns false when queryData is null", () => { + expect(resolveSessionDryRun(null)).toBe(false); + }); + + it("returns false when queryData is undefined", () => { + expect(resolveSessionDryRun(undefined)).toBe(false); + }); + + it("returns false when status is not 200", () => { + expect(resolveSessionDryRun({ status: 404 })).toBe(false); + }); + + it("returns false when status is 200 but metadata.dry_run is false", () => { + expect( + resolveSessionDryRun({ + status: 200, + data: { metadata: { dry_run: false } }, + }), + ).toBe(false); + }); + + it("returns false when status is 200 but metadata is missing", () => { + expect(resolveSessionDryRun({ status: 200, data: {} })).toBe(false); + }); + + it("returns true when status is 200 and metadata.dry_run is true", () => { + expect( + resolveSessionDryRun({ + status: 200, + data: { metadata: { dry_run: true } }, + }), + ).toBe(true); + }); +}); + describe("getCopilotAuthHeaders", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts index 34e2bea51a..b1d87a25d2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts @@ -52,6 +52,24 @@ export function parseSessionIDs(raw: string | null | undefined): Set { } } +/** + * Resolve the actual dry_run value for a session from the raw API response. + * Returns true only when the session response is a 200 with metadata.dry_run === true. + * Returns false for missing/non-200 responses so callers never show a stale + * preference value when the real session state is unknown. + */ +export function resolveSessionDryRun(queryData: unknown): boolean { + if ( + queryData == null || + typeof queryData !== "object" || + !("status" in queryData) || + (queryData as { status: unknown }).status !== 200 + ) + return false; + const d = queryData as { data?: { metadata?: { dry_run?: unknown } } }; + return d.data?.metadata?.dry_run === true; +} + /** * Check whether a refetchSession result indicates the backend still has an * active SSE stream for this session. diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts index 3751548b13..b5a02620c2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts @@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { parseAsString, useQueryState } from "nuqs"; import { useEffect, useMemo, useRef } from "react"; import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages"; +import { resolveSessionDryRun } from "./helpers"; interface UseChatSessionOptions { dryRun?: boolean; @@ -170,10 +171,10 @@ export function useChatSession({ dryRun = false }: UseChatSessionOptions = {}) { // Design intent: the global isDryRun store is only used when creating NEW // sessions. Once a session exists, its dry_run flag is immutable and should // be read from here rather than from the store, which may have changed. - const sessionDryRun = useMemo(() => { - if (sessionQuery.data?.status !== 200) return false; - return sessionQuery.data.data.metadata?.dry_run === true; - }, [sessionQuery.data]); + const sessionDryRun = useMemo( + () => resolveSessionDryRun(sessionQuery.data), + [sessionQuery.data], + ); return { sessionId,