- Implement components for `ReleaseChannelSelector`, including panel and sidebar variants. - Add `ReleaseAssignmentSubjectAutocomplete` for user, subuser, and customer search functionality. - Update i18n phrases for channel selector and subject assignment. - Enhance unit and E2E tests for release channel availability, switching, and runtime configuration. - Modify modules for channel-related state handling and runtime refresh mechanics.
161 lines
5.2 KiB
JavaScript
161 lines
5.2 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
__resetReleaseTimelineForTests,
|
|
__setReleaseTimelineTransportForTests,
|
|
buildReleaseTimelineContext,
|
|
configureReleaseRuntime,
|
|
flushReleaseTimelineEvents,
|
|
getRecentFrontendFailureEvents,
|
|
recordReleaseTimelineEvent,
|
|
redactReleasePayload,
|
|
releaseRuntimeState,
|
|
} 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({
|
|
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.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("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);
|
|
});
|
|
});
|