// @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { __resetReleaseTimelineForTests, __setReleaseTimelineTransportForTests, buildReleaseSessionSummary, buildCurrentReleaseHeaders, buildReleaseTimelineContext, configureReleaseRuntime, flushReleaseTimelineEvents, getReleaseRuntimeApiBaseUrl, getRecentFrontendFailureEvents, recordReleaseTimelineEvent, redactReleasePayload, releaseRuntimeState, resolveReleaseApiUrl, } from "@/services/releaseTimeline.js"; describe("release timeline runtime", () => { beforeEach(() => { vi.useFakeTimers(); localStorage.clear(); __resetReleaseTimelineForTests(); }); afterEach(() => { __resetReleaseTimelineForTests(); localStorage.clear(); vi.useRealTimers(); vi.restoreAllMocks(); }); it("configures release runtime from the JSON session contract", () => { configureReleaseRuntime({ source: "deployment", trace_id: "trace-123", channel: { slug: "canary", name: "Canary" }, available_channels: [{ channel: { slug: "stable", name: "Stable", default_channel: true } }], versions: { frontend: { version_label: "abc123" }, api: null, bundle_id: 31 }, capture_policy: { enabled: true, capture_level: "full_redacted", retention_days: 7 }, }); expect(releaseRuntimeState.source).toBe("deployment"); expect(releaseRuntimeState.requestedSource).toBe("deployment"); expect(releaseRuntimeState.channel.slug).toBe("canary"); expect(releaseRuntimeState.availableChannels).toHaveLength(1); expect(releaseRuntimeState.traceId).toBe("trace-123"); expect(releaseRuntimeState.versions.frontend.version_label).toBe("abc123"); expect(releaseRuntimeState.versions.bundle_id).toBe(31); }); it("preserves the bootstrap source when later session runtime omits it", () => { configureReleaseRuntime({ source: "local", requested_source: "auto", api_base_url: "/api", }); configureReleaseRuntime({ trace_id: "trace-session", channel: { slug: "stable", name: "Stable", default_channel: true }, availability: { configured: true, missing: [], status: "ready" }, }); expect(releaseRuntimeState.source).toBe("local"); expect(releaseRuntimeState.requestedSource).toBe("auto"); expect(releaseRuntimeState.traceId).toBe("trace-session"); }); it("builds display rows for the active release bundle and connected services", () => { configureReleaseRuntime({ generated_at: "2026-05-19T09:30:00.000Z", trace_id: "trace-session-release", channel: { slug: "canary", name: "Canary", default_channel: false }, versions: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee0000001111222233334444555566667777", repository: "truckwash/front-end-vue", branch: "release/canary", status: "deployed", }, api: { version_label: "api-canary", commit_sha: "feedface00001111222233334444555566667777", repository: "truckwash/backend-php", branch: "release/canary", status: "deployed", }, service_set: { id: 53, name: "Canary isolated stack", stack: { frontend: { id: 51, label: "canary-frontend", coolify_service_uuid: "frontend-service-uuid" }, api: { id: 52, label: "canary-api", coolify_service_uuid: "api-service-uuid" }, database: { id: 54, resource_name: "canary-db", resource_uuid: "database-resource-uuid", deployment_status: "ready", }, redis: { id: 55, resource_name: "canary-redis", resource_uuid: "redis-resource-uuid", deployment_status: "ready", }, minio: { id: 56, resource_name: "canary-minio", resource_uuid: "minio-resource-uuid", availability_state: "healthy", }, }, }, bundle_id: 31, bundle: { id: 31, version_label: "canary-bundle", status: "deployed" }, }, urls: { frontend_base_url: "https://api-v2.truckwash.io/canary/frontend", api_base_url: "https://api-v2.truckwash.io/canary/api", }, availability: { configured: true, missing: [], status: "ready" }, }); const summary = buildReleaseSessionSummary(); expect(releaseRuntimeState.versions.service_set.name).toBe("Canary isolated stack"); expect(summary.channelLabel).toBe("Canary"); expect(summary.traceId).toBe("trace-session-release"); expect(summary.bundleLabel).toBe("#31 canary-bundle"); expect(summary.serviceSetLabel).toBe("Canary isolated stack"); expect(summary.appRows.find((row) => row.key === "frontend")).toMatchObject({ primaryText: "frontend-canary", url: "https://api-v2.truckwash.io/canary/frontend", tone: "ok", }); expect(summary.appRows.find((row) => row.key === "api")?.secondaryText).toContain("feedface0000"); expect(summary.serviceRows.find((row) => row.key === "database")).toMatchObject({ primaryText: "canary-db #54", tone: "ok", }); expect(summary.serviceRows.find((row) => row.key === "redis")?.secondaryText).toContain("redis-resour"); expect(summary.serviceRows.find((row) => row.key === "minio")?.status).toBe("healthy"); }); it("marks stable sessions without release bundle data as shared runtime", () => { const summary = buildReleaseSessionSummary({ trace_id: "trace-stable", channel: { slug: "stable", name: "Stable", default_channel: true }, versions: { frontend: null, api: null, bundle_id: null, bundle: null }, availability: { configured: true, missing: [], status: "ready" }, }); expect(summary.bundleLabel).toBe("Default/shared runtime"); expect(summary.serviceSetLabel).toBe("Default/shared runtime"); expect(summary.appRows.every((row) => row.primaryText === "Default/shared runtime")).toBe(true); expect(summary.appRows.every((row) => row.tone === "ok")).toBe(true); expect(summary.serviceRows.every((row) => row.primaryText === "Default/shared runtime")).toBe(true); expect(summary.serviceRows.every((row) => row.tone === "ok")).toBe(true); }); it("surfaces non-default release runtime missing values as warnings", () => { const summary = buildReleaseSessionSummary({ trace_id: "trace-internal", channel: { slug: "internal", name: "Internal", default_channel: false }, versions: { frontend: null, api: { version_label: "api-internal" }, bundle_id: null, bundle: null, }, availability: { configured: false, missing: ["release_bundle", "frontend_version", "api_base_url"], status: "unconfigured", }, }); expect(summary.missingLabels).toEqual(["Frontend version", "API URL"]); expect(summary.appRows.find((row) => row.key === "frontend")).toMatchObject({ tone: "warning", missingLabel: "Frontend version", }); expect(summary.appRows.find((row) => row.key === "api")).toMatchObject({ tone: "warning", missingLabel: "API URL", }); expect(summary.serviceRows.find((row) => row.key === "database")).toMatchObject({ tone: "ok", primaryText: "Default/shared runtime", }); }); it("uses release runtime frontend and API URLs when provided", () => { configureReleaseRuntime({ trace_id: "trace-urls", channel: { slug: "canary", name: "Canary" }, versions: { frontend: { version_label: "frontend-canary" }, api: { version_label: "api-canary" }, bundle_id: 31, }, urls: { frontend_base_url: "https://api-v2.truckwash.io/canary/frontend/", api_base_url: "https://api-v2.truckwash.io/canary/api/", }, }); expect(releaseRuntimeState.frontendBaseUrl).toBe("https://api-v2.truckwash.io/canary/frontend"); expect(releaseRuntimeState.apiBaseUrl).toBe("https://api-v2.truckwash.io/canary/api"); expect(getReleaseRuntimeApiBaseUrl()).toBe("https://api-v2.truckwash.io/canary/api"); expect(resolveReleaseApiUrl("/orders")).toBe("https://api-v2.truckwash.io/canary/api/orders"); }); it("keeps same-origin release API URLs visible in the session summary", () => { configureReleaseRuntime({ trace_id: "trace-local-api", channel: { slug: "canary", name: "Canary" }, versions: { frontend: { version_label: "frontend-canary" }, api: { version_label: "api-canary" }, bundle_id: 31, }, urls: { api_base_url: "/api/", }, }); const summary = buildReleaseSessionSummary(); expect(releaseRuntimeState.apiBaseUrl).toBe("/api"); expect(getReleaseRuntimeApiBaseUrl()).toBe("/api"); expect(resolveReleaseApiUrl("/orders")).toBe("/api/orders"); expect(summary.appRows.find((row) => row.key === "api")?.url).toBe("/api"); }); it("builds release headers from the active runtime state", () => { configureReleaseRuntime({ trace_id: "trace-headers", channel: { slug: "internal", name: "Intern" }, versions: { frontend: { version_label: "frontend-internal", commit_sha: "130cc2fc106a" }, api: { version_label: "api-internal" }, bundle_id: 42, }, }); expect(buildCurrentReleaseHeaders()).toMatchObject({ "X-Release-Trace": "trace-headers", "X-Release-Channel": "internal", "X-Frontend-Version": "130cc2fc106a", }); }); it("redacts sensitive payload keys recursively", () => { expect( redactReleasePayload({ Authorization: "Bearer token", nested: { api_key: "secret", safe: "visible", }, }) ).toEqual({ Authorization: "[redacted]", nested: { api_key: "[redacted]", safe: "visible", }, }); }); it("always sends failure metadata and keeps non-failure events gated by capture policy", async () => { const sentBodies = []; __setReleaseTimelineTransportForTests(async (body) => { sentBodies.push(body); return { accepted: body.events.length }; }); expect(recordReleaseTimelineEvent("route_change", { to: "/superuser" })).toBe(false); expect( recordReleaseTimelineEvent( "request_failed", { request: { headers: { Authorization: "Bearer token" } }, response: { status: 500 }, }, { severity: "error", moduleKey: "requestqueue" } ) ).toBe(true); await flushReleaseTimelineEvents(); expect(sentBodies).toHaveLength(1); expect(sentBodies[0].events[0]).toMatchObject({ type: "request_failed", severity: "error", module_key: "requestqueue", }); expect(sentBodies[0].events[0].payload.request.headers.Authorization).toBe("[redacted]"); }); it("keeps a local redacted frontend failure buffer for error reports", () => { recordReleaseTimelineEvent( "vue_component_error", { message: "Render failed", token: "secret", component: "BrokenWidget", }, { severity: "error", moduleKey: "frontend" } ); recordReleaseTimelineEvent( "request_failed", { message: "Handled by request queue" }, { severity: "error", moduleKey: "requestqueue" } ); const events = getRecentFrontendFailureEvents(); expect(events).toHaveLength(1); expect(events[0]).toMatchObject({ type: "vue_component_error", module_key: "frontend", }); expect(events[0].payload.token).toBe("[redacted]"); }); it("sends non-failure timeline events when replay capture is enabled", async () => { const sentBodies = []; __setReleaseTimelineTransportForTests(async (body) => { sentBodies.push(body); return { accepted: body.events.length }; }); configureReleaseRuntime({ trace_id: "trace-enabled", channel: { slug: "beta" }, capture_policy: { enabled: true, capture_level: "full_redacted" }, }); expect(recordReleaseTimelineEvent("route_change", { to: "/dashboard" })).toBe(true); await flushReleaseTimelineEvents(); expect(sentBodies).toHaveLength(1); expect(sentBodies[0].context.channel_slug).toBe("beta"); expect(sentBodies[0].context.device.type).toBe("desktop"); expect(sentBodies[0].context.frontend.version_label).toBeTruthy(); expect(sentBodies[0].events[0].type).toBe("route_change"); }); it("builds device and release context for replay sessions", () => { configureReleaseRuntime({ trace_id: "trace-context", channel: { slug: "canary" }, versions: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" }, api: { version_label: "api-canary", commit_sha: "def456" }, }, capture_policy: { enabled: true, capture_level: "full_redacted" }, }); const context = buildReleaseTimelineContext(); expect(context.trace_id).toBe("trace-context"); expect(context.channel_slug).toBe("canary"); expect(context.device.type).toBe("desktop"); expect(context.frontend.version_label).toBe("frontend-canary"); expect(context.api.version_label).toBe("api-canary"); expect(context.viewport.width).toBeGreaterThan(0); }); });