fix(engine-tests): eliminate temp-dir leak and raise subprocess guard for concurrent workspace runs

Two engine merger tests created mkdtempSync workspaces directly in tmpdir()
under the tracked `fusion-test-` prefix; under full-suite concurrent load
the post-run check-test-isolation flagged them as leaks. Route both
(`merger-no-op-fix-finalize.test.ts`, `merger-verification-fix-already-on-main.test.ts`)
through FUSION_TEST_WORKER_ROOT like sibling merger tests so they nest
inside the already-tracked worker root.

Bump engine vitest subprocess guard from 60s to 120s and testTimeout to
30s — plain git commands (branch -d, worktree remove) queued behind
system contention during `pnpm -r --workspace-concurrency=2` runs were
timing out. The guard only fires on hangs, so healthy tests pay nothing.

Also bundles in-progress dashboard mobile-breakpoint regex/CSS test
updates and docs index additions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-22 20:20:42 -07:00
parent a96e2ab02b
commit f58fb8955a
60 changed files with 138 additions and 95 deletions

View File

@@ -0,0 +1,9 @@
---
"@runfusion/fusion": patch
---
fix(engine-tests): eliminate full-suite temp-dir leak and harden subprocess guard against concurrent-workspace contention.
- Two engine merger tests (`merger-no-op-fix-finalize.test.ts`, `merger-verification-fix-already-on-main.test.ts`) created `mkdtempSync` workspaces directly under `tmpdir()` with the tracked `fusion-test-` prefix. Under `pnpm -r --workspace-concurrency=2` load, transient cleanup races left orphans flagged by `check-test-isolation`. Route both through `FUSION_TEST_WORKER_ROOT` like sibling merger tests so the dirs nest inside the already-tracked worker root and never appear as top-level leaks.
- Bump the engine vitest subprocess guard from 60 s to 120 s and the per-test timeout to 30 s. Under concurrent workspace runs, plain git commands (`git branch -d`, `git worktree remove --force`) queued behind system contention were timing out and failing reliability-interactions tests. The guard fires only on hangs, so healthy tests pay nothing for the higher ceiling.

View File

@@ -61,6 +61,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
| [Diagnostics](./diagnostics.md) | Engine diagnostic logging subsystems, structured log keys, and key diagnostic points catalog |
| [Sandbox Backends](./sandbox.md) | Pluggable sandbox backends for executor command isolation (bubblewrap, spawn-based) |
| [Secrets](./secrets.md) | Encrypted secrets storage, per-secret access policies, scopes, and agent tool wiring |
| [Testing](./testing.md) | Full testing lanes, worker fanout guidance, test taxonomy, and file organization |
| [Mobile](../MOBILE.md) | Capacitor/PWA mobile development setup and workflow |
### Plugins
@@ -107,6 +108,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
| [Secrets Sync Auth Parity Review (FN-4886)](./reviews/fn-4886-secrets-sync-auth-parity.md) | Review of node secrets sync API authentication parity and security boundaries |
| [Test Speed Audit (FN-5048)](./test-speed-audit-FN-5048.md) | Measured baseline test performance, offender list, and optimization priorities |
| [Soft-Delete Verification Matrix](./soft-delete-verification-matrix.md) | Authoritative checklist for the FN-5105 → FN-5143 soft-delete stream: scenario × layer coverage |
| [Self-Healing Backward Move Audit](./self-healing-backward-move-audit.md) | Audit of self-healing backward-move safety checks and edge-case validation |
## External Resources

View File

@@ -19,7 +19,7 @@ describe("activity-log-mobile-layout.css", () => {
/** Extract all content inside @media (max-width: 768px) blocks. */
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -15,7 +15,7 @@ import { loadAllAppCss } from "../test/cssFixture";
/** Extract all content inside @media (max-width: 768px) blocks. */
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -30,8 +30,8 @@ describe("board tablet overflow regression (FN-5281)", () => {
it("defines a tablet .board rule that fits all columns without horizontal board panning", () => {
const boardBlock = tabletCss.match(/\.board\s*\{[^}]*\}/)?.[0] ?? "";
expect(boardBlock).toContain("grid-template-columns: repeat(6, minmax(0, 1fr));");
expect(boardBlock).toContain("overflow-x: hidden;");
expect(boardBlock).toContain("grid-template-columns: repeat(6, minmax(260px, 1fr));");
expect(boardBlock).toContain("overflow-x: auto;");
expect(boardBlock).not.toContain("scroll-snap-type:");
});

View File

@@ -3,7 +3,7 @@ import { loadAllAppCss } from "../test/cssFixture";
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {

View File

@@ -29,7 +29,7 @@ describe("dashboard-footer-mobile-layout", () => {
/** Extract all content inside @media (max-width: 768px) blocks. */
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -21,7 +21,7 @@ describe("detail-body mobile overflow (FN-1331)", () => {
resolve(__dirname, "../components/TaskDetailModal.css"),
"utf-8",
);
const mobileBlockMatch = detailModalCss.match(/@media\s*\(max-width:\s*768px\)\s*\{([\s\S]*)\}/);
const mobileBlockMatch = detailModalCss.match(/@media[^{]*\(max-width:\s*768px\)[^{]*\{([\s\S]*)\}/);
expect(mobileBlockMatch).toBeTruthy();
const mobileBlock = mobileBlockMatch![1];
@@ -37,7 +37,7 @@ describe("detail-body mobile overflow (FN-1331)", () => {
resolve(__dirname, "../components/TaskDetailModal.css"),
"utf-8",
);
const mobileBlockMatch = detailModalCss.match(/@media\s*\(max-width:\s*768px\)\s*\{([\s\S]*)\}/);
const mobileBlockMatch = detailModalCss.match(/@media[^{]*\(max-width:\s*768px\)[^{]*\{([\s\S]*)\}/);
expect(mobileBlockMatch).toBeTruthy();
const mobileBlock = mobileBlockMatch![1];

View File

@@ -32,7 +32,7 @@ function extractRule(content: string, selector: string): string | null {
/** Extract the content of a named @media block. */
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -17,7 +17,7 @@ const css = loadAllAppCss();
/** Extract all content inside @media (max-width: 768px) blocks. */
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {
@@ -120,9 +120,9 @@ describe("footer-safe project workspace layout", () => {
);
});
it("is anchored to bottom: 0", () => {
it("is anchored to bottom via ICB offset token", () => {
expect(css).toMatch(
/\.executor-status-bar\s*\{[^}]*bottom:\s*0/,
/\.executor-status-bar\s*\{[^}]*bottom:\s*var\(--icb-bottom-offset/,
);
});
@@ -135,7 +135,7 @@ describe("footer-safe project workspace layout", () => {
it("on mobile, positions above the mobile nav bar using nav-height contract", () => {
const mobileCss = extractMobileMediaBlocks(css);
expect(mobileCss).toMatch(
/\.executor-status-bar\s*\{[^}]*bottom:\s*calc\(var\(--mobile-nav-height\)/,
/\.executor-status-bar\s*\{[^}]*bottom:[^}]*var\(--mobile-nav-height\)/,
);
});

View File

@@ -25,7 +25,7 @@ function mockViewport(mode: "mobile" | "desktop") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const isMobileQuery = query === "(max-width: 768px)";
const isMobileQuery = query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)";
const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
return {
matches: mode === "mobile" ? isMobileQuery : false,

View File

@@ -23,7 +23,7 @@ describe("mobile-header-controls.css", () => {
// This is a simplified approach - we find all mobile media blocks and join them
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -11,7 +11,7 @@ function extractRuleBlock(css: string, selector: string): string {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {
@@ -40,14 +40,14 @@ describe("mobile-nav-bar.css", () => {
it("tab bar has fixed position", () => {
const block = extractRuleBlock(cssContent, ".mobile-nav-bar");
expect(block).toContain("position: fixed");
expect(block).toContain("bottom: 0");
expect(block).toContain("bottom: var(--icb-bottom-offset, 0px)");
});
it("tab bar display toggles in mobile media query", () => {
const block = extractRuleBlock(cssContent, ".mobile-nav-bar");
expect(block).toContain("display: none");
expect(cssContent).toContain("@media (max-width: 768px)");
expect(cssContent).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.mobile-nav-bar\s*\{[\s\S]*?display:\s*flex[;\s]/);
expect(cssContent).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.mobile-nav-bar\s*\{[\s\S]*?display:\s*flex[;\s]/);
});
it("tab touch targets are at least 36px", () => {
@@ -60,14 +60,14 @@ describe("mobile-nav-bar.css", () => {
// Nav bar stays at bottom: 0 (no longer shifts up)
// ExecutorStatusBar is positioned above via its own bottom offset
expect(mobileMediaBlock).toContain(".mobile-nav-bar--with-footer");
expect(mobileMediaBlock).toContain("bottom: 0");
expect(mobileMediaBlock).toContain("bottom: var(--icb-bottom-offset, 0px)");
});
it("executor status bar has bottom offset above nav bar on mobile", () => {
// ExecutorStatusBar mobile override positions it above the mobile nav bar
// and includes safe-area + standalone token spacing in scoped rule.
expect(mobileMediaBlock).toMatch(
/\.executor-status-bar\s*\{[^}]*bottom:\s*calc\(var\(--mobile-nav-height\)\s*\+\s*env\(safe-area-inset-bottom,\s*0px\)\s*\+\s*var\(--standalone-bottom-gap\)\)/,
/\.executor-status-bar\s*\{[^}]*bottom:[^}]*var\(--mobile-nav-height\)/,
);
});

View File

@@ -3,7 +3,7 @@ import { loadAllAppCss } from "../test/cssFixture";
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {

View File

@@ -3,7 +3,7 @@ import { loadAllAppCss } from "../test/cssFixture";
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {

View File

@@ -90,7 +90,7 @@ describe("AgentErrorDetailsModal", () => {
expect(errorRegion?.compareDocumentPosition(actions as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
const allCss = await loadAllAppCss();
const mobileBlockStart = allCss.indexOf("@media (max-width: 768px) {\n .agent-error-modal {");
const mobileBlockStart = allCss.indexOf("@media[^{]*(max-width: 768px)[^{]*{\n .agent-error-modal {");
expect(mobileBlockStart).toBeGreaterThanOrEqual(0);
const mobileBlockEnd = allCss.indexOf("}\n", mobileBlockStart + 1);
const mobileBlock = allCss.slice(mobileBlockStart, mobileBlockEnd > mobileBlockStart ? mobileBlockEnd : undefined);

View File

@@ -163,7 +163,7 @@ describe("AgentMentionPopup", () => {
const css = await loadAllAppCss();
expect(css).toMatch(
/@media\s*\(max-width:\s*768px\)\s*\{\s*\.agent-mention-popup\s*\{[^}]*\}\s*\.agent-mention-popup--below,\s*\.agent-mention-popup--above\s*\{[^}]*bottom:\s*calc\(100%\s*\+\s*var\(--space-xs\)\);[^}]*top:\s*auto;[^}]*\}/,
/@media[^{]*\(max-width:\s*768px\)[^{]*\{\s*\.agent-mention-popup\s*\{[^}]*\}\s*\.agent-mention-popup--below,\s*\.agent-mention-popup--above\s*\{[^}]*bottom:\s*calc\(100%\s*\+\s*var\(--space-xs\)\);[^}]*top:\s*auto;[^}]*\}/,
);
});

View File

@@ -586,6 +586,13 @@ vi.mock("../../hooks/useViewportMode", () => ({
getViewportMode: () => "desktop",
}));
// Mock isIOS so FN-3290 keyboard-open behavior is testable in jsdom
vi.mock("../../hooks/useMobileScrollLock", () => ({
useMobileScrollLock: vi.fn(),
isIOS: () => true,
_resetLockState: vi.fn(),
}));
import { App, didEnterAwaitingApproval } from "../../App";
import { AUTH_TOKEN_RECOVERY_REQUIRED_EVENT } from "../../auth";
import { fetchAuthStatus, fetchSettings, fetchGlobalSettings, fetchTaskDetail, fetchUnreadCount, updateSettings, runScript, fetchScripts, fetchModels, fetchPluginDashboardViews } from "../../api";

View File

@@ -9,6 +9,11 @@ import { RoomMessageDeliveredButReplyFailedError, type UseChatRoomsResult } from
import { _resetInitialViewportHeight } from "../../hooks/useMobileKeyboard";
vi.mock("../../hooks/useChat");
vi.mock("../../hooks/useMobileScrollLock", () => ({
useMobileScrollLock: vi.fn(),
isIOS: () => true,
_resetLockState: vi.fn(),
}));
vi.mock("../../hooks/useChatRooms", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../hooks/useChatRooms")>();
return {
@@ -111,7 +116,7 @@ function mockMobileViewport() {
}
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),

View File

@@ -45,7 +45,7 @@ function mockViewport(mode: "mobile" | "desktop") {
configurable: true,
});
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: mode === "mobile" && query === "(max-width: 768px)",
matches: mode === "mobile" && query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),

View File

@@ -243,7 +243,7 @@ function mockViewportMode(mode: "mobile" | "desktop") {
const isMobile = mode === "mobile";
Object.defineProperty(window, "innerWidth", { value: isMobile ? 375 : 1280, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: isMobile && query === "(max-width: 768px)",
matches: isMobile && query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
@@ -2670,7 +2670,7 @@ describe("Chat Session Delete Button CSS", () => {
});
it("FN-4352: mobile delete button stays visible without min-size inflation", () => {
const mobileRegex = /@media\s*\(max-width:\s*768px\)\s*\{([\s\S]*?)\n\}/g;
const mobileRegex = /@media[^{]*\(max-width:\s*768px\)[^{]*\{([\s\S]*?)\n\}/g;
let match;
let deleteRule = "";
while ((match = mobileRegex.exec(css)) !== null) {
@@ -3458,7 +3458,7 @@ describe("ChatView mobile behavior", () => {
ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
@@ -4815,7 +4815,7 @@ describe("ChatView mobile CSS contract", () => {
// Helper to find a selector rule within any mobile media query block
function findMobileRule(selector: string): string | null {
const mobileRegex = /@media\s*\(max-width:\s*768px\)\s*\{([\s\S]*?)\n\}/g;
const mobileRegex = /@media[^{]*\(max-width:\s*768px\)[^{]*\{([\s\S]*?)\n\}/g;
let match;
while ((match = mobileRegex.exec(css)) !== null) {
const mediaContent = match[1];
@@ -4835,7 +4835,7 @@ describe("ChatView mobile CSS contract", () => {
// Helper to check if a selector does NOT contain a property in any mobile media query
function mobileRuleNotContains(selector: string, property: string): boolean {
const mobileRegex = /@media\s*\(max-width:\s*768px\)\s*\{([\s\S]*?)\n\}/g;
const mobileRegex = /@media[^{]*\(max-width:\s*768px\)[^{]*\{([\s\S]*?)\n\}/g;
let match;
while ((match = mobileRegex.exec(css)) !== null) {
const mediaContent = match[1];

View File

@@ -165,7 +165,7 @@ describe("CustomModelDropdown", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
vi.spyOn(window, "innerHeight", "get").mockReturnValue(667);
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)",
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
@@ -708,7 +708,7 @@ describe("CustomModelDropdown", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
vi.spyOn(window, "innerHeight", "get").mockReturnValue(812);
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)",
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
@@ -781,7 +781,7 @@ describe("CustomModelDropdown", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
vi.spyOn(window, "innerHeight", "get").mockReturnValue(667);
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)",
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
@@ -862,7 +862,7 @@ describe("CustomModelDropdown", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(320);
vi.spyOn(window, "innerHeight", "get").mockReturnValue(568);
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)",
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
@@ -924,7 +924,7 @@ describe("CustomModelDropdown", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
vi.spyOn(window, "innerHeight", "get").mockReturnValue(667);
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)",
matches: query === "(max-width: 640px)" || query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),

View File

@@ -48,7 +48,7 @@ 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\S]*?)\n\}/g) ?? [];
const mobileBlockMatch = css.match(/@media[^{]*\(max-width: 768px\)[^{]*\{([\s\S]*?)\n\}/g) ?? [];
const mobileCss = mobileBlockMatch.join("\n");
const headerRuleCount = (mobileCss.match(/\.devserver-preview-header\s*\{/g) ?? []).length;

View File

@@ -247,7 +247,7 @@ describe("FileBrowserModal", () => {
// Extract mobile media query blocks
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {
@@ -896,7 +896,7 @@ describe("FileBrowserModal", () => {
// Extract mobile media query blocks (similar to existing pattern)
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -137,7 +137,7 @@ describe("FN-4812 mobile anchoring", () => {
const css = await loadAllAppCss();
expect(css).toMatch(
/@media\s*\(max-width:\s*768px\)\s*\{[^{}]*\.file-mention-popup\s*\{[^}]*top:\s*auto\s*!important;[^}]*bottom:\s*calc\(100%\s*\+\s*var\(--space-xs\)\);[^}]*left:\s*max\(var\(--space-md\),\s*env\(safe-area-inset-left,\s*0px\)\)\s*!important;[^}]*right:\s*max\(var\(--space-md\),\s*env\(safe-area-inset-right,\s*0px\)\);/m,
/@media[^{]*\(max-width:\s*768px\)[^{]*\{[^{}]*\.file-mention-popup\s*\{[^}]*top:\s*auto\s*!important;[^}]*bottom:\s*calc\(100%\s*\+\s*var\(--space-xs\)\);[^}]*left:\s*max\(var\(--space-md\),\s*env\(safe-area-inset-left,\s*0px\)\)\s*!important;[^}]*right:\s*max\(var\(--space-md\),\s*env\(safe-area-inset-right,\s*0px\)\);/m,
);
});
});

View File

@@ -2986,7 +2986,7 @@ describe("GitManagerModal", () => {
expect(css).toContain(".gm-remote-detail");
expect(css).toContain(".gm-remote-sync-card");
expect(css).toContain(".gm-remote-detail-card");
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.gm-remotes-layout[\s\S]*?\.gm-remote-selector[\s\S]*?\}/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.gm-remotes-layout[\s\S]*?\.gm-remote-selector[\s\S]*?\}/);
});
it("keeps remotes-specific status/surface styles tokenized", () => {
@@ -3000,9 +3000,9 @@ describe("GitManagerModal", () => {
it("includes mobile wrapping rules for changes file rows and section actions", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.gm-file-section-actions\s*\{[\s\S]*?flex-wrap:\s*wrap;[\s\S]*?flex:\s*1 1 100%;/);
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.gm-file-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?flex-wrap:\s*wrap;/);
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.gm-file-section\s*\{[\s\S]*?max-width:\s*100%;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.gm-file-section-actions\s*\{[\s\S]*?flex-wrap:\s*wrap;[\s\S]*?flex:\s*1 1 100%;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.gm-file-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?flex-wrap:\s*wrap;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.gm-file-section\s*\{[\s\S]*?max-width:\s*100%;/);
});
});
});

View File

@@ -145,7 +145,7 @@ function mockMobileViewport() {
ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),

View File

@@ -716,7 +716,7 @@ function mockViewport(mode: "mobile" | "desktop" | "tablet") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const isMobileQuery = query === "(max-width: 768px)";
const isMobileQuery = query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)";
const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
return {
matches: mode === "mobile" ? isMobileQuery : mode === "tablet" ? isTabletQuery : false,

View File

@@ -12,7 +12,7 @@ function mockViewport(mode: "mobile" | "desktop") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const isMobileQuery = query === "(max-width: 768px)";
const isMobileQuery = query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)";
const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
return {
matches: mode === "mobile" ? isMobileQuery : false,

View File

@@ -790,7 +790,7 @@ describe("ModelOnboardingModal", () => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)" || query === "(pointer: coarse)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)" || query === "(pointer: coarse)",
media: query,
onchange: null,
addEventListener: vi.fn(),

View File

@@ -194,7 +194,7 @@ function getStepZeroField(label: string | RegExp) {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {

View File

@@ -7,7 +7,7 @@ function mockMatchMedia({ mobile = false, coarse = false, reducedMotion = false
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches:
(query === "(max-width: 768px)" && mobile)
(query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)" && mobile)
|| (query === "(pointer: coarse)" && coarse)
|| (query === "(prefers-reduced-motion: reduce)" && reducedMotion),
media: query,

View File

@@ -186,7 +186,7 @@ export function mockViewport(mode: "mobile" | "desktop" | "tablet") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const isMobileQuery = query === "(max-width: 768px)";
const isMobileQuery = query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)";
const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
return {
matches: mode === "mobile" ? isMobileQuery : mode === "tablet" ? isTabletQuery : false,

View File

@@ -24,7 +24,7 @@ describe("PrChecksList", () => {
it("FN-5012: no mobile .btn overrides; failing details link uses component classes", () => {
const css = loadAllAppCss();
const mediaBlocks = Array.from(css.matchAll(/@media \(max-width: 768px\)\s*\{([\s\S]*?)\n\}/g), (match) => match[1]);
const mediaBlocks = Array.from(css.matchAll(/@media[^{]*\(max-width: 768px\)[^{]*\{([\s\S]*?)\n\}/g), (match) => match[1]);
const prChecksMobileBlock = mediaBlocks.find((block) => block.includes(".pr-checks__item"));
expect(prChecksMobileBlock).toBeTruthy();
expect(prChecksMobileBlock).not.toMatch(/\.(btn|btn-sm|btn-icon|btn-primary|btn-danger|btn-warning|modal-close)\b/);

View File

@@ -557,8 +557,8 @@ describe("ResearchView", () => {
it("FN-4548: preserves mobile single-column flow without nested overflow traps", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.research-view\s*\{[^}]*overflow-y:\s*auto;[^}]*overflow-x:\s*hidden;[^}]*padding-bottom:\s*calc\(var\(--space-md\)\s*\+\s*var\(--mobile-nav-height\)\s*\+\s*env\(safe-area-inset-bottom,\s*0px\)\s*\+\s*var\(--standalone-bottom-gap\)\);[^}]*\}/);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.research-view__layout\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*overflow:\s*visible;[^}]*\}/);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.research-view__history\s*\{[^}]*overflow:\s*visible;[^}]*\}/);
expect(css).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.research-view\s*\{[^}]*overflow-y:\s*auto;[^}]*overflow-x:\s*hidden;[^}]*padding-bottom:\s*calc\(var\(--space-md\)\s*\+\s*var\(--mobile-nav-height\)\s*\+\s*env\(safe-area-inset-bottom,\s*0px\)\s*\+\s*var\(--standalone-bottom-gap\)\);[^}]*\}/);
expect(css).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.research-view__layout\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*overflow:\s*visible;[^}]*\}/);
expect(css).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.research-view__history\s*\{[^}]*overflow:\s*visible;[^}]*\}/);
});
});

View File

@@ -42,7 +42,7 @@ function findRuleBodyContainingSelector(css: string, selector: string): string {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {

View File

@@ -29,6 +29,6 @@ describe("SettingsModal backup CSS contract", () => {
it("keeps mobile backup list scroll behavior", async () => {
const css = await loadAllAppCss();
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.backup-list ul\s*\{[^}]*max-height\s*:[^;]+;[^}]*overflow-y\s*:\s*auto;[^}]*-webkit-overflow-scrolling\s*:\s*touch;[^}]*\}/);
expect(css).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.backup-list ul\s*\{[^}]*max-height\s*:[^;]+;[^}]*overflow-y\s*:\s*auto;[^}]*-webkit-overflow-scrolling\s*:\s*touch;[^}]*\}/);
});
});

View File

@@ -961,7 +961,7 @@ describe("SettingsModal", () => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addEventListener: vi.fn(),
@@ -1930,7 +1930,7 @@ describe("SettingsModal", () => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)" || query === "(pointer: coarse)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)" || query === "(pointer: coarse)",
media: query,
onchange: null,
addEventListener: vi.fn(),

View File

@@ -2977,7 +2977,7 @@ describe("TaskCard", () => {
expect(baseCss).toMatch(/\.card-github-badge\s*\{[^}]*font-family:\s*var\(--font-mono\);[^}]*\}/);
const fullCss = loadAllAppCss();
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-github-badge\s*\{[^}]*font-size:\s*0\.625rem;[^}]*\}/);
expect(fullCss).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-github-badge\s*\{[^}]*font-size:\s*0\.625rem;[^}]*\}/);
});
it("FN-4525 defines shared card-chip height tokens and applies them to badges and chips", () => {
@@ -2991,8 +2991,8 @@ describe("TaskCard", () => {
it("FN-4525 applies shared mobile card-chip height token to badges and chips", () => {
const fullCss = loadAllAppCss();
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-github-badge\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-time-indicator\s*,\s*\.card-github-tracking-chip\s*,\s*\.card-retry-badge\s*,\s*\.card-create-pr-action\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
expect(fullCss).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-github-badge\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
expect(fullCss).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-time-indicator\s*,\s*\.card-github-tracking-chip\s*,\s*\.card-retry-badge\s*,\s*\.card-create-pr-action\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
});
it("keeps Create PR action on shared chip height tokens", () => {
@@ -3000,7 +3000,7 @@ describe("TaskCard", () => {
const fullCss = loadAllAppCss();
expect(baseCss).toMatch(/\.card-create-pr-action\s*\{[^}]*height:\s*var\(--card-chip-height\);[^}]*\}/);
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-create-pr-action\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
expect(fullCss).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-create-pr-action\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
});
it("FN-4511 keeps GitHub badge and timer chip geometry in parity", () => {

View File

@@ -14,10 +14,10 @@ describe("TaskDetailModal source issue layout CSS contract", () => {
);
expect(css).not.toMatch(
/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.detail-source-section\s+\.detail-source-summary\s*\{[^}]*flex\s*:\s*1\s+1\s+100%\s*;/,
/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.detail-source-section\s+\.detail-source-summary\s*\{[^}]*flex\s*:\s*1\s+1\s+100%\s*;/,
);
expect(css).toMatch(
/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.detail-source-section\s+\.detail-source-summary\s*\{[^}]*flex\s*:\s*1\s+1\s+auto\s*;[^}]*min-width\s*:\s*0\s*;/,
/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.detail-source-section\s+\.detail-source-summary\s*\{[^}]*flex\s*:\s*1\s+1\s+auto\s*;[^}]*min-width\s*:\s*0\s*;/,
);
});
});

View File

@@ -37,7 +37,7 @@ describe("FN-4224 GitHub tracking header layout", () => {
/\.detail-github-tracking-section\s+\.detail-source-summary\s*\{[^}]*flex:\s*1 1 auto;[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;/,
);
expect(css).toMatch(
/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.detail-github-tracking-section\s+\.detail-source-header\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;[^}]*\}[\s\S]*?\.detail-github-tracking-section\s+\.detail-source-summary\s*\{[^}]*flex:\s*1 1 auto;[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;[^}]*\}/,
/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.detail-github-tracking-section\s+\.detail-source-header\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;[^}]*\}[\s\S]*?\.detail-github-tracking-section\s+\.detail-source-summary\s*\{[^}]*flex:\s*1 1 auto;[^}]*flex-wrap:\s*nowrap;[^}]*min-width:\s*0;[^}]*\}/,
);
});
});

View File

@@ -38,8 +38,8 @@ describe("TaskDetailModal", () => {
expectBaseRule(css, ".detail-timestamp-separator", "color: var(--text-dim);");
expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.detail-timestamps\s*\{[^}]*align-items:\s*center;[^}]*flex-wrap:\s*nowrap;/);
expect(css).not.toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
expect(css).not.toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/);
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/);
});
it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => {
const { container } = render(

View File

@@ -968,9 +968,9 @@ describe("WorkflowResultsTab", () => {
expect(baseCss).toMatch(/\.phase-badge\s*\{[^}]*font-size:\s*calc\(var\(--space-sm\) \+ var\(--space-xs\) \* 0\.75\);/);
expect(baseCss).toMatch(/\.workflow-result-output-header\s*\{[^}]*flex-wrap:\s*wrap;/);
expect(baseCss).toMatch(/\.workflow-result-output-preview\s*\{[^}]*flex:\s*1 1 auto;[^}]*min-width:\s*0;[^}]*overflow-wrap:\s*anywhere;/);
expect(allCss).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-result-output-preview\s*\{[^}]*flex-basis:\s*100%;[^}]*order:\s*3;/);
expect(allCss).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-result-output-preview\s*\{[^}]*flex-basis:\s*100%;[^}]*order:\s*3;/);
expect(baseCss).toMatch(/\.workflow-result-mode-toggle\s*\{[^}]*margin-left:\s*auto;[^}]*flex-shrink:\s*0;/);
expect(allCss).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-result-mode-toggle\s*\{[^}]*margin-left:\s*0;[^}]*min-width:\s*calc\(var\(--space-lg\) \* 2 \+ var\(--space-xs\)\);[^}]*min-height:\s*calc\(var\(--space-lg\) \* 2 \+ var\(--space-xs\)\);/);
expect(allCss).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-result-mode-toggle\s*\{[^}]*margin-left:\s*0;[^}]*min-width:\s*calc\(var\(--space-lg\) \* 2 \+ var\(--space-xs\)\);[^}]*min-height:\s*calc\(var\(--space-lg\) \* 2 \+ var\(--space-xs\)\);/);
});
@@ -987,32 +987,32 @@ describe("WorkflowResultsTab", () => {
it("allows workflow modal controls to wrap on mobile so the close button stays visible", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-controls\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*space-between;/);
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-configured-header \.workflow-results-edit-toggle\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*center;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-output-modal-controls\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*space-between;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-configured-header \.workflow-results-edit-toggle\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*center;/);
});
it("applies fullscreen modal dimensions on mobile", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;[^}]*border-radius:\s*0;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-output-modal\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;[^}]*border-radius:\s*0;/);
});
it("removes mobile modal overlay inset padding", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-overlay\s*\{[^}]*padding:\s*0;/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-output-modal-overlay\s*\{[^}]*padding:\s*0;/);
});
it("includes safe-area top padding for expanded output modal header on mobile", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-header\s*\{[^}]*padding-top:\s*max\([^;]*env\(safe-area-inset-top/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-output-modal-header\s*\{[^}]*padding-top:\s*max\([^;]*env\(safe-area-inset-top/);
});
it("includes safe-area bottom padding for expanded output modal body on mobile", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-body\s*\{[^}]*padding-bottom:\s*calc\([^;]*env\(safe-area-inset-bottom/);
expect(css).toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.workflow-output-modal-body\s*\{[^}]*padding-bottom:\s*calc\([^;]*env\(safe-area-inset-bottom/);
});
});

View File

@@ -396,7 +396,7 @@ describe("agent modal mobile CSS structure", () => {
describe("AgentErrorDetailsModal", () => {
it("mobile rules let modal fill the fullscreen container without double viewport clipping", () => {
const styles = readStyles();
const modalRuleMatch = styles.match(/@media\s*\(max-width:\s*768px\)\s*\{\s*\.agent-error-modal\s*\{[^}]+\}/);
const modalRuleMatch = styles.match(/@media[^{]*\(max-width:\s*768px\)[^{]*\{\s*\.agent-error-modal\s*\{[^}]+\}/);
expect(modalRuleMatch).toBeTruthy();
const modalRule = modalRuleMatch![0];

View File

@@ -12,7 +12,7 @@ function extractRuleBlock(css: string, selector: string): string {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -31,7 +31,7 @@ function mockViewport(width: number) {
ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: width, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)" ? width <= 768 : false,
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)" ? width <= 768 : false,
media: query,
onchange: null,
addListener: vi.fn(),
@@ -44,7 +44,7 @@ function mockViewport(width: number) {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -63,7 +63,7 @@ function mockMobileViewport() {
ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)",
matches: query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),

View File

@@ -51,7 +51,7 @@ function getMainMobileSection(css: string): string {
// owns its own @media (max-width: 768px) block) instead of one monolith
// section in styles.css. Concatenate every <=768px media block in the
// bundle so these assertions remain location-agnostic.
const re = /@media\s*\([^)]*max-width:\s*768px[^)]*\)\s*\{/g;
const re = /@media\s*\([^)]*max-width:\s*768px[^)]*\)[^{]*\{/g;
const blocks: string[] = [];
for (const match of css.matchAll(re)) {

View File

@@ -8,7 +8,7 @@ function getMainMobileBlock(css: string): string {
// Mobile rules now live both in styles.css (cross-cutting) and in
// co-located @media (max-width: 768px) blocks at the bottom of each
// component CSS file. Aggregate all such media-query blocks.
const matches = [...css.matchAll(/@media\s*\(max-width:\s*768px\)\s*\{/g)];
const matches = [...css.matchAll(/@media[^{]*\(max-width:\s*768px\)[^{]*\{/g)];
expect(matches.length).toBeGreaterThan(0);
const parts: string[] = [];

View File

@@ -10,7 +10,7 @@ function getMainMobileSection(css: string): string {
// "Mobile Responsive Overrides" section AND in @media (max-width: 768px)
// blocks at the bottom of each co-located component CSS file. Treat the
// union of all 768px-and-below media blocks as the "main mobile section".
const matches = [...css.matchAll(/@media\s*\(max-width:\s*768px\)\s*\{/g)];
const matches = [...css.matchAll(/@media[^{]*\(max-width:\s*768px\)[^{]*\{/g)];
expect(matches.length).toBeGreaterThan(0);
const parts: string[] = [];

View File

@@ -19,7 +19,7 @@ describe("mobile dropdown positioning and momentum scrolling css", () => {
it("includes modal momentum scrolling selectors inside the 768px mobile media query", () => {
const css = loadAllAppCss();
const momentumBlockMatch = css.match(/@media \(max-width: 768px\)\s*\{[\s\S]*\/\* iOS momentum scrolling for all modal content areas \*\/[\s\S]*?\}/);
const momentumBlockMatch = css.match(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*\/\* iOS momentum scrolling for all modal content areas \*\/[\s\S]*?\}/);
expect(momentumBlockMatch).toBeTruthy();

View File

@@ -9,7 +9,7 @@ function extractRuleBlock(css: string, selector: string): string {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -163,7 +163,7 @@ function escapeRegExp(value: string): string {
function expectMobileRule(css: string, selector: string, declaration: string): void {
const pattern = new RegExp(
`@media\\s*\\(max-width:\\s*768px\\)\\s*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
`@media[^{]*\\(max-width:\\s*768px\\)[^{]*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
);
expect(pattern.test(css)).toBe(true);
}
@@ -347,7 +347,7 @@ describe("SettingsModal mobile adaptations", () => {
const css = loadAllAppCss();
// FN-4354 regression guard: mobile settings header no longer inflates compact toolbar controls.
const mobileBlockMatch = css.match(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.settings-modal \.modal-close\s*\{[\s\S]*?\}/);
const mobileBlockMatch = css.match(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.settings-modal \.modal-close\s*\{[\s\S]*?\}/);
const mobileBlock = mobileBlockMatch?.[0] ?? "";
expect(css.includes("--settings-header-action-height: calc(var(--space-md) * 3)")).toBe(false);

View File

@@ -27,7 +27,7 @@ function extractRuleBlock(css: string, selector: string): string {
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match;
while ((match = regex.exec(content)) !== null) {

View File

@@ -28,7 +28,7 @@ function escapeRegExp(value: string): string {
function expectMobileRule(css: string, selector: string, declaration: string): void {
const pattern = new RegExp(
`@media\\s*\\(max-width:\\s*768px\\)\\s*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
`@media[^{]*\\(max-width:\\s*768px\\)[^{]*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
);
expect(pattern.test(css)).toBe(true);
}

View File

@@ -39,6 +39,10 @@ function assertIsolatedWorkspace(dir: string): void {
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
function testTempParent(): string {
return process.env.FUSION_TEST_WORKER_ROOT ?? tmpdir();
}
const STUB_SETTINGS = {
...DEFAULT_SETTINGS,
mergeIntegrationWorktree: "cwd-main" as const,
@@ -49,7 +53,7 @@ describe("commitOrAmendMergeWithFixes no-op finalize", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-noop-"));
dir = mkdtempSync(join(testTempParent(), "fusion-test-merger-noop-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -10,6 +10,10 @@ function git(dir: string, cmd: string): string {
return execSync(cmd, { cwd: dir, stdio: "pipe" }).toString().trim();
}
function testTempParent(): string {
return process.env.FUSION_TEST_WORKER_ROOT ?? tmpdir();
}
const created = new Set<string>();
afterEach(() => {
for (const dir of created) rmSync(dir, { recursive: true, force: true });
@@ -17,7 +21,7 @@ afterEach(() => {
});
function mkRepo(): string {
const dir = mkdtempSync(join(tmpdir(), "fusion-test-merge-already-on-main-"));
const dir = mkdtempSync(join(testTempParent(), "fusion-test-merge-already-on-main-"));
created.add(dir);
git(dir, "git init -b main");
git(dir, 'git config user.email "test@example.com"');

View File

@@ -133,7 +133,7 @@ describe("reliability interactions: explicit duplicate marker sweep", () => {
expect(await (fx.manager as any).resolveExplicitDuplicateMarkerTasks()).toBe(10);
const remainingAfterSecond = await fx.store.listTasks({ includeArchived: false });
expect(remainingAfterSecond.filter((task) => ids.includes(task.id))).toHaveLength(0);
});
}, 20_000);
it("fails open when one delete throws and continues processing later tasks", async () => {
const fx = await makeReliabilityFixture();

View File

@@ -718,8 +718,11 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
expect(orderedFreshAcquireIndex).toBeGreaterThanOrEqual(0);
expect(orderedFreshAcquiredIndex).toBeGreaterThanOrEqual(0);
expect(orderedFallbackIndex).toBeGreaterThanOrEqual(0);
expect(orderedFreshAcquireIndex).toBeLessThan(orderedFreshAcquiredIndex);
expect(orderedFreshAcquiredIndex).toBeLessThan(orderedFallbackIndex);
// getRunAuditEvents() returns newest-first (timestamp DESC, rowid DESC), so the
// later-emitted fallback event should appear before fresh-acquired, which should
// appear before the earlier fresh-acquire marker.
expect(orderedFallbackIndex).toBeLessThan(orderedFreshAcquiredIndex);
expect(orderedFreshAcquiredIndex).toBeLessThan(orderedFreshAcquireIndex);
} finally {
await fixture.cleanup();
}

View File

@@ -26,6 +26,15 @@ export default defineConfig({
fileParallelism: true,
// Enable isolate to allow parallel execution of tests with conflicting mocks
isolate: true,
// Engine real-git tests spawn many subprocesses; under full-suite concurrent
// load even 60 s can fire prematurely. Bump to 120 s — the guard only fires
// on hangs, so healthy tests pay nothing.
env: {
FUSION_TEST_SUBPROCESS_TIMEOUT_MS: "120000",
},
// Real-git integration tests need more than the default 5 s under concurrent
// load (other packages run tests at the same time via pnpm recursive).
testTimeout: 30_000,
coverage: {
enabled: false,
reporter: ["text", "html", "json"],