From b5a8199472e9c8a0d3ea90e44b0005fc3894133e Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Tue, 14 Apr 2026 17:55:38 +0200 Subject: [PATCH] Reduce default Vitest batch size to 5, enforce single-threaded execution, and refactor unit test wait conditions: - Adjusted `run-vitest-unit-batches.mjs` to lower default batch size from 10 to 5 and enforce single-threaded execution for more predictable test behavior. - Updated GitHub workflows to align with new batch size defaults. - Introduced `waitForCondition` helper in unit tests to replace arbitrary delays with predicate-based waiting. - Refactored `useEdgeGatewayTerminal` for improved session handling, including idle delay for shell polling and cleanup of request queuing logic. --- .github/workflows/tests.yml | 2 +- scripts/run-vitest-unit-batches.mjs | 33 +++++++++++++++----- src/composables/useEdgeGatewayTerminal.js | 24 ++++++-------- tests/unit/use-edge-gateway-terminal.spec.js | 22 ++++++++++--- 4 files changed, 53 insertions(+), 28 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bab7f5e2..f5745064 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: - name: Unit tests run: npm run test:unit env: - VITEST_BATCH_SIZE: 10 + VITEST_BATCH_SIZE: 5 e2e-smoke: if: github.event_name != 'schedule' diff --git a/scripts/run-vitest-unit-batches.mjs b/scripts/run-vitest-unit-batches.mjs index 974bd623..274086f0 100644 --- a/scripts/run-vitest-unit-batches.mjs +++ b/scripts/run-vitest-unit-batches.mjs @@ -6,7 +6,7 @@ const workingDirectory = process.cwd(); const vitestCliPath = path.join(workingDirectory, "node_modules", "vitest", "vitest.mjs"); const unitTestsRoot = path.join(workingDirectory, "tests", "unit"); const args = parseArgs(process.argv.slice(2)); -const batchSize = normalizePositiveInt(args.batchSize ?? process.env.VITEST_BATCH_SIZE, 10); +const batchSize = normalizePositiveInt(args.batchSize ?? process.env.VITEST_BATCH_SIZE, 5); let activeChild = null; let isShuttingDown = false; @@ -79,7 +79,7 @@ Usage: node scripts/run-vitest-unit-batches.mjs [spec-filter ...] [options] [-- ] Options: - --batch-size Spec files per batch. Default: 10 + --batch-size Spec files per batch. Default: 5 -h, --help Show help Examples: @@ -152,18 +152,35 @@ function formatDuration(durationMs) { return `${(durationMs / 1000).toFixed(1)}s`; } +function hasForwardedArg(name) { + return args.forwardedArgs.some((value) => value === name || value.startsWith(`${name}=`)); +} + async function runBatch(chunk, totalBatches) { console.log(`[vitest-batch] Running batch ${chunk.batch}/${totalBatches} with ${chunk.files.length} spec file(s).`); const startedAt = Date.now(); + const enforcedArgs = []; + + if (!hasForwardedArg("--maxWorkers")) { + enforcedArgs.push("--maxWorkers=1"); + } + + if (!hasForwardedArg("--fileParallelism") && !hasForwardedArg("--no-file-parallelism")) { + enforcedArgs.push("--no-file-parallelism"); + } return new Promise((resolve) => { - activeChild = spawn(process.execPath, [vitestCliPath, "run", ...chunk.files, ...args.forwardedArgs], { - cwd: workingDirectory, - env: process.env, - stdio: "inherit", - windowsHide: true, - }); + activeChild = spawn( + process.execPath, + [vitestCliPath, "run", ...chunk.files, ...enforcedArgs, ...args.forwardedArgs], + { + cwd: workingDirectory, + env: process.env, + stdio: "inherit", + windowsHide: true, + } + ); activeChild.on("close", (code) => { activeChild = null; diff --git a/src/composables/useEdgeGatewayTerminal.js b/src/composables/useEdgeGatewayTerminal.js index b7837b51..420e3dc1 100644 --- a/src/composables/useEdgeGatewayTerminal.js +++ b/src/composables/useEdgeGatewayTerminal.js @@ -11,6 +11,7 @@ export const EDGE_GATEWAY_TERMINAL_STATES = Object.freeze({ const CONNECTION_TIMEOUT_MS = 8000; const SHELL_EVENT_WAIT_SECONDS = 5; +const SHELL_POLL_IDLE_DELAY_MS = 250; const SHELL_POLL_RETRY_DELAY_MS = 1000; function defaultCopyText(value) { @@ -44,8 +45,7 @@ export function useEdgeGatewayTerminal({ createTerminal, createFitAddon, copyText = defaultCopyText, - resizeObserverFactory = (callback) => - typeof ResizeObserver === "function" ? new ResizeObserver(callback) : null, + resizeObserverFactory = (callback) => (typeof ResizeObserver === "function" ? new ResizeObserver(callback) : null), } = {}) { const connectionState = ref(EDGE_GATEWAY_TERMINAL_STATES.idle); const session = ref(null); @@ -176,12 +176,7 @@ export function useEdgeGatewayTerminal({ }; const sendInput = (data) => { - if ( - !inputEnabled.value || - !session.value?.id || - !activeGatewayId || - typeof sendSessionInput !== "function" - ) { + if (!inputEnabled.value || !session.value?.id || !activeGatewayId || typeof sendSessionInput !== "function") { return; } @@ -302,9 +297,7 @@ export function useEdgeGatewayTerminal({ } markClosed(terminationRequested ? "Sessionen blev lukket." : "Sessionen blev lukket af gateway-agenten."); - appendNotice( - `[Session lukket${payload.code !== undefined ? ` (exit ${payload.code})` : ""}]` - ); + appendNotice(`[Session lukket${payload.code !== undefined ? ` (exit ${payload.code})` : ""}]`); } }; @@ -356,6 +349,10 @@ export function useEdgeGatewayTerminal({ if (connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.closed) { return; } + + if (events.length === 0) { + await wait(SHELL_POLL_IDLE_DELAY_MS); + } } catch { if (pollSequence !== currentSequence) { return; @@ -436,10 +433,7 @@ export function useEdgeGatewayTerminal({ inputEnabled.value = false; statusDetail.value = "Lukker den aktive shell-session."; - queueRequest( - () => closeSession(activeGatewayId, session.value.id), - "Kunne ikke lukke gateway-shell." - ); + queueRequest(() => closeSession(activeGatewayId, session.value.id), "Kunne ikke lukke gateway-shell."); }; const copyOutput = async () => { diff --git a/tests/unit/use-edge-gateway-terminal.spec.js b/tests/unit/use-edge-gateway-terminal.spec.js index 22123a5a..baa5d005 100644 --- a/tests/unit/use-edge-gateway-terminal.spec.js +++ b/tests/unit/use-edge-gateway-terminal.spec.js @@ -148,6 +148,20 @@ function createShellApi(mode = "success") { }; } +async function waitForCondition(predicate, { timeoutMs = 1000, intervalMs = 10 } = {}) { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (predicate()) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + + throw new Error("Timed out waiting for condition."); +} + describe("useEdgeGatewayTerminal", () => { it("approves a shell session, polls output, sends input, and closes cleanly", async () => { const fakeTerminal = createFakeTerminal(); @@ -192,7 +206,7 @@ describe("useEdgeGatewayTerminal", () => { }), }); - await new Promise((resolve) => setTimeout(resolve, 10)); + await waitForCondition(() => composable.connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.connected); expect(composable.connectionState.value).toBe(EDGE_GATEWAY_TERMINAL_STATES.connected); expect(composable.outputBuffer.value).toContain("[Live shell forbundet]"); @@ -204,13 +218,13 @@ describe("useEdgeGatewayTerminal", () => { expect(shellApi.sendSessionResize).toHaveBeenCalled(); fakeTerminal.emitData("ls\r"); - await new Promise((resolve) => setTimeout(resolve, 10)); + await waitForCondition(() => composable.outputBuffer.value.includes("agent.mjs")); expect(shellApi.sendSessionInput).toHaveBeenCalledWith(701, 44, "ls\r"); expect(composable.outputBuffer.value).toContain("agent.mjs"); composable.terminateSession(); - await new Promise((resolve) => setTimeout(resolve, 10)); + await waitForCondition(() => composable.connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.closed); expect(shellApi.closeSession).toHaveBeenCalledWith(701, 44); expect(composable.connectionState.value).toBe(EDGE_GATEWAY_TERMINAL_STATES.closed); @@ -261,7 +275,7 @@ describe("useEdgeGatewayTerminal", () => { }), }); - await new Promise((resolve) => setTimeout(resolve, 10)); + await waitForCondition(() => composable.connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.error); expect(composable.connectionState.value).toBe(EDGE_GATEWAY_TERMINAL_STATES.error); expect(composable.inputEnabled.value).toBe(false);