feat(FN-3106): add draft planning workflow with createAiSession plugin API
This merge delivers the full draft planning feature (FN-3106) — API routes for planning subtasks, the AiSessionStore backend for draft sessions, a draft planning API client, auto-creation of planning drafts from user input, and corresponding UI polish in PlanningModeModal with accessibility-focused Fusion-Task-Id: FN-3106
This commit is contained in:
14
.changeset/dashboard-pty-flush-throttle.md
Normal file
14
.changeset/dashboard-pty-flush-throttle.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix dashboard freezing for several seconds while a Fusion agent runs a long verification command (e.g. `pnpm test`).
|
||||
|
||||
Root cause was in `runVerificationCommand`'s output capture (`packages/engine/src/run-verification-tool.ts`). The captured stdout/stderr buffers used a string-concat + re-encode pattern: once total output exceeded 200 KB, every subsequent line did `Buffer.from(buf.tail).subarray(...).toString("utf8")`, allocating and re-decoding the entire ~100 KB tail per line. A vitest run dumping 50k+ lines produced multiple GB of GC churn, which stalled the dashboard event loop in stop-the-world pauses (matching the symptom: occasional multi-second freezes with no CPU spike on the host).
|
||||
|
||||
The buffer is now stored as a chunk array; tail compaction runs only when accumulated size grows past 2× the cap, making per-line append amortized O(1). All 12 existing `run-verification-command` tests pass unchanged.
|
||||
|
||||
Two follow-on changes shipped in the same patch:
|
||||
|
||||
- **Embedded terminal PTY ingestion** (`packages/dashboard/src/terminal-service.ts`) had the same anti-pattern: `outputBuffer.slice(0, 4096)` + `outputBuffer.slice(4096)` on every 4 ms flush tick. Switched to a chunk array with O(1) drain. Throttle bumped from 4 ms to 16 ms (60 fps) and per-flush cap from 4 KB to 64 KB. This was not the cause of the user-reported freeze, but the same O(N²) hazard would surface under any flood from a terminal pane.
|
||||
- **Vitest worker fan-out tightened**: per-package cap lowered from `min(6, cpus()-1)` to `min(4, cpus()-1)` in cli/dashboard/desktop/mobile/plugin-sdk/engine (engine had no cap before). Each config now explicitly pins `pool` (`forks` or `threads`) and only sets the matching `poolOptions`, removing the dual-pool declaration. Worst-case `pnpm test` fan-out: ~12 workers → ~8.
|
||||
@@ -2,11 +2,11 @@ import { defineConfig } from "vitest/config";
|
||||
import { resolve } from "node:path";
|
||||
import { cpus } from "node:os";
|
||||
|
||||
// Cap fan-out to 6 so high-core dev machines don't spawn 27+ workers per
|
||||
// Cap fan-out to 4 so high-core dev machines don't spawn 27+ workers per
|
||||
// package — that saturates the box when multiple workspace packages test
|
||||
// concurrently or when the dashboard has agents running tests in parallel.
|
||||
// Override with VITEST_MAX_WORKERS for explicit fast/serial runs.
|
||||
const defaultMaxWorkers = Math.min(6, Math.max(1, cpus().length - 1));
|
||||
const defaultMaxWorkers = Math.min(4, Math.max(1, cpus().length - 1));
|
||||
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
|
||||
const maxWorkers = Math.max(1, Number.isFinite(requestedMaxWorkers) ? requestedMaxWorkers : defaultMaxWorkers);
|
||||
process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
||||
@@ -38,8 +38,9 @@ export default defineConfig({
|
||||
resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"),
|
||||
],
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
pool: "forks",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
poolOptions: { forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
coverage: {
|
||||
enabled: false,
|
||||
|
||||
@@ -23,8 +23,9 @@ export default defineConfig({
|
||||
"./src/__test-utils__/vitest-setup.ts",
|
||||
],
|
||||
globalSetup: ["./src/__test-utils__/vitest-teardown.ts"],
|
||||
pool: "forks",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
poolOptions: { forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
// Core runs a large SQLite-heavy suite while other workspace packages test concurrently.
|
||||
// Use a slightly higher timeout to reduce nondeterministic slow-machine flakes.
|
||||
|
||||
@@ -569,9 +569,9 @@ describe("TerminalService", () => {
|
||||
// Advance exactly to the resize debounce boundary
|
||||
vi.advanceTimersByTime(150);
|
||||
|
||||
// The resize debounce should have moved suppressed data to outputBuffer
|
||||
// and scheduled a flush. Advance past the flush throttle.
|
||||
vi.advanceTimersByTime(10);
|
||||
// The resize debounce should have moved suppressed data to the output
|
||||
// queue and scheduled a flush. Advance past the flush throttle window.
|
||||
vi.advanceTimersByTime(20);
|
||||
|
||||
expect(dataListener).toHaveBeenCalledWith(session.id, "shell prompt> ");
|
||||
|
||||
|
||||
@@ -190,9 +190,12 @@ const MIN_MAX_SESSIONS = 1;
|
||||
const MAX_MAX_SESSIONS = 100;
|
||||
const DEFAULT_MAX_SESSIONS = 10;
|
||||
|
||||
// Throttle output to prevent overwhelming WebSocket under heavy load
|
||||
const OUTPUT_THROTTLE_MS = 4; // ~250fps max update rate for responsive input
|
||||
const OUTPUT_BATCH_SIZE = 4096; // Smaller batches for lower latency
|
||||
// Throttle output to prevent overwhelming WebSocket under heavy load.
|
||||
// 16ms = 60fps, plenty for terminal redraw while leaving event-loop budget
|
||||
// for the rest of the dashboard. Larger flush cap keeps `pnpm test`-class
|
||||
// floods from generating thousands of timer-driven micro-flushes.
|
||||
const OUTPUT_THROTTLE_MS = 16;
|
||||
const OUTPUT_BATCH_SIZE = 64 * 1024; // 64KB per WebSocket frame
|
||||
|
||||
// Stale session threshold: sessions inactive for more than 5 minutes are eligible for eviction
|
||||
export const STALE_SESSION_THRESHOLD_MS = 300_000; // 5 minutes
|
||||
@@ -230,7 +233,14 @@ export interface TerminalSession {
|
||||
lastActivityAt: Date;
|
||||
shell: string;
|
||||
scrollbackBuffer: string;
|
||||
outputBuffer: string;
|
||||
/**
|
||||
* Pending output chunks awaiting flush to clients. Stored as an array
|
||||
* (not a single concatenated string) so heavy bursts — e.g. a `pnpm test`
|
||||
* run inside an embedded terminal — don't trigger O(N²) string copies on
|
||||
* every flush tick, which previously caused multi-second event-loop stalls.
|
||||
*/
|
||||
outputChunks: string[];
|
||||
outputBytes: number;
|
||||
flushTimeout: NodeJS.Timeout | null;
|
||||
resizeInProgress: boolean;
|
||||
resizeDebounceTimeout: NodeJS.Timeout | null;
|
||||
@@ -241,7 +251,7 @@ export interface TerminalSession {
|
||||
* This prevents the initial shell prompt (and other output) from being lost
|
||||
* when it falls inside the 150 ms resize-suppression window.
|
||||
*/
|
||||
resizeSuppressedBuffer: string;
|
||||
resizeSuppressedChunks: string[];
|
||||
/** Internal flush callback set by createSession; used by resize debounce */
|
||||
_flushOutput: (() => void) | null;
|
||||
}
|
||||
@@ -659,17 +669,21 @@ export class TerminalService extends EventEmitter {
|
||||
lastActivityAt: new Date(),
|
||||
shell,
|
||||
scrollbackBuffer: "",
|
||||
outputBuffer: "",
|
||||
outputChunks: [],
|
||||
outputBytes: 0,
|
||||
flushTimeout: null,
|
||||
resizeInProgress: false,
|
||||
resizeDebounceTimeout: null,
|
||||
resizeSuppressedBuffer: "",
|
||||
resizeSuppressedChunks: [],
|
||||
_flushOutput: null,
|
||||
};
|
||||
|
||||
this.sessions.set(id, session);
|
||||
|
||||
// Flush buffered output to clients (throttled)
|
||||
// Flush buffered output to clients (throttled).
|
||||
// Drains chunks from the front of the queue up to OUTPUT_BATCH_SIZE bytes
|
||||
// per frame. Using an array avoids the O(N) string-slice that the previous
|
||||
// implementation paid on every tick under heavy load.
|
||||
const flushOutput = () => {
|
||||
// Guard against firing after session was killed
|
||||
if (!this.sessions.has(id)) {
|
||||
@@ -677,21 +691,29 @@ export class TerminalService extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.outputBuffer.length === 0) {
|
||||
if (session.outputChunks.length === 0) {
|
||||
session.outputBytes = 0;
|
||||
session.flushTimeout = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let dataToSend = session.outputBuffer;
|
||||
if (dataToSend.length > OUTPUT_BATCH_SIZE) {
|
||||
dataToSend = session.outputBuffer.slice(0, OUTPUT_BATCH_SIZE);
|
||||
session.outputBuffer = session.outputBuffer.slice(OUTPUT_BATCH_SIZE);
|
||||
const drained: string[] = [];
|
||||
let drainedBytes = 0;
|
||||
while (session.outputChunks.length > 0 && drainedBytes < OUTPUT_BATCH_SIZE) {
|
||||
const next = session.outputChunks.shift() as string;
|
||||
drained.push(next);
|
||||
drainedBytes += next.length;
|
||||
}
|
||||
session.outputBytes = Math.max(0, session.outputBytes - drainedBytes);
|
||||
|
||||
if (session.outputChunks.length > 0) {
|
||||
// More to send — schedule another flush after the throttle window.
|
||||
session.flushTimeout = setTimeout(flushOutput, OUTPUT_THROTTLE_MS);
|
||||
} else {
|
||||
session.outputBuffer = "";
|
||||
session.flushTimeout = null;
|
||||
}
|
||||
|
||||
const dataToSend = drained.length === 1 ? drained[0] : drained.join("");
|
||||
this.dataCallbacks.forEach((cb) => cb(id, dataToSend));
|
||||
this.emit("data", id, dataToSend);
|
||||
};
|
||||
@@ -712,12 +734,13 @@ export class TerminalService extends EventEmitter {
|
||||
// to avoid rendering artifacts, but queue the data so it is flushed to
|
||||
// clients once the resize debounce completes (no data loss).
|
||||
if (session.resizeInProgress) {
|
||||
session.resizeSuppressedBuffer += data;
|
||||
session.resizeSuppressedChunks.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer output for throttled delivery
|
||||
session.outputBuffer += data;
|
||||
session.outputChunks.push(data);
|
||||
session.outputBytes += data.length;
|
||||
|
||||
if (!session.flushTimeout) {
|
||||
session.flushTimeout = setTimeout(flushOutput, OUTPUT_THROTTLE_MS);
|
||||
@@ -737,7 +760,9 @@ export class TerminalService extends EventEmitter {
|
||||
session.resizeDebounceTimeout = null;
|
||||
}
|
||||
session._flushOutput = null;
|
||||
session.resizeSuppressedBuffer = "";
|
||||
session.resizeSuppressedChunks.length = 0;
|
||||
session.outputChunks.length = 0;
|
||||
session.outputBytes = 0;
|
||||
this.sessions.delete(id);
|
||||
this.exitCallbacks.forEach((cb) => cb(id, exitCode ?? 0));
|
||||
this.emit("exit", id, exitCode ?? 0);
|
||||
@@ -812,9 +837,12 @@ export class TerminalService extends EventEmitter {
|
||||
// This ensures the initial shell prompt (and any other output that
|
||||
// landed inside the suppression window) is delivered to clients
|
||||
// rather than being silently dropped.
|
||||
if (session.resizeSuppressedBuffer.length > 0) {
|
||||
session.outputBuffer += session.resizeSuppressedBuffer;
|
||||
session.resizeSuppressedBuffer = "";
|
||||
if (session.resizeSuppressedChunks.length > 0) {
|
||||
for (const chunk of session.resizeSuppressedChunks) {
|
||||
session.outputChunks.push(chunk);
|
||||
session.outputBytes += chunk.length;
|
||||
}
|
||||
session.resizeSuppressedChunks.length = 0;
|
||||
if (!session.flushTimeout && session._flushOutput) {
|
||||
session.flushTimeout = setTimeout(session._flushOutput, OUTPUT_THROTTLE_MS);
|
||||
}
|
||||
@@ -920,7 +948,8 @@ export class TerminalService extends EventEmitter {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return null;
|
||||
|
||||
session.outputBuffer = "";
|
||||
session.outputChunks.length = 0;
|
||||
session.outputBytes = 0;
|
||||
if (session.flushTimeout) {
|
||||
clearTimeout(session.flushTimeout);
|
||||
session.flushTimeout = null;
|
||||
|
||||
@@ -3,9 +3,9 @@ import react from "@vitejs/plugin-react";
|
||||
import { resolve } from "node:path";
|
||||
import { cpus } from "node:os";
|
||||
|
||||
// Cap fan-out to 6 so high-core dev machines don't spawn 27+ workers per
|
||||
// Cap fan-out to 4 so high-core dev machines don't spawn 27+ workers per
|
||||
// package — that saturates the box when workspace packages test concurrently.
|
||||
const defaultMaxWorkers = Math.min(6, Math.max(1, cpus().length - 1));
|
||||
const defaultMaxWorkers = Math.min(4, Math.max(1, cpus().length - 1));
|
||||
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
|
||||
const maxWorkers = Math.max(1, Number.isFinite(requestedMaxWorkers) ? requestedMaxWorkers : defaultMaxWorkers);
|
||||
process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
||||
@@ -33,8 +33,9 @@ export default defineConfig({
|
||||
"./vitest.setup.ts",
|
||||
],
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
pool: "forks",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
poolOptions: { forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
isolate: true,
|
||||
// Dashboard route and integration-heavy suites can exceed the Vitest
|
||||
|
||||
@@ -2,8 +2,8 @@ import { defineConfig } from "vitest/config";
|
||||
import { cpus } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// Cap fan-out to 6 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(6, Math.max(1, cpus().length - 1));
|
||||
// Cap fan-out to 4 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(4, Math.max(1, cpus().length - 1));
|
||||
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
|
||||
const maxWorkers = Math.max(1, Number.isFinite(requestedMaxWorkers) ? requestedMaxWorkers : defaultMaxWorkers);
|
||||
process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
||||
@@ -14,8 +14,9 @@ export default defineConfig({
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
pool: "threads",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
passWithNoTests: true,
|
||||
projects: [
|
||||
|
||||
@@ -91,36 +91,60 @@ export interface VerificationResult {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output buffer helper — keeps head + tail within the byte cap
|
||||
//
|
||||
// Stores head/tail as chunk arrays rather than concatenated strings.
|
||||
// The previous implementation re-encoded the entire ~100 KB tail through
|
||||
// `Buffer.from(...).subarray(...).toString()` on *every* appended line once
|
||||
// output crossed MAX_OUTPUT_BYTES — for a `pnpm test` run dumping 50k lines
|
||||
// that produced gigabytes of GC churn and stalled the dashboard event loop.
|
||||
// Now we just push chunks and only compact the tail when its byte size grows
|
||||
// past 2× the cap, making the amortized cost per append O(1).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function appendToBuffer(
|
||||
buf: { head: string; tail: string; totalBytes: number },
|
||||
chunk: string,
|
||||
): void {
|
||||
interface OutputBuffer {
|
||||
headChunks: string[];
|
||||
headBytes: number;
|
||||
tailChunks: string[];
|
||||
tailBytes: number;
|
||||
totalBytes: number;
|
||||
}
|
||||
|
||||
function createBuffer(): OutputBuffer {
|
||||
return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
|
||||
}
|
||||
|
||||
function appendToBuffer(buf: OutputBuffer, chunk: string): void {
|
||||
const chunkBytes = Buffer.byteLength(chunk, "utf8");
|
||||
buf.totalBytes += chunkBytes;
|
||||
|
||||
if (buf.totalBytes <= MAX_OUTPUT_BYTES) {
|
||||
buf.head += chunk;
|
||||
if (buf.headBytes + chunkBytes <= MAX_OUTPUT_BYTES) {
|
||||
buf.headChunks.push(chunk);
|
||||
buf.headBytes += chunkBytes;
|
||||
return;
|
||||
}
|
||||
|
||||
// Overflow: funnel excess into tail (keep at most half the cap in tail)
|
||||
// Overflow: funnel into tail. Keep at most half the cap in tail, but only
|
||||
// compact when we're well over so per-line cost stays amortized O(1).
|
||||
const tailCap = MAX_OUTPUT_BYTES / 2;
|
||||
buf.tail += chunk;
|
||||
if (Buffer.byteLength(buf.tail, "utf8") > tailCap) {
|
||||
// Truncate tail from the front — keep newest content
|
||||
const bytes = Buffer.from(buf.tail, "utf8");
|
||||
buf.tail = bytes.subarray(bytes.length - tailCap).toString("utf8");
|
||||
buf.tailChunks.push(chunk);
|
||||
buf.tailBytes += chunkBytes;
|
||||
if (buf.tailBytes > tailCap * 2) {
|
||||
// Drop oldest chunks until under the cap.
|
||||
while (buf.tailChunks.length > 1 && buf.tailBytes - Buffer.byteLength(buf.tailChunks[0], "utf8") >= tailCap) {
|
||||
const dropped = buf.tailChunks.shift() as string;
|
||||
buf.tailBytes -= Buffer.byteLength(dropped, "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenBuffer(buf: { head: string; tail: string; totalBytes: number }): string {
|
||||
if (buf.tail.length === 0) return buf.head;
|
||||
function flattenBuffer(buf: OutputBuffer): string {
|
||||
const head = buf.headChunks.join("");
|
||||
if (buf.tailChunks.length === 0) return head;
|
||||
const tail = buf.tailChunks.join("");
|
||||
return (
|
||||
buf.head +
|
||||
head +
|
||||
`\n\n[... output truncated — ${buf.totalBytes} bytes total, showing head + tail ...]\n\n` +
|
||||
buf.tail
|
||||
tail
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,8 +174,8 @@ export async function runVerificationCommand(
|
||||
const startMs = Date.now();
|
||||
const warnings: string[] = [];
|
||||
|
||||
const stdoutBuf = { head: "", tail: "", totalBytes: 0 };
|
||||
const stderrBuf = { head: "", tail: "", totalBytes: 0 };
|
||||
const stdoutBuf = createBuffer();
|
||||
const stderrBuf = createBuffer();
|
||||
|
||||
return new Promise<VerificationResult>((resolve) => {
|
||||
// Use shell: true so Node picks the platform default — /bin/sh on POSIX,
|
||||
|
||||
@@ -2,7 +2,8 @@ import { defineConfig } from "vitest/config";
|
||||
import { resolve } from "node:path";
|
||||
import { cpus } from "node:os";
|
||||
|
||||
const defaultMaxWorkers = Math.max(1, cpus().length - 1);
|
||||
// Cap fan-out to 4 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(4, Math.max(1, cpus().length - 1));
|
||||
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
|
||||
const maxWorkers = Math.max(1, Number.isFinite(requestedMaxWorkers) ? requestedMaxWorkers : defaultMaxWorkers);
|
||||
process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
||||
@@ -23,10 +24,10 @@ export default defineConfig({
|
||||
resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"),
|
||||
],
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
pool: "threads",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
// Enable isolate to allow parallel execution of tests with conflicting mocks
|
||||
isolate: true,
|
||||
coverage: {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { defineConfig } from "vitest/config";
|
||||
import { cpus } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// Cap fan-out to 6 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(6, Math.max(1, cpus().length - 1));
|
||||
// Cap fan-out to 4 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(4, Math.max(1, cpus().length - 1));
|
||||
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
|
||||
const maxWorkers = Math.max(1, Number.isFinite(requestedMaxWorkers) ? requestedMaxWorkers : defaultMaxWorkers);
|
||||
process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
||||
@@ -13,8 +13,9 @@ export default defineConfig({
|
||||
include: ["src/**/*.test.ts"],
|
||||
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
pool: "forks",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
poolOptions: { forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { cpus } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Cap fan-out to 6 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(6, Math.max(1, cpus().length - 1));
|
||||
// Cap fan-out to 4 to avoid saturating high-core machines under workspace concurrency.
|
||||
const defaultMaxWorkers = Math.min(4, Math.max(1, cpus().length - 1));
|
||||
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
|
||||
const maxWorkers = Math.max(1, Number.isFinite(requestedMaxWorkers) ? requestedMaxWorkers : defaultMaxWorkers);
|
||||
process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
||||
@@ -21,6 +21,6 @@ export default defineConfig({
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
pool: "threads",
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers } },
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user