371 lines
13 KiB
JavaScript
371 lines
13 KiB
JavaScript
// @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);
|
|
});
|
|
});
|