From c0430342c654125b44c0a6ec0351d2c49f8f49cc Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:41:43 -0300 Subject: [PATCH] test(ci): quarantine concurrency-sensitive flakes into a serial pass (#6347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(ci): quarantine concurrency-sensitive flakes into a serial pass (tests/unit/serial/) glm-coding-plan-monthly-3580, quota-division-blocks and provider-health-autopilot fail under --test-concurrency>1 CPU contention but pass isolated (v3.8.45 release benchmark: this class cost ~28min re-runs per CI round). They now live in tests/unit/serial/ and run in a dedicated --test-concurrency=1 step appended to every runner (test:unit, :ci, :ci:shard, :fast, :shard:1/2, coverage:runner — sharded variants shard the serial pass too, so concurrent shard jobs never self-collide). Discovery + TIA gates know the new glob; stryker.conf.json path updated. Guard: tests/unit/test-serial-quarantine.test.ts (4 tests). * test(ci): quarantine combo-health-autopilot too (async logger writes after test end under load) Fresh evidence from this PR's own CI: all 3 subtests pass but an async log write lands after the test ends (ENOENT app.log -> uncaughtException) — same concurrency-flake family. Moved to tests/unit/serial/, imports adjusted, stryker path updated, guard test now asserts 4 quarantined files. --- package.json | 15 ++-- scripts/check/check-test-discovery.mjs | 5 ++ scripts/quality/build-test-impact-map.mjs | 2 + stryker.conf.json | 4 +- .../combo-health-autopilot.test.ts | 20 +++--- .../glm-coding-plan-monthly-3580.test.ts | 2 +- .../provider-health-autopilot.test.ts | 18 ++--- .../quota-division-blocks.test.ts | 10 +-- tests/unit/test-serial-quarantine.test.ts | 72 +++++++++++++++++++ 9 files changed, 114 insertions(+), 34 deletions(-) rename tests/unit/{ => serial}/combo-health-autopilot.test.ts (88%) rename tests/unit/{ => serial}/glm-coding-plan-monthly-3580.test.ts (94%) rename tests/unit/{ => serial}/provider-health-autopilot.test.ts (92%) rename tests/unit/{ => serial}/quota-division-blocks.test.ts (94%) create mode 100644 tests/unit/test-serial-quarantine.test.ts diff --git a/package.json b/package.json index a195f42438..8f62f43fa3 100644 --- a/package.json +++ b/package.json @@ -97,13 +97,13 @@ "electron:build:linux": "npm run build && cd electron && npm run build:linux", "electron:smoke:packaged": "node scripts/dev/smoke-electron-packaged.mjs", "test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:ci:shard": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none \"tests/unit/dashboard/**/*.test.ts\"", + "test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", + "test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", + "test:unit:ci:shard": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=$TEST_SHARD \"tests/unit/serial/**/*.test.ts\"", + "test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", "test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"", - "test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 \"tests/unit/dashboard/**/*.test.ts\"", + "test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=1/2 \"tests/unit/serial/**/*.test.ts\"", + "test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=2/2 \"tests/unit/serial/**/*.test.ts\"", "test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/plan3-p0.test.ts", "test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/fixes-p1.test.ts", "test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/security-fase01.test.ts", @@ -212,7 +212,8 @@ "build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs", "release:contributors": "node scripts/release/gen-contributors.mjs", "release:uncovered": "node scripts/release/list-uncovered-commits.mjs", - "test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\"" + "test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", + "test:unit:serial": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/unit/serial/**/*.test.ts\"" }, "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.1073.0", diff --git a/scripts/check/check-test-discovery.mjs b/scripts/check/check-test-discovery.mjs index 144157122b..299e38b207 100644 --- a/scripts/check/check-test-discovery.mjs +++ b/scripts/check/check-test-discovery.mjs @@ -67,6 +67,11 @@ export const COLLECTORS = [ // 24.18 do CI; no 24.16 local vira um crawl de ~60s/arquivo). O resto da suíte roda // sob tsx/esm (~-50% de bootstrap por processo). Plano mestre testes+CI, QW-b. { glob: "tests/unit/dashboard/**/*.test.ts", sources: ["package.json"] }, + // Quarentena de flakes de concorrência (plano melhorias v3.8.46, P0.3): arquivos + // sensíveis a contenção de CPU/timing (classe glm-3580 / quota-division / + // provider-health-autopilot) rodam num passo dedicado --test-concurrency=1 ao FIM + // de cada runner. Fora dos globs paralelos acima por diretório próprio. + { glob: "tests/unit/serial/**/*.test.ts", sources: ["package.json"] }, // Órfãos religados (plano mestre QW-c): arquivos .test.mjs (top-level + db/ + feature-triage/) — fora do glob // *.test.ts histórico, nunca rodava em job nenhum (53 casos recuperados). { glob: "tests/unit/**/*.test.mjs", sources: ["package.json"] }, diff --git a/scripts/quality/build-test-impact-map.mjs b/scripts/quality/build-test-impact-map.mjs index 1704c3a356..223d23bf2b 100644 --- a/scripts/quality/build-test-impact-map.mjs +++ b/scripts/quality/build-test-impact-map.mjs @@ -61,6 +61,8 @@ const testFiles = globSync( [ "tests/unit/*.test.ts", "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts", + // Quarentena serial (P0.3): também são node:test — a TIA precisa mapeá-los. + "tests/unit/serial/**/*.test.ts", ], { cwd: ROOT, absolute: true } ); diff --git a/stryker.conf.json b/stryker.conf.json index 6bf64db11f..943b03b491 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -118,7 +118,7 @@ "tests/unit/combo-config.test.ts", "tests/unit/combo-context-relay.test.ts", "tests/unit/combo-headroom-strategy.test.ts", - "tests/unit/combo-health-autopilot.test.ts", + "tests/unit/serial/combo-health-autopilot.test.ts", "tests/unit/combo-health-dashboard.test.ts", "tests/unit/combo-health-route.test.ts", "tests/unit/combo-hedging.test.ts", @@ -189,7 +189,7 @@ "tests/unit/plugin-sandbox-permissions.test.ts", "tests/unit/plugins-route-error-sanitization.test.ts", "tests/unit/provider-error-rules.test.ts", - "tests/unit/provider-health-autopilot.test.ts", + "tests/unit/serial/provider-health-autopilot.test.ts", "tests/unit/provider-health-matrix.test.ts", "tests/unit/provider-request-failure-pipeline.test.ts", "tests/unit/public-client-ids-3493.test.ts", diff --git a/tests/unit/combo-health-autopilot.test.ts b/tests/unit/serial/combo-health-autopilot.test.ts similarity index 88% rename from tests/unit/combo-health-autopilot.test.ts rename to tests/unit/serial/combo-health-autopilot.test.ts index cb1f4c8aee..091930283b 100644 --- a/tests/unit/combo-health-autopilot.test.ts +++ b/tests/unit/serial/combo-health-autopilot.test.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { makeManagementSessionRequest } from "../helpers/managementSession.ts"; +import { makeManagementSessionRequest } from "../../helpers/managementSession.ts"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-autopilot-")); const ORIGINAL_DATA_DIR = process.env.DATA_DIR; @@ -13,15 +13,15 @@ const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; process.env.DATA_DIR = TEST_DATA_DIR; -const core = await import("../../src/lib/db/core.ts"); -const combosDb = await import("../../src/lib/db/combos.ts"); -const settingsDb = await import("../../src/lib/db/settings.ts"); -const quotaSnapshotsDb = await import("../../src/lib/db/quotaSnapshots.ts"); -const callLogs = await import("../../src/lib/usage/callLogs.ts"); -const comboMetrics = await import("../../open-sse/services/comboMetrics.ts"); -const comboAutopilot = await import("../../src/lib/monitoring/comboHealthAutopilot.ts"); -const route = await import("../../src/app/api/usage/combo-health-autopilot/route.ts"); -const { normalizeComboStep } = await import("../../src/lib/combos/steps.ts"); +const core = await import("../../../src/lib/db/core.ts"); +const combosDb = await import("../../../src/lib/db/combos.ts"); +const settingsDb = await import("../../../src/lib/db/settings.ts"); +const quotaSnapshotsDb = await import("../../../src/lib/db/quotaSnapshots.ts"); +const callLogs = await import("../../../src/lib/usage/callLogs.ts"); +const comboMetrics = await import("../../../open-sse/services/comboMetrics.ts"); +const comboAutopilot = await import("../../../src/lib/monitoring/comboHealthAutopilot.ts"); +const route = await import("../../../src/app/api/usage/combo-health-autopilot/route.ts"); +const { normalizeComboStep } = await import("../../../src/lib/combos/steps.ts"); async function resetStorage() { comboMetrics.resetAllComboMetrics(); diff --git a/tests/unit/glm-coding-plan-monthly-3580.test.ts b/tests/unit/serial/glm-coding-plan-monthly-3580.test.ts similarity index 94% rename from tests/unit/glm-coding-plan-monthly-3580.test.ts rename to tests/unit/serial/glm-coding-plan-monthly-3580.test.ts index 418e9a9117..835528b306 100644 --- a/tests/unit/glm-coding-plan-monthly-3580.test.ts +++ b/tests/unit/serial/glm-coding-plan-monthly-3580.test.ts @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { glmMonthlyRemainingPercentage } from "../../open-sse/services/usage.ts"; +import { glmMonthlyRemainingPercentage } from "../../../open-sse/services/usage.ts"; // #3580 — z.ai/GLM coding plans have no monthly cap (only 5-hour windows), so the // quota API reports the TIME_LIMIT ("Monthly") entry with total=0. The previous diff --git a/tests/unit/provider-health-autopilot.test.ts b/tests/unit/serial/provider-health-autopilot.test.ts similarity index 92% rename from tests/unit/provider-health-autopilot.test.ts rename to tests/unit/serial/provider-health-autopilot.test.ts index fee5096900..b0dafa2914 100644 --- a/tests/unit/provider-health-autopilot.test.ts +++ b/tests/unit/serial/provider-health-autopilot.test.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { NextRequest } from "next/server"; -import { makeManagementSessionRequest } from "../helpers/managementSession.ts"; +import { makeManagementSessionRequest } from "../../helpers/managementSession.ts"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-health-autopilot-")); const ORIGINAL_DATA_DIR = process.env.DATA_DIR; @@ -13,14 +13,14 @@ const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; process.env.DATA_DIR = TEST_DATA_DIR; -const core = await import("../../src/lib/db/core.ts"); -const settingsDb = await import("../../src/lib/db/settings.ts"); -const providersDb = await import("../../src/lib/db/providers.ts"); -const autopilot = await import("../../src/lib/monitoring/providerHealthAutopilot.ts"); -const actionsRoute = await import("../../src/app/api/providers/health-autopilot/actions/route.ts"); -const reportRoute = await import("../../src/app/api/providers/health-autopilot/route.ts"); -const routeGuard = await import("../../src/server/authz/routeGuard.ts"); -const authzPipeline = await import("../../src/server/authz/pipeline.ts"); +const core = await import("../../../src/lib/db/core.ts"); +const settingsDb = await import("../../../src/lib/db/settings.ts"); +const providersDb = await import("../../../src/lib/db/providers.ts"); +const autopilot = await import("../../../src/lib/monitoring/providerHealthAutopilot.ts"); +const actionsRoute = await import("../../../src/app/api/providers/health-autopilot/actions/route.ts"); +const reportRoute = await import("../../../src/app/api/providers/health-autopilot/route.ts"); +const routeGuard = await import("../../../src/server/authz/routeGuard.ts"); +const authzPipeline = await import("../../../src/server/authz/pipeline.ts"); const accountFallback = await import("@omniroute/open-sse/services/accountFallback"); const PROVIDER = "autopilot-test-provider"; diff --git a/tests/unit/quota-division-blocks.test.ts b/tests/unit/serial/quota-division-blocks.test.ts similarity index 94% rename from tests/unit/quota-division-blocks.test.ts rename to tests/unit/serial/quota-division-blocks.test.ts index c6058ab870..3119f66619 100644 --- a/tests/unit/quota-division-blocks.test.ts +++ b/tests/unit/serial/quota-division-blocks.test.ts @@ -42,11 +42,11 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-division- process.env.DATA_DIR = TEST_DATA_DIR; // ── Imports ────────────────────────────────────────────────────────────────── -const providerPlans = await import("../../src/lib/db/providerPlans.ts"); -const quotaPools = await import("../../src/lib/db/quotaPools.ts"); -const { SqliteQuotaStore } = await import("../../src/lib/quota/sqliteQuotaStore.ts"); -const { enforceQuotaShare } = await import("../../src/lib/quota/enforce.ts"); -const core = await import("../../src/lib/db/core.ts"); +const providerPlans = await import("../../../src/lib/db/providerPlans.ts"); +const quotaPools = await import("../../../src/lib/db/quotaPools.ts"); +const { SqliteQuotaStore } = await import("../../../src/lib/quota/sqliteQuotaStore.ts"); +const { enforceQuotaShare } = await import("../../../src/lib/quota/enforce.ts"); +const core = await import("../../../src/lib/db/core.ts"); // ── Suite cleanup ───────────────────────────────────────────────────────────── test.after(() => { diff --git a/tests/unit/test-serial-quarantine.test.ts b/tests/unit/test-serial-quarantine.test.ts new file mode 100644 index 0000000000..12e895ed53 --- /dev/null +++ b/tests/unit/test-serial-quarantine.test.ts @@ -0,0 +1,72 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// Quarentena de flakes de concorrência (plano melhorias v3.8.46, P0.3). +// Os arquivos em tests/unit/serial/ falham sob contenção de CPU (--test-concurrency>1 +// com a suíte inteira) mas passam isolados — classe glm-3580 / quota-division / +// provider-health-autopilot, que custava re-runs de ~28min por rodada de CI. +// Este guard garante que a quarentena continua ligada em TODOS os runners. + +const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const pkg = JSON.parse(readFileSync(path.join(ROOT, "package.json"), "utf8")); +const scripts: Record = pkg.scripts; + +const SERIAL_GLOB = 'tests/unit/serial/**/*.test.ts'; + +test("test:unit:serial existe e roda o diretório de quarentena com concurrency=1", () => { + const s = scripts["test:unit:serial"]; + assert.ok(s, "script test:unit:serial deve existir"); + assert.ok(s.includes("--test-concurrency=1"), "quarentena deve rodar serial"); + assert.ok(s.includes(SERIAL_GLOB), "quarentena deve apontar para tests/unit/serial/"); +}); + +test("todos os runners paralelos terminam com o passo serial", () => { + for (const key of ["test:unit", "test:unit:ci", "test:unit:fast", "test:coverage:runner"]) { + assert.ok( + scripts[key].endsWith("&& npm run test:unit:serial"), + `${key} deve encadear o passo serial no fim` + ); + } + for (const [key, shard] of [ + ["test:unit:ci:shard", "$TEST_SHARD"], + ["test:unit:shard:1", "1/2"], + ["test:unit:shard:2", "2/2"], + ] as const) { + const s = scripts[key]; + const serialPart = s.slice(s.lastIndexOf("&&")); + assert.ok(serialPart.includes("--test-concurrency=1"), `${key}: passo serial ausente`); + assert.ok( + serialPart.includes(`--test-shard=${shard}`), + `${key}: o passo serial deve ser SHARDADO (${shard}) — sem isso os dois shards rodam ` + + `os mesmos arquivos ao mesmo tempo e recriam a colisão que a quarentena elimina` + ); + assert.ok(serialPart.includes(SERIAL_GLOB), `${key}: glob da quarentena ausente`); + } +}); + +test("os globs paralelos NÃO capturam tests/unit/serial/ (sem dupla execução)", () => { + const parallel = scripts["test:unit"].split("&& npm run test:unit:serial")[0]; + assert.ok(!parallel.includes("serial"), "parte paralela não deve referenciar serial/"); + // o glob de subdiretórios é uma brace-list explícita — 'serial' não pode entrar nela + const braceList = parallel.match(/tests\/unit\/\{([^}]+)\}/)?.[1] ?? ""; + assert.ok(!braceList.split(",").includes("serial"), "brace-list não deve conter 'serial'"); +}); + +test("a quarentena contém os 3 flakes conhecidos e os gates de discovery/TIA a conhecem", () => { + const files = readdirSync(path.join(ROOT, "tests/unit/serial")); + for (const f of [ + "glm-coding-plan-monthly-3580.test.ts", + "quota-division-blocks.test.ts", + "provider-health-autopilot.test.ts", + "combo-health-autopilot.test.ts", + ]) { + assert.ok(files.includes(f), `${f} deve estar na quarentena`); + } + const discovery = readFileSync(path.join(ROOT, "scripts/check/check-test-discovery.mjs"), "utf8"); + assert.ok(discovery.includes(SERIAL_GLOB), "check-test-discovery deve listar o glob serial"); + const tia = readFileSync(path.join(ROOT, "scripts/quality/build-test-impact-map.mjs"), "utf8"); + assert.ok(tia.includes(SERIAL_GLOB), "build-test-impact-map deve listar o glob serial"); +});