// @vitest-environment jsdom import { afterEach, describe, expect, it } from "vitest"; import { acknowledgeReleaseChannelSwitch, buildReleaseFrontendRedirectUrl, getReleaseChannelSwitchedStatus, getReleaseChannelOptions, getReleaseChannelUnavailableStatus, hasSelectableReleaseChannels, ignoreUnavailableReleaseChannel, isReleaseChannelApiAvailabilityError, markReleaseChannelApiUnavailable, reconcileSelectedReleaseChannel, RELEASE_CHANNEL_IGNORE_MS, RELEASE_CHANNEL_IGNORE_STORAGE_KEY, RELEASE_CHANNEL_SELECTION_STORAGE_KEY, RELEASE_CHANNEL_SWITCH_NOTICE_STORAGE_KEY, releaseChannelRuntimeRequestParams, selectReleaseChannel, switchSelectedReleaseChannel, __resetReleaseChannelAvailabilityForTests, } from "@/services/releaseChannelAvailability.js"; import { __resetReleaseTimelineForTests, configureReleaseRuntime, releaseRuntimeState, } from "@/services/releaseTimeline.js"; const canaryRuntime = { channel: { id: 2, slug: "canary", name: "Canary", description: "Early validation channel", default_channel: false, }, versions: { frontend: null, api: null, bundle_id: null, }, availability: { configured: false, missing: ["release_bundle", "frontend_version", "api_version"], status: "unconfigured", }, }; const configuredCanaryRuntime = { ...canaryRuntime, versions: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" }, api: { version_label: "api-canary", commit_sha: "feedface" }, }, availability: { configured: true, missing: [], status: "ready", }, }; describe("release channel availability", () => { afterEach(() => { __resetReleaseChannelAvailabilityForTests(); __resetReleaseTimelineForTests(); }); it("blocks a non-default assigned channel when app targets are missing", () => { const status = getReleaseChannelUnavailableStatus(canaryRuntime, 1_000, 0); expect(status.shouldBlock).toBe(true); expect(status.channelSlug).toBe("canary"); expect(status.missing).toEqual(["frontend_version", "api_version"]); }); it("does not block the stable default channel even when runtime targets are absent", () => { const status = getReleaseChannelUnavailableStatus({ channel: { slug: "stable", name: "Stable", default_channel: true, }, availability: { configured: false, missing: ["release_bundle"], }, }); expect(status.unavailable).toBe(false); expect(status.shouldBlock).toBe(false); }); it("does not block branch-based channels only because a legacy bundle is missing", () => { const status = getReleaseChannelUnavailableStatus({ channel: { slug: "canary", name: "Canary", default_channel: false, }, availability: { configured: false, missing: ["release_bundle"], status: "unconfigured", }, }); expect(status.configured).toBe(true); expect(status.shouldBlock).toBe(false); expect(status.missing).toEqual([]); expect(status.status).toBe("ready"); }); it("exposes selectable runtime channels when a non-default assignment is available", () => { const runtime = { channel: configuredCanaryRuntime.channel, versions: configuredCanaryRuntime.versions, availability: configuredCanaryRuntime.availability, availableChannels: [ { channel: { slug: "stable", name: "Stable", default_channel: true, }, availability: { configured: true, missing: [], status: "ready", }, }, { channel: configuredCanaryRuntime.channel, versions: configuredCanaryRuntime.versions, availability: configuredCanaryRuntime.availability, }, ], }; const options = getReleaseChannelOptions(runtime); expect(options.map((option) => option.channelSlug)).toEqual(["stable", "canary"]); expect(hasSelectableReleaseChannels(runtime)).toBe(true); }); it("hides the selector when only the default stable channel is available", () => { expect( hasSelectableReleaseChannels({ channel: { slug: "stable", name: "Stable", default_channel: true, }, availableChannels: [ { channel: { slug: "stable", name: "Stable", default_channel: true, }, }, ], }) ).toBe(false); }); it("stores the selected release channel for runtime refresh requests", () => { const selectedSlug = selectReleaseChannel({ slug: "canary" }); expect(selectedSlug).toBe("canary"); expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBe("canary"); expect(releaseChannelRuntimeRequestParams()).toEqual({ release_channel: "canary" }); }); it("rolls back a selected channel when the runtime refresh fails", async () => { let refreshCalls = 0; await expect( switchSelectedReleaseChannel({ slug: "canary" }, async () => { refreshCalls += 1; if (refreshCalls > 1) { return { channel: { slug: "stable", name: "Stable", default_channel: true } }; } throw new Error("runtime unavailable"); }) ).rejects.toThrow("runtime unavailable"); expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBeNull(); expect(releaseChannelRuntimeRequestParams()).toEqual({}); expect(refreshCalls).toBe(2); }); it("rejects and rolls back when the refreshed runtime confirms a different channel", async () => { configureReleaseRuntime({ channel: { slug: "stable", name: "Stable", default_channel: true }, availableChannels: [ { channel: { slug: "stable", name: "Stable", default_channel: true } }, { channel: configuredCanaryRuntime.channel }, ], }); await expect( switchSelectedReleaseChannel({ slug: "canary" }, async () => ({ channel: { slug: "stable", name: "Stable", default_channel: true }, })) ).rejects.toThrow("instead of canary"); expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBeNull(); }); it("keeps a selected channel only after the refreshed runtime confirms it", async () => { const selectedSlug = await switchSelectedReleaseChannel({ slug: "canary" }, async () => configuredCanaryRuntime); expect(selectedSlug).toBe("canary"); expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBe("canary"); }); it("clears a stored selection when the runtime no longer offers that channel", () => { selectReleaseChannel("canary"); const selectedSlug = reconcileSelectedReleaseChannel({ availableChannels: [ { channel: { slug: "stable", name: "Stable", default_channel: true, }, }, ], }); expect(selectedSlug).toBe(""); expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBeNull(); }); it("does not block legacy runtime payloads that omit availability and target URLs", () => { const runtime = { channel: { slug: "canary", name: "Canary", default_channel: false, }, }; const status = getReleaseChannelUnavailableStatus(runtime); const switchedStatus = getReleaseChannelSwitchedStatus(runtime, "user:42", {}); expect(status.configured).toBe(true); expect(status.shouldBlock).toBe(false); expect(switchedStatus.shouldShow).toBe(false); }); it("supports a five-minute channel-specific ignore window", () => { const ignoredUntil = ignoreUnavailableReleaseChannel(canaryRuntime.channel); const stored = JSON.parse(window.localStorage.getItem(RELEASE_CHANNEL_IGNORE_STORAGE_KEY)); const status = getReleaseChannelUnavailableStatus(canaryRuntime, Date.now(), ignoredUntil); expect(ignoredUntil).toBeGreaterThan(Date.now() + RELEASE_CHANNEL_IGNORE_MS - 2_000); expect(stored.canary).toBe(ignoredUntil); expect(status.ignored).toBe(true); expect(status.shouldBlock).toBe(false); }); it("shows a one-time switched-channel notice for configured non-default channels", () => { const status = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "user:42", {}); expect(status.shouldShow).toBe(true); expect(status.noticeKey).toBe("user:42:canary"); }); it("stores switched-channel acknowledgement per account and channel", () => { const noticeKey = acknowledgeReleaseChannelSwitch(configuredCanaryRuntime.channel, "user:42"); const stored = JSON.parse(window.localStorage.getItem(RELEASE_CHANNEL_SWITCH_NOTICE_STORAGE_KEY)); const status = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "user:42", stored); const otherUserStatus = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "user:99", stored); expect(noticeKey).toBe("user:42:canary"); expect(stored["user:42:canary"]).toEqual(expect.any(String)); expect(status.shouldShow).toBe(false); expect(otherUserStatus.shouldShow).toBe(true); }); it("does not show the switched-channel notice for default channels or unknown accounts", () => { const defaultStatus = getReleaseChannelSwitchedStatus( { ...configuredCanaryRuntime, channel: { ...configuredCanaryRuntime.channel, default_channel: true, }, }, "user:42", {} ); const anonymousStatus = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "", {}); expect(defaultStatus.shouldShow).toBe(false); expect(anonymousStatus.shouldShow).toBe(false); }); it("does not build a channel-specific frontend redirect", () => { const redirectUrl = buildReleaseFrontendRedirectUrl( { channel: { slug: "beta", default_channel: false }, versions: { frontend: { version_label: "frontend-beta" }, api: { version_label: "api-beta" }, bundle_id: 52, }, }, { origin: "https://app.example.test", pathname: "/user/bookings", search: "?page=2", hash: "#next", } ); expect(redirectUrl).toBeNull(); }); it("classifies selected channel auth session 404s as release API availability failures", () => { configureReleaseRuntime({ trace_id: "trace-channel-api", channel: { slug: "internal", name: "Intern", default_channel: false }, versions: { frontend: { version_label: "frontend-internal" }, api: { version_label: "api-internal" }, bundle_id: 42, }, api_base_url: "https://api-v2.truckwash.io/internal/api", frontend_base_url: "https://api-v2.truckwash.io/internal/frontend", }); const error = { response: { status: 404 }, config: { url: "https://api-v2.truckwash.io/internal/api/auth/session" }, }; expect(isReleaseChannelApiAvailabilityError(error)).toBe(true); const unavailableRuntime = markReleaseChannelApiUnavailable(); expect(unavailableRuntime.availability).toMatchObject({ configured: false, status: "unconfigured", explicit: true, }); expect(unavailableRuntime.availability.missing).toContain("api_base_url"); expect(releaseRuntimeState.availability.configured).toBe(false); }); });