426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
/**
|
|
* Runs changed Vitest files through their nearest package configuration.
|
|
*
|
|
* The coverage gate executes before workspace builds, so combining unrelated
|
|
* package tests under the root config can resolve absent dist entrypoints and
|
|
* bypass package-specific aliases or setup. Each group runs in isolation, and
|
|
* the per-group LCOV reports are then union-merged into a single
|
|
* `coverage/vitest/lcov.info` (see {@link mergeLcovReports}) so the gate sees
|
|
* one record per file across every group that executed it.
|
|
*
|
|
* Two path-resolution rules make nested and specialty configs runnable:
|
|
* `*.harness.test.ts` files prefer the repo's `vitest.harness.config.ts`
|
|
* convention (the plain package config deliberately EXCLUDES harness tests,
|
|
* so grouping one there exits "no test files"), and each group runs with the
|
|
* owning package directory as cwd — a config nested below the package root
|
|
* (e.g. `packages/test/harness/vitest.config.ts`) declares `include` patterns
|
|
* relative to the package script's cwd, not the config's own directory.
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const CONFIG_NAMES = [
|
|
"vitest.config.ts",
|
|
"vitest.config.mts",
|
|
"vitest.config.js",
|
|
"vitest.config.mjs",
|
|
"vitest.config.cts",
|
|
"vitest.config.cjs",
|
|
];
|
|
|
|
// PGLite-runtime harness suites are excluded from plain package configs and
|
|
// carry their own config with the workspace source-alias set.
|
|
const HARNESS_CONFIG_NAME = "vitest.harness.config.ts";
|
|
const HARNESS_TEST_SUFFIXES = [".harness.test.ts", ".harness.test.tsx"];
|
|
|
|
const normalize = (value) => value.split(path.sep).join("/");
|
|
|
|
function isHarnessTest(testFile) {
|
|
return HARNESS_TEST_SUFFIXES.some((suffix) => testFile.endsWith(suffix));
|
|
}
|
|
|
|
export function findNearestVitestConfig(repoRoot, testFile) {
|
|
const absoluteRoot = path.resolve(repoRoot);
|
|
const absoluteTest = path.resolve(absoluteRoot, testFile);
|
|
const relativeTest = path.relative(absoluteRoot, absoluteTest);
|
|
if (
|
|
relativeTest === ".." ||
|
|
relativeTest.startsWith(`..${path.sep}`) ||
|
|
path.isAbsolute(relativeTest)
|
|
) {
|
|
throw new Error(`Changed test escapes the repository: ${testFile}`);
|
|
}
|
|
|
|
const configNames = isHarnessTest(absoluteTest)
|
|
? [HARNESS_CONFIG_NAME, ...CONFIG_NAMES]
|
|
: CONFIG_NAMES;
|
|
|
|
let directory = path.dirname(absoluteTest);
|
|
while (true) {
|
|
for (const name of configNames) {
|
|
const candidate = path.join(directory, name);
|
|
if (existsSync(candidate)) return candidate;
|
|
}
|
|
if (directory === absoluteRoot) break;
|
|
const parent = path.dirname(directory);
|
|
if (parent === directory) break;
|
|
directory = parent;
|
|
}
|
|
|
|
throw new Error(`No Vitest config found for changed test: ${testFile}`);
|
|
}
|
|
|
|
export function findNearestPackageDir(repoRoot, configDir) {
|
|
const absoluteRoot = path.resolve(repoRoot);
|
|
let directory = path.resolve(configDir);
|
|
while (true) {
|
|
if (existsSync(path.join(directory, "package.json"))) return directory;
|
|
if (directory === absoluteRoot) return absoluteRoot;
|
|
const parent = path.dirname(directory);
|
|
if (parent === directory) return absoluteRoot;
|
|
directory = parent;
|
|
}
|
|
}
|
|
|
|
export function groupChangedVitestTests(repoRoot, testFiles) {
|
|
const absoluteRoot = path.resolve(repoRoot);
|
|
const groups = new Map();
|
|
|
|
for (const testFile of testFiles) {
|
|
const configPath = findNearestVitestConfig(absoluteRoot, testFile);
|
|
const absoluteTest = path.resolve(absoluteRoot, testFile);
|
|
const tests = groups.get(configPath) ?? [];
|
|
tests.push(absoluteTest);
|
|
groups.set(configPath, tests);
|
|
}
|
|
|
|
return [...groups.entries()]
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
.map(([configPath, tests]) => {
|
|
const configDir = path.dirname(configPath);
|
|
const relativeDir = normalize(path.relative(absoluteRoot, configDir));
|
|
// A non-default config (vitest.harness.config.ts) can share a directory
|
|
// with the default one; suffix its slug so the two groups' LCOV reports
|
|
// do not clobber each other. Default-config slugs stay unchanged.
|
|
const configBase = path.basename(configPath);
|
|
const slugSuffix = CONFIG_NAMES.includes(configBase)
|
|
? ""
|
|
: `-${path.basename(configBase, path.extname(configBase))}`;
|
|
const reportSlug = `${relativeDir || "root"}${slugSuffix}`.replaceAll(
|
|
/[^a-zA-Z0-9._-]+/g,
|
|
"-",
|
|
);
|
|
return {
|
|
configDir,
|
|
configPath,
|
|
packageDir: findNearestPackageDir(absoluteRoot, configDir),
|
|
reportDir: path.join(absoluteRoot, "coverage", "vitest", reportSlug),
|
|
tests: tests.sort(),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function normalizeLcovReport(repoRoot, baseDir, reportDir) {
|
|
const lcovPath = path.join(reportDir, "lcov.info");
|
|
if (!existsSync(lcovPath)) return;
|
|
|
|
const absoluteRoot = path.resolve(repoRoot);
|
|
const normalized = readFileSync(lcovPath, "utf8")
|
|
.split("\n")
|
|
.map((line) => {
|
|
if (!line.startsWith("SF:")) return line;
|
|
const sourcePath = line.slice("SF:".length);
|
|
const candidates = path.isAbsolute(sourcePath)
|
|
? [sourcePath]
|
|
: [
|
|
path.resolve(baseDir, sourcePath),
|
|
path.resolve(absoluteRoot, sourcePath),
|
|
];
|
|
const existing = candidates.find((candidate) => existsSync(candidate));
|
|
if (!existing) return line;
|
|
const relative = path.relative(absoluteRoot, existing);
|
|
if (relative.startsWith("..") || path.isAbsolute(relative)) return line;
|
|
return `SF:${normalize(relative)}`;
|
|
})
|
|
.join("\n");
|
|
writeFileSync(lcovPath, normalized);
|
|
}
|
|
|
|
/**
|
|
* Union-merge normalized LCOV reports: per file, a line counts as hit when ANY
|
|
* report hit it. The coverage gate latches a failure on EVERY report occurrence
|
|
* of a changed file below the threshold, so feeding it one merged record per
|
|
* file — instead of one low record per group that merely LOADED the file plus
|
|
* one high record from the group that exercised it — is what makes multi-group
|
|
* coverage mean "covered anywhere in the changed-test run".
|
|
*/
|
|
export function mergeLcovReports(reportPaths, mergedPath) {
|
|
const files = new Map();
|
|
for (const reportPath of reportPaths) {
|
|
if (!existsSync(reportPath)) continue;
|
|
let current = null;
|
|
for (const line of readFileSync(reportPath, "utf8").split("\n")) {
|
|
if (line.startsWith("SF:")) {
|
|
current = line.slice("SF:".length);
|
|
if (!files.has(current)) files.set(current, new Map());
|
|
} else if (line.startsWith("DA:") && current) {
|
|
const [lineNo, hits] = line.slice("DA:".length).split(",");
|
|
const parsedLine = Number(lineNo);
|
|
const parsedHits = Number(hits);
|
|
if (!Number.isFinite(parsedLine) || !Number.isFinite(parsedHits)) {
|
|
continue;
|
|
}
|
|
const lineHits = files.get(current);
|
|
lineHits.set(
|
|
parsedLine,
|
|
Math.max(lineHits.get(parsedLine) ?? 0, parsedHits),
|
|
);
|
|
} else if (line === "end_of_record") {
|
|
current = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const out = ["TN:"];
|
|
for (const [sourceFile, lineHits] of [...files.entries()].sort(
|
|
([left], [right]) => left.localeCompare(right),
|
|
)) {
|
|
out.push(`SF:${sourceFile}`);
|
|
const sortedLines = [...lineHits.entries()].sort(
|
|
([left], [right]) => left - right,
|
|
);
|
|
let hit = 0;
|
|
for (const [lineNo, hits] of sortedLines) {
|
|
out.push(`DA:${lineNo},${hits}`);
|
|
if (hits > 0) hit++;
|
|
}
|
|
out.push(`LF:${sortedLines.length}`, `LH:${hit}`, "end_of_record");
|
|
}
|
|
writeFileSync(mergedPath, `${out.join("\n")}\n`);
|
|
}
|
|
|
|
export function runChangedVitestCoverage(repoRoot, testFiles) {
|
|
const groups = groupChangedVitestTests(repoRoot, testFiles);
|
|
for (const group of groups) {
|
|
const result = spawnSync(
|
|
"bunx",
|
|
[
|
|
"vitest",
|
|
"run",
|
|
...group.tests,
|
|
"--config",
|
|
group.configPath,
|
|
"--coverage",
|
|
"--coverage.reporter=lcov",
|
|
// Package configs carry whole-suite global thresholds. This lane runs
|
|
// only changed files and applies its stricter changed-source floor in
|
|
// coverage-gate.awk after merging the per-package LCOV reports.
|
|
"--coverage.thresholds.lines=0",
|
|
"--coverage.thresholds.functions=0",
|
|
"--coverage.thresholds.statements=0",
|
|
"--coverage.thresholds.branches=0",
|
|
// Cross-package suites (the PGLite runtime harness) execute workspace
|
|
// sources OUTSIDE the package root via source aliases; without this
|
|
// flag that real execution is invisible to the changed-file gate.
|
|
"--coverage.allowExternal=true",
|
|
`--coverage.reportsDirectory=${group.reportDir}`,
|
|
],
|
|
{
|
|
// Run from the owning package (not the config's directory): package
|
|
// scripts invoke nested configs from the package root, and relative
|
|
// `include` patterns resolve against the cwd.
|
|
cwd: group.packageDir,
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) {
|
|
throw new Error(
|
|
`Vitest coverage failed for ${normalize(path.relative(repoRoot, group.configDir)) || "root"} (exit ${result.status ?? "signal"})`,
|
|
);
|
|
}
|
|
normalizeLcovReport(repoRoot, group.packageDir, group.reportDir);
|
|
}
|
|
|
|
// Collapse per-group reports into one union record per file, then remove the
|
|
// group files: the workflow feeds every `coverage/**/lcov.info` to the gate,
|
|
// and a leftover per-group report would re-introduce the low-occurrence
|
|
// latch the merge exists to fix.
|
|
const groupReports = groups.map((group) =>
|
|
path.join(group.reportDir, "lcov.info"),
|
|
);
|
|
if (groupReports.length > 0) {
|
|
mergeLcovReports(
|
|
groupReports,
|
|
path.join(path.resolve(repoRoot), "coverage", "vitest", "lcov.info"),
|
|
);
|
|
for (const reportPath of groupReports) {
|
|
if (existsSync(reportPath)) unlinkSync(reportPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
const isMain = process.argv[1]
|
|
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
: false;
|
|
|
|
if (isMain) {
|
|
const testFiles = process.argv.slice(2).filter(Boolean);
|
|
if (testFiles.length === 0) {
|
|
throw new Error("At least one changed Vitest file is required.");
|
|
}
|
|
const repoRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
"..",
|
|
"..",
|
|
);
|
|
runChangedVitestCoverage(repoRoot, testFiles);
|
|
}
|