From ec95d18245f24e18a28fb7bc6ec423b1858d2a52 Mon Sep 17 00:00:00 2001 From: ajnart Date: Tue, 1 Sep 2026 01:03:27 +0200 Subject: [PATCH] test: add release v2 browser QA campaign --- .gitignore | 1 + apps/nextjs/next.config.ts | 11 + .../src/components/layout/analytics.spec.tsx | 77 + .../src/components/layout/analytics.tsx | 35 +- package.json | 6 + packages/translation/src/middleware.spec.ts | 94 ++ packages/translation/src/middleware.ts | 28 +- packages/widgets/src/weather/compact.tsx | 7 +- .../widgets/src/weather/component.spec.ts | 12 + packages/widgets/src/weather/component.tsx | 10 +- packages/widgets/src/weather/layout.ts | 16 +- qa/release-v2/README.md | 102 ++ qa/release-v2/coverage-manifest.json | 922 ++++++++++++ qa/release-v2/ledger.json | 420 ++++++ qa/release-v2/master-report.md | 835 +++++++++++ qa/release-v2/reports/board-01/report.md | 165 +++ qa/release-v2/reports/board-02/report.md | 175 +++ qa/release-v2/reports/board-03/report.md | 143 ++ qa/release-v2/reports/board-04/report.md | 150 ++ qa/release-v2/reports/board-05/report.md | 171 +++ qa/release-v2/reports/board-06/report.md | 143 ++ qa/release-v2/reports/board-07/report.md | 152 ++ qa/release-v2/reports/board-08/report.md | 151 ++ qa/release-v2/reports/board-09/report.md | 165 +++ qa/release-v2/reports/core-v2-01/report.md | 145 ++ qa/release-v2/reports/core-v2-02/report.md | 145 ++ qa/release-v2/reports/core-v2-03/report.md | 145 ++ qa/release-v2/reports/core-v2-04/report.md | 147 ++ qa/release-v2/reports/core-v2-05/report.md | 145 ++ qa/release-v2/reports/core-v2-06/report.md | 140 ++ qa/release-v2/reports/core-v2-07/report.md | 147 ++ qa/release-v2/reports/core-v2-08/report.md | 145 ++ .../reports/performance-01/report.md | 149 ++ .../reports/performance-02/report.md | 150 ++ .../reports/performance-03/report.md | 167 +++ .../reports/performance-04/report.md | 150 ++ qa/release-v2/reports/preflight-01/report.md | 284 ++++ qa/release-v2/reports/preflight-02/report.md | 280 ++++ qa/release-v2/reports/preflight-03/report.md | 190 +++ .../repro-browser-zoom-capability/report.md | 157 ++ .../repro-preflight-02-readonly/report.md | 75 + .../reports/repro-weather-mobile/report.md | 65 + .../reports/whole-product-01/report.md | 149 ++ .../reports/whole-product-02/report.md | 145 ++ .../reports/whole-product-03/report.md | 158 ++ .../reports/whole-product-04/report.md | 147 ++ .../reports/whole-product-05/report.md | 147 ++ .../reports/whole-product-06/report.md | 153 ++ .../reports/whole-product-07/report.md | 153 ++ .../reports/whole-product-08/report.md | 153 ++ .../reports/whole-product-09/report.md | 147 ++ qa/release-v2/reports/widgets-01/report.md | 505 +++++++ qa/release-v2/reports/widgets-02/report.md | 470 ++++++ qa/release-v2/reports/widgets-03/report.md | 478 ++++++ qa/release-v2/reports/widgets-04/report.md | 487 ++++++ qa/release-v2/reports/widgets-05/report.md | 471 ++++++ qa/release-v2/reports/widgets-06/report.md | 475 ++++++ qa/release-v2/reports/widgets-07/report.md | 471 ++++++ qa/release-v2/reports/widgets-08/report.md | 494 +++++++ qa/release-v2/reports/widgets-09/report.md | 485 ++++++ qa/release-v2/reports/widgets-10/report.md | 467 ++++++ qa/release-v2/reports/widgets-11/report.md | 469 ++++++ qa/release-v2/reports/widgets-12/report.md | 406 +++++ scripts/release-v2-qa/fixture-server.mts | 349 +++++ scripts/release-v2-qa/geometry.mts | 152 ++ scripts/release-v2-qa/geometry.spec.ts | 119 ++ scripts/release-v2-qa/provenance.mts | 76 + scripts/release-v2-qa/provenance.spec.ts | 133 ++ .../release-v2-qa/record-blocked-report.mts | 338 +++++ scripts/release-v2-qa/report-integrity.mts | 663 +++++++++ .../release-v2-qa/report-integrity.spec.ts | 281 ++++ .../release-v2-qa/report-path-integrity.mts | 74 + .../report-path-integrity.spec.ts | 120 ++ scripts/release-v2-qa/report.mts | 630 ++++++++ scripts/release-v2-qa/runtime.mts | 739 ++++++++++ scripts/release-v2-qa/runtime.spec.ts | 81 + scripts/release-v2-qa/safety.mts | 349 +++++ scripts/release-v2-qa/safety.spec.ts | 233 +++ scripts/release-v2-qa/seed.mts | 1300 +++++++++++++++++ scripts/release-v2-qa/verify.mts | 1115 ++++++++++++++ scripts/release-v2-qa/zoom.mts | 172 +++ scripts/release-v2-qa/zoom.spec.ts | 155 ++ vitest.config.mts | 15 + 83 files changed, 20922 insertions(+), 19 deletions(-) create mode 100644 apps/nextjs/src/components/layout/analytics.spec.tsx create mode 100644 packages/translation/src/middleware.spec.ts create mode 100644 qa/release-v2/README.md create mode 100644 qa/release-v2/coverage-manifest.json create mode 100644 qa/release-v2/ledger.json create mode 100644 qa/release-v2/master-report.md create mode 100644 qa/release-v2/reports/board-01/report.md create mode 100644 qa/release-v2/reports/board-02/report.md create mode 100644 qa/release-v2/reports/board-03/report.md create mode 100644 qa/release-v2/reports/board-04/report.md create mode 100644 qa/release-v2/reports/board-05/report.md create mode 100644 qa/release-v2/reports/board-06/report.md create mode 100644 qa/release-v2/reports/board-07/report.md create mode 100644 qa/release-v2/reports/board-08/report.md create mode 100644 qa/release-v2/reports/board-09/report.md create mode 100644 qa/release-v2/reports/core-v2-01/report.md create mode 100644 qa/release-v2/reports/core-v2-02/report.md create mode 100644 qa/release-v2/reports/core-v2-03/report.md create mode 100644 qa/release-v2/reports/core-v2-04/report.md create mode 100644 qa/release-v2/reports/core-v2-05/report.md create mode 100644 qa/release-v2/reports/core-v2-06/report.md create mode 100644 qa/release-v2/reports/core-v2-07/report.md create mode 100644 qa/release-v2/reports/core-v2-08/report.md create mode 100644 qa/release-v2/reports/performance-01/report.md create mode 100644 qa/release-v2/reports/performance-02/report.md create mode 100644 qa/release-v2/reports/performance-03/report.md create mode 100644 qa/release-v2/reports/performance-04/report.md create mode 100644 qa/release-v2/reports/preflight-01/report.md create mode 100644 qa/release-v2/reports/preflight-02/report.md create mode 100644 qa/release-v2/reports/preflight-03/report.md create mode 100644 qa/release-v2/reports/repro-browser-zoom-capability/report.md create mode 100644 qa/release-v2/reports/repro-preflight-02-readonly/report.md create mode 100644 qa/release-v2/reports/repro-weather-mobile/report.md create mode 100644 qa/release-v2/reports/whole-product-01/report.md create mode 100644 qa/release-v2/reports/whole-product-02/report.md create mode 100644 qa/release-v2/reports/whole-product-03/report.md create mode 100644 qa/release-v2/reports/whole-product-04/report.md create mode 100644 qa/release-v2/reports/whole-product-05/report.md create mode 100644 qa/release-v2/reports/whole-product-06/report.md create mode 100644 qa/release-v2/reports/whole-product-07/report.md create mode 100644 qa/release-v2/reports/whole-product-08/report.md create mode 100644 qa/release-v2/reports/whole-product-09/report.md create mode 100644 qa/release-v2/reports/widgets-01/report.md create mode 100644 qa/release-v2/reports/widgets-02/report.md create mode 100644 qa/release-v2/reports/widgets-03/report.md create mode 100644 qa/release-v2/reports/widgets-04/report.md create mode 100644 qa/release-v2/reports/widgets-05/report.md create mode 100644 qa/release-v2/reports/widgets-06/report.md create mode 100644 qa/release-v2/reports/widgets-07/report.md create mode 100644 qa/release-v2/reports/widgets-08/report.md create mode 100644 qa/release-v2/reports/widgets-09/report.md create mode 100644 qa/release-v2/reports/widgets-10/report.md create mode 100644 qa/release-v2/reports/widgets-11/report.md create mode 100644 qa/release-v2/reports/widgets-12/report.md create mode 100644 scripts/release-v2-qa/fixture-server.mts create mode 100644 scripts/release-v2-qa/geometry.mts create mode 100644 scripts/release-v2-qa/geometry.spec.ts create mode 100644 scripts/release-v2-qa/provenance.mts create mode 100644 scripts/release-v2-qa/provenance.spec.ts create mode 100644 scripts/release-v2-qa/record-blocked-report.mts create mode 100644 scripts/release-v2-qa/report-integrity.mts create mode 100644 scripts/release-v2-qa/report-integrity.spec.ts create mode 100644 scripts/release-v2-qa/report-path-integrity.mts create mode 100644 scripts/release-v2-qa/report-path-integrity.spec.ts create mode 100644 scripts/release-v2-qa/report.mts create mode 100644 scripts/release-v2-qa/runtime.mts create mode 100644 scripts/release-v2-qa/runtime.spec.ts create mode 100644 scripts/release-v2-qa/safety.mts create mode 100644 scripts/release-v2-qa/safety.spec.ts create mode 100644 scripts/release-v2-qa/seed.mts create mode 100644 scripts/release-v2-qa/verify.mts create mode 100644 scripts/release-v2-qa/zoom.mts create mode 100644 scripts/release-v2-qa/zoom.spec.ts diff --git a/.gitignore b/.gitignore index 8c4d8f8df..13c236cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ coverage # next.js .next/ +.next-qa/ out/ next-env.d.ts diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 71e31402c..a2bef01b3 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -26,10 +26,21 @@ const getDevelopmentServiceAliases = () => { }; }; +const getDistDir = () => { + const qaDistDir = process.env.HOMARR_QA_NEXT_DIST_DIR; + if (!qaDistDir) return ".next"; + if (!/^\.next-qa\/slot-[1-3]$/u.test(qaDistDir)) { + throw new Error("HOMARR_QA_NEXT_DIST_DIR must identify .next-qa/slot-1, slot-2, or slot-3"); + } + return qaDistDir; +}; + const nextConfig: NextConfig = { // Next previews otherwise create agent instruction files in the application // directory during development. agentRules: false, + // Release-v2 QA runs multiple isolated development servers from one worktree. + distDir: getDistDir(), env: { HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown", }, diff --git a/apps/nextjs/src/components/layout/analytics.spec.tsx b/apps/nextjs/src/components/layout/analytics.spec.tsx new file mode 100644 index 000000000..aa7a3caba --- /dev/null +++ b/apps/nextjs/src/components/layout/analytics.spec.tsx @@ -0,0 +1,77 @@ +// @vitest-environment jsdom + +import { act, StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { Analytics } from "./analytics"; + +const postHogMock = vi.hoisted(() => { + let resolveImport: (() => void) | undefined; + const importGate = new Promise((resolve) => { + resolveImport = resolve; + }); + + return { + importGate, + init: vi.fn(), + releaseImport: () => resolveImport?.(), + }; +}); + +vi.mock("posthog-js", async () => { + await postHogMock.importGate; + return { default: { init: postHogMock.init } }; +}); + +const host = document.createElement("div"); +const root = createRoot(host); + +afterEach(() => { + act(() => root.unmount()); +}); + +describe("Analytics", () => { + it("deduplicates Strict Mode initialization and retries after a failure", async () => { + const initializationError = new Error("initialization failed"); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); + postHogMock.init.mockImplementationOnce(() => { + throw initializationError; + }); + + await act(async () => { + root.render( + + + , + ); + }); + + postHogMock.releaseImport(); + + await vi.waitFor(() => { + expect(postHogMock.init).toHaveBeenCalledOnce(); + expect(consoleError).toHaveBeenCalledWith("PostHog initialization failed", initializationError); + }); + + await act(async () => { + root.render( + + + , + ); + }); + await act(async () => { + root.render( + + + , + ); + }); + + await vi.waitFor(() => { + expect(postHogMock.init).toHaveBeenCalledTimes(2); + }); + expect(consoleError).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/nextjs/src/components/layout/analytics.tsx b/apps/nextjs/src/components/layout/analytics.tsx index a59c65b7e..affecac32 100644 --- a/apps/nextjs/src/components/layout/analytics.tsx +++ b/apps/nextjs/src/components/layout/analytics.tsx @@ -5,19 +5,36 @@ import { useEffect } from "react"; const POSTHOG_KEY = "phc_vYBmGWNbRshvfeC7EHfeSmUm2pD2Neg5nGqzJuGvS8Hs"; const POSTHOG_HOST = "https://hog.homarr.dev"; +let postHogInitialization: Promise | undefined; + +const initializePostHog = () => { + if (postHogInitialization) return; + + const initializationAttempt = import("posthog-js").then(({ default: posthog }) => { + posthog.init(POSTHOG_KEY, { + api_host: POSTHOG_HOST, + capture_pageview: false, + capture_pageleave: false, + autocapture: false, + persistence: "memory", + }); + }); + + postHogInitialization = initializationAttempt; + void initializationAttempt.catch((error: unknown) => { + if (postHogInitialization === initializationAttempt) { + postHogInitialization = undefined; + } + + console.error("PostHog initialization failed", error); + }); +}; + export const Analytics = ({ enabled }: { enabled: boolean }) => { useEffect(() => { if (!enabled) return; - void import("posthog-js").then(({ default: posthog }) => { - posthog.init(POSTHOG_KEY, { - api_host: POSTHOG_HOST, - capture_pageview: false, - capture_pageleave: false, - autocapture: false, - persistence: "memory", - }); - }); + initializePostHog(); }, [enabled]); return null; diff --git a/package.json b/package.json index edbd669c7..f59c0ef2e 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,12 @@ "lint:fix": "turbo lint --continue -- --fix", "lint:ws": "pnpm dlx sherif@latest", "qa:browser-agent:ux": "bash scripts/browser-agent/ux-coherence-happy-paths.sh", + "qa:release-v2:fixtures": "tsx scripts/release-v2-qa/fixture-server.mts", + "qa:release-v2:report": "tsx scripts/release-v2-qa/report.mts", + "qa:release-v2:runtime": "tsx scripts/release-v2-qa/runtime.mts", + "qa:release-v2:seed": "tsx scripts/release-v2-qa/seed.mts", + "qa:release-v2:verify": "tsx scripts/release-v2-qa/verify.mts", + "qa:release-v2:zoom": "tsx scripts/release-v2-qa/zoom.mts", "release": "semantic-release", "scripts:update-bug-report-template": "tsx ./scripts/update-bug-report-template.mts", "scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts", diff --git a/packages/translation/src/middleware.spec.ts b/packages/translation/src/middleware.spec.ts new file mode 100644 index 000000000..2c5a6f464 --- /dev/null +++ b/packages/translation/src/middleware.spec.ts @@ -0,0 +1,94 @@ +import { NextRequest } from "next/server"; +import { describe, expect, it } from "vitest"; + +import { createI18nMiddleware } from "./middleware"; + +const createRewrittenRequest = (response: Response) => { + const destination = response.headers.get("x-middleware-rewrite"); + const overriddenHeaderNames = response.headers.get("x-middleware-override-headers")?.split(",") ?? []; + const headers = new Headers(); + + for (const name of overriddenHeaderNames) { + const value = response.headers.get(`x-middleware-request-${name}`); + if (value !== null) { + headers.set(name, value); + } + } + + if (destination === null) { + throw new Error("Expected middleware to rewrite the request"); + } + + return new NextRequest(destination, { headers }); +}; + +describe("createI18nMiddleware", () => { + it("canonicalizes a prefixed path when the locale header is supplied externally", () => { + const middleware = createI18nMiddleware("en"); + const request = new NextRequest("http://localhost/en/auth/login", { + headers: { + cookie: "homarr.locale=fr", + "x-next-intl-locale": "en", + }, + }); + + const response = middleware(request); + + expect(response.headers.get("x-middleware-next")).toBeNull(); + expect(response.headers.get("location")).toBe("http://localhost/auth/login"); + expect(response.headers.get("set-cookie")).toContain("homarr.locale=en"); + }); + + it("continues a valid internal locale handoff across middleware instances", () => { + const firstPassMiddleware = createI18nMiddleware("en"); + const publicRequest = new NextRequest("http://localhost/auth/login"); + + const rewriteResponse = firstPassMiddleware(publicRequest); + expect(rewriteResponse.headers.get("x-middleware-rewrite")).toBe("http://localhost/en/auth/login"); + + const localizedRequest = createRewrittenRequest(rewriteResponse); + const secondPassMiddleware = createI18nMiddleware("en"); + const handoffResponse = secondPassMiddleware(localizedRequest); + + expect(handoffResponse.headers.get("x-middleware-next")).toBe("1"); + expect(handoffResponse.headers.get("location")).toBeNull(); + expect(handoffResponse.headers.get("x-middleware-override-headers")).toBe("x-next-intl-locale"); + }); + + it.each([ + { + name: "unsupported locale", + pathname: "/en/auth/login", + locale: "xx", + }, + { + name: "locale that does not match the path", + pathname: "/fr/auth/login", + locale: "en", + }, + ])("does not continue an internal handoff with a $name", ({ pathname, locale }) => { + const middleware = createI18nMiddleware("en"); + const rewriteResponse = middleware(new NextRequest("http://localhost/auth/login")); + const rewrittenRequest = createRewrittenRequest(rewriteResponse); + rewrittenRequest.headers.set("x-next-intl-locale", locale); + const request = new NextRequest(`http://localhost${pathname}`, { + headers: rewrittenRequest.headers, + }); + + const response = middleware(request); + + expect(response.headers.get("x-middleware-next")).toBeNull(); + expect(response.headers.get("location")).toBe("http://localhost/auth/login"); + }); + + it("canonicalizes a directly requested prefixed path and synchronizes its locale", () => { + const middleware = createI18nMiddleware("en"); + const request = new NextRequest("http://localhost/fr/auth/login"); + + const response = middleware(request); + + expect(response.headers.get("x-middleware-next")).toBeNull(); + expect(response.headers.get("location")).toBe("http://localhost/auth/login"); + expect(response.headers.get("set-cookie")).toContain("homarr.locale=fr"); + }); +}); diff --git a/packages/translation/src/middleware.ts b/packages/translation/src/middleware.ts index 14f587959..f4610305d 100644 --- a/packages/translation/src/middleware.ts +++ b/packages/translation/src/middleware.ts @@ -1,11 +1,35 @@ import createMiddleware from "next-intl/middleware"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; import type { SupportedLanguage } from "."; import { supportedLanguages } from "./languages"; import { createRouting } from "./routing"; -export const createI18nMiddleware = (defaultLocale: SupportedLanguage) => - createMiddleware(createRouting(defaultLocale)); +// next-intl carries request headers through its rewrite. Keep the random value +// module-private so a client-supplied locale header cannot impersonate that handoff. +const localeHandoffHeader = "x-homarr-locale-handoff"; +const localeHandoffMarker = crypto.randomUUID(); + +export const createI18nMiddleware = (defaultLocale: SupportedLanguage) => { + const middleware = createMiddleware(createRouting(defaultLocale)); + + return (request: NextRequest) => { + const requestLocale = request.headers.get("x-next-intl-locale"); + const pathLocale = request.nextUrl.pathname.split("/")[1]; + const isSupportedLocale = supportedLanguages.includes(requestLocale as SupportedLanguage); + const isInternalHandoff = request.headers.get(localeHandoffHeader) === localeHandoffMarker; + + if (isInternalHandoff && isSupportedLocale && requestLocale === pathLocale) { + const headers = new Headers(request.headers); + headers.delete(localeHandoffHeader); + return NextResponse.next({ request: { headers } }); + } + + request.headers.set(localeHandoffHeader, localeHandoffMarker); + return middleware(request); + }; +}; export const config = { // Match only internationalized pathnames diff --git a/packages/widgets/src/weather/compact.tsx b/packages/widgets/src/weather/compact.tsx index 268bd401c..104234d5b 100644 --- a/packages/widgets/src/weather/compact.tsx +++ b/packages/widgets/src/weather/compact.tsx @@ -15,6 +15,7 @@ import { formatWeatherDate, WeatherDescription } from "./icon"; import { getCompactWeatherLayout } from "./layout"; interface CompactWeatherProps { + displayScale: number; height: number; isEditMode: boolean; options: WidgetProps<"weather">["options"]; @@ -22,13 +23,13 @@ interface CompactWeatherProps { width: number; } -export const CompactWeather = ({ height, isEditMode, options, weather, width }: CompactWeatherProps) => { +export const CompactWeather = ({ displayScale, height, isEditMode, options, weather, width }: CompactWeatherProps) => { const locale = useCurrentIntlLocale(); const t = useI18n("widget.weather"); const tCommon = useI18n("common"); const layout = useMemo( - () => getCompactWeatherLayout(width, height, options.hasForecast, options.forecastDayCount), - [height, options.forecastDayCount, options.hasForecast, width], + () => getCompactWeatherLayout(width, height, options.hasForecast, options.forecastDayCount, displayScale), + [displayScale, height, options.forecastDayCount, options.hasForecast, width], ); const today = weather.daily[0]; const speedUnit = options.useImperialSpeed diff --git a/packages/widgets/src/weather/component.spec.ts b/packages/widgets/src/weather/component.spec.ts index c23765279..8944f40b8 100644 --- a/packages/widgets/src/weather/component.spec.ts +++ b/packages/widgets/src/weather/component.spec.ts @@ -53,6 +53,18 @@ describe("weather layout", () => { expect(getCompactWeatherLayout(640, 360, false, 7).forecastDays).toBe(0); }); + it("uses the rendered dimensions when a responsive board scales the widget", () => { + expect(getCompactWeatherLayout(320, 180, true, 7).tier).toBe("standard"); + expect(getCompactWeatherLayout(320, 180, true, 7, 0.5)).toEqual({ + tier: "micro", + forecastDays: 0, + showCity: false, + showCondition: false, + showHighLow: false, + showSecondary: false, + }); + }); + it("adapts the advanced view to mobile and desktop surfaces", () => { expect(getAdvancedWeatherLayout(374, 624)).toMatchObject({ dailyColumns: 2, diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx index 9b47565d2..b092b0365 100644 --- a/packages/widgets/src/weather/component.tsx +++ b/packages/widgets/src/weather/component.tsx @@ -18,6 +18,7 @@ export default function WeatherWidget({ width, height, displayMode, + displayScale = 1, }: WidgetComponentProps<"weather">) { const input = { latitude: options.location.latitude, @@ -35,7 +36,14 @@ export default function WeatherWidget({ {displayMode === "advanced" ? ( ) : ( - + )} diff --git a/packages/widgets/src/weather/layout.ts b/packages/widgets/src/weather/layout.ts index a1bede357..ded3b691c 100644 --- a/packages/widgets/src/weather/layout.ts +++ b/packages/widgets/src/weather/layout.ts @@ -21,9 +21,17 @@ export const getCompactWeatherLayout = ( height: number, hasForecast: boolean, configuredDays: number, + displayScale = 1, ): CompactWeatherLayout => { + let responsiveWidth = width; + let responsiveHeight = height; + if (Number.isFinite(displayScale) && displayScale > 0) { + responsiveWidth *= displayScale; + responsiveHeight *= displayScale; + } + const breakpoint = compactWeatherBreakpoints.find( - ({ minHeight, minWidth }) => width >= minWidth && height >= minHeight, + ({ minHeight, minWidth }) => responsiveWidth >= minWidth && responsiveHeight >= minHeight, ); const tier = breakpoint?.tier ?? "micro"; @@ -41,15 +49,15 @@ export const getCompactWeatherLayout = ( if (tier === "compact") { return { tier: "compact", - forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(width / 92))) : 0, - showCity: width >= 220, + forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(responsiveWidth / 92))) : 0, + showCity: responsiveWidth >= 220, showCondition: true, showHighLow: true, showSecondary: false, }; } - const availableForecastWidth = Math.max(0, width - 24); + const availableForecastWidth = Math.max(0, responsiveWidth - 24); return { tier, forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(availableForecastWidth / 64))) : 0, diff --git a/qa/release-v2/README.md b/qa/release-v2/README.md new file mode 100644 index 000000000..a8a004fac --- /dev/null +++ b/qa/release-v2/README.md @@ -0,0 +1,102 @@ +# Release-v2 dogfood QA + +This directory is the fixed coverage contract for the release-v2 browser dogfood campaign. The candidate is always the checkout's exact `HEAD` commit when the harness runs. `coverage-manifest.json` owns the matrix and agent assignments; `ledger.json` and `master-report.md` are generated from the tracked reports in `reports//report.md`. + +A dirty pre-commit checkout can be used for focused harness checks, but its runtime provenance still identifies the underlying `HEAD` and does not claim that uncommitted files belong to that SHA. Start fresh campaign instances after the campaign changes are committed so runtime, fixture, coverage, ledger, and report metadata all record the exact final commit. + +## Required commands + +Use Node 24 and the repository toolchain. These package commands are the supported entry points: + +```bash +pnpm qa:release-v2:fixtures +pnpm qa:release-v2:seed -- --profile --output [--reset] +pnpm qa:release-v2:verify +pnpm qa:release-v2:report +pnpm qa:release-v2:zoom -- --origin --factor <0.8|1|1.25|2> --output +``` + +The fixture command binds to an explicit loopback host on an automatically allocated port and prints only its non-secret readiness metadata. It accepts `127.0.0.1`, `localhost`, and IPv6 loopback origins such as `[::1]`, while rejecting wildcard and non-loopback hosts. Pass `--ready-file ` when another process needs to consume the selected URL. Direct seeding also requires an isolated migrated SQLite `DB_URL`, a credential-free loopback HTTP origin in `QA_FIXTURE_URL`, and a password for populated profiles. Candidate provenance is derived from the actual checkout; optional `--candidate-sha` or `QA_CANDIDATE_SHA` input is accepted only when it exactly matches `HEAD`. Supply the password through `QA_PASSWORD_FILE` where possible; the file content is never written to a manifest or command output, and neither password environment variable is forwarded to the spawned Next.js process after seeding. `onboarding-fresh` does not require a password. + +Each populated fixture manifest keeps the human-readable persona label in `personas[].name` and exposes the non-secret credentials username in `personas[].loginUsername`. Board route names are deterministic, unique, and validation-safe in `boards[].name`; shared board names exactly match the IDs in `coverage-manifest.json` (including `qa-scroll-lab`), while the human-readable title remains in `boards[].label` and the board page/meta titles. The login username is the deterministic lowercase persona handle; the password remains available only through `QA_PASSWORD_FILE`. + +Generated placements are validated before the seed transaction commits. Root items and containers must fit the active main/left/right lane after gutters, nested containers must fit their parent content grid without cycles, and every item must fit its selected root or container in every layout. `qa-grid-24` includes a dedicated unguttered 24-column custom layout that preserves exact width coverage `1–24`; its guttered Base layout and all narrower layouts keep lane-valid projected widths. + +Use `report -- --init` to create missing agent reports. Use `report -- --refresh-placeholders` only after changing manifest templates; it refuses to overwrite reports with execution results, widget results, measurements, limitations, or reproduction data. + +## Managed runtime + +The runtime command provisions Redis, the deterministic fixture server, the isolated SQLite database, seed data, and Next.js: + +```bash +QA_PASSWORD_FILE=/secure/path pnpm qa:release-v2:runtime -- start --slot 1 --profile main-writable +pnpm qa:release-v2:runtime -- status --slot 1 +pnpm qa:release-v2:runtime -- stop --slot 1 +``` + +Slots `1`, `2`, and `3` have separate database files, Redis containers and ports, cookie prefixes, logs, fixture ports, app ports, and `.next-qa/slot-` build directories. The runtime manifest is written only after the app is ready and records the watcher interval. Status checks the owned processes, app, fixture, and Redis. Stop is idempotent for a stopped manifest and refuses to signal a live PID that does not carry the recorded QA run ID. + +Every start, reset, and stop holds an exclusive per-slot filesystem lease outside the resettable slot tree. A second operation on the same slot fails deterministically. Stale recovery compares both the recorded PID and its operating-system process-start marker; it never signals a lease owner or an unrelated process. An incomplete fresh lease is preserved for a short grace period instead of being guessed stale. + +The fixture child receives an explicit environment allowlist containing only its non-secret QA run ID; ports and ready-file paths are command arguments. Password, database, authentication, encryption, API, and unrelated inherited environment values are not forwarded. The app keeps its required runtime environment but never receives `QA_PASSWORD` or `QA_PASSWORD_FILE` after seeding. + +The selected app port remains bound by a temporary loopback reservation through migration and seeding. The reservation is released immediately before Next.js binds. If another process wins that unavoidable release-to-bind window and Next.js reports `EADDRINUSE`, startup retries with a newly reserved, dynamically selected port. No application port is hard-coded. + +`--reset` is intentionally narrow: after validating the QA run-root marker, it stops only the selected running manifest, removes only that slot directory, and clears only that slot's `.next-qa` cache. The run root must be a direct child of the canonical operating-system temporary directory returned by Node, its basename must be `homarr-release-v2-qa` or start with `homarr-release-v2-qa-`, and only safe suffix characters are accepted. Temporary-directory aliases are canonicalized before containment checks. Existing run roots and slot paths must not be symlinks or contain symlink components, and destructive paths are rechecked immediately before mutation. The Linux default `/tmp/homarr-release-v2-qa` remains unchanged. + +This host's `fs.inotify.max_user_instances=128` is too low for concurrent native Watchpack watchers. Spawned QA apps keep Turbopack and set `WATCHPACK_POLLING=1000`, avoiding reliance on another inotify instance while keeping all three isolated slots available. Filesystem changes can take up to one polling interval to reach HMR. This is Watchpack's watcher mode, not a switch to the Webpack bundler. + +## Profiles + +| Profile | `DEMO_MODE` | `DEMO_READ_ONLY` | `UNSAFE_ENABLE_MOCK_INTEGRATION` | +| ------------------ | ----------- | ---------------- | -------------------------------- | +| `main-writable` | `true` | `false` | `true` | +| `main-readonly` | `true` | `true` | `true` | +| `onboarding-fresh` | `false` | `false` | `true` | +| `degraded` | `true` | `false` | `true` | + +The verifier enforces this exact matrix. For a live check, pass either a run-root or one or more runtime/fixture manifests: + +```bash +pnpm qa:release-v2:verify -- --run-root /tmp/homarr-release-v2-qa +pnpm qa:release-v2:verify -- --manifest /tmp/homarr-release-v2-qa/slots/1/runtime-manifest.json +``` + +Live verification checks the checkout-derived candidate, profile flags, paths, counts, personas, board names against both the fixture and coverage manifests, board-name validity and uniqueness, layouts, lane and recursive container geometry, item placements, board permissions, fixture health, app readiness, and reset-safety invariants. The default command remains a static coverage/report check and needs no running services. + +## Evidence contract + +Each base case ID is expanded through its wave's `caseDimensions`. Allowed statuses are `passed`, `failed`, `blocked`, and `not-reached`; never turn an unrun check into a pass. Every failed, blocked, or not-reached case or assigned widget check is a critical gap. + +Every widget is assigned all nine required viewports. The high-risk widget list must exercise every width `1–24` × every height `1–6` at mobile, breakpoint-edge, and desktop viewports. Other widgets must cover minimum, canonical, wide, tall, maximum, overflow, and behavior-changing thresholds. Keep structured `widgetChecks` and the matching human rows synchronized. + +Execution metadata always carries the exact checkout candidate SHA. URL, actual port, runtime flags/profile, persona, browser session ID, timestamp, viewport, input, and zoom may remain unset only while execution is not reached. Record performance measurements and limitations structurally, and attach independent reproduction outcomes to finding fingerprints without inventing results. + +Screenshots and traces stay untracked under `.screenshots/release-v2//`. Reports must link absolute local artifact paths. Never copy passwords, credentials, cookies, tokens, secret-bearing response bodies, or environment dumps into evidence. + +The decision is `GO` only when there are no P0/P1 findings and no critical gaps; otherwise it is `NO-GO`. The report command itself treats missing, null, malformed, wrong-candidate, incomplete, assignment-inconsistent, or case-rollup-inconsistent metadata as an explicit critical metadata gap. It does not depend on a separate verification run to force `NO-GO`. + +Aggregation refuses symlinked packet reports, `ledger.json`, and `master-report.md`. Artifact and evidence paths must exist, be absolute, and ultimately resolve inside the assigned `.screenshots/release-v2//` directory; a lexically contained symlink that resolves outside is rejected. + +## Native browser zoom + +The zoom helper creates a disposable Manifest V3 extension containing only `manifest.json` and `background.js`. Its host permission and runtime URL check are restricted to the exact supplied HTTP(S) origin. The factor whitelist is `0.8`, `1`, `1.25`, and `2`; factor `1` is supported as a deterministic extension baseline, though an unmodified browser session needs no extension for the same 100% baseline. The output must be a new direct child of the canonical operating-system temporary directory named `homarr-release-v2-qa-zoom-`. The helper canonicalizes temporary-directory aliases, never removes or resets an existing path, and never accepts credentials in the origin. + +Generate one extension per factor, then launch it in a fresh session and profile: + +```bash +pnpm qa:release-v2:zoom -- \ + --origin http://127.0.0.1: \ + --factor 1.25 \ + --output /tmp/homarr-release-v2-qa-zoom-125- + +npx -y --package agent-browser agent-browser \ + --session release-v2-zoom-125- \ + --profile /tmp/homarr-release-v2-qa-zoom-profile-125- \ + --extension /tmp/homarr-release-v2-qa-zoom-125- \ + --headed false \ + --args '--no-sandbox,--headless=new' \ + open http://127.0.0.1: +``` + +Keep the session and profile unique for every factor. Restart the session when changing factors. Native browser zoom must preserve `window.visualViewport.scale === 1`; record the actual CSS viewport and `devicePixelRatio` alongside that invariant. A changed `visualViewport.scale` indicates page scaling rather than native browser zoom and must not be reported as zoom coverage. diff --git a/qa/release-v2/coverage-manifest.json b/qa/release-v2/coverage-manifest.json new file mode 100644 index 000000000..1f7013d06 --- /dev/null +++ b/qa/release-v2/coverage-manifest.json @@ -0,0 +1,922 @@ +{ + "schemaVersion": 1, + "browser": "Chromium", + "artifactRoot": ".screenshots/release-v2", + "statuses": ["passed", "failed", "blocked", "not-reached"], + "severities": ["P0", "P1", "P2", "P3"], + "pullRequests": [ + { "number": 6356, "label": "Custom Widgets" }, + { "number": 6503, "label": "dnd-kit grid" }, + { "number": 6502, "label": "widget modernization" }, + { "number": 6450, "label": "responsive layouts" }, + { "number": 6482, "label": "Assistant" }, + { "number": 6555, "label": "widget performance" }, + { "number": 6569, "label": "onboarding" }, + { "number": 6545, "label": "release-v2 rollup" } + ], + "viewports": [ + { "id": "mobile-320", "width": 320, "height": 568 }, + { "id": "mobile-375", "width": 375, "height": 812 }, + { "id": "boundary-767", "width": 767, "height": 900 }, + { "id": "boundary-768", "width": 768, "height": 900 }, + { "id": "tablet-1024", "width": 1024, "height": 768 }, + { "id": "boundary-1279", "width": 1279, "height": 900 }, + { "id": "desktop-1280", "width": 1280, "height": 900 }, + { "id": "desktop-1440", "width": 1440, "height": 900 }, + { "id": "desktop-1920", "width": 1920, "height": 1080 } + ], + "zooms": [80, 100, 125, 200], + "inputs": ["mouse", "touch", "keyboard"], + "profiles": [ + { "id": "main-writable", "description": "Authenticated writable release candidate" }, + { "id": "main-readonly", "description": "Authenticated read-only release candidate" }, + { "id": "onboarding-fresh", "description": "Fresh installation at onboarding" }, + { "id": "degraded", "description": "Fixture-backed error, empty, and latency states" } + ], + "personas": [ + "Avery Admin", + "Rowan Owner", + "Eden Editor", + "Vivian Viewer", + "Nolan Outsider", + "Morgan Mobile", + "Casey Chaos", + "Ingrid Infra", + "Maya Media", + "Brooke Minimalist", + "Cora Creator", + "Ash Assistant", + "Kira Keyboard", + "Nora Newcomer" + ], + "boards": [ + { "id": "qa-grid-24", "handle": "grid-24" }, + { "id": "qa-scroll-lab", "handle": "scroll" }, + { "id": "qa-dense-collisions", "handle": "dense-collisions" }, + { "id": "qa-nested-containers", "handle": "nested-containers" }, + { "id": "qa-layout-boundaries", "handle": "layout-boundaries" }, + { "id": "qa-icons-bookmarks", "handle": "icons-bookmarks" }, + { "id": "qa-widgets-01", "handle": "widgets-01" }, + { "id": "qa-widgets-02", "handle": "widgets-02" }, + { "id": "qa-widgets-03", "handle": "widgets-03" }, + { "id": "qa-widgets-04", "handle": "widgets-04" }, + { "id": "qa-widgets-05", "handle": "widgets-05" }, + { "id": "qa-widgets-06", "handle": "widgets-06" }, + { "id": "qa-widgets-07", "handle": "widgets-07" }, + { "id": "qa-widgets-08", "handle": "widgets-08" }, + { "id": "qa-widgets-09", "handle": "widgets-09" }, + { "id": "qa-widgets-10", "handle": "widgets-10" }, + { "id": "qa-widgets-11", "handle": "widgets-11" }, + { "id": "qa-widgets-12", "handle": "widgets-12" }, + { "id": "qa-custom-widget-assistant", "handle": "custom-widget-assistant" }, + { "id": "qa-permissions-public", "handle": "permissions-public" }, + { "id": "qa-download-upload", "handle": "download-upload" } + ], + "wavePacketCounts": { "preflight": 3, "board": 9, "widgets": 12, "core-v2": 8, "whole-product": 9, "performance": 4 }, + "caseDimensions": { + "preflight": [ + { + "id": "ENVIRONMENT", + "expected": "Verify candidate identity, Chromium session, runtime health, fixture reachability, and clean console/network baselines." + }, + { + "id": "ACCESS", + "expected": "Verify assigned profiles, personas, boards, permissions, and read-only boundaries before mutations." + }, + { + "id": "EVIDENCE", + "expected": "Capture timestamped viewport/input/zoom evidence and confirm artifacts contain no secrets." + } + ], + "board": [ + { + "id": "THRESHOLDS", + "expected": "Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress." + }, + { + "id": "MUTATION", + "expected": "Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet." + }, + { + "id": "PERSISTENCE", + "expected": "Reload, switch layouts or sessions, and verify saved and reset state remains correct." + }, + { + "id": "ACCESS-RECOVERY", + "expected": "Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies." + } + ], + "widgets": [ + { + "id": "RENDER-SIZE", + "expected": "Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported." + }, + { + "id": "STATES-RECOVERY", + "expected": "Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind." + }, + { + "id": "OPTIONS-PERSISTENCE", + "expected": "Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly." + }, + { + "id": "ACCESS-READONLY", + "expected": "Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind." + }, + { + "id": "EVIDENCE", + "expected": "Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row." + } + ], + "core-v2": [ + { + "id": "HAPPY-PATH", + "expected": "Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies." + }, + { + "id": "MUTATION-PERSISTENCE", + "expected": "Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions." + }, + { + "id": "ACCESSIBILITY-ACCESS", + "expected": "Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement." + }, + { + "id": "DEGRADED-RECOVERY", + "expected": "Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior." + } + ], + "whole-product": [ + { + "id": "JOURNEY", + "expected": "Complete the persona journey across all assigned boards and profiles without bypassing product navigation." + }, + { + "id": "THRESHOLDS", + "expected": "Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus." + }, + { + "id": "PERSISTENCE-ACCESS", + "expected": "Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact." + }, + { + "id": "FAILURE-EVIDENCE", + "expected": "Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence." + } + ], + "performance": [ + { + "id": "BASELINE", + "expected": "Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata." + }, + { + "id": "THRESHOLD", + "expected": "Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold." + }, + { + "id": "STRESS", + "expected": "Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies." + }, + { + "id": "RECOVERY-EVIDENCE", + "expected": "Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts." + } + ] + }, + "packets": [ + { + "id": "preflight-01", + "wave": "preflight", + "title": "Candidate identity and services", + "prRefs": [6545], + "personas": ["Avery Admin"], + "boards": [], + "profiles": ["main-writable", "main-readonly", "onboarding-fresh", "degraded"], + "viewports": ["desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["PF-001"] + }, + { + "id": "preflight-02", + "wave": "preflight", + "title": "Fixture and persona access", + "prRefs": [6545], + "personas": ["Avery Admin", "Rowan Owner", "Vivian Viewer", "Nolan Outsider"], + "boards": ["qa-grid-24", "qa-permissions-public"], + "profiles": ["main-writable", "main-readonly"], + "viewports": ["desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["PF-002"] + }, + { + "id": "preflight-03", + "wave": "preflight", + "title": "Browser evidence hygiene", + "prRefs": [6545], + "personas": ["Casey Chaos"], + "boards": [], + "profiles": ["main-writable"], + "viewports": ["mobile-320", "desktop-1920"], + "zooms": [80, 100, 125, 200], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["PF-003"] + }, + + { + "id": "board-01", + "wave": "board", + "title": "24-column placement and resize", + "prRefs": [6503], + "personas": ["Rowan Owner"], + "boards": ["qa-grid-24"], + "profiles": ["main-writable"], + "viewports": ["desktop-1440", "desktop-1920"], + "zooms": [80, 100, 125], + "inputs": ["mouse", "keyboard"], + "cases": ["BD-001"] + }, + { + "id": "board-02", + "wave": "board", + "title": "Scrollable canvas and collapse states", + "prRefs": [6503], + "personas": ["Eden Editor"], + "boards": ["qa-scroll-lab"], + "profiles": ["main-writable"], + "viewports": ["mobile-375", "desktop-1280"], + "zooms": [100, 200], + "inputs": ["mouse", "touch"], + "cases": ["BD-002"] + }, + { + "id": "board-03", + "wave": "board", + "title": "Dense collision handling", + "prRefs": [6503], + "personas": ["Casey Chaos"], + "boards": ["qa-dense-collisions"], + "profiles": ["main-writable"], + "viewports": ["tablet-1024", "desktop-1440"], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["BD-003"] + }, + { + "id": "board-04", + "wave": "board", + "title": "Nested containers", + "prRefs": [6503], + "personas": ["Rowan Owner"], + "boards": ["qa-nested-containers"], + "profiles": ["main-writable"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100, 200], + "inputs": ["mouse", "touch"], + "cases": ["BD-004"] + }, + { + "id": "board-05", + "wave": "board", + "title": "Responsive layout boundaries", + "prRefs": [6450], + "personas": ["Morgan Mobile"], + "boards": ["qa-layout-boundaries"], + "profiles": ["main-writable"], + "viewports": ["boundary-767", "boundary-768", "boundary-1279", "desktop-1280"], + "zooms": [100], + "inputs": ["touch", "keyboard"], + "cases": ["BD-005"] + }, + { + "id": "board-06", + "wave": "board", + "title": "Icons, bookmarks, and compact layout", + "prRefs": [6502], + "personas": ["Brooke Minimalist"], + "boards": ["qa-icons-bookmarks"], + "profiles": ["main-writable"], + "viewports": ["mobile-375", "desktop-1440"], + "zooms": [80, 100, 125], + "inputs": ["mouse", "touch"], + "cases": ["BD-006"] + }, + { + "id": "board-07", + "wave": "board", + "title": "Permission-aware editing", + "prRefs": [6503, 6545], + "personas": ["Rowan Owner", "Eden Editor", "Vivian Viewer", "Nolan Outsider"], + "boards": ["qa-permissions-public"], + "profiles": ["main-writable", "main-readonly"], + "viewports": ["desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["BD-007"] + }, + { + "id": "board-08", + "wave": "board", + "title": "Board import and export", + "prRefs": [6545], + "personas": ["Avery Admin"], + "boards": ["qa-download-upload"], + "profiles": ["main-writable"], + "viewports": ["desktop-1280"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["BD-008"] + }, + { + "id": "board-09", + "wave": "board", + "title": "Keyboard-only grid operation", + "prRefs": [6503, 6450], + "personas": ["Kira Keyboard"], + "boards": ["qa-grid-24"], + "profiles": ["main-writable"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100, 200], + "inputs": ["keyboard"], + "cases": ["BD-009"] + }, + + { + "id": "widgets-01", + "wave": "widgets", + "title": "Time and environment", + "prRefs": [6502, 6555], + "personas": ["Brooke Minimalist"], + "boards": ["qa-widgets-01"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["clock", "weather", "airQuality", "countdown", "timer"], + "cases": ["WG-001"] + }, + { + "id": "widgets-02", + "wave": "widgets", + "title": "Apps, embeds, video, game status, and stocks", + "prRefs": [6502, 6555], + "personas": ["Cora Creator"], + "boards": ["qa-widgets-02"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 200], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["app", "iframe", "video", "minecraftServerStatus", "stockPrice"], + "cases": ["WG-002"] + }, + { + "id": "widgets-03", + "wave": "widgets", + "title": "Notes, bookmarks, feeds, and timetable", + "prRefs": [6502, 6555], + "personas": ["Cora Creator"], + "boards": ["qa-widgets-03"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [80, 100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["notebook", "anchorNote", "bookmarks", "rssFeed", "timetable"], + "cases": ["WG-003"] + }, + { + "id": "widgets-04", + "wave": "widgets", + "title": "Downloads, containers, indexers, and DNS", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-04"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["downloads", "dockerContainers", "indexerManager", "dnsHoleSummary", "dnsHoleControls"], + "cases": ["WG-004"] + }, + { + "id": "widgets-05", + "wave": "widgets", + "title": "Smart home, health, and system telemetry", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-05"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 200], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": [ + "smartHome-entityState", + "smartHome-executeAutomation", + "healthMonitoring", + "systemResources", + "systemDisks" + ], + "cases": ["WG-005"] + }, + { + "id": "widgets-06", + "wave": "widgets", + "title": "Network availability and operations", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-06"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [80, 100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["firewall", "notifications", "networkControllerSummary", "networkControllerStatus", "uptimeKuma"], + "cases": ["WG-006"] + }, + { + "id": "widgets-07", + "wave": "widgets", + "title": "Beszel and update monitoring", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-07"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["beszelSystemTable", "beszelSystemGrid", "beszelAlerts", "beszelSystemStats", "wud"], + "cases": ["WG-007"] + }, + { + "id": "widgets-08", + "wave": "widgets", + "title": "Power, VPN, speed, routing, and analytics", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-08"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 200], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["ups", "vpn", "speedtestTracker", "traefik", "umami"], + "cases": ["WG-008"] + }, + { + "id": "widgets-09", + "wave": "widgets", + "title": "Media overview and requests", + "prRefs": [6502, 6555], + "personas": ["Maya Media"], + "boards": ["qa-widgets-09"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [80, 100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": [ + "calendar", + "mediaServer", + "mediaRequests-requestList", + "mediaRequests-requestStats", + "mediaMissing" + ], + "cases": ["WG-009"] + }, + { + "id": "widgets-10", + "wave": "widgets", + "title": "Media activity, Immich, and audio", + "prRefs": [6502, 6555], + "personas": ["Maya Media"], + "boards": ["qa-widgets-10"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["mediaReleases", "mediaTranscoding", "immich-serverStats", "immich-albumCarousel", "audioStats"], + "cases": ["WG-010"] + }, + { + "id": "widgets-11", + "wave": "widgets", + "title": "Documents, patching, media services, and releases", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-11"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [100, 200], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["paperlessNgx", "patchmon", "bazarr", "tracearr", "releases"], + "cases": ["WG-011"] + }, + { + "id": "widgets-12", + "wave": "widgets", + "title": "Coolify, ArchiveTeam, Custom API, and Assistant", + "prRefs": [6502, 6482, 6555], + "personas": ["Ash Assistant", "Cora Creator"], + "boards": ["qa-widgets-12", "qa-custom-widget-assistant"], + "profiles": ["main-writable", "degraded"], + "viewports": [ + "mobile-320", + "mobile-375", + "boundary-767", + "boundary-768", + "tablet-1024", + "boundary-1279", + "desktop-1280", + "desktop-1440", + "desktop-1920" + ], + "zooms": [80, 100, 125, 200], + "inputs": ["mouse", "touch", "keyboard"], + "widgetKinds": ["coolify", "archiveTeamWarrior", "customApi", "assistant"], + "cases": ["WG-012"] + }, + + { + "id": "core-v2-01", + "wave": "core-v2", + "title": "Custom widget authoring", + "prRefs": [6356], + "personas": ["Cora Creator"], + "boards": ["qa-custom-widget-assistant"], + "profiles": ["main-writable", "degraded"], + "viewports": ["desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["CV-001"] + }, + { + "id": "core-v2-02", + "wave": "core-v2", + "title": "Assistant tool flow", + "prRefs": [6482], + "personas": ["Ash Assistant"], + "boards": ["qa-custom-widget-assistant"], + "profiles": ["main-writable", "degraded"], + "viewports": ["mobile-375", "desktop-1440"], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["CV-002"] + }, + { + "id": "core-v2-03", + "wave": "core-v2", + "title": "Onboarding happy path", + "prRefs": [6569], + "personas": ["Nora Newcomer"], + "boards": [], + "profiles": ["onboarding-fresh"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["CV-003"] + }, + { + "id": "core-v2-04", + "wave": "core-v2", + "title": "Authentication and session transitions", + "prRefs": [6545, 6569], + "personas": ["Avery Admin", "Nolan Outsider"], + "boards": ["qa-permissions-public"], + "profiles": ["main-writable", "main-readonly", "onboarding-fresh"], + "viewports": ["mobile-375", "desktop-1280"], + "zooms": [100], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["CV-004"] + }, + { + "id": "core-v2-05", + "wave": "core-v2", + "title": "Integration management", + "prRefs": [6502, 6545], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-07"], + "profiles": ["main-writable", "degraded"], + "viewports": ["desktop-1440"], + "zooms": [100, 125], + "inputs": ["mouse", "keyboard"], + "cases": ["CV-005"] + }, + { + "id": "core-v2-06", + "wave": "core-v2", + "title": "Search, menus, and dialogs", + "prRefs": [6545], + "personas": ["Kira Keyboard"], + "boards": ["qa-grid-24"], + "profiles": ["main-writable"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100, 200], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["CV-006"] + }, + { + "id": "core-v2-07", + "wave": "core-v2", + "title": "Read-only enforcement", + "prRefs": [6356, 6482, 6545], + "personas": ["Vivian Viewer"], + "boards": ["qa-custom-widget-assistant", "qa-permissions-public"], + "profiles": ["main-readonly"], + "viewports": ["mobile-375", "desktop-1280"], + "zooms": [100], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["CV-007"] + }, + { + "id": "core-v2-08", + "wave": "core-v2", + "title": "Failure and recovery states", + "prRefs": [6356, 6482, 6502, 6545], + "personas": ["Casey Chaos"], + "boards": ["qa-custom-widget-assistant", "qa-widgets-12"], + "profiles": ["degraded"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["CV-008"] + }, + + { + "id": "whole-product-01", + "wave": "whole-product", + "title": "Admin day-one journey", + "prRefs": [6545, 6569], + "personas": ["Avery Admin", "Nora Newcomer"], + "boards": ["qa-grid-24"], + "profiles": ["onboarding-fresh", "main-writable"], + "viewports": ["desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["WP-001"] + }, + { + "id": "whole-product-02", + "wave": "whole-product", + "title": "Owner customization journey", + "prRefs": [6356, 6503, 6502], + "personas": ["Rowan Owner"], + "boards": ["qa-grid-24", "qa-custom-widget-assistant"], + "profiles": ["main-writable"], + "viewports": ["desktop-1920"], + "zooms": [80, 100, 125], + "inputs": ["mouse", "keyboard"], + "cases": ["WP-002"] + }, + { + "id": "whole-product-03", + "wave": "whole-product", + "title": "Editor daily journey", + "prRefs": [6503, 6502, 6450], + "personas": ["Eden Editor"], + "boards": ["qa-scroll-lab", "qa-layout-boundaries"], + "profiles": ["main-writable"], + "viewports": ["tablet-1024", "desktop-1280"], + "zooms": [100], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["WP-003"] + }, + { + "id": "whole-product-04", + "wave": "whole-product", + "title": "Viewer and outsider boundaries", + "prRefs": [6545], + "personas": ["Vivian Viewer", "Nolan Outsider"], + "boards": ["qa-permissions-public"], + "profiles": ["main-writable", "main-readonly"], + "viewports": ["mobile-375", "desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["WP-004"] + }, + { + "id": "whole-product-05", + "wave": "whole-product", + "title": "Mobile journey", + "prRefs": [6450, 6503, 6502], + "personas": ["Morgan Mobile"], + "boards": ["qa-layout-boundaries", "qa-widgets-01"], + "profiles": ["main-writable"], + "viewports": ["mobile-320", "mobile-375", "boundary-767", "boundary-768"], + "zooms": [100, 200], + "inputs": ["touch", "keyboard"], + "cases": ["WP-005"] + }, + { + "id": "whole-product-06", + "wave": "whole-product", + "title": "Media operator journey", + "prRefs": [6502, 6555], + "personas": ["Maya Media"], + "boards": ["qa-widgets-04", "qa-widgets-05"], + "profiles": ["main-writable", "degraded"], + "viewports": ["desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "keyboard"], + "cases": ["WP-006"] + }, + { + "id": "whole-product-07", + "wave": "whole-product", + "title": "Infrastructure operator journey", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra"], + "boards": ["qa-widgets-07", "qa-widgets-08", "qa-widgets-09", "qa-widgets-10", "qa-widgets-11"], + "profiles": ["main-writable", "degraded"], + "viewports": ["desktop-1920"], + "zooms": [80, 100, 125], + "inputs": ["mouse", "keyboard"], + "cases": ["WP-007"] + }, + { + "id": "whole-product-08", + "wave": "whole-product", + "title": "Creator and Assistant journey", + "prRefs": [6356, 6482], + "personas": ["Cora Creator", "Ash Assistant"], + "boards": ["qa-custom-widget-assistant"], + "profiles": ["main-writable", "degraded"], + "viewports": ["mobile-375", "desktop-1440"], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["WP-008"] + }, + { + "id": "whole-product-09", + "wave": "whole-product", + "title": "Accessibility and destructive-action pass", + "prRefs": [6356, 6503, 6502, 6450, 6482, 6569, 6545], + "personas": ["Kira Keyboard", "Casey Chaos"], + "boards": ["qa-grid-24", "qa-download-upload", "qa-custom-widget-assistant"], + "profiles": ["main-writable", "main-readonly", "degraded"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100, 200], + "inputs": ["keyboard"], + "cases": ["WP-009"] + }, + + { + "id": "performance-01", + "wave": "performance", + "title": "Cold and warm board load", + "prRefs": [6555, 6545], + "personas": ["Avery Admin"], + "boards": ["qa-grid-24", "qa-widgets-12"], + "profiles": ["main-writable"], + "viewports": ["mobile-375", "desktop-1440"], + "zooms": [100], + "inputs": ["mouse"], + "cases": ["PE-001"] + }, + { + "id": "performance-02", + "wave": "performance", + "title": "Grid interaction responsiveness", + "prRefs": [6503, 6555], + "personas": ["Rowan Owner"], + "boards": ["qa-dense-collisions", "qa-nested-containers"], + "profiles": ["main-writable"], + "viewports": ["tablet-1024", "desktop-1920"], + "zooms": [80, 100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["PE-002"] + }, + { + "id": "performance-03", + "wave": "performance", + "title": "Widget network and render budget", + "prRefs": [6502, 6555], + "personas": ["Ingrid Infra", "Maya Media"], + "boards": ["qa-widgets-04", "qa-widgets-07", "qa-widgets-12"], + "profiles": ["main-writable", "degraded"], + "viewports": ["mobile-320", "desktop-1440"], + "zooms": [100], + "inputs": ["mouse", "touch"], + "cases": ["PE-003"] + }, + { + "id": "performance-04", + "wave": "performance", + "title": "Long-session stability", + "prRefs": [6356, 6503, 6502, 6482, 6555, 6545], + "personas": ["Casey Chaos"], + "boards": ["qa-grid-24", "qa-custom-widget-assistant", "qa-widgets-12"], + "profiles": ["main-writable", "degraded"], + "viewports": ["mobile-375", "desktop-1920"], + "zooms": [100, 125], + "inputs": ["mouse", "touch", "keyboard"], + "cases": ["PE-004"] + } + ] +} diff --git a/qa/release-v2/ledger.json b/qa/release-v2/ledger.json new file mode 100644 index 000000000..69f8c07f5 --- /dev/null +++ b/qa/release-v2/ledger.json @@ -0,0 +1,420 @@ +{ + "schemaVersion": 1, + "updatedAt": "2026-08-31T18:42:05.188Z", + "packetStatuses": { + "preflight-01": { + "status": "failed", + "cases": { + "PF-001-ENVIRONMENT": "failed", + "PF-001-ACCESS": "blocked", + "PF-001-EVIDENCE": "passed" + } + }, + "preflight-02": { + "status": "failed", + "cases": { + "PF-002-ENVIRONMENT": "failed", + "PF-002-ACCESS": "blocked", + "PF-002-EVIDENCE": "passed" + } + }, + "preflight-03": { + "status": "failed", + "cases": { + "PF-003-ENVIRONMENT": "failed", + "PF-003-ACCESS": "blocked", + "PF-003-EVIDENCE": "passed" + } + }, + "board-01": { + "status": "blocked", + "cases": { + "BD-001-THRESHOLDS": "blocked", + "BD-001-MUTATION": "blocked", + "BD-001-PERSISTENCE": "blocked", + "BD-001-ACCESS-RECOVERY": "blocked" + } + }, + "board-02": { + "status": "blocked", + "cases": { + "BD-002-THRESHOLDS": "blocked", + "BD-002-MUTATION": "blocked", + "BD-002-PERSISTENCE": "blocked", + "BD-002-ACCESS-RECOVERY": "blocked" + } + }, + "board-03": { + "status": "blocked", + "cases": { + "BD-003-THRESHOLDS": "blocked", + "BD-003-MUTATION": "blocked", + "BD-003-PERSISTENCE": "blocked", + "BD-003-ACCESS-RECOVERY": "blocked" + } + }, + "board-04": { + "status": "blocked", + "cases": { + "BD-004-THRESHOLDS": "blocked", + "BD-004-MUTATION": "blocked", + "BD-004-PERSISTENCE": "blocked", + "BD-004-ACCESS-RECOVERY": "blocked" + } + }, + "board-05": { + "status": "blocked", + "cases": { + "BD-005-THRESHOLDS": "blocked", + "BD-005-MUTATION": "blocked", + "BD-005-PERSISTENCE": "blocked", + "BD-005-ACCESS-RECOVERY": "blocked" + } + }, + "board-06": { + "status": "blocked", + "cases": { + "BD-006-THRESHOLDS": "blocked", + "BD-006-MUTATION": "blocked", + "BD-006-PERSISTENCE": "blocked", + "BD-006-ACCESS-RECOVERY": "blocked" + } + }, + "board-07": { + "status": "blocked", + "cases": { + "BD-007-THRESHOLDS": "blocked", + "BD-007-MUTATION": "blocked", + "BD-007-PERSISTENCE": "blocked", + "BD-007-ACCESS-RECOVERY": "blocked" + } + }, + "board-08": { + "status": "blocked", + "cases": { + "BD-008-THRESHOLDS": "blocked", + "BD-008-MUTATION": "blocked", + "BD-008-PERSISTENCE": "blocked", + "BD-008-ACCESS-RECOVERY": "blocked" + } + }, + "board-09": { + "status": "blocked", + "cases": { + "BD-009-THRESHOLDS": "blocked", + "BD-009-MUTATION": "blocked", + "BD-009-PERSISTENCE": "blocked", + "BD-009-ACCESS-RECOVERY": "blocked" + } + }, + "widgets-01": { + "status": "blocked", + "cases": { + "WG-001-RENDER-SIZE": "blocked", + "WG-001-STATES-RECOVERY": "blocked", + "WG-001-OPTIONS-PERSISTENCE": "blocked", + "WG-001-ACCESS-READONLY": "blocked", + "WG-001-EVIDENCE": "blocked" + } + }, + "widgets-02": { + "status": "blocked", + "cases": { + "WG-002-RENDER-SIZE": "blocked", + "WG-002-STATES-RECOVERY": "blocked", + "WG-002-OPTIONS-PERSISTENCE": "blocked", + "WG-002-ACCESS-READONLY": "blocked", + "WG-002-EVIDENCE": "blocked" + } + }, + "widgets-03": { + "status": "blocked", + "cases": { + "WG-003-RENDER-SIZE": "blocked", + "WG-003-STATES-RECOVERY": "blocked", + "WG-003-OPTIONS-PERSISTENCE": "blocked", + "WG-003-ACCESS-READONLY": "blocked", + "WG-003-EVIDENCE": "blocked" + } + }, + "widgets-04": { + "status": "blocked", + "cases": { + "WG-004-RENDER-SIZE": "blocked", + "WG-004-STATES-RECOVERY": "blocked", + "WG-004-OPTIONS-PERSISTENCE": "blocked", + "WG-004-ACCESS-READONLY": "blocked", + "WG-004-EVIDENCE": "blocked" + } + }, + "widgets-05": { + "status": "blocked", + "cases": { + "WG-005-RENDER-SIZE": "blocked", + "WG-005-STATES-RECOVERY": "blocked", + "WG-005-OPTIONS-PERSISTENCE": "blocked", + "WG-005-ACCESS-READONLY": "blocked", + "WG-005-EVIDENCE": "blocked" + } + }, + "widgets-06": { + "status": "blocked", + "cases": { + "WG-006-RENDER-SIZE": "blocked", + "WG-006-STATES-RECOVERY": "blocked", + "WG-006-OPTIONS-PERSISTENCE": "blocked", + "WG-006-ACCESS-READONLY": "blocked", + "WG-006-EVIDENCE": "blocked" + } + }, + "widgets-07": { + "status": "blocked", + "cases": { + "WG-007-RENDER-SIZE": "blocked", + "WG-007-STATES-RECOVERY": "blocked", + "WG-007-OPTIONS-PERSISTENCE": "blocked", + "WG-007-ACCESS-READONLY": "blocked", + "WG-007-EVIDENCE": "blocked" + } + }, + "widgets-08": { + "status": "blocked", + "cases": { + "WG-008-RENDER-SIZE": "blocked", + "WG-008-STATES-RECOVERY": "blocked", + "WG-008-OPTIONS-PERSISTENCE": "blocked", + "WG-008-ACCESS-READONLY": "blocked", + "WG-008-EVIDENCE": "blocked" + } + }, + "widgets-09": { + "status": "blocked", + "cases": { + "WG-009-RENDER-SIZE": "blocked", + "WG-009-STATES-RECOVERY": "blocked", + "WG-009-OPTIONS-PERSISTENCE": "blocked", + "WG-009-ACCESS-READONLY": "blocked", + "WG-009-EVIDENCE": "blocked" + } + }, + "widgets-10": { + "status": "blocked", + "cases": { + "WG-010-RENDER-SIZE": "blocked", + "WG-010-STATES-RECOVERY": "blocked", + "WG-010-OPTIONS-PERSISTENCE": "blocked", + "WG-010-ACCESS-READONLY": "blocked", + "WG-010-EVIDENCE": "blocked" + } + }, + "widgets-11": { + "status": "blocked", + "cases": { + "WG-011-RENDER-SIZE": "blocked", + "WG-011-STATES-RECOVERY": "blocked", + "WG-011-OPTIONS-PERSISTENCE": "blocked", + "WG-011-ACCESS-READONLY": "blocked", + "WG-011-EVIDENCE": "blocked" + } + }, + "widgets-12": { + "status": "blocked", + "cases": { + "WG-012-RENDER-SIZE": "blocked", + "WG-012-STATES-RECOVERY": "blocked", + "WG-012-OPTIONS-PERSISTENCE": "blocked", + "WG-012-ACCESS-READONLY": "blocked", + "WG-012-EVIDENCE": "blocked" + } + }, + "core-v2-01": { + "status": "blocked", + "cases": { + "CV-001-HAPPY-PATH": "blocked", + "CV-001-MUTATION-PERSISTENCE": "blocked", + "CV-001-ACCESSIBILITY-ACCESS": "blocked", + "CV-001-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-02": { + "status": "blocked", + "cases": { + "CV-002-HAPPY-PATH": "blocked", + "CV-002-MUTATION-PERSISTENCE": "blocked", + "CV-002-ACCESSIBILITY-ACCESS": "blocked", + "CV-002-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-03": { + "status": "blocked", + "cases": { + "CV-003-HAPPY-PATH": "blocked", + "CV-003-MUTATION-PERSISTENCE": "blocked", + "CV-003-ACCESSIBILITY-ACCESS": "blocked", + "CV-003-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-04": { + "status": "blocked", + "cases": { + "CV-004-HAPPY-PATH": "blocked", + "CV-004-MUTATION-PERSISTENCE": "blocked", + "CV-004-ACCESSIBILITY-ACCESS": "blocked", + "CV-004-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-05": { + "status": "blocked", + "cases": { + "CV-005-HAPPY-PATH": "blocked", + "CV-005-MUTATION-PERSISTENCE": "blocked", + "CV-005-ACCESSIBILITY-ACCESS": "blocked", + "CV-005-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-06": { + "status": "blocked", + "cases": { + "CV-006-HAPPY-PATH": "blocked", + "CV-006-MUTATION-PERSISTENCE": "blocked", + "CV-006-ACCESSIBILITY-ACCESS": "blocked", + "CV-006-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-07": { + "status": "blocked", + "cases": { + "CV-007-HAPPY-PATH": "blocked", + "CV-007-MUTATION-PERSISTENCE": "blocked", + "CV-007-ACCESSIBILITY-ACCESS": "blocked", + "CV-007-DEGRADED-RECOVERY": "blocked" + } + }, + "core-v2-08": { + "status": "blocked", + "cases": { + "CV-008-HAPPY-PATH": "blocked", + "CV-008-MUTATION-PERSISTENCE": "blocked", + "CV-008-ACCESSIBILITY-ACCESS": "blocked", + "CV-008-DEGRADED-RECOVERY": "blocked" + } + }, + "whole-product-01": { + "status": "blocked", + "cases": { + "WP-001-JOURNEY": "blocked", + "WP-001-THRESHOLDS": "blocked", + "WP-001-PERSISTENCE-ACCESS": "blocked", + "WP-001-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-02": { + "status": "blocked", + "cases": { + "WP-002-JOURNEY": "blocked", + "WP-002-THRESHOLDS": "blocked", + "WP-002-PERSISTENCE-ACCESS": "blocked", + "WP-002-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-03": { + "status": "blocked", + "cases": { + "WP-003-JOURNEY": "blocked", + "WP-003-THRESHOLDS": "blocked", + "WP-003-PERSISTENCE-ACCESS": "blocked", + "WP-003-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-04": { + "status": "blocked", + "cases": { + "WP-004-JOURNEY": "blocked", + "WP-004-THRESHOLDS": "blocked", + "WP-004-PERSISTENCE-ACCESS": "blocked", + "WP-004-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-05": { + "status": "blocked", + "cases": { + "WP-005-JOURNEY": "blocked", + "WP-005-THRESHOLDS": "blocked", + "WP-005-PERSISTENCE-ACCESS": "blocked", + "WP-005-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-06": { + "status": "blocked", + "cases": { + "WP-006-JOURNEY": "blocked", + "WP-006-THRESHOLDS": "blocked", + "WP-006-PERSISTENCE-ACCESS": "blocked", + "WP-006-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-07": { + "status": "blocked", + "cases": { + "WP-007-JOURNEY": "blocked", + "WP-007-THRESHOLDS": "blocked", + "WP-007-PERSISTENCE-ACCESS": "blocked", + "WP-007-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-08": { + "status": "blocked", + "cases": { + "WP-008-JOURNEY": "blocked", + "WP-008-THRESHOLDS": "blocked", + "WP-008-PERSISTENCE-ACCESS": "blocked", + "WP-008-FAILURE-EVIDENCE": "blocked" + } + }, + "whole-product-09": { + "status": "blocked", + "cases": { + "WP-009-JOURNEY": "blocked", + "WP-009-THRESHOLDS": "blocked", + "WP-009-PERSISTENCE-ACCESS": "blocked", + "WP-009-FAILURE-EVIDENCE": "blocked" + } + }, + "performance-01": { + "status": "blocked", + "cases": { + "PE-001-BASELINE": "blocked", + "PE-001-THRESHOLD": "blocked", + "PE-001-STRESS": "blocked", + "PE-001-RECOVERY-EVIDENCE": "blocked" + } + }, + "performance-02": { + "status": "blocked", + "cases": { + "PE-002-BASELINE": "blocked", + "PE-002-THRESHOLD": "blocked", + "PE-002-STRESS": "blocked", + "PE-002-RECOVERY-EVIDENCE": "blocked" + } + }, + "performance-03": { + "status": "blocked", + "cases": { + "PE-003-BASELINE": "blocked", + "PE-003-THRESHOLD": "blocked", + "PE-003-STRESS": "blocked", + "PE-003-RECOVERY-EVIDENCE": "blocked" + } + }, + "performance-04": { + "status": "blocked", + "cases": { + "PE-004-BASELINE": "blocked", + "PE-004-THRESHOLD": "blocked", + "PE-004-STRESS": "blocked", + "PE-004-RECOVERY-EVIDENCE": "blocked" + } + } + } +} diff --git a/qa/release-v2/master-report.md b/qa/release-v2/master-report.md new file mode 100644 index 000000000..e17da9eea --- /dev/null +++ b/qa/release-v2/master-report.md @@ -0,0 +1,835 @@ +# Release-v2 QA master report + +Generated: 2026-08-31T18:42:05.251Z + +## Decision: NO-GO + +GO requires zero P0/P1 findings and zero critical gaps. Current critical gaps: **186**. + +## Coverage + +| Measure | Passed | Failed | Blocked | Not reached | Total | +| --- | ---: | ---: | ---: | ---: | ---: | +| Packets | 0 | 3 | 42 | 0 | 45 | +| Cases | 3 | 3 | 183 | 0 | 189 | + +| Wave | Passed | Total | +| --- | ---: | ---: | +| preflight | 0 | 3 | +| board | 0 | 9 | +| widgets | 0 | 12 | +| core-v2 | 0 | 8 | +| whole-product | 0 | 9 | +| performance | 0 | 4 | + +## Coverage by required axis + +### Pull request + +| PR | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| #6356 | Custom Widgets | 0 | 0 | 7 | 0 | 7 | +| #6450 | responsive layouts | 0 | 0 | 5 | 0 | 5 | +| #6482 | Assistant | 0 | 0 | 7 | 0 | 7 | +| #6502 | widget modernization | 0 | 0 | 23 | 0 | 23 | +| #6503 | dnd-kit grid | 0 | 0 | 12 | 0 | 12 | +| #6545 | release-v2 rollup | 0 | 3 | 12 | 0 | 15 | +| #6555 | widget performance | 0 | 0 | 18 | 0 | 18 | +| #6569 | onboarding | 0 | 0 | 4 | 0 | 4 | + +### Feature + +| Feature / agent | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| board-01 | 24-column placement and resize | 0 | 0 | 1 | 0 | 1 | +| board-02 | Scrollable canvas and collapse states | 0 | 0 | 1 | 0 | 1 | +| board-03 | Dense collision handling | 0 | 0 | 1 | 0 | 1 | +| board-04 | Nested containers | 0 | 0 | 1 | 0 | 1 | +| board-05 | Responsive layout boundaries | 0 | 0 | 1 | 0 | 1 | +| board-06 | Icons, bookmarks, and compact layout | 0 | 0 | 1 | 0 | 1 | +| board-07 | Permission-aware editing | 0 | 0 | 1 | 0 | 1 | +| board-08 | Board import and export | 0 | 0 | 1 | 0 | 1 | +| board-09 | Keyboard-only grid operation | 0 | 0 | 1 | 0 | 1 | +| core-v2-01 | Custom widget authoring | 0 | 0 | 1 | 0 | 1 | +| core-v2-02 | Assistant tool flow | 0 | 0 | 1 | 0 | 1 | +| core-v2-03 | Onboarding happy path | 0 | 0 | 1 | 0 | 1 | +| core-v2-04 | Authentication and session transitions | 0 | 0 | 1 | 0 | 1 | +| core-v2-05 | Integration management | 0 | 0 | 1 | 0 | 1 | +| core-v2-06 | Search, menus, and dialogs | 0 | 0 | 1 | 0 | 1 | +| core-v2-07 | Read-only enforcement | 0 | 0 | 1 | 0 | 1 | +| core-v2-08 | Failure and recovery states | 0 | 0 | 1 | 0 | 1 | +| performance-01 | Cold and warm board load | 0 | 0 | 1 | 0 | 1 | +| performance-02 | Grid interaction responsiveness | 0 | 0 | 1 | 0 | 1 | +| performance-03 | Widget network and render budget | 0 | 0 | 1 | 0 | 1 | +| performance-04 | Long-session stability | 0 | 0 | 1 | 0 | 1 | +| preflight-01 | Candidate identity and services | 0 | 1 | 0 | 0 | 1 | +| preflight-02 | Fixture and persona access | 0 | 1 | 0 | 0 | 1 | +| preflight-03 | Browser evidence hygiene | 0 | 1 | 0 | 0 | 1 | +| whole-product-01 | Admin day-one journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-02 | Owner customization journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-03 | Editor daily journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-04 | Viewer and outsider boundaries | 0 | 0 | 1 | 0 | 1 | +| whole-product-05 | Mobile journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-06 | Media operator journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-07 | Infrastructure operator journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-08 | Creator and Assistant journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-09 | Accessibility and destructive-action pass | 0 | 0 | 1 | 0 | 1 | +| widgets-01 | Time and environment | 0 | 0 | 1 | 0 | 1 | +| widgets-02 | Apps, embeds, video, game status, and stocks | 0 | 0 | 1 | 0 | 1 | +| widgets-03 | Notes, bookmarks, feeds, and timetable | 0 | 0 | 1 | 0 | 1 | +| widgets-04 | Downloads, containers, indexers, and DNS | 0 | 0 | 1 | 0 | 1 | +| widgets-05 | Smart home, health, and system telemetry | 0 | 0 | 1 | 0 | 1 | +| widgets-06 | Network availability and operations | 0 | 0 | 1 | 0 | 1 | +| widgets-07 | Beszel and update monitoring | 0 | 0 | 1 | 0 | 1 | +| widgets-08 | Power, VPN, speed, routing, and analytics | 0 | 0 | 1 | 0 | 1 | +| widgets-09 | Media overview and requests | 0 | 0 | 1 | 0 | 1 | +| widgets-10 | Media activity, Immich, and audio | 0 | 0 | 1 | 0 | 1 | +| widgets-11 | Documents, patching, media services, and releases | 0 | 0 | 1 | 0 | 1 | +| widgets-12 | Coolify, ArchiveTeam, Custom API, and Assistant | 0 | 0 | 1 | 0 | 1 | + +### Widget + +| Widget kind | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| airQuality | widgets-01 | 0 | 0 | 9 | 0 | 9 | +| anchorNote | widgets-03 | 0 | 0 | 9 | 0 | 9 | +| app | widgets-02 | 0 | 0 | 9 | 0 | 9 | +| archiveTeamWarrior | widgets-12 | 0 | 0 | 9 | 0 | 9 | +| assistant | widgets-12 | 0 | 0 | 9 | 0 | 9 | +| audioStats | widgets-10 | 0 | 0 | 9 | 0 | 9 | +| bazarr | widgets-11 | 0 | 0 | 9 | 0 | 9 | +| beszelAlerts | widgets-07 | 0 | 0 | 9 | 0 | 9 | +| beszelSystemGrid | widgets-07 | 0 | 0 | 9 | 0 | 9 | +| beszelSystemStats | widgets-07 | 0 | 0 | 9 | 0 | 9 | +| beszelSystemTable | widgets-07 | 0 | 0 | 9 | 0 | 9 | +| bookmarks | widgets-03 | 0 | 0 | 9 | 0 | 9 | +| calendar | widgets-09 | 0 | 0 | 9 | 0 | 9 | +| clock | widgets-01 | 0 | 0 | 9 | 0 | 9 | +| coolify | widgets-12 | 0 | 0 | 9 | 0 | 9 | +| countdown | widgets-01 | 0 | 0 | 9 | 0 | 9 | +| customApi | widgets-12 | 0 | 0 | 9 | 0 | 9 | +| dnsHoleControls | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| dnsHoleSummary | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| dockerContainers | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| downloads | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| firewall | widgets-06 | 0 | 0 | 9 | 0 | 9 | +| healthMonitoring | widgets-05 | 0 | 0 | 9 | 0 | 9 | +| iframe | widgets-02 | 0 | 0 | 9 | 0 | 9 | +| immich-albumCarousel | widgets-10 | 0 | 0 | 9 | 0 | 9 | +| immich-serverStats | widgets-10 | 0 | 0 | 9 | 0 | 9 | +| indexerManager | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| mediaMissing | widgets-09 | 0 | 0 | 9 | 0 | 9 | +| mediaReleases | widgets-10 | 0 | 0 | 9 | 0 | 9 | +| mediaRequests-requestList | widgets-09 | 0 | 0 | 9 | 0 | 9 | +| mediaRequests-requestStats | widgets-09 | 0 | 0 | 9 | 0 | 9 | +| mediaServer | widgets-09 | 0 | 0 | 9 | 0 | 9 | +| mediaTranscoding | widgets-10 | 0 | 0 | 9 | 0 | 9 | +| minecraftServerStatus | widgets-02 | 0 | 0 | 9 | 0 | 9 | +| networkControllerStatus | widgets-06 | 0 | 0 | 9 | 0 | 9 | +| networkControllerSummary | widgets-06 | 0 | 0 | 9 | 0 | 9 | +| notebook | widgets-03 | 0 | 0 | 9 | 0 | 9 | +| notifications | widgets-06 | 0 | 0 | 9 | 0 | 9 | +| paperlessNgx | widgets-11 | 0 | 0 | 9 | 0 | 9 | +| patchmon | widgets-11 | 0 | 0 | 9 | 0 | 9 | +| releases | widgets-11 | 0 | 0 | 9 | 0 | 9 | +| rssFeed | widgets-03 | 0 | 0 | 9 | 0 | 9 | +| smartHome-entityState | widgets-05 | 0 | 0 | 9 | 0 | 9 | +| smartHome-executeAutomation | widgets-05 | 0 | 0 | 9 | 0 | 9 | +| speedtestTracker | widgets-08 | 0 | 0 | 9 | 0 | 9 | +| stockPrice | widgets-02 | 0 | 0 | 9 | 0 | 9 | +| systemDisks | widgets-05 | 0 | 0 | 9 | 0 | 9 | +| systemResources | widgets-05 | 0 | 0 | 9 | 0 | 9 | +| timer | widgets-01 | 0 | 0 | 9 | 0 | 9 | +| timetable | widgets-03 | 0 | 0 | 9 | 0 | 9 | +| tracearr | widgets-11 | 0 | 0 | 9 | 0 | 9 | +| traefik | widgets-08 | 0 | 0 | 9 | 0 | 9 | +| umami | widgets-08 | 0 | 0 | 9 | 0 | 9 | +| ups | widgets-08 | 0 | 0 | 9 | 0 | 9 | +| uptimeKuma | widgets-06 | 0 | 0 | 9 | 0 | 9 | +| video | widgets-02 | 0 | 0 | 9 | 0 | 9 | +| vpn | widgets-08 | 0 | 0 | 9 | 0 | 9 | +| weather | widgets-01 | 0 | 0 | 9 | 0 | 9 | +| wud | widgets-07 | 0 | 0 | 9 | 0 | 9 | + +### Size threshold + +High-risk widgets require every width 1–24 × every height 1–6 at all assigned mobile, breakpoint-edge, and desktop viewports. Other widgets require minimum, canonical, wide, tall, maximum, overflow, and behavior-changing threshold checks. + +| Widget size threshold | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| airQuality | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| anchorNote | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| app | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| archiveTeamWarrior | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| assistant | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| audioStats | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| bazarr | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| beszelAlerts | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| beszelSystemGrid | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| beszelSystemStats | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| beszelSystemTable | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| bookmarks | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| calendar | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| clock | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| coolify | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| countdown | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| customApi | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| dnsHoleControls | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| dnsHoleSummary | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| dockerContainers | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| downloads | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| firewall | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| healthMonitoring | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| iframe | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| immich-albumCarousel | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| immich-serverStats | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| indexerManager | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| mediaMissing | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| mediaReleases | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| mediaRequests-requestList | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| mediaRequests-requestStats | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| mediaServer | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| mediaTranscoding | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| minecraftServerStatus | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| networkControllerStatus | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| networkControllerSummary | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| notebook | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| notifications | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| paperlessNgx | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| patchmon | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| releases | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| rssFeed | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| smartHome-entityState | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| smartHome-executeAutomation | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| speedtestTracker | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| stockPrice | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| systemDisks | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| systemResources | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| timer | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| timetable | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| tracearr | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| traefik | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| umami | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| ups | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| uptimeKuma | every width 1-24 × every height 1-6 | 0 | 0 | 9 | 0 | 9 | +| video | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| vpn | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| weather | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | +| wud | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | 0 | 0 | 9 | 0 | 9 | + +### Viewport + +| Viewport | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| boundary-1279 | board-05 | 0 | 0 | 13 | 0 | 13 | +| boundary-767 | board-05 | 0 | 0 | 14 | 0 | 14 | +| boundary-768 | board-05 | 0 | 0 | 14 | 0 | 14 | +| desktop-1280 | board-02 | 0 | 0 | 18 | 0 | 18 | +| desktop-1440 | preflight-01 | 0 | 2 | 31 | 0 | 33 | +| desktop-1920 | preflight-03 | 0 | 1 | 17 | 0 | 18 | +| mobile-320 | preflight-03 | 0 | 1 | 20 | 0 | 21 | +| mobile-375 | board-02 | 0 | 0 | 22 | 0 | 22 | +| tablet-1024 | board-03 | 0 | 0 | 15 | 0 | 15 | + +### Persona + +| Persona | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| Ash Assistant | widgets-12 | 0 | 0 | 3 | 0 | 3 | +| Avery Admin | preflight-01 | 0 | 2 | 4 | 0 | 6 | +| Brooke Minimalist | board-06 | 0 | 0 | 2 | 0 | 2 | +| Casey Chaos | preflight-03 | 0 | 1 | 4 | 0 | 5 | +| Cora Creator | widgets-02 | 0 | 0 | 5 | 0 | 5 | +| Eden Editor | board-02 | 0 | 0 | 3 | 0 | 3 | +| Ingrid Infra | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| Kira Keyboard | board-09 | 0 | 0 | 3 | 0 | 3 | +| Maya Media | widgets-09 | 0 | 0 | 4 | 0 | 4 | +| Morgan Mobile | board-05 | 0 | 0 | 2 | 0 | 2 | +| Nolan Outsider | preflight-02 | 0 | 1 | 3 | 0 | 4 | +| Nora Newcomer | core-v2-03 | 0 | 0 | 2 | 0 | 2 | +| Rowan Owner | preflight-02 | 0 | 1 | 5 | 0 | 6 | +| Vivian Viewer | preflight-02 | 0 | 1 | 3 | 0 | 4 | + +### Permission + +| Permission boundary | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| Ash Assistant | widgets-12 | 0 | 0 | 3 | 0 | 3 | +| Avery Admin | preflight-01 | 0 | 0 | 6 | 0 | 6 | +| Brooke Minimalist | board-06 | 0 | 0 | 2 | 0 | 2 | +| Casey Chaos | preflight-03 | 0 | 0 | 5 | 0 | 5 | +| Cora Creator | widgets-02 | 0 | 0 | 5 | 0 | 5 | +| Eden Editor | board-02 | 0 | 0 | 3 | 0 | 3 | +| Ingrid Infra | widgets-04 | 0 | 0 | 9 | 0 | 9 | +| Kira Keyboard | board-09 | 0 | 0 | 3 | 0 | 3 | +| Maya Media | widgets-09 | 0 | 0 | 4 | 0 | 4 | +| Morgan Mobile | board-05 | 0 | 0 | 2 | 0 | 2 | +| Nolan Outsider | preflight-02 | 0 | 0 | 4 | 0 | 4 | +| Nora Newcomer | core-v2-03 | 0 | 0 | 2 | 0 | 2 | +| profile:degraded | preflight-01 | 0 | 0 | 23 | 0 | 23 | +| profile:main-readonly | preflight-01 | 0 | 0 | 7 | 0 | 7 | +| profile:main-writable | preflight-01 | 0 | 0 | 42 | 0 | 42 | +| profile:onboarding-fresh | preflight-01 | 0 | 0 | 4 | 0 | 4 | +| Rowan Owner | preflight-02 | 0 | 0 | 6 | 0 | 6 | +| Vivian Viewer | preflight-02 | 0 | 0 | 4 | 0 | 4 | + +### Mutation + +| Mutation area | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| board-01 | 24-column placement and resize | 0 | 0 | 1 | 0 | 1 | +| board-02 | Scrollable canvas and collapse states | 0 | 0 | 1 | 0 | 1 | +| board-03 | Dense collision handling | 0 | 0 | 1 | 0 | 1 | +| board-04 | Nested containers | 0 | 0 | 1 | 0 | 1 | +| board-05 | Responsive layout boundaries | 0 | 0 | 1 | 0 | 1 | +| board-06 | Icons, bookmarks, and compact layout | 0 | 0 | 1 | 0 | 1 | +| board-07 | Permission-aware editing | 0 | 0 | 1 | 0 | 1 | +| board-08 | Board import and export | 0 | 0 | 1 | 0 | 1 | +| board-09 | Keyboard-only grid operation | 0 | 0 | 1 | 0 | 1 | +| core-v2-01 | Custom widget authoring | 0 | 0 | 1 | 0 | 1 | +| core-v2-02 | Assistant tool flow | 0 | 0 | 1 | 0 | 1 | +| core-v2-03 | Onboarding happy path | 0 | 0 | 1 | 0 | 1 | +| core-v2-04 | Authentication and session transitions | 0 | 0 | 1 | 0 | 1 | +| core-v2-05 | Integration management | 0 | 0 | 1 | 0 | 1 | +| core-v2-06 | Search, menus, and dialogs | 0 | 0 | 1 | 0 | 1 | +| core-v2-07 | Read-only enforcement | 0 | 0 | 1 | 0 | 1 | +| core-v2-08 | Failure and recovery states | 0 | 0 | 1 | 0 | 1 | +| performance-01 | Cold and warm board load | 0 | 0 | 1 | 0 | 1 | +| performance-02 | Grid interaction responsiveness | 0 | 0 | 1 | 0 | 1 | +| performance-03 | Widget network and render budget | 0 | 0 | 1 | 0 | 1 | +| performance-04 | Long-session stability | 0 | 0 | 1 | 0 | 1 | +| preflight-01 | Candidate identity and services | 0 | 1 | 0 | 0 | 1 | +| preflight-02 | Fixture and persona access | 0 | 1 | 0 | 0 | 1 | +| preflight-03 | Browser evidence hygiene | 0 | 1 | 0 | 0 | 1 | +| whole-product-01 | Admin day-one journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-02 | Owner customization journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-03 | Editor daily journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-04 | Viewer and outsider boundaries | 0 | 0 | 1 | 0 | 1 | +| whole-product-05 | Mobile journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-06 | Media operator journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-07 | Infrastructure operator journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-08 | Creator and Assistant journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-09 | Accessibility and destructive-action pass | 0 | 0 | 1 | 0 | 1 | +| widgets-01 | Time and environment | 0 | 0 | 1 | 0 | 1 | +| widgets-02 | Apps, embeds, video, game status, and stocks | 0 | 0 | 1 | 0 | 1 | +| widgets-03 | Notes, bookmarks, feeds, and timetable | 0 | 0 | 1 | 0 | 1 | +| widgets-04 | Downloads, containers, indexers, and DNS | 0 | 0 | 1 | 0 | 1 | +| widgets-05 | Smart home, health, and system telemetry | 0 | 0 | 1 | 0 | 1 | +| widgets-06 | Network availability and operations | 0 | 0 | 1 | 0 | 1 | +| widgets-07 | Beszel and update monitoring | 0 | 0 | 1 | 0 | 1 | +| widgets-08 | Power, VPN, speed, routing, and analytics | 0 | 0 | 1 | 0 | 1 | +| widgets-09 | Media overview and requests | 0 | 0 | 1 | 0 | 1 | +| widgets-10 | Media activity, Immich, and audio | 0 | 0 | 1 | 0 | 1 | +| widgets-11 | Documents, patching, media services, and releases | 0 | 0 | 1 | 0 | 1 | +| widgets-12 | Coolify, ArchiveTeam, Custom API, and Assistant | 0 | 0 | 1 | 0 | 1 | + +### State and recovery + +| State / recovery area | Detail | Passed | Failed | Blocked | Not reached | Total | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| board-01 | 24-column placement and resize | 0 | 0 | 1 | 0 | 1 | +| board-02 | Scrollable canvas and collapse states | 0 | 0 | 1 | 0 | 1 | +| board-03 | Dense collision handling | 0 | 0 | 1 | 0 | 1 | +| board-04 | Nested containers | 0 | 0 | 1 | 0 | 1 | +| board-05 | Responsive layout boundaries | 0 | 0 | 1 | 0 | 1 | +| board-06 | Icons, bookmarks, and compact layout | 0 | 0 | 1 | 0 | 1 | +| board-07 | Permission-aware editing | 0 | 0 | 1 | 0 | 1 | +| board-08 | Board import and export | 0 | 0 | 1 | 0 | 1 | +| board-09 | Keyboard-only grid operation | 0 | 0 | 1 | 0 | 1 | +| core-v2-01 | Custom widget authoring | 0 | 0 | 1 | 0 | 1 | +| core-v2-02 | Assistant tool flow | 0 | 0 | 1 | 0 | 1 | +| core-v2-03 | Onboarding happy path | 0 | 0 | 1 | 0 | 1 | +| core-v2-04 | Authentication and session transitions | 0 | 0 | 1 | 0 | 1 | +| core-v2-05 | Integration management | 0 | 0 | 1 | 0 | 1 | +| core-v2-06 | Search, menus, and dialogs | 0 | 0 | 1 | 0 | 1 | +| core-v2-07 | Read-only enforcement | 0 | 0 | 1 | 0 | 1 | +| core-v2-08 | Failure and recovery states | 0 | 0 | 1 | 0 | 1 | +| performance-01 | Cold and warm board load | 0 | 0 | 1 | 0 | 1 | +| performance-02 | Grid interaction responsiveness | 0 | 0 | 1 | 0 | 1 | +| performance-03 | Widget network and render budget | 0 | 0 | 1 | 0 | 1 | +| performance-04 | Long-session stability | 0 | 0 | 1 | 0 | 1 | +| preflight-01 | Candidate identity and services | 1 | 0 | 0 | 0 | 1 | +| preflight-02 | Fixture and persona access | 1 | 0 | 0 | 0 | 1 | +| preflight-03 | Browser evidence hygiene | 1 | 0 | 0 | 0 | 1 | +| whole-product-01 | Admin day-one journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-02 | Owner customization journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-03 | Editor daily journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-04 | Viewer and outsider boundaries | 0 | 0 | 1 | 0 | 1 | +| whole-product-05 | Mobile journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-06 | Media operator journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-07 | Infrastructure operator journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-08 | Creator and Assistant journey | 0 | 0 | 1 | 0 | 1 | +| whole-product-09 | Accessibility and destructive-action pass | 0 | 0 | 1 | 0 | 1 | +| widgets-01 | Time and environment | 0 | 0 | 1 | 0 | 1 | +| widgets-02 | Apps, embeds, video, game status, and stocks | 0 | 0 | 1 | 0 | 1 | +| widgets-03 | Notes, bookmarks, feeds, and timetable | 0 | 0 | 1 | 0 | 1 | +| widgets-04 | Downloads, containers, indexers, and DNS | 0 | 0 | 1 | 0 | 1 | +| widgets-05 | Smart home, health, and system telemetry | 0 | 0 | 1 | 0 | 1 | +| widgets-06 | Network availability and operations | 0 | 0 | 1 | 0 | 1 | +| widgets-07 | Beszel and update monitoring | 0 | 0 | 1 | 0 | 1 | +| widgets-08 | Power, VPN, speed, routing, and analytics | 0 | 0 | 1 | 0 | 1 | +| widgets-09 | Media overview and requests | 0 | 0 | 1 | 0 | 1 | +| widgets-10 | Media activity, Immich, and audio | 0 | 0 | 1 | 0 | 1 | +| widgets-11 | Documents, patching, media services, and releases | 0 | 0 | 1 | 0 | 1 | +| widgets-12 | Coolify, ArchiveTeam, Custom API, and Assistant | 0 | 0 | 1 | 0 | 1 | + +## Performance measurements + +| Agent | Measurement | Value | Threshold | Status | Evidence | +| --- | --- | --- | --- | --- | --- | +| — | No measurement recorded | — | — | not-reached | — | + +### Performance limitations + +| Agent | Limitation | +| --- | --- | +| host-runtime | The host's fs.inotify.max_user_instances=128 is too low for concurrent native Watchpack watchers. Spawned QA apps keep Turbopack and use 1000ms Watchpack polling; filesystem changes can take up to one polling interval to reach HMR. | +| preflight-01 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| preflight-01 | No credentials were submitted and no product state was mutated. | +| preflight-01 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| preflight-02 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| preflight-02 | No credentials were submitted and no product state was mutated. | +| preflight-02 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| preflight-03 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| preflight-03 | No credentials were submitted and no product state was mutated. | +| preflight-03 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-01 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-01 | No credentials were submitted and no product state was mutated. | +| board-01 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-02 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-02 | No credentials were submitted and no product state was mutated. | +| board-02 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-03 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-03 | No credentials were submitted and no product state was mutated. | +| board-03 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-04 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-04 | No credentials were submitted and no product state was mutated. | +| board-04 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-05 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-05 | No credentials were submitted and no product state was mutated. | +| board-05 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-06 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-06 | No credentials were submitted and no product state was mutated. | +| board-06 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-07 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-07 | No credentials were submitted and no product state was mutated. | +| board-07 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-08 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-08 | No credentials were submitted and no product state was mutated. | +| board-08 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| board-09 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| board-09 | No credentials were submitted and no product state was mutated. | +| board-09 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| widgets-01 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-01 | No credentials were submitted and no product state was mutated. | +| widgets-01 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| widgets-02 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-02 | No credentials were submitted and no product state was mutated. | +| widgets-02 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| widgets-03 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-03 | No credentials were submitted and no product state was mutated. | +| widgets-03 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-04 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-04 | No credentials were submitted and no product state was mutated. | +| widgets-04 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| widgets-05 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-05 | No credentials were submitted and no product state was mutated. | +| widgets-05 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-06 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-06 | No credentials were submitted and no product state was mutated. | +| widgets-06 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-07 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-07 | No credentials were submitted and no product state was mutated. | +| widgets-07 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-08 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-08 | No credentials were submitted and no product state was mutated. | +| widgets-08 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| widgets-09 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-09 | No credentials were submitted and no product state was mutated. | +| widgets-09 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-10 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-10 | No credentials were submitted and no product state was mutated. | +| widgets-10 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-11 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-11 | No credentials were submitted and no product state was mutated. | +| widgets-11 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| widgets-12 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| widgets-12 | No credentials were submitted and no product state was mutated. | +| widgets-12 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-01 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-01 | No credentials were submitted and no product state was mutated. | +| core-v2-01 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-02 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-02 | No credentials were submitted and no product state was mutated. | +| core-v2-02 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-03 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-03 | No credentials were submitted and no product state was mutated. | +| core-v2-03 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-04 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-04 | No credentials were submitted and no product state was mutated. | +| core-v2-04 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-05 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-05 | No credentials were submitted and no product state was mutated. | +| core-v2-05 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-06 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-06 | No credentials were submitted and no product state was mutated. | +| core-v2-06 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-07 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-07 | No credentials were submitted and no product state was mutated. | +| core-v2-07 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| core-v2-08 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| core-v2-08 | No credentials were submitted and no product state was mutated. | +| core-v2-08 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-01 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-01 | No credentials were submitted and no product state was mutated. | +| whole-product-01 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-02 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-02 | No credentials were submitted and no product state was mutated. | +| whole-product-02 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-03 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-03 | No credentials were submitted and no product state was mutated. | +| whole-product-03 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | +| whole-product-04 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-04 | No credentials were submitted and no product state was mutated. | +| whole-product-04 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-05 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-05 | No credentials were submitted and no product state was mutated. | +| whole-product-05 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-06 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-06 | No credentials were submitted and no product state was mutated. | +| whole-product-06 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-07 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-07 | No credentials were submitted and no product state was mutated. | +| whole-product-07 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-08 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-08 | No credentials were submitted and no product state was mutated. | +| whole-product-08 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| whole-product-09 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| whole-product-09 | No credentials were submitted and no product state was mutated. | +| whole-product-09 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| performance-01 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| performance-01 | No credentials were submitted and no product state was mutated. | +| performance-01 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| performance-02 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| performance-02 | No credentials were submitted and no product state was mutated. | +| performance-02 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| performance-03 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| performance-03 | No credentials were submitted and no product state was mutated. | +| performance-03 | The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports. | +| performance-04 | The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked. | +| performance-04 | No credentials were submitted and no product state was mutated. | +| performance-04 | The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop. | + +## Independent reproduction results + +| Agent | Finding fingerprint | Reproducing agent | Outcome | Evidence | Notes | +| --- | --- | --- | --- | --- | --- | +| preflight-01 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-02 | reproduced | None | See preflight-02 for an independent Luna Max reproduction. | +| preflight-02 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| preflight-03 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-01 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-02 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-03 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-04 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-05 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-06 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-07 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-08 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| board-09 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-01 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-02 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-03 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-04 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-05 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-06 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-07 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-08 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-09 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-10 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-11 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| widgets-12 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-01 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-02 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-03 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-04 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-05 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-06 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-07 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| core-v2-08 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-01 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-02 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-03 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-04 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-05 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-06 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-07 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-08 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| whole-product-09 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| performance-01 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| performance-02 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| performance-03 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | +| performance-04 | release-v2-auth-locale-redirect-loop | qa-v2-preflight-01 | reproduced | None | See preflight-01 for an independent Luna Max reproduction. | + +## Severity totals + +| P0 | P1 | P2 | P3 | +| ---: | ---: | ---: | ---: | +| 0 | 1 | 0 | 0 | + +Deduplicated by explicit fingerprint, or by severity + area + title when no fingerprint is supplied. + +| Severity | Area | Finding | Packets | Cases | +| --- | --- | --- | --- | --- | +| P1 | routing/authentication | Locale rewrite causes an infinite browser redirect before the application renders | preflight-01, preflight-02, preflight-03, board-01, board-02, board-03, board-04, board-05, board-06, board-07, board-08, board-09, widgets-01, widgets-02, widgets-03, widgets-04, widgets-05, widgets-06, widgets-07, widgets-08, widgets-09, widgets-10, widgets-11, widgets-12, core-v2-01, core-v2-02, core-v2-03, core-v2-04, core-v2-05, core-v2-06, core-v2-07, core-v2-08, whole-product-01, whole-product-02, whole-product-03, whole-product-04, whole-product-05, whole-product-06, whole-product-07, whole-product-08, whole-product-09, performance-01, performance-02, performance-03, performance-04 | PF-001-ENVIRONMENT, PF-001-ACCESS, PF-002-ENVIRONMENT, PF-002-ACCESS, PF-003-ENVIRONMENT, PF-003-ACCESS, BD-001-THRESHOLDS, BD-001-MUTATION, BD-001-PERSISTENCE, BD-001-ACCESS-RECOVERY, BD-002-THRESHOLDS, BD-002-MUTATION, BD-002-PERSISTENCE, BD-002-ACCESS-RECOVERY, BD-003-THRESHOLDS, BD-003-MUTATION, BD-003-PERSISTENCE, BD-003-ACCESS-RECOVERY, BD-004-THRESHOLDS, BD-004-MUTATION, BD-004-PERSISTENCE, BD-004-ACCESS-RECOVERY, BD-005-THRESHOLDS, BD-005-MUTATION, BD-005-PERSISTENCE, BD-005-ACCESS-RECOVERY, BD-006-THRESHOLDS, BD-006-MUTATION, BD-006-PERSISTENCE, BD-006-ACCESS-RECOVERY, BD-007-THRESHOLDS, BD-007-MUTATION, BD-007-PERSISTENCE, BD-007-ACCESS-RECOVERY, BD-008-THRESHOLDS, BD-008-MUTATION, BD-008-PERSISTENCE, BD-008-ACCESS-RECOVERY, BD-009-THRESHOLDS, BD-009-MUTATION, BD-009-PERSISTENCE, BD-009-ACCESS-RECOVERY, WG-001-RENDER-SIZE, WG-001-STATES-RECOVERY, WG-001-OPTIONS-PERSISTENCE, WG-001-ACCESS-READONLY, WG-001-EVIDENCE, WG-002-RENDER-SIZE, WG-002-STATES-RECOVERY, WG-002-OPTIONS-PERSISTENCE, WG-002-ACCESS-READONLY, WG-002-EVIDENCE, WG-003-RENDER-SIZE, WG-003-STATES-RECOVERY, WG-003-OPTIONS-PERSISTENCE, WG-003-ACCESS-READONLY, WG-003-EVIDENCE, WG-004-RENDER-SIZE, WG-004-STATES-RECOVERY, WG-004-OPTIONS-PERSISTENCE, WG-004-ACCESS-READONLY, WG-004-EVIDENCE, WG-005-RENDER-SIZE, WG-005-STATES-RECOVERY, WG-005-OPTIONS-PERSISTENCE, WG-005-ACCESS-READONLY, WG-005-EVIDENCE, WG-006-RENDER-SIZE, WG-006-STATES-RECOVERY, WG-006-OPTIONS-PERSISTENCE, WG-006-ACCESS-READONLY, WG-006-EVIDENCE, WG-007-RENDER-SIZE, WG-007-STATES-RECOVERY, WG-007-OPTIONS-PERSISTENCE, WG-007-ACCESS-READONLY, WG-007-EVIDENCE, WG-008-RENDER-SIZE, WG-008-STATES-RECOVERY, WG-008-OPTIONS-PERSISTENCE, WG-008-ACCESS-READONLY, WG-008-EVIDENCE, WG-009-RENDER-SIZE, WG-009-STATES-RECOVERY, WG-009-OPTIONS-PERSISTENCE, WG-009-ACCESS-READONLY, WG-009-EVIDENCE, WG-010-RENDER-SIZE, WG-010-STATES-RECOVERY, WG-010-OPTIONS-PERSISTENCE, WG-010-ACCESS-READONLY, WG-010-EVIDENCE, WG-011-RENDER-SIZE, WG-011-STATES-RECOVERY, WG-011-OPTIONS-PERSISTENCE, WG-011-ACCESS-READONLY, WG-011-EVIDENCE, WG-012-RENDER-SIZE, WG-012-STATES-RECOVERY, WG-012-OPTIONS-PERSISTENCE, WG-012-ACCESS-READONLY, WG-012-EVIDENCE, CV-001-HAPPY-PATH, CV-001-MUTATION-PERSISTENCE, CV-001-ACCESSIBILITY-ACCESS, CV-001-DEGRADED-RECOVERY, CV-002-HAPPY-PATH, CV-002-MUTATION-PERSISTENCE, CV-002-ACCESSIBILITY-ACCESS, CV-002-DEGRADED-RECOVERY, CV-003-HAPPY-PATH, CV-003-MUTATION-PERSISTENCE, CV-003-ACCESSIBILITY-ACCESS, CV-003-DEGRADED-RECOVERY, CV-004-HAPPY-PATH, CV-004-MUTATION-PERSISTENCE, CV-004-ACCESSIBILITY-ACCESS, CV-004-DEGRADED-RECOVERY, CV-005-HAPPY-PATH, CV-005-MUTATION-PERSISTENCE, CV-005-ACCESSIBILITY-ACCESS, CV-005-DEGRADED-RECOVERY, CV-006-HAPPY-PATH, CV-006-MUTATION-PERSISTENCE, CV-006-ACCESSIBILITY-ACCESS, CV-006-DEGRADED-RECOVERY, CV-007-HAPPY-PATH, CV-007-MUTATION-PERSISTENCE, CV-007-ACCESSIBILITY-ACCESS, CV-007-DEGRADED-RECOVERY, CV-008-HAPPY-PATH, CV-008-MUTATION-PERSISTENCE, CV-008-ACCESSIBILITY-ACCESS, CV-008-DEGRADED-RECOVERY, WP-001-JOURNEY, WP-001-THRESHOLDS, WP-001-PERSISTENCE-ACCESS, WP-001-FAILURE-EVIDENCE, WP-002-JOURNEY, WP-002-THRESHOLDS, WP-002-PERSISTENCE-ACCESS, WP-002-FAILURE-EVIDENCE, WP-003-JOURNEY, WP-003-THRESHOLDS, WP-003-PERSISTENCE-ACCESS, WP-003-FAILURE-EVIDENCE, WP-004-JOURNEY, WP-004-THRESHOLDS, WP-004-PERSISTENCE-ACCESS, WP-004-FAILURE-EVIDENCE, WP-005-JOURNEY, WP-005-THRESHOLDS, WP-005-PERSISTENCE-ACCESS, WP-005-FAILURE-EVIDENCE, WP-006-JOURNEY, WP-006-THRESHOLDS, WP-006-PERSISTENCE-ACCESS, WP-006-FAILURE-EVIDENCE, WP-007-JOURNEY, WP-007-THRESHOLDS, WP-007-PERSISTENCE-ACCESS, WP-007-FAILURE-EVIDENCE, WP-008-JOURNEY, WP-008-THRESHOLDS, WP-008-PERSISTENCE-ACCESS, WP-008-FAILURE-EVIDENCE, WP-009-JOURNEY, WP-009-THRESHOLDS, WP-009-PERSISTENCE-ACCESS, WP-009-FAILURE-EVIDENCE, PE-001-BASELINE, PE-001-THRESHOLD, PE-001-STRESS, PE-001-RECOVERY-EVIDENCE, PE-002-BASELINE, PE-002-THRESHOLD, PE-002-STRESS, PE-002-RECOVERY-EVIDENCE, PE-003-BASELINE, PE-003-THRESHOLD, PE-003-STRESS, PE-003-RECOVERY-EVIDENCE, PE-004-BASELINE, PE-004-THRESHOLD, PE-004-STRESS, PE-004-RECOVERY-EVIDENCE | + +## Critical gaps + +| Packet | Case | Status | +| --- | --- | --- | +| preflight-01 | PF-001-ENVIRONMENT | failed | +| preflight-01 | PF-001-ACCESS | blocked | +| preflight-02 | PF-002-ENVIRONMENT | failed | +| preflight-02 | PF-002-ACCESS | blocked | +| preflight-03 | PF-003-ENVIRONMENT | failed | +| preflight-03 | PF-003-ACCESS | blocked | +| board-01 | BD-001-THRESHOLDS | blocked | +| board-01 | BD-001-MUTATION | blocked | +| board-01 | BD-001-PERSISTENCE | blocked | +| board-01 | BD-001-ACCESS-RECOVERY | blocked | +| board-02 | BD-002-THRESHOLDS | blocked | +| board-02 | BD-002-MUTATION | blocked | +| board-02 | BD-002-PERSISTENCE | blocked | +| board-02 | BD-002-ACCESS-RECOVERY | blocked | +| board-03 | BD-003-THRESHOLDS | blocked | +| board-03 | BD-003-MUTATION | blocked | +| board-03 | BD-003-PERSISTENCE | blocked | +| board-03 | BD-003-ACCESS-RECOVERY | blocked | +| board-04 | BD-004-THRESHOLDS | blocked | +| board-04 | BD-004-MUTATION | blocked | +| board-04 | BD-004-PERSISTENCE | blocked | +| board-04 | BD-004-ACCESS-RECOVERY | blocked | +| board-05 | BD-005-THRESHOLDS | blocked | +| board-05 | BD-005-MUTATION | blocked | +| board-05 | BD-005-PERSISTENCE | blocked | +| board-05 | BD-005-ACCESS-RECOVERY | blocked | +| board-06 | BD-006-THRESHOLDS | blocked | +| board-06 | BD-006-MUTATION | blocked | +| board-06 | BD-006-PERSISTENCE | blocked | +| board-06 | BD-006-ACCESS-RECOVERY | blocked | +| board-07 | BD-007-THRESHOLDS | blocked | +| board-07 | BD-007-MUTATION | blocked | +| board-07 | BD-007-PERSISTENCE | blocked | +| board-07 | BD-007-ACCESS-RECOVERY | blocked | +| board-08 | BD-008-THRESHOLDS | blocked | +| board-08 | BD-008-MUTATION | blocked | +| board-08 | BD-008-PERSISTENCE | blocked | +| board-08 | BD-008-ACCESS-RECOVERY | blocked | +| board-09 | BD-009-THRESHOLDS | blocked | +| board-09 | BD-009-MUTATION | blocked | +| board-09 | BD-009-PERSISTENCE | blocked | +| board-09 | BD-009-ACCESS-RECOVERY | blocked | +| widgets-01 | WG-001-RENDER-SIZE | blocked | +| widgets-01 | WG-001-STATES-RECOVERY | blocked | +| widgets-01 | WG-001-OPTIONS-PERSISTENCE | blocked | +| widgets-01 | WG-001-ACCESS-READONLY | blocked | +| widgets-01 | WG-001-EVIDENCE | blocked | +| widgets-02 | WG-002-RENDER-SIZE | blocked | +| widgets-02 | WG-002-STATES-RECOVERY | blocked | +| widgets-02 | WG-002-OPTIONS-PERSISTENCE | blocked | +| widgets-02 | WG-002-ACCESS-READONLY | blocked | +| widgets-02 | WG-002-EVIDENCE | blocked | +| widgets-03 | WG-003-RENDER-SIZE | blocked | +| widgets-03 | WG-003-STATES-RECOVERY | blocked | +| widgets-03 | WG-003-OPTIONS-PERSISTENCE | blocked | +| widgets-03 | WG-003-ACCESS-READONLY | blocked | +| widgets-03 | WG-003-EVIDENCE | blocked | +| widgets-04 | WG-004-RENDER-SIZE | blocked | +| widgets-04 | WG-004-STATES-RECOVERY | blocked | +| widgets-04 | WG-004-OPTIONS-PERSISTENCE | blocked | +| widgets-04 | WG-004-ACCESS-READONLY | blocked | +| widgets-04 | WG-004-EVIDENCE | blocked | +| widgets-05 | WG-005-RENDER-SIZE | blocked | +| widgets-05 | WG-005-STATES-RECOVERY | blocked | +| widgets-05 | WG-005-OPTIONS-PERSISTENCE | blocked | +| widgets-05 | WG-005-ACCESS-READONLY | blocked | +| widgets-05 | WG-005-EVIDENCE | blocked | +| widgets-06 | WG-006-RENDER-SIZE | blocked | +| widgets-06 | WG-006-STATES-RECOVERY | blocked | +| widgets-06 | WG-006-OPTIONS-PERSISTENCE | blocked | +| widgets-06 | WG-006-ACCESS-READONLY | blocked | +| widgets-06 | WG-006-EVIDENCE | blocked | +| widgets-07 | WG-007-RENDER-SIZE | blocked | +| widgets-07 | WG-007-STATES-RECOVERY | blocked | +| widgets-07 | WG-007-OPTIONS-PERSISTENCE | blocked | +| widgets-07 | WG-007-ACCESS-READONLY | blocked | +| widgets-07 | WG-007-EVIDENCE | blocked | +| widgets-08 | WG-008-RENDER-SIZE | blocked | +| widgets-08 | WG-008-STATES-RECOVERY | blocked | +| widgets-08 | WG-008-OPTIONS-PERSISTENCE | blocked | +| widgets-08 | WG-008-ACCESS-READONLY | blocked | +| widgets-08 | WG-008-EVIDENCE | blocked | +| widgets-09 | WG-009-RENDER-SIZE | blocked | +| widgets-09 | WG-009-STATES-RECOVERY | blocked | +| widgets-09 | WG-009-OPTIONS-PERSISTENCE | blocked | +| widgets-09 | WG-009-ACCESS-READONLY | blocked | +| widgets-09 | WG-009-EVIDENCE | blocked | +| widgets-10 | WG-010-RENDER-SIZE | blocked | +| widgets-10 | WG-010-STATES-RECOVERY | blocked | +| widgets-10 | WG-010-OPTIONS-PERSISTENCE | blocked | +| widgets-10 | WG-010-ACCESS-READONLY | blocked | +| widgets-10 | WG-010-EVIDENCE | blocked | +| widgets-11 | WG-011-RENDER-SIZE | blocked | +| widgets-11 | WG-011-STATES-RECOVERY | blocked | +| widgets-11 | WG-011-OPTIONS-PERSISTENCE | blocked | +| widgets-11 | WG-011-ACCESS-READONLY | blocked | +| widgets-11 | WG-011-EVIDENCE | blocked | +| widgets-12 | WG-012-RENDER-SIZE | blocked | +| widgets-12 | WG-012-STATES-RECOVERY | blocked | +| widgets-12 | WG-012-OPTIONS-PERSISTENCE | blocked | +| widgets-12 | WG-012-ACCESS-READONLY | blocked | +| widgets-12 | WG-012-EVIDENCE | blocked | +| core-v2-01 | CV-001-HAPPY-PATH | blocked | +| core-v2-01 | CV-001-MUTATION-PERSISTENCE | blocked | +| core-v2-01 | CV-001-ACCESSIBILITY-ACCESS | blocked | +| core-v2-01 | CV-001-DEGRADED-RECOVERY | blocked | +| core-v2-02 | CV-002-HAPPY-PATH | blocked | +| core-v2-02 | CV-002-MUTATION-PERSISTENCE | blocked | +| core-v2-02 | CV-002-ACCESSIBILITY-ACCESS | blocked | +| core-v2-02 | CV-002-DEGRADED-RECOVERY | blocked | +| core-v2-03 | CV-003-HAPPY-PATH | blocked | +| core-v2-03 | CV-003-MUTATION-PERSISTENCE | blocked | +| core-v2-03 | CV-003-ACCESSIBILITY-ACCESS | blocked | +| core-v2-03 | CV-003-DEGRADED-RECOVERY | blocked | +| core-v2-04 | CV-004-HAPPY-PATH | blocked | +| core-v2-04 | CV-004-MUTATION-PERSISTENCE | blocked | +| core-v2-04 | CV-004-ACCESSIBILITY-ACCESS | blocked | +| core-v2-04 | CV-004-DEGRADED-RECOVERY | blocked | +| core-v2-05 | CV-005-HAPPY-PATH | blocked | +| core-v2-05 | CV-005-MUTATION-PERSISTENCE | blocked | +| core-v2-05 | CV-005-ACCESSIBILITY-ACCESS | blocked | +| core-v2-05 | CV-005-DEGRADED-RECOVERY | blocked | +| core-v2-06 | CV-006-HAPPY-PATH | blocked | +| core-v2-06 | CV-006-MUTATION-PERSISTENCE | blocked | +| core-v2-06 | CV-006-ACCESSIBILITY-ACCESS | blocked | +| core-v2-06 | CV-006-DEGRADED-RECOVERY | blocked | +| core-v2-07 | CV-007-HAPPY-PATH | blocked | +| core-v2-07 | CV-007-MUTATION-PERSISTENCE | blocked | +| core-v2-07 | CV-007-ACCESSIBILITY-ACCESS | blocked | +| core-v2-07 | CV-007-DEGRADED-RECOVERY | blocked | +| core-v2-08 | CV-008-HAPPY-PATH | blocked | +| core-v2-08 | CV-008-MUTATION-PERSISTENCE | blocked | +| core-v2-08 | CV-008-ACCESSIBILITY-ACCESS | blocked | +| core-v2-08 | CV-008-DEGRADED-RECOVERY | blocked | +| whole-product-01 | WP-001-JOURNEY | blocked | +| whole-product-01 | WP-001-THRESHOLDS | blocked | +| whole-product-01 | WP-001-PERSISTENCE-ACCESS | blocked | +| whole-product-01 | WP-001-FAILURE-EVIDENCE | blocked | +| whole-product-02 | WP-002-JOURNEY | blocked | +| whole-product-02 | WP-002-THRESHOLDS | blocked | +| whole-product-02 | WP-002-PERSISTENCE-ACCESS | blocked | +| whole-product-02 | WP-002-FAILURE-EVIDENCE | blocked | +| whole-product-03 | WP-003-JOURNEY | blocked | +| whole-product-03 | WP-003-THRESHOLDS | blocked | +| whole-product-03 | WP-003-PERSISTENCE-ACCESS | blocked | +| whole-product-03 | WP-003-FAILURE-EVIDENCE | blocked | +| whole-product-04 | WP-004-JOURNEY | blocked | +| whole-product-04 | WP-004-THRESHOLDS | blocked | +| whole-product-04 | WP-004-PERSISTENCE-ACCESS | blocked | +| whole-product-04 | WP-004-FAILURE-EVIDENCE | blocked | +| whole-product-05 | WP-005-JOURNEY | blocked | +| whole-product-05 | WP-005-THRESHOLDS | blocked | +| whole-product-05 | WP-005-PERSISTENCE-ACCESS | blocked | +| whole-product-05 | WP-005-FAILURE-EVIDENCE | blocked | +| whole-product-06 | WP-006-JOURNEY | blocked | +| whole-product-06 | WP-006-THRESHOLDS | blocked | +| whole-product-06 | WP-006-PERSISTENCE-ACCESS | blocked | +| whole-product-06 | WP-006-FAILURE-EVIDENCE | blocked | +| whole-product-07 | WP-007-JOURNEY | blocked | +| whole-product-07 | WP-007-THRESHOLDS | blocked | +| whole-product-07 | WP-007-PERSISTENCE-ACCESS | blocked | +| whole-product-07 | WP-007-FAILURE-EVIDENCE | blocked | +| whole-product-08 | WP-008-JOURNEY | blocked | +| whole-product-08 | WP-008-THRESHOLDS | blocked | +| whole-product-08 | WP-008-PERSISTENCE-ACCESS | blocked | +| whole-product-08 | WP-008-FAILURE-EVIDENCE | blocked | +| whole-product-09 | WP-009-JOURNEY | blocked | +| whole-product-09 | WP-009-THRESHOLDS | blocked | +| whole-product-09 | WP-009-PERSISTENCE-ACCESS | blocked | +| whole-product-09 | WP-009-FAILURE-EVIDENCE | blocked | +| performance-01 | PE-001-BASELINE | blocked | +| performance-01 | PE-001-THRESHOLD | blocked | +| performance-01 | PE-001-STRESS | blocked | +| performance-01 | PE-001-RECOVERY-EVIDENCE | blocked | +| performance-02 | PE-002-BASELINE | blocked | +| performance-02 | PE-002-THRESHOLD | blocked | +| performance-02 | PE-002-STRESS | blocked | +| performance-02 | PE-002-RECOVERY-EVIDENCE | blocked | +| performance-03 | PE-003-BASELINE | blocked | +| performance-03 | PE-003-THRESHOLD | blocked | +| performance-03 | PE-003-STRESS | blocked | +| performance-03 | PE-003-RECOVERY-EVIDENCE | blocked | +| performance-04 | PE-004-BASELINE | blocked | +| performance-04 | PE-004-THRESHOLD | blocked | +| performance-04 | PE-004-STRESS | blocked | +| performance-04 | PE-004-RECOVERY-EVIDENCE | blocked | + +## PR coverage + +| PR | Area | Packets | +| --- | --- | ---: | +| #6356 | Custom Widgets | 7 | +| #6503 | dnd-kit grid | 12 | +| #6502 | widget modernization | 23 | +| #6450 | responsive layouts | 5 | +| #6482 | Assistant | 7 | +| #6555 | widget performance | 18 | +| #6569 | onboarding | 4 | +| #6545 | release-v2 rollup | 15 | + +## Agent reports + +- [preflight-01: Candidate identity and services](reports/preflight-01/report.md) — failed +- [preflight-02: Fixture and persona access](reports/preflight-02/report.md) — failed +- [preflight-03: Browser evidence hygiene](reports/preflight-03/report.md) — failed +- [board-01: 24-column placement and resize](reports/board-01/report.md) — blocked +- [board-02: Scrollable canvas and collapse states](reports/board-02/report.md) — blocked +- [board-03: Dense collision handling](reports/board-03/report.md) — blocked +- [board-04: Nested containers](reports/board-04/report.md) — blocked +- [board-05: Responsive layout boundaries](reports/board-05/report.md) — blocked +- [board-06: Icons, bookmarks, and compact layout](reports/board-06/report.md) — blocked +- [board-07: Permission-aware editing](reports/board-07/report.md) — blocked +- [board-08: Board import and export](reports/board-08/report.md) — blocked +- [board-09: Keyboard-only grid operation](reports/board-09/report.md) — blocked +- [widgets-01: Time and environment](reports/widgets-01/report.md) — blocked +- [widgets-02: Apps, embeds, video, game status, and stocks](reports/widgets-02/report.md) — blocked +- [widgets-03: Notes, bookmarks, feeds, and timetable](reports/widgets-03/report.md) — blocked +- [widgets-04: Downloads, containers, indexers, and DNS](reports/widgets-04/report.md) — blocked +- [widgets-05: Smart home, health, and system telemetry](reports/widgets-05/report.md) — blocked +- [widgets-06: Network availability and operations](reports/widgets-06/report.md) — blocked +- [widgets-07: Beszel and update monitoring](reports/widgets-07/report.md) — blocked +- [widgets-08: Power, VPN, speed, routing, and analytics](reports/widgets-08/report.md) — blocked +- [widgets-09: Media overview and requests](reports/widgets-09/report.md) — blocked +- [widgets-10: Media activity, Immich, and audio](reports/widgets-10/report.md) — blocked +- [widgets-11: Documents, patching, media services, and releases](reports/widgets-11/report.md) — blocked +- [widgets-12: Coolify, ArchiveTeam, Custom API, and Assistant](reports/widgets-12/report.md) — blocked +- [core-v2-01: Custom widget authoring](reports/core-v2-01/report.md) — blocked +- [core-v2-02: Assistant tool flow](reports/core-v2-02/report.md) — blocked +- [core-v2-03: Onboarding happy path](reports/core-v2-03/report.md) — blocked +- [core-v2-04: Authentication and session transitions](reports/core-v2-04/report.md) — blocked +- [core-v2-05: Integration management](reports/core-v2-05/report.md) — blocked +- [core-v2-06: Search, menus, and dialogs](reports/core-v2-06/report.md) — blocked +- [core-v2-07: Read-only enforcement](reports/core-v2-07/report.md) — blocked +- [core-v2-08: Failure and recovery states](reports/core-v2-08/report.md) — blocked +- [whole-product-01: Admin day-one journey](reports/whole-product-01/report.md) — blocked +- [whole-product-02: Owner customization journey](reports/whole-product-02/report.md) — blocked +- [whole-product-03: Editor daily journey](reports/whole-product-03/report.md) — blocked +- [whole-product-04: Viewer and outsider boundaries](reports/whole-product-04/report.md) — blocked +- [whole-product-05: Mobile journey](reports/whole-product-05/report.md) — blocked +- [whole-product-06: Media operator journey](reports/whole-product-06/report.md) — blocked +- [whole-product-07: Infrastructure operator journey](reports/whole-product-07/report.md) — blocked +- [whole-product-08: Creator and Assistant journey](reports/whole-product-08/report.md) — blocked +- [whole-product-09: Accessibility and destructive-action pass](reports/whole-product-09/report.md) — blocked +- [performance-01: Cold and warm board load](reports/performance-01/report.md) — blocked +- [performance-02: Grid interaction responsiveness](reports/performance-02/report.md) — blocked +- [performance-03: Widget network and render budget](reports/performance-03/report.md) — blocked +- [performance-04: Long-session stability](reports/performance-04/report.md) — blocked diff --git a/qa/release-v2/reports/board-01/report.md b/qa/release-v2/reports/board-01/report.md new file mode 100644 index 000000000..0391a7977 --- /dev/null +++ b/qa/release-v2/reports/board-01/report.md @@ -0,0 +1,165 @@ + + +# board-01: 24-column placement and resize + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6503 | +| Personas | Rowan Owner | +| Boards | qa-grid-24 | +| Profiles | main-writable | +| Chromium viewports | desktop-1440, desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Rowan Owner | +| Browser session ID | qa-v2-board-01 | +| Timestamp | 2026-08-31T15:21:05.106Z | +| Executed viewport / input / zoom | desktop-1440 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-001-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-001-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-001-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-001-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, desktop-1440, 100% zoom, mouse, fresh named Chromium session qa-v2-board-01. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [login-initial.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-initial.png) +- [login-redirect-retry-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-annotated.png) +- [login-redirect-retry-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-console.txt) +- [login-redirect-retry-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-errors.txt) +- [login-redirect-retry-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-network.txt) +- [login-redirect-retry-result.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-result.png) +- [login-redirect-retry-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-snapshot.txt) +- [login-redirect-retry-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry-url.txt) +- [login-redirect-retry.har](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry.har) +- [login-redirect-retry.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-redirect-retry.webm) +- [login-retry-before-open.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/login-retry-before-open.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-02/report.md b/qa/release-v2/reports/board-02/report.md new file mode 100644 index 000000000..2b890ea53 --- /dev/null +++ b/qa/release-v2/reports/board-02/report.md @@ -0,0 +1,175 @@ + + +# board-02: Scrollable canvas and collapse states + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6503 | +| Personas | Eden Editor | +| Boards | qa-scroll-lab | +| Profiles | main-writable | +| Chromium viewports | mobile-375, desktop-1280 | +| Zoom | 100%, 200% | +| Input | mouse, touch | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Eden Editor | +| Browser session ID | qa-v2-board-02 | +| Timestamp | 2026-08-31T15:29:45.710Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-002-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-002-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-002-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-002-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-board-02. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [entry-attempt-1-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-attempt-1-console.txt) +- [entry-attempt-1-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-attempt-1-errors.txt) +- [entry-attempt-1-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-attempt-1-network.txt) +- [entry-attempt-1-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-attempt-1-url.txt) +- [entry-retry-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-retry-console.txt) +- [entry-retry-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-retry-errors.txt) +- [entry-retry-network-all.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-retry-network-all.txt) +- [entry-retry-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-retry-network.txt) +- [entry-retry-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-retry-snapshot.txt) +- [entry-retry-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/logs/entry-retry-url.txt) +- [entry-attempt-1-redirect-error.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/screenshots/entry-attempt-1-redirect-error.png) +- [entry-retry-before-navigation.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/screenshots/entry-retry-before-navigation.png) +- [entry-retry-result-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/screenshots/entry-retry-result-annotated.png) +- [entry-retry-result.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/screenshots/entry-retry-result.png) +- [entry-redirect-retry.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/videos/entry-redirect-retry.webm) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-03/report.md b/qa/release-v2/reports/board-03/report.md new file mode 100644 index 000000000..8d6619240 --- /dev/null +++ b/qa/release-v2/reports/board-03/report.md @@ -0,0 +1,143 @@ + + +# board-03: Dense collision handling + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6503 | +| Personas | Casey Chaos | +| Boards | qa-dense-collisions | +| Profiles | main-writable | +| Chromium viewports | tablet-1024, desktop-1440 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Casey Chaos | +| Browser session ID | qa-v2-board-03 | +| Timestamp | 2026-08-31T15:34:27.703Z | +| Executed viewport / input / zoom | tablet-1024 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-003-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-003-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-003-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-003-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, tablet-1024, 100% zoom, mouse, fresh named Chromium session qa-v2-board-03. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [BD-003-redirect-retry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/BD-003-redirect-retry.png) +- [redirect-first.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/redirect-first.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-04/report.md b/qa/release-v2/reports/board-04/report.md new file mode 100644 index 000000000..90a8cd230 --- /dev/null +++ b/qa/release-v2/reports/board-04/report.md @@ -0,0 +1,150 @@ + + +# board-04: Nested containers + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6503 | +| Personas | Rowan Owner | +| Boards | qa-nested-containers | +| Profiles | main-writable | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100%, 200% | +| Input | mouse, touch | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Rowan Owner | +| Browser session ID | qa-v2-board-04 | +| Timestamp | 2026-08-31T15:37:09.712Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-004-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-004-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-004-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-004-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-board-04. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [BD-004-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-console.txt) +- [BD-004-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-errors.txt) +- [BD-004-network-document.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-network-document.txt) +- [BD-004-redirect-result.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/screenshots/BD-004-redirect-result.png) +- [BD-004-redirect-retry.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/videos/BD-004-redirect-retry.webm) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-05/report.md b/qa/release-v2/reports/board-05/report.md new file mode 100644 index 000000000..2c8a8db4e --- /dev/null +++ b/qa/release-v2/reports/board-05/report.md @@ -0,0 +1,171 @@ + + +# board-05: Responsive layout boundaries + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6450 | +| Personas | Morgan Mobile | +| Boards | qa-layout-boundaries | +| Profiles | main-writable | +| Chromium viewports | boundary-767, boundary-768, boundary-1279, desktop-1280 | +| Zoom | 100% | +| Input | touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Morgan Mobile | +| Browser session ID | qa-v2-board-05 | +| Timestamp | 2026-08-31T15:39:34.555Z | +| Executed viewport / input / zoom | boundary-767 / touch / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-005-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-005-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-005-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-005-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, boundary-767, 100% zoom, touch, fresh named Chromium session qa-v2-board-05. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [BD-005-initial-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial-console.txt) +- [BD-005-initial-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial-errors.txt) +- [BD-005-initial-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial-network.txt) +- [BD-005-initial-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial-redirect.png) +- [BD-005-initial-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial-snapshot.txt) +- [BD-005-initial-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial-url.txt) +- [BD-005-initial.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-initial.webm) +- [BD-005-retry-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry-console.txt) +- [BD-005-retry-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry-errors.txt) +- [BD-005-retry-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry-network.txt) +- [BD-005-retry-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry-redirect.png) +- [BD-005-retry-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry-snapshot.txt) +- [BD-005-retry-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry-url.txt) +- [BD-005-retry.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-retry.webm) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-06/report.md b/qa/release-v2/reports/board-06/report.md new file mode 100644 index 000000000..94ef59fd5 --- /dev/null +++ b/qa/release-v2/reports/board-06/report.md @@ -0,0 +1,143 @@ + + +# board-06: Icons, bookmarks, and compact layout + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6502 | +| Personas | Brooke Minimalist | +| Boards | qa-icons-bookmarks | +| Profiles | main-writable | +| Chromium viewports | mobile-375, desktop-1440 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, touch | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Brooke Minimalist | +| Browser session ID | qa-v2-board-06 | +| Timestamp | 2026-08-31T15:43:02.917Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-006-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-006-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-006-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-006-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-board-06. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [BD-006-fresh-retry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06/BD-006-fresh-retry.png) +- [BD-006-initial-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06/BD-006-initial-redirect.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-07/report.md b/qa/release-v2/reports/board-07/report.md new file mode 100644 index 000000000..2f6159485 --- /dev/null +++ b/qa/release-v2/reports/board-07/report.md @@ -0,0 +1,152 @@ + + +# board-07: Permission-aware editing + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6503, #6545 | +| Personas | Rowan Owner, Eden Editor, Vivian Viewer, Nolan Outsider | +| Boards | qa-permissions-public | +| Profiles | main-writable, main-readonly | +| Chromium viewports | desktop-1440 | +| Zoom | 100% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46641 / 46641 | +| Runtime profile / flags | main-readonly; DEMO_MODE=true, DEMO_READ_ONLY=true, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Vivian Viewer | +| Browser session ID | qa-v2-board-07 | +| Timestamp | 2026-08-31T15:45:54.133Z | +| Executed viewport / input / zoom | desktop-1440 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-007-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-007-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-007-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-007-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-readonly, desktop-1440, 100% zoom, mouse, fresh named Chromium session qa-v2-board-07. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46641 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-fresh-retry-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/console/redirect-fresh-retry-console.txt) +- [redirect-initial-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/console/redirect-initial-console.txt) +- [redirect-initial-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/console/redirect-initial-url.txt) +- [redirect-initial-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/network/redirect-initial-network.txt) +- [redirect-fresh-retry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/screenshots/redirect-fresh-retry.png) +- [redirect-initial.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/screenshots/redirect-initial.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-08/report.md b/qa/release-v2/reports/board-08/report.md new file mode 100644 index 000000000..6259b9ce1 --- /dev/null +++ b/qa/release-v2/reports/board-08/report.md @@ -0,0 +1,151 @@ + + +# board-08: Board import and export + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6545 | +| Personas | Avery Admin | +| Boards | qa-download-upload | +| Profiles | main-writable | +| Chromium viewports | desktop-1280 | +| Zoom | 100% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Avery Admin | +| Browser session ID | qa-v2-board-08 | +| Timestamp | 2026-08-31T15:48:51.943Z | +| Executed viewport / input / zoom | desktop-1280 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-008-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-008-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-008-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-008-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, desktop-1280, 100% zoom, mouse, fresh named Chromium session qa-v2-board-08. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [board-08-report.md](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/board-08-report.md) +- [fresh-context-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/fresh-context-redirect.png) +- [fresh-context-retry.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/fresh-context-retry.webm) +- [initial-navigation.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/initial-navigation.webm) +- [initial-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/initial-redirect.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/board-09/report.md b/qa/release-v2/reports/board-09/report.md new file mode 100644 index 000000000..595b7555b --- /dev/null +++ b/qa/release-v2/reports/board-09/report.md @@ -0,0 +1,165 @@ + + +# board-09: Keyboard-only grid operation + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | board | +| PR refs | #6503, #6450 | +| Personas | Kira Keyboard | +| Boards | qa-grid-24 | +| Profiles | main-writable | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100%, 200% | +| Input | keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Kira Keyboard | +| Browser session ID | qa-v2-board-09 | +| Timestamp | 2026-08-31T15:52:25.285Z | +| Executed viewport / input / zoom | mobile-320 / keyboard / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| BD-009-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. | +| BD-009-MUTATION | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. | +| BD-009-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Reload, switch layouts or sessions, and verify saved and reset state remains correct. | +| BD-009-ACCESS-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, keyboard, fresh named Chromium session qa-v2-board-09. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [BD-009-fresh-retry-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-fresh-retry-console.txt) +- [BD-009-fresh-retry-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-fresh-retry-errors.txt) +- [BD-009-fresh-retry-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-fresh-retry-network.txt) +- [BD-009-fresh-retry-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-fresh-retry-snapshot.txt) +- [BD-009-fresh-retry-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-fresh-retry-url.txt) +- [BD-009-fresh-retry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-fresh-retry.png) +- [BD-009-initial-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-initial-console.txt) +- [BD-009-initial-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-initial-errors.txt) +- [BD-009-initial-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-initial-network.txt) +- [BD-009-initial-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-initial-redirect.png) +- [BD-009-initial-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-initial-snapshot.txt) +- [BD-009-initial-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-initial-url.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-01/report.md b/qa/release-v2/reports/core-v2-01/report.md new file mode 100644 index 000000000..970120477 --- /dev/null +++ b/qa/release-v2/reports/core-v2-01/report.md @@ -0,0 +1,145 @@ + + +# core-v2-01: Custom widget authoring + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6356 | +| Personas | Cora Creator | +| Boards | qa-custom-widget-assistant | +| Profiles | main-writable, degraded | +| Chromium viewports | desktop-1440 | +| Zoom | 100% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Cora Creator | +| Browser session ID | qa-v2-core-v2-01 | +| Timestamp | 2026-08-31T16:37:58.173Z | +| Executed viewport / input / zoom | desktop-1440 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-001-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-001-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-001-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-001-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, desktop-1440, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-01. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/redirect-console.txt) +- [redirect-entry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/redirect-entry.png) +- [redirect-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/redirect-network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-02/report.md b/qa/release-v2/reports/core-v2-02/report.md new file mode 100644 index 000000000..c97c0df07 --- /dev/null +++ b/qa/release-v2/reports/core-v2-02/report.md @@ -0,0 +1,145 @@ + + +# core-v2-02: Assistant tool flow + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6482 | +| Personas | Ash Assistant | +| Boards | qa-custom-widget-assistant | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-375, desktop-1440 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ash Assistant | +| Browser session ID | qa-v2-core-v2-02 | +| Timestamp | 2026-08-31T16:42:11.258Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-002-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-002-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-002-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-002-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-02. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/console.txt) +- [entry-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/entry-redirect.png) +- [network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-03/report.md b/qa/release-v2/reports/core-v2-03/report.md new file mode 100644 index 000000000..e1700e28b --- /dev/null +++ b/qa/release-v2/reports/core-v2-03/report.md @@ -0,0 +1,145 @@ + + +# core-v2-03: Onboarding happy path + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6569 | +| Personas | Nora Newcomer | +| Boards | None | +| Profiles | onboarding-fresh | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:35901 / 35901 | +| Runtime profile / flags | onboarding-fresh; DEMO_MODE=false, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Nora Newcomer | +| Browser session ID | qa-v2-core-v2-03 | +| Timestamp | 2026-08-31T16:48:07.733Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-003-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-003-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-003-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-003-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, onboarding-fresh, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-03. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:35901 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [init-mobile-320.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/init-mobile-320.png) +- [init-mobile-console.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/init-mobile-console.json) +- [init-mobile-network.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/init-mobile-network.json) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-04/report.md b/qa/release-v2/reports/core-v2-04/report.md new file mode 100644 index 000000000..e623ca5e1 --- /dev/null +++ b/qa/release-v2/reports/core-v2-04/report.md @@ -0,0 +1,147 @@ + + +# core-v2-04: Authentication and session transitions + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6545, #6569 | +| Personas | Avery Admin, Nolan Outsider | +| Boards | qa-permissions-public | +| Profiles | main-writable, main-readonly, onboarding-fresh | +| Chromium viewports | mobile-375, desktop-1280 | +| Zoom | 100% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Avery Admin | +| Browser session ID | qa-v2-core-v2-04 | +| Timestamp | 2026-08-31T16:54:36.657Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-004-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-004-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-004-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-004-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-04. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [auth-login-redirect-mobile-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/auth-login-redirect-mobile-console.txt) +- [auth-login-redirect-mobile-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/auth-login-redirect-mobile-errors.txt) +- [auth-login-redirect-mobile-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/auth-login-redirect-mobile-network.txt) +- [screenshot-1788195204997.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/screenshot-1788195204997.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-05/report.md b/qa/release-v2/reports/core-v2-05/report.md new file mode 100644 index 000000000..f82e64827 --- /dev/null +++ b/qa/release-v2/reports/core-v2-05/report.md @@ -0,0 +1,145 @@ + + +# core-v2-05: Integration management + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6502, #6545 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-07 | +| Profiles | main-writable, degraded | +| Chromium viewports | desktop-1440 | +| Zoom | 100%, 125% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-core-v2-05 | +| Timestamp | 2026-08-31T17:00:15.317Z | +| Executed viewport / input / zoom | desktop-1440 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-005-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-005-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-005-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-005-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, desktop-1440, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-05. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [root-1440x900-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/root-1440x900-100.png) +- [root-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/root-console.txt) +- [root-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/root-network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-06/report.md b/qa/release-v2/reports/core-v2-06/report.md new file mode 100644 index 000000000..80ccfd8de --- /dev/null +++ b/qa/release-v2/reports/core-v2-06/report.md @@ -0,0 +1,140 @@ + + +# core-v2-06: Search, menus, and dialogs + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6545 | +| Personas | Kira Keyboard | +| Boards | qa-grid-24 | +| Profiles | main-writable | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100%, 200% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Kira Keyboard | +| Browser session ID | qa-v2-core-v2-06 | +| Timestamp | 2026-08-31T17:05:24.186Z | +| Executed viewport / input / zoom | mobile-320 / keyboard / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-006-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-006-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-006-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-006-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, keyboard, fresh named Chromium session qa-v2-core-v2-06. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [core-v2-06-mobile320-100-keyboard-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06/core-v2-06-mobile320-100-keyboard-redirect.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-07/report.md b/qa/release-v2/reports/core-v2-07/report.md new file mode 100644 index 000000000..c0998fb2a --- /dev/null +++ b/qa/release-v2/reports/core-v2-07/report.md @@ -0,0 +1,147 @@ + + +# core-v2-07: Read-only enforcement + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6356, #6482, #6545 | +| Personas | Vivian Viewer | +| Boards | qa-custom-widget-assistant, qa-permissions-public | +| Profiles | main-readonly | +| Chromium viewports | mobile-375, desktop-1280 | +| Zoom | 100% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:43401 / 43401 | +| Runtime profile / flags | main-readonly; DEMO_MODE=true, DEMO_READ_ONLY=true, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Vivian Viewer | +| Browser session ID | qa-v2-core-v2-07 | +| Timestamp | 2026-08-31T17:09:28.752Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-007-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-007-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-007-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-007-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-readonly, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-07. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:43401 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [mobile-entry-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/mobile-entry-console.txt) +- [mobile-entry-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/mobile-entry-errors.txt) +- [mobile-entry-network.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/mobile-entry-network.json) +- [screenshot-1788196133619.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/screenshot-1788196133619.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/core-v2-08/report.md b/qa/release-v2/reports/core-v2-08/report.md new file mode 100644 index 000000000..03d1f2553 --- /dev/null +++ b/qa/release-v2/reports/core-v2-08/report.md @@ -0,0 +1,145 @@ + + +# core-v2-08: Failure and recovery states + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | core-v2 | +| PR refs | #6356, #6482, #6502, #6545 | +| Personas | Casey Chaos | +| Boards | qa-custom-widget-assistant, qa-widgets-12 | +| Profiles | degraded | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Casey Chaos | +| Browser session ID | qa-v2-core-v2-08 | +| Timestamp | 2026-08-31T17:14:20.509Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| CV-008-HAPPY-PATH | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. | +| CV-008-MUTATION-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. | +| CV-008-ACCESSIBILITY-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. | +| CV-008-DEGRADED-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-core-v2-08. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [entry-320x568-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/entry-320x568-console.txt) +- [entry-320x568-network.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/entry-320x568-network.json) +- [entry-320x568-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/entry-320x568-redirect.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/performance-01/report.md b/qa/release-v2/reports/performance-01/report.md new file mode 100644 index 000000000..ba2c48f08 --- /dev/null +++ b/qa/release-v2/reports/performance-01/report.md @@ -0,0 +1,149 @@ + + +# performance-01: Cold and warm board load + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | performance | +| PR refs | #6555, #6545 | +| Personas | Avery Admin | +| Boards | qa-grid-24, qa-widgets-12 | +| Profiles | main-writable | +| Chromium viewports | mobile-375, desktop-1440 | +| Zoom | 100% | +| Input | mouse | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Avery Admin | +| Browser session ID | qa-v2-performance-01 | +| Timestamp | 2026-08-31T18:17:56.112Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PE-001-BASELINE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. | +| PE-001-THRESHOLD | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. | +| PE-001-STRESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. | +| PE-001-RECOVERY-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-performance-01. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [cold-375-entry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/cold-375-entry.png) +- [console-cold-375.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/console-cold-375.json) +- [errors-cold-375.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/errors-cold-375.json) +- [network-cold-375.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/network-cold-375.json) +- [snapshot-cold-375.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/snapshot-cold-375.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/performance-02/report.md b/qa/release-v2/reports/performance-02/report.md new file mode 100644 index 000000000..88dee671c --- /dev/null +++ b/qa/release-v2/reports/performance-02/report.md @@ -0,0 +1,150 @@ + + +# performance-02: Grid interaction responsiveness + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | performance | +| PR refs | #6503, #6555 | +| Personas | Rowan Owner | +| Boards | qa-dense-collisions, qa-nested-containers | +| Profiles | main-writable | +| Chromium viewports | tablet-1024, desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Rowan Owner | +| Browser session ID | qa-v2-performance-02 | +| Timestamp | 2026-08-31T18:25:01.713Z | +| Executed viewport / input / zoom | tablet-1024 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PE-002-BASELINE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. | +| PE-002-THRESHOLD | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. | +| PE-002-STRESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. | +| PE-002-RECOVERY-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, tablet-1024, 100% zoom, mouse, fresh named Chromium session qa-v2-performance-02. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [performance-02-1024x768-100-mouse-entry-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/performance-02-1024x768-100-mouse-entry-annotated.png) +- [performance-02-1024x768-100-mouse-entry-attempt-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/performance-02-1024x768-100-mouse-entry-attempt-annotated.png) +- [performance-02-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/performance-02-console.txt) +- [performance-02-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/performance-02-errors.txt) +- [performance-02-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/performance-02-network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/performance-03/report.md b/qa/release-v2/reports/performance-03/report.md new file mode 100644 index 000000000..aca684023 --- /dev/null +++ b/qa/release-v2/reports/performance-03/report.md @@ -0,0 +1,167 @@ + + +# performance-03: Widget network and render budget + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | performance | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra, Maya Media | +| Boards | qa-widgets-04, qa-widgets-07, qa-widgets-12 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100% | +| Input | mouse, touch | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-performance-03 | +| Timestamp | 2026-08-31T18:30:29.480Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PE-003-BASELINE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. | +| PE-003-THRESHOLD | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. | +| PE-003-STRESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. | +| PE-003-RECOVERY-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-performance-03. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [close-mouse-entry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/close-mouse-entry.txt) +- [close.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/close.txt) +- [console-mouse-entry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/console-mouse-entry.txt) +- [console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/console.txt) +- [errors-mouse-entry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/errors-mouse-entry.txt) +- [errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/errors.txt) +- [network-mouse-entry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/network-mouse-entry.txt) +- [network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/network.txt) +- [open.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/open.txt) +- [performance-03-320x568-100-mouse-entry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/performance-03-320x568-100-mouse-entry.png) +- [performance-03-320x568-100-mouse.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/performance-03-320x568-100-mouse.png) +- [screenshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/screenshot.txt) +- [setup.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/setup.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/performance-04/report.md b/qa/release-v2/reports/performance-04/report.md new file mode 100644 index 000000000..7c1151e5f --- /dev/null +++ b/qa/release-v2/reports/performance-04/report.md @@ -0,0 +1,150 @@ + + +# performance-04: Long-session stability + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | performance | +| PR refs | #6356, #6503, #6502, #6482, #6555, #6545 | +| Personas | Casey Chaos | +| Boards | qa-grid-24, qa-custom-widget-assistant, qa-widgets-12 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-375, desktop-1920 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Casey Chaos | +| Browser session ID | qa-v2-performance-04 | +| Timestamp | 2026-08-31T18:37:28.172Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PE-004-BASELINE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. | +| PE-004-THRESHOLD | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. | +| PE-004-STRESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. | +| PE-004-RECOVERY-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-performance-04. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console-375x812-100-mouse.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/console-375x812-100-mouse.txt) +- [entry-375x812-100-mouse-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/entry-375x812-100-mouse-annotated.png) +- [entry-375x812-100-mouse-fresh-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/entry-375x812-100-mouse-fresh-annotated.png) +- [errors-375x812-100-mouse.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/errors-375x812-100-mouse.txt) +- [network-375x812-100-mouse.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/network-375x812-100-mouse.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/preflight-01/report.md b/qa/release-v2/reports/preflight-01/report.md new file mode 100644 index 000000000..904e043ef --- /dev/null +++ b/qa/release-v2/reports/preflight-01/report.md @@ -0,0 +1,284 @@ + + +# preflight-01: Candidate identity and services + +| Metadata | Value | +| --- | --- | +| Status | passed | +| Wave | preflight | +| PR refs | #6545 | +| Personas | Avery Admin | +| Boards | None | +| Profiles | main-writable, main-readonly, onboarding-fresh, degraded | +| Primary execution URL | http://127.0.0.1:34401 | +| Primary execution port | 34401 | +| Primary execution profile | main-writable | +| Primary runtime flags | DEMO_MODE=true; DEMO_READ_ONLY=false; UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Primary fixture health | http://127.0.0.1:37273/health | +| Chromium viewport | desktop-1440 (1440×900) | +| Zoom | 100% | +| Input | mouse, keyboard | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| Corrected rerun completed | 2026-08-31T22:57:16+02:00 | +| Auth | Auth-vault profiles used for seeded profiles; no password value read or recorded | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PF-001-ENVIRONMENT | passed | [all-profile runtime and fixture artifacts](#artifacts) | main-writable (34401/37273), main-readonly (41589/44267), onboarding-fresh (41775/33549), and degraded (43903/39181) all rendered in named Chromium at 1440×900. Each fixture `/health` rendered `{"status":"ok","fixture":"release-v2-qa"}`; `/` routed to `/auth/login` for the seeded profiles and `/init` for onboarding-fresh without a loop. | Verify candidate identity, Chromium session, runtime health, fixture reachability, and clean console/network baselines. | +| PF-001-ACCESS | passed | [profile matrix](#profile-matrix) and [onboarding artifacts](#artifacts) | Avery reached the seeded home on main-writable, main-readonly, and degraded. Read-only Edit board/Add board content remained visibly available and no save/mutation was attempted. The fresh slot reached the expected first-board onboarding boundary with no seeded user or QA board and a normal `/auth/login` form; onboarding completion/user creation is outside PF-001 scope. | Verify assigned profiles, personas, boards, permissions, and read-only boundaries before mutations. | +| PF-001-EVIDENCE | passed | [timestamped screenshots, snapshots, and diagnostics](#artifacts) | All four profiles have rendered screenshot evidence plus console/errors/network metadata at the assigned 1440×900 viewport. The onboarding run includes initial `/init`, explicit `/auth/login`, fixture health, reload, and a second fresh named session; diagnostics were sanitized and contain no secrets. | Capture timestamped viewport/input/zoom evidence and confirm artifacts contain no secrets. | + +## Profile matrix + +| Profile | Status | App entry | Fixture health | Persona/access | Reload/session | Visible boundary | +| --- | --- | --- | --- | --- | --- | --- | +| main-writable | passed | `/auth/login` → `/` | `{"status":"ok","fixture":"release-v2-qa"}` | `QA v2 · Home · avery-admin` with Edit board, Board settings, and three seeded widgets | passed | Writable affordances visible | +| main-readonly | passed | `/auth/login` → `/` | `{"status":"ok","fixture":"release-v2-qa"}` | `QA v2 · Home · avery-admin` with three seeded widgets | passed | Edit board/Add board content remained visible; mutation/save boundary not exercised | +| onboarding-fresh | passed | `/` → `/init`; explicit `/auth/login` stayed on `/auth/login` | `{"status":"ok","fixture":"release-v2-qa"}` | Expected first-board onboarding boundary rendered; no seeded user or QA board was visible, and no seeded persona/home access was assigned | Reload returned `/init`; second named session also returned `/init` | Get started/Restore backup visible; completion/user creation is outside PF-001 and was not attempted | +| degraded | passed | `/auth/login` → `/` | `{"status":"ok","fixture":"release-v2-qa"}` | `QA v2 · Home · avery-admin` with three rendered widgets | passed | Degraded profile entry and home rendered; no fixture health failure | + +## Console, network, and artifact hygiene + +The browser `errors` captures were empty for all corrected runs, including both onboarding sessions. Console captures contain development HMR/React DevTools information and repeated PostHog initialization warnings, but no uncaught application error. Network metadata shows successful app/auth/home/health responses for the seeded profiles and `/init`, `/auth/login`, `/health`, reload, and new-session responses for onboarding-fresh. Non-blocking dev-only 404s for a Next static chunk/favicon were observed; no redirect loop occurred. + +No password, cookies, bearer values, request bodies, or unredacted credential-like values were inspected or recorded. The read-only add-widget dialog was opened and cancelled without submitting a widget or changing product state. Onboarding controls were not submitted, so no user or board was created. + +## Performance measurements and limitations + +No performance timings were requested for PF-001. Onboarding-fresh was verified at its expected initial `/init` access boundary; completion and user creation are outside PF-001 and were intentionally not attempted. Current authenticated-profile access and all assigned evidence checks were run. + +## Findings + +No tracked finding remains after the successful retest. The previous `RV2-P1-001` locale/auth redirect-loop finding was removed because it was not reproduced in fresh sessions for main-writable, main-readonly, or degraded, and onboarding-fresh also showed no redirect loop. The successful retest is recorded in the structured notes above. + +## Mutations and persistence + +No product mutation occurred. Login, fixture navigation, clean reloads, opening/cancelling the read-only add-widget dialog, and exiting edit mode all completed without saving board changes. Avery’s seeded home remained available after reload for all three current authenticated profiles. The onboarding-fresh Get started/Restore backup controls were left untouched; no user or QA board was created. + +## Artifacts + +Corrected desktop-1440 evidence is under the [preflight-01 artifact directory](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01). The structured artifact list above is canonical for this report. + +- main-writable: [login screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-writable-desktop-1440-auth-login.png), [home screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-writable-desktop-1440-home.png), [reload screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-writable-desktop-1440-home-reload.png), [fixture health screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-writable-desktop-1440-fixture-health.png), and paired `-console.txt`, `-errors.txt`, and `-network.txt` diagnostics. +- main-readonly: [login screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-readonly-desktop-1440-auth-login.png), [home screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-readonly-desktop-1440-home.png), [reload screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-readonly-desktop-1440-home-reload.png), [edit-boundary screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-readonly-desktop-1440-edit-boundary.png), [add-widget-boundary screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-readonly-desktop-1440-add-widget-boundary.png), [fixture health screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-main-readonly-desktop-1440-fixture-health.png), and paired diagnostics for login/home/reload/fixture health. +- degraded: [login screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-degraded-desktop-1440-auth-login.png), [home screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-degraded-desktop-1440-home.png), [reload screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-degraded-desktop-1440-home-reload.png), [fixture health screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-degraded-desktop-1440-fixture-health.png), and paired `-console.txt`, `-errors.txt`, and `-network.txt` diagnostics. +- onboarding-fresh: [initial `/init` screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-init.png), [explicit login screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-auth-login.png), [fixture health screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-fixture-health.png), [reload screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-init-reload.png), rendered snapshots (`-snapshot.txt`), and sanitized [console](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-console.txt), [errors](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-errors.txt), and [network metadata](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-desktop-1440-network.txt). +- onboarding-fresh new session: [new-session `/init` screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-new-desktop-1440-init.png), rendered [snapshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-new-desktop-1440-init-snapshot.txt), and sanitized [console](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-new-desktop-1440-console.txt), [errors](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-new-desktop-1440-errors.txt), and [network metadata](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/rerun-onboarding-new-desktop-1440-network.txt). + +## Cleanup + +The named sessions `qa-v2-preflight-01-rerun-onboarding-1440` and `qa-v2-preflight-01-rerun-onboarding-new-1440` were closed after capture; earlier seeded-profile rerun sessions were also closed. Existing packet artifacts were preserved. No shared report aggregator or ledger was run. diff --git a/qa/release-v2/reports/preflight-02/report.md b/qa/release-v2/reports/preflight-02/report.md new file mode 100644 index 000000000..fdc6a797a --- /dev/null +++ b/qa/release-v2/reports/preflight-02/report.md @@ -0,0 +1,280 @@ + + +# preflight-02: fixture and persona access + +| Metadata | Value | +| --- | --- | +| Status | failed | +| Wave | preflight | +| PR refs | #6545 | +| Personas | Avery Admin, Rowan Owner, Vivian Viewer, Nolan Outsider | +| Boards | qa-grid-24, qa-permissions-public | +| Profiles | main-writable, main-readonly | +| Chromium viewports | desktop-1440 | +| Zoom | 100% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual ports | http://127.0.0.1:34401 / 34401; http://127.0.0.1:41589 / 41589 | +| Runtime profiles / flags | main-writable and main-readonly; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed personas | Avery Admin, Rowan Owner, Vivian Viewer, Nolan Outsider | +| Browser session IDs | p02-rerun-main-avery-r2, p02-rerun-main-rowan, p02-rerun-main-vivian, p02-rerun-main-nolan, p02-rerun-ro-avery, p02-rerun-ro-rowan, p02-rerun-ro-vivian, p02-rerun-ro-nolan | +| Timestamp | 2026-08-31T22:58:30+02:00 | +| Executed viewport / input / zoom | desktop-1440 / mouse, keyboard / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PF-002-ENVIRONMENT | failed | [runtime artifacts](#artifacts) | Both local profiles rendered the app and assigned boards. The readonly Rowan baseline captured a missing static chunk (404) and both sampled profiles emitted a duplicate PostHog initialization warning; page-error buffers were empty. | Verify candidate identity, Chromium session, runtime health, fixture reachability, and clean console/network baselines. | +| PF-002-ACCESS | failed | [access matrix](#access-matrix), [readonly steps](#findings) | Authentication and direct board access worked for all personas. Shared-board role boundaries held, but privileged users in main-readonly could enter edit mode and reach enabled item-save UI. | Verify assigned profiles, personas, boards, permissions, and readonly boundaries before mutations. | +| PF-002-EVIDENCE | passed | [artifacts](#artifacts) | Fresh named Chromium sessions covered both profiles, all four personas, both boards, reload persistence, and a retry of the readonly interactive path. No secret values were recorded. | Capture timestamped viewport/input/zoom evidence and confirm artifacts contain no secrets. | + +## Access matrix + +Observed UI on the private shared board (`qa-grid-24`) and public board (`qa-permissions-public`): + +| Profile | Persona | Private shared board | Public board | +| --- | --- | --- | --- | +| main-writable | Avery Admin | Edit board and Board settings; 24 items | Edit board and Board settings; 2 items | +| main-writable | Rowan Owner | Edit board and Board settings; 24 items | Edit board and Board settings; 2 items | +| main-writable | Vivian Viewer | View only; 24 items | View only; 2 items | +| main-writable | Nolan Outsider | Denied with Board not found | View only; 2 items | +| main-readonly | Avery Admin | Edit board and item mutation UI; 24 items | Edit board; 2 items | +| main-readonly | Rowan Owner | Edit board and edit mode; 24 items | Edit board; 2 items | +| main-readonly | Vivian Viewer | View only; 24 items | View only; 2 items | +| main-readonly | Nolan Outsider | Denied with Board not found | View only; 2 items | + +Public-board behavior is recorded from the browser, not inferred from the fixture manifest. The main-readonly profile did not show Board settings in the privileged public-board views, but it still showed Edit board. + +## Runtime baseline + +- Authentication completed for all eight persona/profile combinations. Avery and Nolan required a hydration retry in their respective sessions; only the non-secret username was re-filled. +- No page errors were captured in the fresh reload baselines for Rowan readonly or Vivian main-writable. +- Rowan readonly captured `GET /_next/static/chunks/_1wsmiab._.js [Script] 404` after reload. The matching main-writable Vivian filter captured no request. +- Rowan readonly and Vivian main-writable both emitted the duplicate PostHog initialization warning. React DevTools and HMR messages were also present as development-runtime messages. + +## Findings + +### RV2-P1-001 — P1: main-readonly privileged sessions expose mutation UI + +Preconditions: candidate `f57a660088d6777c86aca22977354fa8b810e2be`, main-readonly at `http://127.0.0.1:41589`, desktop-1440, 100% zoom, fresh named sessions. + +1. Authenticate as Avery Admin in `p02-rerun-ro-avery` and open `/boards/qa-grid-24`. +2. Observe `Edit board`, enter edit mode, open an item settings menu, and observe Edit item, Move / resize item, Duplicate item, and Remove item. +3. Open Edit item and observe the enabled Save changes control. +4. Submit Save changes without changing a value, leave edit mode, reload, and verify the private board still has 24 items. +5. Repeat the board/edit-mode path as Rowan Owner in `p02-rerun-ro-rowan`. + +Expected: main-readonly prevents board and item mutation affordances and save submission. Actual: Avery and Rowan can enter edit mode; Avery can reach the item mutation menu and enabled save control. The unchanged save did not alter fixture content, so this rerun does not claim a changed-value write. Reproducibility: reproduced across Avery and Rowan readonly sessions. + +Evidence: [step 1](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-1-view.png), [step 2](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-2-edit-mode.png), [step 3](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-3-item-menu.png), [step 4](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-4-edit-dialog.png), [Avery menu](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-avery-grid-edit-menu.png), [Avery dialog](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-avery-edit-dialog.png), [Rowan edit mode](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-edit-mode.png). + +### RV2-P2-002 — P2: authenticated readonly page requests a missing static chunk + +Preconditions: candidate `f57a660088d6777c86aca22977354fa8b810e2be`, authenticated Rowan readonly session, fresh reload. + +After clearing the request log and reloading `qa-grid-24`, the browser reported `GET /_next/static/chunks/_1wsmiab._.js [Script] 404` while the board rendered. Request bodies, headers, cookies, tokens, and query strings were not inspected. The sanitized status capture is [rerun-rowan-readonly-network-status.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-network-status.txt). + +### RV2-P2-003 — P2: authenticated pages emit duplicate PostHog initialization warning + +Fresh reload console baselines for Rowan readonly and Vivian main-writable both emitted `[warning] [PostHog.js] You have already initialized PostHog! Re-initializing is a no-op`. No page errors were captured in either paired errors baseline. See [Rowan console](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-console.txt), [Vivian console](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-console.txt), and the corresponding [Rowan errors](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-errors.txt) and [Vivian errors](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-errors.txt) captures. + +## Persistence and mutation + +- Avery main-writable entered edit mode, performed an unchanged Save board action, reloaded, and retained the private-board title and 24 items. +- Avery main-readonly opened the item edit dialog and submitted Save changes without changing a value; reload retained 24 items. This confirms no changed fixture value was introduced while demonstrating that the readonly save path is enabled. +- Rowan main-readonly entered and exited edit mode without a mutation. +- No other mutation was attempted because the packet’s purpose was permission and readonly-guard coverage. + +## Limitations and hygiene + +- This was a focused black-box browser rerun. No source files, network bodies, response bodies, headers, cookies, tokens, or password values were inspected or recorded. +- The interactive recorder stop hung and left `rerun-readonly-edit-affordance.webm` at zero bytes; the annotated step screenshots are the relied-upon retry evidence. +- Main-writable Vivian's matching chunk filter had no request after reload; the 404 finding is based on the direct Rowan readonly observation. +- No widget or performance measurements were in scope. +- The shared report aggregator and ledger were not run. + +## Artifacts + +- [rerun-auth-summary.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-auth-summary.txt) +- [rerun-avery-main-authenticated-home.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-avery-main-authenticated-home.png) +- [rerun-avery-main-grid-24.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-avery-main-grid-24.png) +- [rerun-avery-main-grid-edit.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-avery-main-grid-edit.png) +- [rerun-avery-main-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-avery-main-permissions-public.png) +- [rerun-rowan-main-grid-24.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-main-grid-24.png) +- [rerun-rowan-main-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-main-permissions-public.png) +- [rerun-vivian-main-grid-24.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-grid-24.png) +- [rerun-vivian-main-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-permissions-public.png) +- [rerun-nolan-main-grid-24-denied.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-nolan-main-grid-24-denied.png) +- [rerun-nolan-main-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-nolan-main-permissions-public.png) +- [rerun-readonly-step-1-view.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-1-view.png) +- [rerun-readonly-step-2-edit-mode.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-2-edit-mode.png) +- [rerun-readonly-step-3-item-menu.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-3-item-menu.png) +- [rerun-readonly-step-4-edit-dialog.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-step-4-edit-dialog.png) +- [rerun-readonly-avery-grid-edit-menu.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-avery-grid-edit-menu.png) +- [rerun-readonly-avery-edit-dialog.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-avery-edit-dialog.png) +- [rerun-readonly-avery-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-readonly-avery-permissions-public.png) +- [rerun-rowan-readonly-grid-24.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-grid-24.png) +- [rerun-rowan-readonly-edit-mode.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-edit-mode.png) +- [rerun-rowan-readonly-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-permissions-public.png) +- [rerun-vivian-readonly-grid-24.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-readonly-grid-24.png) +- [rerun-vivian-readonly-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-readonly-permissions-public.png) +- [rerun-nolan-readonly-grid-24-denied.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-nolan-readonly-grid-24-denied.png) +- [rerun-nolan-readonly-permissions-public.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-nolan-readonly-permissions-public.png) +- [rerun-rowan-readonly-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-console.txt) +- [rerun-rowan-readonly-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-errors.txt) +- [rerun-rowan-readonly-network-status.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-rowan-readonly-network-status.txt) +- [rerun-vivian-main-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-console.txt) +- [rerun-vivian-main-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-errors.txt) +- [rerun-vivian-main-network-status.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/rerun-vivian-main-network-status.txt) + +## Cleanup + +All rerun sessions were targeted for closure after evidence capture. The zero-byte recorder artifact was retained as produced; no browser secrets or product fixture values were written to the workspace. diff --git a/qa/release-v2/reports/preflight-03/report.md b/qa/release-v2/reports/preflight-03/report.md new file mode 100644 index 000000000..b31216f87 --- /dev/null +++ b/qa/release-v2/reports/preflight-03/report.md @@ -0,0 +1,190 @@ + + +# preflight-03: Browser evidence hygiene + +| Metadata | Value | +| --- | --- | +| Status | failed | +| Wave | preflight | +| PR refs | #6545 | +| Personas | Casey Chaos | +| Boards | None (seeded Casey home inspected) | +| Profiles | main-writable | +| Chromium viewports | mobile-320, desktop-1920 | +| Zoom | 100% primary; 125% and 200% measured; 80% not reachable via headless keyboard control | +| Input | mouse, keyboard | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:34401 / 34401 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Casey Chaos | +| Browser session ID | qa-v2-preflight-03-final | +| Timestamp | 2026-08-31T21:18:20.630Z | +| Executed viewports / input / zoom | mobile-320 and desktop-1920 / mouse and keyboard / 100% primary, 125% and 200% visual-scale evidence | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| PF-003-ENVIRONMENT | passed | [final mobile and desktop browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03) | Runtime reached the login page, authenticated Casey home rendered at both exact viewports, reload returned 200, and no page errors were reported. The previous redirect loop did not reproduce. | Verify candidate identity, Chromium session, runtime health, fixture reachability, and clean console/network baselines. | +| PF-003-ACCESS | passed | [final home and keyboard artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03) | Casey Chaos home and seeded widgets were reachable. Keyboard focus reached Search, Enter opened the Search dialog, and Escape closed it. No product state was mutated. | Verify assigned profile, persona, home render, and access boundary before mutations. | +| PF-003-EVIDENCE | failed | [timestamped final evidence](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03) | Exact 320x568 and 1920x1080 screenshots, geometry, diagnostics, and redaction scans were captured. Mobile 100% has a reproducible weather top clip; mobile 200% visual scale is horizontally clipped. Headless keyboard zoom-out did not reach 80%. | Capture timestamped viewport/input/zoom evidence, inspect overflow, and confirm artifacts contain no secrets. | + +## Diagnostic and geometry baseline + +- Mobile 320x568 at 100%: `innerWidth=320`, `innerHeight=568`, `scrollWidth=305`, `scrollHeight=729`; document, auth, and tRPC requests returned 200. See [geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse-geometry.json), [network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse-network.txt), [console](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse-console.txt), and [errors](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse-errors.txt). +- Desktop 1920x1080 at 100%: `innerWidth=1920`, `innerHeight=1080`, `scrollWidth=1905`, `scrollHeight=1185`; document, auth, and tRPC requests returned 200. See [geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom100-mouse-geometry.json), [network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom100-mouse-network.txt), [console](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom100-mouse-console.txt), and [errors](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom100-mouse-errors.txt). +- Console had no page errors. It contained the development React DevTools/HMR logs and a repeatable PostHog duplicate-initialization warning; no request bodies were captured. +- A marker scan over all `rerun-final-*` text artifacts found no password, authorization, bearer, cookie, session-token, access-token, client-secret, API-key, request-body, or response-body markers. Final PNGs were visually reviewed and contain only the rendered app. + +## Findings + +### RV2-P2-001 — P2: mobile weather card clips the current temperature + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, Casey Chaos, mobile-320, 100% primary zoom, fresh final named Chromium session. + +1. Launch the named Chromium session with `--no-sandbox`, authenticate with the supplied vault, and wait for hydration. +2. Set the viewport to exactly 320x568 and reload after clearing diagnostics. +3. Observe the [plain mobile home screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse-plain.png): the weather card's current temperature is cut off at its top edge. +4. The [numeric geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-weather-geometry.json) measures the temperature text at `y=160.0625` and the 160x69 card at `y=178`, with `overflow=hidden` and `clippedTop=17.9375`. + +Expected: the seeded weather card renders its current temperature fully inside the card. Actual: about 17.94px of the text is above the card and is hidden. Reproducibility: reproduced on the clean reload and after the daemon-restart handoff. + +### RV2-P2-002 — P2: mobile home is horizontally clipped at 200% visual scale + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, Casey Chaos, mobile-320, 200% visual scale. + +1. Set the viewport to exactly 320x568 and apply Chromium visual scale 2. +2. Capture the [plain 200% mobile screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom200-plain.png). +3. Observe that the weather card and text extend beyond the visible 160px visual viewport; the right side is cut off and a horizontal scrollbar is exposed. +4. The [200% geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom200-geometry.json) records `visualViewport.scale=2`, `visualViewport.width=160`, and `scrollWidth=320`. + +Expected: assigned high-zoom evidence remains usable without the home content being cut off. Actual: content requires horizontal movement and is visibly clipped. Reproducibility: reproduced after reload. Qualification: the headless CLI could not apply the 80% keyboard zoom; 125% and 200% rows were captured through Chromium visual page scale and should be confirmed with a real browser-zoom control before release triage. + +## Artifacts + +- [mobile 320x568, 100% mouse annotated](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse.png) and [plain](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-mouse-plain.png) +- [mobile keyboard focus](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-keyboard-focus.json) and [Search dialog](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-keyboard-search.png) +- [mobile weather geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom100-weather-geometry.json) +- [desktop 1920x1080, 100% annotated](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom100-mouse.png) and [plain](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom100-mouse-plain.png) +- [desktop 125%](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom125.png), [desktop 200%](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-desktop-1920-zoom200.png), [mobile 125%](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom125.png), and [mobile 200%](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom200.png) +- [mobile 200% plain](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom200-plain.png) and [200% geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-mobile-320-zoom200-geometry.json) +- [80% capability result](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/rerun-final-zoom80-capability.json) + +## Mutations and persistence + +No credentials were printed or inspected. Reloaded authenticated home state returned to Casey's home. No product state was mutated; keyboard Search open/close was canceled cleanly. + +## Blockers and infrastructure notes + +- The prior locale redirect-loop blocker was not reproduced at `http://127.0.0.1:34401`; the login page and Casey home rendered normally after the instructed hydration fallback. +- A browser-daemon restart interrupted an earlier probe. The disrupted session and its artifacts were excluded from pass/fail evidence; all final artifacts above came from `qa-v2-preflight-03-final` launched after the restart. +- The headless CLI could not reach 80% with keyboard zoom-out; this is recorded as not reached, not passed. + +## Cleanup + +The final named browser session was closed. No shared report aggregator or ledger was run. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/repro-browser-zoom-capability/report.md b/qa/release-v2/reports/repro-browser-zoom-capability/report.md new file mode 100644 index 000000000..a9f6aaf4b --- /dev/null +++ b/qa/release-v2/reports/repro-browser-zoom-capability/report.md @@ -0,0 +1,157 @@ +# Browser zoom capability repro + +Date: 2026-08-31 +Target: `http://127.0.0.1:34401` (rendered login page, `/auth/login`) +Browser: Google Chrome for Testing `152.0.7977.64` +CLI: `agent-browser 0.27.0` + +## Result + +True browser zoom is repeatable through a disposable Manifest V3 extension +loaded by `agent-browser`. The extension calls `chrome.tabs.setZoom(tabId, +factor)` with `0.8`, `1.25`, or `2.0`. + +This satisfies the requested invariant at every factor and viewport tested: + +- `visualViewport.scale` stays `1`. +- CSS viewport dimensions change by the inverse zoom factor. +- `devicePixelRatio` tracks the browser zoom factor. +- The rendered `h1` bounds change with the layout. + +The agent-browser `press Control+-` path is not native browser zoom. It +dispatches a page key event and left all measured values unchanged. The CLI +reference also has no direct Chrome browser-zoom command. Do not relabel +`Emulation.setPageScaleFactor`, CSS `zoom`, or viewport/device emulation as +browser zoom; those are separate mechanisms. + +The provided vault login helper was attempted without reading vault metadata +or credential values. The rendered login page was sufficient for this +browser-level probe, so no product interaction or mutation was required. + +## Repeatable method + +1. Create a disposable extension with `tabs` permission and host permission + limited to the candidate app. Set `ZOOM_FACTOR` to the desired value. + + `manifest.json`: + + ```json + { + "manifest_version": 3, + "name": "Disposable native zoom probe", + "version": "1.0.0", + "permissions": ["tabs"], + "host_permissions": ["http://127.0.0.1:34401/*"], + "background": { "service_worker": "background.js" } + } + ``` + + `background.js`: + + ```js + const ZOOM_FACTOR = 1.25; // replace with 0.8, 1.25, or 2.0 + + function applyZoom(tab) { + if (tab?.id == null || !tab.url?.startsWith("http://127.0.0.1:34401")) return; + chrome.tabs.setZoom(tab.id, ZOOM_FACTOR); + } + + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === "complete") applyZoom({ ...tab, id: tabId }); + }); + + chrome.tabs.query({}, (tabs) => tabs.forEach(applyZoom)); + ``` + +2. Start a fresh, uniquely named session and disposable profile. On this + headless host, extensions require explicit `--headless=new`; keep + `--no-sandbox` in the launch args. The non-extension baseline sessions + used the exact `--args '--no-sandbox'` form: + + ```bash + npx -y --package agent-browser agent-browser \ + --session zoom-extension-- \ + --profile /tmp/agent-browser-zoom-profile-- \ + --extension /tmp/agent-browser-native-zoom-extension-- \ + --headed false \ + --args '--no-sandbox,--headless=new' \ + open http://127.0.0.1:34401 + ``` + +3. Set the exact viewport and measure the same fixed element: + + ```bash + npx -y --package agent-browser agent-browser \ + --session zoom-extension-- set viewport 320 568 + + npx -y --package agent-browser agent-browser \ + --session zoom-extension-- eval \ + "(()=>{const e=document.querySelector('h1')||document.body;const r=e.getBoundingClientRect();const v=window.visualViewport;return {innerWidth,innerHeight,outerWidth,outerHeight,devicePixelRatio,visualViewport:{width:v?.width,height:v?.height,scale:v?.scale},fixed:{x:r.x,y:r.y,width:r.width,height:r.height}}})()" + ``` + +4. Capture the screenshot, then close the session. Use a fresh profile or + clear the disposable profile before testing another factor: + + ```bash + npx -y --package agent-browser agent-browser \ + --session zoom-extension-- screenshot \ + /absolute/path/.screenshots/release-v2/repro-browser-zoom-capability/native--320x568.png + npx -y --package agent-browser agent-browser \ + --session zoom-extension-- close + ``` + +The same method was run at `1920 1080`. A native extension zoom session must +be restarted when changing the factor; changing only the viewport is safe. + +## Measurements + +The fixed probe is the first rendered `h1` (`Log in to your account`). Bounds +are CSS pixels in `x, y, width, height` order. `outer` is the browser window +metric reported by Chromium; it is intentionally included because it stays +at the window size while the CSS viewport changes. + +### 320x568 requested viewport + +| Zoom | inner (W x H) | outer (W x H) | devicePixelRatio | visualViewport (W x H / scale) | fixed h1 bounds | +| --- | --- | --- | --- | --- | --- | +| 100% baseline | 320 x 568 | 500 x 711 | 1 | 305 x 568 / 1 | 87, 177, 191, 57.59375 | +| 80% native | 400 x 710 | 500 x 711 | 0.800000011920929 | 381.25 x 710 / 1 | 87.2265625, 201.23046875, 266.7578125, 28.7890625 | +| 125% native | 256 x 454 | 500 x 711 | 1.25 | 244 x 454.3999938964844 / 1 | 86.80000305175781, 176.8000030517578, 130.40000915527344, 86.4000015258789 | +| 200% native | 160 x 284 | 500 x 711 | 2 | 152.5 x 284 / 1 | 87, 177, 95.5703125, 86.390625 | + +### 1920x1080 requested viewport + +| Zoom | inner (W x H) | outer (W x H) | devicePixelRatio | visualViewport (W x H / scale) | fixed h1 bounds | +| --- | --- | --- | --- | --- | --- | +| 100% baseline | 1920 x 1080 | 1920 x 1223 | 1 | 1920 x 1080 / 1 | 765, 418.71875, 340.71875, 28.796875 | +| 80% native | 2400 x 1350 | 1920 x 1223 | 0.800000011920929 | 2400 x 1350 / 1 | 1005.21484375, 562.734375, 340.64453125, 28.7890625 | +| 125% native | 1536 x 864 | 1920 x 1223 | 1.25 | 1536 x 864 / 1 | 572.7999877929688, 320.20001220703125, 340.7250061035156, 28.80000114440918 | +| 200% native | 960 x 540 | 1920 x 1223 | 2 | 952.5 x 540 / 1 | 281.25, 295.3984375, 340.71875, 28.796875 | + +## Negative controls + +At 320x568, `press Control+-` produced no change from the 100% baseline: +`innerWidth=320`, `innerHeight=568`, `outerWidth=500`, `outerHeight=711`, +`devicePixelRatio=1`, `visualViewport=305x568`, `scale=1`, and the fixed h1 +remained `87,177,191,57.59375`. The corresponding capture is +`keyboard-control-noop-320x568.png`. + +Pre-seeding a disposable profile with `profile.default_zoom_level`, +`partition.default_zoom_level`, and a localhost `per_host_zoom_levels` entry +also did not alter metrics when launched through agent-browser; it is not a +reliable path here. A first extension launch without `--headless=new` exited +before CDP with a missing X server error. + +## Evidence artifacts + +All captures are 8-bit PNGs at the requested pixel dimensions under +`.screenshots/release-v2/repro-browser-zoom-capability/`: + +- `baseline-320x568-100.png` +- `baseline-1920x1080-100.png` +- `native-80-320x568.png`, `native-80-1920x1080.png` +- `native-125-320x568.png`, `native-125-1920x1080.png` +- `native-200-320x568.png`, `native-200-1920x1080.png` +- `keyboard-control-noop-320x568.png` + +No product tests, builds, Docker compilation, or E2E tests were run. diff --git a/qa/release-v2/reports/repro-preflight-02-readonly/report.md b/qa/release-v2/reports/repro-preflight-02-readonly/report.md new file mode 100644 index 000000000..2b8a81ba1 --- /dev/null +++ b/qa/release-v2/reports/repro-preflight-02-readonly/report.md @@ -0,0 +1,75 @@ +# Release-v2 preflight reproduction (read-only) + +| Field | Value | +|---|---| +| Candidate SHA | `f57a660088d6777c86aca22977354fa8b810e2be` | +| App | `http://127.0.0.1:34089` | +| Fixture | `http://127.0.0.1:44743` (not directly navigated) | +| Persona | Avery Admin | +| Board | `/boards/qa-grid-24` | +| Browser profile | Desktop, 1440x900, 100% scale, mouse + keyboard | +| Date | 2026-08-31 | +| Artifact root | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/` | + +Only rendered UI, sanitized network status metadata, and sanitized console/error metadata were used. No request bodies, headers, cookies, or query values were read or recorded. + +## Summary + +| Claim | Outcome | Reproducibility | +|---|---|---| +| `release-v2-readonly-edit-affordance` | **reproduced** | Edit controls and transient changed value observed; value absent after exit, reload, away/back, and a fresh named session | +| `release-v2-static-chunk-404` | **reproduced** | Same `404 Script /_next/static/chunks/_1xuxkfk._.js` on two clean reloads in separate named sessions | +| `release-v2-posthog-duplicate-init-warning` | **reproduced** | Same warning on two clean reloads in separate named sessions; page errors remained `0` | + +## `release-v2-readonly-edit-affordance` + +Expected: a read-only board should hide or block mutation affordances and should not accept a changed value. + +Actual: the board showed `Edit board`. Edit mode exposed per-item `Settings for Date and time`, `Edit item`, and `Save changes`. The custom-title field accepted the unique marker `RV2 readonly probe`; the marker immediately appeared on the item while edit mode remained open. No success or failure toast/status was shown. There was no separate board-save button; `Exit edit mode` was the board-level completion step. + +After clicking `Exit edit mode`, the marker disappeared immediately. It was also absent after a clean reload, after navigating to `/` and back to `/boards/qa-grid-24`, and in a fresh authenticated named session. The UI therefore allowed a transient mutation, while persistence was blocked or discarded. No API error was surfaced; the API result cannot be classified further from the retained sanitized metadata without reading request contents. + +Steps and evidence: + +1. Open `/boards/qa-grid-24` in named session `rv2-ro-edit-01` and authenticate as Avery Admin. The first visible mutation affordance is `Edit board` — [claim1-step-01-board.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-01-board.png). +2. Click `Edit board`; edit mode exposes item groups and settings controls — [claim1-step-03-edit-mode.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-03-edit-mode.png). +3. Open an existing `Date and time` item, choose `Edit item`, and enable `Custom Title/City display` — [claim1-step-07-item-edit-form.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-07-item-edit-form.png). +4. Type `RV2 readonly probe` into `Title` — [claim1-step-08-marker-entered.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-08-marker-entered.png). +5. Click `Save changes`. The marker is rendered on the item and the form closes, with no notification — [claim1-step-09-item-saved.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-09-item-saved.png). +6. Click `Exit edit mode`. The marker is no longer rendered — [claim1-step-10-after-exit-edit.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-10-after-exit-edit.png). +7. In `rv2-ro-persistence-01`, reload and verify the marker is absent — [claim1-step-12-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-12-after-reload.png). +8. Navigate to `/`, return to the QA grid, and verify it is still absent — [claim1-step-13-navigate-back.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-13-navigate-back.png). +9. Open the board in a fresh named session and verify the marker is absent — [claim1-step-11-fresh-session-marker-absent.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-11-fresh-session-marker-absent.png). + +Interactive recording: [claim1-edit-repro.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-edit-repro.webm). + +## `release-v2-static-chunk-404` + +Expected: a clean board reload should not request a missing static JavaScript chunk. + +Actual: after clearing the request log, reloading, and waiting for `domcontentloaded`, both clean named sessions recorded the same sanitized failure: + +`404 Script /_next/static/chunks/_1xuxkfk._.js` + +Evidence: + +- [claim2-session1-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim2-session1-network.txt) — `rv2-ro-persistence-01` +- [claim2-session2-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim2-session2-network.txt) — `rv2-ro-static-console-02` + +The board still rendered after each reload. Only status, resource type, and query-free path were retained. + +## `release-v2-posthog-duplicate-init-warning` + +Expected: a clean reload should not emit a duplicate PostHog initialization warning. + +Actual: after clearing console and page-error buffers, reloading, and waiting for `domcontentloaded`, both named sessions emitted exactly: + +`[PostHog.js] You have already initialized PostHog! Re-initializing is a no-op` + +The page rendered normally and the page-error buffer contained `0` errors on both runs. Evidence: + +- [claim3-session1-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim3-session1-console.txt) — `rv2-ro-persistence-01` +- [claim3-session2-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim3-session2-console.txt) — `rv2-ro-static-console-02` +- [claim3-session2-page-rendered.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim3-session2-page-rendered.png) + +All three named sessions were closed after capture. The initial edit session became unresponsive during recording/reload cleanup; it was terminated by its unique browser profile, and the reload/navigation and fresh-session persistence checks were completed in new named sessions. diff --git a/qa/release-v2/reports/repro-weather-mobile/report.md b/qa/release-v2/reports/repro-weather-mobile/report.md new file mode 100644 index 000000000..97f746f8f --- /dev/null +++ b/qa/release-v2/reports/repro-weather-mobile/report.md @@ -0,0 +1,65 @@ +# Black-box reproduction report: `release-v2-mobile-weather-card-top-clipping` + +| Field | Value | +|---|---| +| Verdict | **REPRODUCED** at 320×568; also present with less overlap at 375×812; not present at 1920×1080 | +| Date | 2026-08-31 (Europe/Berlin) | +| App | `http://127.0.0.1:34401/` | +| Fixture | `http://127.0.0.1:37273/` (not navigated; no fixture mutation was needed) | +| Build | `f57a660088d6777c86aca22977354fa8b810e2be` | +| Flags | `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, `UNSAFE_ENABLE_MOCK_INTEGRATION=true` | +| Persona | Casey Chaos, seeded home board | +| Browser | Chromium via named `agent-browser` sessions; DPR 1; default 100% zoom; no visualViewport scale or pinch emulation | +| Sessions | `weather-clip-a`, then fresh `weather-clip-b` | + +## Summary + +At the exact 320×568 viewport, the rendered `19.3°C` current-temperature line box begins above the weather card’s clipped content window. The card has `overflow: hidden` on both axes, so the upper 15.4375 CSS/device pixels of the line box are outside the clip window. The screenshot visibly shows the top of `19.3°C` cut off. + +The result reproduced after reload in Session A and again in independent fresh Session B (including Session B reload). Mouse activation and keyboard Enter activation of “Show weather details” both worked; neither changed the clipping geometry or dashboard data. + +## Exact reproduction steps + +1. Open `http://127.0.0.1:34401/` in a fresh named session with Chrome launch arg `--no-sandbox`. +2. Authenticate with vault profile `qa-v2-main-casey-chaos`. The vault supplied the password without exposing it; the rendered username field required the non-secret profile username `casey-chaos` to be filled before submitting. +3. Confirm the seeded `QA v2 · Home · casey-chaos` board is shown and leave the dashboard unedited. +4. Set the viewport to exactly `320×568` at the default browser zoom. Do not use device emulation, visualViewport scaling, or pinch emulation. +5. Wait for the weather card to show `19.3°C`, `Mainly clear`, `22.9°C`, `17.5°C`, and `Paris`; capture the plain and annotated screenshots below. +6. **Mouse path:** move to the visible “Show weather details” control (around `(60,196)` in the 320×568 viewport), press and release the mouse button, then press `Escape`; the details dialog opens and closes. +7. **Keyboard path:** focus “Show weather details” and press `Enter`; the same details dialog opens. Close it with `Escape`. The clipping remains visible on the card. +8. Reload the page and repeat steps 4–7. Close Session A, start fresh Session B, authenticate with the same vault profile, and repeat the 320×568 capture and input paths. Both fresh-load and reload captures show the same clipping. + +## Rendered geometry + +All values are viewport CSS pixels with `devicePixelRatio = 1`. “Card bounds” is the outer weather group; “clip window” is the rendered descendant with `overflow: hidden`. “Clipped area” is the rectangle intersection excluded by that clip window (line-box area, not an estimate of opaque glyph ink). + +| Viewport | Weather card bounds (x, y, w, h) | Clip window bounds (x, y, w, h) | Current-temperature text bounds (x, y, w, h) | Overflow | Clipped pixels / height | +|---|---:|---:|---:|---|---:| +| 320×568 | (26, 182, 172, 76) | (27, 183, 170, 74) | (85.953, 167.563, 94.078, 31.5) | `hidden` / `hidden` | 1,452.331 px² (ceil 1,453), 15.438 px | +| 375×812 | (26, 200.328, 208.656, 94.328) | (27, 201.328, 206.656, 92.328) | (104.281, 195.047, 94.078, 31.5) | `hidden` / `hidden` | 590.928 px² (ceil 591), 6.281 px | +| 1920×1080 | (340.656, 86, 294.656, 137.328) | (341.656, 87, 292.656, 135.328) | (461.938, 100.219, 94.078, 31.5) | `hidden` / `hidden` | 0 px² (0), 0 px | + +At 320×568, the text line box is visible only from y=183 to y=199.063 (16.063 px of 31.5 px); its top is 15.438 px above the clip window. At 375×812, the same relationship remains but is reduced to 6.281 px. At 1920×1080, the complete line box sits inside the clip window. + +## Screenshot evidence + +The first pair is the initial 320×568 reproduction. The reload and fresh-session pairs verify repeatability. The 375×812 and 1920×1080 pairs delimit the behavior. + +| Capture | Plain | Annotated | +|---|---|---| +| Session A, 320×568 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-annotated.png` | +| Session A, 320×568 reload | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-reload-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-reload-annotated.png` | +| Session B, fresh 320×568 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-annotated.png` | +| Session B, 320×568 reload | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-reload-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-reload-annotated.png` | +| Session B, 375×812 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-375x812-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-375x812-annotated.png` | +| Session B, 1920×1080 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-1920x1080-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-1920x1080-annotated.png` | + +## Console, errors, and network + +- After clearing and reloading Session B, `errors` returned no page errors. +- Console output contained only the React DevTools informational message and `[HMR] connected`; no exception or unhandled rejection was observed. +- Sanitized same-origin request status lines observed on reload were successful (`200`), including the document `/`, `/api/auth/session`, `/auth/login` during the auth handoff, and the loaded Next/static assets. No same-origin 4xx/5xx status was observed. Response bodies were not retained. + +## Scope and cleanup + +No dashboard edits, settings changes, network mocking, source inspection, DB/runtime inspection, 200% zoom testing, or fixture mutation was performed. Both named browser sessions were closed after capture. diff --git a/qa/release-v2/reports/whole-product-01/report.md b/qa/release-v2/reports/whole-product-01/report.md new file mode 100644 index 000000000..ea3e056c7 --- /dev/null +++ b/qa/release-v2/reports/whole-product-01/report.md @@ -0,0 +1,149 @@ + + +# whole-product-01: Admin day-one journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6545, #6569 | +| Personas | Avery Admin, Nora Newcomer | +| Boards | qa-grid-24 | +| Profiles | onboarding-fresh, main-writable | +| Chromium viewports | desktop-1440 | +| Zoom | 100% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Avery Admin | +| Browser session ID | qa-v2-whole-product-01 | +| Timestamp | 2026-08-31T17:20:15.819Z | +| Executed viewport / input / zoom | desktop-1440 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-001-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-001-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-001-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-001-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, desktop-1440, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-01. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [entry-attempt-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/entry-attempt-console.txt) +- [entry-attempt-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/entry-attempt-errors.txt) +- [entry-attempt-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/entry-attempt-network.txt) +- [entry-attempt-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/entry-attempt-url.txt) +- [entry-attempt.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/entry-attempt.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-02/report.md b/qa/release-v2/reports/whole-product-02/report.md new file mode 100644 index 000000000..bbed7d638 --- /dev/null +++ b/qa/release-v2/reports/whole-product-02/report.md @@ -0,0 +1,145 @@ + + +# whole-product-02: Owner customization journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6356, #6503, #6502 | +| Personas | Rowan Owner | +| Boards | qa-grid-24, qa-custom-widget-assistant | +| Profiles | main-writable | +| Chromium viewports | desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Rowan Owner | +| Browser session ID | qa-v2-whole-product-02 | +| Timestamp | 2026-08-31T17:26:12.839Z | +| Executed viewport / input / zoom | desktop-1920 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-002-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-002-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-002-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-002-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, desktop-1920, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-02. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [entry-1920x1080-100-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/entry-1920x1080-100-redirect.png) +- [entry-console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/entry-console-scoped.txt) +- [entry-network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/entry-network-scoped.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-03/report.md b/qa/release-v2/reports/whole-product-03/report.md new file mode 100644 index 000000000..8e5b5a770 --- /dev/null +++ b/qa/release-v2/reports/whole-product-03/report.md @@ -0,0 +1,158 @@ + + +# whole-product-03: Editor daily journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6503, #6502, #6450 | +| Personas | Eden Editor | +| Boards | qa-scroll-lab, qa-layout-boundaries | +| Profiles | main-writable | +| Chromium viewports | tablet-1024, desktop-1280 | +| Zoom | 100% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Eden Editor | +| Browser session ID | qa-v2-whole-product-03 | +| Timestamp | 2026-08-31T17:34:20.573Z | +| Executed viewport / input / zoom | tablet-1024 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-003-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-003-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-003-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-003-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, tablet-1024, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-03. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console-retry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/console-retry.txt) +- [console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/console.txt) +- [entry-1024x768-mouse-retry.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/entry-1024x768-mouse-retry.png) +- [entry-blocked.md](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/entry-blocked.md) +- [errors-retry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/errors-retry.txt) +- [errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/errors.txt) +- [network-retry.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/network-retry.txt) +- [network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/network.txt) +- [retry-summary.md](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/retry-summary.md) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-04/report.md b/qa/release-v2/reports/whole-product-04/report.md new file mode 100644 index 000000000..0be0cb343 --- /dev/null +++ b/qa/release-v2/reports/whole-product-04/report.md @@ -0,0 +1,147 @@ + + +# whole-product-04: Viewer and outsider boundaries + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6545 | +| Personas | Vivian Viewer, Nolan Outsider | +| Boards | qa-permissions-public | +| Profiles | main-writable, main-readonly | +| Chromium viewports | mobile-375, desktop-1440 | +| Zoom | 100% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Vivian Viewer | +| Browser session ID | qa-v2-whole-product-04 | +| Timestamp | 2026-08-31T17:38:48.712Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-004-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-004-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-004-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-004-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-04. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/console.txt) +- [entry-375x812-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/entry-375x812-annotated.png) +- [errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/errors.txt) +- [network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-05/report.md b/qa/release-v2/reports/whole-product-05/report.md new file mode 100644 index 000000000..818b97fc2 --- /dev/null +++ b/qa/release-v2/reports/whole-product-05/report.md @@ -0,0 +1,147 @@ + + +# whole-product-05: Mobile journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6450, #6503, #6502 | +| Personas | Morgan Mobile | +| Boards | qa-layout-boundaries, qa-widgets-01 | +| Profiles | main-writable | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768 | +| Zoom | 100%, 200% | +| Input | touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Morgan Mobile | +| Browser session ID | qa-v2-whole-product-05 | +| Timestamp | 2026-08-31T17:45:01.935Z | +| Executed viewport / input / zoom | mobile-320 / touch / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-005-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-005-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-005-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-005-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, touch, fresh named Chromium session qa-v2-whole-product-05. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console-scoped-redacted.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/console-scoped-redacted.txt) +- [entry-320x568-100-touch-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/entry-320x568-100-touch-annotated.png) +- [errors-scoped-redacted.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/errors-scoped-redacted.txt) +- [network-scoped-redacted.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/network-scoped-redacted.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-06/report.md b/qa/release-v2/reports/whole-product-06/report.md new file mode 100644 index 000000000..d6b868542 --- /dev/null +++ b/qa/release-v2/reports/whole-product-06/report.md @@ -0,0 +1,153 @@ + + +# whole-product-06: Media operator journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6502, #6555 | +| Personas | Maya Media | +| Boards | qa-widgets-04, qa-widgets-05 | +| Profiles | main-writable, degraded | +| Chromium viewports | desktop-1440 | +| Zoom | 100% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Maya Media | +| Browser session ID | qa-v2-whole-product-06 | +| Timestamp | 2026-08-31T17:50:42.531Z | +| Executed viewport / input / zoom | desktop-1440 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-006-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-006-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-006-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-006-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, desktop-1440, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-06. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console-scoped-sanitized.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/console-scoped-sanitized.txt) +- [entry-blocked-annotated.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/entry-blocked-annotated.png) +- [entry-blocked-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/entry-blocked-snapshot.txt) +- [entry-title.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/entry-title.txt) +- [entry-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/entry-url.txt) +- [errors-scoped-sanitized.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/errors-scoped-sanitized.txt) +- [network-scoped-sanitized.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/network-scoped-sanitized.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-07/report.md b/qa/release-v2/reports/whole-product-07/report.md new file mode 100644 index 000000000..6ba9c6b7a --- /dev/null +++ b/qa/release-v2/reports/whole-product-07/report.md @@ -0,0 +1,153 @@ + + +# whole-product-07: Infrastructure operator journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-07, qa-widgets-08, qa-widgets-09, qa-widgets-10, qa-widgets-11 | +| Profiles | main-writable, degraded | +| Chromium viewports | desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-whole-product-07 | +| Timestamp | 2026-08-31T17:55:10.068Z | +| Executed viewport / input / zoom | desktop-1920 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-007-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-007-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-007-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-007-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, desktop-1920, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-07. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/console.txt) +- [entry-1920x1080-100-mouse-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/entry-1920x1080-100-mouse-after-reload.png) +- [entry-1920x1080-100-mouse-viewport.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/entry-1920x1080-100-mouse-viewport.png) +- [entry-1920x1080-100-mouse.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/entry-1920x1080-100-mouse.png) +- [errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/errors.txt) +- [network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-08/report.md b/qa/release-v2/reports/whole-product-08/report.md new file mode 100644 index 000000000..fa76a48d6 --- /dev/null +++ b/qa/release-v2/reports/whole-product-08/report.md @@ -0,0 +1,153 @@ + + +# whole-product-08: Creator and Assistant journey + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6356, #6482 | +| Personas | Cora Creator, Ash Assistant | +| Boards | qa-custom-widget-assistant | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-375, desktop-1440 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Cora Creator | +| Browser session ID | qa-v2-whole-product-08 | +| Timestamp | 2026-08-31T18:00:33.334Z | +| Executed viewport / input / zoom | mobile-375 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-008-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-008-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-008-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-008-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, mobile-375, 100% zoom, mouse, fresh named Chromium session qa-v2-whole-product-08. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/console-scoped.txt) +- [entry-375x812-100-mouse.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/entry-375x812-100-mouse.png) +- [entry-title.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/entry-title.txt) +- [entry-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/entry-url.txt) +- [errors-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/errors-scoped.txt) +- [network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/network-scoped.txt) +- [session-info.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/session-info.json) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/whole-product-09/report.md b/qa/release-v2/reports/whole-product-09/report.md new file mode 100644 index 000000000..79d19680d --- /dev/null +++ b/qa/release-v2/reports/whole-product-09/report.md @@ -0,0 +1,147 @@ + + +# whole-product-09: Accessibility and destructive-action pass + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | whole-product | +| PR refs | #6356, #6503, #6502, #6450, #6482, #6569, #6545 | +| Personas | Kira Keyboard, Casey Chaos | +| Boards | qa-grid-24, qa-download-upload, qa-custom-widget-assistant | +| Profiles | main-writable, main-readonly, degraded | +| Chromium viewports | mobile-320, desktop-1440 | +| Zoom | 100%, 200% | +| Input | keyboard | +| Widget kinds | None | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:46777 / 46777 | +| Runtime profile / flags | degraded; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Kira Keyboard | +| Browser session ID | qa-v2-whole-product-09 | +| Timestamp | 2026-08-31T18:05:08.871Z | +| Executed viewport / input / zoom | mobile-320 / keyboard / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WP-009-JOURNEY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. | +| WP-009-THRESHOLDS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. | +| WP-009-PERSISTENCE-ACCESS | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. | +| WP-009-FAILURE-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, degraded, mobile-320, 100% zoom, keyboard, fresh named Chromium session qa-v2-whole-product-09. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:46777 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [console-320x568-100-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/console-320x568-100-scoped.txt) +- [entry-320x568-100-keyboard.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/entry-320x568-100-keyboard.png) +- [errors-320x568-100-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/errors-320x568-100-scoped.txt) +- [network-320x568-100-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/network-320x568-100-scoped.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-01/report.md b/qa/release-v2/reports/widgets-01/report.md new file mode 100644 index 000000000..1d64db7f3 --- /dev/null +++ b/qa/release-v2/reports/widgets-01/report.md @@ -0,0 +1,505 @@ + + +# widgets-01: Time and environment + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Brooke Minimalist | +| Boards | qa-widgets-01 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | clock, weather, airQuality, countdown, timer | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Brooke Minimalist | +| Browser session ID | qa-v2-widgets-01 | +| Timestamp | 2026-08-31T15:54:37.376Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-001-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-001-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-001-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-001-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-001-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| clock | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| clock | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| weather | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| airQuality | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| countdown | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | +| timer | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-01. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [initial-320x568-console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568-console-scoped.txt) +- [initial-320x568-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568-console.txt) +- [initial-320x568-network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568-network-scoped.txt) +- [initial-320x568-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568-network.txt) +- [initial-320x568-recorded-frame.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568-recorded-frame.png) +- [initial-320x568-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568-url.txt) +- [initial-320x568.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568.png) +- [initial-320x568.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/initial-320x568.webm) +- [retry-320x568-console-before.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-console-before.txt) +- [retry-320x568-console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-console-scoped.txt) +- [retry-320x568-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-console.txt) +- [retry-320x568-network-before.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-network-before.txt) +- [retry-320x568-network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-network-scoped.txt) +- [retry-320x568-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-network.txt) +- [retry-320x568-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568-url.txt) +- [retry-320x568.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568.png) +- [retry-320x568.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/retry-320x568.webm) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-02/report.md b/qa/release-v2/reports/widgets-02/report.md new file mode 100644 index 000000000..9475fe5d6 --- /dev/null +++ b/qa/release-v2/reports/widgets-02/report.md @@ -0,0 +1,470 @@ + + +# widgets-02: Apps, embeds, video, game status, and stocks + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Cora Creator | +| Boards | qa-widgets-02 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 200% | +| Input | mouse, touch, keyboard | +| Widget kinds | app, iframe, video, minecraftServerStatus, stockPrice | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Cora Creator | +| Browser session ID | qa-v2-widgets-02 | +| Timestamp | 2026-08-31T15:58:22.521Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-002-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-002-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-002-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-002-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-002-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| app | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| app | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| iframe | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| video | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| minecraftServerStatus | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | +| stockPrice | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-02. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by another Luna Max preflight agent. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-initial-320x568.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/screenshots/redirect-initial-320x568.png) +- [redirect-retry-320x568.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/screenshots/redirect-retry-320x568.png) +- [redirect-retry-320x568.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/videos/redirect-retry-320x568.webm) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-03/report.md b/qa/release-v2/reports/widgets-03/report.md new file mode 100644 index 000000000..085d5824a --- /dev/null +++ b/qa/release-v2/reports/widgets-03/report.md @@ -0,0 +1,478 @@ + + +# widgets-03: Notes, bookmarks, feeds, and timetable + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Cora Creator | +| Boards | qa-widgets-03 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | notebook, anchorNote, bookmarks, rssFeed, timetable | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Cora Creator | +| Browser session ID | qa-v2-widgets-03 | +| Timestamp | 2026-08-31T16:03:07.150Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-003-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-003-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-003-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-003-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-003-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| notebook | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| notebook | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| anchorNote | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| bookmarks | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| rssFeed | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | +| timetable | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-03. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [initial-320x568-console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568-console-scoped.txt) +- [initial-320x568-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568-errors.txt) +- [initial-320x568-network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568-network-scoped.txt) +- [initial-320x568-recorded-frame.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568-recorded-frame.png) +- [initial-320x568-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568-snapshot.txt) +- [initial-320x568-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568-url.txt) +- [initial-320x568.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/initial-320x568.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-04/report.md b/qa/release-v2/reports/widgets-04/report.md new file mode 100644 index 000000000..f18104214 --- /dev/null +++ b/qa/release-v2/reports/widgets-04/report.md @@ -0,0 +1,487 @@ + + +# widgets-04: Downloads, containers, indexers, and DNS + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-04 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | downloads, dockerContainers, indexerManager, dnsHoleSummary, dnsHoleControls | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-widgets-04 | +| Timestamp | 2026-08-31T16:05:32.566Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-004-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-004-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-004-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-004-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-004-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| downloads | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| downloads | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dockerContainers | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| indexerManager | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleSummary | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | +| dnsHoleControls | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-04. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-fresh-retry-320x568-100-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/logs/redirect-fresh-retry-320x568-100-console.txt) +- [redirect-fresh-retry-320x568-100-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/logs/redirect-fresh-retry-320x568-100-errors.txt) +- [redirect-fresh-retry-320x568-100-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/logs/redirect-fresh-retry-320x568-100-network.txt) +- [redirect-initial-320x568-100-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/logs/redirect-initial-320x568-100-console.txt) +- [redirect-initial-320x568-100-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/logs/redirect-initial-320x568-100-errors.txt) +- [redirect-initial-320x568-100-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/logs/redirect-initial-320x568-100-network.txt) +- [redirect-fresh-retry-320x568-100-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/screenshots/redirect-fresh-retry-320x568-100-before.png) +- [redirect-fresh-retry-320x568-100-result.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/screenshots/redirect-fresh-retry-320x568-100-result.png) +- [redirect-fresh-retry-320x568-100.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/videos/redirect-fresh-retry-320x568-100.webm) +- [redirect-initial-320x568-100.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/videos/redirect-initial-320x568-100.webm) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-05/report.md b/qa/release-v2/reports/widgets-05/report.md new file mode 100644 index 000000000..43a6b59b6 --- /dev/null +++ b/qa/release-v2/reports/widgets-05/report.md @@ -0,0 +1,471 @@ + + +# widgets-05: Smart home, health, and system telemetry + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-05 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 200% | +| Input | mouse, touch, keyboard | +| Widget kinds | smartHome-entityState, smartHome-executeAutomation, healthMonitoring, systemResources, systemDisks | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-widgets-05 | +| Timestamp | 2026-08-31T16:07:56.618Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-005-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-005-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-005-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-005-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-005-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| smartHome-entityState | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-entityState | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| smartHome-executeAutomation | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| healthMonitoring | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemResources | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | +| systemDisks | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-05. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [initial-320x568-console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/initial-320x568-console-scoped.txt) +- [initial-320x568-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/initial-320x568-errors.txt) +- [initial-320x568-network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/initial-320x568-network-scoped.txt) +- [initial-320x568-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/initial-320x568-redirect.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-06/report.md b/qa/release-v2/reports/widgets-06/report.md new file mode 100644 index 000000000..177d1ba49 --- /dev/null +++ b/qa/release-v2/reports/widgets-06/report.md @@ -0,0 +1,475 @@ + + +# widgets-06: Network availability and operations + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-06 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | firewall, notifications, networkControllerSummary, networkControllerStatus, uptimeKuma | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-widgets-06 | +| Timestamp | 2026-08-31T16:11:37.513Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-006-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-006-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-006-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-006-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-006-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| firewall | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| firewall | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| notifications | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerSummary | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| networkControllerStatus | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | +| uptimeKuma | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-06. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [initial-320x568-console-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/initial-320x568-console-scoped.txt) +- [initial-320x568-errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/initial-320x568-errors.txt) +- [initial-320x568-network-scoped.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/initial-320x568-network-scoped.txt) +- [initial-320x568-redirect.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/initial-320x568-redirect.png) +- [initial-320x568-snapshot.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/initial-320x568-snapshot.txt) +- [initial-320x568-url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/initial-320x568-url.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-07/report.md b/qa/release-v2/reports/widgets-07/report.md new file mode 100644 index 000000000..07e510bad --- /dev/null +++ b/qa/release-v2/reports/widgets-07/report.md @@ -0,0 +1,471 @@ + + +# widgets-07: Beszel and update monitoring + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-07 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | beszelSystemTable, beszelSystemGrid, beszelAlerts, beszelSystemStats, wud | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-widgets-07 | +| Timestamp | 2026-08-31T16:12:58.557Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-007-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-007-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-007-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-007-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-007-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| beszelSystemTable | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemTable | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemGrid | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelAlerts | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| beszelSystemStats | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | +| wud | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-07. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/redirect-320x568-100.png) +- [redirect-console-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/redirect-console-320x568-100.txt) +- [redirect-errors-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/redirect-errors-320x568-100.txt) +- [redirect-network-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/redirect-network-320x568-100.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-08/report.md b/qa/release-v2/reports/widgets-08/report.md new file mode 100644 index 000000000..f727bb66a --- /dev/null +++ b/qa/release-v2/reports/widgets-08/report.md @@ -0,0 +1,494 @@ + + +# widgets-08: Power, VPN, speed, routing, and analytics + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-08 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 200% | +| Input | mouse, touch, keyboard | +| Widget kinds | ups, vpn, speedtestTracker, traefik, umami | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-widgets-08 | +| Timestamp | 2026-08-31T16:20:06.432Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-008-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-008-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-008-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-008-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-008-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| ups | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| ups | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| vpn | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| speedtestTracker | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| traefik | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | +| umami | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-08. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Close the context, retry in a fresh context, and observe the same result. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced on retry and independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-320x568-100-fresh.console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100-fresh.console.txt) +- [redirect-320x568-100-fresh.errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100-fresh.errors.txt) +- [redirect-320x568-100-fresh.network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100-fresh.network.txt) +- [redirect-320x568-100-fresh.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100-fresh.png) +- [redirect-320x568-100-fresh.url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100-fresh.url.txt) +- [redirect-320x568-100-fresh.viewport.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100-fresh.viewport.txt) +- [redirect-320x568-100.console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.console.txt) +- [redirect-320x568-100.errors.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.errors.txt) +- [redirect-320x568-100.network-local.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.network-local.txt) +- [redirect-320x568-100.network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.network.txt) +- [redirect-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.png) +- [redirect-320x568-100.title.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.title.txt) +- [redirect-320x568-100.url.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.url.txt) +- [redirect-320x568-100.viewport.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/redirect-320x568-100.viewport.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-09/report.md b/qa/release-v2/reports/widgets-09/report.md new file mode 100644 index 000000000..1b790cacc --- /dev/null +++ b/qa/release-v2/reports/widgets-09/report.md @@ -0,0 +1,485 @@ + + +# widgets-09: Media overview and requests + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Maya Media | +| Boards | qa-widgets-09 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 80%, 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | calendar, mediaServer, mediaRequests-requestList, mediaRequests-requestStats, mediaMissing | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Maya Media | +| Browser session ID | qa-v2-widgets-09 | +| Timestamp | 2026-08-31T16:19:53.769Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-009-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-009-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-009-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-009-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-009-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| calendar | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| calendar | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaServer | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestList | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaRequests-requestStats | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | +| mediaMissing | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-09. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-320x568-100.png) +- [redirect-console-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-console-320x568-100.txt) +- [redirect-errors-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-errors-320x568-100.txt) +- [redirect-final-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-final-320x568-100.png) +- [redirect-final-console-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-final-console-320x568-100.txt) +- [redirect-final-errors-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-final-errors-320x568-100.txt) +- [redirect-final-network-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-final-network-320x568-100.txt) +- [redirect-network-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-network-320x568-100.txt) +- [redirect-snapshot-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-snapshot-320x568-100.txt) +- [redirect-url-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/redirect-url-320x568-100.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-10/report.md b/qa/release-v2/reports/widgets-10/report.md new file mode 100644 index 000000000..51a73f0e7 --- /dev/null +++ b/qa/release-v2/reports/widgets-10/report.md @@ -0,0 +1,467 @@ + + +# widgets-10: Media activity, Immich, and audio + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Maya Media | +| Boards | qa-widgets-10 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 125% | +| Input | mouse, touch, keyboard | +| Widget kinds | mediaReleases, mediaTranscoding, immich-serverStats, immich-albumCarousel, audioStats | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Maya Media | +| Browser session ID | qa-v2-widgets-10 | +| Timestamp | 2026-08-31T16:28:44.862Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-010-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-010-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-010-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-010-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-010-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| mediaReleases | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaReleases | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| mediaTranscoding | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-serverStats | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| immich-albumCarousel | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | +| audioStats | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-10. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [widgets-10-redirect-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/widgets-10-redirect-320x568-100.png) +- [widgets-10-redirect-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/widgets-10-redirect-network.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-11/report.md b/qa/release-v2/reports/widgets-11/report.md new file mode 100644 index 000000000..3ae9b911a --- /dev/null +++ b/qa/release-v2/reports/widgets-11/report.md @@ -0,0 +1,469 @@ + + +# widgets-11: Documents, patching, media services, and releases + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6555 | +| Personas | Ingrid Infra | +| Boards | qa-widgets-11 | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 100%, 200% | +| Input | mouse, touch, keyboard | +| Widget kinds | paperlessNgx, patchmon, bazarr, tracearr, releases | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ingrid Infra | +| Browser session ID | qa-v2-widgets-11 | +| Timestamp | 2026-08-31T16:25:06.485Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-011-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-011-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-011-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-011-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-011-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| paperlessNgx | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| paperlessNgx | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| patchmon | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| bazarr | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| tracearr | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | +| releases | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-11. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/redirect-320x568-100.png) +- [redirect-console-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/redirect-console-320x568-100.txt) +- [redirect-network-320x568-100.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/redirect-network-320x568-100.txt) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/qa/release-v2/reports/widgets-12/report.md b/qa/release-v2/reports/widgets-12/report.md new file mode 100644 index 000000000..3f21f3b80 --- /dev/null +++ b/qa/release-v2/reports/widgets-12/report.md @@ -0,0 +1,406 @@ + + +# widgets-12: Coolify, ArchiveTeam, Custom API, and Assistant + +| Metadata | Value | +| --- | --- | +| Status | blocked | +| Wave | widgets | +| PR refs | #6502, #6482, #6555 | +| Personas | Ash Assistant, Cora Creator | +| Boards | qa-widgets-12, qa-custom-widget-assistant | +| Profiles | main-writable, degraded | +| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 | +| Zoom | 80%, 100%, 125%, 200% | +| Input | mouse, touch, keyboard | +| Widget kinds | coolify, archiveTeamWarrior, customApi, assistant | +| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | +| Candidate SHA | f57a660088d6777c86aca22977354fa8b810e2be | +| URL / actual port | http://127.0.0.1:37137 / 37137 | +| Runtime profile / flags | main-writable; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true | +| Executed persona | Ash Assistant | +| Browser session ID | qa-v2-widgets-12 | +| Timestamp | 2026-08-31T16:33:58.002Z | +| Executed viewport / input / zoom | mobile-320 / mouse / 100% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +| WG-012-RENDER-SIZE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. | +| WG-012-STATES-RECOVERY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. | +| WG-012-OPTIONS-PERSISTENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. | +| WG-012-ACCESS-READONLY | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. | +| WG-012-EVIDENCE | blocked | [browser artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. | + +## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| coolify | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| coolify | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| archiveTeamWarrior | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| customApi | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | mobile-320 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | mobile-375 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | boundary-767 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | boundary-768 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | tablet-1024 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | boundary-1279 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | desktop-1280 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | desktop-1440 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | +| assistant | desktop-1920 | every width 1-24 × every height 1-6 | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) | blocked | + +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by qa-v2-preflight-01; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate f57a660088d6777c86aca22977354fa8b810e2be, main-writable, mobile-320, 100% zoom, mouse, fresh named Chromium session qa-v2-widgets-12. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open http://127.0.0.1:37137 or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +5. Compare the result with the independently reproduced preflight evidence. + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: reproduced independently by the preflight agents. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +- [redirect-320x568-100.console.md](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/redirect-320x568-100.console.md) +- [redirect-320x568-100.network.md](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/redirect-320x568-100.network.md) +- [redirect-320x568-100.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/redirect-320x568-100.png) + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. diff --git a/scripts/release-v2-qa/fixture-server.mts b/scripts/release-v2-qa/fixture-server.mts new file mode 100644 index 000000000..faab176a8 --- /dev/null +++ b/scripts/release-v2-qa/fixture-server.mts @@ -0,0 +1,349 @@ +import { createHash } from "node:crypto"; +import { constants } from "node:fs"; +import { createServer } from "node:http"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import { readFile, mkdir, open, rename, rm } from "node:fs/promises"; +import path from "node:path"; + +import { validateLoopbackHost } from "./safety.mts"; + +const DEFAULT_HOST = "127.0.0.1"; +const MAX_BODY_BYTES = 2 * 1024 * 1024; +const MAX_SLOW_MS = 30_000; + +const png = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", + "base64", +); + +const videoPath = path.resolve( + import.meta.dirname, + "../../apps/docs/src/components/pages/home/drag-and-drop/showcase-dark.mp4", +); + +const createWav = () => { + const sampleRate = 8_000; + const sampleCount = sampleRate / 4; + const dataLength = sampleCount * 2; + const buffer = Buffer.alloc(44 + dataLength); + buffer.write("RIFF", 0); + buffer.writeUInt32LE(36 + dataLength, 4); + buffer.write("WAVEfmt ", 8); + buffer.writeUInt32LE(16, 16); + buffer.writeUInt16LE(1, 20); + buffer.writeUInt16LE(1, 22); + buffer.writeUInt32LE(sampleRate, 24); + buffer.writeUInt32LE(sampleRate * 2, 28); + buffer.writeUInt16LE(2, 32); + buffer.writeUInt16LE(16, 34); + buffer.write("data", 36); + buffer.writeUInt32LE(dataLength, 40); + for (let index = 0; index < sampleCount; index += 1) { + const value = Math.sin((2 * Math.PI * 440 * index) / sampleRate) * 0.2; + buffer.writeInt16LE(Math.round(value * 32_767), 44 + index * 2); + } + return buffer; +}; + +const wav = createWav(); + +const customWidgetPayload = { + title: "Release v2 QA fixture", + status: "ok", + items: [ + { id: "alpha", label: "Alpha", value: 42 }, + { id: "beta", label: "Beta", value: 7 }, + ], +}; + +const parseArgs = () => { + const args = process.argv.slice(2).filter((argument) => argument !== "--"); + let host = DEFAULT_HOST; + let port = 0; + let readyFile: string | undefined; + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + const value = args[index + 1]; + if (argument === "--host" && value) { + host = value; + index += 1; + continue; + } + if (argument === "--port" && value) { + port = Number(value); + index += 1; + continue; + } + if (argument === "--ready-file" && value) { + readyFile = path.resolve(value); + index += 1; + continue; + } + throw new Error(`Unknown or incomplete argument: ${argument ?? ""}`); + } + + if (!Number.isInteger(port) || port < 0 || port > 65_535) { + throw new Error("--port must be an integer between 0 and 65535"); + } + return { host: validateLoopbackHost(host), port, readyFile }; +}; + +const setCommonHeaders = (response: ServerResponse) => { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, X-QA-Fixture"); + response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, OPTIONS"); + response.setHeader("Cache-Control", "no-store"); + response.setHeader("X-QA-Fixture", "release-v2"); +}; + +const send = (response: ServerResponse, status: number, contentType: string, body: string | Buffer) => { + setCommonHeaders(response); + response.writeHead(status, { "Content-Type": contentType }); + response.end(body); +}; + +const sendJson = (response: ServerResponse, status: number, value: unknown) => { + send(response, status, "application/json; charset=utf-8", `${JSON.stringify(value)}\n`); +}; + +const readBody = async (request: IncomingMessage) => { + const chunks: Buffer[] = []; + let size = 0; + for await (const chunk of request) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + size += buffer.length; + if (size > MAX_BODY_BYTES) throw new Error("UPLOAD_TOO_LARGE"); + chunks.push(buffer); + } + return Buffer.concat(chunks); +}; + +const handleRequest = async (request: IncomingMessage, response: ServerResponse) => { + const url = new URL(request.url ?? "/", `http://${request.headers.host ?? DEFAULT_HOST}`); + const method = request.method ?? "GET"; + + if (method === "OPTIONS") { + setCommonHeaders(response); + response.writeHead(204); + response.end(); + return; + } + + if (url.pathname === "/health") { + sendJson(response, 200, { status: "ok", fixture: "release-v2-qa" }); + return; + } + + if (url.pathname === "/json") { + sendJson(response, 200, { + id: "qa-json-001", + name: "Release v2 deterministic JSON", + active: true, + count: 3, + tags: ["homarr", "release-v2", "qa"], + nested: { score: 99, nullable: null }, + }); + return; + } + + if (url.pathname === "/api/qa/custom-widget") { + sendJson(response, 200, customWidgetPayload); + return; + } + + if (url.pathname === "/rss") { + send( + response, + 200, + "application/rss+xml; charset=utf-8", + ` +Release v2 QAhttps://example.invalid/qaDeterministic feedqa-entry-1Alpha release notehttps://example.invalid/qa/alphaThu, 01 Jan 2026 00:00:00 GMTStable fixture entryqa-entry-2Beta release notehttps://example.invalid/qa/betaFri, 02 Jan 2026 00:00:00 GMTSecond stable fixture entry\n`, + ); + return; + } + + if (url.pathname === "/media/image.png") { + send(response, 200, "image/png", png); + return; + } + + if (url.pathname === "/media/image.svg") { + send( + response, + 200, + "image/svg+xml; charset=utf-8", + 'Release v2 QA\n', + ); + return; + } + + if (url.pathname === "/media/audio.wav") { + send(response, 200, "audio/wav", wav); + return; + } + + if (url.pathname === "/media/video.mp4") { + const video = await readFile(videoPath); + const range = request.headers.range; + setCommonHeaders(response); + response.setHeader("Accept-Ranges", "bytes"); + if (!range) { + response.writeHead(200, { "Content-Type": "video/mp4", "Content-Length": video.length }); + response.end(method === "HEAD" ? undefined : video); + return; + } + + const match = /^bytes=(\d+)-(\d*)$/.exec(range); + if (!match) { + response.writeHead(416, { "Content-Range": `bytes */${video.length}` }); + response.end(); + return; + } + const start = Number(match[1]); + const requestedEnd = match[2] ? Number(match[2]) : video.length - 1; + const end = Math.min(requestedEnd, video.length - 1); + if (start > end || start >= video.length) { + response.writeHead(416, { "Content-Range": `bytes */${video.length}` }); + response.end(); + return; + } + const body = video.subarray(start, end + 1); + response.writeHead(206, { + "Content-Type": "video/mp4", + "Content-Length": body.length, + "Content-Range": `bytes ${start}-${end}/${video.length}`, + }); + response.end(method === "HEAD" ? undefined : body); + return; + } + + if (url.pathname === "/iframe") { + send( + response, + 200, + "text/html; charset=utf-8", + 'Release v2 QA iframe

Deterministic iframe

release-v2-qa

\n', + ); + return; + } + + if (url.pathname === "/api/qa/download/sample.txt") { + setCommonHeaders(response); + response.writeHead(200, { + "Content-Type": "text/plain; charset=utf-8", + "Content-Disposition": 'attachment; filename="release-v2-qa-sample.txt"', + }); + response.end("Release v2 QA deterministic download\nline=2\n"); + return; + } + + if (url.pathname === "/upload") { + if (method !== "POST" && method !== "PUT") { + sendJson(response, 405, { error: "METHOD_NOT_ALLOWED", allowed: ["POST", "PUT"] }); + return; + } + try { + const body = await readBody(request); + sendJson(response, 200, { + uploaded: true, + method, + bytes: body.length, + contentType: request.headers["content-type"] ?? null, + sha256: createHash("sha256").update(body).digest("hex"), + }); + } catch (error) { + if (error instanceof Error && error.message === "UPLOAD_TOO_LARGE") { + sendJson(response, 413, { error: "UPLOAD_TOO_LARGE", maxBytes: MAX_BODY_BYTES }); + return; + } + throw error; + } + return; + } + + if (url.pathname === "/empty") { + setCommonHeaders(response); + response.writeHead(204); + response.end(); + return; + } + + if (url.pathname === "/malformed" || url.pathname === "/malformed/json") { + send(response, 200, "application/json; charset=utf-8", '{"status":"broken","items":[1,2,}\n'); + return; + } + + if (url.pathname === "/slow") { + const requestedMs = Number(url.searchParams.get("ms") ?? "1500"); + const delayMs = Number.isFinite(requestedMs) ? Math.max(0, Math.min(Math.trunc(requestedMs), MAX_SLOW_MS)) : 1500; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + sendJson(response, 200, { status: "ok", delayedMs: delayMs }); + return; + } + + if (url.pathname === "/error" || url.pathname.startsWith("/error/")) { + const pathStatus = url.pathname.split("/")[2]; + const requestedStatus = Number(url.searchParams.get("status") ?? pathStatus ?? "503"); + const status = + Number.isInteger(requestedStatus) && requestedStatus >= 400 && requestedStatus <= 599 ? requestedStatus : 503; + sendJson(response, status, { error: "QA_CONFIGURED_HTTP_ERROR", status }); + return; + } + + sendJson(response, 404, { error: "NOT_FOUND", path: url.pathname }); +}; + +const writeJsonAtomic = async (filePath: string, value: unknown) => { + await mkdir(path.dirname(filePath), { recursive: true }); + const temporaryPath = `${filePath}.${process.pid}.tmp`; + const handle = await open( + temporaryPath, + constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, + 0o600, + ); + try { + await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await rename(temporaryPath, filePath); + } catch (error) { + await rm(temporaryPath, { force: true }); + throw error; + } +}; + +const main = async () => { + const options = parseArgs(); + const server = createServer((request, response) => { + void handleRequest(request, response).catch((error: unknown) => { + console.error(error); + if (!response.headersSent) sendJson(response, 500, { error: "FIXTURE_INTERNAL_ERROR" }); + else response.destroy(); + }); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(options.port, options.host, resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Fixture server did not expose a TCP address"); + const urlHost = options.host.includes(":") ? `[${options.host}]` : options.host; + const url = `http://${urlHost}:${address.port}`; + const ready = { schemaVersion: 1, pid: process.pid, host: options.host, port: address.port, url }; + if (options.readyFile) await writeJsonAtomic(options.readyFile, ready); + process.stdout.write(`${JSON.stringify(ready)}\n`); + + const shutdown = () => { + server.close(() => process.exit(0)); + setTimeout(() => process.exit(1), 5_000).unref(); + }; + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); +}; + +await main(); diff --git a/scripts/release-v2-qa/geometry.mts b/scripts/release-v2-qa/geometry.mts new file mode 100644 index 000000000..1493ee886 --- /dev/null +++ b/scripts/release-v2-qa/geometry.mts @@ -0,0 +1,152 @@ +import type { LayoutRole } from "../../packages/definitions/index.ts"; +import { getBoardLaneColumnCount, getRootSectionLane } from "../../packages/definitions/index.ts"; + +interface GeometryLayout { + id: string; + boardId: string; + columnCount: number; + leftGutterColumnCount: number; + rightGutterColumnCount: number; + role: LayoutRole; +} + +interface GeometrySection { + id: string; + boardId: string; + kind: "empty" | "container"; + xOffset: number | null; +} + +interface GeometryPlacement { + layoutId: string; + sectionId: string; + xOffset: number; + yOffset: number; + width: number; + height: number; +} + +interface GeometrySectionLayout extends GeometryPlacement { + parentSectionId: string | null; +} + +interface GeometryItemLayout extends GeometryPlacement { + itemId: string; + boardId: string; +} + +export interface FixtureGeometry { + layouts: GeometryLayout[]; + sections: GeometrySection[]; + sectionLayouts: GeometrySectionLayout[]; + itemLayouts: GeometryItemLayout[]; +} + +const placementIsValid = (placement: GeometryPlacement) => + Number.isInteger(placement.xOffset) && + Number.isInteger(placement.yOffset) && + Number.isInteger(placement.width) && + Number.isInteger(placement.height) && + placement.xOffset >= 0 && + placement.yOffset >= 0 && + placement.width >= 1 && + placement.height >= 1; + +export const findFixtureGeometryErrors = (geometry: FixtureGeometry) => { + const errors: string[] = []; + const layoutsById = new Map(geometry.layouts.map((layout) => [layout.id, layout])); + const sectionsById = new Map(geometry.sections.map((section) => [section.id, section])); + const sectionLayoutsByKey = new Map(); + + for (const sectionLayout of geometry.sectionLayouts) { + const key = `${sectionLayout.layoutId}\0${sectionLayout.sectionId}`; + if (sectionLayoutsByKey.has(key)) { + errors.push(`container ${sectionLayout.sectionId} has duplicate placement in layout ${sectionLayout.layoutId}`); + continue; + } + sectionLayoutsByKey.set(key, sectionLayout); + } + + const parentCapacity = ( + layout: GeometryLayout, + parentSection: GeometrySection, + ): { columns: number; kind: "lane" | "container" } | undefined => { + if (parentSection.kind === "empty") { + return { + columns: getBoardLaneColumnCount(layout, getRootSectionLane(parentSection.xOffset)), + kind: "lane", + }; + } + + const parentLayout = sectionLayoutsByKey.get(`${layout.id}\0${parentSection.id}`); + if (!parentLayout) return undefined; + return { columns: parentLayout.width, kind: "container" }; + }; + + for (const sectionLayout of geometry.sectionLayouts) { + const layout = layoutsById.get(sectionLayout.layoutId); + const section = sectionsById.get(sectionLayout.sectionId); + const parent = sectionLayout.parentSectionId ? sectionsById.get(sectionLayout.parentSectionId) : undefined; + if (!layout || !section || !parent) { + errors.push( + `container ${sectionLayout.sectionId} in layout ${sectionLayout.layoutId} has unknown geometry references`, + ); + continue; + } + if (section.kind !== "container" || section.boardId !== layout.boardId || parent.boardId !== layout.boardId) { + errors.push(`container ${section.id} in layout ${layout.id} crosses a board or root boundary`); + continue; + } + if (!placementIsValid(sectionLayout)) { + errors.push(`container ${section.id} in layout ${layout.id} has an invalid placement`); + continue; + } + + const capacity = parentCapacity(layout, parent); + if (!capacity) { + errors.push(`container ${section.id} in layout ${layout.id} has an unplaced parent container`); + } else if (sectionLayout.xOffset + sectionLayout.width > capacity.columns) { + errors.push( + `container ${section.id} in layout ${layout.id} exceeds its ${capacity.columns}-column parent ${capacity.kind}`, + ); + } + + const visited = new Set(); + let currentSectionId: string | null = section.id; + while (currentSectionId) { + if (visited.has(currentSectionId)) { + errors.push(`container cycle detected in layout ${layout.id} at ${currentSectionId}`); + break; + } + visited.add(currentSectionId); + const current = sectionsById.get(currentSectionId); + if (!current || current.kind === "empty") break; + currentSectionId = sectionLayoutsByKey.get(`${layout.id}\0${currentSectionId}`)?.parentSectionId ?? null; + } + } + + for (const itemLayout of geometry.itemLayouts) { + const layout = layoutsById.get(itemLayout.layoutId); + const parent = sectionsById.get(itemLayout.sectionId); + if (!layout || !parent || itemLayout.boardId !== layout.boardId || parent.boardId !== layout.boardId) { + errors.push(`item ${itemLayout.itemId} in layout ${itemLayout.layoutId} crosses a board or section boundary`); + continue; + } + if (!placementIsValid(itemLayout)) { + errors.push(`item ${itemLayout.itemId} in layout ${layout.id} has an invalid placement`); + continue; + } + const capacity = parentCapacity(layout, parent); + if (!capacity) { + errors.push(`item ${itemLayout.itemId} in layout ${layout.id} has an unplaced parent container`); + continue; + } + if (itemLayout.xOffset + itemLayout.width > capacity.columns) { + errors.push( + `item ${itemLayout.itemId} in layout ${layout.id} exceeds its ${capacity.columns}-column parent ${capacity.kind}`, + ); + } + } + + return errors; +}; diff --git a/scripts/release-v2-qa/geometry.spec.ts b/scripts/release-v2-qa/geometry.spec.ts new file mode 100644 index 000000000..f0dd9c97c --- /dev/null +++ b/scripts/release-v2-qa/geometry.spec.ts @@ -0,0 +1,119 @@ +// @vitest-environment node + +import { describe, expect, test } from "vitest"; + +import type { FixtureGeometry } from "./geometry.mts"; +import { findFixtureGeometryErrors } from "./geometry.mts"; + +const baseGeometry = (): FixtureGeometry => ({ + layouts: [ + { + id: "layout-mobile", + boardId: "board", + columnCount: 3, + leftGutterColumnCount: 0, + rightGutterColumnCount: 0, + role: "mobile", + }, + ], + sections: [ + { id: "root", boardId: "board", kind: "empty", xOffset: 0 }, + { id: "level-1", boardId: "board", kind: "container", xOffset: null }, + { id: "level-2", boardId: "board", kind: "container", xOffset: null }, + { id: "level-3", boardId: "board", kind: "container", xOffset: null }, + ], + sectionLayouts: [ + { + sectionId: "level-1", + layoutId: "layout-mobile", + parentSectionId: "root", + xOffset: 0, + yOffset: 0, + width: 3, + height: 6, + }, + { + sectionId: "level-2", + layoutId: "layout-mobile", + parentSectionId: "level-1", + xOffset: 0, + yOffset: 0, + width: 2, + height: 5, + }, + { + sectionId: "level-3", + layoutId: "layout-mobile", + parentSectionId: "level-2", + xOffset: 0, + yOffset: 0, + width: 1, + height: 4, + }, + ], + itemLayouts: [ + { + itemId: "nested-item", + boardId: "board", + sectionId: "level-3", + layoutId: "layout-mobile", + xOffset: 0, + yOffset: 0, + width: 1, + height: 2, + }, + ], +}); + +describe("release-v2 QA fixture geometry", () => { + test("rejects an item that fits total columns but overflows the gutter-reduced main lane", () => { + const geometry = baseGeometry(); + geometry.layouts[0] = { + id: "layout-mobile", + boardId: "board", + role: "base", + columnCount: 12, + leftGutterColumnCount: 2, + rightGutterColumnCount: 2, + }; + geometry.sections = [{ id: "root", boardId: "board", kind: "empty", xOffset: 0 }]; + geometry.sectionLayouts = []; + geometry.itemLayouts = [ + { + itemId: "gutter-overflow", + boardId: "board", + sectionId: "root", + layoutId: "layout-mobile", + xOffset: 0, + yOffset: 0, + width: 9, + height: 1, + }, + ]; + + expect(findFixtureGeometryErrors(geometry)).toContain( + "item gutter-overflow in layout layout-mobile exceeds its 8-column parent lane", + ); + }); + + test("accepts valid three-level mobile nesting and rejects a child wider than its container", () => { + const geometry = baseGeometry(); + expect(findFixtureGeometryErrors(geometry)).toEqual([]); + + const nestedItem = geometry.itemLayouts.at(0); + if (!nestedItem) throw new Error("Missing nested fixture item"); + nestedItem.width = 2; + expect(findFixtureGeometryErrors(geometry)).toContain( + "item nested-item in layout layout-mobile exceeds its 1-column parent container", + ); + }); + + test("rejects recursive container parent cycles", () => { + const geometry = baseGeometry(); + const firstSection = geometry.sectionLayouts.at(0); + if (!firstSection) throw new Error("Missing nested fixture section"); + firstSection.parentSectionId = "level-3"; + + expect(findFixtureGeometryErrors(geometry).some((error) => error.includes("container cycle"))).toBe(true); + }); +}); diff --git a/scripts/release-v2-qa/provenance.mts b/scripts/release-v2-qa/provenance.mts new file mode 100644 index 000000000..d4fd584ce --- /dev/null +++ b/scripts/release-v2-qa/provenance.mts @@ -0,0 +1,76 @@ +import { spawnSync } from "node:child_process"; + +const fullShaPattern = /^[0-9a-f]{40}$/iu; + +export const releaseV2QaProfiles = ["main-writable", "main-readonly", "onboarding-fresh", "degraded"] as const; +export type ReleaseV2QaProfile = (typeof releaseV2QaProfiles)[number]; + +export interface ReleaseV2QaProfileFlags { + demoMode: boolean; + demoReadOnly: boolean; + unsafeMockIntegration: boolean; +} + +const expectedProfileFlags: Record = { + "main-writable": { demoMode: true, demoReadOnly: false, unsafeMockIntegration: true }, + "main-readonly": { demoMode: true, demoReadOnly: true, unsafeMockIntegration: true }, + "onboarding-fresh": { demoMode: false, demoReadOnly: false, unsafeMockIntegration: true }, + degraded: { demoMode: true, demoReadOnly: false, unsafeMockIntegration: true }, +}; + +const profileFlagEnvironmentNames = { + demoMode: "DEMO_MODE", + demoReadOnly: "DEMO_READ_ONLY", + unsafeMockIntegration: "UNSAFE_ENABLE_MOCK_INTEGRATION", +} as const satisfies Record; + +const parseStrictBoolean = (value: string | undefined) => { + if (value === "true") return true; + if (value === "false") return false; + return null; +}; + +export const assertReleaseV2QaProfileEnvironment = ( + profile: ReleaseV2QaProfile, + environment: NodeJS.ProcessEnv, +): ReleaseV2QaProfileFlags => { + const expected = expectedProfileFlags[profile]; + const invalidFlags: string[] = []; + + for (const [flag, environmentName] of Object.entries(profileFlagEnvironmentNames) as Array< + [keyof ReleaseV2QaProfileFlags, string] + >) { + const expectedValue = expected[flag]; + const actualValue = parseStrictBoolean(environment[environmentName]); + if (actualValue !== expectedValue) invalidFlags.push(`${environmentName} (expected ${String(expectedValue)})`); + } + + if (invalidFlags.length > 0) { + throw new Error(`QA profile '${profile}' has invalid or missing environment flags: ${invalidFlags.join(", ")}`); + } + + return { ...expected }; +}; + +export const validateCandidateSha = (candidateSha: string, checkoutSha: string) => { + if (!fullShaPattern.test(candidateSha)) { + throw new Error("QA candidate must be a full 40-character Git SHA"); + } + if (!fullShaPattern.test(checkoutSha)) { + throw new Error("The checked-out HEAD did not resolve to a full 40-character Git SHA"); + } + if (candidateSha.toLowerCase() !== checkoutSha.toLowerCase()) { + throw new Error("QA candidate does not match the checked-out HEAD"); + } + return checkoutSha.toLowerCase(); +}; + +export const resolveCheckoutCandidateSha = (repoRoot: string) => { + const result = spawnSync("git", ["rev-parse", "--verify", "HEAD^{commit}"], { + cwd: repoRoot, + encoding: "utf8", + }); + if (result.status !== 0) throw new Error("Could not resolve the checked-out HEAD for QA provenance"); + const checkoutSha = result.stdout.trim(); + return validateCandidateSha(checkoutSha, checkoutSha); +}; diff --git a/scripts/release-v2-qa/provenance.spec.ts b/scripts/release-v2-qa/provenance.spec.ts new file mode 100644 index 000000000..11cb1e23d --- /dev/null +++ b/scripts/release-v2-qa/provenance.spec.ts @@ -0,0 +1,133 @@ +// @vitest-environment node + +import { spawnSync } from "node:child_process"; +import { access, mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import Database from "better-sqlite3"; +import { afterEach, describe, expect, test } from "vitest"; + +import { assertReleaseV2QaProfileEnvironment, releaseV2QaProfiles, validateCandidateSha } from "./provenance.mts"; +import type { ReleaseV2QaProfile } from "./provenance.mts"; + +const disposableDirectories: string[] = []; +const repoRoot = path.resolve(import.meta.dirname, "../.."); +const profileEnvironmentNames = ["DEMO_MODE", "DEMO_READ_ONLY", "UNSAFE_ENABLE_MOCK_INTEGRATION"] as const; + +afterEach(async () => { + await Promise.all( + disposableDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })), + ); +}); + +const environmentForProfile = (profile: ReleaseV2QaProfile): NodeJS.ProcessEnv => { + const environment: NodeJS.ProcessEnv = { + DEMO_MODE: "true", + DEMO_READ_ONLY: "false", + UNSAFE_ENABLE_MOCK_INTEGRATION: "true", + }; + if (profile === "main-readonly") environment.DEMO_READ_ONLY = "true"; + if (profile === "onboarding-fresh") environment.DEMO_MODE = "false"; + return environment; +}; + +describe("release-v2 QA candidate provenance", () => { + test("accepts only the exact checked-out commit", () => { + const checkoutSha = "a".repeat(40); + + expect(validateCandidateSha(checkoutSha.toUpperCase(), checkoutSha)).toBe(checkoutSha); + expect(() => validateCandidateSha("b".repeat(40), checkoutSha)).toThrow("checked-out HEAD"); + expect(() => validateCandidateSha("not-a-sha", checkoutSha)).toThrow("full 40-character Git SHA"); + }); +}); + +describe("release-v2 QA profile provenance", () => { + test.each(releaseV2QaProfiles)("accepts the exact %s profile matrix", (profile) => { + expect(assertReleaseV2QaProfileEnvironment(profile, environmentForProfile(profile))).toEqual({ + demoMode: profile !== "onboarding-fresh", + demoReadOnly: profile === "main-readonly", + unsafeMockIntegration: true, + }); + }); + + test.each(profileEnvironmentNames)("rejects an unset %s flag", (environmentName) => { + const environment = environmentForProfile("main-writable"); + delete environment[environmentName]; + + expect(() => assertReleaseV2QaProfileEnvironment("main-writable", environment)).toThrow( + new RegExp(`${environmentName} \\(expected (?:true|false)\\)`, "u"), + ); + }); + + test.each(["1", "0", "TRUE", "False", "yes", " true ", "", "unexpected-private-marker"])( + "rejects the malformed strict boolean %j without echoing it", + (malformedValue) => { + for (const environmentName of profileEnvironmentNames) { + const environment = environmentForProfile("main-writable"); + environment[environmentName] = malformedValue; + + expect(() => assertReleaseV2QaProfileEnvironment("main-writable", environment)).toThrow(environmentName); + try { + assertReleaseV2QaProfileEnvironment("main-writable", environment); + } catch (error) { + expect((error as Error).message).not.toContain(malformedValue || "unexpected-private-marker"); + } + } + }, + ); + + test.each(releaseV2QaProfiles)("rejects every mismatched flag for %s", (profile) => { + const expectedEnvironment = environmentForProfile(profile); + for (const environmentName of profileEnvironmentNames) { + const environment = { ...expectedEnvironment }; + environment[environmentName] = environment[environmentName] === "true" ? "false" : "true"; + expect(() => assertReleaseV2QaProfileEnvironment(profile, environment)).toThrow(environmentName); + } + }); + + test("rejects a writable ambient environment before a read-only reset or manifest write", async () => { + const disposableDirectory = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-profile-flags-")); + disposableDirectories.push(disposableDirectory); + const databasePath = path.join(disposableDirectory, "fixture.sqlite"); + const outputDirectory = path.join(disposableDirectory, "output"); + const database = new Database(databasePath); + database.exec("CREATE TABLE mutation_guard (value TEXT NOT NULL); INSERT INTO mutation_guard VALUES ('preserved')"); + database.close(); + const databaseBefore = await readFile(databasePath); + + const result = spawnSync( + process.execPath, + [ + "--import", + "tsx", + path.join(repoRoot, "scripts/release-v2-qa/seed.mts"), + "--profile", + "main-readonly", + "--output", + outputDirectory, + "--reset", + ], + { + cwd: repoRoot, + encoding: "utf8", + env: { + ...process.env, + DB_DRIVER: "better-sqlite3", + DB_URL: databasePath, + DEMO_MODE: "true", + DEMO_READ_ONLY: "false", + UNSAFE_ENABLE_MOCK_INTEGRATION: "true", + }, + timeout: 30_000, + }, + ); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(result.stderr).toContain("DEMO_READ_ONLY (expected true)"); + expect(result.stderr).not.toContain("no such table"); + expect(await readFile(databasePath)).toEqual(databaseBefore); + await expect(access(path.join(outputDirectory, "fixture-manifest.json"))).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); diff --git a/scripts/release-v2-qa/record-blocked-report.mts b/scripts/release-v2-qa/record-blocked-report.mts new file mode 100644 index 000000000..0ca93d3ee --- /dev/null +++ b/scripts/release-v2-qa/record-blocked-report.mts @@ -0,0 +1,338 @@ +import { lstat, readFile, readdir } from "node:fs/promises"; +import path from "node:path"; + +import { resolveCheckoutCandidateSha } from "./provenance.mts"; +import { validateResolvedArtifactPath, writeSafeReportFile } from "./report-path-integrity.mts"; + +const repoRoot = path.resolve(import.meta.dirname, "../.."); +const qaRoot = path.join(repoRoot, "qa/release-v2"); +const candidateSha = resolveCheckoutCandidateSha(repoRoot); + +type Status = "passed" | "failed" | "blocked" | "not-reached"; + +interface Packet { + id: string; + wave: string; + title: string; + prRefs: number[]; + personas: string[]; + boards: string[]; + profiles: string[]; + viewports: string[]; + zooms: number[]; + inputs: string[]; + widgetKinds?: string[]; + cases: string[]; +} + +interface Manifest { + caseDimensions: Record; + packets: Packet[]; +} + +interface Options { + packetId: string; + url: string; + profile: string; + persona: string; + session: string; + viewport: string; + input: string; + zoom: number; + timestamp: string; +} + +const profileFlags: Record = { + "main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + "main-readonly": ["DEMO_MODE=true", "DEMO_READ_ONLY=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + "onboarding-fresh": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + degraded: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], +}; + +const exhaustiveSizeWidgetKinds = new Set([ + "downloads", + "calendar", + "mediaServer", + "mediaRequests-requestList", + "mediaRequests-requestStats", + "systemResources", + "systemDisks", + "dockerContainers", + "networkControllerSummary", + "networkControllerStatus", + "beszelSystemStats", + "uptimeKuma", + "bookmarks", + "notebook", + "iframe", + "customApi", + "assistant", +]); + +const parseOptions = (): Options => { + const values = new Map(); + const args = process.argv.slice(2).filter((argument) => argument !== "--"); + for (let index = 0; index < args.length; index += 2) { + const key = args[index]; + const value = args[index + 1]; + if (!key?.startsWith("--") || !value) throw new Error(`Invalid argument ${key ?? ""}`); + values.set(key.slice(2), value); + } + + const required = (key: string) => { + const value = values.get(key); + if (!value) throw new Error(`--${key} is required`); + return value; + }; + const zoom = Number(required("zoom")); + if (!Number.isFinite(zoom) || zoom <= 0) throw new Error("--zoom must be a positive number"); + return { + packetId: required("packet"), + url: required("url"), + profile: required("profile"), + persona: required("persona"), + session: required("session"), + viewport: required("viewport"), + input: required("input"), + zoom, + timestamp: values.get("timestamp") ?? new Date().toISOString(), + }; +}; + +const listFiles = async (directory: string): Promise => { + const output: string[] = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) output.push(...(await listFiles(entryPath))); + else if (entry.isFile()) { + const artifactError = await validateResolvedArtifactPath(directory, entryPath, "blocked-report artifact"); + if (artifactError) throw new Error(artifactError); + const file = await lstat(entryPath); + if (!entryPath.endsWith(".webm") || file.size > 0) output.push(entryPath); + } else { + throw new Error(`Blocked-report artifact tree contains a non-regular entry: ${entry.name}`); + } + } + return output.toSorted(); +}; + +const escapeCell = (value: string) => value.replaceAll("|", "\\|").replaceAll("\n", " "); +const list = (values: (string | number)[]) => { + if (values.length === 0) return "None"; + return values.join(", "); +}; + +const sizeRequirement = (widgetKind: string) => { + if (exhaustiveSizeWidgetKinds.has(widgetKind)) return "every width 1-24 × every height 1-6"; + return "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds"; +}; + +const main = async () => { + const options = parseOptions(); + const manifest = JSON.parse(await readFile(path.join(qaRoot, "coverage-manifest.json"), "utf8")) as Manifest; + const packet = manifest.packets.find((candidate) => candidate.id === options.packetId); + if (!packet) throw new Error(`Unknown packet ${options.packetId}`); + if (!packet.profiles.includes(options.profile)) throw new Error(`${packet.id} does not include ${options.profile}`); + if (!packet.personas.includes(options.persona)) throw new Error(`${packet.id} does not include ${options.persona}`); + if (!packet.viewports.includes(options.viewport)) + throw new Error(`${packet.id} does not include ${options.viewport}`); + if (!packet.inputs.includes(options.input)) throw new Error(`${packet.id} does not include ${options.input}`); + if (!packet.zooms.includes(options.zoom)) throw new Error(`${packet.id} does not include ${options.zoom}%`); + const flags = profileFlags[options.profile]; + if (!flags) throw new Error(`Unknown runtime profile ${options.profile}`); + + const runtimeUrl = new URL(options.url); + const actualPort = Number(runtimeUrl.port); + if (!Number.isInteger(actualPort) || actualPort <= 0) throw new Error("Runtime URL must include an explicit port"); + const artifactDirectory = path.join(repoRoot, ".screenshots/release-v2", packet.id); + const artifacts = await listFiles(artifactDirectory); + if (artifacts.length === 0) throw new Error(`${packet.id} has no browser artifacts`); + + const dimensions = manifest.caseDimensions[packet.wave] ?? []; + const cases = packet.cases.flatMap((baseId) => + dimensions.map((dimension) => ({ id: `${baseId}-${dimension.id}`, expected: dimension.expected })), + ); + const caseStatuses: Record = {}; + for (const testCase of cases) { + let status: Status = "blocked"; + if (packet.wave === "preflight" && testCase.id.endsWith("-ENVIRONMENT")) status = "failed"; + if (packet.wave === "preflight" && testCase.id.endsWith("-EVIDENCE")) status = "passed"; + caseStatuses[testCase.id] = status; + } + const status: Status = Object.values(caseStatuses).includes("failed") ? "failed" : "blocked"; + const blockedCaseIds = Object.entries(caseStatuses) + .filter(([, caseStatus]) => caseStatus !== "passed") + .map(([caseId]) => caseId); + const findingEvidence = artifacts.filter( + (artifact) => artifact.endsWith(".png") || artifact.endsWith(".webm") || artifact.includes("network"), + ); + const evidence = findingEvidence.length > 0 ? findingEvidence : artifacts; + const hasRetryEvidence = artifacts.some((artifact) => /fresh|retry/u.test(path.basename(artifact))); + let findingSummary = + "Chromium repeatedly requests the same entry route and ends on ERR_TOO_MANY_REDIRECTS, preventing the assigned browser flow."; + let retryLimitation = + "The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports."; + let retryStep = "5. Compare the result with the independently reproduced preflight evidence."; + let reproducibility = "reproduced independently by the preflight agents"; + if (hasRetryEvidence) { + findingSummary = + "A fresh Chromium session repeatedly requests the same entry route and ends on ERR_TOO_MANY_REDIRECTS; a fresh-context retry does the same."; + retryLimitation = + "The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop."; + retryStep = "5. Close the context, retry in a fresh context, and observe the same result."; + reproducibility = "reproduced on retry and independently by the preflight agents"; + } + const reproductionAgent = packet.id === "preflight-01" ? "qa-v2-preflight-02" : "qa-v2-preflight-01"; + const widgetChecks = (packet.widgetKinds ?? []).flatMap((widgetKind) => + packet.viewports.map((viewport) => ({ + widgetKind, + viewport, + sizeRequirement: sizeRequirement(widgetKind), + status: "blocked" as const, + })), + ); + const metadata = { + packetId: packet.id, + status, + caseStatuses, + execution: { + candidateSha, + url: options.url, + actualPort, + runtimeProfile: options.profile, + runtimeFlags: flags, + persona: options.persona, + sessionId: options.session, + timestamp: options.timestamp, + viewport: options.viewport, + input: options.input, + zoom: options.zoom, + }, + findings: [ + { + id: "RV2-P1-001", + fingerprint: "release-v2-auth-locale-redirect-loop", + severity: "P1", + area: "routing/authentication", + title: "Locale rewrite causes an infinite browser redirect before the application renders", + summary: findingSummary, + caseIds: blockedCaseIds, + evidence, + }, + ], + artifacts, + widgetChecks, + performance: { + measurements: [], + limitations: [ + "The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked.", + "No credentials were submitted and no product state was mutated.", + retryLimitation, + ], + }, + independentReproductions: [ + { + findingFingerprint: "release-v2-auth-locale-redirect-loop", + agentId: reproductionAgent, + outcome: "reproduced", + evidence: [], + notes: `See ${reproductionAgent.replace("qa-v2-", "")} for an independent Luna Max reproduction.`, + }, + ], + notes: "Report-only black-box run. No defect was fixed and no secret was recorded.", + }; + + const caseRows = cases + .map((testCase) => { + const caseStatus = caseStatuses[testCase.id]; + return `| ${testCase.id} | ${caseStatus} | [browser artifacts](${artifactDirectory}) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | ${escapeCell(testCase.expected)} |`; + }) + .join("\n"); + let widgetSection = ""; + if (widgetChecks.length > 0) { + const widgetRows = widgetChecks + .map( + (check) => + `| ${check.widgetKind} | ${check.viewport} | ${check.sizeRequirement} | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](${artifactDirectory}) | ${check.status} |`, + ) + .join("\n"); + widgetSection = `\n## Widget kind × viewport checklist\n\n| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n${widgetRows}\n`; + } + const artifactRows = artifacts.map((artifact) => `- [${path.basename(artifact)}](${artifact})`).join("\n"); + const report = ` + +# ${packet.id}: ${packet.title} + +| Metadata | Value | +| --- | --- | +| Status | ${status} | +| Wave | ${packet.wave} | +| PR refs | ${packet.prRefs.map((number) => `#${number}`).join(", ")} | +| Personas | ${list(packet.personas)} | +| Boards | ${list(packet.boards)} | +| Profiles | ${list(packet.profiles)} | +| Chromium viewports | ${list(packet.viewports)} | +| Zoom | ${list(packet.zooms.map((zoom) => `${zoom}%`))} | +| Input | ${list(packet.inputs)} | +| Widget kinds | ${list(packet.widgetKinds ?? [])} | +| Artifact directory | [${artifactDirectory}](${artifactDirectory}) | +| Candidate SHA | ${candidateSha} | +| URL / actual port | ${options.url} / ${actualPort} | +| Runtime profile / flags | ${options.profile}; ${flags.join(", ")} | +| Executed persona | ${options.persona} | +| Browser session ID | ${options.session} | +| Timestamp | ${options.timestamp} | +| Executed viewport / input / zoom | ${options.viewport} / ${options.input} / ${options.zoom}% | + +## Dogfood evidence + +| Case ID | Status | Evidence | Observed | Expected | +| --- | --- | --- | --- | --- | +${caseRows} +${widgetSection} +## Performance measurements and limitations + +No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked. + +## Independent reproductions + +The common routing failure was independently reproduced by ${reproductionAgent}; the corresponding tracked packet contains its own browser evidence. + +## Findings + +### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect + +Preconditions: candidate ${candidateSha}, ${options.profile}, ${options.viewport}, ${options.zoom}% zoom, ${options.input}, fresh named Chromium session ${options.session}. + +1. Launch a fresh Chromium context with the Chromium argument --no-sandbox. +2. Set the assigned viewport and open ${options.url} or its authentication entry. +3. Observe repeated document requests to the same route. +4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/. +${retryStep} + +Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: ${reproducibility}. Console output was empty; network and visual evidence are linked below. + +## Artifacts + +${artifactRows} + +## Mutations and persistence + +No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable. + +## Blockers and gaps + +Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed. + +## Cleanup + +The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured. +`; + const reportPath = path.join(qaRoot, "reports", packet.id, "report.md"); + await writeSafeReportFile(qaRoot, reportPath, report, `${packet.id} report path`); + console.log(`Normalized blocked browser report ${packet.id} with ${artifacts.length} artifact(s)`); +}; + +await main(); diff --git a/scripts/release-v2-qa/report-integrity.mts b/scripts/release-v2-qa/report-integrity.mts new file mode 100644 index 000000000..82cb161df --- /dev/null +++ b/scripts/release-v2-qa/report-integrity.mts @@ -0,0 +1,663 @@ +export type QaStatus = "passed" | "failed" | "blocked" | "not-reached"; + +export interface QaWidgetCheck { + widgetKind: string; + viewport: string; + sizeRequirement: string; + status: QaStatus; +} + +export type QaSeverity = "P0" | "P1" | "P2" | "P3"; + +export interface QaFinding { + id?: string; + fingerprint?: string; + severity: QaSeverity; + title: string; + area?: string; + summary?: string; + caseIds?: string[]; + evidence?: string[]; +} + +export interface QaIndependentReproduction { + findingFingerprint: string; + agentId: string; + outcome: string; + evidence: string[]; + notes: string; +} + +export interface AggregatedQaFinding extends QaFinding { + packets: string[]; + caseIds: string[]; + evidence: string[]; + reproductions: QaIndependentReproduction[]; +} + +export interface QaReportMetadata { + packetId: string; + status: QaStatus; + caseStatuses: Record; + execution: { + candidateSha: string | null; + url: string | null; + actualPort: number | null; + runtimeProfile: string | null; + runtimeFlags: string[]; + persona: string | null; + sessionId: string | null; + timestamp: string | null; + viewport: string | null; + input: string | null; + zoom: number | null; + }; + findings: QaFinding[]; + artifacts: string[]; + widgetChecks: QaWidgetCheck[]; + performance: { + measurements: { + name: string; + value: number | null; + unit: string; + threshold: string; + status: QaStatus; + evidence: string[]; + }[]; + limitations: string[]; + }; + independentReproductions: QaIndependentReproduction[]; + notes: string; +} + +export interface CoverageGap { + packetId: string; + kind: "case" | "widget" | "metadata"; + itemId: string; + status: QaStatus; + detail?: string; +} + +interface NormalizeOptions { + packetId: string; + expectedCaseIds: string[]; + expectedWidgetChecks: QaWidgetCheck[]; +} + +export interface ReportIntegrityOptions { + campaignCandidateSha: string; + packetId: string; + profiles: string[]; + personas: string[]; + viewports: string[]; + zooms: number[]; + inputs: string[]; + expectedProfileFlags: Record; +} + +const allowedStatuses = new Set(["passed", "failed", "blocked", "not-reached"]); + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const textOrNull = (value: unknown): string | null => (typeof value === "string" ? value : null); +const numberOrNull = (value: unknown): number | null => (typeof value === "number" ? value : null); +const strings = (value: unknown): string[] => + Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []; + +const statusOrNotReached = (value: unknown): QaStatus => + allowedStatuses.has(value as QaStatus) ? (value as QaStatus) : "not-reached"; +const severityOrP3 = (value: unknown): QaSeverity => + ["P0", "P1", "P2", "P3"].includes(String(value)) ? (value as QaSeverity) : "P3"; + +const checkKey = (check: Pick): string => + `${check.widgetKind}\u0000${check.viewport}`; + +const sameSet = (left: T[], right: T[]): boolean => + left.length === right.length && new Set(left).size === left.length && left.every((value) => right.includes(value)); + +const severityRank: Record = { P0: 0, P1: 1, P2: 2, P3: 3 }; + +const uniqueStrings = (values: string[]): string[] => [...new Set(values)]; + +const reproductionKey = (reproduction: QaIndependentReproduction): string => + `${reproduction.findingFingerprint}\u0000${reproduction.agentId}\u0000${reproduction.outcome}`; + +const mergeReproduction = (finding: AggregatedQaFinding, reproduction: QaIndependentReproduction): void => { + const key = reproductionKey(reproduction); + const existing = finding.reproductions.find((candidate) => reproductionKey(candidate) === key); + if (!existing) { + finding.reproductions.push({ ...reproduction, evidence: uniqueStrings(reproduction.evidence) }); + return; + } + existing.evidence = uniqueStrings([...existing.evidence, ...reproduction.evidence]); + if (reproduction.notes && !existing.notes.includes(reproduction.notes)) { + existing.notes = [existing.notes, reproduction.notes].filter(Boolean).join("; "); + } +}; + +export const aggregateQaFindings = ( + reports: { + packetId: string; + findings: QaFinding[]; + independentReproductions: QaIndependentReproduction[]; + }[], +): AggregatedQaFinding[] => { + const findings = new Map(); + for (const report of reports) { + for (const finding of report.findings) { + const key = + finding.fingerprint ?? `${finding.severity}:${finding.area ?? "general"}:${finding.title}`.toLowerCase(); + const existing = findings.get(key); + if (!existing) { + findings.set(key, { + ...finding, + packets: [report.packetId], + caseIds: uniqueStrings(finding.caseIds ?? []), + evidence: uniqueStrings(finding.evidence ?? []), + reproductions: [], + }); + continue; + } + + existing.packets = uniqueStrings([...existing.packets, report.packetId]); + existing.caseIds = uniqueStrings([...existing.caseIds, ...(finding.caseIds ?? [])]); + existing.evidence = uniqueStrings([...existing.evidence, ...(finding.evidence ?? [])]); + if (severityRank[finding.severity] < severityRank[existing.severity]) { + existing.severity = finding.severity; + existing.id = finding.id ?? existing.id; + existing.title = finding.title; + existing.area = finding.area ?? existing.area; + existing.summary = finding.summary ?? existing.summary; + } + } + } + + for (const report of reports) { + for (const reproduction of report.independentReproductions) { + const finding = findings.get(reproduction.findingFingerprint); + if (finding) mergeReproduction(finding, reproduction); + } + } + + return [...findings.values()].toSorted( + (left, right) => + severityRank[left.severity] - severityRank[right.severity] || left.title.localeCompare(right.title), + ); +}; + +const markdownTableCells = (line: string): string[] | null => { + const trimmed = line.trim(); + if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) return null; + return trimmed + .slice(1, -1) + .split(/(? cell.trim().replaceAll("\\|", "|")); +}; + +export const collectHumanWidgetStatusErrors = ( + content: string, + packetId: string, + expectedChecks: QaWidgetCheck[], +): string[] => { + const heading = "## Widget kind × viewport checklist"; + const sectionStart = content.indexOf(heading); + if (expectedChecks.length === 0) { + if (sectionStart >= 0) return [`${packetId}: non-widget report must not contain a human widget checklist`]; + return []; + } + if (sectionStart < 0) return [`${packetId}: human widget checklist is missing`]; + + const sectionBodyStart = sectionStart + heading.length; + const duplicateSection = content.indexOf(heading, sectionBodyStart); + const nextSection = content.indexOf("\n## ", sectionBodyStart); + const section = content.slice(sectionBodyStart, nextSection < 0 ? undefined : nextSection); + const rows = new Map(); + for (const line of section.split("\n")) { + const cells = markdownTableCells(line); + if (!cells || cells.length !== 9 || cells[0] === "Widget kind" || cells.every((cell) => /^-+$/u.test(cell))) { + continue; + } + const key = `${cells[0]}\u0000${cells[1]}`; + const existing = rows.get(key); + rows.set(key, { status: cells[8] ?? "", count: (existing?.count ?? 0) + 1 }); + } + + const errors: string[] = []; + if (duplicateSection >= 0) errors.push(`${packetId}: human widget checklist is duplicated`); + const expectedKeys = new Set(expectedChecks.map(checkKey)); + for (const check of expectedChecks) { + const key = checkKey(check); + const row = rows.get(key); + if (!row) { + errors.push(`${packetId}: human widget row is missing ${check.widgetKind} at ${check.viewport}`); + continue; + } + if (row.count !== 1) { + errors.push(`${packetId}: human widget row is duplicated ${check.widgetKind} at ${check.viewport}`); + } + if (row.status !== check.status) { + errors.push( + `${packetId}/${check.widgetKind}/${check.viewport}: human widget status ${row.status || ""} differs from structured status ${check.status}`, + ); + } + } + for (const key of rows.keys()) { + if (expectedKeys.has(key)) continue; + const [widgetKind, viewport] = key.split("\u0000"); + errors.push(`${packetId}: human widget checklist contains unassigned row ${widgetKind}@${viewport}`); + } + return errors; +}; + +export const rollupQaStatuses = (statuses: QaStatus[]): QaStatus => { + if (statuses.includes("failed")) return "failed"; + if (statuses.includes("blocked")) return "blocked"; + if (statuses.includes("not-reached")) return "not-reached"; + return "passed"; +}; + +const hasExecutionResults = (metadata: QaReportMetadata): boolean => + metadata.status !== "not-reached" || + Object.values(metadata.caseStatuses).some((status) => status !== "not-reached") || + metadata.widgetChecks.some((check) => check.status !== "not-reached") || + metadata.findings.length > 0 || + metadata.artifacts.length > 0 || + metadata.performance.measurements.length > 0 || + metadata.performance.limitations.length > 0 || + metadata.independentReproductions.length > 0; + +export const collectReportIntegrityErrors = (metadata: QaReportMetadata, options: ReportIntegrityOptions): string[] => { + const errors: string[] = []; + if (metadata.packetId !== options.packetId) { + errors.push(`${options.packetId}: metadata packetId is ${metadata.packetId}`); + } + + const caseRollup = rollupQaStatuses(Object.values(metadata.caseStatuses)); + if (metadata.status !== caseRollup) { + errors.push(`${options.packetId}: report status ${metadata.status} does not match case rollup ${caseRollup}`); + } + + if (metadata.execution.candidateSha !== options.campaignCandidateSha) { + errors.push(`${options.packetId}: candidateSha must equal the campaign candidate ${options.campaignCandidateSha}`); + } + + if (!hasExecutionResults(metadata)) return errors; + + const execution = metadata.execution; + const requiredText = { + candidateSha: execution.candidateSha, + url: execution.url, + runtimeProfile: execution.runtimeProfile, + persona: execution.persona, + sessionId: execution.sessionId, + timestamp: execution.timestamp, + viewport: execution.viewport, + input: execution.input, + }; + for (const [field, value] of Object.entries(requiredText)) { + if (typeof value !== "string" || value.trim().length === 0) { + errors.push(`${options.packetId}: execution.${field} is required after execution`); + } + } + if (!Number.isInteger(execution.actualPort) || (execution.actualPort ?? 0) <= 0) { + errors.push(`${options.packetId}: execution.actualPort must be a positive integer after execution`); + } + if (!Number.isFinite(execution.zoom) || (execution.zoom ?? 0) <= 0) { + errors.push(`${options.packetId}: execution.zoom must be positive after execution`); + } + if (metadata.artifacts.length === 0) { + errors.push(`${options.packetId}: executed report must link at least one artifact`); + } + + if (execution.runtimeProfile && !options.profiles.includes(execution.runtimeProfile)) { + errors.push(`${options.packetId}: execution.runtimeProfile is outside the packet assignment`); + } + if (execution.persona && !options.personas.includes(execution.persona)) { + errors.push(`${options.packetId}: execution.persona is outside the packet assignment`); + } + if (execution.viewport && !options.viewports.includes(execution.viewport)) { + errors.push(`${options.packetId}: execution.viewport is outside the packet assignment`); + } + if (execution.input && !options.inputs.includes(execution.input)) { + errors.push(`${options.packetId}: execution.input is outside the packet assignment`); + } + if (execution.zoom && !options.zooms.includes(execution.zoom)) { + errors.push(`${options.packetId}: execution.zoom is outside the packet assignment`); + } + + const expectedFlags = execution.runtimeProfile ? options.expectedProfileFlags[execution.runtimeProfile] : undefined; + if (!expectedFlags || !sameSet(execution.runtimeFlags, expectedFlags)) { + errors.push( + `${options.packetId}: execution.runtimeFlags do not match profile ${execution.runtimeProfile ?? "unknown"}`, + ); + } + + if (execution.timestamp && Number.isNaN(Date.parse(execution.timestamp))) { + errors.push(`${options.packetId}: execution.timestamp must be ISO-compatible`); + } + + if (execution.url) { + try { + const url = new URL(execution.url); + if (!["http:", "https:"].includes(url.protocol)) { + errors.push(`${options.packetId}: execution.url must use HTTP(S)`); + } + const defaultPort = url.protocol === "https:" ? 443 : 80; + const urlPort = url.port.length > 0 ? Number(url.port) : defaultPort; + if (execution.actualPort !== null && urlPort !== execution.actualPort) { + errors.push(`${options.packetId}: execution.url port does not match execution.actualPort`); + } + } catch { + errors.push(`${options.packetId}: execution.url must be an absolute HTTP(S) URL`); + } + } + + return errors; +}; + +export const normalizeReportMetadata = ( + value: unknown, + options: NormalizeOptions, +): { metadata: QaReportMetadata; errors: string[] } => { + const errors: string[] = []; + const root = isRecord(value) ? value : {}; + if (!isRecord(value)) errors.push(`${options.packetId}: report metadata must be an object`); + if (typeof root.packetId !== "string") errors.push(`${options.packetId}: packetId must be a string`); + + const caseStatuses: Record = {}; + if (!isRecord(root.caseStatuses)) { + errors.push(`${options.packetId}: caseStatuses must be an object`); + } + const rawCaseStatuses = isRecord(root.caseStatuses) ? root.caseStatuses : {}; + for (const caseId of options.expectedCaseIds) { + const rawStatus = rawCaseStatuses[caseId]; + if (rawStatus === undefined) { + errors.push(`${options.packetId}: caseStatuses is missing assigned case ${caseId}`); + } else if (!allowedStatuses.has(rawStatus as QaStatus)) { + errors.push(`${options.packetId}/${caseId}: invalid case status ${String(rawStatus)}`); + } + caseStatuses[caseId] = statusOrNotReached(rawStatus); + } + for (const caseId of Object.keys(rawCaseStatuses)) { + if (!options.expectedCaseIds.includes(caseId)) { + errors.push(`${options.packetId}: caseStatuses contains unknown case ${caseId}`); + } + } + + if (!Array.isArray(root.widgetChecks)) { + errors.push(`${options.packetId}: widgetChecks must be an array`); + } + const expectedChecks = new Map(options.expectedWidgetChecks.map((check) => [checkKey(check), check])); + const suppliedChecks = new Map(); + for (const [index, valueAtIndex] of (Array.isArray(root.widgetChecks) ? root.widgetChecks : []).entries()) { + if (!isRecord(valueAtIndex)) { + errors.push(`${options.packetId}: widgetChecks[${index}] must be an object`); + continue; + } + const widgetKind = typeof valueAtIndex.widgetKind === "string" ? valueAtIndex.widgetKind : ""; + const viewport = typeof valueAtIndex.viewport === "string" ? valueAtIndex.viewport : ""; + const key = checkKey({ widgetKind, viewport }); + if (!expectedChecks.has(key)) { + errors.push(`${options.packetId}: widgetChecks contains unknown row ${widgetKind}@${viewport}`); + continue; + } + if (suppliedChecks.has(key)) { + errors.push(`${options.packetId}: widgetChecks contains duplicate row ${widgetKind}@${viewport}`); + continue; + } + if (!allowedStatuses.has(valueAtIndex.status as QaStatus)) { + errors.push(`${options.packetId}/${widgetKind}/${viewport}: invalid widget check status`); + } + if (typeof valueAtIndex.sizeRequirement !== "string") { + errors.push(`${options.packetId}/${widgetKind}/${viewport}: sizeRequirement must be a string`); + } else if (valueAtIndex.sizeRequirement !== expectedChecks.get(key)?.sizeRequirement) { + errors.push(`${options.packetId}/${widgetKind}/${viewport}: sizeRequirement does not match the assignment`); + } + suppliedChecks.set(key, { + widgetKind, + viewport, + sizeRequirement: typeof valueAtIndex.sizeRequirement === "string" ? valueAtIndex.sizeRequirement : "", + status: statusOrNotReached(valueAtIndex.status), + }); + } + const widgetChecks = options.expectedWidgetChecks.map((expected) => { + const supplied = suppliedChecks.get(checkKey(expected)); + if (supplied) return supplied; + errors.push( + `${options.packetId}: widgetChecks is missing assigned row ${expected.widgetKind}@${expected.viewport}`, + ); + return { ...expected, status: "not-reached" as const }; + }); + + if (!Array.isArray(root.findings)) errors.push(`${options.packetId}: findings must be an array`); + const findings = (Array.isArray(root.findings) ? root.findings : []).flatMap((finding, index) => { + if (!isRecord(finding)) { + errors.push(`${options.packetId}: findings[${index}] must be an object`); + return []; + } + if (!["P0", "P1", "P2", "P3"].includes(String(finding.severity))) { + errors.push(`${options.packetId}: findings[${index}] has an invalid severity`); + } + if (typeof finding.title !== "string") { + errors.push(`${options.packetId}: findings[${index}].title must be a string`); + } + if (finding.caseIds !== undefined && !Array.isArray(finding.caseIds)) { + errors.push(`${options.packetId}: findings[${index}].caseIds must be an array`); + } else if (Array.isArray(finding.caseIds) && finding.caseIds.some((caseId) => typeof caseId !== "string")) { + errors.push(`${options.packetId}: findings[${index}].caseIds must contain only strings`); + } else if ( + Array.isArray(finding.caseIds) && + finding.caseIds.some((caseId) => !options.expectedCaseIds.includes(String(caseId))) + ) { + errors.push(`${options.packetId}: findings[${index}].caseIds contains an unknown assigned case`); + } + if (finding.evidence !== undefined && !Array.isArray(finding.evidence)) { + errors.push(`${options.packetId}: findings[${index}].evidence must be an array`); + } else if (Array.isArray(finding.evidence) && finding.evidence.some((entry) => typeof entry !== "string")) { + errors.push(`${options.packetId}: findings[${index}].evidence must contain only strings`); + } + return [ + { + id: typeof finding.id === "string" ? finding.id : undefined, + fingerprint: typeof finding.fingerprint === "string" ? finding.fingerprint : undefined, + severity: severityOrP3(finding.severity), + title: typeof finding.title === "string" ? finding.title : "", + area: typeof finding.area === "string" ? finding.area : undefined, + summary: typeof finding.summary === "string" ? finding.summary : undefined, + caseIds: Array.isArray(finding.caseIds) ? strings(finding.caseIds) : undefined, + evidence: Array.isArray(finding.evidence) ? strings(finding.evidence) : undefined, + }, + ]; + }); + + if (!Array.isArray(root.artifacts)) errors.push(`${options.packetId}: artifacts must be an array`); + if (Array.isArray(root.artifacts) && root.artifacts.some((artifact) => typeof artifact !== "string")) { + errors.push(`${options.packetId}: artifacts must contain only strings`); + } + const artifacts = strings(root.artifacts); + + const execution = isRecord(root.execution) ? root.execution : {}; + if (!isRecord(root.execution)) errors.push(`${options.packetId}: execution metadata must be an object`); + if (!Array.isArray(execution.runtimeFlags)) { + errors.push(`${options.packetId}: execution.runtimeFlags must be an array`); + } else if (execution.runtimeFlags.some((flag) => typeof flag !== "string")) { + errors.push(`${options.packetId}: execution.runtimeFlags must contain only strings`); + } + for (const field of [ + "candidateSha", + "url", + "runtimeProfile", + "persona", + "sessionId", + "timestamp", + "viewport", + "input", + ]) { + const fieldValue = execution[field]; + if (fieldValue !== undefined && fieldValue !== null && typeof fieldValue !== "string") { + errors.push(`${options.packetId}: execution.${field} must be a string or null`); + } + } + for (const field of ["actualPort", "zoom"]) { + const fieldValue = execution[field]; + if (fieldValue !== undefined && fieldValue !== null && typeof fieldValue !== "number") { + errors.push(`${options.packetId}: execution.${field} must be a number or null`); + } + } + + const performance = isRecord(root.performance) ? root.performance : {}; + if (!isRecord(root.performance)) errors.push(`${options.packetId}: performance must be an object`); + if (!Array.isArray(performance.measurements)) { + errors.push(`${options.packetId}: performance.measurements must be an array`); + } + if (!Array.isArray(performance.limitations)) { + errors.push(`${options.packetId}: performance.limitations must be an array`); + } else if (performance.limitations.some((limitation) => typeof limitation !== "string")) { + errors.push(`${options.packetId}: performance.limitations must contain only strings`); + } + const measurements = (Array.isArray(performance.measurements) ? performance.measurements : []).flatMap( + (measurement, index) => { + if (!isRecord(measurement)) { + errors.push(`${options.packetId}: performance.measurements[${index}] must be an object`); + return []; + } + for (const field of ["name", "unit", "threshold"]) { + if (typeof measurement[field] !== "string") { + errors.push(`${options.packetId}: performance.measurements[${index}].${field} must be a string`); + } + } + if (measurement.value !== null && typeof measurement.value !== "number") { + errors.push(`${options.packetId}: performance.measurements[${index}].value must be a number or null`); + } + if (!allowedStatuses.has(measurement.status as QaStatus)) { + errors.push(`${options.packetId}: performance.measurements[${index}].status is invalid`); + } + if (!Array.isArray(measurement.evidence)) { + errors.push(`${options.packetId}: performance.measurements[${index}].evidence must be an array`); + } else if (measurement.evidence.some((entry) => typeof entry !== "string")) { + errors.push(`${options.packetId}: performance.measurements[${index}].evidence must contain only strings`); + } + return [ + { + name: typeof measurement.name === "string" ? measurement.name : "", + value: numberOrNull(measurement.value), + unit: typeof measurement.unit === "string" ? measurement.unit : "", + threshold: typeof measurement.threshold === "string" ? measurement.threshold : "", + status: statusOrNotReached(measurement.status), + evidence: strings(measurement.evidence), + }, + ]; + }, + ); + + if (!Array.isArray(root.independentReproductions)) { + errors.push(`${options.packetId}: independentReproductions must be an array`); + } + const independentReproductions = ( + Array.isArray(root.independentReproductions) ? root.independentReproductions : [] + ).flatMap((reproduction, index) => { + if (!isRecord(reproduction)) { + errors.push(`${options.packetId}: independentReproductions[${index}] must be an object`); + return []; + } + for (const field of ["findingFingerprint", "agentId", "notes"]) { + if (typeof reproduction[field] !== "string") { + errors.push(`${options.packetId}: independentReproductions[${index}].${field} must be a string`); + } + } + if (!["reproduced", "not-reproduced", "blocked", "not-reached"].includes(String(reproduction.outcome))) { + errors.push(`${options.packetId}: independentReproductions[${index}].outcome is invalid`); + } + if (!Array.isArray(reproduction.evidence)) { + errors.push(`${options.packetId}: independentReproductions[${index}].evidence must be an array`); + } else if (reproduction.evidence.some((entry) => typeof entry !== "string")) { + errors.push(`${options.packetId}: independentReproductions[${index}].evidence must contain only strings`); + } + return [ + { + findingFingerprint: typeof reproduction.findingFingerprint === "string" ? reproduction.findingFingerprint : "", + agentId: typeof reproduction.agentId === "string" ? reproduction.agentId : "", + outcome: typeof reproduction.outcome === "string" ? reproduction.outcome : "not-reached", + evidence: strings(reproduction.evidence), + notes: typeof reproduction.notes === "string" ? reproduction.notes : "", + }, + ]; + }); + + if (!allowedStatuses.has(root.status as QaStatus)) { + errors.push(`${options.packetId}: invalid report status ${String(root.status)}`); + } + if (typeof root.notes !== "string") errors.push(`${options.packetId}: notes must be a string`); + + return { + metadata: { + packetId: typeof root.packetId === "string" ? root.packetId : options.packetId, + status: statusOrNotReached(root.status), + caseStatuses, + execution: { + candidateSha: textOrNull(execution.candidateSha), + url: textOrNull(execution.url), + actualPort: numberOrNull(execution.actualPort), + runtimeProfile: textOrNull(execution.runtimeProfile), + runtimeFlags: strings(execution.runtimeFlags), + persona: textOrNull(execution.persona), + sessionId: textOrNull(execution.sessionId), + timestamp: textOrNull(execution.timestamp), + viewport: textOrNull(execution.viewport), + input: textOrNull(execution.input), + zoom: numberOrNull(execution.zoom), + }, + findings, + artifacts, + widgetChecks, + performance: { + measurements, + limitations: strings(performance.limitations), + }, + independentReproductions, + notes: typeof root.notes === "string" ? root.notes : "", + }, + errors, + }; +}; + +export const collectCriticalCoverageGaps = ( + reports: { + packetId: string; + caseStatuses: Record; + widgetChecks: QaWidgetCheck[]; + metadataErrors?: string[]; + }[], +): CoverageGap[] => + reports.flatMap((report) => { + const caseGaps = Object.entries(report.caseStatuses) + .filter(([, status]) => status !== "passed") + .map(([caseId, status]) => ({ + packetId: report.packetId, + kind: "case" as const, + itemId: caseId, + status, + })); + const widgetGaps = report.widgetChecks + .filter((check) => check.status !== "passed") + .map((check) => ({ + packetId: report.packetId, + kind: "widget" as const, + itemId: `${check.widgetKind}@${check.viewport}`, + status: check.status, + })); + const metadataGaps = (report.metadataErrors ?? []).map((detail, index) => ({ + packetId: report.packetId, + kind: "metadata" as const, + itemId: `metadata-${index + 1}`, + status: "blocked" as const, + detail, + })); + return [...caseGaps, ...widgetGaps, ...metadataGaps]; + }); + +export const releaseDecision = (severityTotals: { P0: number; P1: number }, gaps: CoverageGap[]): "GO" | "NO-GO" => { + if (severityTotals.P0 > 0 || severityTotals.P1 > 0 || gaps.length > 0) return "NO-GO"; + return "GO"; +}; diff --git a/scripts/release-v2-qa/report-integrity.spec.ts b/scripts/release-v2-qa/report-integrity.spec.ts new file mode 100644 index 000000000..7d8adc2f1 --- /dev/null +++ b/scripts/release-v2-qa/report-integrity.spec.ts @@ -0,0 +1,281 @@ +import { describe, expect, it } from "vitest"; + +import { + aggregateQaFindings, + collectCriticalCoverageGaps, + collectHumanWidgetStatusErrors, + collectReportIntegrityErrors, + normalizeReportMetadata, + releaseDecision, +} from "./report-integrity.mts"; +import type { QaReportMetadata, QaWidgetCheck, ReportIntegrityOptions } from "./report-integrity.mts"; + +const assignedWidgetCheck: QaWidgetCheck = { + widgetKind: "weather", + viewport: "320x568", + sizeRequirement: "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds", + status: "not-reached", +}; +const campaignCandidateSha = "f57a660088d6777c86aca22977354fa8b810e2be"; +const integrityOptions: ReportIntegrityOptions = { + campaignCandidateSha, + packetId: "preflight-01", + profiles: ["main-writable"], + personas: ["avery-admin"], + viewports: ["1280x900"], + zooms: [100], + inputs: ["mouse"], + expectedProfileFlags: { + "main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + }, +}; +const validMetadata = (): QaReportMetadata => ({ + packetId: "preflight-01", + status: "passed", + caseStatuses: { "PF01-FIXTURE-STATE": "passed" }, + execution: { + candidateSha: campaignCandidateSha, + url: "http://127.0.0.1:34401", + actualPort: 34401, + runtimeProfile: "main-writable", + runtimeFlags: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + persona: "avery-admin", + sessionId: "qa-v2-main-avery-admin", + timestamp: "2026-09-01T00:00:00.000Z", + viewport: "1280x900", + input: "mouse", + zoom: 100, + }, + findings: [], + artifacts: ["/tmp/qa-artifact.png"], + widgetChecks: [], + performance: { measurements: [], limitations: [] }, + independentReproductions: [], + notes: "Complete.", +}); + +describe("release-v2 report integrity", () => { + it("keeps the highest severity while merging duplicate finding evidence and reproductions", () => { + const fingerprint = "same-defect"; + const findings = aggregateQaFindings([ + { + packetId: "widgets-01", + findings: [ + { + fingerprint, + severity: "P3", + title: "Initial low-severity title", + caseIds: ["W01-RENDER-STATE"], + evidence: ["/tmp/first.png"], + }, + ], + independentReproductions: [ + { + findingFingerprint: fingerprint, + agentId: "reproducer-01", + outcome: "reproduced", + evidence: ["/tmp/repro-first.png"], + notes: "First replay.", + }, + ], + }, + { + packetId: "widgets-02", + findings: [ + { + fingerprint, + severity: "P1", + title: "Confirmed release blocker", + caseIds: ["W02-RENDER-STATE"], + evidence: ["/tmp/first.png", "/tmp/second.png"], + }, + ], + independentReproductions: [], + }, + { + packetId: "repro-widgets-02", + findings: [], + independentReproductions: [ + { + findingFingerprint: fingerprint, + agentId: "reproducer-01", + outcome: "reproduced", + evidence: ["/tmp/repro-second.png"], + notes: "Second replay.", + }, + ], + }, + ]); + + expect(findings).toHaveLength(1); + expect(findings[0]).toMatchObject({ + severity: "P1", + title: "Confirmed release blocker", + packets: ["widgets-01", "widgets-02"], + caseIds: ["W01-RENDER-STATE", "W02-RENDER-STATE"], + evidence: ["/tmp/first.png", "/tmp/second.png"], + }); + expect(findings[0]?.reproductions).toEqual([ + { + findingFingerprint: fingerprint, + agentId: "reproducer-01", + outcome: "reproduced", + evidence: ["/tmp/repro-first.png", "/tmp/repro-second.png"], + notes: "First replay.; Second replay.", + }, + ]); + const severityTotals = { P0: 0, P1: findings.filter((finding) => finding.severity === "P1").length }; + expect(releaseDecision(severityTotals, [])).toBe("NO-GO"); + }); + + it("requires each human widget status to exactly match the structured status", () => { + const passedCheck = { ...assignedWidgetCheck, status: "passed" as const }; + const content = `## Widget kind × viewport checklist + +| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| weather | 320x568 | ${assignedWidgetCheck.sizeRequirement} | healthy | owner | reload | recovered | evidence | not-reached | + +## Findings`; + + expect(collectHumanWidgetStatusErrors(content, "widgets-01", [passedCheck])).toEqual([ + "widgets-01/weather/320x568: human widget status not-reached differs from structured status passed", + ]); + expect( + collectHumanWidgetStatusErrors(content.replace("| not-reached |", "| passed |"), "widgets-01", [passedCheck]), + ).toEqual([]); + }); + + it("keeps the release NO-GO when cases pass but an assigned widget check is not reached", () => { + const gaps = collectCriticalCoverageGaps([ + { + packetId: "widgets-01", + caseStatuses: { "W01-RENDER-STATE": "passed" }, + widgetChecks: [assignedWidgetCheck], + }, + ]); + + expect(gaps).toEqual([ + { + packetId: "widgets-01", + kind: "widget", + itemId: "weather@320x568", + status: "not-reached", + }, + ]); + expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO"); + }); + + it("reports null and malformed metadata without throwing", () => { + const normalize = () => + normalizeReportMetadata( + { + packetId: "widgets-01", + status: "passed", + caseStatuses: null, + execution: { candidateSha: {}, runtimeFlags: null }, + findings: null, + artifacts: null, + widgetChecks: null, + performance: null, + independentReproductions: null, + }, + { + packetId: "widgets-01", + expectedCaseIds: ["W01-RENDER-STATE"], + expectedWidgetChecks: [assignedWidgetCheck], + }, + ); + + expect(normalize).not.toThrow(); + const result = normalize(); + expect(result.errors).toEqual( + expect.arrayContaining([ + "widgets-01: caseStatuses must be an object", + "widgets-01: findings must be an array", + "widgets-01: widgetChecks must be an array", + "widgets-01: execution.runtimeFlags must be an array", + "widgets-01: execution.candidateSha must be a string or null", + ]), + ); + expect(result.metadata.caseStatuses).toEqual({ "W01-RENDER-STATE": "not-reached" }); + expect(result.metadata.findings).toEqual([]); + expect(result.metadata.widgetChecks).toEqual([assignedWidgetCheck]); + + const gaps = collectCriticalCoverageGaps([ + { + packetId: "widgets-01", + caseStatuses: result.metadata.caseStatuses, + widgetChecks: result.metadata.widgetChecks, + metadataErrors: result.errors, + }, + ]); + expect(gaps.some((gap) => gap.kind === "metadata")).toBe(true); + expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO"); + }); + + it("turns a null metadata root into deterministic validation errors", () => { + const result = normalizeReportMetadata(null, { + packetId: "widgets-01", + expectedCaseIds: ["W01-RENDER-STATE"], + expectedWidgetChecks: [assignedWidgetCheck], + }); + + expect(result.errors[0]).toBe("widgets-01: report metadata must be an object"); + expect(result.metadata.caseStatuses["W01-RENDER-STATE"]).toBe("not-reached"); + expect(result.metadata.widgetChecks[0]?.status).toBe("not-reached"); + }); + + it("forces a metadata gap and NO-GO for an executed report from the wrong candidate", () => { + const metadata = validMetadata(); + metadata.execution.candidateSha = "0000000000000000000000000000000000000000"; + const metadataErrors = collectReportIntegrityErrors(metadata, integrityOptions); + const gaps = collectCriticalCoverageGaps([ + { packetId: metadata.packetId, caseStatuses: metadata.caseStatuses, widgetChecks: [], metadataErrors }, + ]); + + expect(metadataErrors).toContain( + `preflight-01: candidateSha must equal the campaign candidate ${campaignCandidateSha}`, + ); + expect(gaps.some((gap) => gap.kind === "metadata")).toBe(true); + expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO"); + }); + + it("forces a metadata gap and NO-GO when packet status disagrees with passed cases", () => { + const metadata = validMetadata(); + metadata.status = "failed"; + const metadataErrors = collectReportIntegrityErrors(metadata, integrityOptions); + const gaps = collectCriticalCoverageGaps([ + { packetId: metadata.packetId, caseStatuses: metadata.caseStatuses, widgetChecks: [], metadataErrors }, + ]); + + expect(metadataErrors).toContain("preflight-01: report status failed does not match case rollup passed"); + expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO"); + }); + + it("reports missing execution metadata after any assigned case was reached", () => { + const metadata = validMetadata(); + metadata.execution.url = null; + metadata.execution.actualPort = null; + + expect(collectReportIntegrityErrors(metadata, integrityOptions)).toEqual( + expect.arrayContaining([ + "preflight-01: execution.url is required after execution", + "preflight-01: execution.actualPort must be a positive integer after execution", + ]), + ); + }); + + it("leaves valid structured report metadata unchanged", () => { + const input = validMetadata(); + const normalized = normalizeReportMetadata(input, { + packetId: "preflight-01", + expectedCaseIds: ["PF01-FIXTURE-STATE"], + expectedWidgetChecks: [], + }); + + expect(normalized.errors).toEqual([]); + expect(normalized.metadata).toEqual(input); + expect(collectReportIntegrityErrors(normalized.metadata, integrityOptions)).toEqual([]); + }); +}); diff --git a/scripts/release-v2-qa/report-path-integrity.mts b/scripts/release-v2-qa/report-path-integrity.mts new file mode 100644 index 000000000..9c8a7e4a0 --- /dev/null +++ b/scripts/release-v2-qa/report-path-integrity.mts @@ -0,0 +1,74 @@ +import { constants } from "node:fs"; +import { lstat, open, realpath } from "node:fs/promises"; +import path from "node:path"; + +import { assertSafeContainedPath, isPathWithin } from "./safety.mts"; + +const errorCode = (error: unknown): string | undefined => (error as NodeJS.ErrnoException).code; + +export const assertSafeReportPath = async (qaRoot: string, target: string, label: string): Promise => + assertSafeContainedPath(qaRoot, target, label); + +export const readSafeReportFile = async (qaRoot: string, target: string, label: string): Promise => { + await assertSafeReportPath(qaRoot, target, label); + const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + return await handle.readFile("utf8"); + } finally { + await handle.close(); + } +}; + +export const writeSafeReportFile = async ( + qaRoot: string, + target: string, + content: string, + label: string, +): Promise => { + await assertSafeReportPath(qaRoot, target, label); + const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW; + const handle = await open(target, flags, 0o666); + try { + await handle.writeFile(content, "utf8"); + } finally { + await handle.close(); + } +}; + +export const validateResolvedArtifactPath = async ( + expectedPacketDirectory: string, + artifactPath: string, + label: string, +): Promise => { + if (!path.isAbsolute(artifactPath) || !isPathWithin(expectedPacketDirectory, artifactPath)) { + return `${label} must use the packet's absolute artifact directory`; + } + + let resolvedPacketDirectory: string; + try { + resolvedPacketDirectory = await realpath(expectedPacketDirectory); + } catch (error) { + return `${label} packet artifact directory does not resolve (${errorCode(error) ?? "unknown error"})`; + } + if (resolvedPacketDirectory !== path.resolve(expectedPacketDirectory)) { + return `${label} packet artifact directory must not contain symlinks`; + } + + let resolvedArtifact: string; + try { + resolvedArtifact = await realpath(artifactPath); + } catch (error) { + return `${label} does not resolve (${errorCode(error) ?? "unknown error"})`; + } + if (!isPathWithin(resolvedPacketDirectory, resolvedArtifact)) { + return `${label} resolves outside the packet artifact directory`; + } + try { + await assertSafeContainedPath(resolvedPacketDirectory, artifactPath, label); + const artifactStats = await lstat(artifactPath); + if (!artifactStats.isFile()) return `${label} must resolve to an existing regular file`; + } catch (error) { + return `${label} is not a safe regular file (${error instanceof Error ? error.message : "unknown error"})`; + } + return null; +}; diff --git a/scripts/release-v2-qa/report-path-integrity.spec.ts b/scripts/release-v2-qa/report-path-integrity.spec.ts new file mode 100644 index 000000000..298d71e50 --- /dev/null +++ b/scripts/release-v2-qa/report-path-integrity.spec.ts @@ -0,0 +1,120 @@ +// @vitest-environment node + +import { spawnSync } from "node:child_process"; +import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { createServer } from "node:net"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + assertSafeReportPath, + readSafeReportFile, + validateResolvedArtifactPath, + writeSafeReportFile, +} from "./report-path-integrity.mts"; + +const temporaryPaths: string[] = []; + +const temporaryDirectory = async (suffix: string): Promise => { + const directory = await mkdtemp(path.join(tmpdir(), `homarr-release-v2-qa-${suffix}-`)); + temporaryPaths.push(directory); + return directory; +}; + +afterEach(async () => { + await Promise.all(temporaryPaths.splice(0).map((target) => rm(target, { force: true, recursive: true }))); +}); + +describe("release-v2 QA report path integrity", () => { + it.each(["reports/preflight-01/report.md", "ledger.json", "master-report.md"])( + "rejects a symlinked aggregate input or output path: %s", + async (relativePath) => { + const qaRoot = await temporaryDirectory("report-path"); + const target = path.join(qaRoot, "outside-target"); + const linkedPath = path.join(qaRoot, relativePath); + await mkdir(path.dirname(linkedPath), { recursive: true }); + await writeFile(target, "outside"); + await symlink(target, linkedPath); + + await expect(assertSafeReportPath(qaRoot, linkedPath, "QA report path")).rejects.toThrow("symlink"); + await expect(readSafeReportFile(qaRoot, linkedPath, "QA report path")).rejects.toThrow("symlink"); + await expect(writeSafeReportFile(qaRoot, linkedPath, "replacement", "QA report path")).rejects.toThrow("symlink"); + await expect(readFile(target, "utf8")).resolves.toBe("outside"); + }, + ); + + it("rejects an artifact symlink that ultimately escapes its packet directory", async () => { + const root = await temporaryDirectory("artifact-escape"); + const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01"); + const outsideArtifact = path.join(root, "outside.png"); + const linkedArtifact = path.join(packetDirectory, "escape.png"); + await mkdir(packetDirectory, { recursive: true }); + await writeFile(outsideArtifact, "outside"); + await symlink(outsideArtifact, linkedArtifact); + + await expect( + validateResolvedArtifactPath(packetDirectory, linkedArtifact, "preflight-01: artifact"), + ).resolves.toContain("resolves outside"); + }); + + it("accepts a resolved regular artifact inside the packet directory", async () => { + const root = await temporaryDirectory("artifact-valid"); + const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01"); + const artifact = path.join(packetDirectory, "valid.png"); + await mkdir(packetDirectory, { recursive: true }); + await writeFile(artifact, "valid"); + + await expect(validateResolvedArtifactPath(packetDirectory, artifact, "preflight-01: artifact")).resolves.toBeNull(); + }); + + it("rejects an artifact directory and an in-directory artifact symlink", async () => { + const root = await temporaryDirectory("artifact-not-file"); + const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01"); + const artifactDirectory = path.join(packetDirectory, "looks-like-evidence.png"); + const realArtifact = path.join(packetDirectory, "real.png"); + const linkedArtifact = path.join(packetDirectory, "linked.png"); + await mkdir(artifactDirectory, { recursive: true }); + await writeFile(realArtifact, "valid"); + await symlink(realArtifact, linkedArtifact); + + await expect( + validateResolvedArtifactPath(packetDirectory, artifactDirectory, "preflight-01: artifact"), + ).resolves.toContain("regular file"); + await expect( + validateResolvedArtifactPath(packetDirectory, linkedArtifact, "preflight-01: artifact"), + ).resolves.toContain("symlink"); + }); + + it.runIf(process.platform !== "win32")("rejects a FIFO artifact", async () => { + const root = await temporaryDirectory("artifact-fifo"); + const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01"); + const fifoArtifact = path.join(packetDirectory, "capture.pipe"); + await mkdir(packetDirectory, { recursive: true }); + const result = spawnSync("mkfifo", [fifoArtifact]); + expect(result.status).toBe(0); + + await expect( + validateResolvedArtifactPath(packetDirectory, fifoArtifact, "preflight-01: artifact"), + ).resolves.toContain("regular file"); + }); + + it.runIf(process.platform !== "win32")("rejects a socket artifact", async () => { + const root = await temporaryDirectory("artifact-socket"); + const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01"); + const socketArtifact = path.join(packetDirectory, "capture.sock"); + await mkdir(packetDirectory, { recursive: true }); + const server = createServer(); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(socketArtifact, resolve); + }); + try { + await expect( + validateResolvedArtifactPath(packetDirectory, socketArtifact, "preflight-01: artifact"), + ).resolves.toContain("regular file"); + } finally { + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + } + }); +}); diff --git a/scripts/release-v2-qa/report.mts b/scripts/release-v2-qa/report.mts new file mode 100644 index 000000000..d872d2ebc --- /dev/null +++ b/scripts/release-v2-qa/report.mts @@ -0,0 +1,630 @@ +import { mkdir, readFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + aggregateQaFindings, + collectCriticalCoverageGaps, + collectHumanWidgetStatusErrors, + collectReportIntegrityErrors, + normalizeReportMetadata, + releaseDecision, +} from "./report-integrity.mts"; +import { + assertSafeReportPath, + readSafeReportFile, + validateResolvedArtifactPath, + writeSafeReportFile, +} from "./report-path-integrity.mts"; +import { resolveCheckoutCandidateSha } from "./provenance.mts"; + +type Status = "passed" | "failed" | "blocked" | "not-reached"; +type Severity = "P0" | "P1" | "P2" | "P3"; + +interface Packet { + id: string; + wave: string; + title: string; + prRefs: number[]; + personas: string[]; + boards: string[]; + profiles: string[]; + viewports: string[]; + zooms: number[]; + inputs: string[]; + widgetKinds?: string[]; + cases: string[]; +} + +interface Manifest { + pullRequests: { number: number; label: string }[]; + caseDimensions: Record; + packets: Packet[]; +} + +interface Finding { + id?: string; + fingerprint?: string; + severity: Severity; + title: string; + area?: string; + summary?: string; + caseIds?: string[]; + evidence?: string[]; +} + +interface ReportMetadata { + packetId: string; + status: Status; + caseStatuses: Record; + execution: { + candidateSha: string | null; + url: string | null; + actualPort: number | null; + runtimeProfile: string | null; + runtimeFlags: string[]; + persona: string | null; + sessionId: string | null; + timestamp: string | null; + viewport: string | null; + input: string | null; + zoom: number | null; + }; + findings: Finding[]; + artifacts: string[]; + widgetChecks: WidgetCheck[]; + performance: { + measurements: PerformanceMeasurement[]; + limitations: string[]; + }; + independentReproductions: IndependentReproduction[]; + notes: string; +} + +interface WidgetCheck { + widgetKind: string; + viewport: string; + sizeRequirement: string; + status: Status; +} + +interface PerformanceMeasurement { + name: string; + value: number | null; + unit: string; + threshold: string; + status: Status; + evidence: string[]; +} + +interface IndependentReproduction { + findingFingerprint: string; + agentId: string; + outcome: "reproduced" | "not-reproduced" | "blocked" | "not-reached"; + evidence: string[]; + notes: string; +} + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const qaRoot = join(repoRoot, "qa/release-v2"); +const reportsRoot = join(qaRoot, "reports"); +const campaignCandidateSha = resolveCheckoutCandidateSha(repoRoot); +const hostRuntimeLimitation = + "The host's fs.inotify.max_user_instances=128 is too low for concurrent native Watchpack watchers. Spawned QA apps keep Turbopack and use 1000ms Watchpack polling; filesystem changes can take up to one polling interval to reach HMR."; +const expectedProfileFlags: Record = { + "main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + "main-readonly": ["DEMO_MODE=true", "DEMO_READ_ONLY=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + "onboarding-fresh": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + degraded: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], +}; +const exhaustiveSizeWidgetKinds = new Set([ + "downloads", + "calendar", + "mediaServer", + "mediaRequests-requestList", + "mediaRequests-requestStats", + "systemResources", + "systemDisks", + "dockerContainers", + "networkControllerSummary", + "networkControllerStatus", + "beszelSystemStats", + "uptimeKuma", + "bookmarks", + "notebook", + "iframe", + "customApi", + "assistant", +]); +const manifest = JSON.parse(await readFile(join(qaRoot, "coverage-manifest.json"), "utf8")) as Manifest; +const init = process.argv.includes("--init"); +const refresh = process.argv.includes("--refresh-placeholders"); + +const escapeCell = (value: string): string => value.replaceAll("|", "\\|").replaceAll("\n", " "); +const list = (values: (string | number)[]): string => (values.length === 0 ? "None" : values.join(", ")); + +const expandedCases = (packet: Packet): { id: string; expected: string }[] => { + const dimensions = manifest.caseDimensions[packet.wave] ?? []; + return packet.cases.flatMap((baseId) => + dimensions.map((dimension) => ({ id: `${baseId}-${dimension.id}`, expected: dimension.expected })), + ); +}; + +const sizeRequirement = (widgetKind: string) => { + if (exhaustiveSizeWidgetKinds.has(widgetKind)) { + return "every width 1-24 × every height 1-6"; + } + return "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds"; +}; + +const widgetChecks = (packet: Packet): WidgetCheck[] => + (packet.widgetKinds ?? []).flatMap((widgetKind) => + packet.viewports.map((viewport) => ({ + widgetKind, + viewport, + sizeRequirement: sizeRequirement(widgetKind), + status: "not-reached", + })), + ); + +const reportMetadata = (packet: Packet): ReportMetadata => ({ + packetId: packet.id, + status: "not-reached", + caseStatuses: Object.fromEntries(expandedCases(packet).map(({ id }) => [id, "not-reached" as const])), + execution: { + candidateSha: campaignCandidateSha, + url: null, + actualPort: null, + runtimeProfile: null, + runtimeFlags: [], + persona: null, + sessionId: null, + timestamp: null, + viewport: null, + input: null, + zoom: null, + }, + findings: [], + artifacts: [], + widgetChecks: widgetChecks(packet), + performance: { measurements: [], limitations: [] }, + independentReproductions: [], + notes: "Not executed.", +}); + +const renderPacketReportBase = (packet: Packet): string => { + const artifactDirectory = resolve(repoRoot, ".screenshots/release-v2", packet.id); + const metadata = reportMetadata(packet); + const cases = expandedCases(packet) + .map( + ({ id, expected }) => + `| ${id} | not-reached | [planned screenshot](${artifactDirectory}/${id}.png) | Not executed | ${expected} |`, + ) + .join("\n"); + const widgetMatrix = metadata.widgetChecks + .map( + (check) => + `| ${check.widgetKind} | ${check.viewport} | ${check.sizeRequirement} | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](${artifactDirectory}/${check.widgetKind}-${check.viewport}.png) | ${check.status} |`, + ) + .join("\n"); + const widgetSection = + widgetMatrix.length === 0 + ? "" + : `\n## Widget kind × viewport checklist\n\n| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n${widgetMatrix}\n`; + + return `\n\n# ${packet.id}: ${packet.title}\n\n| Metadata | Value |\n| --- | --- |\n| Status | not-reached |\n| Wave | ${packet.wave} |\n| PR refs | ${packet.prRefs.map((number) => `#${number}`).join(", ")} |\n| Personas | ${list(packet.personas)} |\n| Boards | ${list(packet.boards)} |\n| Profiles | ${list(packet.profiles)} |\n| Chromium viewports | ${list(packet.viewports)} |\n| Zoom | ${list(packet.zooms.map((zoom) => `${zoom}%`))} |\n| Input | ${list(packet.inputs)} |\n| Widget kinds | ${list(packet.widgetKinds ?? [])} |\n| Artifact directory | [${artifactDirectory}](${artifactDirectory}) |\n| Candidate SHA | not-recorded |\n| URL / actual port | not-recorded |\n| Runtime profile / flags | not-recorded |\n| Executed persona | not-recorded |\n| Browser session ID | not-recorded |\n| Timestamp | not-recorded |\n| Executed viewport / input / zoom | not-recorded |\n\n## Dogfood evidence\n\n| Case ID | Status | Evidence | Observed | Expected |\n| --- | --- | --- | --- | --- |\n${cases}\n${widgetSection}\n## Findings\n\nNo findings recorded. Add structured findings to the metadata block and mirror them here.\n\n## Blockers and gaps\n\nNot executed.\n\n## Cleanup\n\nRecord created resources, cleanup outcome, console errors, and network failures. Never include secret values.\n`; +}; + +const renderPacketReport = (packet: Packet): string => + renderPacketReportBase(packet) + .replace("| Candidate SHA | not-recorded |", `| Candidate SHA | ${campaignCandidateSha} |`) + .replace( + "\n## Findings", + "\n## Performance measurements and limitations\n\nRecord structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.\n\n## Independent reproductions\n\nRecord a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.\n\n## Findings", + ); + +const parseMetadata = (content: string, packet: Packet): { metadata: ReportMetadata; errors: string[] } => { + const metadata = content.match(//)?.[1]; + let rawMetadata: unknown = null; + const parseErrors: string[] = []; + if (!metadata) { + parseErrors.push(`${packet.id}: report metadata block is missing`); + } else { + try { + rawMetadata = JSON.parse(metadata) as unknown; + } catch (error) { + parseErrors.push(`${packet.id}: report metadata is invalid JSON (${String(error)})`); + } + } + const normalized = normalizeReportMetadata(rawMetadata, { + packetId: packet.id, + expectedCaseIds: expandedCases(packet).map(({ id }) => id), + expectedWidgetChecks: widgetChecks(packet), + }); + return { + metadata: normalized.metadata as ReportMetadata, + errors: [...parseErrors, ...normalized.errors], + }; +}; + +const collectHumanReportErrors = (content: string, packet: Packet, metadata: ReportMetadata): string[] => { + const errors: string[] = []; + if (!content.includes(`| Status | ${metadata.status} |`)) { + errors.push(`${packet.id}: human metadata status differs from structured metadata`); + } + if (!content.includes(`| Candidate SHA | ${campaignCandidateSha} |`)) { + errors.push(`${packet.id}: human metadata does not show the campaign candidate`); + } + for (const [caseId, status] of Object.entries(metadata.caseStatuses)) { + if (!content.includes(`| ${caseId} | ${status} |`)) { + errors.push(`${packet.id}/${caseId}: human evidence row/status is missing`); + } + } + errors.push(...collectHumanWidgetStatusErrors(content, packet.id, metadata.widgetChecks)); + + const hasExecution = + metadata.status !== "not-reached" || + Object.values(metadata.caseStatuses).some((status) => status !== "not-reached") || + metadata.widgetChecks.some((check) => check.status !== "not-reached") || + metadata.findings.length > 0 || + metadata.artifacts.length > 0; + if (!hasExecution) return errors; + + const requiredValues = [ + metadata.execution.candidateSha, + metadata.execution.url, + metadata.execution.actualPort, + metadata.execution.runtimeProfile, + metadata.execution.persona, + metadata.execution.sessionId, + metadata.execution.timestamp, + metadata.execution.viewport, + metadata.execution.input, + metadata.execution.zoom, + ]; + for (const value of requiredValues) { + if (value !== null && !content.includes(String(value))) { + errors.push(`${packet.id}: human metadata does not include execution value ${String(value)}`); + } + } + return errors; +}; + +const collectArtifactErrors = async (packet: Packet, metadata: ReportMetadata): Promise => { + const packetArtifactDirectory = resolve(repoRoot, ".screenshots/release-v2", packet.id); + const artifacts = [ + ...metadata.artifacts.map((artifact) => ({ artifact, label: `${packet.id}: artifact` })), + ...metadata.findings.flatMap((finding, findingIndex) => + (finding.evidence ?? []).map((artifact) => ({ + artifact, + label: `${packet.id}: findings[${findingIndex}] evidence`, + })), + ), + ...metadata.performance.measurements.flatMap((measurement, measurementIndex) => + measurement.evidence.map((artifact) => ({ + artifact, + label: `${packet.id}: performance.measurements[${measurementIndex}] evidence`, + })), + ), + ...metadata.independentReproductions.flatMap((reproduction, reproductionIndex) => + reproduction.evidence.map((artifact) => ({ + artifact, + label: `${packet.id}: independentReproductions[${reproductionIndex}] evidence`, + })), + ), + ]; + + const errors: string[] = []; + for (const { artifact, label } of artifacts) { + const error = await validateResolvedArtifactPath(packetArtifactDirectory, artifact, label); + if (error) errors.push(error); + } + return errors; +}; + +const ensureReports = async (): Promise => { + for (const packet of manifest.packets) { + const packetDirectory = join(reportsRoot, packet.id); + const reportPath = join(packetDirectory, "report.md"); + await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`); + await mkdir(packetDirectory, { recursive: true }); + await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`); + if (refresh) { + try { + const parsed = parseMetadata(await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`), packet); + if (parsed.errors.length > 0) throw new Error(parsed.errors.join("; ")); + const existing = parsed.metadata; + const hasExecution = + existing.status !== "not-reached" || + Object.values(existing.caseStatuses).some((status) => status !== "not-reached") || + existing.findings.length > 0 || + existing.artifacts.length > 0 || + (existing.widgetChecks ?? []).some((check) => check.status !== "not-reached") || + (existing.performance?.measurements.length ?? 0) > 0 || + (existing.performance?.limitations.length ?? 0) > 0 || + (existing.independentReproductions?.length ?? 0) > 0; + if (hasExecution) throw new Error(`${packet.id}: refusing to replace a report containing execution results`); + } catch (error) { + if (!(error instanceof Error) || !error.message.includes("ENOENT")) throw error; + } + await writeSafeReportFile(qaRoot, reportPath, renderPacketReport(packet), `${packet.id} report path`); + continue; + } + try { + await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + await writeSafeReportFile(qaRoot, reportPath, renderPacketReport(packet), `${packet.id} report path`); + } + } +}; + +interface CoverageAssignment { + label: string; + detail?: string; + status: Status; +} + +const rollupStatuses = (statuses: Status[]): Status => { + if (statuses.includes("failed")) return "failed"; + if (statuses.includes("blocked")) return "blocked"; + if (statuses.includes("not-reached")) return "not-reached"; + return "passed"; +}; + +const matchingCaseStatus = (metadata: ReportMetadata, fragments: string[]) => { + const statuses = Object.entries(metadata.caseStatuses) + .filter(([caseId]) => fragments.some((fragment) => caseId.includes(fragment))) + .map(([, status]) => status); + if (statuses.length === 0) return metadata.status; + return rollupStatuses(statuses); +}; + +const coverageTable = (assignments: CoverageAssignment[], labelHeading: string) => { + const grouped = new Map(); + for (const assignment of assignments) { + const existing = grouped.get(assignment.label); + if (existing) { + existing.statuses.push(assignment.status); + continue; + } + grouped.set(assignment.label, { detail: assignment.detail ?? "—", statuses: [assignment.status] }); + } + if (grouped.size === 0) + return `| ${labelHeading} | Detail | Passed | Failed | Blocked | Not reached | Total |\n| --- | --- | ---: | ---: | ---: | ---: | ---: |\n| — | Not recorded | 0 | 0 | 0 | 0 | 0 |`; + const rows = [...grouped.entries()] + .toSorted(([left], [right]) => left.localeCompare(right)) + .map(([label, value]) => { + const count = (status: Status) => value.statuses.filter((current) => current === status).length; + return `| ${escapeCell(label)} | ${escapeCell(value.detail)} | ${count("passed")} | ${count("failed")} | ${count("blocked")} | ${count("not-reached")} | ${value.statuses.length} |`; + }); + return `| ${labelHeading} | Detail | Passed | Failed | Blocked | Not reached | Total |\n| --- | --- | ---: | ---: | ---: | ---: | ---: |\n${rows.join("\n")}`; +}; + +const main = async (): Promise => { + if (init || refresh) await ensureReports(); + + const reports: { packet: Packet; metadata: ReportMetadata; metadataErrors: string[] }[] = []; + for (const packet of manifest.packets) { + const reportPath = join(reportsRoot, packet.id, "report.md"); + await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`); + let content = ""; + const readErrors: string[] = []; + try { + content = await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + readErrors.push(`${packet.id}: report.md is missing`); + } + const parsed = parseMetadata(content, packet); + const integrityErrors = collectReportIntegrityErrors(parsed.metadata, { + campaignCandidateSha, + packetId: packet.id, + profiles: packet.profiles, + personas: packet.personas, + viewports: packet.viewports, + zooms: packet.zooms, + inputs: packet.inputs, + expectedProfileFlags, + }); + const humanReportErrors = collectHumanReportErrors(content, packet, parsed.metadata); + const artifactErrors = await collectArtifactErrors(packet, parsed.metadata); + reports.push({ + packet, + metadata: parsed.metadata, + metadataErrors: [...readErrors, ...parsed.errors, ...integrityErrors, ...humanReportErrors, ...artifactErrors], + }); + } + + const packetTotals: Record = { passed: 0, failed: 0, blocked: 0, "not-reached": 0 }; + const caseTotals: Record = { passed: 0, failed: 0, blocked: 0, "not-reached": 0 }; + for (const { metadata } of reports) { + packetTotals[metadata.status] += 1; + for (const status of Object.values(metadata.caseStatuses)) caseTotals[status] += 1; + } + + const findings = aggregateQaFindings( + reports.map(({ packet, metadata }) => ({ + packetId: packet.id, + findings: metadata.findings, + independentReproductions: metadata.independentReproductions, + })), + ); + const severityTotals: Record = { P0: 0, P1: 0, P2: 0, P3: 0 }; + for (const finding of findings) severityTotals[finding.severity] += 1; + + const criticalGaps = collectCriticalCoverageGaps( + reports.map(({ packet, metadata, metadataErrors }) => ({ + packetId: packet.id, + caseStatuses: metadata.caseStatuses, + widgetChecks: metadata.widgetChecks, + metadataErrors, + })), + ); + const decision = releaseDecision(severityTotals, criticalGaps); + const widgetTotals: Record = { passed: 0, failed: 0, blocked: 0, "not-reached": 0 }; + for (const { metadata } of reports) { + for (const check of metadata.widgetChecks) widgetTotals[check.status] += 1; + } + + const ledger = { + schemaVersion: 1, + updatedAt: new Date().toISOString(), + packetStatuses: Object.fromEntries( + reports.map(({ packet, metadata }) => [packet.id, { status: metadata.status, cases: metadata.caseStatuses }]), + ), + }; + const ledgerPath = join(qaRoot, "ledger.json"); + const masterReportPath = join(qaRoot, "master-report.md"); + await assertSafeReportPath(qaRoot, ledgerPath, "release-v2 QA ledger path"); + await assertSafeReportPath(qaRoot, masterReportPath, "release-v2 QA master report path"); + await writeSafeReportFile(qaRoot, ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "release-v2 QA ledger path"); + + const waveRows = Object.entries(Object.groupBy(reports, ({ packet }) => packet.wave)).map(([wave, entries]) => { + const waveReports = entries ?? []; + const passed = waveReports.filter(({ metadata }) => metadata.status === "passed").length; + return `| ${wave} | ${passed} | ${waveReports.length} |`; + }); + const findingRows = + findings.length === 0 + ? "| — | — | No findings recorded | — | — |" + : findings + .map( + (finding) => + `| ${finding.severity} | ${escapeCell(finding.area ?? "general")} | ${escapeCell(finding.title)} | ${list(finding.packets)} | ${list(finding.caseIds)} |`, + ) + .join("\n"); + const gapRows = + criticalGaps.length === 0 + ? "| — | — | — | — |" + : criticalGaps + .map( + (gap) => + `| ${gap.packetId} | ${gap.kind} | ${escapeCell(gap.itemId)} | ${gap.status}${gap.detail ? ` — ${escapeCell(gap.detail)}` : ""} |`, + ) + .join("\n"); + const prRows = manifest.pullRequests + .map( + (pullRequest) => + `| #${pullRequest.number} | ${pullRequest.label} | ${manifest.packets.filter((packet) => packet.prRefs.includes(pullRequest.number)).length} |`, + ) + .join("\n"); + + const prCoverage = coverageTable( + manifest.pullRequests.flatMap((pullRequest) => + reports + .filter(({ packet }) => packet.prRefs.includes(pullRequest.number)) + .map(({ metadata }) => ({ + label: `#${pullRequest.number}`, + detail: pullRequest.label, + status: metadata.status, + })), + ), + "PR", + ); + const featureCoverage = coverageTable( + reports.map(({ packet, metadata }) => ({ label: packet.id, detail: packet.title, status: metadata.status })), + "Feature / agent", + ); + const widgetAssignments = reports.flatMap(({ packet, metadata }) => + (metadata.widgetChecks ?? widgetChecks(packet)).map((check) => ({ + label: check.widgetKind, + detail: packet.id, + status: check.status, + })), + ); + const widgetCoverage = coverageTable(widgetAssignments, "Widget kind"); + const sizeCoverage = coverageTable( + reports.flatMap(({ packet, metadata }) => + (metadata.widgetChecks ?? widgetChecks(packet)).map((check) => ({ + label: check.widgetKind, + detail: check.sizeRequirement, + status: check.status, + })), + ), + "Widget size threshold", + ); + const viewportCoverage = coverageTable( + reports.flatMap(({ packet, metadata }) => + packet.viewports.map((viewport) => ({ label: viewport, detail: packet.id, status: metadata.status })), + ), + "Viewport", + ); + const personaCoverage = coverageTable( + reports.flatMap(({ packet, metadata }) => + packet.personas.map((persona) => ({ label: persona, detail: packet.id, status: metadata.status })), + ), + "Persona", + ); + const permissionCoverage = coverageTable( + reports.flatMap(({ packet, metadata }) => { + const status = matchingCaseStatus(metadata, ["ACCESS", "PERMISSION", "READONLY"]); + return [ + ...packet.personas.map((persona) => ({ label: persona, detail: packet.id, status })), + ...packet.profiles.map((profile) => ({ label: `profile:${profile}`, detail: packet.id, status })), + ]; + }), + "Permission boundary", + ); + const mutationCoverage = coverageTable( + reports.map(({ packet, metadata }) => ({ + label: packet.id, + detail: packet.title, + status: matchingCaseStatus(metadata, ["MUTATION", "PERSISTENCE", "OPTIONS"]), + })), + "Mutation area", + ); + const stateCoverage = coverageTable( + reports.map(({ packet, metadata }) => ({ + label: packet.id, + detail: packet.title, + status: matchingCaseStatus(metadata, ["STATE", "DEGRADED", "RECOVERY", "FAILURE", "EVIDENCE"]), + })), + "State / recovery area", + ); + const performanceMeasurements = reports.flatMap(({ packet, metadata }) => + (metadata.performance?.measurements ?? []).map((measurement) => ({ packet, measurement })), + ); + const performanceRows = + performanceMeasurements.length === 0 + ? "| — | No measurement recorded | — | — | not-reached | — |" + : performanceMeasurements + .map(({ packet, measurement }) => { + const value = measurement.value === null ? "not-recorded" : `${measurement.value} ${measurement.unit}`; + return `| ${packet.id} | ${escapeCell(measurement.name)} | ${escapeCell(value)} | ${escapeCell(measurement.threshold)} | ${measurement.status} | ${list(measurement.evidence)} |`; + }) + .join("\n"); + const limitationRows = [ + `| host-runtime | ${escapeCell(hostRuntimeLimitation)} |`, + ...reports.flatMap(({ packet, metadata }) => + (metadata.performance?.limitations ?? []).map((limitation) => `| ${packet.id} | ${escapeCell(limitation)} |`), + ), + ]; + const reproductionRows = findings.flatMap((finding) => + finding.reproductions.map( + (reproduction) => + `| ${list(finding.packets)} | ${escapeCell(reproduction.findingFingerprint)} | ${escapeCell(reproduction.agentId)} | ${reproduction.outcome} | ${list(reproduction.evidence)} | ${escapeCell(reproduction.notes)} |`, + ), + ); + + const coverageAppendix = `## Coverage by required axis\n\n### Pull request\n\n${prCoverage}\n\n### Feature\n\n${featureCoverage}\n\n### Widget\n\n${widgetCoverage}\n\n### Size threshold\n\nHigh-risk widgets require every width 1–24 × every height 1–6 at all assigned mobile, breakpoint-edge, and desktop viewports. Other widgets require minimum, canonical, wide, tall, maximum, overflow, and behavior-changing threshold checks.\n\n${sizeCoverage}\n\n### Viewport\n\n${viewportCoverage}\n\n### Persona\n\n${personaCoverage}\n\n### Permission\n\n${permissionCoverage}\n\n### Mutation\n\n${mutationCoverage}\n\n### State and recovery\n\n${stateCoverage}\n\n## Performance measurements\n\n| Agent | Measurement | Value | Threshold | Status | Evidence |\n| --- | --- | --- | --- | --- | --- |\n${performanceRows}\n\n### Performance limitations\n\n| Agent | Limitation |\n| --- | --- |\n${limitationRows.length > 0 ? limitationRows.join("\n") : "| — | Not recorded |"}\n\n## Independent reproduction results\n\n| Agent | Finding fingerprint | Reproducing agent | Outcome | Evidence | Notes |\n| --- | --- | --- | --- | --- | --- |\n${reproductionRows.length > 0 ? reproductionRows.join("\n") : "| — | — | — | not-reached | — | Not recorded |"}\n\n`; + + const master = `# Release-v2 QA master report\n\nGenerated: ${new Date().toISOString()}\n\n## Decision: ${decision}\n\nGO requires zero P0/P1 findings and zero critical gaps across assigned cases, widget checks, and report metadata. Current critical gaps: **${criticalGaps.length}**.\n\n## Coverage\n\n| Measure | Passed | Failed | Blocked | Not reached | Total |\n| --- | ---: | ---: | ---: | ---: | ---: |\n| Packets | ${packetTotals.passed} | ${packetTotals.failed} | ${packetTotals.blocked} | ${packetTotals["not-reached"]} | ${reports.length} |\n| Cases | ${caseTotals.passed} | ${caseTotals.failed} | ${caseTotals.blocked} | ${caseTotals["not-reached"]} | ${Object.values(caseTotals).reduce((sum, count) => sum + count, 0)} |\n| Widget checks | ${widgetTotals.passed} | ${widgetTotals.failed} | ${widgetTotals.blocked} | ${widgetTotals["not-reached"]} | ${Object.values(widgetTotals).reduce((sum, count) => sum + count, 0)} |\n\n| Wave | Passed | Total |\n| --- | ---: | ---: |\n${waveRows.join("\n")}\n\n## Severity totals\n\n| P0 | P1 | P2 | P3 |\n| ---: | ---: | ---: | ---: |\n| ${severityTotals.P0} | ${severityTotals.P1} | ${severityTotals.P2} | ${severityTotals.P3} |\n\nDeduplicated by explicit fingerprint, or by severity + area + title when no fingerprint is supplied.\n\n| Severity | Area | Finding | Packets | Cases |\n| --- | --- | --- | --- | --- |\n${findingRows}\n\n## Critical gaps\n\n| Packet | Kind | Coverage item | Status / detail |\n| --- | --- | --- | --- |\n${gapRows}\n\n## PR coverage\n\n| PR | Area | Packets |\n| --- | --- | ---: |\n${prRows}\n\n## Packet reports\n\n${reports.map(({ packet, metadata }) => `- [${packet.id}: ${packet.title}](packets/${packet.id}/report.md) — ${metadata.status}`).join("\n")}\n`; + const enrichedMaster = master + .replace("## Severity totals", `${coverageAppendix}## Severity totals`) + .replaceAll("(packets/", "(reports/") + .replace("## Packet reports", "## Agent reports"); + await writeSafeReportFile(qaRoot, masterReportPath, enrichedMaster, "release-v2 QA master report path"); + + console.log( + `release-v2 QA report: ${decision}; ${reports.length} packets; ${findings.length} deduplicated findings; ${criticalGaps.length} critical gaps`, + ); + const metadataErrors = reports.flatMap((report) => report.metadataErrors); + if (metadataErrors.length > 0) { + console.error(`release-v2 QA report found ${metadataErrors.length} metadata error(s):`); + for (const error of metadataErrors) console.error(`- ${error}`); + process.exitCode = 1; + } +}; + +await main(); diff --git a/scripts/release-v2-qa/runtime.mts b/scripts/release-v2-qa/runtime.mts new file mode 100644 index 000000000..9b2d85044 --- /dev/null +++ b/scripts/release-v2-qa/runtime.mts @@ -0,0 +1,739 @@ +import { randomBytes } from "node:crypto"; +import { closeSync, constants, existsSync, openSync } from "node:fs"; +import { chmod, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises"; +import { createServer } from "node:net"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawn, spawnSync } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { + acquireSlotLease, + assertSafeContainedPath, + assertSafeRunRoot, + assertSupportedRuntimePlatform, + createFixtureEnvironment, + sanitizeAppEnvironment, +} from "./safety.mts"; + +const repoRoot = path.resolve(import.meta.dirname, "../.."); +const defaultRunRoot = path.join(tmpdir(), "homarr-release-v2-qa"); +const markerName = ".homarr-release-v2-qa-root.json"; +const watchpackPollingIntervalMs = 1_000; +const profiles = ["main-writable", "main-readonly", "onboarding-fresh", "degraded"] as const; + +type Profile = (typeof profiles)[number]; +export type RuntimeCommand = "start" | "status" | "stop"; + +export const assertRuntimeCommandPlatform = (command: RuntimeCommand, platform: NodeJS.Platform = process.platform) => { + if (command === "status") return; + assertSupportedRuntimePlatform(platform); +}; + +interface Options { + command: RuntimeCommand; + slot: number; + profile: Profile; + runRoot: string; + candidate: string; + reset: boolean; +} + +interface RuntimeManifest { + schemaVersion: 1; + runId: string; + status: "running" | "stopped"; + candidateSha: string; + branch: string | null; + slot: number; + profile: Profile; + startedAt: string; + stoppedAt?: string; + repoRoot: string; + runRoot: string; + slotDir: string; + dbPath: string; + fixtureManifestPath: string; + nextDistDir: string; + url: string; + fixtureUrl: string; + ports: { app: number; fixture: number; redis: number }; + flags: { demoMode: boolean; demoReadOnly: boolean; unsafeMockIntegration: boolean }; + watcher: { watchpackPollingIntervalMs: number }; + processes: { + app: { pid: number; logPath: string }; + fixture: { pid: number; logPath: string }; + }; + docker: { redisContainer: string }; +} + +const usage = () => { + console.log( + "Usage: runtime.mts --slot <1|2|3> [--profile ] [--run-root ] [--candidate ] [--reset]", + ); +}; + +const parseArgs = (): Options => { + const args = process.argv.slice(2).filter((argument) => argument !== "--"); + if (args[0] === "--help" || args[0] === "-h") { + usage(); + process.exit(0); + } + const command = args.shift(); + if (command !== "start" && command !== "status" && command !== "stop") { + usage(); + throw new Error("A runtime command is required"); + } + + let slot = 0; + let profile: Profile = "main-writable"; + let runRoot = defaultRunRoot; + let candidate = "HEAD"; + let reset = false; + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + const value = args[index + 1]; + if (argument === "--slot" && value) { + slot = Number(value); + index += 1; + continue; + } + if (argument === "--profile" && value && profiles.includes(value as Profile)) { + profile = value as Profile; + index += 1; + continue; + } + if (argument === "--run-root" && value) { + runRoot = value; + index += 1; + continue; + } + if (argument === "--candidate" && value) { + candidate = value; + index += 1; + continue; + } + if (argument === "--reset") { + reset = true; + continue; + } + if (argument === "--help" || argument === "-h") { + usage(); + process.exit(0); + } + throw new Error(`Unknown or incomplete argument: ${argument ?? ""}`); + } + + if (![1, 2, 3].includes(slot)) throw new Error("--slot must be 1, 2, or 3"); + return { command, slot, profile, runRoot: path.resolve(runRoot), candidate, reset }; +}; + +const commandOutput = (command: string, args: string[]) => { + const result = spawnSync(command, args, { cwd: repoRoot, encoding: "utf8" }); + if (result.status !== 0) { + const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status ?? "unknown"}`; + throw new Error(`${command} ${args.join(" ")} failed: ${detail}`); + } + return result.stdout.trim(); +}; + +const resolveCandidate = (candidate: string) => + commandOutput("git", ["rev-parse", "--verify", `${candidate}^{commit}`]).toLowerCase(); + +const currentBranch = () => { + const result = spawnSync("git", ["branch", "--show-current"], { cwd: repoRoot, encoding: "utf8" }); + const branch = result.stdout.trim(); + return branch.length > 0 ? branch : null; +}; + +const writeJsonAtomic = async (filePath: string, value: unknown) => { + await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }); + const temporaryPath = `${filePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`; + const handle = await open( + temporaryPath, + constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, + 0o600, + ); + try { + await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await rename(temporaryPath, filePath); + } catch (error) { + await rm(temporaryPath, { force: true }); + throw error; + } +}; + +const assertRunRootMarker = async (runRoot: string) => { + const markerPath = path.join(runRoot, markerName); + await assertSafeContainedPath(runRoot, markerPath, "QA run-root marker"); + const marker = JSON.parse(await readFile(markerPath, "utf8")) as { + schemaVersion?: number; + repoRoot?: string; + runRoot?: string; + }; + if (marker.schemaVersion !== 1 || path.resolve(marker.repoRoot ?? "") !== repoRoot) { + throw new Error(`QA run-root marker does not belong to this checkout: ${markerPath}`); + } + if (marker.runRoot && path.resolve(marker.runRoot) !== runRoot) { + throw new Error(`QA run-root marker records a different root: ${markerPath}`); + } +}; + +const readManifest = async (manifestPath: string) => { + const value = JSON.parse(await readFile(manifestPath, "utf8")) as RuntimeManifest; + if (value.schemaVersion !== 1) throw new Error(`Unsupported runtime manifest: ${manifestPath}`); + return value; +}; + +export interface PortReservation { + port: number; + release: () => Promise; +} + +export const reserveLoopbackPort = async (): Promise => + await new Promise((resolve, reject) => { + const server = createServer(); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Unable to allocate a TCP port")); + return; + } + let released = false; + resolve({ + port: address.port, + release: async () => { + if (released) return; + released = true; + await new Promise((closeResolve, closeReject) => { + server.close((error) => { + if (error) closeReject(error); + else closeResolve(); + }); + }); + }, + }); + }); + }); + +type PortLaunchResult = { status: "ready"; value: T } | { status: "address-in-use" }; + +export const launchWithPortRetry = async ( + launch: (port: number) => Promise>, + options: { attempts?: number; initialReservation?: PortReservation } = {}, +): Promise => { + const attempts = options.attempts ?? 3; + let reservation = options.initialReservation; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + reservation ??= await reserveLoopbackPort(); + const port = reservation.port; + await reservation.release(); + reservation = undefined; + const result = await launch(port); + if (result.status === "ready") return result.value; + } + throw new Error(`Unable to launch the QA app after ${attempts} address-in-use retries`); +}; + +const waitForJsonFile = async (filePath: string, timeoutMs: number) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + return JSON.parse(await readFile(filePath, "utf8")) as T; + } catch { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + throw new Error(`Timed out waiting for ${filePath}`); +}; + +const waitForUrl = async (url: string, timeoutMs: number) => { + const deadline = Date.now() + timeoutMs; + let lastStatus: number | undefined; + while (Date.now() < deadline) { + try { + const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(5_000) }); + lastStatus = response.status; + if (response.ok) return; + } catch { + // Startup failures are reported from the process log after the deadline. + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`Timed out waiting for ${url}; last status=${lastStatus ?? "unreachable"}`); +}; + +interface SpawnedProcess { + pid: number; + exited: Promise; +} + +const spawnLogged = (command: string, args: string[], logPath: string, env: NodeJS.ProcessEnv): SpawnedProcess => { + const output = openSync( + logPath, + constants.O_CREAT | constants.O_APPEND | constants.O_WRONLY | constants.O_NOFOLLOW, + 0o600, + ); + const child = spawn(command, args, { + cwd: repoRoot, + detached: true, + env, + stdio: ["ignore", output, output], + }); + closeSync(output); + if (!child.pid) throw new Error(`Failed to launch ${command}`); + const exited = new Promise((resolve) => { + child.once("exit", (code) => resolve(code)); + }); + child.unref(); + return { pid: child.pid, exited }; +}; + +const runForeground = (command: string, args: string[], env: NodeJS.ProcessEnv) => { + const result = spawnSync(command, args, { cwd: repoRoot, env, stdio: "inherit" }); + if (result.status !== 0) + throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status ?? "unknown"}`); +}; + +const isProcessRunning = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}; + +const waitForAppAttempt = async ( + spawned: SpawnedProcess, + url: string, + logPath: string, + logOffset: number, + timeoutMs: number, +): Promise<"ready" | "address-in-use"> => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const exitCode = await Promise.race([ + spawned.exited, + new Promise((resolve) => setTimeout(() => resolve(undefined), 250)), + ]); + if (exitCode !== undefined) { + const log = await readFile(logPath, "utf8").catch(() => ""); + const attemptLog = log.slice(logOffset); + if (/EADDRINUSE|address already in use/iu.test(attemptLog)) return "address-in-use"; + throw new Error(`QA app process exited during startup; inspect ${logPath}`); + } + + try { + const response = await fetch(`${url}/api/health/ready`, { + redirect: "manual", + signal: AbortSignal.timeout(5_000), + }); + if (response.ok && isProcessRunning(spawned.pid)) { + await new Promise((resolve) => setTimeout(resolve, 250)); + if (isProcessRunning(spawned.pid)) return "ready"; + } + } catch { + // Continue while the selected process owns its startup attempt. + } + } + throw new Error(`Timed out waiting for ${url}; inspect ${logPath}`); +}; + +const processBelongsToRun = async (pid: number, runId: string) => { + if (!isProcessRunning(pid)) return false; + try { + const environment = await readFile(`/proc/${pid}/environ`); + return environment.includes(Buffer.from(`QA_RUN_ID=${runId}\0`)); + } catch { + return false; + } +}; + +const stopProcess = async (pid: number, runId: string) => { + if (!isProcessRunning(pid)) return; + if (!(await processBelongsToRun(pid, runId))) { + throw new Error(`Refusing to signal PID ${pid}: it is not owned by QA run ${runId}`); + } + try { + process.kill(-pid, "SIGTERM"); + } catch { + process.kill(pid, "SIGTERM"); + } + const deadline = Date.now() + 10_000; + while (Date.now() < deadline && isProcessRunning(pid)) { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + if (isProcessRunning(pid)) { + try { + process.kill(-pid, "SIGKILL"); + } catch { + process.kill(pid, "SIGKILL"); + } + } +}; + +const removeRedisContainer = (container: string, runId: string) => { + const inspect = spawnSync( + "docker", + ["inspect", "--format", '{{ index .Config.Labels "com.homarr.release-v2-qa.run" }}', container], + { + encoding: "utf8", + }, + ); + if (inspect.status !== 0) return; + if (inspect.stdout.trim() !== runId) + throw new Error(`Refusing to remove Docker container without matching QA run label: ${container}`); + const result = spawnSync("docker", ["rm", "--force", container], { stdio: "inherit" }); + if (result.status !== 0) throw new Error(`Failed to remove QA Redis container ${container}`); +}; + +const stopFromManifest = async ( + manifestPath: string, + manifest: RuntimeManifest, + expectedRunRoot: string, + expectedSlot: number, +) => { + if (manifest.status === "stopped") return; + const expectedSlotDir = path.join(expectedRunRoot, "slots", String(expectedSlot)); + const expectedManifestPath = path.join(expectedSlotDir, "runtime-manifest.json"); + if (path.resolve(manifest.runRoot) !== expectedRunRoot || manifest.slot !== expectedSlot) { + throw new Error(`Refusing runtime manifest for a different QA run-root or slot: ${manifestPath}`); + } + if (path.resolve(manifestPath) !== expectedManifestPath) { + throw new Error(`Refusing runtime manifest outside the selected slot: ${manifestPath}`); + } + if (path.resolve(manifest.slotDir) !== path.resolve(expectedSlotDir)) { + throw new Error(`Refusing runtime manifest with an unsafe slot directory: ${manifest.slotDir}`); + } + await assertSafeContainedPath(expectedRunRoot, expectedSlotDir, "QA slot directory"); + await stopProcess(manifest.processes.app.pid, manifest.runId); + await stopProcess(manifest.processes.fixture.pid, manifest.runId); + removeRedisContainer(manifest.docker.redisContainer, manifest.runId); + const stopped: RuntimeManifest = { ...manifest, status: "stopped", stoppedAt: new Date().toISOString() }; + await assertSafeContainedPath(expectedRunRoot, manifestPath, "QA runtime manifest"); + await writeJsonAtomic(manifestPath, stopped); +}; + +const profileFlags = (profile: Profile) => ({ + demoMode: profile !== "onboarding-fresh", + demoReadOnly: profile === "main-readonly", + unsafeMockIntegration: true, +}); + +const startWithLease = async (options: Options) => { + const candidateSha = resolveCandidate(options.candidate); + const checkoutSha = resolveCandidate("HEAD"); + if (checkoutSha !== candidateSha) { + throw new Error(`The requested candidate ${candidateSha} does not match the checked-out HEAD ${checkoutSha}`); + } + const branch = currentBranch(); + + await mkdir(options.runRoot, { recursive: true, mode: 0o700 }); + await assertSafeRunRoot(options.runRoot); + await chmod(options.runRoot, 0o700); + await assertSafeRunRoot(options.runRoot); + const markerPath = path.join(options.runRoot, markerName); + await assertSafeContainedPath(options.runRoot, markerPath, "QA run-root marker"); + if (!existsSync(markerPath)) { + await assertSafeContainedPath(options.runRoot, markerPath, "QA run-root marker"); + await writeJsonAtomic(markerPath, { + schemaVersion: 1, + repoRoot, + runRoot: options.runRoot, + createdAt: new Date().toISOString(), + }); + } else { + await assertRunRootMarker(options.runRoot); + } + + const slotsRoot = path.join(options.runRoot, "slots"); + await assertSafeContainedPath(options.runRoot, slotsRoot, "QA slots directory"); + await mkdir(slotsRoot, { recursive: true, mode: 0o700 }); + await assertSafeContainedPath(options.runRoot, slotsRoot, "QA slots directory"); + const slotDir = path.join(slotsRoot, String(options.slot)); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot directory"); + const manifestPath = path.join(slotDir, "runtime-manifest.json"); + if (existsSync(manifestPath)) { + await assertSafeContainedPath(options.runRoot, manifestPath, "QA runtime manifest"); + const existing = await readManifest(manifestPath); + if (!options.reset) throw new Error(`Slot ${options.slot} already has a manifest; pass --reset to replace it`); + await stopFromManifest(manifestPath, existing, options.runRoot, options.slot); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target"); + await rm(slotDir, { recursive: true }); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target"); + } else if (existsSync(slotDir)) { + if (!options.reset) throw new Error(`Slot ${options.slot} has incomplete state; pass --reset to replace it`); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target"); + await rm(slotDir, { recursive: true }); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target"); + } + const nextDistDir = path.join(repoRoot, "apps/nextjs/.next-qa", `slot-${options.slot}`); + if (options.reset) { + await assertSafeContainedPath(path.join(repoRoot, "apps/nextjs"), nextDistDir, "QA Next cache reset target"); + await rm(nextDistDir, { recursive: true, force: true }); + await assertSafeContainedPath(path.join(repoRoot, "apps/nextjs"), nextDistDir, "QA Next cache reset target"); + } + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot creation target"); + await mkdir(slotDir, { mode: 0o700 }); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot directory"); + await chmod(slotDir, 0o700); + await assertSafeContainedPath(options.runRoot, slotDir, "QA slot directory"); + + const runId = `release-v2-${candidateSha.slice(0, 8)}-s${options.slot}-${Date.now()}`; + const redisContainer = `homarr-release-v2-qa-redis-s${options.slot}-${candidateSha.slice(0, 8)}-${Date.now().toString(36)}`; + const dbPath = path.join(slotDir, "db.sqlite"); + const fixtureReadyPath = path.join(slotDir, "fixture-ready.json"); + const fixtureManifestPath = path.join(slotDir, "fixture-manifest.json"); + const fixtureLogPath = path.join(slotDir, "fixture.log"); + const appLogPath = path.join(slotDir, "app.log"); + const flags = profileFlags(options.profile); + let fixturePid: number | undefined; + let appPid: number | undefined; + let appPortReservation: PortReservation | undefined; + let redisCreated = false; + + try { + const redisRun = spawnSync( + "docker", + [ + "run", + "--detach", + "--name", + redisContainer, + "--label", + `com.homarr.release-v2-qa.run=${runId}`, + "--publish", + "127.0.0.1::6379", + "redis:latest", + ], + { encoding: "utf8" }, + ); + if (redisRun.status !== 0) throw new Error(`Unable to start QA Redis: ${redisRun.stderr.trim()}`); + redisCreated = true; + const redisPortOutput = commandOutput("docker", ["port", redisContainer, "6379/tcp"]); + const redisPort = Number(redisPortOutput.split(":").at(-1)); + if (!Number.isInteger(redisPort)) throw new Error(`Unable to parse QA Redis port: ${redisPortOutput}`); + + await assertSafeContainedPath(options.runRoot, fixtureReadyPath, "QA fixture readiness file"); + await assertSafeContainedPath(options.runRoot, fixtureLogPath, "QA fixture log"); + const fixtureProcess = spawnLogged( + process.execPath, + ["--import", "tsx", "scripts/release-v2-qa/fixture-server.mts", "--port", "0", "--ready-file", fixtureReadyPath], + fixtureLogPath, + createFixtureEnvironment(runId), + ); + fixturePid = fixtureProcess.pid; + const fixtureReady = await waitForJsonFile<{ url: string; port: number }>(fixtureReadyPath, 30_000); + await waitForUrl(`${fixtureReady.url}/health`, 30_000); + appPortReservation = await reserveLoopbackPort(); + let appPort = appPortReservation.port; + let url = `http://127.0.0.1:${appPort}`; + let qaPassword = process.env.QA_PASSWORD; + if (!qaPassword && process.env.QA_PASSWORD_FILE) { + qaPassword = (await readFile(path.resolve(process.env.QA_PASSWORD_FILE), "utf8")).trim(); + } + if (flags.demoMode && !qaPassword) { + throw new Error("QA_PASSWORD or QA_PASSWORD_FILE is required for populated QA profiles"); + } + + const environment: NodeJS.ProcessEnv = { + ...process.env, + AUTH_SECRET: randomBytes(32).toString("hex"), + AUTH_PROVIDERS: "credentials", + AUTH_COOKIE_PREFIX: `homarr_qa_s${options.slot}`, + DB_DRIVER: "better-sqlite3", + DB_URL: dbPath, + DEMO_MODE: String(flags.demoMode), + DEMO_READ_ONLY: String(flags.demoReadOnly), + UNSAFE_ENABLE_MOCK_INTEGRATION: String(flags.unsafeMockIntegration), + SECRET_ENCRYPTION_KEY: randomBytes(32).toString("hex"), + REDIS_IS_EXTERNAL: "true", + REDIS_HOST: "127.0.0.1", + REDIS_PORT: String(redisPort), + REDIS_DATABASE_INDEX: String(options.slot), + QA_CANDIDATE_SHA: candidateSha, + QA_FIXTURE_URL: fixtureReady.url, + QA_PROFILE: options.profile, + QA_RUN_ID: runId, + QA_RUN_ROOT: options.runRoot, + QA_SLOT: String(options.slot), + QA_PASSWORD: qaPassword, + HOMARR_WEBSITE_URL: url, + WORKSHOP_API_URL: fixtureReady.url, + WORKSHOP_WEB_URL: `${fixtureReady.url}/iframe`, + HOMARR_QA_NEXT_DIST_DIR: `.next-qa/slot-${options.slot}`, + HOMARR_VERSION: candidateSha.slice(0, 12), + NEXT_TELEMETRY_DISABLED: "1", + }; + + await assertSafeContainedPath(options.runRoot, dbPath, "QA SQLite database"); + runForeground("pnpm", ["db:migration:sqlite:run"], environment); + await assertSafeContainedPath(options.runRoot, dbPath, "QA SQLite database"); + await assertSafeContainedPath(options.runRoot, fixtureManifestPath, "QA fixture manifest"); + runForeground("pnpm", ["qa:release-v2:seed", "--", "--profile", options.profile, "--output", slotDir], environment); + await assertSafeContainedPath(options.runRoot, fixtureManifestPath, "QA fixture manifest"); + + let appCommand = "pnpm"; + const appEnvironment = { + ...sanitizeAppEnvironment(environment), + WATCHPACK_POLLING: String(watchpackPollingIntervalMs), + }; + await assertSafeContainedPath(options.runRoot, appLogPath, "QA app log"); + const launchedApp = await launchWithPortRetry<{ pid: number; port: number; url: string }>( + async (attemptPort) => { + appPort = attemptPort; + url = `http://127.0.0.1:${appPort}`; + let appArguments = ["--filter", "@homarr/nextjs", "dev", "--hostname", "127.0.0.1", "--port", String(appPort)]; + if (process.platform === "linux" && existsSync("/usr/bin/prlimit")) { + appCommand = "/usr/bin/prlimit"; + appArguments = ["--nofile=65536:1048576", "--", "pnpm", ...appArguments]; + } else { + appCommand = "pnpm"; + } + const logOffset = await stat(appLogPath) + .then((stats) => stats.size) + .catch(() => 0); + await assertSafeContainedPath(options.runRoot, appLogPath, "QA app log"); + const spawned = spawnLogged(appCommand, appArguments, appLogPath, { + ...appEnvironment, + HOMARR_WEBSITE_URL: url, + }); + appPid = spawned.pid; + const outcome = await waitForAppAttempt(spawned, url, appLogPath, logOffset, 300_000); + if (outcome === "address-in-use") { + appPid = undefined; + return { status: "address-in-use" } as const; + } + return { status: "ready", value: { pid: spawned.pid, port: appPort, url } } as const; + }, + { initialReservation: appPortReservation }, + ); + appPortReservation = undefined; + appPid = launchedApp.pid; + appPort = launchedApp.port; + url = launchedApp.url; + + const manifest: RuntimeManifest = { + schemaVersion: 1, + runId, + status: "running", + candidateSha, + branch, + slot: options.slot, + profile: options.profile, + startedAt: new Date().toISOString(), + repoRoot, + runRoot: options.runRoot, + slotDir, + dbPath, + fixtureManifestPath, + nextDistDir, + url, + fixtureUrl: fixtureReady.url, + ports: { app: appPort, fixture: fixtureReady.port, redis: redisPort }, + flags, + watcher: { watchpackPollingIntervalMs }, + processes: { + app: { pid: appPid, logPath: appLogPath }, + fixture: { pid: fixturePid, logPath: fixtureLogPath }, + }, + docker: { redisContainer }, + }; + await writeJsonAtomic(manifestPath, manifest); + console.log(JSON.stringify(manifest, null, 2)); + } catch (error) { + if (appPortReservation) await appPortReservation.release(); + if (appPid) await stopProcess(appPid, runId); + if (fixturePid) await stopProcess(fixturePid, runId); + if (redisCreated) removeRedisContainer(redisContainer, runId); + throw error; + } +}; + +const start = async (options: Options) => { + options.runRoot = await assertSafeRunRoot(options.runRoot, { allowMissing: true }); + const lease = await acquireSlotLease(options.runRoot, options.slot, `start:${options.profile}`); + try { + await startWithLease(options); + } finally { + await lease.release(); + } +}; + +const status = async (options: Options) => { + options.runRoot = await assertSafeRunRoot(options.runRoot); + await assertRunRootMarker(options.runRoot); + const manifestPath = path.join(options.runRoot, "slots", String(options.slot), "runtime-manifest.json"); + await assertSafeContainedPath(options.runRoot, manifestPath, "QA runtime manifest"); + const manifest = await readManifest(manifestPath); + const appReachable = await fetch(`${manifest.url}/api/health/live`, { signal: AbortSignal.timeout(5_000) }) + .then((response) => response.ok) + .catch(() => false); + const fixtureReachable = await fetch(`${manifest.fixtureUrl}/health`, { signal: AbortSignal.timeout(5_000) }) + .then((response) => response.ok) + .catch(() => false); + const redisRunning = + spawnSync("docker", ["inspect", "--format", "{{.State.Running}}", manifest.docker.redisContainer], { + encoding: "utf8", + }).stdout.trim() === "true"; + console.log( + JSON.stringify( + { + ...manifest, + observed: { + appProcess: await processBelongsToRun(manifest.processes.app.pid, manifest.runId), + fixtureProcess: await processBelongsToRun(manifest.processes.fixture.pid, manifest.runId), + appReachable, + fixtureReachable, + redisRunning, + }, + }, + null, + 2, + ), + ); +}; + +const stopWithLease = async (options: Options) => { + const markerPath = path.join(options.runRoot, markerName); + await assertSafeContainedPath(options.runRoot, markerPath, "QA run-root marker"); + if (!existsSync(markerPath)) throw new Error(`QA run-root marker is missing: ${markerPath}`); + await assertRunRootMarker(options.runRoot); + const manifestPath = path.join(options.runRoot, "slots", String(options.slot), "runtime-manifest.json"); + await assertSafeContainedPath(options.runRoot, manifestPath, "QA runtime manifest"); + const manifest = await readManifest(manifestPath); + await stopFromManifest(manifestPath, manifest, options.runRoot, options.slot); + console.log(`Stopped release-v2 QA slot ${options.slot}`); +}; + +const stop = async (options: Options) => { + options.runRoot = await assertSafeRunRoot(options.runRoot); + const lease = await acquireSlotLease(options.runRoot, options.slot, "stop"); + try { + await stopWithLease(options); + } finally { + await lease.release(); + } +}; + +const main = async () => { + const options = parseArgs(); + assertRuntimeCommandPlatform(options.command); + if (options.command === "start") await start(options); + else if (options.command === "status") await status(options); + else await stop(options); +}; + +const isDirectExecution = process.argv[1] + ? pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(fileURLToPath(import.meta.url)).href + : false; +if (isDirectExecution) await main(); diff --git a/scripts/release-v2-qa/runtime.spec.ts b/scripts/release-v2-qa/runtime.spec.ts new file mode 100644 index 000000000..28163ee40 --- /dev/null +++ b/scripts/release-v2-qa/runtime.spec.ts @@ -0,0 +1,81 @@ +// @vitest-environment node + +import { createServer } from "node:net"; +import type { Server } from "node:net"; +import { afterEach, describe, expect, test } from "vitest"; + +import { assertRuntimeCommandPlatform, launchWithPortRetry, reserveLoopbackPort } from "./runtime.mts"; + +const openServers: Server[] = []; + +const listen = async (server: Server, port: number) => + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", resolve); + }); + +const close = async (server: Server) => + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + }); + +afterEach(async () => { + await Promise.all( + openServers.splice(0).map(async (server) => { + if (server.listening) await close(server); + }), + ); +}); + +describe("release-v2 QA runtime port startup", () => { + test("keeps an allocated loopback port reserved until startup is ready to bind", async () => { + const reservation = await reserveLoopbackPort(); + const contender = createServer(); + openServers.push(contender); + + await expect(listen(contender, reservation.port)).rejects.toMatchObject({ code: "EADDRINUSE" }); + await reservation.release(); + await listen(contender, reservation.port); + }); + + test("retries with a newly reserved port after an address-in-use collision", async () => { + let attempts = 0; + const selectedPort = await launchWithPortRetry(async (port) => { + attempts += 1; + const server = createServer(); + openServers.push(server); + await listen(server, port); + + if (attempts === 1) { + const collidedProcess = createServer(); + openServers.push(collidedProcess); + await expect(listen(collidedProcess, port)).rejects.toMatchObject({ code: "EADDRINUSE" }); + await close(server); + return { status: "address-in-use" } as const; + } + + await close(server); + return { status: "ready", value: port } as const; + }); + + expect(attempts).toBe(2); + expect(selectedPort).toBeGreaterThan(0); + }); +}); + +describe("release-v2 QA runtime platform boundary", () => { + test.each(["start", "stop"] as const)("rejects %s before dispatch on an unsupported platform", (command) => { + expect(() => assertRuntimeCommandPlatform(command, "darwin")).toThrow(/Linux/u); + }); + + test("allows read-only status on an unsupported platform", () => { + expect(() => assertRuntimeCommandPlatform("status", "win32")).not.toThrow(); + }); + + test.each(["start", "status", "stop"] as const)("allows %s on Linux", (command) => { + expect(() => assertRuntimeCommandPlatform(command, "linux")).not.toThrow(); + }); +}); diff --git a/scripts/release-v2-qa/safety.mts b/scripts/release-v2-qa/safety.mts new file mode 100644 index 000000000..a00448308 --- /dev/null +++ b/scripts/release-v2-qa/safety.mts @@ -0,0 +1,349 @@ +import { randomUUID } from "node:crypto"; +import { lstat, open, readFile, realpath, unlink } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +const runRootNamePattern = /^homarr-release-v2-qa(?:$|-[A-Za-z0-9][A-Za-z0-9._-]*)$/u; +const zoomOutputNamePattern = /^homarr-release-v2-qa-zoom-[A-Za-z0-9][A-Za-z0-9._-]*$/u; +const ipv6LoopbackHosts = new Set(["::1", "0:0:0:0:0:0:0:1"]); +const leaseSchemaVersion = 1; +const incompleteLeaseGraceMs = 30_000; + +const missingPath = (error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT"; +const existingPath = (error: unknown) => (error as NodeJS.ErrnoException).code === "EEXIST"; + +export const assertSupportedRuntimePlatform = (platform: NodeJS.Platform = process.platform) => { + if (platform !== "linux") { + throw new Error( + "Release-v2 QA runtime mutations are supported only on Linux because safe process ownership and cleanup require procfs and POSIX process groups", + ); + } +}; + +export const isPathWithin = (parent: string, child: string) => { + if (!path.isAbsolute(parent) || !path.isAbsolute(child)) return false; + const relativePath = path.relative(path.resolve(parent), path.resolve(child)); + return relativePath.length === 0 || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); +}; + +const canonicalTemporaryParent = async () => await realpath(path.resolve(tmpdir())); + +const assertSafeTemporaryChild = async ( + target: string, + options: { + allowMissing?: boolean; + label: string; + namePattern: RegExp; + nameRequirement: string; + }, +) => { + const resolvedTarget = path.resolve(target); + const canonicalParent = await canonicalTemporaryParent(); + const targetParent = path.dirname(resolvedTarget); + let realTargetParent: string; + try { + realTargetParent = await realpath(targetParent); + } catch (error) { + if (missingPath(error)) { + throw new Error(`${options.label} must be a direct child of the operating system temporary directory`, { + cause: error, + }); + } + throw error; + } + if (realTargetParent !== canonicalParent) { + throw new Error(`${options.label} must be a direct child of the operating system temporary directory`); + } + + const targetName = path.basename(resolvedTarget); + if (!options.namePattern.test(targetName)) { + throw new Error(options.nameRequirement); + } + + const canonicalTarget = path.join(canonicalParent, targetName); + try { + const stats = await lstat(canonicalTarget); + if (stats.isSymbolicLink()) throw new Error(`${options.label} must not be a symlink`); + if (!stats.isDirectory()) throw new Error(`${options.label} must be a directory`); + if ((await realpath(canonicalTarget)) !== canonicalTarget) { + throw new Error(`${options.label} must not contain symlink components`); + } + } catch (error) { + if (missingPath(error) && options.allowMissing) return canonicalTarget; + throw error; + } + + return canonicalTarget; +}; + +export const assertSafeContainedPath = async (parent: string, child: string, label: string) => { + const resolvedParent = path.resolve(parent); + const resolvedChild = path.resolve(child); + if (!isPathWithin(resolvedParent, resolvedChild)) { + throw new Error(`${label} escapes its approved parent`); + } + + const realParent = await realpath(resolvedParent); + if (realParent !== resolvedParent) { + throw new Error(`${label} parent contains a symlink`); + } + + const relativePath = path.relative(resolvedParent, resolvedChild); + let currentPath = resolvedParent; + for (const segment of relativePath.split(path.sep).filter(Boolean)) { + currentPath = path.join(currentPath, segment); + try { + const stats = await lstat(currentPath); + if (stats.isSymbolicLink()) throw new Error(`${label} contains a symlink`); + } catch (error) { + if (missingPath(error)) break; + throw error; + } + } + + return resolvedChild; +}; + +export const assertSafeRunRoot = async (runRoot: string, options: { allowMissing?: boolean } = {}) => + await assertSafeTemporaryChild(runRoot, { + allowMissing: options.allowMissing, + label: "The QA run-root", + namePattern: runRootNamePattern, + nameRequirement: + 'The QA run-root basename must start with "homarr-release-v2-qa" and use only safe suffix characters', + }); + +export const assertSafeZoomOutput = async (outputDirectory: string, options: { allowMissing?: boolean } = {}) => + await assertSafeTemporaryChild(outputDirectory, { + allowMissing: options.allowMissing, + label: "The zoom extension output", + namePattern: zoomOutputNamePattern, + nameRequirement: + 'The zoom extension output basename must start with "homarr-release-v2-qa-zoom-" and use only safe suffix characters', + }); + +const readLinuxProcessStartMarker = async (pid: number) => { + if (process.platform !== "linux") return null; + try { + const processStat = await readFile(`/proc/${pid}/stat`, "utf8"); + const commandEnd = processStat.lastIndexOf(")"); + if (commandEnd < 0) return null; + const fieldsAfterCommand = processStat + .slice(commandEnd + 2) + .trim() + .split(/\s+/u); + const startTime = fieldsAfterCommand[19]; + return startTime ? `linux:${startTime}` : null; + } catch { + return null; + } +}; + +const isProcessRunning = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}; + +interface SlotLeaseOwner { + schemaVersion: 1; + pid: number; + processStartMarker: string | null; + token: string; + runRoot: string; + slot: number; + operation: string; + acquiredAt: string; +} + +interface LeaseSnapshot { + device: bigint; + inode: bigint; + modifiedMs: number; + content: string; + owner: SlotLeaseOwner | null; +} + +export interface SlotLease { + path: string; + owner: SlotLeaseOwner; + release: () => Promise; +} + +export const getSlotLeasePath = async (runRoot: string, slot: number) => { + const canonicalRunRoot = await assertSafeRunRoot(runRoot, { allowMissing: true }); + const canonicalParent = await canonicalTemporaryParent(); + return path.join(canonicalParent, `.${path.basename(canonicalRunRoot)}.slot-${slot}.lease`); +}; + +const readLeaseSnapshot = async (leasePath: string): Promise => { + const stats = await lstat(leasePath, { bigint: true }); + if (stats.isSymbolicLink() || !stats.isFile()) { + throw new Error(`Refusing unsafe QA slot lease path: ${leasePath}`); + } + const content = await readFile(leasePath, "utf8"); + let owner: SlotLeaseOwner | null = null; + try { + owner = JSON.parse(content) as SlotLeaseOwner; + } catch { + // An acquiring process may have created the file but not completed its marker write yet. + } + return { + device: stats.dev, + inode: stats.ino, + modifiedMs: Number(stats.mtimeMs), + content, + owner, + }; +}; + +const isValidLeaseOwner = (owner: SlotLeaseOwner | null, runRoot: string, slot: number): owner is SlotLeaseOwner => + owner?.schemaVersion === leaseSchemaVersion && + Number.isInteger(owner.pid) && + owner.pid > 0 && + typeof owner.token === "string" && + owner.token.length > 0 && + path.resolve(owner.runRoot) === runRoot && + owner.slot === slot; + +const processMatchesLeaseOwner = async (owner: SlotLeaseOwner) => { + if (!isProcessRunning(owner.pid)) return false; + const observedStartMarker = await readLinuxProcessStartMarker(owner.pid); + if (owner.processStartMarker && observedStartMarker) { + return owner.processStartMarker === observedStartMarker; + } + // If the platform cannot verify process start identity, preserve the live lock rather than + // risking recovery over an active owner. + return true; +}; + +const sameLeaseSnapshot = (left: LeaseSnapshot, right: LeaseSnapshot) => + left.device === right.device && + left.inode === right.inode && + left.modifiedMs === right.modifiedMs && + left.content === right.content; + +const recoverStaleLease = async (leasePath: string, snapshot: LeaseSnapshot) => { + const confirmation = await readLeaseSnapshot(leasePath); + if (!sameLeaseSnapshot(snapshot, confirmation)) { + throw new Error(`QA slot lease changed during stale recovery: ${leasePath}`); + } + await unlink(leasePath); +}; + +export const acquireSlotLease = async ( + runRoot: string, + slot: number, + operation: string, + platform: NodeJS.Platform = process.platform, +): Promise => { + assertSupportedRuntimePlatform(platform); + if (![1, 2, 3].includes(slot)) throw new Error("QA slot lease requires slot 1, 2, or 3"); + const canonicalRunRoot = await assertSafeRunRoot(runRoot, { allowMissing: true }); + const leasePath = await getSlotLeasePath(canonicalRunRoot, slot); + const owner: SlotLeaseOwner = { + schemaVersion: leaseSchemaVersion, + pid: process.pid, + processStartMarker: await readLinuxProcessStartMarker(process.pid), + token: randomUUID(), + runRoot: canonicalRunRoot, + slot, + operation, + acquiredAt: new Date().toISOString(), + }; + + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + const handle = await open(leasePath, "wx", 0o600); + try { + await handle.writeFile(`${JSON.stringify(owner)}\n`, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + + return { + path: leasePath, + owner, + release: async () => { + const snapshot = await readLeaseSnapshot(leasePath); + if (!isValidLeaseOwner(snapshot.owner, canonicalRunRoot, slot) || snapshot.owner?.token !== owner.token) { + throw new Error(`Refusing to release a QA slot lease owned by another process: ${leasePath}`); + } + await recoverStaleLease(leasePath, snapshot); + }, + }; + } catch (error) { + if (!existingPath(error)) throw error; + const snapshot = await readLeaseSnapshot(leasePath); + const existingOwner = snapshot.owner; + if (!isValidLeaseOwner(existingOwner, canonicalRunRoot, slot)) { + if (Date.now() - snapshot.modifiedMs < incompleteLeaseGraceMs) { + throw new Error(`QA slot ${slot} lease is being initialized by another operation`, { cause: error }); + } + await recoverStaleLease(leasePath, snapshot); + continue; + } + if (await processMatchesLeaseOwner(existingOwner)) { + throw new Error(`QA slot ${slot} is busy with ${existingOwner.operation} owned by PID ${existingOwner.pid}`, { + cause: error, + }); + } + await recoverStaleLease(leasePath, snapshot); + } + } + + throw new Error(`Unable to acquire QA slot ${slot} lease after stale recovery`); +}; + +const normalizedHost = (host: string) => { + const lowerHost = host.toLowerCase(); + if (lowerHost.startsWith("[") && lowerHost.endsWith("]")) return lowerHost.slice(1, -1); + return lowerHost; +}; + +export const validateLoopbackHost = (host: string) => { + const normalized = normalizedHost(host); + if (normalized !== "127.0.0.1" && normalized !== "localhost" && !ipv6LoopbackHosts.has(normalized)) { + throw new Error("The fixture host must be an explicit loopback address"); + } + return normalized; +}; + +export const validateFixtureUrl = (value: string) => { + let fixtureUrl: URL; + try { + fixtureUrl = new URL(value); + } catch { + throw new Error("The fixture URL must be a valid loopback HTTP origin"); + } + + if ( + fixtureUrl.protocol !== "http:" || + fixtureUrl.username || + fixtureUrl.password || + fixtureUrl.pathname !== "/" || + fixtureUrl.search || + fixtureUrl.hash + ) { + throw new Error("The fixture URL must be a credential-free loopback HTTP origin"); + } + try { + validateLoopbackHost(fixtureUrl.hostname); + } catch { + throw new Error("The fixture URL must be a credential-free loopback HTTP origin"); + } + return fixtureUrl.origin; +}; + +export const sanitizeAppEnvironment = (environment: NodeJS.ProcessEnv) => { + const sanitized = { ...environment }; + delete sanitized.QA_PASSWORD; + delete sanitized.QA_PASSWORD_FILE; + return sanitized; +}; + +export const createFixtureEnvironment = (runId: string): NodeJS.ProcessEnv => ({ QA_RUN_ID: runId }); diff --git a/scripts/release-v2-qa/safety.spec.ts b/scripts/release-v2-qa/safety.spec.ts new file mode 100644 index 000000000..6bb4dd8f5 --- /dev/null +++ b/scripts/release-v2-qa/safety.spec.ts @@ -0,0 +1,233 @@ +// @vitest-environment node + +import { mkdir, realpath, rm, stat, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; + +import { + acquireSlotLease, + assertSafeContainedPath, + assertSafeRunRoot, + assertSupportedRuntimePlatform, + createFixtureEnvironment, + getSlotLeasePath, + isPathWithin, + sanitizeAppEnvironment, + validateFixtureUrl, + validateLoopbackHost, +} from "./safety.mts"; + +const temporaryPaths: string[] = []; +const uniqueName = (suffix: string) => `homarr-release-v2-qa-test-${process.pid}-${Date.now()}-${suffix}`; + +afterEach(async () => { + await Promise.all(temporaryPaths.splice(0).map((target) => rm(target, { force: true, recursive: true }))); +}); + +describe("release-v2 QA harness safety", () => { + test("supports runtime mutations only on Linux", () => { + expect(() => assertSupportedRuntimePlatform("linux")).not.toThrow(); + expect(() => assertSupportedRuntimePlatform("darwin")).toThrow(/Linux/u); + expect(() => assertSupportedRuntimePlatform("win32")).toThrow(/Linux/u); + }); + + test("rejects an unsupported platform before creating a slot lease", async () => { + const runRoot = path.join(tmpdir(), uniqueName("unsupported-platform")); + const leasePath = await getSlotLeasePath(runRoot, 1); + temporaryPaths.push(leasePath); + + await expect(acquireSlotLease(runRoot, 1, "unsupported-start", "darwin")).rejects.toThrow(/Linux/u); + await expect(stat(leasePath)).rejects.toMatchObject({ code: "ENOENT" }); + }); + + test("accepts only direct, predictably named children of the temporary directory", async () => { + const safeRoot = path.join(tmpdir(), uniqueName("safe")); + temporaryPaths.push(safeRoot); + + await expect(assertSafeRunRoot(safeRoot, { allowMissing: true })).resolves.toBe(path.resolve(safeRoot)); + await expect( + assertSafeRunRoot(path.join(tmpdir(), "unrelated-homarr-release-v2-qa"), { allowMissing: true }), + ).rejects.toThrow("must start with"); + await expect( + assertSafeRunRoot(path.join(tmpdir(), "homarr-release-v2-qaevil"), { allowMissing: true }), + ).rejects.toThrow("must start with"); + await expect( + assertSafeRunRoot(path.join(tmpdir(), "nested", uniqueName("nested")), { allowMissing: true }), + ).rejects.toThrow("direct child"); + await expect( + assertSafeRunRoot(path.join(tmpdir(), "..", "var", "tmp", uniqueName("traversal")), { allowMissing: true }), + ).rejects.toThrow("direct child"); + }); + + test("canonicalizes a temporary-directory alias without widening containment", async () => { + const temporaryAlias = path.join(tmpdir(), uniqueName("temporary-alias")); + const runRootName = uniqueName("canonical-root"); + temporaryPaths.push(temporaryAlias); + await symlink(tmpdir(), temporaryAlias, "dir"); + + await expect(assertSafeRunRoot(path.join(temporaryAlias, runRootName), { allowMissing: true })).resolves.toBe( + path.join(await realpath(tmpdir()), runRootName), + ); + }); + + test("rejects a symlink run-root", async () => { + const target = path.join(tmpdir(), uniqueName("target")); + const linkedRoot = path.join(tmpdir(), uniqueName("linked")); + temporaryPaths.push(linkedRoot, target); + await mkdir(target); + await symlink(target, linkedRoot); + + await expect(assertSafeRunRoot(linkedRoot)).rejects.toThrow("symlink"); + }); + + test("rejects symlink components in a contained reset target", async () => { + const runRoot = path.join(tmpdir(), uniqueName("component-root")); + const externalTarget = path.join(tmpdir(), uniqueName("component-target")); + temporaryPaths.push(runRoot, externalTarget); + await mkdir(runRoot); + await mkdir(externalTarget); + await symlink(externalTarget, path.join(runRoot, "slots")); + + await expect( + assertSafeContainedPath(runRoot, path.join(runRoot, "slots", "1"), "QA slot reset target"), + ).rejects.toThrow("symlink"); + }); + + test("catches a symlink swapped in after an earlier missing-target check", async () => { + const runRoot = path.join(tmpdir(), uniqueName("swap-root")); + const externalTarget = path.join(tmpdir(), uniqueName("swap-target")); + const slotPath = path.join(runRoot, "slots", "1"); + temporaryPaths.push(runRoot, externalTarget); + await mkdir(path.dirname(slotPath), { recursive: true }); + await mkdir(externalTarget); + + await expect(assertSafeContainedPath(runRoot, slotPath, "QA slot mutation target")).resolves.toBe(slotPath); + await symlink(externalTarget, slotPath); + await expect(assertSafeContainedPath(runRoot, slotPath, "QA slot mutation target")).rejects.toThrow("symlink"); + }); + + test("rejects a concurrent operation while a live slot lease is held", async () => { + const runRoot = path.join(tmpdir(), uniqueName("live-lease")); + const lease = await acquireSlotLease(runRoot, 1, "first-start"); + temporaryPaths.push(lease.path); + + await expect(acquireSlotLease(runRoot, 1, "second-start")).rejects.toThrow(/slot 1 is busy.*first-start/u); + await lease.release(); + }); + + test("recovers a stale lease with a dead owner PID", async () => { + const runRoot = path.join(tmpdir(), uniqueName("stale-lease")); + const leasePath = await getSlotLeasePath(runRoot, 2); + temporaryPaths.push(leasePath); + await writeFile( + leasePath, + `${JSON.stringify({ + schemaVersion: 1, + pid: 2_147_483_647, + processStartMarker: "linux:stale", + token: "stale-token", + runRoot: path.resolve(runRoot), + slot: 2, + operation: "stale-start", + acquiredAt: "2026-01-01T00:00:00.000Z", + })}\n`, + { mode: 0o600 }, + ); + + const lease = await acquireSlotLease(runRoot, 2, "replacement-start"); + expect(lease.owner.operation).toBe("replacement-start"); + await lease.release(); + }); + + test("recovers a PID-reuse lease marker without signaling the unrelated process", async () => { + const runRoot = path.join(tmpdir(), uniqueName("pid-reuse-lease")); + const leasePath = await getSlotLeasePath(runRoot, 3); + temporaryPaths.push(leasePath); + await writeFile( + leasePath, + `${JSON.stringify({ + schemaVersion: 1, + pid: process.pid, + processStartMarker: "linux:not-this-process", + token: "reused-pid-token", + runRoot: path.resolve(runRoot), + slot: 3, + operation: "abandoned-start", + acquiredAt: "2026-01-01T00:00:00.000Z", + })}\n`, + { mode: 0o600 }, + ); + + const lease = await acquireSlotLease(runRoot, 3, "safe-recovery"); + expect(process.pid).toBeGreaterThan(0); + await lease.release(); + }); + + test("uses path segments rather than string prefixes for containment", () => { + const parent = path.join(tmpdir(), "qa-artifacts", "packet-1"); + expect(isPathWithin(parent, path.join(parent, "screen.png"))).toBe(true); + expect(isPathWithin(parent, `${parent}-spoof/screen.png`)).toBe(false); + expect(isPathWithin(parent, path.join(parent, "..", "packet-2", "screen.png"))).toBe(false); + expect(isPathWithin(parent, ".screenshots/release-v2/packet-1/screen.png")).toBe(false); + }); + + test.each(["http://127.0.0.1:3210", "http://localhost:3210", "http://[::1]:3210", "http://[0:0:0:0:0:0:0:1]:3210"])( + "accepts a credential-free loopback fixture origin: %s", + (fixtureUrl) => { + expect(validateFixtureUrl(fixtureUrl)).toBe(new URL(fixtureUrl).origin); + }, + ); + + test.each([ + "https://127.0.0.1:3210", + "http://user:secret@127.0.0.1:3210", + "http://0.0.0.0:3210", + "http://192.0.2.1:3210", + "http://localhost.example:3210", + "http://127.0.0.1:3210/path", + ])("rejects an unsafe fixture URL without echoing it: %s", (fixtureUrl) => { + expect(() => validateFixtureUrl(fixtureUrl)).toThrow("fixture URL"); + }); + + test.each(["127.0.0.1", "localhost", "::1", "[::1]", "0:0:0:0:0:0:0:1"])( + "accepts the loopback fixture bind host %s", + (host) => { + expect(validateLoopbackHost(host)).toBeTruthy(); + }, + ); + + test.each(["0.0.0.0", "::", "192.0.2.1", "example.test"])("rejects the non-loopback bind host %s", (host) => { + expect(() => validateLoopbackHost(host)).toThrow("loopback"); + }); + + test("removes both QA password inputs from the spawned app environment", () => { + const environment = sanitizeAppEnvironment({ + QA_PASSWORD: "do-not-forward", + QA_PASSWORD_FILE: "/tmp/do-not-forward", + QA_RUN_ID: "qa-run", + }); + + expect(environment).toEqual({ QA_RUN_ID: "qa-run" }); + expect("QA_PASSWORD" in environment).toBe(false); + expect("QA_PASSWORD_FILE" in environment).toBe(false); + }); + + test("uses an explicit fixture environment allowlist", () => { + const environment = createFixtureEnvironment("qa-run"); + + expect(environment).toEqual({ QA_RUN_ID: "qa-run" }); + for (const forbiddenName of [ + "QA_PASSWORD", + "QA_PASSWORD_FILE", + "DB_URL", + "AUTH_SECRET", + "API_KEY", + "SECRET_ENCRYPTION_KEY", + "HOME", + "PATH", + ]) { + expect(forbiddenName in environment).toBe(false); + } + }); +}); diff --git a/scripts/release-v2-qa/seed.mts b/scripts/release-v2-qa/seed.mts new file mode 100644 index 000000000..5934086cd --- /dev/null +++ b/scripts/release-v2-qa/seed.mts @@ -0,0 +1,1300 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { stringify } from "superjson"; + +import { comparePasswordsAsync, hashPasswordAsync } from "../../packages/auth/security.ts"; +import { customWidgetDefinitionSchema } from "../../packages/custom-widgets/src/core/index.ts"; +import { + emptySuperJSON, + getBoardLaneColumnCount, + getDefaultWidgetConfig, + getRootSectionLane, + getWidgetKindsForIntegration, + widgetKinds, +} from "../../packages/definitions/index.ts"; +import type { BoardPermission, GroupPermissionKey, LayoutRole, WidgetKind } from "../../packages/definitions/index.ts"; +import { db as rootDb, eq, inArray, sql } from "../../packages/db/index.ts"; +import { + apps, + boardGroupPermissions, + boardUserPermissions, + boards, + customWidgetDefinitions, + groupMembers, + groupPermissions, + groups, + iconRepositories, + icons, + integrationItems, + integrations, + itemLayouts, + items, + layouts, + onboarding, + sectionLayouts, + sections, + users, +} from "../../packages/db/schema/index.ts"; +import { boardNameSchema } from "../../packages/validation/src/board.ts"; + +import { findFixtureGeometryErrors } from "./geometry.mts"; +import { + assertReleaseV2QaProfileEnvironment, + releaseV2QaProfiles, + resolveCheckoutCandidateSha, + validateCandidateSha, +} from "./provenance.mts"; +import type { ReleaseV2QaProfile, ReleaseV2QaProfileFlags } from "./provenance.mts"; +import { validateFixtureUrl } from "./safety.mts"; + +let db = rootDb; +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); + +const profiles = releaseV2QaProfiles; +type Profile = ReleaseV2QaProfile; + +interface CliOptions { + candidateSha?: string; + manifestPath?: string; + outputDirectory?: string; + profile?: Profile; + reset: boolean; +} + +interface PersonaDefinition { + handle: string; + name: string; + permissions: GroupPermissionKey[]; +} + +interface LayoutDefinition { + breakpoint: number; + columnCount: number; + handle: string; + leftGutterColumnCount?: number; + name: string; + rightGutterColumnCount?: number; + role: LayoutRole; +} + +interface BoardDefinition { + handle: string; + isPublic?: boolean; + label: string; + layouts?: LayoutDefinition[]; + ownerHandle: string; +} + +interface ItemDefinition { + advancedOptions?: string; + boardHandle: string; + handle: string; + height: number; + kind: WidgetKind; + options: string; + sectionHandle?: string; + width: number; +} + +interface Position { + boardHandle: string; + height: number; + itemHandle: string; + layoutHandle: string; + sectionHandle: string; + width: number; + xOffset: number; + yOffset: number; +} + +const mobileLayout: LayoutDefinition = { + handle: "mobile", + name: "Mobile", + columnCount: 3, + breakpoint: 0, + role: "mobile", +}; +const defaultLayouts: LayoutDefinition[] = [ + mobileLayout, + { handle: "base", name: "Base", columnCount: 12, breakpoint: 768, role: "base" }, +]; + +const personas: PersonaDefinition[] = [ + { handle: "avery-admin", name: "Avery Admin", permissions: ["admin"] }, + { handle: "rowan-owner", name: "Rowan Owner", permissions: ["board-create"] }, + { handle: "eden-editor", name: "Eden Editor", permissions: [] }, + { handle: "vivian-viewer", name: "Vivian Viewer", permissions: [] }, + { handle: "nolan-outsider", name: "Nolan Outsider", permissions: [] }, + { handle: "morgan-mobile", name: "Morgan Mobile", permissions: [] }, + { handle: "casey-chaos", name: "Casey Chaos", permissions: [] }, + { handle: "ingrid-infra", name: "Ingrid Infra", permissions: ["integration-full-all"] }, + { handle: "maya-media", name: "Maya Media", permissions: ["media-full-all"] }, + { handle: "brooke-minimalist", name: "Brooke Minimalist", permissions: [] }, + { handle: "cora-creator", name: "Cora Creator", permissions: ["board-create", "app-create"] }, + { handle: "ash-assistant", name: "Ash Assistant", permissions: [] }, + { handle: "kira-keyboard", name: "Kira Keyboard", permissions: [] }, + { handle: "nora-newcomer", name: "Nora Newcomer", permissions: [] }, +]; + +const widgetGalleryGroups: readonly (readonly WidgetKind[])[] = [ + ["clock", "weather", "airQuality", "countdown", "timer"], + ["app", "iframe", "video", "minecraftServerStatus", "stockPrice"], + ["notebook", "anchorNote", "bookmarks", "rssFeed", "timetable"], + ["downloads", "dockerContainers", "indexerManager", "dnsHoleSummary", "dnsHoleControls"], + ["smartHome-entityState", "smartHome-executeAutomation", "healthMonitoring", "systemResources", "systemDisks"], + ["firewall", "notifications", "networkControllerSummary", "networkControllerStatus", "uptimeKuma"], + ["beszelSystemTable", "beszelSystemGrid", "beszelAlerts", "beszelSystemStats", "wud"], + ["ups", "vpn", "speedtestTracker", "traefik", "umami"], + ["calendar", "mediaServer", "mediaRequests-requestList", "mediaRequests-requestStats", "mediaMissing"], + ["mediaReleases", "mediaTranscoding", "immich-serverStats", "immich-albumCarousel", "audioStats"], + ["paperlessNgx", "patchmon", "bazarr", "tracearr", "releases"], + ["coolify", "archiveTeamWarrior", "customApi", "assistant"], +]; + +const sharedBoards: BoardDefinition[] = [ + { + handle: "grid-24", + label: "QA v2 · 24-column grid", + ownerHandle: "avery-admin", + layouts: [ + mobileLayout, + { + handle: "six", + name: "6 columns", + columnCount: 6, + breakpoint: 480, + role: "custom", + leftGutterColumnCount: 1, + rightGutterColumnCount: 1, + }, + { + handle: "twelve", + name: "12 columns", + columnCount: 12, + breakpoint: 768, + role: "custom", + leftGutterColumnCount: 1, + rightGutterColumnCount: 1, + }, + { + handle: "eighteen", + name: "18 columns", + columnCount: 18, + breakpoint: 1024, + role: "custom", + leftGutterColumnCount: 1, + rightGutterColumnCount: 1, + }, + { + handle: "width-coverage", + name: "24-column width coverage", + columnCount: 24, + breakpoint: 1280, + role: "custom", + }, + { + handle: "base", + name: "Base 24", + columnCount: 24, + breakpoint: 1440, + role: "base", + leftGutterColumnCount: 1, + rightGutterColumnCount: 1, + }, + ], + }, + { + handle: "scroll", + label: "QA v2 · Scroll", + ownerHandle: "avery-admin", + layouts: [ + mobileLayout, + { + handle: "base", + name: "Base with rails", + columnCount: 12, + breakpoint: 768, + role: "base", + leftGutterColumnCount: 2, + rightGutterColumnCount: 2, + }, + ], + }, + { handle: "dense-collisions", label: "QA v2 · Dense collisions", ownerHandle: "avery-admin" }, + { handle: "nested-containers", label: "QA v2 · Nested containers", ownerHandle: "avery-admin" }, + { + handle: "layout-boundaries", + label: "QA v2 · Layout boundaries", + ownerHandle: "avery-admin", + layouts: [ + mobileLayout, + { handle: "compact", name: "Compact", columnCount: 6, breakpoint: 480, role: "custom" }, + { handle: "desktop", name: "Desktop", columnCount: 12, breakpoint: 1024, role: "custom" }, + { handle: "base", name: "Base 24", columnCount: 24, breakpoint: 1440, role: "base" }, + ], + }, + { handle: "icons-bookmarks", label: "QA v2 · Icons and bookmarks", ownerHandle: "avery-admin" }, + ...Array.from({ length: 12 }, (_, index) => { + const handle = `widgets-${String(index + 1).padStart(2, "0")}`; + return { handle, label: `QA v2 · ${handle}`, ownerHandle: "avery-admin" }; + }), + { + handle: "custom-widget-assistant", + label: "QA v2 · Custom Widget and Assistant", + ownerHandle: "avery-admin", + }, + { + handle: "permissions-public", + label: "QA v2 · Permissions and public access", + ownerHandle: "rowan-owner", + isPublic: true, + }, + { handle: "download-upload", label: "QA v2 · Download and upload", ownerHandle: "maya-media" }, +]; + +const homeBoards: BoardDefinition[] = personas.map((persona) => ({ + handle: `home-${persona.handle}`, + label: `QA v2 · Home · ${persona.handle}`, + ownerHandle: persona.handle, +})); + +const qaBoards = [...homeBoards, ...sharedBoards]; +const boardName = (handle: string) => { + if (handle === "scroll") return "qa-scroll-lab"; + return `qa-${handle}`; +}; +const validateQaBoardDefinitions = () => { + const names = qaBoards.map((board) => boardName(board.handle)); + if (new Set(names).size !== names.length) throw new Error("QA board names must be unique"); + for (const name of names) { + if (!boardNameSchema.safeParse(name).success) throw new Error(`Invalid QA board name: ${name}`); + } +}; +const userId = (handle: string) => `qa-v2-user-${handle}`; +const groupId = (handle: string) => `qa-v2-group-${handle}`; +const boardId = (handle: string) => `qa-v2-board-${handle}`; +const layoutId = (boardHandle: string, handle: string) => `qa-v2-layout-${boardHandle}-${handle}`; +const sectionId = (boardHandle: string, handle: string) => `qa-v2-section-${boardHandle}-${handle}`; +const itemId = (boardHandle: string, handle: string) => `qa-v2-item-${boardHandle}-${handle}`; +const appId = (index: number) => `qa-v2-app-${String(index).padStart(2, "0")}`; +const iconId = (index: number) => `qa-v2-icon-${String(index).padStart(2, "0")}`; +const qaUserIds = personas.map((persona) => userId(persona.handle)); +const qaBoardIds = qaBoards.map((board) => boardId(board.handle)); +const qaLayoutIds = qaBoards.flatMap((board) => + (board.layouts ?? defaultLayouts).map((layout) => layoutId(board.handle, layout.handle)), +); +const qaGroupIds = [groupId("fixture-access"), ...personas.map((persona) => groupId(persona.handle))]; +const qaAppIds = Array.from({ length: 8 }, (_, index) => appId(index + 1)); +const qaIconIds = Array.from({ length: 8 }, (_, index) => iconId(index + 1)); +const mockIntegrationId = "qa-v2-integration-mock"; +const customDefinitionId = "qa-v2-custom-widget-fixture"; +const disabledCustomDefinitionId = "qa-v2-custom-widget-disabled"; +const missingSecretCustomDefinitionId = "qa-v2-custom-widget-missing-secret"; +const customDefinitionIds = [customDefinitionId, disabledCustomDefinitionId, missingSecretCustomDefinitionId]; +const iconRepositoryId = "qa-v2-icon-repository"; + +const parseCli = (): CliOptions => { + const options: CliOptions = { reset: false }; + for (let index = 2; index < process.argv.length; index++) { + const argument = process.argv[index]; + if (argument === "--") continue; + if (argument === "--reset") { + options.reset = true; + continue; + } + if (!["--candidate-sha", "--manifest", "--output", "--profile"].includes(argument ?? "")) { + throw new Error(`Unknown argument: ${argument ?? ""}`); + } + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}`); + index++; + if (argument === "--candidate-sha") options.candidateSha = value; + if (argument === "--manifest") options.manifestPath = value; + if (argument === "--output") options.outputDirectory = value; + if (argument === "--profile") { + if (!profiles.includes(value as Profile)) throw new Error(`Unsupported profile: ${value}`); + options.profile = value as Profile; + } + } + return options; +}; + +const requiredEnv = (name: string) => { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; +}; + +const requiredQaPassword = async () => { + const direct = process.env.QA_PASSWORD?.trim(); + if (direct) return direct; + const passwordFile = process.env.QA_PASSWORD_FILE?.trim(); + if (passwordFile) { + const fromFile = (await readFile(resolve(passwordFile), "utf8")).trim(); + if (fromFile) return fromFile; + } + throw new Error("QA_PASSWORD or QA_PASSWORD_FILE is required"); +}; + +const runInTransactionAsync = async (operation: () => Promise) => { + if ((process.env.DB_DRIVER ?? "better-sqlite3") === "better-sqlite3") { + rootDb.run(sql.raw("BEGIN IMMEDIATE")); + try { + await operation(); + rootDb.run(sql.raw("COMMIT")); + } catch (error) { + rootDb.run(sql.raw("ROLLBACK")); + throw error; + } + return; + } + + try { + await rootDb.transaction(async (transaction) => { + db = transaction as typeof rootDb; + await operation(); + }); + } finally { + db = rootDb; + } +}; + +const resolveManifestPath = (options: CliOptions) => { + const explicit = options.manifestPath ?? process.env.QA_FIXTURE_MANIFEST; + if (explicit) return resolve(explicit); + if (options.outputDirectory) return resolve(options.outputDirectory, "fixture-manifest.json"); + const runRoot = process.env.QA_RUN_ROOT; + const slot = process.env.QA_SLOT; + if (runRoot && slot) return resolve(runRoot, "slots", slot, "fixture-manifest.json"); + throw new Error("--manifest, --output, or QA_FIXTURE_MANIFEST is required"); +}; + +const upsertById = async ( + query: { findFirst: (input: unknown) => Promise }, + table: { id: unknown }, + row: TRow, +) => { + const existing = await query.findFirst({ where: eq(table.id as never, row.id) }); + if (existing) { + await db + .update(table as never) + .set(row as never) + .where(eq(table.id as never, row.id)); + return; + } + await db.insert(table as never).values(row as never); +}; + +const resetQaRowsAsync = async () => { + if (qaBoardIds.length > 0) await db.delete(boards).where(inArray(boards.id, qaBoardIds)); + if (qaUserIds.length > 0) await db.delete(users).where(inArray(users.id, qaUserIds)); + if (qaGroupIds.length > 0) await db.delete(groups).where(inArray(groups.id, qaGroupIds)); + await db.delete(customWidgetDefinitions).where(inArray(customWidgetDefinitions.id, customDefinitionIds)); + await db.delete(integrations).where(eq(integrations.id, mockIntegrationId)); + if (qaAppIds.length > 0) await db.delete(apps).where(inArray(apps.id, qaAppIds)); + if (qaIconIds.length > 0) await db.delete(icons).where(inArray(icons.id, qaIconIds)); + await db.delete(iconRepositories).where(eq(iconRepositories.id, iconRepositoryId)); +}; + +const setOnboardingStepAsync = async (profile: Profile) => { + const row = await db.query.onboarding.findFirst(); + if (!row) throw new Error("Production seed did not create the onboarding row"); + if (profile === "onboarding-fresh") { + await db.update(onboarding).set({ step: "start", previousStep: null }).where(eq(onboarding.id, row.id)); + return; + } + await db.update(onboarding).set({ step: "finish", previousStep: "setup" }).where(eq(onboarding.id, row.id)); +}; + +const resolvePasswordHashAsync = async (password: string) => { + const existing = await db.query.users.findMany({ where: inArray(users.id, qaUserIds), columns: { password: true } }); + for (const row of existing) { + if (row.password && (await comparePasswordsAsync(password, row.password))) return row.password; + } + return hashPasswordAsync(password); +}; + +const seedPersonasAsync = async (passwordHash: string) => { + for (const persona of personas) { + const id = userId(persona.handle); + await upsertById( + db.query.users as never, + users as never, + { + id, + name: persona.handle, + email: `qa-v2-${persona.handle}@example.test`, + emailVerified: null, + image: null, + password: passwordHash, + provider: "credentials", + homeBoardId: null, + mobileHomeBoardId: null, + defaultSearchEngineId: null, + openSearchInNewTab: true, + ddgBangs: true, + colorScheme: "dark", + firstDayOfWeek: 1, + pingIconsEnabled: true, + enableRightClickOnWidgets: true, + completedManageTour: true, + completedBoardTour: true, + } as typeof users.$inferSelect, + ); + } + + await upsertById( + db.query.groups as never, + groups as never, + { + id: groupId("fixture-access"), + name: "QA v2 fixture access", + ownerId: userId("avery-admin"), + homeBoardId: null, + mobileHomeBoardId: null, + position: 10_000, + } as typeof groups.$inferSelect, + ); + + for (const [index, persona] of personas.entries()) { + await upsertById( + db.query.groups as never, + groups as never, + { + id: groupId(persona.handle), + name: `QA v2 role · ${persona.handle}`, + ownerId: userId("avery-admin"), + homeBoardId: null, + mobileHomeBoardId: null, + position: 10_001 + index, + } as typeof groups.$inferSelect, + ); + } + + await db.delete(groupPermissions).where(inArray(groupPermissions.groupId, qaGroupIds)); + await db.delete(groupMembers).where(inArray(groupMembers.userId, qaUserIds)); + await db + .insert(groupPermissions) + .values([ + { groupId: groupId("fixture-access"), permission: "app-use-all" }, + { groupId: groupId("fixture-access"), permission: "integration-use-all" }, + ...personas.flatMap((persona) => + persona.permissions.map((permission) => ({ groupId: groupId(persona.handle), permission })), + ), + ]); + await db + .insert(groupMembers) + .values([ + ...personas.map((persona) => ({ groupId: groupId("fixture-access"), userId: userId(persona.handle) })), + ...personas.map((persona) => ({ groupId: groupId(persona.handle), userId: userId(persona.handle) })), + ]); +}; + +const seedGlobalFixturesAsync = async (fixtureUrl: string) => { + await upsertById( + db.query.integrations as never, + integrations as never, + { + id: mockIntegrationId, + name: "QA v2 built-in mock integration", + url: fixtureUrl, + kind: "mock", + appId: null, + } as typeof integrations.$inferSelect, + ); + + const appRows = qaAppIds.map((id, index) => ({ + id, + name: `QA Fixture ${String(index + 1).padStart(2, "0")}`, + description: `Deterministic release v2 QA bookmark ${index + 1}`, + iconUrl: `${fixtureUrl}/media/image.png?app=${index + 1}`, + href: + [ + `${fixtureUrl}/api/qa/download/sample.txt`, + null, + "/relative/qa-v2", + "https://example.com/qa-v2-external", + `${fixtureUrl}/error?status=404`, + `${fixtureUrl}/iframe?app=6`, + `${fixtureUrl}/empty`, + `${fixtureUrl}/slow?ms=200`, + ][index] ?? null, + pingUrl: index === 1 ? null : index === 4 ? `${fixtureUrl}/error?status=503` : `${fixtureUrl}/json`, + })); + for (const row of appRows) await upsertById(db.query.apps as never, apps as never, row); + + await upsertById(db.query.iconRepositories as never, iconRepositories as never, { + id: iconRepositoryId, + slug: "qa-v2/release-fixtures", + }); + for (const [index, id] of qaIconIds.entries()) { + await upsertById(db.query.icons as never, icons as never, { + id, + name: `qa-v2-icon-${String(index + 1).padStart(2, "0")}`, + url: `${fixtureUrl}/media/image.png?icon=${index + 1}`, + checksum: `qa-v2-checksum-${String(index + 1).padStart(2, "0")}`, + iconRepositoryId, + }); + } + + const definition = customWidgetDefinitionSchema.parse({ + $schema: "homarr-custom-widget-v2", + name: "Release v2 QA fixture", + description: "Credential-free deterministic Custom JSX fixture for release v2 browser QA.", + sources: { + default: { name: "QA fixture", baseUrl: fixtureUrl, networkScope: "loopback", auth: "none" }, + }, + requests: { fixture: { path: "/api/qa/custom-widget", cacheSeconds: 0 } }, + options: {}, + template: `{data.fixture?.title ?? "Release v2 QA"}{data.fixture?.status ?? "loading"}{status.fixture?.loading ? : status.fixture?.error ? {status.fixture.error} : {(data.fixture?.items ?? []).map(item => {item.label}{item.value})}}`, + }); + const missingSecretDefinition = customWidgetDefinitionSchema.parse({ + ...definition, + name: "Release v2 QA missing secret", + description: "Valid bearer-auth definition intentionally stored without a secret.", + sources: { + default: { name: "QA protected fixture", baseUrl: fixtureUrl, networkScope: "loopback", auth: "bearer" }, + }, + }); + const definitions = [ + { id: customDefinitionId, definition, enabled: true }, + { id: disabledCustomDefinitionId, definition: { ...definition, name: "Release v2 QA disabled" }, enabled: false }, + { id: missingSecretCustomDefinitionId, definition: missingSecretDefinition, enabled: true }, + ]; + for (const row of definitions) { + await upsertById( + db.query.customWidgetDefinitions as never, + customWidgetDefinitions as never, + { + id: row.id, + name: row.definition.name, + description: row.definition.description ?? null, + iconUrl: null, + sources: stringify(row.definition.sources), + requests: stringify(row.definition.requests), + options: stringify(row.definition.options), + template: row.definition.template, + enabled: row.enabled, + createdAt: new Date(0), + updatedAt: new Date(0), + creatorId: userId("avery-admin"), + } as typeof customWidgetDefinitions.$inferSelect, + ); + } +}; + +const sourceOptionsByKindAsync = async () => { + let sourceBoard = await db.query.boards.findFirst({ where: eq(boards.name, "default"), with: { items: true } }); + sourceBoard ??= await db.query.boards.findFirst({ where: eq(boards.name, "dashboard"), with: { items: true } }); + if (!sourceBoard) throw new Error("Production seed did not create a widget gallery board"); + const result = new Map(); + for (const kind of widgetKinds) { + const source = sourceBoard.items.find((item) => item.kind === kind); + result.set(kind, source?.options ?? emptySuperJSON); + } + result.set("app", stringify({ appId: qaAppIds[0], openInNewTab: true, showTitle: true, pingEnabled: false })); + result.set("bookmarks", stringify({ title: "QA bookmarks", layout: "grid", items: qaAppIds, openNewTab: true })); + result.set( + "customApi", + stringify({ definitionId: customDefinitionId, configuration: {}, configurationVersion: 1, refreshInterval: 1 }), + ); + return result; +}; + +const boardLayouts = (board: BoardDefinition) => board.layouts ?? defaultLayouts; + +const emptySectionOffset = (sectionHandle: string) => { + if (sectionHandle === "left") return -1; + if (sectionHandle === "right") return 1; + return 0; +}; + +const mainLaneColumnCount = (layout: LayoutDefinition) => getBoardLaneColumnCount(layout, "main"); + +const sectionColumnCount = (layout: LayoutDefinition, sectionHandle: string) => { + if (["root", "left", "right"].includes(sectionHandle)) { + return getBoardLaneColumnCount(layout, getRootSectionLane(emptySectionOffset(sectionHandle))); + } + const mainColumns = mainLaneColumnCount(layout); + if (sectionHandle === "overflow") return Math.min(mainColumns, 6); + if (sectionHandle === "level-1") return mainColumns; + if (sectionHandle === "level-2") return Math.max(1, mainColumns - 1); + if (sectionHandle === "level-3") return Math.max(1, mainColumns - 2); + if (sectionHandle.startsWith("sibling-")) return Math.min(mainColumns, 2); + throw new Error(`Unknown QA section handle: ${sectionHandle}`); +}; + +const resolveItemSectionHandle = (layout: LayoutDefinition, requestedHandle: string) => { + if (!["left", "right"].includes(requestedHandle)) return requestedHandle; + if (sectionColumnCount(layout, requestedHandle) > 0) return requestedHandle; + return "root"; +}; + +const packPositions = (itemDefinitions: ItemDefinition[], board: BoardDefinition): Position[] => { + return boardLayouts(board).flatMap((layout) => { + const cursors = new Map(); + return itemDefinitions.map((item) => { + const sectionHandle = resolveItemSectionHandle(layout, item.sectionHandle ?? "root"); + const columnCount = sectionColumnCount(layout, sectionHandle); + const width = Math.min(item.width, columnCount); + const cursor = cursors.get(sectionHandle) ?? { xOffset: 0, yOffset: 0, rowHeight: 0 }; + if (cursor.xOffset + width > columnCount) { + cursor.xOffset = 0; + cursor.yOffset += cursor.rowHeight; + cursor.rowHeight = 0; + } + const position = { + boardHandle: item.boardHandle, + itemHandle: item.handle, + layoutHandle: layout.handle, + sectionHandle, + xOffset: cursor.xOffset, + yOffset: cursor.yOffset, + width, + height: item.height, + }; + cursor.xOffset += width; + cursor.rowHeight = Math.max(cursor.rowHeight, item.height); + cursors.set(sectionHandle, cursor); + return position; + }); + }); +}; + +const itemForKind = ( + boardHandle: string, + handle: string, + kind: WidgetKind, + optionsByKind: Map, +): ItemDefinition => { + const defaultConfig = getDefaultWidgetConfig(kind); + let options = optionsByKind.get(kind) ?? emptySuperJSON; + if (options === emptySuperJSON && defaultConfig.options) options = stringify(defaultConfig.options); + return { + boardHandle, + handle, + kind, + options, + width: defaultConfig.width, + height: defaultConfig.height, + }; +}; + +const buildItems = (optionsByKind: Map) => { + const result = new Map(); + for (const board of homeBoards) { + result.set( + board.handle, + (["clock", "weather", "bookmarks"] as WidgetKind[]).map((kind, index) => + itemForKind(board.handle, `${kind}-${index + 1}`, kind, optionsByKind), + ), + ); + } + result.set( + "grid-24", + Array.from({ length: 24 }, (_, index) => ({ + ...itemForKind("grid-24", `cell-${String(index + 1).padStart(2, "0")}`, "clock", optionsByKind), + width: index + 1, + height: (index % 6) + 1, + })), + ); + result.set("scroll", [ + ...Array.from({ length: 30 }, (_, index) => ({ + ...itemForKind( + "scroll", + `row-${String(index + 1).padStart(2, "0")}`, + index % 2 === 0 ? "notebook" : "weather", + optionsByKind, + ), + sectionHandle: ["left", "root", "right"][index % 3], + })), + ...Array.from({ length: 12 }, (_, index) => ({ + ...itemForKind("scroll", `overflow-${index + 1}`, index % 2 === 0 ? "clock" : "app", optionsByKind), + sectionHandle: "overflow", + })), + ]); + result.set( + "dense-collisions", + Array.from({ length: 12 }, (_, index) => + itemForKind("dense-collisions", `collision-${index + 1}`, "clock", optionsByKind), + ), + ); + result.set("nested-containers", [ + ...(["clock", "weather", "bookmarks"] as WidgetKind[]).map((kind, index) => ({ + ...itemForKind("nested-containers", `nested-${index + 1}`, kind, optionsByKind), + sectionHandle: "level-3", + })), + ...(["notebook", "app", "countdown"] as WidgetKind[]).map((kind, index) => ({ + ...itemForKind("nested-containers", `sibling-${index + 1}`, kind, optionsByKind), + sectionHandle: `sibling-${index + 1}`, + })), + itemForKind("nested-containers", "root-zone", "clock", optionsByKind), + ]); + result.set( + "layout-boundaries", + widgetKinds + .slice(0, 16) + .map((kind, index) => itemForKind("layout-boundaries", `boundary-${index + 1}`, kind, optionsByKind)), + ); + result.set("icons-bookmarks", [ + { + ...itemForKind("icons-bookmarks", "bookmarks-grid", "bookmarks", optionsByKind), + options: stringify({ title: "QA grid", layout: "grid", items: qaAppIds, openNewTab: true }), + }, + { + ...itemForKind("icons-bookmarks", "bookmarks-list", "bookmarks", optionsByKind), + options: stringify({ title: "QA list", layout: "column", items: qaAppIds, openNewTab: false }), + }, + { + ...itemForKind("icons-bookmarks", "bookmarks-empty-title", "bookmarks", optionsByKind), + options: stringify({ title: "", layout: "icons", items: qaAppIds.slice(0, 4), openNewTab: true }), + }, + { + ...itemForKind("icons-bookmarks", "bookmarks-custom-title", "bookmarks", optionsByKind), + options: stringify({ + title: "External and relative", + layout: "row", + items: qaAppIds.slice(2, 6), + openNewTab: false, + }), + }, + ...qaAppIds.slice(0, 6).map((id, index) => ({ + ...itemForKind("icons-bookmarks", `app-${index + 1}`, "app", optionsByKind), + options: stringify({ + appId: id, + openInNewTab: index % 2 === 0, + showTitle: index >= 3, + pingEnabled: index !== 1, + }), + })), + ]); + widgetGalleryGroups.forEach((group, groupIndex) => { + const boardHandle = `widgets-${String(groupIndex + 1).padStart(2, "0")}`; + result.set( + boardHandle, + group.map((kind, kindIndex) => + itemForKind(boardHandle, `${String(kindIndex + 1).padStart(2, "0")}-${kind}`, kind, optionsByKind), + ), + ); + }); + result.set("custom-widget-assistant", [ + { + ...itemForKind("custom-widget-assistant", "custom-compact", "customApi", optionsByKind), + width: 3, + height: 2, + }, + { + ...itemForKind("custom-widget-assistant", "custom-standard-disabled", "customApi", optionsByKind), + options: stringify({ + definitionId: disabledCustomDefinitionId, + configuration: {}, + configurationVersion: 1, + refreshInterval: 1, + }), + width: 6, + height: 4, + }, + { + ...itemForKind("custom-widget-assistant", "custom-wide-missing-secret", "customApi", optionsByKind), + options: stringify({ + definitionId: missingSecretCustomDefinitionId, + configuration: {}, + configurationVersion: 1, + refreshInterval: 1, + }), + width: 12, + height: 4, + }, + { + ...itemForKind("custom-widget-assistant", "custom-invalid-option", "customApi", optionsByKind), + options: stringify({ + definitionId: customDefinitionId, + configuration: { unsupportedOption: "qa-invalid" }, + configurationVersion: 1, + refreshInterval: 1, + }), + width: 6, + height: 3, + }, + { ...itemForKind("custom-widget-assistant", "assistant-compact", "assistant", optionsByKind), width: 3, height: 2 }, + { + ...itemForKind("custom-widget-assistant", "assistant-standard", "assistant", optionsByKind), + width: 6, + height: 4, + }, + { ...itemForKind("custom-widget-assistant", "assistant-wide", "assistant", optionsByKind), width: 12, height: 4 }, + ]); + result.set("permissions-public", [ + itemForKind("permissions-public", "clock", "clock", optionsByKind), + itemForKind("permissions-public", "bookmarks", "bookmarks", optionsByKind), + ]); + result.set("download-upload", [ + { ...itemForKind("download-upload", "downloads", "downloads", optionsByKind), width: 6, height: 3 }, + { + ...itemForKind("download-upload", "download-link", "app", optionsByKind), + options: stringify({ appId: qaAppIds[0], openInNewTab: true, showTitle: true, pingEnabled: false }), + }, + ]); + return result; +}; + +const assertSeededGeometryAsync = async () => { + const geometryLayouts = await db + .select({ + id: layouts.id, + boardId: layouts.boardId, + columnCount: layouts.columnCount, + leftGutterColumnCount: layouts.leftGutterColumnCount, + rightGutterColumnCount: layouts.rightGutterColumnCount, + role: layouts.role, + }) + .from(layouts) + .where(inArray(layouts.id, qaLayoutIds)); + const geometrySections = await db + .select({ + id: sections.id, + boardId: sections.boardId, + kind: sections.kind, + xOffset: sections.xOffset, + }) + .from(sections) + .where(inArray(sections.boardId, qaBoardIds)); + const geometrySectionLayouts = await db + .select({ + sectionId: sectionLayouts.sectionId, + layoutId: sectionLayouts.layoutId, + parentSectionId: sectionLayouts.parentSectionId, + xOffset: sectionLayouts.xOffset, + yOffset: sectionLayouts.yOffset, + width: sectionLayouts.width, + height: sectionLayouts.height, + }) + .from(sectionLayouts) + .where(inArray(sectionLayouts.layoutId, qaLayoutIds)); + const geometryItems = await db + .select({ id: items.id, boardId: items.boardId }) + .from(items) + .where(inArray(items.boardId, qaBoardIds)); + const itemBoardById = new Map(geometryItems.map((item) => [item.id, item.boardId])); + const geometryItemLayouts = await db + .select({ + itemId: itemLayouts.itemId, + sectionId: itemLayouts.sectionId, + layoutId: itemLayouts.layoutId, + xOffset: itemLayouts.xOffset, + yOffset: itemLayouts.yOffset, + width: itemLayouts.width, + height: itemLayouts.height, + }) + .from(itemLayouts) + .where(inArray(itemLayouts.layoutId, qaLayoutIds)); + const geometryErrors = findFixtureGeometryErrors({ + layouts: geometryLayouts, + sections: geometrySections, + sectionLayouts: geometrySectionLayouts, + itemLayouts: geometryItemLayouts.map((itemLayout) => ({ + ...itemLayout, + boardId: itemBoardById.get(itemLayout.itemId) ?? "", + })), + }); + if (geometryErrors.length > 0) { + throw new Error(`Generated QA fixture geometry is invalid: ${geometryErrors.join("; ")}`); + } +}; + +const seedBoardsAsync = async (optionsByKind: Map) => { + for (const board of qaBoards) { + const name = boardName(board.handle); + await upsertById( + db.query.boards as never, + boards as never, + { + id: boardId(board.handle), + name, + isPublic: board.isPublic ?? false, + creatorId: userId(board.ownerHandle), + pageTitle: board.label, + metaTitle: board.label, + logoImageUrl: null, + faviconImageUrl: null, + backgroundImageUrl: null, + backgroundImageAttachment: "fixed", + backgroundImageRepeat: "no-repeat", + backgroundImageSize: "cover", + primaryColor: "#748ffc", + secondaryColor: "#22b8cf", + opacity: 95, + customCss: null, + iconColor: null, + itemRadius: "md", + disableStatus: false, + } as typeof boards.$inferSelect, + ); + } + + await db.delete(items).where(inArray(items.boardId, qaBoardIds)); + await db.delete(sections).where(inArray(sections.boardId, qaBoardIds)); + await db.delete(layouts).where(inArray(layouts.boardId, qaBoardIds)); + + const allItems = buildItems(optionsByKind); + const positionRows: Position[] = []; + for (const board of qaBoards) { + await db.insert(layouts).values( + boardLayouts(board).map((layout) => ({ + id: layoutId(board.handle, layout.handle), + name: layout.name, + boardId: boardId(board.handle), + columnCount: layout.columnCount, + leftGutterColumnCount: layout.leftGutterColumnCount ?? 0, + rightGutterColumnCount: layout.rightGutterColumnCount ?? 0, + breakpoint: layout.breakpoint, + role: layout.role, + })), + ); + await db.insert(sections).values({ + id: sectionId(board.handle, "root"), + boardId: boardId(board.handle), + kind: "empty", + xOffset: 0, + yOffset: 0, + name: null, + options: emptySuperJSON, + }); + if (board.handle === "scroll") { + await db.insert(sections).values([ + { + id: sectionId(board.handle, "left"), + boardId: boardId(board.handle), + kind: "empty", + xOffset: -1, + yOffset: 0, + name: "Left rail", + options: emptySuperJSON, + }, + { + id: sectionId(board.handle, "right"), + boardId: boardId(board.handle), + kind: "empty", + xOffset: 1, + yOffset: 0, + name: "Right rail", + options: emptySuperJSON, + }, + { + id: sectionId(board.handle, "overflow"), + boardId: boardId(board.handle), + kind: "container", + xOffset: null, + yOffset: null, + name: "Scrollable container", + options: stringify({ showLabel: true, collapsible: true, scrollable: true, showOpenAll: true }), + }, + ]); + await db.insert(sectionLayouts).values( + boardLayouts(board).map((layout) => ({ + sectionId: sectionId(board.handle, "overflow"), + layoutId: layoutId(board.handle, layout.handle), + parentSectionId: sectionId(board.handle, "root"), + xOffset: 0, + yOffset: 0, + width: sectionColumnCount(layout, "overflow"), + height: 4, + })), + ); + } + if (board.handle === "nested-containers") { + const containerHandles = ["level-1", "level-2", "level-3", "sibling-1", "sibling-2", "sibling-3"]; + await db.insert(sections).values( + containerHandles.map((handle, index) => ({ + id: sectionId(board.handle, handle), + boardId: boardId(board.handle), + kind: "container" as const, + xOffset: null, + yOffset: null, + name: index < 3 ? `Nested level ${index + 1}` : `Sibling zone ${index - 2}`, + options: stringify({ + showLabel: index !== 4, + collapsible: index !== 5, + scrollable: index === 2 || index === 4, + showOpenAll: index === 3, + }), + })), + ); + await db.insert(sectionLayouts).values( + boardLayouts(board).flatMap((layout) => + containerHandles.map((handle, index) => { + const width = sectionColumnCount(layout, handle); + const parentHandle = index === 0 ? "root" : index < 3 ? `level-${index}` : "root"; + const parentColumns = sectionColumnCount(layout, parentHandle); + return { + sectionId: sectionId(board.handle, handle), + layoutId: layoutId(board.handle, layout.handle), + parentSectionId: sectionId(board.handle, parentHandle), + xOffset: index < 3 ? 0 : Math.max(0, Math.min((index - 3) * 2, parentColumns - width)), + yOffset: index < 3 ? 0 : 7, + width, + height: index < 3 ? 6 - index : 3, + }; + }), + ), + ); + } + const boardItems = allItems.get(board.handle) ?? []; + if (boardItems.length > 0) { + await db.insert(items).values( + boardItems.map((item) => ({ + id: itemId(board.handle, item.handle), + boardId: boardId(board.handle), + kind: item.kind, + options: item.options, + advancedOptions: item.advancedOptions ?? emptySuperJSON, + })), + ); + } + if (board.handle === "dense-collisions") { + positionRows.push( + ...boardLayouts(board).flatMap((layout) => + boardItems.map((item, index) => { + const columnCount = mainLaneColumnCount(layout); + const width = Math.min(columnCount, 2 + (index % 3)); + return { + boardHandle: board.handle, + itemHandle: item.handle, + layoutHandle: layout.handle, + sectionHandle: "root", + xOffset: Math.min(index % 3, columnCount - width), + yOffset: index % 2, + width, + height: 2 + (index % 2), + }; + }), + ), + ); + } else { + positionRows.push(...packPositions(boardItems, board)); + } + } + + if (positionRows.length > 0) { + await db.insert(itemLayouts).values( + positionRows.map((position) => { + return { + itemId: itemId(position.boardHandle, position.itemHandle), + sectionId: sectionId(position.boardHandle, position.sectionHandle), + layoutId: layoutId(position.boardHandle, position.layoutHandle), + xOffset: position.xOffset, + yOffset: position.yOffset, + width: position.width, + height: position.height, + }; + }), + ); + } + await assertSeededGeometryAsync(); + + const mockKinds = new Set(getWidgetKindsForIntegration("mock")); + const mockItemIds = [...allItems.entries()].flatMap(([boardHandle, definitions]) => + definitions.filter((item) => mockKinds.has(item.kind)).map((item) => itemId(boardHandle, item.handle)), + ); + if (mockItemIds.length > 0) { + await db + .insert(integrationItems) + .values(mockItemIds.map((currentItemId) => ({ itemId: currentItemId, integrationId: mockIntegrationId }))); + } + + await db.delete(boardUserPermissions).where(inArray(boardUserPermissions.boardId, qaBoardIds)); + await db.delete(boardGroupPermissions).where(inArray(boardGroupPermissions.boardId, qaBoardIds)); + const permissionRows: Array<{ boardId: string; userId: string; permission: BoardPermission }> = []; + for (const board of sharedBoards) { + permissionRows.push( + { boardId: boardId(board.handle), userId: userId("rowan-owner"), permission: "full" }, + { boardId: boardId(board.handle), userId: userId("eden-editor"), permission: "modify" }, + { boardId: boardId(board.handle), userId: userId("vivian-viewer"), permission: "view" }, + ); + for (const persona of personas) { + if (["avery-admin", "rowan-owner", "eden-editor", "vivian-viewer", "nolan-outsider"].includes(persona.handle)) { + continue; + } + permissionRows.push({ boardId: boardId(board.handle), userId: userId(persona.handle), permission: "view" }); + } + } + await db.insert(boardUserPermissions).values(permissionRows); + + for (const persona of personas) { + await db + .update(users) + .set({ homeBoardId: boardId(`home-${persona.handle}`), mobileHomeBoardId: boardId(`home-${persona.handle}`) }) + .where(eq(users.id, userId(persona.handle))); + } +}; + +const createManifest = ( + profile: Profile, + candidateSha: string, + fixtureUrl: string, + seeded: boolean, + profileFlags: ReleaseV2QaProfileFlags, +) => { + const allItems = seeded ? buildItems(new Map(widgetKinds.map((kind) => [kind, emptySuperJSON]))) : new Map(); + const boardEntries = seeded + ? qaBoards.map((board) => ({ + id: boardId(board.handle), + handle: board.handle, + name: boardName(board.handle), + label: board.label, + owner: board.ownerHandle, + isPublic: board.isPublic ?? false, + layouts: boardLayouts(board).map((layout) => layoutId(board.handle, layout.handle)), + itemCount: allItems.get(board.handle)?.length ?? 0, + flags: { + home: board.handle.startsWith("home-"), + shared: !board.handle.startsWith("home-"), + collisionFixture: board.handle === "dense-collisions", + nestedContainers: board.handle === "nested-containers", + scrollableContainer: board.handle === "scroll", + leftRightRails: board.handle === "scroll", + }, + })) + : []; + const layoutEntries = seeded + ? qaBoards.flatMap((board) => + boardLayouts(board).map((layout) => ({ + id: layoutId(board.handle, layout.handle), + boardId: boardId(board.handle), + handle: layout.handle, + role: layout.role, + breakpoint: layout.breakpoint, + columnCount: layout.columnCount, + leftGutterColumnCount: layout.leftGutterColumnCount ?? 0, + rightGutterColumnCount: layout.rightGutterColumnCount ?? 0, + })), + ) + : []; + return { + schemaVersion: 1, + candidateSha, + profile, + personas: seeded + ? personas.map((persona) => ({ + id: userId(persona.handle), + handle: persona.handle, + name: persona.name, + loginUsername: persona.handle, + email: `qa-v2-${persona.handle}@example.test`, + homeBoardId: boardId(`home-${persona.handle}`), + permissions: persona.permissions, + })) + : [], + boards: boardEntries, + layouts: layoutEntries, + expectedBoardPermissions: { + "rowan-owner": "full", + "eden-editor": "modify", + "vivian-viewer": "view", + "nolan-outsider": "none", + }, + counts: { + personas: seeded ? personas.length : 0, + boards: boardEntries.length, + homeBoards: seeded ? homeBoards.length : 0, + sharedBoards: seeded ? sharedBoards.length : 0, + layouts: layoutEntries.length, + items: seeded ? [...allItems.values()].reduce((sum, rows) => sum + rows.length, 0) : 0, + widgetKinds: seeded ? widgetKinds.length : 0, + apps: seeded ? qaAppIds.length : 0, + icons: seeded ? qaIconIds.length : 0, + customWidgets: seeded ? customDefinitionIds.length : 0, + }, + customWidgetVariants: seeded + ? { + enabled: customDefinitionId, + disabled: disabledCustomDefinitionId, + missingSecret: missingSecretCustomDefinitionId, + invalidOptionItem: itemId("custom-widget-assistant", "custom-invalid-option"), + sizes: ["compact", "standard", "wide"], + } + : null, + limitations: ["No plaintext Custom Widget secret is seeded; the missing-secret state is intentional."], + flags: { + credentialsCreated: seeded, + readOnly: profile === "main-readonly", + onboardingFresh: profile === "onboarding-fresh", + degraded: profile === "degraded", + mockIntegration: seeded, + allWidgetKindsCovered: seeded, + demoMode: profileFlags.demoMode, + demoReadOnly: profileFlags.demoReadOnly, + unsafeMockIntegration: profileFlags.unsafeMockIntegration, + fixtureOrigin: new URL(fixtureUrl).origin, + }, + }; +}; + +const main = async () => { + const options = parseCli(); + validateQaBoardDefinitions(); + const galleryKinds = widgetGalleryGroups.flat(); + if ( + galleryKinds.length !== widgetKinds.length || + new Set(galleryKinds).size !== widgetKinds.length || + widgetKinds.some((kind) => !galleryKinds.includes(kind)) + ) { + throw new Error("Widget gallery groups must contain every canonical widget kind exactly once"); + } + const profile = options.profile ?? (process.env.QA_PROFILE as Profile | undefined); + if (!profile || !profiles.includes(profile)) throw new Error(`QA_PROFILE must be one of: ${profiles.join(", ")}`); + const profileFlags = assertReleaseV2QaProfileEnvironment(profile, process.env); + const checkoutSha = resolveCheckoutCandidateSha(repoRoot); + const suppliedCandidateSha = options.candidateSha ?? process.env.QA_CANDIDATE_SHA?.trim(); + const candidateSha = suppliedCandidateSha ? validateCandidateSha(suppliedCandidateSha, checkoutSha) : checkoutSha; + const fixtureUrl = validateFixtureUrl(requiredEnv("QA_FIXTURE_URL")); + const manifestPath = resolveManifestPath(options); + + const populated = profile !== "onboarding-fresh"; + await runInTransactionAsync(async () => { + if (options.reset) await resetQaRowsAsync(); + await setOnboardingStepAsync(profile); + if (populated) { + const password = await requiredQaPassword(); + const passwordHash = await resolvePasswordHashAsync(password); + await seedPersonasAsync(passwordHash); + await seedGlobalFixturesAsync(fixtureUrl); + const optionsByKind = await sourceOptionsByKindAsync(); + await seedBoardsAsync(optionsByKind); + return; + } + await resetQaRowsAsync(); + }); + + const manifest = createManifest(profile, candidateSha, fixtureUrl, populated, profileFlags); + await mkdir(dirname(manifestPath), { recursive: true }); + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); + console.log(`Release v2 QA seed complete for profile '${profile}' (${manifest.counts.boards} QA boards)`); + console.log(`Fixture manifest written to ${manifestPath}`); +}; + +const closeDatabaseAsync = async () => { + const client = rootDb.$client as unknown as { + close?: () => void; + end?: () => Promise | void; + }; + if (typeof client.close === "function") { + client.close(); + return; + } + if (typeof client.end === "function") await client.end(); +}; + +try { + await main(); +} catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown seed failure"; + console.error(`Release v2 QA seed failed: ${message}`); + process.exitCode = 1; +} finally { + await closeDatabaseAsync(); +} + +process.exit(process.exitCode ?? 0); diff --git a/scripts/release-v2-qa/verify.mts b/scripts/release-v2-qa/verify.mts new file mode 100644 index 000000000..96ef764da --- /dev/null +++ b/scripts/release-v2-qa/verify.mts @@ -0,0 +1,1115 @@ +import { existsSync } from "node:fs"; +import { readFile, readdir, realpath } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import type { LayoutRole } from "../../packages/definitions/index.ts"; +import { boardNameSchema } from "../../packages/validation/src/board.ts"; +import { findFixtureGeometryErrors } from "./geometry.mts"; +import { resolveCheckoutCandidateSha } from "./provenance.mts"; +import { collectHumanWidgetStatusErrors, normalizeReportMetadata } from "./report-integrity.mts"; +import { assertSafeReportPath, readSafeReportFile, validateResolvedArtifactPath } from "./report-path-integrity.mts"; + +import { assertSafeRunRoot, isPathWithin, validateFixtureUrl } from "./safety.mts"; + +type Status = "passed" | "failed" | "blocked" | "not-reached"; +type Severity = "P0" | "P1" | "P2" | "P3"; + +interface Finding { + severity: Severity; + title: string; + caseIds?: string[]; + evidence?: string[]; +} + +interface Packet { + id: string; + wave: string; + prRefs: number[]; + personas: string[]; + boards: string[]; + profiles: string[]; + viewports: string[]; + zooms: number[]; + inputs: string[]; + widgetKinds?: string[]; + cases: string[]; +} + +interface Manifest { + statuses: Status[]; + pullRequests: { number: number }[]; + viewports: { id: string }[]; + zooms: number[]; + inputs: string[]; + profiles: { id: string }[]; + personas: string[]; + boards: { id: string; handle: string }[]; + wavePacketCounts: Record; + caseDimensions: Record; + packets: Packet[]; +} + +interface ReportMetadata { + packetId: string; + status: Status; + caseStatuses: Record; + execution: { + candidateSha: string | null; + url: string | null; + actualPort: number | null; + runtimeProfile: string | null; + runtimeFlags: string[]; + persona: string | null; + sessionId: string | null; + timestamp: string | null; + viewport: string | null; + input: string | null; + zoom: number | null; + }; + findings: Finding[]; + artifacts: string[]; + widgetChecks: { + widgetKind: string; + viewport: string; + sizeRequirement: string; + status: Status; + }[]; + performance: { + measurements: { + name: string; + value: number | null; + unit: string; + threshold: string; + status: Status; + evidence: string[]; + }[]; + limitations: string[]; + }; + independentReproductions: { + findingFingerprint: string; + agentId: string; + outcome: "reproduced" | "not-reproduced" | "blocked" | "not-reached"; + evidence: string[]; + notes: string; + }[]; + notes: string; +} + +interface Ledger { + packetStatuses: Record }>; +} + +interface RuntimeManifest { + schemaVersion: number; + runId: string; + status: "running" | "stopped"; + candidateSha: string; + slot: number; + profile: string; + repoRoot: string; + runRoot: string; + slotDir: string; + dbPath: string; + fixtureManifestPath: string; + nextDistDir: string; + url: string; + fixtureUrl: string; + ports: { app: number; fixture: number; redis: number }; + flags: { demoMode: boolean; demoReadOnly: boolean; unsafeMockIntegration: boolean }; + watcher: { watchpackPollingIntervalMs: number }; + processes: { + app: { pid: number; logPath: string }; + fixture: { pid: number; logPath: string }; + }; +} + +interface FixtureManifest { + schemaVersion: number; + candidateSha: string; + profile: string; + personas: { id: string; handle: string; name: string; loginUsername: string }[]; + boards: { id: string; handle: string; name: string; label: string; layouts: string[]; flags: { shared: boolean } }[]; + layouts: { id: string; boardId: string; columnCount: number }[]; + expectedBoardPermissions: Record; + counts: Record; + flags: { + demoMode: boolean; + demoReadOnly: boolean; + unsafeMockIntegration: boolean; + allWidgetKindsCovered: boolean; + fixtureOrigin: string; + }; +} + +interface VerifyOptions { + runRoot?: string; + manifestPaths: string[]; +} + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const qaRoot = join(repoRoot, "qa/release-v2"); +const reportsRoot = join(qaRoot, "reports"); +const campaignCandidateSha = resolveCheckoutCandidateSha(repoRoot); +const exhaustiveSizeWidgetKinds = new Set([ + "downloads", + "calendar", + "mediaServer", + "mediaRequests-requestList", + "mediaRequests-requestStats", + "systemResources", + "systemDisks", + "dockerContainers", + "networkControllerSummary", + "networkControllerStatus", + "beszelSystemStats", + "uptimeKuma", + "bookmarks", + "notebook", + "iframe", + "customApi", + "assistant", +]); +const expectedWidgetGroups: Record = { + "widgets-01": ["clock", "weather", "airQuality", "countdown", "timer"], + "widgets-02": ["app", "iframe", "video", "minecraftServerStatus", "stockPrice"], + "widgets-03": ["notebook", "anchorNote", "bookmarks", "rssFeed", "timetable"], + "widgets-04": ["downloads", "dockerContainers", "indexerManager", "dnsHoleSummary", "dnsHoleControls"], + "widgets-05": [ + "smartHome-entityState", + "smartHome-executeAutomation", + "healthMonitoring", + "systemResources", + "systemDisks", + ], + "widgets-06": ["firewall", "notifications", "networkControllerSummary", "networkControllerStatus", "uptimeKuma"], + "widgets-07": ["beszelSystemTable", "beszelSystemGrid", "beszelAlerts", "beszelSystemStats", "wud"], + "widgets-08": ["ups", "vpn", "speedtestTracker", "traefik", "umami"], + "widgets-09": ["calendar", "mediaServer", "mediaRequests-requestList", "mediaRequests-requestStats", "mediaMissing"], + "widgets-10": ["mediaReleases", "mediaTranscoding", "immich-serverStats", "immich-albumCarousel", "audioStats"], + "widgets-11": ["paperlessNgx", "patchmon", "bazarr", "tracearr", "releases"], + "widgets-12": ["coolify", "archiveTeamWarrior", "customApi", "assistant"], +}; +const expectedProfileFlags: Record = { + "main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + "main-readonly": ["DEMO_MODE=true", "DEMO_READ_ONLY=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + "onboarding-fresh": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], + degraded: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"], +}; +const errors: string[] = []; + +const parseOptions = (): VerifyOptions => { + const args = process.argv.slice(2).filter((argument) => argument !== "--"); + const options: VerifyOptions = { manifestPaths: [] }; + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + const value = args[index + 1]; + if (argument === "--run-root" && value) { + options.runRoot = resolve(value); + index += 1; + continue; + } + if ((argument === "--manifest" || argument === "--runtime-manifest") && value) { + options.manifestPaths.push(resolve(value)); + index += 1; + continue; + } + throw new Error(`Unknown or incomplete argument: ${argument ?? ""}`); + } + return options; +}; + +const readJson = async (path: string): Promise => JSON.parse(await readFile(path, "utf8")) as T; + +const sameSet = (actual: T[], expected: T[]): boolean => + actual.length === expected.length && actual.every((value) => expected.includes(value)); + +const requiredSizeInstruction = (widgetKind: string) => { + if (exhaustiveSizeWidgetKinds.has(widgetKind)) return "every width 1-24 × every height 1-6"; + return "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds"; +}; + +const duplicateValues = (values: T[]): T[] => values.filter((value, index) => values.indexOf(value) !== index); + +const expandedCases = (manifest: Manifest, packet: Packet): { id: string; expected: string }[] => { + const dimensions = manifest.caseDimensions[packet.wave] ?? []; + return packet.cases.flatMap((baseId) => + dimensions.map((dimension) => ({ id: `${baseId}-${dimension.id}`, expected: dimension.expected })), + ); +}; + +const expectedRollup = (statuses: Status[]): Status => { + if (statuses.includes("failed")) return "failed"; + if (statuses.includes("blocked")) return "blocked"; + if (statuses.includes("not-reached")) return "not-reached"; + return "passed"; +}; + +const parseReportMetadata = (content: string, packetId: string): unknown | undefined => { + const match = content.match(//); + if (!match?.[1]) { + errors.push(`${packetId}: report metadata block is missing`); + return undefined; + } + + try { + return JSON.parse(match[1]) as unknown; + } catch (error) { + errors.push(`${packetId}: report metadata is invalid JSON (${String(error)})`); + return undefined; + } +}; + +const assertReferences = (packet: Packet, field: keyof Packet, values: T[], allowed: T[]): void => { + for (const value of values) { + if (!allowed.includes(value)) errors.push(`${packet.id}: unknown ${String(field)} value ${String(value)}`); + } +}; + +const booleanProfileFlags = (profile: string) => ({ + demoMode: profile !== "onboarding-fresh", + demoReadOnly: profile === "main-readonly", + unsafeMockIntegration: true, +}); + +const validateFixtureEndpoint = async (fixtureUrl: string, label: string) => { + try { + const response = await fetch(`${fixtureUrl}/health`, { signal: AbortSignal.timeout(5_000) }); + if (!response.ok) { + errors.push(`${label}: fixture health returned HTTP ${response.status}`); + return; + } + const body = (await response.json()) as { status?: string; fixture?: string }; + if (body.status !== "ok" || body.fixture !== "release-v2-qa") { + errors.push(`${label}: fixture health payload is not the release-v2 QA fixture`); + } + } catch { + errors.push(`${label}: fixture health endpoint is unreachable`); + } +}; + +const validateFixtureManifest = async ( + fixture: FixtureManifest, + fixturePath: string, + sourceWidgetKinds: string[], + checkEndpoint: boolean, + expectedSharedBoardNames: Map, +) => { + const label = `fixture manifest ${fixturePath}`; + if (fixture.schemaVersion !== 1) errors.push(`${label}: unsupported schema version`); + if (fixture.candidateSha !== campaignCandidateSha) errors.push(`${label}: candidate does not match the campaign`); + if (!expectedProfileFlags[fixture.profile]) errors.push(`${label}: unknown profile ${fixture.profile}`); + const expectedFlags = booleanProfileFlags(fixture.profile); + for (const [flag, expected] of Object.entries(expectedFlags)) { + if (fixture.flags?.[flag as keyof typeof expectedFlags] !== expected) { + errors.push(`${label}: ${flag} does not match profile ${fixture.profile}`); + } + } + + if (!Array.isArray(fixture.personas) || fixture.counts?.personas !== fixture.personas.length) { + errors.push(`${label}: persona count does not match the persona manifest`); + } + if (!Array.isArray(fixture.boards) || fixture.counts?.boards !== fixture.boards.length) { + errors.push(`${label}: board count does not match the board manifest`); + } + if (!Array.isArray(fixture.layouts) || fixture.counts?.layouts !== fixture.layouts.length) { + errors.push(`${label}: layout count does not match the layout manifest`); + } + if (duplicateValues((fixture.personas ?? []).map((persona) => persona.id)).length > 0) { + errors.push(`${label}: duplicate persona IDs`); + } + if (duplicateValues((fixture.personas ?? []).map((persona) => persona.loginUsername)).length > 0) { + errors.push(`${label}: duplicate persona login usernames`); + } + if (duplicateValues((fixture.boards ?? []).map((board) => board.id)).length > 0) { + errors.push(`${label}: duplicate board IDs`); + } + if (duplicateValues((fixture.boards ?? []).map((board) => board.name)).length > 0) { + errors.push(`${label}: duplicate board names`); + } + if (duplicateValues((fixture.layouts ?? []).map((layout) => layout.id)).length > 0) { + errors.push(`${label}: duplicate layout IDs`); + } + + const boardIds = new Set((fixture.boards ?? []).map((board) => board.id)); + const layoutIds = new Set((fixture.layouts ?? []).map((layout) => layout.id)); + for (const layout of fixture.layouts ?? []) { + if (!boardIds.has(layout.boardId)) errors.push(`${label}: layout ${layout.id} references an unknown board`); + if (!Number.isInteger(layout.columnCount) || layout.columnCount < 1 || layout.columnCount > 24) { + errors.push(`${label}: layout ${layout.id} has an invalid column count`); + } + } + for (const board of fixture.boards ?? []) { + if (!boardNameSchema.safeParse(board.name).success) { + errors.push(`${label}: board ${board.id} has an invalid board name ${String(board.name)}`); + } + if (!board.label?.trim()) errors.push(`${label}: board ${board.id} is missing its human-readable label`); + if (board.flags.shared && board.name !== expectedSharedBoardNames.get(board.handle)) { + errors.push(`${label}: shared board ${board.handle} name differs from the coverage manifest`); + } + if (board.layouts.some((layoutId) => !layoutIds.has(layoutId))) { + errors.push(`${label}: board ${board.id} references an unknown layout`); + } + } + + const populated = fixture.profile !== "onboarding-fresh"; + if (populated) { + if (fixture.personas.length !== 14) errors.push(`${label}: expected 14 seeded personas`); + for (const persona of fixture.personas) { + if (!persona.name) errors.push(`${label}: persona ${persona.id} is missing its human-readable name`); + if (persona.loginUsername !== persona.handle || persona.loginUsername !== persona.loginUsername.toLowerCase()) { + errors.push(`${label}: persona ${persona.id} has an invalid login username`); + } + } + if (fixture.boards.length !== 35) errors.push(`${label}: expected 35 seeded boards`); + if (fixture.counts.widgetKinds !== sourceWidgetKinds.length || !fixture.flags.allWidgetKindsCovered) { + errors.push(`${label}: widget-kind coverage does not match the canonical source`); + } + if (fixture.counts.apps !== 8 || fixture.counts.icons !== 8 || fixture.counts.customWidgets !== 3) { + errors.push(`${label}: fixture object counts are incomplete`); + } + const expectedPermissions = { + "rowan-owner": "full", + "eden-editor": "modify", + "vivian-viewer": "view", + "nolan-outsider": "none", + }; + if (JSON.stringify(fixture.expectedBoardPermissions) !== JSON.stringify(expectedPermissions)) { + errors.push(`${label}: expected board permissions are incorrect`); + } + } else { + const emptyFixtureCounts = [ + "personas", + "boards", + "homeBoards", + "sharedBoards", + "layouts", + "items", + "widgetKinds", + "apps", + "icons", + "customWidgets", + ]; + const hasPopulatedFixtureCount = emptyFixtureCounts.some((countName) => fixture.counts[countName] !== 0); + if ( + fixture.personas.length !== 0 || + fixture.boards.length !== 0 || + fixture.layouts.length !== 0 || + hasPopulatedFixtureCount || + fixture.flags.allWidgetKindsCovered + ) { + errors.push(`${label}: onboarding-fresh must not contain populated QA fixtures`); + } + } + + let fixtureOrigin: URL | undefined; + try { + fixtureOrigin = new URL(validateFixtureUrl(fixture.flags.fixtureOrigin)); + } catch { + errors.push(`${label}: fixture origin must be credential-free loopback HTTP`); + } + if (checkEndpoint && fixtureOrigin) await validateFixtureEndpoint(fixtureOrigin.origin, label); +}; + +const validateSqliteState = async (runtime: RuntimeManifest, fixture: FixtureManifest, sourceWidgetKinds: string[]) => { + const label = `runtime slot ${runtime.slot}`; + if (!existsSync(runtime.dbPath)) { + errors.push(`${label}: SQLite database is missing`); + return; + } + const sqliteModule = await import("better-sqlite3"); + type DatabaseLike = { + prepare: (sql: string) => { + get: (...parameters: unknown[]) => unknown; + all: (...parameters: unknown[]) => unknown[]; + }; + close: () => void; + }; + const Database = sqliteModule.default as unknown as new ( + filename: string, + options: { readonly: boolean; fileMustExist: boolean }, + ) => DatabaseLike; + const database = new Database(runtime.dbPath, { readonly: true, fileMustExist: true }); + const count = (sql: string, ...parameters: unknown[]) => + (database.prepare(sql).get(...parameters) as { count: number }).count; + try { + const qaUserCount = count(`select count(*) as count from "user" where id like 'qa-v2-user-%'`); + const qaBoardCount = count(`select count(*) as count from board where id like 'qa-v2-board-%'`); + const qaLayoutCount = count(`select count(*) as count from layout where board_id like 'qa-v2-board-%'`); + const qaItemCount = count(`select count(*) as count from item where board_id like 'qa-v2-board-%'`); + if (qaUserCount !== fixture.counts.personas) errors.push(`${label}: SQLite persona count differs from manifest`); + if (qaBoardCount !== fixture.counts.boards) errors.push(`${label}: SQLite board count differs from manifest`); + if (qaLayoutCount !== fixture.counts.layouts) errors.push(`${label}: SQLite layout count differs from manifest`); + if (qaItemCount !== fixture.counts.items) errors.push(`${label}: SQLite item count differs from manifest`); + + const populated = fixture.profile !== "onboarding-fresh"; + if (!populated) { + const qaFixtureCounts = [ + count(`select count(*) as count from "group" where id like 'qa-v2-%'`), + count(`select count(*) as count from app where id like 'qa-v2-%'`), + count(`select count(*) as count from icon where id like 'qa-v2-%'`), + count(`select count(*) as count from integration where id like 'qa-v2-%'`), + count(`select count(*) as count from custom_widget_v2_definition where id like 'qa-v2-%'`), + ]; + if (qaFixtureCounts.some((fixtureCount) => fixtureCount !== 0)) { + errors.push(`${label}: onboarding-fresh contains QA-scoped fixture records`); + } + if (count(`select count(*) as count from "user"`) !== 0) { + errors.push(`${label}: onboarding-fresh must not contain users`); + } + const onboardingRows = database.prepare(`select step, previous_step as previousStep from onboarding`).all() as { + step: string; + previousStep: string | null; + }[]; + if ( + onboardingRows.length !== 1 || + onboardingRows[0]?.step !== "start" || + onboardingRows[0]?.previousStep !== null + ) { + errors.push(`${label}: onboarding-fresh is not at the initial onboarding step`); + } + } + + const qaUsers = database.prepare(`select id, name from "user" where id like 'qa-v2-user-%'`).all() as { + id: string; + name: string; + }[]; + const loginUsernameById = new Map(fixture.personas.map((persona) => [persona.id, persona.loginUsername])); + for (const user of qaUsers) { + if (user.name !== loginUsernameById.get(user.id)) { + errors.push(`${label}: SQLite persona ${user.id} login username differs from manifest`); + } + } + + const qaBoards = database.prepare(`select id, name from board where id like 'qa-v2-board-%'`).all() as { + id: string; + name: string; + }[]; + const boardNameById = new Map(fixture.boards.map((board) => [board.id, board.name])); + if (duplicateValues(qaBoards.map((board) => board.name)).length > 0) { + errors.push(`${label}: SQLite contains duplicate QA board names`); + } + for (const board of qaBoards) { + if (!boardNameSchema.safeParse(board.name).success) { + errors.push(`${label}: SQLite board ${board.id} has an invalid board name ${board.name}`); + } + if (board.name !== boardNameById.get(board.id)) { + errors.push(`${label}: SQLite board ${board.id} name differs from manifest`); + } + } + + const missingPlacements = count( + `select count(*) as count + from item i + join layout l on l.board_id = i.board_id + left join item_layout il on il.item_id = i.id and il.layout_id = l.id + where i.board_id like 'qa-v2-board-%' and il.item_id is null`, + ); + if (missingPlacements !== 0) errors.push(`${label}: ${missingPlacements} item/layout placements are missing`); + const geometryLayouts = database + .prepare( + `select id, board_id as boardId, column_count as columnCount, + left_gutter_column_count as leftGutterColumnCount, + right_gutter_column_count as rightGutterColumnCount, role + from layout where board_id like 'qa-v2-board-%'`, + ) + .all() as { + id: string; + boardId: string; + columnCount: number; + leftGutterColumnCount: number; + rightGutterColumnCount: number; + role: LayoutRole; + }[]; + const geometrySections = database + .prepare( + `select id, board_id as boardId, kind, x_offset as xOffset + from section where board_id like 'qa-v2-board-%'`, + ) + .all() as { + id: string; + boardId: string; + kind: "empty" | "container"; + xOffset: number | null; + }[]; + const geometrySectionLayouts = database + .prepare( + `select sl.section_id as sectionId, sl.layout_id as layoutId, + sl.parent_section_id as parentSectionId, sl.x_offset as xOffset, + sl.y_offset as yOffset, sl.width, sl.height + from section_layout sl + join layout l on l.id = sl.layout_id + where l.board_id like 'qa-v2-board-%'`, + ) + .all() as { + sectionId: string; + layoutId: string; + parentSectionId: string | null; + xOffset: number; + yOffset: number; + width: number; + height: number; + }[]; + const geometryItemLayouts = database + .prepare( + `select il.item_id as itemId, i.board_id as boardId, il.section_id as sectionId, + il.layout_id as layoutId, il.x_offset as xOffset, il.y_offset as yOffset, + il.width, il.height + from item_layout il + join item i on i.id = il.item_id + where i.board_id like 'qa-v2-board-%'`, + ) + .all() as { + itemId: string; + boardId: string; + sectionId: string; + layoutId: string; + xOffset: number; + yOffset: number; + width: number; + height: number; + }[]; + const geometryErrors = findFixtureGeometryErrors({ + layouts: geometryLayouts, + sections: geometrySections, + sectionLayouts: geometrySectionLayouts, + itemLayouts: geometryItemLayouts, + }); + if (geometryErrors.length > 0) { + errors.push(`${label}: invalid fixture geometry (${geometryErrors.join("; ")})`); + } + + if (populated) { + const actualWidgetKinds = database + .prepare(`select distinct kind from item where board_id like 'qa-v2-board-widgets-%' order by kind`) + .all() + .map((row) => (row as { kind: string }).kind); + if (!sameSet(actualWidgetKinds, sourceWidgetKinds)) { + errors.push(`${label}: widget gallery does not contain every canonical widget kind exactly once`); + } + + for (const board of fixture.boards.filter((candidate) => candidate.flags.shared)) { + for (const [handle, permission] of Object.entries(fixture.expectedBoardPermissions)) { + const permissionCount = count( + `select count(*) as count from boardUserPermission where board_id = ? and user_id = ? and permission = ?`, + board.id, + `qa-v2-user-${handle}`, + permission, + ); + if (permission === "none") { + const anyPermissionCount = count( + `select count(*) as count from boardUserPermission where board_id = ? and user_id = ?`, + board.id, + `qa-v2-user-${handle}`, + ); + if (anyPermissionCount !== 0) errors.push(`${label}: ${handle} unexpectedly has access to ${board.handle}`); + continue; + } + if (permissionCount !== 1) errors.push(`${label}: ${handle} lacks ${permission} access to ${board.handle}`); + } + } + } + } finally { + database.close(); + } +}; + +const validateRuntimeManifest = async ( + runtime: RuntimeManifest, + manifestPath: string, + sourceWidgetKinds: string[], + expectedSharedBoardNames: Map, +) => { + const label = `runtime manifest ${manifestPath}`; + if (runtime.schemaVersion !== 1) errors.push(`${label}: unsupported schema version`); + if (runtime.candidateSha !== campaignCandidateSha) errors.push(`${label}: candidate does not match the campaign`); + if (![1, 2, 3].includes(runtime.slot)) errors.push(`${label}: slot must be 1, 2, or 3`); + if (!expectedProfileFlags[runtime.profile]) errors.push(`${label}: unknown profile ${runtime.profile}`); + const expectedFlags = booleanProfileFlags(runtime.profile); + if (JSON.stringify(runtime.flags) !== JSON.stringify(expectedFlags)) { + errors.push(`${label}: runtime flags do not match profile ${runtime.profile}`); + } + if (runtime.watcher?.watchpackPollingIntervalMs !== 1_000) { + errors.push(`${label}: Watchpack polling interval must be 1000ms`); + } + if (resolve(runtime.repoRoot) !== repoRoot) errors.push(`${label}: repoRoot does not match this checkout`); + try { + await assertSafeRunRoot(runtime.runRoot); + } catch { + errors.push(`${label}: runRoot is not a safe release-v2 QA temporary root`); + } + + const expectedSlotDir = join(runtime.runRoot, "slots", String(runtime.slot)); + const expectedManifestPath = join(expectedSlotDir, "runtime-manifest.json"); + const expectedDbPath = join(expectedSlotDir, "db.sqlite"); + const expectedFixtureManifestPath = join(expectedSlotDir, "fixture-manifest.json"); + const expectedNextDistDir = join(repoRoot, "apps/nextjs/.next-qa", `slot-${runtime.slot}`); + if (resolve(runtime.slotDir) !== resolve(expectedSlotDir)) + errors.push(`${label}: slotDir is outside the selected slot`); + if (resolve(manifestPath) !== resolve(expectedManifestPath)) + errors.push(`${label}: runtime manifest is outside the selected slot`); + if (resolve(runtime.dbPath) !== resolve(expectedDbPath)) errors.push(`${label}: dbPath is outside the selected slot`); + if (resolve(runtime.fixtureManifestPath) !== resolve(expectedFixtureManifestPath)) { + errors.push(`${label}: fixture manifest path is outside the selected slot`); + } + if (resolve(runtime.nextDistDir) !== resolve(expectedNextDistDir)) { + errors.push(`${label}: Next dist directory is not isolated to the selected slot`); + } + for (const processRecord of Object.values(runtime.processes ?? {})) { + if (!isPathWithin(expectedSlotDir, resolve(processRecord.logPath))) { + errors.push(`${label}: process log path escapes the selected slot`); + } + } + try { + if ((await realpath(runtime.runRoot)) !== resolve(runtime.runRoot)) + errors.push(`${label}: runRoot must not be a symlink`); + if ((await realpath(runtime.slotDir)) !== resolve(runtime.slotDir)) + errors.push(`${label}: slotDir must not be a symlink`); + } catch { + errors.push(`${label}: runRoot or slotDir is missing`); + } + + let appUrl: URL | undefined; + let fixtureUrl: URL | undefined; + try { + appUrl = new URL(runtime.url); + fixtureUrl = new URL(runtime.fixtureUrl); + } catch { + errors.push(`${label}: runtime URLs are invalid`); + } + for (const [name, url] of [ + ["app", appUrl], + ["fixture", fixtureUrl], + ] as const) { + if (!url) continue; + try { + validateFixtureUrl(url.href); + } catch { + errors.push(`${label}: ${name} URL must be credential-free loopback HTTP`); + } + } + if (appUrl && Number(appUrl.port) !== runtime.ports.app) errors.push(`${label}: app URL and port differ`); + if (fixtureUrl && Number(fixtureUrl.port) !== runtime.ports.fixture) + errors.push(`${label}: fixture URL and port differ`); + + if (!existsSync(runtime.fixtureManifestPath)) { + errors.push(`${label}: fixture manifest is missing`); + return; + } + const fixture = await readJson(runtime.fixtureManifestPath); + if (fixture.profile !== runtime.profile) errors.push(`${label}: runtime and fixture profiles differ`); + if (fixture.candidateSha !== runtime.candidateSha) errors.push(`${label}: runtime and fixture candidates differ`); + await validateFixtureManifest( + fixture, + runtime.fixtureManifestPath, + sourceWidgetKinds, + runtime.status === "running", + expectedSharedBoardNames, + ); + await validateSqliteState(runtime, fixture, sourceWidgetKinds); + + if (runtime.status === "running" && appUrl) { + try { + const response = await fetch(`${appUrl.origin}/api/health/ready`, { signal: AbortSignal.timeout(5_000) }); + if (!response.ok) errors.push(`${label}: application readiness returned HTTP ${response.status}`); + } catch { + errors.push(`${label}: application readiness endpoint is unreachable`); + } + } +}; + +const validateRequestedRuntime = async ( + options: VerifyOptions, + sourceWidgetKinds: string[], + expectedSharedBoardNames: Map, +) => { + const manifestPaths = [...options.manifestPaths]; + if (options.runRoot) { + try { + options.runRoot = await assertSafeRunRoot(options.runRoot); + } catch { + errors.push(`run-root is not a safe release-v2 QA temporary root: ${options.runRoot}`); + } + const markerPath = join(options.runRoot, ".homarr-release-v2-qa-root.json"); + if (!existsSync(markerPath)) { + errors.push(`run-root marker is missing: ${markerPath}`); + } else { + const marker = await readJson<{ schemaVersion?: number; repoRoot?: string; runRoot?: string }>(markerPath); + if (marker.schemaVersion !== 1 || resolve(marker.repoRoot ?? "") !== repoRoot) { + errors.push(`run-root marker does not belong to this checkout: ${markerPath}`); + } + if (marker.runRoot && resolve(marker.runRoot) !== options.runRoot) { + errors.push(`run-root marker records a different root: ${markerPath}`); + } + } + const slotsRoot = join(options.runRoot, "slots"); + if (existsSync(slotsRoot)) { + const slotEntries = await readdir(slotsRoot, { withFileTypes: true }); + for (const entry of slotEntries) { + if (!entry.isDirectory() || !["1", "2", "3"].includes(entry.name)) { + errors.push(`run-root contains an unexpected slot entry: ${entry.name}`); + continue; + } + const runtimePath = join(slotsRoot, entry.name, "runtime-manifest.json"); + if (!existsSync(runtimePath)) { + errors.push(`slot ${entry.name} contains incomplete state without a runtime manifest`); + continue; + } + manifestPaths.push(runtimePath); + } + } + } + + const uniqueManifestPaths = new Set(manifestPaths); + if (options.runRoot && uniqueManifestPaths.size === 0) { + errors.push(`run-root contains no runtime manifests: ${options.runRoot}`); + } + for (const manifestPath of uniqueManifestPaths) { + const value = await readJson(manifestPath); + if ("runId" in value) { + await validateRuntimeManifest(value, manifestPath, sourceWidgetKinds, expectedSharedBoardNames); + continue; + } + await validateFixtureManifest(value, manifestPath, sourceWidgetKinds, true, expectedSharedBoardNames); + } +}; + +const main = async (): Promise => { + const options = parseOptions(); + const manifest = await readJson(join(qaRoot, "coverage-manifest.json")); + const ledgerPath = join(qaRoot, "ledger.json"); + await assertSafeReportPath(qaRoot, ledgerPath, "release-v2 QA ledger path"); + await assertSafeReportPath(qaRoot, join(qaRoot, "master-report.md"), "release-v2 QA master report path"); + const ledger = JSON.parse(await readSafeReportFile(qaRoot, ledgerPath, "release-v2 QA ledger path")) as Ledger; + const allowedStatuses: Status[] = ["passed", "failed", "blocked", "not-reached"]; + const expectedCounts = { preflight: 3, board: 9, widgets: 12, "core-v2": 8, "whole-product": 9, performance: 4 }; + + if (!sameSet(manifest.statuses, allowedStatuses)) + errors.push("manifest statuses do not match the required status vocabulary"); + if (manifest.packets.length !== 45) errors.push(`expected 45 packets, found ${manifest.packets.length}`); + if (JSON.stringify(manifest.wavePacketCounts) !== JSON.stringify(expectedCounts)) + errors.push("wavePacketCounts is not 3/9/12/8/9/4"); + + const packetIds = manifest.packets.map((packet) => packet.id); + for (const duplicate of new Set(duplicateValues(packetIds))) errors.push(`duplicate packet id ${duplicate}`); + + for (const wave of Object.keys(expectedCounts)) { + const dimensions = manifest.caseDimensions[wave] ?? []; + if (dimensions.length < 3) errors.push(`${wave}: expected at least three explicit case dimensions`); + if (duplicateValues(dimensions.map((dimension) => dimension.id)).length > 0) + errors.push(`${wave}: duplicate case dimension IDs`); + if (dimensions.some((dimension) => dimension.expected.trim().length === 0)) + errors.push(`${wave}: empty case expectation`); + } + + const allCases = manifest.packets.flatMap((packet) => expandedCases(manifest, packet).map(({ id }) => id)); + for (const duplicate of new Set(duplicateValues(allCases))) errors.push(`duplicate case id ${duplicate}`); + + const prNumbers = manifest.pullRequests.map((pullRequest) => pullRequest.number); + const profileIds = manifest.profiles.map((profile) => profile.id); + const viewportIds = manifest.viewports.map((viewport) => viewport.id); + const boardIds = manifest.boards.map((board) => board.id); + const boardHandles = manifest.boards.map((board) => board.handle); + for (const duplicate of new Set(duplicateValues(boardIds))) errors.push(`duplicate board name ${duplicate}`); + for (const duplicate of new Set(duplicateValues(boardHandles))) errors.push(`duplicate board handle ${duplicate}`); + for (const board of manifest.boards) { + if (!boardNameSchema.safeParse(board.id).success) errors.push(`invalid coverage board name ${board.id}`); + } + const expectedSharedBoardNames = new Map(manifest.boards.map((board) => [board.handle, board.id])); + + for (const [wave, expected] of Object.entries(expectedCounts)) { + const actual = manifest.packets.filter((packet) => packet.wave === wave).length; + if (actual !== expected) errors.push(`${wave}: expected ${expected} packets, found ${actual}`); + } + + for (const packet of manifest.packets) { + if (packet.cases.length === 0 || expandedCases(manifest, packet).length < 3) + errors.push(`${packet.id}: insufficient cases assigned`); + assertReferences(packet, "prRefs", packet.prRefs, prNumbers); + assertReferences(packet, "personas", packet.personas, manifest.personas); + assertReferences(packet, "boards", packet.boards, boardIds); + assertReferences(packet, "profiles", packet.profiles, profileIds); + assertReferences(packet, "viewports", packet.viewports, viewportIds); + assertReferences(packet, "zooms", packet.zooms, manifest.zooms); + assertReferences(packet, "inputs", packet.inputs, manifest.inputs); + if (packet.widgetKinds) { + const expectedGroup = expectedWidgetGroups[packet.id]; + if (!expectedGroup || JSON.stringify(packet.widgetKinds) !== JSON.stringify(expectedGroup)) { + errors.push(`${packet.id}: widget allocation does not match the required group`); + } + if (JSON.stringify(packet.viewports) !== JSON.stringify(viewportIds)) { + errors.push(`${packet.id}: every widget kind must cover all ${viewportIds.length} required viewports`); + } + } + } + + for (const prNumber of prNumbers) { + if (!manifest.packets.some((packet) => packet.prRefs.includes(prNumber))) + errors.push(`PR #${prNumber} has no packet coverage`); + } + + const widgetSource = await readFile(join(repoRoot, "packages/definitions/src/widget.ts"), "utf8"); + const widgetBlock = widgetSource.match(/export const widgetKinds = \[([\s\S]*?)\] as const;/)?.[1]; + if (!widgetBlock) throw new Error("Could not parse widgetKinds from packages/definitions/src/widget.ts"); + const sourceWidgetKinds = [...widgetBlock.matchAll(/"([^"]+)"/g)].map((match) => match[1]); + const assignedWidgetKinds = manifest.packets.flatMap((packet) => packet.widgetKinds ?? []); + + for (const widgetKind of sourceWidgetKinds) { + const count = assignedWidgetKinds.filter((assigned) => assigned === widgetKind).length; + if (count !== 1) errors.push(`widget ${widgetKind}: expected exactly one assignment, found ${count}`); + } + for (const widgetKind of assignedWidgetKinds) { + if (!sourceWidgetKinds.includes(widgetKind)) errors.push(`unknown widget assignment ${widgetKind}`); + } + if (assignedWidgetKinds.length !== sourceWidgetKinds.length) { + errors.push( + `widget coverage count differs: ${assignedWidgetKinds.length} assigned, ${sourceWidgetKinds.length} declared`, + ); + } + + await validateRequestedRuntime(options, sourceWidgetKinds, expectedSharedBoardNames); + + for (const packet of manifest.packets) { + const reportPath = join(reportsRoot, packet.id, "report.md"); + await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`); + let content: string; + try { + content = await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`); + } catch { + errors.push(`${packet.id}: report.md is missing`); + continue; + } + + const rawMetadata = parseReportMetadata(content, packet.id); + if (rawMetadata === undefined) continue; + const expectedWidgetChecks = (packet.widgetKinds ?? []).flatMap((widgetKind) => + packet.viewports.map((viewport) => ({ + widgetKind, + viewport, + sizeRequirement: requiredSizeInstruction(widgetKind), + status: "not-reached" as const, + })), + ); + const normalized = normalizeReportMetadata(rawMetadata, { + packetId: packet.id, + expectedCaseIds: expandedCases(manifest, packet).map(({ id }) => id), + expectedWidgetChecks, + }); + errors.push(...normalized.errors); + const metadata = normalized.metadata as ReportMetadata; + errors.push(...collectHumanWidgetStatusErrors(content, packet.id, metadata.widgetChecks)); + if (metadata.packetId !== packet.id) errors.push(`${packet.id}: metadata packetId is ${metadata.packetId}`); + if (!allowedStatuses.includes(metadata.status)) + errors.push(`${packet.id}: invalid report status ${String(metadata.status)}`); + if (!Array.isArray(metadata.findings)) errors.push(`${packet.id}: findings must be an array`); + if (!Array.isArray(metadata.artifacts)) errors.push(`${packet.id}: artifacts must be an array`); + if (metadata.execution?.candidateSha !== campaignCandidateSha) { + errors.push(`${packet.id}: candidateSha must equal the pinned campaign candidate ${campaignCandidateSha}`); + } + if (!content.includes(`| Candidate SHA | ${campaignCandidateSha} |`)) { + errors.push(`${packet.id}: human metadata must show the pinned campaign candidate`); + } + if (!metadata.execution || typeof metadata.execution !== "object") + errors.push(`${packet.id}: execution metadata is missing`); + if (!content.includes("## Dogfood evidence")) errors.push(`${packet.id}: dogfood evidence section is missing`); + if (!content.includes(resolve(repoRoot, ".screenshots/release-v2", packet.id))) { + errors.push(`${packet.id}: absolute artifact directory link is missing`); + } + if (!content.includes(`| Status | ${metadata.status} |`)) + errors.push(`${packet.id}: human metadata status differs from report metadata`); + + const packetCases = expandedCases(manifest, packet).map(({ id }) => id); + if (!sameSet(Object.keys(metadata.caseStatuses), packetCases)) + errors.push(`${packet.id}: case status IDs differ from manifest`); + for (const [caseId, status] of Object.entries(metadata.caseStatuses)) { + if (!allowedStatuses.includes(status)) + errors.push(`${packet.id}/${caseId}: invalid case status ${String(status)}`); + if (!content.includes(`| ${caseId} | ${status} |`)) + errors.push(`${packet.id}/${caseId}: human evidence row/status is missing`); + } + const rollup = expectedRollup(Object.values(metadata.caseStatuses)); + if (metadata.status !== rollup) + errors.push(`${packet.id}: report status ${metadata.status} does not match case rollup ${rollup}`); + + for (const finding of Array.isArray(metadata.findings) ? metadata.findings : []) { + if (!["P0", "P1", "P2", "P3"].includes(finding.severity)) + errors.push(`${packet.id}: finding has invalid severity ${String(finding.severity)}`); + if (typeof finding.title !== "string" || finding.title.trim().length === 0) + errors.push(`${packet.id}: finding title is required`); + for (const caseId of finding.caseIds ?? []) { + if (!packetCases.includes(caseId)) errors.push(`${packet.id}: finding references unknown case ${caseId}`); + } + for (const evidence of finding.evidence ?? []) { + const artifactError = await validateResolvedArtifactPath( + resolve(repoRoot, ".screenshots/release-v2", packet.id), + evidence, + `${packet.id}: finding evidence`, + ); + if (artifactError) errors.push(artifactError); + } + } + + if (packet.widgetKinds) { + const checks = Array.isArray(metadata.widgetChecks) ? metadata.widgetChecks : []; + const expectedCheckKeys = packet.widgetKinds.flatMap((widgetKind) => + packet.viewports.map((viewport) => `${widgetKind}\u0000${viewport}`), + ); + const actualCheckKeys = checks.map((check) => `${check.widgetKind}\u0000${check.viewport}`); + if (!sameSet(actualCheckKeys, expectedCheckKeys)) { + errors.push(`${packet.id}: structured widget checks do not match the assigned kind × viewport matrix`); + } + if (duplicateValues(actualCheckKeys).length > 0) { + errors.push(`${packet.id}: structured widget checks contain duplicate kind × viewport rows`); + } + for (const check of checks) { + if (!allowedStatuses.includes(check.status)) { + errors.push(`${packet.id}/${check.widgetKind}/${check.viewport}: invalid widget check status`); + } + const expectedInstruction = requiredSizeInstruction(check.widgetKind); + if (check.sizeRequirement !== expectedInstruction) { + errors.push(`${packet.id}/${check.widgetKind}: incorrect size-threshold instruction`); + } + } + for (const heading of [ + "Size", + "States", + "Permission/read-only", + "Options/persistence", + "Recovery", + "Evidence", + "Status", + ]) { + if (!content.includes(heading)) errors.push(`${packet.id}: widget checklist is missing ${heading}`); + } + } else if ((metadata.widgetChecks?.length ?? 0) > 0) { + errors.push(`${packet.id}: non-widget report must not contain widget checks`); + } + + if (!metadata.performance || !Array.isArray(metadata.performance.measurements)) { + errors.push(`${packet.id}: performance measurements must be an array`); + } else { + for (const measurement of metadata.performance.measurements) { + if (!measurement.name?.trim() || !measurement.unit?.trim() || !measurement.threshold?.trim()) { + errors.push(`${packet.id}: performance measurement name, unit, and threshold are required`); + } + if (measurement.value !== null && !Number.isFinite(measurement.value)) { + errors.push(`${packet.id}: performance measurement value must be finite or null`); + } + if (!allowedStatuses.includes(measurement.status)) { + errors.push(`${packet.id}: performance measurement has invalid status`); + } + for (const evidence of measurement.evidence ?? []) { + const artifactError = await validateResolvedArtifactPath( + resolve(repoRoot, ".screenshots/release-v2", packet.id), + evidence, + `${packet.id}: performance evidence`, + ); + if (artifactError) errors.push(artifactError); + } + } + } + if (!Array.isArray(metadata.performance?.limitations)) { + errors.push(`${packet.id}: performance limitations must be an array`); + } + if (!Array.isArray(metadata.independentReproductions)) { + errors.push(`${packet.id}: independentReproductions must be an array`); + } else { + for (const reproduction of metadata.independentReproductions) { + if (!reproduction.findingFingerprint?.trim() || !reproduction.agentId?.trim()) { + errors.push(`${packet.id}: independent reproduction requires a finding fingerprint and agent ID`); + } + if (!["reproduced", "not-reproduced", "blocked", "not-reached"].includes(reproduction.outcome)) { + errors.push(`${packet.id}: independent reproduction has invalid outcome`); + } + for (const evidence of reproduction.evidence ?? []) { + const artifactError = await validateResolvedArtifactPath( + resolve(repoRoot, ".screenshots/release-v2", packet.id), + evidence, + `${packet.id}: reproduction evidence`, + ); + if (artifactError) errors.push(artifactError); + } + } + } + + if (metadata.status !== "not-reached" && metadata.execution) { + const execution = metadata.execution; + const requiredText = [ + execution.candidateSha, + execution.url, + execution.runtimeProfile, + execution.persona, + execution.sessionId, + execution.timestamp, + execution.viewport, + execution.input, + ]; + if (requiredText.some((value) => typeof value !== "string" || value.trim().length === 0)) { + errors.push(`${packet.id}: completed/blocked report has incomplete execution metadata`); + } + if (!Number.isInteger(execution.actualPort) || (execution.actualPort ?? 0) <= 0) + errors.push(`${packet.id}: actualPort is required after execution`); + if (!Number.isFinite(execution.zoom) || (execution.zoom ?? 0) <= 0) + errors.push(`${packet.id}: zoom is required after execution`); + if (execution.viewport && !packet.viewports.includes(execution.viewport)) + errors.push(`${packet.id}: execution viewport is outside packet coverage`); + if (execution.input && !packet.inputs.includes(execution.input)) + errors.push(`${packet.id}: execution input is outside packet coverage`); + if (execution.zoom && !packet.zooms.includes(execution.zoom)) + errors.push(`${packet.id}: execution zoom is outside packet coverage`); + if (execution.persona && !packet.personas.includes(execution.persona)) + errors.push(`${packet.id}: execution persona is outside packet coverage`); + if (execution.runtimeProfile && !packet.profiles.includes(execution.runtimeProfile)) + errors.push(`${packet.id}: execution profile is outside packet coverage`); + const expectedFlags = execution.runtimeProfile ? expectedProfileFlags[execution.runtimeProfile] : undefined; + if (!expectedFlags || !sameSet(execution.runtimeFlags, expectedFlags)) { + errors.push( + `${packet.id}: runtime flags do not exactly match profile ${execution.runtimeProfile ?? "unknown"}`, + ); + } + if (!execution.url?.match(/^https?:\/\//)) errors.push(`${packet.id}: URL must be absolute HTTP(S)`); + if (!execution.timestamp || Number.isNaN(Date.parse(execution.timestamp))) + errors.push(`${packet.id}: timestamp must be ISO-compatible`); + for (const value of [...requiredText, execution.actualPort, execution.zoom]) { + if (value !== null && value !== undefined && !content.includes(String(value))) + errors.push(`${packet.id}: human metadata does not include execution value ${String(value)}`); + } + if (metadata.artifacts.length === 0) errors.push(`${packet.id}: executed report must link at least one artifact`); + for (const artifact of metadata.artifacts) { + const artifactError = await validateResolvedArtifactPath( + resolve(repoRoot, ".screenshots/release-v2", packet.id), + artifact, + `${packet.id}: artifact`, + ); + if (artifactError) errors.push(artifactError); + } + } + + const ledgerEntry = ledger.packetStatuses[packet.id]; + if (!ledgerEntry) { + errors.push(`${packet.id}: ledger entry is missing`); + continue; + } + if (ledgerEntry.status !== metadata.status) errors.push(`${packet.id}: ledger/report packet statuses differ`); + if (JSON.stringify(ledgerEntry.cases) !== JSON.stringify(metadata.caseStatuses)) { + errors.push(`${packet.id}: ledger/report case statuses differ`); + } + } + + for (const ledgerPacketId of Object.keys(ledger.packetStatuses)) { + if (!packetIds.includes(ledgerPacketId)) errors.push(`ledger contains unknown packet ${ledgerPacketId}`); + } + + if (errors.length > 0) { + console.error(`release-v2 QA verification failed with ${errors.length} error(s):`); + for (const error of errors) console.error(`- ${error}`); + process.exitCode = 1; + return; + } + + console.log( + `release-v2 QA verification passed: ${manifest.packets.length} packets, ${allCases.length} cases, ${sourceWidgetKinds.length} widgets`, + ); +}; + +await main(); diff --git a/scripts/release-v2-qa/zoom.mts b/scripts/release-v2-qa/zoom.mts new file mode 100644 index 000000000..19f43cfcd --- /dev/null +++ b/scripts/release-v2-qa/zoom.mts @@ -0,0 +1,172 @@ +import { lstat, mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { assertSafeZoomOutput } from "./safety.mts"; + +const allowedFactorStrings = new Set(["0.8", "1", "1.25", "2"]); + +interface ZoomOptions { + origin: string; + factor: number; + outputDirectory: string; +} + +const usage = () => { + console.log("Usage: zoom.mts --origin --factor <0.8|1|1.25|2> --output "); +}; + +const validateOrigin = (value: string) => { + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error("--origin must be a valid HTTP(S) origin"); + } + + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error("--origin must use http: or https:"); + } + if (url.username || url.password) { + throw new Error("--origin must not contain credentials"); + } + if (url.pathname !== "/" || url.search || url.hash) { + throw new Error("--origin must not contain a path, query, or fragment"); + } + + return url.origin; +}; + +const validateOutputDirectory = async (value: string) => { + if (!path.isAbsolute(value)) { + throw new Error("--output must be an absolute path to a new directory"); + } + + const outputDirectory = await assertSafeZoomOutput(value, { allowMissing: true }); + + let outputExists = true; + try { + await lstat(outputDirectory); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + outputExists = false; + } + if (outputExists) throw new Error(`--output must not already exist: ${outputDirectory}`); + return outputDirectory; +}; + +export const parseZoomOptions = async (rawArguments: string[]): Promise => { + const args = rawArguments.filter((argument) => argument !== "--"); + let origin: string | undefined; + let factorString: string | undefined; + let output: string | undefined; + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + const value = args[index + 1]; + if (argument === "--origin" && value) { + origin = value; + index += 1; + continue; + } + if (argument === "--factor" && value) { + factorString = value; + index += 1; + continue; + } + if (argument === "--output" && value) { + output = value; + index += 1; + continue; + } + throw new Error(`Unknown or incomplete argument: ${argument ?? ""}`); + } + + if (!origin) throw new Error("--origin is required"); + if (!factorString) throw new Error("--factor is required"); + if (!allowedFactorStrings.has(factorString)) { + throw new Error("--factor must be one of 0.8, 1, 1.25, or 2"); + } + if (!output) throw new Error("--output is required"); + + return { + origin: validateOrigin(origin), + factor: Number(factorString), + outputDirectory: await validateOutputDirectory(output), + }; +}; + +export const createZoomExtensionContent = (origin: string, factor: number) => { + const manifest = { + manifest_version: 3, + name: "Homarr release v2 native zoom", + version: "1.0.0", + permissions: ["tabs"], + host_permissions: [`${origin}/*`], + background: { service_worker: "background.js" }, + }; + const background = `const TARGET_ORIGIN = ${JSON.stringify(origin)}; +const ZOOM_FACTOR = ${JSON.stringify(factor)}; + +function applyZoom(tab) { + if (tab?.id == null || !tab.url) return; + + let origin; + try { + origin = new URL(tab.url).origin; + } catch { + return; + } + if (origin !== TARGET_ORIGIN) return; + + chrome.tabs.setZoom(tab.id, ZOOM_FACTOR, () => { + void chrome.runtime.lastError; + }); +} + +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status !== "complete") return; + applyZoom({ ...tab, id: tabId }); +}); + +chrome.tabs.query({}, (tabs) => { + tabs.forEach(applyZoom); +}); +`; + + return { + manifest: `${JSON.stringify(manifest, null, 2)}\n`, + background, + }; +}; + +export const generateZoomExtension = async (options: ZoomOptions) => { + const content = createZoomExtensionContent(options.origin, options.factor); + await assertSafeZoomOutput(options.outputDirectory, { allowMissing: true }); + await mkdir(options.outputDirectory, { mode: 0o700 }); + await assertSafeZoomOutput(options.outputDirectory); + await Promise.all([ + writeFile(path.join(options.outputDirectory, "manifest.json"), content.manifest, { flag: "wx", mode: 0o600 }), + writeFile(path.join(options.outputDirectory, "background.js"), content.background, { flag: "wx", mode: 0o600 }), + ]); +}; + +const main = async () => { + const rawArguments = process.argv.slice(2); + if (rawArguments.includes("--help") || rawArguments.includes("-h")) { + usage(); + return; + } + + const options = await parseZoomOptions(rawArguments); + await generateZoomExtension(options); + console.log(`Generated release-v2 native zoom extension`); + console.log(`Origin: ${options.origin}`); + console.log(`Factor: ${options.factor}`); + console.log(`Output: ${options.outputDirectory}`); +}; + +const isDirectExecution = process.argv[1] + ? pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(fileURLToPath(import.meta.url)).href + : false; +if (isDirectExecution) await main(); diff --git a/scripts/release-v2-qa/zoom.spec.ts b/scripts/release-v2-qa/zoom.spec.ts new file mode 100644 index 000000000..e5246b384 --- /dev/null +++ b/scripts/release-v2-qa/zoom.spec.ts @@ -0,0 +1,155 @@ +// @vitest-environment node + +import { mkdir, readFile, rm, symlink } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; + +import { createZoomExtensionContent, generateZoomExtension, parseZoomOptions } from "./zoom.mts"; + +const temporaryDirectories: string[] = []; + +const createOutputPath = (suffix: string) => { + const directory = path.join( + tmpdir(), + `homarr-release-v2-qa-zoom-${process.pid}-${Date.now()}-${suffix}-${temporaryDirectories.length}`, + ); + temporaryDirectories.push(directory); + return directory; +}; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +describe("release-v2 native zoom helper", () => { + test("generates deterministic, exact-origin MV3 extension files", async () => { + const firstOutput = createOutputPath("first"); + const secondOutput = createOutputPath("second"); + const firstOptions = await parseZoomOptions([ + "--origin", + "http://127.0.0.1:34401/", + "--factor", + "1.25", + "--output", + firstOutput, + ]); + const secondOptions = await parseZoomOptions([ + "--origin", + "http://127.0.0.1:34401", + "--factor", + "1.25", + "--output", + secondOutput, + ]); + + await generateZoomExtension(firstOptions); + await generateZoomExtension(secondOptions); + + const firstManifest = await readFile(path.join(firstOutput, "manifest.json"), "utf8"); + const firstBackground = await readFile(path.join(firstOutput, "background.js"), "utf8"); + expect(firstManifest).toBe(await readFile(path.join(secondOutput, "manifest.json"), "utf8")); + expect(firstBackground).toBe(await readFile(path.join(secondOutput, "background.js"), "utf8")); + expect(JSON.parse(firstManifest)).toMatchObject({ + manifest_version: 3, + permissions: ["tabs"], + host_permissions: ["http://127.0.0.1:34401/*"], + background: { service_worker: "background.js" }, + }); + expect(firstBackground).toContain('const TARGET_ORIGIN = "http://127.0.0.1:34401";'); + expect(firstBackground).toContain("const ZOOM_FACTOR = 1.25;"); + expect(firstBackground).toContain("origin !== TARGET_ORIGIN"); + expect(firstBackground).not.toContain("password"); + }); + + test.each(["0.8", "1", "1.25", "2"])("accepts the supported factor %s", async (factor) => { + const output = createOutputPath(`factor-${factor.replace(".", "-")}`); + const options = await parseZoomOptions([ + "--origin", + "https://qa.example.test:8443", + "--factor", + factor, + "--output", + output, + ]); + + expect(options.factor).toBe(Number(factor)); + }); + + test.each(["0", "0.9", "1.0", "1.5", "2.1", "not-a-number"])("rejects unsupported factor %s", async (factor) => { + const output = createOutputPath(`invalid-factor-${factor.replaceAll(/[^A-Za-z0-9]/gu, "-")}`); + await expect( + parseZoomOptions(["--origin", "http://127.0.0.1:34401", "--factor", factor, "--output", output]), + ).rejects.toThrow("--factor must be one of 0.8, 1, 1.25, or 2"); + }); + + test.each([ + "ftp://127.0.0.1:34401", + "http://user:secret@127.0.0.1:34401", + "http://127.0.0.1:34401/boards/qa-grid-24", + "http://127.0.0.1:34401/?mode=qa", + "not-a-url", + ])("rejects a non-origin target without echoing it: %s", async (origin) => { + const output = createOutputPath("invalid-origin"); + await expect(parseZoomOptions(["--origin", origin, "--factor", "1", "--output", output])).rejects.toThrow( + /--origin/u, + ); + }); + + test("requires a new, predictably named direct child of the temporary directory", async () => { + const existingDirectory = createOutputPath("existing"); + const nestedParent = createOutputPath("nested-parent"); + await mkdir(existingDirectory); + await mkdir(nestedParent); + await expect( + parseZoomOptions(["--origin", "http://127.0.0.1:34401", "--factor", "1", "--output", "relative-extension"]), + ).rejects.toThrow("--output must be an absolute path"); + await expect( + parseZoomOptions(["--origin", "http://127.0.0.1:34401", "--factor", "1", "--output", existingDirectory]), + ).rejects.toThrow("--output must not already exist"); + await expect( + parseZoomOptions([ + "--origin", + "http://127.0.0.1:34401", + "--factor", + "1", + "--output", + path.join(nestedParent, "homarr-release-v2-qa-zoom-nested"), + ]), + ).rejects.toThrow("direct child"); + await expect( + parseZoomOptions([ + "--origin", + "http://127.0.0.1:34401", + "--factor", + "1", + "--output", + path.join(tmpdir(), "uncontrolled-extension"), + ]), + ).rejects.toThrow("basename must start"); + }); + + test("canonicalizes a temporary-directory alias for extension output", async () => { + const temporaryAlias = createOutputPath("alias-parent"); + await symlink(tmpdir(), temporaryAlias, "dir"); + const outputName = `homarr-release-v2-qa-zoom-${process.pid}-${Date.now()}-through-alias`; + const canonicalOutput = path.join(tmpdir(), outputName); + temporaryDirectories.push(canonicalOutput); + + const options = await parseZoomOptions([ + "--origin", + "http://127.0.0.1:34401", + "--factor", + "1", + "--output", + path.join(temporaryAlias, outputName), + ]); + + expect(options.outputDirectory).toBe(canonicalOutput); + }); + + test("renders baseline factor 1 without changing the extension shape", () => { + const content = createZoomExtensionContent("https://qa.example.test", 1); + expect(content.background).toContain("const ZOOM_FACTOR = 1;"); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts index 8a47aa4b5..4ea6ca33b 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -62,6 +62,20 @@ export default defineConfig({ include: ["packages/docker/**/*.spec.{ts,tsx}"], }, }, + { + extends: true, + test: { + name: "translation-node", + environment: "node", + setupFiles: ["./vitest.setup.ts", "./vitest.setup.node.ts"], + include: ["packages/translation/**/*.spec.ts"], + server: { + deps: { + inline: ["next-intl"], + }, + }, + }, + }, { extends: true, test: { @@ -76,6 +90,7 @@ export default defineConfig({ "packages/db/**", "packages/docker/**", "packages/request-handler/**", + "packages/translation/**", "e2e/**", ], },