mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
improve: speed up focused test execution (#106307)
* test: speed up focused test execution * style: format vitest config test * test: preserve targeted watch routing
This commit is contained in:
committed by
GitHub
parent
cad6df0a7c
commit
418dd85e66
@@ -4201,6 +4201,7 @@ describe("active-memory plugin", () => {
|
||||
});
|
||||
|
||||
it("uses a late verbose summary after a successful result and later unavailable trace", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||
const CONFIGURED_TIMEOUT_MS = 1_000;
|
||||
testing.setMinimumTimeoutMsForTests(1);
|
||||
testing.setSetupGraceTimeoutMsForTests(0);
|
||||
@@ -4219,6 +4220,10 @@ describe("active-memory plugin", () => {
|
||||
};
|
||||
const verboseSummary =
|
||||
"This memory says the user usually orders tonkotsu ramen, keeps chili crisp nearby, and prefers short dinner suggestions without menu preamble.";
|
||||
let markDelayScheduled: (() => void) | undefined;
|
||||
const delayScheduled = new Promise<void>((resolve) => {
|
||||
markDelayScheduled = resolve;
|
||||
});
|
||||
runEmbeddedAgent.mockImplementationOnce(async (params: { sessionFile: string }) => {
|
||||
await writeTranscriptJsonl(params.sessionFile, [
|
||||
{
|
||||
@@ -4259,6 +4264,7 @@ describe("active-memory plugin", () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
markDelayScheduled?.();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 550);
|
||||
});
|
||||
@@ -4281,10 +4287,13 @@ describe("active-memory plugin", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const result = await requireHook("before_prompt_build")(
|
||||
const resultPromise = requireHook("before_prompt_build")(
|
||||
{ prompt: "what food do i usually order? unavailable then summary", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
await delayScheduled;
|
||||
await vi.advanceTimersByTimeAsync(550);
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(requirePrependContext(result)).toContain(
|
||||
"This memory says the user usually orders tonkotsu ramen",
|
||||
|
||||
@@ -26,6 +26,11 @@ const SUCCESS_STREAM_CHUNK = Buffer.alloc(64 * 1024, "x");
|
||||
const SUCCESS_STREAM_BODY_BYTES = 33 * 1024 * 1024;
|
||||
const BROWSER_SUCCESS_BODY_LIMIT_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
function scheduleStreamChunk(writeNext: () => void): void {
|
||||
// Separate event-loop turns preserve streaming and backpressure without a wall-clock sleep.
|
||||
setImmediate(writeNext);
|
||||
}
|
||||
|
||||
describe("fetchHttpJson error body boundary", () => {
|
||||
let server: http.Server;
|
||||
let baseUrl: string;
|
||||
@@ -91,11 +96,10 @@ describe("fetchHttpJson error body boundary", () => {
|
||||
? SUCCESS_STREAM_CHUNK
|
||||
: SUCCESS_STREAM_CHUNK.subarray(0, remaining);
|
||||
written += chunk.byteLength;
|
||||
const scheduleNext = () => setTimeout(writeNext, 2);
|
||||
if (res.write(chunk)) {
|
||||
scheduleNext();
|
||||
scheduleStreamChunk(writeNext);
|
||||
} else {
|
||||
res.once("drain", scheduleNext);
|
||||
res.once("drain", () => scheduleStreamChunk(writeNext));
|
||||
}
|
||||
};
|
||||
writeNext();
|
||||
@@ -143,11 +147,10 @@ describe("fetchHttpJson error body boundary", () => {
|
||||
return;
|
||||
}
|
||||
written += STREAM_CHUNK.byteLength;
|
||||
const writeMore = () => setTimeout(writeNext, 2);
|
||||
if (res.write(STREAM_CHUNK)) {
|
||||
writeMore();
|
||||
scheduleStreamChunk(writeNext);
|
||||
} else {
|
||||
res.once("drain", writeMore);
|
||||
res.once("drain", () => scheduleStreamChunk(writeNext));
|
||||
}
|
||||
};
|
||||
writeNext();
|
||||
|
||||
+92
-54
@@ -28,6 +28,26 @@ function createMutableFrame(initialUrl: string) {
|
||||
};
|
||||
}
|
||||
|
||||
async function runWithVirtualNavigationGrace<T>(run: () => Promise<T>): Promise<T> {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
// Observe rejection before advancing the production grace timer to avoid a transient
|
||||
// unhandled rejection; timing-specific cases below still advance exact durations.
|
||||
const settled = run().then(
|
||||
(value) => ({ status: "fulfilled" as const, value }),
|
||||
(reason: unknown) => ({ status: "rejected" as const, reason }),
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
const result = await settled;
|
||||
if (result.status === "rejected") {
|
||||
throw result.reason;
|
||||
}
|
||||
return result.value;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}
|
||||
|
||||
describe("pw-tools-core interaction navigation guard", () => {
|
||||
it("waits for the grace window before completing a successful non-navigating click", async () => {
|
||||
vi.useFakeTimers();
|
||||
@@ -845,12 +865,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely.mockRejectedValueOnce(blocked);
|
||||
|
||||
await expect(
|
||||
mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
runWithVirtualNavigationGrace(() =>
|
||||
mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("blocked interaction navigation");
|
||||
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
@@ -918,12 +940,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
};
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
|
||||
const result = await mod.evaluateViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
fn: "() => location.href = 'http://127.0.0.1:9222/json/version'",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
const result = await runWithVirtualNavigationGrace(() =>
|
||||
mod.evaluateViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
fn: "() => location.href = 'http://127.0.0.1:9222/json/version'",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBe("ok");
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
@@ -1125,12 +1149,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
setPwToolsCoreCurrentRefLocator({ click });
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
|
||||
await mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
await runWithVirtualNavigationGrace(() =>
|
||||
mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
@@ -1152,12 +1178,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
setPwToolsCoreCurrentRefLocator({ click });
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
|
||||
await mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
await runWithVirtualNavigationGrace(() =>
|
||||
mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
@@ -1196,12 +1224,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
setPwToolsCoreCurrentRefLocator({ click });
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
|
||||
await mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
await runWithVirtualNavigationGrace(() =>
|
||||
mod.clickViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ref: "1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
@@ -1219,12 +1249,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
};
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
|
||||
const result = await mod.evaluateViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
fn: "() => 1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
const result = await runWithVirtualNavigationGrace(() =>
|
||||
mod.evaluateViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
fn: "() => 1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBe("ok");
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
@@ -1250,12 +1282,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
setPwToolsCoreCurrentRefLocator({ click });
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
|
||||
await mod.batchViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
actions: [{ kind: "click", ref: "1" }],
|
||||
});
|
||||
await runWithVirtualNavigationGrace(() =>
|
||||
mod.batchViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
actions: [{ kind: "click", ref: "1" }],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
@@ -1343,12 +1377,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
setPwToolsCoreCurrentRefLocator({ click });
|
||||
|
||||
const result = await mod.executeActViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
action: { kind: "click", ref: "1" },
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
const result = await runWithVirtualNavigationGrace(() =>
|
||||
mod.executeActViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
action: { kind: "click", ref: "1" },
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.downloads).toEqual([
|
||||
{
|
||||
@@ -1395,12 +1431,14 @@ describe("pw-tools-core interaction navigation guard", () => {
|
||||
setPwToolsCoreCurrentPage(page);
|
||||
setPwToolsCoreCurrentRefLocator(locator);
|
||||
|
||||
await mod.executeActViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
action,
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
});
|
||||
await runWithVirtualNavigationGrace(() =>
|
||||
mod.executeActViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
action,
|
||||
ssrfPolicy: { allowPrivateNetwork: false },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(drain).toHaveBeenCalledWith({
|
||||
firstEventGraceMs: 0,
|
||||
|
||||
@@ -122,7 +122,9 @@ function runPnpmSpecCommand(spec, pnpmArgs, label) {
|
||||
|
||||
async function runVitestSpec(spec) {
|
||||
if (spec.includeFilePath && spec.includePatterns) {
|
||||
writeVitestIncludeFile(spec.includeFilePath, spec.includePatterns);
|
||||
writeVitestIncludeFile(spec.includeFilePath, spec.includePatterns, {
|
||||
expandGlobs: !spec.watchMode,
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (spec.preflightPnpmArgs) {
|
||||
|
||||
@@ -156,7 +156,11 @@ export function shouldAcquireLocalHeavyCheckLock(
|
||||
env?: Record<string, string | undefined>,
|
||||
): boolean;
|
||||
|
||||
export function writeVitestIncludeFile(filePath: string, includePatterns: string[]): void;
|
||||
export function writeVitestIncludeFile(
|
||||
filePath: string,
|
||||
includePatterns: string[],
|
||||
options?: { cwd?: string; expandGlobs?: boolean },
|
||||
): void;
|
||||
|
||||
export function formatFailedShardDigest(
|
||||
failures: FailedVitestShard[],
|
||||
|
||||
@@ -2618,7 +2618,7 @@ function isExistingDirectoryTarget(arg, cwd) {
|
||||
}
|
||||
|
||||
function isGlobTarget(arg) {
|
||||
return /[*?[\]{}]/u.test(arg);
|
||||
return /[*?[\]{}]|[@+!]\(/u.test(arg);
|
||||
}
|
||||
|
||||
function isFileLikeTarget(arg) {
|
||||
@@ -4554,8 +4554,28 @@ export function shouldAcquireLocalHeavyCheckLock(runSpecs, env = process.env) {
|
||||
);
|
||||
}
|
||||
|
||||
export function writeVitestIncludeFile(filePath, includePatterns) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(includePatterns, null, 2)}\n`);
|
||||
function expandVitestIncludePatterns(includePatterns, cwd) {
|
||||
const candidateFiles = includePatterns.some(isGlobTarget)
|
||||
? listExplicitTestTargetFilesForCwd(cwd)
|
||||
: [];
|
||||
return uniqueOrdered(
|
||||
includePatterns.flatMap((pattern) => {
|
||||
if (!isGlobTarget(pattern)) {
|
||||
return [pattern];
|
||||
}
|
||||
return candidateFiles.filter((file) => path.matchesGlob(file, pattern));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function writeVitestIncludeFile(filePath, includePatterns, options = {}) {
|
||||
// Shared Vitest projects intersect this file with their ownership globs.
|
||||
// One-shot runs emit concrete paths; watch runs retain globs for new files.
|
||||
const expandedPatterns =
|
||||
options.expandGlobs === false
|
||||
? includePatterns
|
||||
: expandVitestIncludePatterns(includePatterns, options.cwd ?? process.cwd());
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(expandedPatterns, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
resolveChangedTargetArgs,
|
||||
resolveParallelFullSuiteConcurrency,
|
||||
shouldRetryVitestNoOutputTimeout,
|
||||
writeVitestIncludeFile,
|
||||
} from "../../scripts/test-projects.test-support.mjs";
|
||||
import { captureReaddirSyncCallsDuring } from "../../src/test-utils/fs-scan-assertions.js";
|
||||
import { toRepoPath } from "../../src/test-utils/repo-files.js";
|
||||
@@ -3570,6 +3571,50 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
expect(spec?.includeFilePath).not.toMatch(new RegExp(`${process.pid}-\\d+-0\\.json$`, "u"));
|
||||
});
|
||||
|
||||
it("expands routed glob targets to literal include-file paths", () => {
|
||||
withTinyGitRepo(
|
||||
{
|
||||
"src/gateway/core.test.ts": "",
|
||||
"src/gateway/server-methods/ping.test.ts": "",
|
||||
"src/gateway/server-startup.test.ts": "",
|
||||
},
|
||||
(cwd) => {
|
||||
const includeFile = path.join(cwd, "include.json");
|
||||
writeVitestIncludeFile(
|
||||
includeFile,
|
||||
[
|
||||
"src/gateway/**/*.test.ts",
|
||||
"src/gateway/server-*.test.ts",
|
||||
"src/gateway/@(core|server-startup).test.ts",
|
||||
],
|
||||
{ cwd },
|
||||
);
|
||||
|
||||
expect(JSON.parse(fs.readFileSync(includeFile, "utf8"))).toEqual([
|
||||
"src/gateway/core.test.ts",
|
||||
"src/gateway/server-methods/ping.test.ts",
|
||||
"src/gateway/server-startup.test.ts",
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("retains routed glob targets in watch-mode include files", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-projects-watch-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
writeVitestIncludeFile(includeFile, ["src/gateway/**/*.test.ts"], {
|
||||
expandGlobs: false,
|
||||
});
|
||||
|
||||
expect(JSON.parse(fs.readFileSync(includeFile, "utf8"))).toEqual([
|
||||
"src/gateway/**/*.test.ts",
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preflights targeted UI E2E specs with Playwright browser assets", () => {
|
||||
const [spec] = createVitestRunSpecs(["ui/src/pages/tasks/tasks.e2e.test.ts"], {
|
||||
baseEnv: {},
|
||||
|
||||
@@ -45,6 +45,10 @@ import { createExtensionVoiceCallVitestConfig } from "./vitest/vitest.extension-
|
||||
import { createExtensionWhatsAppVitestConfig } from "./vitest/vitest.extension-whatsapp.config.ts";
|
||||
import { createExtensionZaloVitestConfig } from "./vitest/vitest.extension-zalo.config.ts";
|
||||
import { createExtensionsVitestConfig } from "./vitest/vitest.extensions.config.ts";
|
||||
import { createGatewayClientVitestConfig } from "./vitest/vitest.gateway-client.config.ts";
|
||||
import { createGatewayCoreVitestConfig } from "./vitest/vitest.gateway-core.config.ts";
|
||||
import { createGatewayMethodsVitestConfig } from "./vitest/vitest.gateway-methods.config.ts";
|
||||
import { createGatewayServerVitestConfig } from "./vitest/vitest.gateway-server.config.ts";
|
||||
import { createGatewayVitestConfig } from "./vitest/vitest.gateway.config.ts";
|
||||
import { createHooksVitestConfig } from "./vitest/vitest.hooks.config.ts";
|
||||
import { createInfraVitestConfig } from "./vitest/vitest.infra.config.ts";
|
||||
@@ -386,6 +390,107 @@ describe("createScopedVitestConfig", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps include-file targets inside the scoped project's ownership", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-scoped-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
fs.writeFileSync(
|
||||
includeFile,
|
||||
JSON.stringify(["src/gateway/server.node-pairing-ssh-verify.test.ts"]),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const config = createScopedVitestConfig(["src/gateway/server-methods/**/*.test.ts"], {
|
||||
dir: "src/gateway",
|
||||
env: {
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
},
|
||||
intersectIncludeFile: true,
|
||||
});
|
||||
const testConfig = requireTestConfig(config);
|
||||
|
||||
expect(testConfig.include).toEqual([]);
|
||||
expect(testConfig.passWithNoTests).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
"src/gateway/**/*{server,client}*.test.ts",
|
||||
"src/gateway/@(server|core).test.ts",
|
||||
"src/gateway/nested/**/*.test.ts",
|
||||
])(
|
||||
"rejects ambiguous watch-mode include-file target %s at an ownership boundary",
|
||||
(candidate) => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-scoped-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
fs.writeFileSync(includeFile, JSON.stringify([candidate]), "utf8");
|
||||
|
||||
expect(() =>
|
||||
createScopedVitestConfig(["src/gateway/**/*server*.test.ts"], {
|
||||
dir: "src/gateway",
|
||||
env: {
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
},
|
||||
intersectIncludeFile: true,
|
||||
}),
|
||||
).toThrow(`cannot safely intersect non-literal include path: ${candidate}`);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("intersects a watch-mode directory target with project ownership", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-scoped-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
fs.writeFileSync(includeFile, JSON.stringify(["src/gateway/**/*.test.ts"]), "utf8");
|
||||
|
||||
const config = createScopedVitestConfig(["src/gateway/**/*server*.test.ts"], {
|
||||
dir: "src/gateway",
|
||||
env: {
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
},
|
||||
intersectIncludeFile: true,
|
||||
});
|
||||
|
||||
expect(requireTestConfig(config).include).toEqual(["**/*server*.test.ts"]);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps shared gateway include files inside their actual child projects", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-scoped-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
fs.writeFileSync(
|
||||
includeFile,
|
||||
JSON.stringify(["src/gateway/server.node-pairing-ssh-verify.test.ts"]),
|
||||
"utf8",
|
||||
);
|
||||
const env = { OPENCLAW_VITEST_INCLUDE_FILE: includeFile };
|
||||
|
||||
expect(requireTestConfig(createGatewayServerVitestConfig(env)).include).toEqual([
|
||||
"server.node-pairing-ssh-verify.test.ts",
|
||||
]);
|
||||
const coreConfig = requireTestConfig(createGatewayCoreVitestConfig(env));
|
||||
expect(coreConfig.include).toEqual(["server.node-pairing-ssh-verify.test.ts"]);
|
||||
expect(coreConfig.passWithNoTests).toBe(true);
|
||||
const clientConfig = requireTestConfig(createGatewayClientVitestConfig(env));
|
||||
expect(clientConfig.include).toEqual([]);
|
||||
expect(clientConfig.passWithNoTests).toBe(true);
|
||||
const methodsConfig = requireTestConfig(createGatewayMethodsVitestConfig(env));
|
||||
expect(methodsConfig.include).toEqual([]);
|
||||
expect(methodsConfig.passWithNoTests).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("overrides setup files when a scoped config requests them", () => {
|
||||
const config = createScopedVitestConfig(["src/example.test.ts"], {
|
||||
env: {},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Vitest contract shared helpers build contract test project configuration.
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
|
||||
import {
|
||||
intersectIncludePatterns,
|
||||
loadPatternListFromEnv,
|
||||
narrowIncludePatternsForCli,
|
||||
} from "./vitest.pattern-file.ts";
|
||||
import { nonIsolatedRunnerPath, sharedVitestConfig } from "./vitest.shared.config.ts";
|
||||
|
||||
const base = sharedVitestConfig as Record<string, unknown>;
|
||||
@@ -53,25 +56,6 @@ function loadContractsIncludePatternsFromEnv(
|
||||
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
|
||||
}
|
||||
|
||||
function narrowContractIncludePatterns(
|
||||
includePatterns: string[],
|
||||
candidatePatterns: string[] | null,
|
||||
): string[] | null {
|
||||
if (!candidatePatterns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
candidatePatterns.filter((candidate) =>
|
||||
includePatterns.some(
|
||||
(pattern) => path.matchesGlob(candidate, pattern) || path.matchesGlob(pattern, candidate),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function createContractsVitestConfig(
|
||||
includePatterns: string[],
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
@@ -79,7 +63,7 @@ export function createContractsVitestConfig(
|
||||
options: { name?: string } = {},
|
||||
) {
|
||||
const cliIncludePatterns = narrowIncludePatternsForCli(includePatterns, argv);
|
||||
const envIncludePatterns = narrowContractIncludePatterns(
|
||||
const envIncludePatterns = intersectIncludePatterns(
|
||||
includePatterns,
|
||||
loadContractsIncludePatternsFromEnv(env),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest gateway client config wires the gateway client test shard.
|
||||
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
|
||||
|
||||
function createGatewayClientVitestConfig(env?: Record<string, string | undefined>) {
|
||||
export function createGatewayClientVitestConfig(env?: Record<string, string | undefined>) {
|
||||
return createScopedVitestConfig(
|
||||
[
|
||||
"packages/gateway-client/src/**/*.test.ts",
|
||||
@@ -14,6 +14,8 @@ function createGatewayClientVitestConfig(env?: Record<string, string | undefined
|
||||
{
|
||||
env,
|
||||
exclude: ["src/gateway/**/*server*.test.ts"],
|
||||
// Gateway child projects share one include file; preserve this project's ownership.
|
||||
intersectIncludeFile: true,
|
||||
isolate: true,
|
||||
name: "gateway-client",
|
||||
},
|
||||
|
||||
@@ -19,11 +19,13 @@ const nonCoreGatewayTestExclude = [
|
||||
"src/gateway/sessions-history-http.test.ts",
|
||||
];
|
||||
|
||||
function createGatewayCoreVitestConfig(env?: Record<string, string | undefined>) {
|
||||
export function createGatewayCoreVitestConfig(env?: Record<string, string | undefined>) {
|
||||
return createScopedVitestConfig(["src/gateway/**/*.test.ts"], {
|
||||
dir: "src/gateway",
|
||||
env,
|
||||
exclude: nonCoreGatewayTestExclude,
|
||||
// Gateway child projects share one include file; preserve this project's ownership.
|
||||
intersectIncludeFile: true,
|
||||
name: "gateway-core",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Vitest gateway methods config wires the gateway methods test shard.
|
||||
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
|
||||
|
||||
function createGatewayMethodsVitestConfig(env?: Record<string, string | undefined>) {
|
||||
export function createGatewayMethodsVitestConfig(env?: Record<string, string | undefined>) {
|
||||
return createScopedVitestConfig(["src/gateway/server-methods/**/*.test.ts"], {
|
||||
dir: "src/gateway",
|
||||
env,
|
||||
// Gateway child projects share one include file; preserve this project's ownership.
|
||||
intersectIncludeFile: true,
|
||||
name: "gateway-methods",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const gatewayServerBackedHttpTests = [
|
||||
"src/gateway/probe.auth.integration.test.ts",
|
||||
];
|
||||
|
||||
function createGatewayServerVitestConfig(env?: Record<string, string | undefined>) {
|
||||
export function createGatewayServerVitestConfig(env?: Record<string, string | undefined>) {
|
||||
return createScopedVitestConfig(
|
||||
["src/gateway/**/*server*.test.ts", ...gatewayServerBackedHttpTests],
|
||||
{
|
||||
@@ -22,6 +22,8 @@ function createGatewayServerVitestConfig(env?: Record<string, string | undefined
|
||||
"src/gateway/sessions-history-http.test.ts",
|
||||
],
|
||||
fileParallelism: false,
|
||||
// Gateway child projects share one include file; preserve this project's ownership.
|
||||
intersectIncludeFile: true,
|
||||
isolate: false,
|
||||
name: "gateway-server",
|
||||
},
|
||||
|
||||
@@ -124,6 +124,111 @@ function patternsCouldOverlap(value: string, pattern: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function narrowIncludePatterns(
|
||||
includePatterns: string[],
|
||||
candidatePatterns: string[] | null,
|
||||
): string[] | null {
|
||||
if (!candidatePatterns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
candidatePatterns.filter((value) =>
|
||||
includePatterns.some((pattern) => patternsCouldOverlap(value, pattern)),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function isPlainRepoRelativePath(value: string): boolean {
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(value) || path.isAbsolute(value)) {
|
||||
return false;
|
||||
}
|
||||
return value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
||||
}
|
||||
|
||||
function directoryTestPatternRoot(value: string): string | null {
|
||||
const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
if (normalized === "**/*.test.ts") {
|
||||
return "";
|
||||
}
|
||||
const suffix = "/**/*.test.ts";
|
||||
if (!normalized.endsWith(suffix)) {
|
||||
return null;
|
||||
}
|
||||
const root = normalized.slice(0, -suffix.length);
|
||||
return isPlainRepoRelativePath(root) ? root : null;
|
||||
}
|
||||
|
||||
function isAtOrUnder(value: string, root: string): boolean {
|
||||
return root === "" || value === root || value.startsWith(`${root}/`);
|
||||
}
|
||||
|
||||
function patternIsFullyUnderDirectory(pattern: string, root: string): boolean {
|
||||
const normalized = pattern.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
if (!normalized.endsWith(".test.ts")) {
|
||||
return false;
|
||||
}
|
||||
const literalPrefix = literalPrefixForGlobPattern(normalized).replace(/\/+$/u, "");
|
||||
return isAtOrUnder(literalPrefix, root);
|
||||
}
|
||||
|
||||
function intersectDirectoryTestPattern(
|
||||
includePatterns: string[],
|
||||
candidatePattern: string,
|
||||
): string[] | null {
|
||||
const candidateRoot = directoryTestPatternRoot(candidatePattern);
|
||||
if (candidateRoot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
let hasAmbiguousOverlap = false;
|
||||
for (const includePattern of includePatterns) {
|
||||
const includeRoot = directoryTestPatternRoot(includePattern);
|
||||
if (includeRoot !== null && isAtOrUnder(candidateRoot, includeRoot)) {
|
||||
return [candidatePattern];
|
||||
} else if (patternIsFullyUnderDirectory(includePattern, candidateRoot)) {
|
||||
result.push(includePattern);
|
||||
} else if (patternsCouldOverlap(candidatePattern, includePattern)) {
|
||||
hasAmbiguousOverlap = true;
|
||||
}
|
||||
}
|
||||
if (hasAmbiguousOverlap) {
|
||||
return null;
|
||||
}
|
||||
return [...new Set(result)];
|
||||
}
|
||||
|
||||
export function intersectIncludePatterns(
|
||||
includePatterns: string[],
|
||||
candidatePatterns: string[] | null,
|
||||
): string[] | null {
|
||||
if (!candidatePatterns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
for (const candidate of candidatePatterns) {
|
||||
if (!isPlainRepoRelativePath(candidate)) {
|
||||
// Watch directory targets retain their glob so newly added tests appear.
|
||||
// Only generated directory globs have a provable ownership intersection.
|
||||
const intersection = intersectDirectoryTestPattern(includePatterns, candidate);
|
||||
if (!intersection) {
|
||||
throw new Error(`cannot safely intersect non-literal include path: ${candidate}`);
|
||||
}
|
||||
result.push(...intersection);
|
||||
continue;
|
||||
}
|
||||
if (includePatterns.some((include) => path.matchesGlob(candidate, include))) {
|
||||
result.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(result)];
|
||||
}
|
||||
|
||||
function loadPatternListFile(filePath: string, label: string): string[] {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
@@ -186,9 +291,5 @@ export function narrowIncludePatternsForCli(
|
||||
return null;
|
||||
}
|
||||
|
||||
const matched = cliPatterns.filter((value) =>
|
||||
includePatterns.some((pattern) => patternsCouldOverlap(value, pattern)),
|
||||
);
|
||||
|
||||
return [...new Set(matched)];
|
||||
return narrowIncludePatterns(includePatterns, cliPatterns);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Vitest scoped config helper builds test configs for scoped file patterns.
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
|
||||
import {
|
||||
intersectIncludePatterns,
|
||||
loadPatternListFromEnv,
|
||||
narrowIncludePatternsForCli,
|
||||
} from "./vitest.pattern-file.ts";
|
||||
import {
|
||||
nonIsolatedRunnerPath,
|
||||
repoRoot,
|
||||
@@ -203,6 +207,7 @@ export function createScopedVitestConfig(
|
||||
isolate?: boolean;
|
||||
name?: string;
|
||||
fileParallelism?: boolean;
|
||||
intersectIncludeFile?: boolean;
|
||||
pool?: "forks" | "threads";
|
||||
passWithNoTests?: boolean;
|
||||
excludeUnitFastTests?: boolean;
|
||||
@@ -216,7 +221,10 @@ export function createScopedVitestConfig(
|
||||
const scopedDir = options?.dir;
|
||||
const resolvedScopedDir = scopedDir ? path.join(repoRoot, scopedDir) : undefined;
|
||||
const env = options?.env;
|
||||
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
|
||||
const externalIncludePatterns = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
|
||||
const includeFromEnv = options?.intersectIncludeFile
|
||||
? intersectIncludePatterns(include, externalIncludePatterns)
|
||||
: externalIncludePatterns;
|
||||
const cliInclude = narrowIncludePatternsForCli(include, options?.argv, {
|
||||
scopedDir,
|
||||
});
|
||||
@@ -230,6 +238,9 @@ export function createScopedVitestConfig(
|
||||
[...(baseTest.exclude ?? []), ...unitFastExcludePatterns, ...(options?.exclude ?? [])],
|
||||
scopedDir,
|
||||
);
|
||||
const scopedEnvInclude = includeFromEnv
|
||||
? relativizeScopedPatterns(includeFromEnv, scopedDir)
|
||||
: includeFromEnv;
|
||||
const scopedCliInclude = cliInclude ? relativizeScopedPatterns(cliInclude, scopedDir) : null;
|
||||
const isolate = options?.isolate ?? resolveVitestIsolation(options?.env);
|
||||
const setupFiles = [
|
||||
@@ -270,9 +281,12 @@ export function createScopedVitestConfig(
|
||||
}),
|
||||
...(options?.passWithNoTests !== undefined
|
||||
? { passWithNoTests: options.passWithNoTests }
|
||||
: shouldPassWithNoTestsForCliIncludes(scopedCliInclude, exclude)
|
||||
: externalIncludePatterns !== null &&
|
||||
shouldPassWithNoTestsForCliIncludes(scopedEnvInclude, exclude)
|
||||
? { passWithNoTests: true }
|
||||
: {}),
|
||||
: shouldPassWithNoTestsForCliIncludes(scopedCliInclude, exclude)
|
||||
? { passWithNoTests: true }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,6 +370,22 @@ async function raceWithMacrotask(promise: Promise<unknown>): Promise<"resolved"
|
||||
]);
|
||||
}
|
||||
|
||||
async function completesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise.then(() => true),
|
||||
new Promise<boolean>((resolve) => {
|
||||
timeout = setTimeout(() => resolve(false), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("refreshChat", () => {
|
||||
beforeAll(async () => {
|
||||
await loadChatHelpers();
|
||||
@@ -4259,12 +4275,7 @@ describe("handleSendChat", () => {
|
||||
});
|
||||
admitHostQueueItems(host);
|
||||
|
||||
const completed = await Promise.race([
|
||||
retryReconnectableQueuedChatSends(host).then(() => true),
|
||||
new Promise<boolean>((resolve) => {
|
||||
setTimeout(() => resolve(false), 500);
|
||||
}),
|
||||
]);
|
||||
const completed = await completesWithin(retryReconnectableQueuedChatSends(host), 500);
|
||||
|
||||
expect(completed).toBe(true);
|
||||
expect(executeSlashCommandMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
Reference in New Issue
Block a user