diff --git a/packages/dashboard/app/components/__tests__/DevServerView.mobile.test.tsx b/packages/dashboard/app/components/__tests__/DevServerView.mobile.test.tsx index cf828f3b7e..bd4603160a 100644 --- a/packages/dashboard/app/components/__tests__/DevServerView.mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/DevServerView.mobile.test.tsx @@ -23,6 +23,46 @@ vi.mock("../DevServerLogViewer", () => ({ DevServerLogViewer: () =>
, })); +function extractAtRuleBlocks(css: string, marker: string): string[] { + const blocks: string[] = []; + let searchFrom = 0; + + while (searchFrom < css.length) { + const start = css.indexOf(marker, searchFrom); + if (start === -1) break; + const open = css.indexOf("{", start); + if (open === -1) break; + + let depth = 1; + let cursor = open + 1; + while (cursor < css.length && depth > 0) { + if (css[cursor] === "{") depth++; + else if (css[cursor] === "}") depth--; + cursor++; + } + + blocks.push(css.slice(open + 1, cursor - 1)); + searchFrom = cursor; + } + + return blocks; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function selectorRule(css: string, selector: string): string | null { + const escapedSelector = escapeRegExp(selector); + const match = css.match(new RegExp(`(?:^|\\n)\\s*${escapedSelector}\\s*\\{[\\s\\S]*?\\n\\s*\\}`, "m")); + return match?.[0] ?? null; +} + +function countSelectorRules(css: string, selector: string): number { + const escapedSelector = escapeRegExp(selector); + return (css.match(new RegExp(`(?:^|\\n)\\s*${escapedSelector}\\s*\\{`, "g")) ?? []).length; +} + function createDevServerHookState() { return { session: { @@ -48,25 +88,31 @@ function createDevServerHookState() { describe("DevServerView mobile CSS/structure", () => { it("defines one mobile rule-set for preview header/actions and wraps badge correctly", () => { const css = loadAllAppCss(); - const mobileBlockMatch = css.match(/@media[^{]*\(max-width: 768px\)[^{]*\{([\s\S]*?)\n\}/g) ?? []; - const mobileCss = mobileBlockMatch.join("\n"); + /* + FNXC:DashboardTests 2026-06-26-13:05: + DevServerView intentionally keeps preview-header and modal-launcher copy mobile wrapping in separate rules so each surface can be asserted directly. Extract the balanced viewport at-rule from the loaded app CSS instead of counting a stale grouped selector that drifted after the CSS was split. + */ + const mobileCss = extractAtRuleBlocks(css, "@media (max-width: 768px)") + .find((block) => block.includes(".devserver-preview-header") && block.includes(".devserver-preview-modal-launcher__copy")); - const headerRuleCount = (mobileCss.match(/\.devserver-preview-header,\s*\.devserver-preview-modal-launcher__copy\s*\{/g) ?? []).length; - expect(headerRuleCount).toBe(1); - expect(mobileCss).toMatch(/\.devserver-preview-url-badge\s*\{[\s\S]*max-width:\s*100%/); - expect(mobileCss).toMatch(/\.dev-server-header-title\s*\{[\s\S]*flex-wrap:\s*wrap/); + expect(mobileCss).toBeTruthy(); + expect(countSelectorRules(mobileCss ?? "", ".devserver-preview-header")).toBe(1); + expect(selectorRule(mobileCss ?? "", ".devserver-preview-header")).toMatch(/flex-wrap:\s*wrap/); + expect(countSelectorRules(mobileCss ?? "", ".devserver-preview-modal-launcher__copy")).toBe(1); + expect(selectorRule(mobileCss ?? "", ".devserver-preview-modal-launcher__copy")).toMatch(/flex-wrap:\s*wrap/); + expect(selectorRule(mobileCss ?? "", ".devserver-preview-url-badge")).toMatch(/max-width:\s*100%/); + expect(selectorRule(mobileCss ?? "", ".dev-server-header-title")).toMatch(/flex-wrap:\s*wrap/); }); it("defines narrow right-dock launcher and modal rules without duplicating mobile media rules", () => { const css = loadAllAppCss(); - const containerStart = css.indexOf("@container right-dock-body (max-width: 768px)"); - expect(containerStart).toBeGreaterThan(-1); - const containerCss = css.slice(containerStart); + const containerCss = extractAtRuleBlocks(css, "@container right-dock-body (max-width: 768px)")[0]; + expect(containerCss).toBeTruthy(); - expect(containerCss).toMatch(/\.devserver-preview-panel,\s*\.devserver-preview-modal-launcher\s*\{[\s\S]*grid-column:\s*auto/); - expect(containerCss).toMatch(/\.devserver-preview-modal\s*\{[\s\S]*width:\s*min\(calc\(var\(--space-2xl\) \* 20\), calc\(100vw - var\(--space-md\) \* 2\)\)/); - expect(containerCss).toMatch(/\.devserver-preview-panel \.devserver-preview-container/); - expect(containerCss).not.toMatch(/\.dev-server-logs,\s*\.devserver-preview-container,\s*\.devserver-preview-iframe/); + expect(containerCss ?? "").toMatch(/\.devserver-preview-panel,\s*\.devserver-preview-modal-launcher\s*\{[\s\S]*grid-column:\s*auto/); + expect(containerCss ?? "").toMatch(/\.devserver-preview-modal\s*\{[\s\S]*width:\s*min\(calc\(var\(--space-2xl\) \* 20\), calc\(100vw - var\(--space-md\) \* 2\)\)/); + expect(containerCss ?? "").toMatch(/\.devserver-preview-panel \.devserver-preview-container/); + expect(containerCss ?? "").not.toMatch(/\.dev-server-logs,\s*\.devserver-preview-container,\s*\.devserver-preview-iframe/); expect(css).toMatch(/@media[^{]*\(max-width: 768px\)/); expect(css).toMatch(/@container right-dock-body \(max-width: 768px\)/); diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts index f211190a31..8fd70269c4 100644 --- a/packages/dashboard/vitest.config.ts +++ b/packages/dashboard/vitest.config.ts @@ -320,12 +320,10 @@ FNXC:DashboardTestQuarantine 2026-06-22-18:05: FN-6937 verified that FN-6860's claimed session-cross-tab ledger removal had not landed: the file was active because this exclude list was empty, but `test-quarantine.json` still carried the stale 2026-06-19 row. The repeated loaded `dashboard-api-quality-backfill` runs and lock-holder mutation proof confirmed FN-6742's rescue still holds, so remove the orphaned ledger row and keep this list empty to restore ledger↔config lockstep. */ /* -FNXC:DashboardTestQuarantine 2026-06-25-09:50: -Quarantine DevServerView.mobile.test.tsx: CI full-suite shard 4/4 fails with 'expected +0 to be 1' on the mobile CSS structure assertion. Under the deletion ratchet — see scripts/lib/test-quarantine.json. +FNXC:DashboardTestQuarantine 2026-06-26-13:16: +FN-7068 rescued DevServerView.mobile before the deletion deadline by realigning its mobile CSS assertion to the split preview-header and launcher-copy rules in DevServerView.css. Keep this quarantine list empty until a new flaky dashboard file is added with a matching ledger entry. */ -const quarantinedDashboardTests: string[] = [ - "app/components/__tests__/DevServerView.mobile.test.tsx", -]; +const quarantinedDashboardTests: string[] = []; const qualityApiTests = [ // Critical HTTP/server behavior: auth, task/project/settings mutation, diff --git a/packages/engine/src/__tests__/in-review-merge-stall-deadlock-recovery.test.ts b/packages/engine/src/__tests__/in-review-merge-stall-deadlock-recovery.test.ts index 92c53ccbd7..fd6d6d2911 100644 --- a/packages/engine/src/__tests__/in-review-merge-stall-deadlock-recovery.test.ts +++ b/packages/engine/src/__tests__/in-review-merge-stall-deadlock-recovery.test.ts @@ -40,6 +40,13 @@ describe("SelfHealingManager in-review merge stall deadlock recovery (FN-5488)", if (!opts?.column) return all; return all.filter((task) => task.column === opts.column); }), + getTask: vi.fn().mockImplementation(async (id: string) => tasks.get(id) ?? null), + /* + FNXC:OverlapSelfHealing 2026-06-26-12:53: + The FN-5488 stale-blockedBy sweep now reads overlap scopes, soft-deleted blockers, and completion-handoff markers through TaskStore. Keep this fake in lockstep with every clearStaleBlockedBy() store seam so the merge-stall recovery counts stay deterministic across isolated and full-suite shard runs. + */ + parseFileScopeFromPrompt: vi.fn().mockResolvedValue(["packages/engine/src/self-healing.ts"]), + getCompletionHandoffAcceptedMarker: vi.fn().mockReturnValue(null), updateTask: vi.fn().mockImplementation(async (id: string, patch: Partial