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
This commit is contained in:
Zamil Majdy
2026-04-15 18:51:43 +07:00
parent fa064aa4f1
commit d0592d63a6
4 changed files with 61 additions and 7 deletions

View File

@@ -113,8 +113,7 @@ export function CopilotPage() {
// Rate limit reset
rateLimitMessage,
dismissRateLimit,
// Dry run dev toggle
isDryRun,
// Dry run session state
sessionDryRun,
} = useCopilotPage();

View File

@@ -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();

View File

@@ -52,6 +52,24 @@ export function parseSessionIDs(raw: string | null | undefined): Set<string> {
}
}
/**
* 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.

View File

@@ -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,