improve: keep isolated tests under one second (#100019)

* test: speed up isolated test suite

* test: finish isolated latency cleanup

* test: eliminate remaining isolated latency spikes

* test: remove final isolated timing outliers

* test: bound full-suite tooling processes

* test: bound native test process lifetime

* test: warm isolated runtime suites

* test: eliminate final isolated timing outliers

* test: fix isolated timing fixture types

* test: make timeout cleanup timing deterministic

* test: pin media manifests to source checkout

* test: isolate provider manifest contracts

* test: eliminate residual isolated timing spikes

* test: restore final isolated timing fixes

* test: eliminate remaining isolated timing spikes

* test: warm Zalo lifecycle imports

* test: keep isolated suites below one second

* test: use readable browser response fixtures
This commit is contained in:
Peter Steinberger
2026-07-05 10:57:19 -07:00
committed by GitHub
parent 91f188301d
commit c757675f34
93 changed files with 1819 additions and 997 deletions
@@ -146,13 +146,15 @@ describe("pw-session getPageForTargetId", () => {
urls: ["https://alpha.example", "https://beta.example"],
}).pages;
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => [
{ id: "TARGET_A", url: "https://alpha.example" },
{ id: "TARGET_B", url: "https://beta.example" },
],
} as Response);
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify([
{ id: "TARGET_A", url: "https://alpha.example" },
{ id: "TARGET_B", url: "https://beta.example" },
]),
{ headers: { "content-type": "application/json" } },
),
);
try {
const resolved = await getPageForTargetId({
@@ -180,13 +182,15 @@ describe("pw-session getPageForTargetId", () => {
});
const [, pageB] = pages;
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => [
{ id: "TARGET_A", url: "https://alpha.example" },
{ id: "TARGET_B", url: "https://beta.example" },
],
} as Response);
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify([
{ id: "TARGET_A", url: "https://alpha.example" },
{ id: "TARGET_B", url: "https://beta.example" },
]),
{ headers: { "content-type": "application/json" } },
),
);
try {
const resolved = await getPageForTargetId({
@@ -39,9 +39,8 @@ describe("browser tab routes attachOnly loopback profiles", () => {
const fetchMock = vi.fn(async (url: unknown) => {
expect(String(url)).toBe("http://127.0.0.1:9222/json/list");
return {
ok: true,
json: async () => [
return new Response(
JSON.stringify([
{
id: "PAGE-1",
title: "WordPress",
@@ -49,8 +48,9 @@ describe("browser tab routes attachOnly loopback profiles", () => {
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/PAGE-1",
type: "page",
},
],
} as unknown as Response;
]),
{ headers: { "content-type": "application/json" } },
);
});
vi.stubGlobal("fetch", fetchMock);
@@ -551,15 +551,12 @@ export function makeResponse(
body: unknown,
init?: { ok?: boolean; status?: number; text?: string },
): Response {
const ok = init?.ok ?? true;
const status = init?.status ?? 200;
const text = init?.text ?? "";
return {
ok,
const status = init?.status ?? (init?.ok === false ? 500 : 200);
const responseBody = init?.text ?? JSON.stringify(body);
return new Response(responseBody, {
status,
json: async () => body,
text: async () => text,
} as unknown as Response;
headers: { "content-type": "application/json" },
});
}
function mockClearAll(obj: Record<string, { mockClear: () => unknown }>) {
+26 -1
View File
@@ -13,14 +13,39 @@ type FetchWithPreconnect = {
__openclawAcceptsDispatcher: true;
};
type FetchFunction = (...args: unknown[]) => unknown;
// Bounded CDP readers consume arrayBuffer(); keep lightweight json-only test
// responses compatible without weakening the production response boundary.
function addArrayBufferFallback(response: unknown): unknown {
if (!response || typeof response !== "object") {
return response;
}
const partial = response as {
arrayBuffer?: () => Promise<ArrayBuffer>;
json?: () => Promise<unknown>;
};
if (typeof partial.arrayBuffer === "function" || typeof partial.json !== "function") {
return response;
}
partial.arrayBuffer = async () =>
new TextEncoder().encode(JSON.stringify(await partial.json!())).buffer;
return response;
}
/** Adds Browser test preconnect metadata to a fetch-like function. */
export function withBrowserFetchPreconnect<T extends typeof fetch>(fn: T): T & FetchWithPreconnect;
export function withBrowserFetchPreconnect<T extends object>(
fn: T,
): T & FetchWithPreconnect & typeof fetch;
export function withBrowserFetchPreconnect(fn: object) {
return Object.assign(fn, {
const fetchFn = Object.assign(fn as FetchFunction, {
preconnect: (_url: string | URL, _options?: FetchPreconnectOptions) => {},
__openclawAcceptsDispatcher: true as const,
});
return new Proxy(fetchFn, {
async apply(target, thisArg, args) {
return addArrayBufferFallback(await Reflect.apply(target, thisArg, args));
},
});
}
+53 -35
View File
@@ -860,43 +860,61 @@ describe("maybeCompactCodexAppServerSession", () => {
rejectInterrupt: true,
});
const sessionFile = await writeTestBinding();
const nativeSetTimeout = globalThis.setTimeout;
let triggerCompletionTimeout: (() => void) | undefined;
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, delay, ...args) => {
if (delay === 1_000 && !triggerCompletionTimeout) {
triggerCompletionTimeout = () => callback(...args);
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, delay, ...args);
});
const pendingResult = maybeCompactCodexAppServerSessionImpl(
{
sessionId: "session-1",
sessionKey: "agent:main:session-1",
sessionFile,
workspaceDir: tempDir,
trigger: "manual",
config: { agents: { defaults: { compaction: { timeoutSeconds: 1 } } } },
},
{
clientFactory: async () => fake.client,
nativeInterruptGraceMs: 10,
},
);
await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce());
fake.emit({
method: "turn/started",
params: {
threadId: "thread-1",
turn: { id: "compact-turn-configured", threadId: "thread-1", status: "inProgress" },
},
});
try {
const pendingResult = maybeCompactCodexAppServerSessionImpl(
{
sessionId: "session-1",
sessionKey: "agent:main:session-1",
sessionFile,
workspaceDir: tempDir,
trigger: "manual",
config: { agents: { defaults: { compaction: { timeoutSeconds: 1 } } } },
},
{
clientFactory: async () => fake.client,
nativeInterruptGraceMs: 10,
},
);
await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce());
fake.emit({
method: "turn/started",
params: {
threadId: "thread-1",
turn: { id: "compact-turn-configured", threadId: "thread-1", status: "inProgress" },
},
});
await expect(pendingResult).resolves.toMatchObject({
ok: false,
compacted: false,
reason: "codex app-server compaction did not reach terminal state after interruption",
});
expect(fake.request).toHaveBeenCalledWith(
"turn/interrupt",
{
threadId: "thread-1",
turnId: "compact-turn-configured",
},
{ timeoutMs: 10 },
);
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1_000);
expect(triggerCompletionTimeout).toBeDefined();
triggerCompletionTimeout?.();
expect(fake.request).toHaveBeenCalledWith(
"turn/interrupt",
{
threadId: "thread-1",
turnId: "compact-turn-configured",
},
{ timeoutMs: 10 },
);
await expect(pendingResult).resolves.toMatchObject({
ok: false,
compacted: false,
reason: "codex app-server compaction did not reach terminal state after interruption",
});
} finally {
setTimeoutSpy.mockRestore();
}
});
it("detaches a remote thread when its interrupted turn cannot be confirmed", async () => {
@@ -1,6 +1,6 @@
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it, vi } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { createCodexWebSearchProvider as createContractCodexWebSearchProvider } from "../web-search-contract-api.js";
import type { CodexAppServerClient } from "./app-server/client.js";
import type { CodexAppServerStartOptions } from "./app-server/config.js";
@@ -180,6 +180,12 @@ function createConfig(): OpenClawConfig {
};
}
beforeAll(async () => {
// Execution cases share this lazy runtime. Import it once so the first case
// does not absorb module initialization that every later case reuses.
await import("./web-search-provider.runtime.js");
});
describe("codex web search provider", () => {
it("registers a selectable keyless provider contract", () => {
const provider = createContractCodexWebSearchProvider();
+17 -2
View File
@@ -2,10 +2,25 @@
import { describe, expect, it } from "vitest";
import setupEntry from "./setup-entry.js";
type LegacyStateMigrationsApi = typeof import("./legacy-state-migrations-api.js");
const migrationDetector =
(() => []) satisfies LegacyStateMigrationsApi["detectDiscordLegacyStateMigrations"];
const setupEntryLoadOptions = {
createLoaderForTest: (() => (specifier: string) => {
expect(specifier).toMatch(/[\\/]legacy-state-migrations-api\.[jt]s$/u);
return {
detectDiscordLegacyStateMigrations: migrationDetector,
} satisfies Pick<LegacyStateMigrationsApi, "detectDiscordLegacyStateMigrations">;
}) as never,
};
describe("discord setup entry", () => {
it("exposes legacy state migration detector through setup entry metadata", () => {
it("resolves the legacy state migration detector through the setup entry", () => {
expect(setupEntry.kind).toBe("bundled-channel-setup-entry");
expect(setupEntry.features).toEqual({ legacyStateMigrations: true });
expect(setupEntry.loadLegacyStateMigrationDetector?.()).toBeTypeOf("function");
expect(setupEntry.loadLegacyStateMigrationDetector?.(setupEntryLoadOptions)).toBe(
migrationDetector,
);
});
});
@@ -307,7 +307,7 @@ describe("Google image-generation provider", () => {
it("accepts valid multi-image inline JSON responses above the generic provider JSON cap", async () => {
mockGoogleApiKeyAuth();
const imageBytes = Buffer.alloc(6 * 1024 * 1024, 1);
const imageBytes = Buffer.alloc(4 * 1024 * 1024, 1);
const imagePayload = imageBytes.toString("base64");
vi.stubGlobal(
"fetch",
+15 -92
View File
@@ -6,8 +6,23 @@ import type {
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js";
import {
isGoogleGenerativeAiApi,
isGoogleVertexBaseUrl,
normalizeGoogleApiBaseUrl,
normalizeGoogleGenerativeAiBaseUrl,
} from "./src/google-api-base-url.js";
import { isGoogleGemini3ProModel, isGoogleGemini3ThinkingLevelModel } from "./thinking-api.js";
export {
DEFAULT_GOOGLE_API_BASE_URL,
isGoogleGenerativeAiApi,
isGoogleVertexBaseUrl,
isGoogleVertexHostname,
normalizeGoogleApiBaseUrl,
normalizeGoogleGenerativeAiBaseUrl,
} from "./src/google-api-base-url.js";
type GoogleApiCarrier = {
api?: string | null;
};
@@ -17,100 +32,8 @@ type GoogleProviderConfigLike = GoogleApiCarrier & {
models?: ReadonlyArray<GoogleApiCarrier | null | undefined> | null;
};
export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
const GOOGLE_MODEL_ID_PROVIDERS = new Set(["google", "google-gemini-cli", "google-vertex"]);
function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, "");
}
function isCanonicalGoogleApiOriginShorthand(value: string): boolean {
return /^https:\/\/generativelanguage\.googleapis\.com\/?$/i.test(value);
}
function isGoogleGenerativeAiUrl(url: URL): boolean {
return (
url.protocol === "https:" && url.hostname.toLowerCase() === "generativelanguage.googleapis.com"
);
}
function stripUrlUserInfo(url: URL): void {
url.username = "";
url.password = "";
}
const GOOGLE_VERTEX_HOST = "aiplatform.googleapis.com";
const GOOGLE_VERTEX_REGION_HOST_SUFFIX = "-aiplatform.googleapis.com";
const GOOGLE_VERTEX_MULTI_REGION_HOSTS = new Set([
"aiplatform.eu.rep.googleapis.com",
"aiplatform.us.rep.googleapis.com",
]);
export function isGoogleVertexHostname(hostname: string): boolean {
const normalized = hostname.toLowerCase();
return (
normalized === GOOGLE_VERTEX_HOST ||
normalized.endsWith(GOOGLE_VERTEX_REGION_HOST_SUFFIX) ||
GOOGLE_VERTEX_MULTI_REGION_HOSTS.has(normalized)
);
}
export function isGoogleVertexBaseUrl(baseUrl?: string | null): boolean {
const raw = normalizeOptionalString(baseUrl);
if (!raw) {
return false;
}
try {
return isGoogleVertexHostname(new URL(raw).hostname);
} catch {
return false;
}
}
export function normalizeGoogleApiBaseUrl(baseUrl?: string): string {
const raw = trimTrailingSlashes(normalizeOptionalString(baseUrl) || DEFAULT_GOOGLE_API_BASE_URL);
try {
const url = new URL(raw);
url.hash = "";
url.search = "";
stripUrlUserInfo(url);
if (isGoogleGenerativeAiUrl(url)) {
const normalizedPath = trimTrailingSlashes(url.pathname || "");
url.pathname = normalizedPath || "/v1beta";
}
return trimTrailingSlashes(url.toString());
} catch {
if (isCanonicalGoogleApiOriginShorthand(raw)) {
return DEFAULT_GOOGLE_API_BASE_URL;
}
return raw;
}
}
export function isGoogleGenerativeAiApi(api?: string | null): boolean {
return api === "google-generative-ai";
}
export function normalizeGoogleGenerativeAiBaseUrl(baseUrl?: string): string | undefined {
if (!baseUrl) {
return baseUrl;
}
const normalized = normalizeGoogleApiBaseUrl(baseUrl);
try {
const url = new URL(normalized);
stripUrlUserInfo(url);
if (isGoogleGenerativeAiUrl(url)) {
url.pathname = trimTrailingSlashes(url.pathname || "").replace(/\/openai$/i, "") || "/v1beta";
return trimTrailingSlashes(url.toString());
}
} catch {
// `normalizeGoogleApiBaseUrl` already returned the best-effort input form.
}
return normalized;
}
export function resolveGoogleGenerativeAiTransport<TApi extends string | null | undefined>(params: {
provider?: string;
api: TApi;
@@ -3,7 +3,7 @@ import {
isRecord,
normalizeOptionalString as trimToUndefined,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeGoogleApiBaseUrl } from "../provider-policy.js";
import { normalizeGoogleApiBaseUrl } from "./google-api-base-url.js";
const DEFAULT_GEMINI_WEB_SEARCH_MODEL = "gemini-2.5-flash";
@@ -0,0 +1,95 @@
// Lightweight Google API URL normalization shared by provider contract surfaces.
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, "");
}
function isCanonicalGoogleApiOriginShorthand(value: string): boolean {
return /^https:\/\/generativelanguage\.googleapis\.com\/?$/i.test(value);
}
function isGoogleGenerativeAiUrl(url: URL): boolean {
return (
url.protocol === "https:" && url.hostname.toLowerCase() === "generativelanguage.googleapis.com"
);
}
function stripUrlUserInfo(url: URL): void {
url.username = "";
url.password = "";
}
const GOOGLE_VERTEX_HOST = "aiplatform.googleapis.com";
const GOOGLE_VERTEX_REGION_HOST_SUFFIX = "-aiplatform.googleapis.com";
const GOOGLE_VERTEX_MULTI_REGION_HOSTS = new Set([
"aiplatform.eu.rep.googleapis.com",
"aiplatform.us.rep.googleapis.com",
]);
export function isGoogleVertexHostname(hostname: string): boolean {
const normalized = hostname.toLowerCase();
return (
normalized === GOOGLE_VERTEX_HOST ||
normalized.endsWith(GOOGLE_VERTEX_REGION_HOST_SUFFIX) ||
GOOGLE_VERTEX_MULTI_REGION_HOSTS.has(normalized)
);
}
export function isGoogleVertexBaseUrl(baseUrl?: string | null): boolean {
const raw = normalizeOptionalString(baseUrl);
if (!raw) {
return false;
}
try {
return isGoogleVertexHostname(new URL(raw).hostname);
} catch {
return false;
}
}
export function normalizeGoogleApiBaseUrl(baseUrl?: string): string {
const raw = trimTrailingSlashes(normalizeOptionalString(baseUrl) || DEFAULT_GOOGLE_API_BASE_URL);
try {
const url = new URL(raw);
url.hash = "";
url.search = "";
stripUrlUserInfo(url);
if (isGoogleGenerativeAiUrl(url)) {
const normalizedPath = trimTrailingSlashes(url.pathname || "");
url.pathname = normalizedPath || "/v1beta";
}
return trimTrailingSlashes(url.toString());
} catch {
if (isCanonicalGoogleApiOriginShorthand(raw)) {
return DEFAULT_GOOGLE_API_BASE_URL;
}
return raw;
}
}
export function isGoogleGenerativeAiApi(api?: string | null): boolean {
return api === "google-generative-ai";
}
export function normalizeGoogleGenerativeAiBaseUrl(baseUrl?: string): string | undefined {
if (!baseUrl) {
return baseUrl;
}
const normalized = normalizeGoogleApiBaseUrl(baseUrl);
try {
const url = new URL(normalized);
stripUrlUserInfo(url);
if (isGoogleGenerativeAiUrl(url)) {
url.pathname = trimTrailingSlashes(url.pathname || "").replace(/\/openai$/i, "") || "/v1beta";
return trimTrailingSlashes(url.toString());
}
} catch {
// `normalizeGoogleApiBaseUrl` already returned the best-effort input form.
}
return normalized;
}
+1
View File
@@ -23,6 +23,7 @@ const getIMessageSetupStatus = createPluginSetupWizardStatus({
const spawnMock = vi.hoisted(() => vi.fn());
const setupToolsMocks = vi.hoisted(() => ({
detectBinary: vi.fn(async () => false),
formatDocsLink: vi.fn((path: string) => path),
}));
vi.mock("openclaw/plugin-sdk/setup-tools", async (importOriginal) => ({
@@ -3217,45 +3217,31 @@ describe("short-term promotion", () => {
});
});
it("uses score tie-breakers when capping stores with invalid timestamps", async () => {
await withTempWorkspace(async (workspaceDir) => {
const maxEntries = testing.SHORT_TERM_RECALL_MAX_ENTRIES;
await testing.writeRawRecallStore(workspaceDir, {
version: 1,
updatedAt: "2026-04-04T00:00:00.000Z",
entries: Object.fromEntries(
Array.from({ length: maxEntries + 3 }, (_, index) => [
`entry-${index}`,
{
key: `entry-${index}`,
path: "memory/2026-04-01.md",
startLine: index + 1,
endLine: index + 1,
source: "memory",
snippet: `Invalid timestamp recall ${index}`,
recallCount: 1,
dailyCount: 0,
groundedCount: 0,
totalScore: index,
maxScore: 0.75,
firstRecalledAt: "not-a-date",
lastRecalledAt: "not-a-date",
queryHashes: [`q-${index}`],
recallDays: ["2026-04-01"],
conceptTags: [],
},
]),
),
});
it("uses score tie-breakers when retention timestamps are invalid", () => {
const entry = {
key: "lower-score",
path: "memory/2026-04-01.md",
startLine: 1,
endLine: 1,
source: "memory" as const,
snippet: "Invalid timestamp recall",
recallCount: 1,
dailyCount: 0,
groundedCount: 0,
totalScore: 1,
maxScore: 0.75,
firstRecalledAt: "not-a-date",
lastRecalledAt: "not-a-date",
queryHashes: ["q"],
recallDays: ["2026-04-01"],
conceptTags: [],
};
const higherScoreEntry = { ...entry, key: "higher-score", totalScore: 2 };
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
expect(repair.removedOverflowEntries).toBe(3);
const entries = await readRecallStoreEntries(workspaceDir);
expect(Object.keys(entries)).toHaveLength(maxEntries);
expect(entries["entry-0"]).toBeUndefined();
expect(entries[`entry-${maxEntries + 2}`]).toBeDefined();
});
expect([entry, higherScoreEntry].toSorted(testing.compareShortTermRecallRetention)).toEqual([
higherScoreEntry,
entry,
]);
});
it("rejects long contaminated legacy recall entries before truncating snippets", async () => {
@@ -2913,6 +2913,7 @@ export const testing = {
deriveConceptTags,
calculateConsolidationComponent,
calculatePhaseSignalBoost,
compareShortTermRecallRetention,
buildClaimHash,
totalSignalCountForEntry,
isContaminatedDreamingSnippet,
+2 -2
View File
@@ -350,7 +350,7 @@ describe("OpenAI embedding batch output", () => {
],
wait: true,
concurrency: 1,
pollIntervalMs: 1000,
pollIntervalMs: 1,
timeoutMs: 60_000,
}),
).rejects.toThrow(/openai\.batch-status/);
@@ -630,7 +630,7 @@ describe("OpenAI embedding batch output", () => {
],
wait: true,
concurrency: 1,
pollIntervalMs: 1000,
pollIntervalMs: 1,
timeoutMs: 60_000,
}),
).rejects.toThrow(/openai batch status failed: 400 batch status unavailable/);
+13 -5
View File
@@ -2,7 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const {
runQaManualLane,
@@ -89,6 +89,7 @@ import {
} from "./cli.runtime.js";
import { QaSuiteInfraError } from "./errors.js";
import { QA_EVIDENCE_FILENAME } from "./evidence-summary.js";
import { loadNonYamlScenarioRefs } from "./live-transports/shared/live-transport-scenarios.js";
import { runQaTelegramCommand } from "./live-transports/telegram/cli.runtime.js";
import { defaultQaModelForMode as defaultQaProviderModelForMode } from "./model-selection.js";
import type { QaProviderModeInput } from "./run-config.js";
@@ -1636,11 +1637,18 @@ describe("qa cli runtime", () => {
).rejects.toThrow("--token-efficiency requires --runtime-axis.");
});
it("prints a markdown coverage report from scenario metadata", async () => {
await runQaCoverageReportCommand({ repoRoot: process.cwd() });
describe("coverage inventory command", () => {
beforeAll(async () => {
listTelegramQaScenarioCatalog.mockReturnValue([]);
await loadNonYamlScenarioRefs();
});
expectWriteContains(stdoutWrite, "# QA Coverage Inventory");
expectWriteContains(stdoutWrite, "memory.recall");
it("prints a markdown report from scenario metadata", async () => {
await runQaCoverageReportCommand({ repoRoot: process.cwd() });
expectWriteContains(stdoutWrite, "# QA Coverage Inventory");
expectWriteContains(stdoutWrite, "memory.recall");
});
});
it("prints a focused scenario match report from coverage metadata", async () => {
@@ -127,6 +127,7 @@ describe("qa runner model catalog", () => {
const runPromise = loadQaRunnerModelOptions({
repoRoot,
signal: controller.signal,
abortKillGraceMs: 100,
});
await waitForFile(pidPath, 2_000);
+10 -7
View File
@@ -173,7 +173,12 @@ async function waitForProcessTreeExit(pid: number | undefined, timeoutMs: number
return !processTreeIsAlive(pid);
}
export async function loadQaRunnerModelOptions(params: { repoRoot: string; signal?: AbortSignal }) {
export async function loadQaRunnerModelOptions(params: {
repoRoot: string;
signal?: AbortSignal;
abortKillGraceMs?: number;
}) {
const abortKillGraceMs = Math.max(1, params.abortKillGraceMs ?? CATALOG_ABORT_KILL_GRACE_MS);
const tempRoot = await fs.mkdtemp(
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-qa-model-catalog-"),
);
@@ -240,9 +245,7 @@ export async function loadQaRunnerModelOptions(params: { repoRoot: string; signa
const finishAbortedCatalogLoad = async () => {
cleanupAbortListener();
const graceRemainingMs =
forceKillAt === undefined
? CATALOG_ABORT_KILL_GRACE_MS
: Math.max(0, forceKillAt - Date.now());
forceKillAt === undefined ? abortKillGraceMs : Math.max(0, forceKillAt - Date.now());
if (graceRemainingMs > 0) {
await waitForProcessTreeExit(child.pid, graceRemainingMs);
}
@@ -252,18 +255,18 @@ export async function loadQaRunnerModelOptions(params: { repoRoot: string; signa
}
if (processTreeIsAlive(child.pid)) {
killProcessTree(child.pid, "SIGKILL");
await waitForProcessTreeExit(child.pid, CATALOG_ABORT_KILL_GRACE_MS);
await waitForProcessTreeExit(child.pid, abortKillGraceMs);
}
forceKillAt = undefined;
};
const abortCatalogLoad = () => {
aborted = true;
killProcessTree(child.pid, "SIGTERM");
forceKillAt = Date.now() + CATALOG_ABORT_KILL_GRACE_MS;
forceKillAt = Date.now() + abortKillGraceMs;
forceKillTimer ??= setTimeout(() => {
forceKillAt = undefined;
killProcessTree(child.pid, "SIGKILL");
}, CATALOG_ABORT_KILL_GRACE_MS);
}, abortKillGraceMs);
forceKillTimer.unref();
};
if (aborted) {
@@ -28,10 +28,11 @@ afterEach(async () => {
}
});
async function startMockServer() {
async function startMockServer(params?: { finalOnlyMarkerPauseMs?: number }) {
const server = await startQaMockOpenAiServer({
host: "127.0.0.1",
port: 0,
...params,
});
cleanups.push(async () => {
await server.stop();
@@ -336,6 +337,37 @@ describe("qa mock openai server", () => {
expect(text.match(/[.!?]+(?:\s|$)/g)).toHaveLength(2);
});
it("keeps final-only marker preview deltas separate from the final answer", async () => {
const server = await startMockServer({ finalOnlyMarkerPauseMs: 1 });
const response = await fetch(`${server.baseUrl}/v1/responses`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
stream: true,
input: [
makeUserInput(
"Final-only marker streaming QA check. Reply exactly: QA-FINAL-ONLY-STREAMING-OK",
),
],
}),
});
expect(response.status).toBe(200);
const responseBody = await response.text();
const deltaText = responseBody
.split("\n")
.filter((line) => line.startsWith("data: {"))
.map((line) => JSON.parse(line.slice("data: ".length)) as { type?: string; delta?: string })
.filter((event) => event.type === "response.output_text.delta")
.map((event) => event.delta ?? "")
.join("");
expect(deltaText).toBe("QA streaming preview in progress");
expect(deltaText).not.toContain("QA-FINAL-ONLY-STREAMING-OK");
expect(responseBody).toContain('"text":"QA-FINAL-ONLY-STREAMING-OK"');
});
it("emits deterministic text deltas for generic streaming QA prompts", async () => {
const server = await startMockServer();
@@ -355,33 +387,6 @@ describe("qa mock openai server", () => {
expect(quietBody).toContain('"phase":"final_answer"');
expect(quietBody).toContain("QA_STREAMING_OK");
const finalOnlyMarkerResponse = await fetch(`${server.baseUrl}/v1/responses`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
stream: true,
input: [
makeUserInput(
"Final-only marker streaming QA check. Reply exactly: QA-FINAL-ONLY-STREAMING-OK",
),
],
}),
});
expect(finalOnlyMarkerResponse.status).toBe(200);
const finalOnlyMarkerBody = await finalOnlyMarkerResponse.text();
const finalOnlyMarkerDeltaText = finalOnlyMarkerBody
.split("\n")
.filter((line) => line.startsWith("data: {"))
.map((line) => JSON.parse(line.slice("data: ".length)) as { type?: string; delta?: string })
.filter((event) => event.type === "response.output_text.delta")
.map((event) => event.delta ?? "")
.join("");
expect(finalOnlyMarkerDeltaText).toBe("QA streaming preview in progress");
expect(finalOnlyMarkerDeltaText).not.toContain("QA-FINAL-ONLY-STREAMING-OK");
expect(finalOnlyMarkerBody).toContain('"text":"QA-FINAL-ONLY-STREAMING-OK"');
const partialResponse = await fetch(`${server.baseUrl}/v1/responses`, {
method: "POST",
headers: {
@@ -3639,8 +3639,13 @@ async function buildMessagesPayload(
return { events, input, extracted, responseBody, streamEvents, model: normalizedModel };
}
export async function startQaMockOpenAiServer(params?: { host?: string; port?: number }) {
export async function startQaMockOpenAiServer(params?: {
host?: string;
port?: number;
finalOnlyMarkerPauseMs?: number;
}) {
const host = params?.host ?? "127.0.0.1";
const finalOnlyMarkerPauseMs = params?.finalOnlyMarkerPauseMs ?? 1_500;
const scenarioState: MockScenarioState = {
anthropicThinkingErrorPhase: 0,
subagentFanoutPhase: 0,
@@ -3791,7 +3796,7 @@ export async function startQaMockOpenAiServer(params?: { host?: string; port?: n
return;
}
if (QA_FINAL_ONLY_MARKER_STREAMING_PROMPT_RE.test(allInputText)) {
await writeSseWithPreviewPause(res, events, 1_500);
await writeSseWithPreviewPause(res, events, finalOnlyMarkerPauseMs);
} else {
writeSse(res, events);
}
@@ -0,0 +1,25 @@
// QA Lab web_search metadata shared by runtime and contract-only loading.
import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract";
export const QA_LAB_WEB_SEARCH_PROVIDER_ID = "qa-lab-search";
export const QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY = "OPENCLAW_QA_WEB_SEARCH_DENIED_INPUT";
export function createQaLabWebSearchProviderBase(): Omit<WebSearchProviderPlugin, "createTool"> {
return {
id: QA_LAB_WEB_SEARCH_PROVIDER_ID,
label: "QA Lab Search",
hint: "Deterministic QA-only web search fixture",
requiresCredential: false,
envVars: [],
placeholder: "(no key needed)",
signupUrl: "https://docs.openclaw.ai/concepts/qa-e2e-automation",
docsUrl: "https://docs.openclaw.ai/concepts/qa-e2e-automation",
credentialPath: "",
inactiveSecretPaths: [],
getCredentialValue: () => undefined,
setCredentialValue: (searchConfigTarget, value) => {
void searchConfigTarget;
void value;
},
};
}
@@ -7,9 +7,15 @@ import {
wrapWebContent,
type WebSearchProviderPlugin,
} from "openclaw/plugin-sdk/provider-web-search";
import {
createQaLabWebSearchProviderBase,
QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY,
} from "./qa-web-search-provider.shared.js";
export const QA_LAB_WEB_SEARCH_PROVIDER_ID = "qa-lab-search";
export const QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY = "OPENCLAW_QA_WEB_SEARCH_DENIED_INPUT";
export {
QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY,
QA_LAB_WEB_SEARCH_PROVIDER_ID,
} from "./qa-web-search-provider.shared.js";
const QaLabWebSearchSchema = {
type: "object",
@@ -44,21 +50,7 @@ function buildQaLabSearchResult(query: string, index: number) {
export function createQaLabWebSearchProvider(): WebSearchProviderPlugin {
return {
id: QA_LAB_WEB_SEARCH_PROVIDER_ID,
label: "QA Lab Search",
hint: "Deterministic QA-only web search fixture",
requiresCredential: false,
envVars: [],
placeholder: "(no key needed)",
signupUrl: "https://docs.openclaw.ai/concepts/qa-e2e-automation",
docsUrl: "https://docs.openclaw.ai/concepts/qa-e2e-automation",
credentialPath: "",
inactiveSecretPaths: [],
getCredentialValue: () => undefined,
setCredentialValue: (searchConfigTarget, value) => {
void searchConfigTarget;
void value;
},
...createQaLabWebSearchProviderBase(),
createTool: () => ({
description:
"Search a deterministic QA Lab fixture corpus. This provider is for QA runtime parity only and never calls the public web.",
@@ -290,14 +290,15 @@ describe("qa suite gateway helpers", () => {
});
const { env } = createConfigMutationEnv(gatewayCall);
await expect(
patchConfig({
env,
patch: { tools: { deny: ["read"] } },
replacePaths: ["tools.deny"],
restartDelayMs: 0,
}),
).resolves.toEqual({ ok: true });
const mutation = patchConfig({
env,
patch: { tools: { deny: ["read"] } },
replacePaths: ["tools.deny"],
restartDelayMs: 0,
restartSettleBufferMs: 1,
});
await expect(mutation).resolves.toEqual({ ok: true });
expect(gatewayCall).toHaveBeenCalledWith(
"config.patch",
+17 -3
View File
@@ -76,10 +76,11 @@ async function waitForConfigRestartSettle(
env: Pick<QaSuiteRuntimeEnv, "gateway" | "transport">,
restartDelayMs = 1_000,
timeoutMs = 60_000,
settleBufferMs = 750,
) {
const startedAt = Date.now();
const deadline = startedAt + timeoutMs;
const readyAfterMs = restartDelayMs + 750;
const readyAfterMs = restartDelayMs + settleBufferMs;
let lastHealthError: unknown = null;
while (Date.now() < deadline) {
@@ -244,6 +245,7 @@ async function runConfigMutation(params: {
};
note?: string;
restartDelayMs?: number;
restartSettleBufferMs?: number;
replacePaths?: readonly string[];
}) {
const restartDelayMs = params.restartDelayMs ?? 1_000;
@@ -271,7 +273,12 @@ async function runConfigMutation(params: {
},
{ timeoutMs },
);
await waitForConfigRestartSettle(params.env, restartDelayMs, timeoutMs);
await waitForConfigRestartSettle(
params.env,
restartDelayMs,
timeoutMs,
params.restartSettleBufferMs,
);
return result;
} catch (error) {
if (isConfigHashConflict(error)) {
@@ -292,7 +299,12 @@ async function runConfigMutation(params: {
if (!isGatewayRestartRace(error)) {
throw error;
}
await waitForConfigRestartSettle(params.env, restartDelayMs, timeoutMs);
await waitForConfigRestartSettle(
params.env,
restartDelayMs,
timeoutMs,
params.restartSettleBufferMs,
);
const postRestartSnapshot = await readConfigSnapshot(params.env);
if (isConfigMutationNoopForSnapshot(params.action, postRestartSnapshot.config, params.raw)) {
return { ok: true, restarted: true };
@@ -321,6 +333,7 @@ async function patchConfig(params: {
};
note?: string;
restartDelayMs?: number;
restartSettleBufferMs?: number;
replacePaths?: readonly string[];
}) {
return await runConfigMutation({
@@ -331,6 +344,7 @@ async function patchConfig(params: {
deliveryContext: params.deliveryContext,
note: params.note,
restartDelayMs: params.restartDelayMs,
restartSettleBufferMs: params.restartSettleBufferMs,
replacePaths: params.replacePaths,
});
}
+2 -4
View File
@@ -1,12 +1,10 @@
// Qa Lab API module exposes the deterministic QA web_search contract.
import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract";
import { createQaLabWebSearchProvider as createQaLabRuntimeWebSearchProvider } from "./src/qa-web-search-provider.js";
import { createQaLabWebSearchProviderBase } from "./src/qa-web-search-provider.shared.js";
export function createQaLabWebSearchProvider(): WebSearchProviderPlugin {
const { createTool: _createTool, ...provider } = createQaLabRuntimeWebSearchProvider();
void _createTool;
return {
...provider,
...createQaLabWebSearchProviderBase(),
createTool: () => null,
};
}
@@ -1,5 +1,8 @@
// Telegram tests cover bot native commands.session meta plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
import { resolveChunkMode } from "openclaw/plugin-sdk/reply-dispatch-runtime";
import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { TelegramNativeCommandDeps } from "./bot-native-command-deps.runtime.js";
@@ -26,9 +29,13 @@ type DispatchReplyWithBufferedBlockDispatcherParams =
type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
ReturnType<DispatchReplyWithBufferedBlockDispatcherFn>
>;
type ResolveCommandArgMenuFn =
typeof import("openclaw/plugin-sdk/command-auth-native").resolveCommandArgMenu;
type DeliverRepliesFn = typeof import("./bot/delivery.js").deliverReplies;
type DeliverRepliesParams = Parameters<DeliverRepliesFn>[0];
type LoadModelCatalogFn = typeof import("openclaw/plugin-sdk/agent-runtime").loadModelCatalog;
type ResolveDefaultModelForAgentFn =
typeof import("openclaw/plugin-sdk/agent-runtime").resolveDefaultModelForAgent;
type MatchPluginCommandFn = typeof import("./bot-native-commands.runtime.js").matchPluginCommand;
const dispatchReplyResult: DispatchReplyWithBufferedBlockDispatcherResult = {
@@ -53,7 +60,7 @@ const sessionMocks = vi.hoisted(() => ({
resolveStorePath: vi.fn(),
}));
const commandAuthMocks = vi.hoisted(() => ({
resolveCommandArgMenu: vi.fn(),
resolveCommandArgMenu: vi.fn<ResolveCommandArgMenuFn>(),
}));
const agentRuntimeMocks = vi.hoisted(() => ({
loadModelCatalog: vi.fn<LoadModelCatalogFn>(async () => [
@@ -64,6 +71,7 @@ const agentRuntimeMocks = vi.hoisted(() => ({
reasoning: true,
},
]),
resolveDefaultModelForAgent: vi.fn<ResolveDefaultModelForAgentFn>(),
}));
const pluginRuntimeMocks = vi.hoisted(() => ({
executePluginCommand: vi.fn(async () => ({ text: "ok" })),
@@ -194,18 +202,48 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/agent-runtime")>(
"openclaw/plugin-sdk/agent-runtime",
);
return {
...actual,
loadModelCatalog: agentRuntimeMocks.loadModelCatalog,
};
});
vi.mock("./bot-native-commands.runtime.js", async () => {
const actual = await vi.importActual<typeof import("./bot-native-commands.runtime.js")>(
"./bot-native-commands.runtime.js",
agentRuntimeMocks.resolveDefaultModelForAgent.mockImplementation(
actual.resolveDefaultModelForAgent,
);
return {
...actual,
loadModelCatalog: agentRuntimeMocks.loadModelCatalog,
resolveDefaultModelForAgent: agentRuntimeMocks.resolveDefaultModelForAgent,
};
});
vi.mock("./bot-native-commands.runtime.js", () => {
return {
ensureConfiguredBindingRouteReady: persistentBindingMocks.ensureConfiguredBindingRouteReady,
executePluginCommand: pluginRuntimeMocks.executePluginCommand,
finalizeInboundContext: vi.fn((ctx: unknown) => ctx),
getAgentScopedMediaLocalRoots,
getPluginCommandSpecs: vi.fn(() => []),
getSessionEntry: sessionMocks.getSessionEntry,
matchPluginCommand: pluginRuntimeMocks.matchPluginCommand,
recordInboundSessionMetaSafe: vi.fn(
async (params: {
cfg: OpenClawConfig;
agentId: string;
sessionKey: string;
ctx: unknown;
onError?: (error: unknown) => void;
}) => {
const storePath = sessionMocks.resolveStorePath(params.cfg.session?.store, {
agentId: params.agentId,
});
try {
await sessionMocks.recordSessionMetaFromInbound({
storePath,
sessionKey: params.sessionKey,
ctx: params.ctx,
});
} catch (error) {
params.onError?.(error);
}
},
),
resolveChunkMode,
resolveThreadSessionKeys,
dispatchReplyWithBufferedBlockDispatcher: replyMocks.dispatchReplyWithBufferedBlockDispatcher,
};
});
@@ -556,52 +594,79 @@ function expectUnauthorizedNewCommandBlocked(sendMessage: ReturnType<typeof vi.f
});
}
function resetSessionMetaMocks() {
persistentBindingMocks.resolveConfiguredBindingRoute.mockClear();
persistentBindingMocks.resolveConfiguredBindingRoute.mockImplementation(({ route }) =>
createConfiguredBindingRoute(route, null),
);
persistentBindingMocks.ensureConfiguredBindingRouteReady.mockClear();
persistentBindingMocks.ensureConfiguredBindingRouteReady.mockResolvedValue({ ok: true });
commandAuthMocks.resolveCommandArgMenu.mockClear().mockImplementation(({ command, args }) => {
if (args?.raw || (args?.values && Object.keys(args.values).length > 0)) {
return null;
}
const arg = command.args?.[0];
if (!arg) {
return null;
}
if (command.key === "think") {
return {
arg,
choices: ["low", "medium", "high"].map((value) => ({ label: value, value })),
};
}
if (command.key === "fast") {
const choices = ["on", "off", "auto (30 sec)", "default", "status"];
return {
arg,
choices: choices.map((value) => ({ label: value, value })),
};
}
return null;
});
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue([
{
provider: "openai",
id: "gpt-5.5",
name: "GPT-5.5",
reasoning: true,
},
]);
sessionMocks.getSessionEntry.mockClear().mockReturnValue(undefined);
sessionMocks.loadSessionStore.mockClear().mockReturnValue({});
sessionMocks.getSessionEntry.mockImplementation(
({ storePath, sessionKey }: { storePath: string; sessionKey: string }) =>
sessionMocks.loadSessionStore(storePath)[sessionKey],
);
sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined);
sessionMocks.resolveSessionTranscriptLegacyFileTarget.mockClear().mockResolvedValue({
agentId: "main",
memoryKey: "transcript:main:sess-topic",
sessionId: "sess-topic",
sessionKey: "agent:main:telegram:group:-1001234567890:topic:42",
sessionFile: "/tmp/openclaw-sessions/sess-topic-topic-42.jsonl",
targetKind: "runtime-session",
});
sessionMocks.resolveStorePath.mockClear().mockReturnValue("/tmp/openclaw-sessions.json");
pluginRuntimeMocks.executePluginCommand.mockClear().mockResolvedValue({ text: "ok" });
pluginRuntimeMocks.matchPluginCommand.mockClear().mockReturnValue(null);
replyMocks.dispatchReplyWithBufferedBlockDispatcher
.mockClear()
.mockResolvedValue(dispatchReplyResult);
sessionBindingMocks.resolveByConversation.mockReset().mockReturnValue(null);
sessionBindingMocks.touch.mockReset();
deliveryMocks.deliverReplies.mockClear().mockResolvedValue({ delivered: true });
}
describe("registerTelegramNativeCommands — session metadata", () => {
beforeAll(async () => {
({ registerTelegramNativeCommands } = await import("./bot-native-commands.js"));
const commandModule = await import("./bot-native-commands.js");
registerTelegramNativeCommands = commandModule.registerTelegramNativeCommands;
await commandModule.testing.loadNativeCommandRuntime();
agentRuntimeMocks.resolveDefaultModelForAgent({ cfg: {}, agentId: "main" });
});
beforeEach(() => {
persistentBindingMocks.resolveConfiguredBindingRoute.mockClear();
persistentBindingMocks.resolveConfiguredBindingRoute.mockImplementation(({ route }) =>
createConfiguredBindingRoute(route, null),
);
persistentBindingMocks.ensureConfiguredBindingRouteReady.mockClear();
persistentBindingMocks.ensureConfiguredBindingRouteReady.mockResolvedValue({ ok: true });
commandAuthMocks.resolveCommandArgMenu.mockClear();
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue([
{
provider: "openai",
id: "gpt-5.5",
name: "GPT-5.5",
reasoning: true,
},
]);
sessionMocks.getSessionEntry.mockClear().mockReturnValue(undefined);
sessionMocks.loadSessionStore.mockClear().mockReturnValue({});
sessionMocks.getSessionEntry.mockImplementation(
({ storePath, sessionKey }: { storePath: string; sessionKey: string }) =>
sessionMocks.loadSessionStore(storePath)[sessionKey],
);
sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined);
sessionMocks.resolveSessionTranscriptLegacyFileTarget.mockClear().mockResolvedValue({
agentId: "main",
memoryKey: "transcript:main:sess-topic",
sessionId: "sess-topic",
sessionKey: "agent:main:telegram:group:-1001234567890:topic:42",
sessionFile: "/tmp/openclaw-sessions/sess-topic-topic-42.jsonl",
targetKind: "runtime-session",
});
sessionMocks.resolveStorePath.mockClear().mockReturnValue("/tmp/openclaw-sessions.json");
pluginRuntimeMocks.executePluginCommand.mockClear().mockResolvedValue({ text: "ok" });
pluginRuntimeMocks.matchPluginCommand.mockClear().mockReturnValue(null);
replyMocks.dispatchReplyWithBufferedBlockDispatcher
.mockClear()
.mockResolvedValue(dispatchReplyResult);
sessionBindingMocks.resolveByConversation.mockReset().mockReturnValue(null);
sessionBindingMocks.touch.mockReset();
deliveryMocks.deliverReplies.mockClear().mockResolvedValue({ delivered: true });
});
beforeEach(resetSessionMetaMocks);
it("calls recordSessionMetaFromInbound after a native slash command", async () => {
const cfg: OpenClawConfig = {};
@@ -208,6 +208,11 @@ const loadTelegramNativeCommandRuntime = createLazyRuntimeModule(
() => import("./bot-native-commands.runtime.js"),
);
export const testing = {
loadNativeCommandRuntime: loadTelegramNativeCommandRuntime,
};
export { testing as __testing };
type TelegramNativeCommandRuntime = Awaited<ReturnType<typeof loadTelegramNativeCommandRuntime>>;
function resolveTelegramProgressPlaceholder(command: {
+2
View File
@@ -391,6 +391,8 @@ describe("monitorTelegramProvider (grammY)", () => {
beforeAll(async () => {
({ monitorTelegramProvider } = await import("./monitor.js"));
({ resetTelegramPollingLeasesForTests } = await import("./polling-lease.js"));
resetTelegramPollingLeasesForTests();
await monitorWithAutoAbort();
});
beforeEach(() => {
@@ -11,7 +11,7 @@ import type {
OpenKeyedStoreOptions,
PluginDoctorStateMigrationContext,
} from "openclaw/plugin-sdk/runtime-doctor";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { resolveSessionStoreAgentIds, stateMigrations } from "./doctor-contract-api.js";
import {
createTestStorePath,
@@ -52,6 +52,64 @@ describe("voice-call doctor state migration", () => {
let stateDir = "";
let storePath = "";
let env: NodeJS.ProcessEnv;
let overCapacityMigration: {
warnings: string[];
changes: string[];
activeCallIds: Set<string>;
latestProviderCallId: string | undefined;
historyCallIds: string[];
};
beforeAll(async () => {
resetPluginStateStoreForTests();
const warmStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voice-call-doctor-"));
const warmStorePath = createTestStorePath();
const warmEnv = {
...process.env,
HOME: warmStateDir,
OPENCLAW_STATE_DIR: warmStateDir,
};
try {
installStateRuntime();
const calls = Array.from({ length: 1002 }, (_, index) =>
makePersistedCall({
callId: `call-${index}`,
providerCallId: `provider-${index}`,
}),
);
writeLegacyCallsJsonl(warmStorePath, calls);
const config = {
plugins: {
entries: {
"@openclaw/voice-call": {
config: { store: warmStorePath },
},
},
},
};
const result = await stateMigrations[0].migrateLegacyState({
config,
env: warmEnv,
stateDir: warmStateDir,
oauthDir: path.join(warmStateDir, "oauth"),
context: createDoctorContext(warmEnv),
});
const restored = loadActiveCallsFromStore(warmStorePath);
const history = await getCallHistoryFromStore(warmStorePath, 1000);
overCapacityMigration = {
warnings: result.warnings,
changes: result.changes,
activeCallIds: new Set(restored.activeCalls.keys()),
latestProviderCallId: restored.activeCalls.get("call-1001")?.providerCallId,
historyCallIds: history.map((entry) => entry.callId),
};
} finally {
clearVoiceCallStateRuntime();
resetPluginStateStoreForTests();
await fs.rm(warmStateDir, { recursive: true, force: true });
await fs.rm(warmStorePath, { recursive: true, force: true });
}
});
beforeEach(async () => {
resetPluginStateStoreForTests();
@@ -167,49 +225,20 @@ describe("voice-call doctor state migration", () => {
expect(history[0]?.callId).toBe("call-doctor");
});
it("imports the newest legacy call records when the JSONL log is over capacity", async () => {
const calls = Array.from({ length: 1002 }, (_, index) =>
makePersistedCall({
callId: `call-${index}`,
providerCallId: `provider-${index}`,
}),
);
writeLegacyCallsJsonl(storePath, calls);
const config = {
plugins: {
entries: {
"@openclaw/voice-call": {
config: { store: storePath },
},
},
},
};
const result = await stateMigrations[0].migrateLegacyState({
config,
env,
stateDir,
oauthDir: path.join(stateDir, "oauth"),
context: createDoctorContext(env),
});
expect(result.warnings).toEqual([
it("imports the newest legacy call records when the JSONL log is over capacity", () => {
expect(overCapacityMigration.warnings).toEqual([
expect.stringContaining("Pruned 2 older Voice Call call-log records"),
]);
expect(result.changes).toEqual([
expect(overCapacityMigration.changes).toEqual([
expect.stringContaining("Migrated 1000 Voice Call call-log records"),
expect.stringContaining("Archived Voice Call call-log legacy source"),
]);
const restored = loadActiveCallsFromStore(storePath);
expect(restored.activeCalls.has("call-0")).toBe(false);
expect(restored.activeCalls.has("call-1")).toBe(false);
expect(restored.activeCalls.get("call-1001")?.providerCallId).toBe("provider-1001");
const history = await getCallHistoryFromStore(storePath, 1000);
expect(history).toHaveLength(1000);
expect(history[0]?.callId).toBe("call-2");
expect(history.at(-1)?.callId).toBe("call-1001");
expect(overCapacityMigration.activeCallIds.has("call-0")).toBe(false);
expect(overCapacityMigration.activeCallIds.has("call-1")).toBe(false);
expect(overCapacityMigration.latestProviderCallId).toBe("provider-1001");
expect(overCapacityMigration.historyCallIds).toHaveLength(1000);
expect(overCapacityMigration.historyCallIds[0]).toBe("call-2");
expect(overCapacityMigration.historyCallIds.at(-1)).toBe("call-1001");
});
it("leaves malformed mixed legacy logs in place after importing valid records", async () => {
@@ -1,6 +1,6 @@
// Zalo tests cover monitor.pairing.lifecycle plugin behavior.
import { withServer } from "openclaw/plugin-sdk/test-env";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
createLifecycleMonitorSetup,
createTextUpdate,
@@ -8,6 +8,7 @@ import {
settleAsyncWork,
} from "./test-support/lifecycle-test-support.js";
import {
loadCachedLifecycleMonitorModule,
resetLifecycleTestState,
sendMessageMock,
setLifecycleRuntimeCore,
@@ -18,6 +19,10 @@ describe("Zalo pairing lifecycle", () => {
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
beforeAll(async () => {
await loadCachedLifecycleMonitorModule("zalo-pairing-lifecycle");
});
beforeEach(async () => {
await resetLifecycleTestState();
setLifecycleRuntimeCore({
@@ -10,7 +10,7 @@ import {
createRuntimeEnv,
setActivePluginRegistry,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginRuntime } from "../runtime-api.js";
import { setZaloRuntime } from "./runtime.js";
import {
@@ -117,6 +117,10 @@ describe("Zalo polling media replies", () => {
}));
const dispatchReplyWithBufferedBlockDispatcherMock = vi.fn();
beforeAll(async () => {
await loadCachedLifecycleMonitorModule("zalo-polling-media-reply");
});
beforeEach(async () => {
await resetLifecycleTestState();
await clearHostedZaloMediaForTest();
@@ -1,6 +1,6 @@
// Zalo tests cover monitor.reply once.lifecycle plugin behavior.
import { withServer } from "openclaw/plugin-sdk/test-env";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginRuntime } from "../runtime-api.js";
import {
createLifecycleMonitorSetup,
@@ -9,6 +9,7 @@ import {
settleAsyncWork,
} from "./test-support/lifecycle-test-support.js";
import {
loadCachedLifecycleMonitorModule,
resetLifecycleTestState,
sendMessageMock,
setLifecycleRuntimeCore,
@@ -30,6 +31,10 @@ describe("Zalo reply-once lifecycle", () => {
}));
const dispatchReplyWithBufferedBlockDispatcherMock = vi.fn();
beforeAll(async () => {
await loadCachedLifecycleMonitorModule("zalo-reply-once-lifecycle");
});
beforeEach(async () => {
await resetLifecycleTestState();
setLifecycleRuntimeCore({
+32 -21
View File
@@ -14,13 +14,7 @@ import os from "node:os";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { resolveDefaultAgentDir } from "../src/agents/agent-scope.js";
import { ensureAuthProfileStore, type AuthProfileCredential } from "../src/agents/auth-profiles.js";
import { normalizeProviderId } from "../src/agents/model-selection.js";
import { validateAnthropicSetupToken } from "../src/commands/auth-token.js";
import { callGateway } from "../src/gateway/call.js";
import { extractPayloadText } from "../src/gateway/test-helpers.agent-results.js";
import { getFreePortBlockWithPermissionFallback } from "../src/test-utils/ports.js";
import type { AuthProfileCredential } from "../src/agents/auth-profiles.js";
import {
parseBooleanEnv,
parseStrictIntegerOption,
@@ -214,9 +208,10 @@ function isSetupToken(value: string): boolean {
return value.startsWith("sk-ant-oat01-");
}
function listSetupTokenProfiles(store: {
profiles: Record<string, AuthProfileCredential>;
}): Array<{ id: string; token: string }> {
function listSetupTokenProfiles(
store: { profiles: Record<string, AuthProfileCredential> },
normalizeProviderId: (provider: string) => string,
): Array<{ id: string; token: string }> {
return Object.entries(store.profiles)
.filter(([, cred]) => {
if (cred.type !== "token") {
@@ -244,15 +239,25 @@ function pickSetupTokenProfile(candidates: Array<{ id: string; token: string }>)
return candidates[0] ?? null;
}
function validateSetupToken(value: string): string {
const error = validateAnthropicSetupToken(value);
if (error) {
throw new Error(`invalid setup-token: ${error}`);
}
return value;
}
function resolveSetupTokenSource(): TokenSource {
async function resolveSetupTokenSource(): Promise<TokenSource> {
const [
{ resolveDefaultAgentDir },
{ ensureAuthProfileStore },
{ normalizeProviderId },
tokenApi,
] = await Promise.all([
import("../src/agents/agent-scope.js"),
import("../src/agents/auth-profiles.js"),
import("../src/agents/model-selection.js"),
import("../src/commands/auth-token.js"),
]);
const validateSetupToken = (value: string): string => {
const error = tokenApi.validateAnthropicSetupToken(value);
if (error) {
throw new Error(`invalid setup-token: ${error}`);
}
return value;
};
const explicitToken =
(SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || SETUP_TOKEN_VALUE;
if (explicitToken) {
@@ -266,7 +271,7 @@ function resolveSetupTokenSource(): TokenSource {
const store = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false,
});
const candidates = listSetupTokenProfiles(store);
const candidates = listSetupTokenProfiles(store, normalizeProviderId);
if (SETUP_TOKEN_PROFILE) {
const match = candidates.find((entry) => entry.id === SETUP_TOKEN_PROFILE);
if (!match) {
@@ -449,6 +454,7 @@ async function startAnthropicProxy(params: { port: number; upstreamBaseUrl: stri
}
async function getFreePort(): Promise<number> {
const { getFreePortBlockWithPermissionFallback } = await import("../src/test-utils/ports.js");
return await getFreePortBlockWithPermissionFallback({
offsets: [0, 1, 2, 4],
fallbackBase: 44_000,
@@ -747,6 +753,7 @@ function isMissingProcessError(error: unknown): boolean {
}
async function waitForGatewayReady(url: string, token: string): Promise<void> {
const { callGateway } = await import("../src/gateway/call.js");
const deadline = Date.now() + 45_000;
let lastError = "gateway start timeout";
while (Date.now() < deadline) {
@@ -787,7 +794,11 @@ async function readLogTail(logPath: string, maxBytes = GATEWAY_LOG_TAIL_BYTES):
}
async function runGatewayPrompt(prompt: string): Promise<PromptResult> {
const tokenSource = resolveSetupTokenSource();
const tokenSource = await resolveSetupTokenSource();
const [{ callGateway }, { extractPayloadText }] = await Promise.all([
import("../src/gateway/call.js"),
import("../src/gateway/test-helpers.agent-results.js"),
]);
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-prompt-probe-"));
const stateDir = path.join(tmpDir, "state");
const agentDir = path.join(stateDir, "agents", "main", "agent");
+12 -3
View File
@@ -1017,9 +1017,18 @@ export async function runReportPlans(params) {
);
includeEntry = false;
} else {
console.error(
`[test-group-report] config failed; keeping partial report from ${run.reportPath}`,
);
try {
readReportInput({ config: plan.label, reportPath: run.reportPath, run });
console.error(
`[test-group-report] config failed; keeping partial report from ${run.reportPath}`,
);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.error(
`[test-group-report] config failed; skipping unusable JSON report from ${run.reportPath} (${reason})`,
);
includeEntry = false;
}
}
if (!params.args.allowFailures) {
exitCode = run.status || 1;
+38 -5
View File
@@ -44,6 +44,7 @@ import {
import { fullSuiteVitestShards } from "../test/vitest/vitest.test-shards.mjs";
import {
getUnitFastTestFiles,
getUnitFastTimerTestFiles,
resolveUnitFastTestIncludePattern,
resolveUnitFastTimerTestIncludePattern,
} from "../test/vitest/vitest.unit-fast-paths.mjs";
@@ -289,6 +290,8 @@ const BROAD_TOOLING_SCRIPT_TEST_PATTERNS = new Set([
"test/scripts/*.test.ts",
]);
const BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE = 60;
const FULL_SUITE_TOOLING_TEST_TARGET_CHUNK_SIZE = 2;
const FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE = 70;
const TUI_VITEST_CONFIG = "test/vitest/vitest.tui.config.ts";
const TUI_PTY_VITEST_CONFIG = "test/vitest/vitest.tui-pty.config.ts";
const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";
@@ -2353,6 +2356,21 @@ function listBroadToolingScriptTestTargets(pattern, cwd) {
);
}
function listToolingFullSuiteTestTargets(cwd) {
return uniqueOrdered(
[path.join(cwd, "test"), path.join(cwd, "src", "scripts")].flatMap((root) =>
fs.existsSync(root) ? listRepoFilesRecursive(root, cwd) : [],
),
)
.filter((file) => file.endsWith(".test.ts") && classifyTarget(file, cwd) === "tooling")
.toSorted((left, right) => left.localeCompare(right));
}
function listUnitFastFullSuiteTestTargets() {
const timerTargets = new Set(getUnitFastTimerTestFiles());
return getUnitFastTestFiles().filter((file) => !timerTargets.has(file));
}
function createBroadToolingScriptPlans({ config, forwardedArgs, includePatterns, watchMode, cwd }) {
if (watchMode || config !== TOOLING_VITEST_CONFIG || !includePatterns) {
return null;
@@ -4018,11 +4036,26 @@ export function buildFullSuiteVitestRunPlans(args, cwd = process.cwd()) {
const expandShard = expandToProjectConfigs;
const configs = expandShard ? shard.projects : [shard.config];
return configs.flatMap((config) => {
if (expandShard && targetArgs.length === 0 && config === GATEWAY_SERVER_VITEST_CONFIG) {
const chunks = splitTargetChunks(
resolveGatewayServerFullSuiteTargets(cwd),
GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT,
);
if (expandShard && targetArgs.length === 0) {
let chunks = [];
if (config === UNIT_FAST_VITEST_CONFIG) {
const targets = listUnitFastFullSuiteTestTargets();
const chunkCount = Math.ceil(
targets.length / FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE,
);
chunks = splitTargetChunks(targets, chunkCount);
} else if (config === TOOLING_VITEST_CONFIG) {
// Tooling tests spawn package managers and native helpers. Keep native
// process lifetime short enough that unrelated files cannot crash together.
const targets = listToolingFullSuiteTestTargets(cwd);
const chunkCount = Math.ceil(targets.length / FULL_SUITE_TOOLING_TEST_TARGET_CHUNK_SIZE);
chunks = splitTargetChunks(targets, chunkCount);
} else if (config === GATEWAY_SERVER_VITEST_CONFIG) {
chunks = splitTargetChunks(
resolveGatewayServerFullSuiteTargets(cwd),
GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT,
);
}
if (chunks.length > 0) {
return chunks.map((targets) => ({
config,
@@ -1,10 +1,15 @@
/** Tests internal model discovery imports avoid public SDK facade coupling. */
import { describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it } from "vitest";
let modelDiscovery: typeof import("./agent-model-discovery.js");
describe("agent-model-discovery internal runtime", () => {
it("loads without the public agent-sessions SDK facade", async () => {
const module = await import("./agent-model-discovery.js");
expect(typeof module.discoverAuthStorage).toBe("function");
expect(typeof module.discoverModels).toBe("function");
beforeAll(async () => {
modelDiscovery = await import("./agent-model-discovery.js");
});
it("loads without the public agent-sessions SDK facade", () => {
expect(typeof modelDiscovery.discoverAuthStorage).toBe("function");
expect(typeof modelDiscovery.discoverModels).toBe("function");
});
});
@@ -1,7 +1,7 @@
// Covers compaction sanitization for toolResult details and runtime context.
import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
import type { AssistantMessage, ToolResultMessage } from "openclaw/plugin-sdk/llm";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { makeAgentAssistantMessage } from "./test-helpers/agent-message-fixtures.js";
const agentSessionMocks = vi.hoisted(() => ({
@@ -21,13 +21,6 @@ vi.mock("./sessions/index.js", async () => {
let isOversizedForSummary: typeof import("./compaction.js").isOversizedForSummary;
let summarizeWithFallback: typeof import("./compaction.js").summarizeWithFallback;
async function loadFreshCompactionModuleForTest() {
// Reset modules so each test observes the mocked token/summary helpers from a
// fresh compaction import.
vi.resetModules();
({ isOversizedForSummary, summarizeWithFallback } = await import("./compaction.js"));
}
function makeAssistantToolCall(timestamp: number): AssistantMessage {
return makeAgentAssistantMessage({
content: [{ type: "toolCall", id: "call_1", name: "browser", arguments: { action: "tabs" } }],
@@ -52,8 +45,11 @@ function makeToolResultWithDetails(timestamp: number): ToolResultMessage<{ raw:
}
describe("compaction toolResult details stripping", () => {
beforeEach(async () => {
await loadFreshCompactionModuleForTest();
beforeAll(async () => {
({ isOversizedForSummary, summarizeWithFallback } = await import("./compaction.js"));
});
beforeEach(() => {
agentSessionMocks.generateSummary.mockReset();
agentSessionMocks.generateSummary.mockResolvedValue("summary");
agentSessionMocks.estimateTokens.mockReset();
@@ -7,6 +7,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
@@ -50,6 +51,7 @@ function attemptCall(index: number): {
describe("runEmbeddedAgent before_agent_finalize", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -8,6 +8,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
@@ -49,6 +50,7 @@ function firstAttemptParams(): {
describe("runEmbeddedAgent cron before_agent_reply seam", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -9,6 +9,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import { hasCodexAppServerRecoveryRetryBudget } from "./run/codex-app-server-recovery.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./run/types.js";
@@ -86,6 +87,7 @@ function asAttemptParams(value: unknown): EmbeddedRunAttemptParams {
describe("runEmbeddedAgent Codex app-server recovery", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -13,6 +13,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
@@ -20,6 +21,7 @@ let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
describe("runEmbeddedAgent Codex server_error fallback handoff", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -27,6 +27,7 @@ import {
mockedIsLikelyContextOverflowError,
mockedRunEmbeddedAttempt,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
@@ -99,6 +100,7 @@ async function executeWrappedToolOutcome(
describe("post-compaction loop guard wired into runEmbeddedAgent", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
// Re-import after the harness reset so we share module instances with
// the runner. The runner imports both modules through its own graph.
({ diagnosticSessionStates, getDiagnosticSessionState } =
@@ -6,6 +6,7 @@ import { makeAttemptResult } from "./run.overflow-compaction.fixture.js";
import {
loadRunOverflowCompactionHarness,
MockedFailoverError,
mockedClassifyFailoverReason,
mockedFormatAssistantErrorText,
mockedGlobalHookRunner,
mockedIsFailoverAssistantError,
@@ -13,11 +14,13 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
const DEEPSEEK_ERROR_MESSAGE = "429 deepseek rate limit";
const COMPACTION_REMOVED_ERROR_MESSAGE = "current candidate model unavailable";
type CurrentAttemptAssistantWithError = NonNullable<
EmbeddedRunAttemptResult["currentAttemptAssistant"]
> & { errorMessage: string };
@@ -82,6 +85,39 @@ function makeCrossProviderFallbackConfig() {
});
}
function setupCompactionRemovedFallbackAttempt() {
mockedIsFailoverAssistantError.mockImplementation((...args: unknown[]) => {
const assistant = args[0];
return isCurrentAttemptAssistant(assistant) && assistant.provider === "anthropic";
});
mockedClassifyFailoverReason.mockReturnValue("model_not_found");
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({
assistantTexts: [],
lastAssistant: makeAssistantMessageFixture({
stopReason: "error",
errorMessage: COMPACTION_REMOVED_ERROR_MESSAGE,
provider: "anthropic",
model: "test-model",
content: [],
}),
currentAttemptAssistant: undefined,
}),
);
}
function runCompactionRemovedFallbackAttempt() {
return runEmbeddedAgent({
...overflowBaseRunParams,
runId: "run-compaction-fallback-error-context",
config: makeCrossProviderFallbackConfig(),
agentHarnessRuntimeOverride: "openclaw",
provider: "anthropic",
model: "test-model",
modelFallbacksOverride: ["deepseek/deepseek-chat"],
});
}
async function expectDeepseekFallbackError(
promise: Promise<unknown>,
getLastFormattedAssistant: () => unknown,
@@ -97,6 +133,14 @@ async function expectDeepseekFallbackError(
describe("runEmbeddedAgent cross-provider fallback error handling", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent, {
config: makeCrossProviderFallbackConfig(),
agentHarnessRuntimeOverride: "openclaw",
provider: "deepseek",
model: "deepseek-chat",
});
setupCompactionRemovedFallbackAttempt();
await runCompactionRemovedFallbackAttempt().catch(() => undefined);
});
beforeEach(() => {
@@ -142,46 +186,18 @@ describe("runEmbeddedAgent cross-provider fallback error handling", () => {
it("falls back to the session assistant when compaction removes the current attempt slice", async () => {
const getLastFormattedAssistant = captureFormattedAssistant();
const sameCandidateErrorMessage = "429 current candidate rate limit";
mockedIsFailoverAssistantError.mockImplementation((...args: unknown[]) => {
const assistant = args[0];
return isCurrentAttemptAssistant(assistant) && assistant.provider === "anthropic";
});
mockedIsRateLimitAssistantError.mockImplementation((...args: unknown[]) => {
const assistant = args[0];
return isCurrentAttemptAssistant(assistant) && assistant.provider === "anthropic";
});
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({
assistantTexts: [],
lastAssistant: makeAssistantMessageFixture({
stopReason: "error",
errorMessage: sameCandidateErrorMessage,
provider: "anthropic",
model: "test-model",
content: [],
}),
currentAttemptAssistant: undefined,
}),
);
const promise = runEmbeddedAgent({
...overflowBaseRunParams,
runId: "run-compaction-fallback-error-context",
config: makeCrossProviderFallbackConfig(),
agentHarnessRuntimeOverride: "openclaw",
provider: "anthropic",
model: "test-model",
modelFallbacksOverride: ["deepseek/deepseek-chat"],
});
setupCompactionRemovedFallbackAttempt();
const promise = runCompactionRemovedFallbackAttempt();
await expect(promise).rejects.toBeInstanceOf(MockedFailoverError);
await expect(promise).rejects.toThrow(`anthropic/test-model: ${sameCandidateErrorMessage}`);
expect(mockedIsRateLimitAssistantError).toHaveBeenCalledTimes(1);
await expect(promise).rejects.toThrow(
`anthropic/test-model: ${COMPACTION_REMOVED_ERROR_MESSAGE}`,
);
expect(mockedIsFailoverAssistantError).toHaveBeenCalledTimes(1);
expect(getLastFormattedAssistant()).toMatchObject({
provider: "anthropic",
model: "test-model",
errorMessage: sameCandidateErrorMessage,
errorMessage: COMPACTION_REMOVED_ERROR_MESSAGE,
});
});
@@ -9,6 +9,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
@@ -60,6 +61,7 @@ function successAttempt(provider: string, model: string): EmbeddedRunAttemptResu
describe("runEmbeddedAgent silent-error retry", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -12,6 +12,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
@@ -52,6 +53,7 @@ function resolveAttemptFastMode(params: unknown): void {
describe("runEmbeddedAgent fast auto progress", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -18,6 +18,7 @@ import {
mockedSleepWithAbort,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import {
buildAttemptReplayMetadata,
@@ -53,15 +54,7 @@ function resolveIncompleteTurnPayloadText(
describe("runEmbeddedAgent incomplete-turn safety", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
resetRunOverflowCompactionHarnessMocks();
mockedGlobalHookRunner.hasHooks.mockImplementation(() => false);
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({ assistantTexts: ["warmup"] }),
);
await runEmbeddedAgent({
...overflowBaseRunParams,
runId: "run-incomplete-turn-warmup",
});
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -19,6 +19,7 @@ import type {
import { resetCommandQueueStateForTest } from "../../process/command-queue.js";
import type { FailoverReason } from "../embedded-agent-helpers/types.js";
import { clearAgentHarnesses, registerAgentHarness } from "../harness/registry.js";
import { makeAttemptResult } from "./run.overflow-compaction.fixture.js";
import type { buildEmbeddedRunPayloads } from "./run/payloads.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
@@ -810,3 +811,19 @@ export async function loadRunOverflowCompactionHarness(): Promise<{
const { runEmbeddedAgent } = await import("./run.js");
return { runEmbeddedAgent };
}
/** Move one-time runner compilation out of individual behavior timings. */
export async function warmRunOverflowCompactionHarness(
runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent,
params?: Partial<Parameters<typeof runEmbeddedAgent>[0]>,
): Promise<void> {
resetRunOverflowCompactionHarnessMocks();
mockedGlobalHookRunner.hasHooks.mockReturnValue(false);
mockedBuildEmbeddedRunPayloads.mockReturnValue([{ text: "warmup" }]);
mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ assistantTexts: ["warmup"] }));
await runEmbeddedAgent({
...overflowBaseRunParams,
...params,
runId: params?.runId ?? "run-overflow-compaction-harness-warmup",
});
}
@@ -21,6 +21,7 @@ import {
mockedSessionLikelyHasOversizedToolResults,
mockedTruncateOversizedToolResultsInSession,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
@@ -66,6 +67,7 @@ function expectRetryContinuesFromTranscript() {
describe("overflow compaction in run loop", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -50,6 +50,7 @@ import {
mockedWaitForDeferredTurnMaintenanceForSession,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { RunEmbeddedAgentParams } from "./run/params.js";
import type { EmbeddedRunAttemptParams } from "./run/types.js";
@@ -252,6 +253,7 @@ async function waitForRunEvent(events: string[], expected: string): Promise<void
describe("runEmbeddedAgent overflow compaction trigger routing", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -9,6 +9,7 @@ import {
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
@@ -16,6 +17,7 @@ let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
describe("runEmbeddedAgent prompt timeout fallback handoff", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -13,6 +13,7 @@ import {
mockedRunPostCompactionSideEffects,
overflowBaseRunParams,
resetRunOverflowCompactionHarnessMocks,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
@@ -114,6 +115,7 @@ function hookCallAt(index: number, kind: "before" | "after"): [HookEvent, HookCo
describe("timeout-triggered compaction", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { resolveSessionTranscriptPathInDir } from "../../../config/sessions/paths.js";
import {
appendSessionTranscriptEvent,
@@ -43,6 +43,25 @@ const lockOptions = {
};
const tempDirs: string[] = [];
const LARGE_LINEAR_TRANSCRIPT_LINE = `${JSON.stringify({
type: "message",
id: "large-linear-user",
timestamp: "2026-01-01T00:00:00.000Z",
message: {
role: "user",
content: [{ type: "text", text: "界".repeat(Math.ceil((7 * 1024 * 1024) / 3)) }],
},
})}\n`;
const LARGE_LEGACY_DELIVERY_TRANSCRIPT_LINE = `${JSON.stringify({
type: "message",
id: "large-legacy-user",
timestamp: "2026-01-01T00:00:00.000Z",
message: {
role: "user",
content: [{ type: "text", text: "界".repeat(Math.ceil((8 * 1024 * 1024) / 3)) }],
},
})}\n`;
const LARGE_OWNED_TRANSCRIPT_TEXT = "界".repeat(1024 * 1024);
afterEach(async () => {
vi.restoreAllMocks();
@@ -63,6 +82,19 @@ async function createTempSessionFile(): Promise<string> {
return sessionFile;
}
async function readSessionFileTail(sessionFile: string, maxBytes = 512): Promise<string> {
const stat = await fs.stat(sessionFile);
const length = Math.min(maxBytes, stat.size);
const buffer = Buffer.alloc(length);
const handle = await fs.open(sessionFile, "r");
try {
const { bytesRead } = await handle.read(buffer, 0, length, stat.size - length);
return buffer.toString("utf8", 0, bytesRead);
} finally {
await handle.close();
}
}
async function waitUntil(predicate: () => boolean, message: string): Promise<void> {
const deadline = Date.now() + 1_000;
while (!predicate()) {
@@ -1685,64 +1717,68 @@ describe("embedded attempt session lock lifecycle", () => {
expect(controller.hasSessionTakeover()).toBe(false);
});
it("allows parentless delivery mirrors appended to large legacy linear transcripts", async () => {
const sessionFile = await createTempSessionFile();
await fs.appendFile(
sessionFile,
`${JSON.stringify({
type: "message",
id: "large-legacy-user",
timestamp: new Date().toISOString(),
message: {
role: "user",
content: [{ type: "text", text: "x".repeat(8 * 1024 * 1024) }],
describe("large legacy linear transcript delivery", () => {
let result: {
hasTakeover: boolean;
lastLine?: string;
mergedEntries: unknown;
};
beforeAll(async () => {
const sessionFile = await createTempSessionFile();
await fs.appendFile(sessionFile, LARGE_LEGACY_DELIVERY_TRANSCRIPT_LINE, "utf8");
const mergePromptReleasedSessionEntries = vi.fn();
const controller = await createEmbeddedAttemptSessionLockController({
acquireSessionWriteLock,
lockOptions: { ...lockOptions, sessionFile },
mergePromptReleasedSessionEntries,
});
await controller.releaseForPrompt();
const sessionKey = "agent:main:large-linear-delivery";
await withOwnedSessionTranscriptWrites(
{
sessionFile,
sessionKey,
withSessionWriteLock: (operation, options) =>
controller.withSessionWriteLock(operation, options),
},
})}\n`,
"utf8",
);
const mergePromptReleasedSessionEntries = vi.fn();
const controller = await createEmbeddedAttemptSessionLockController({
acquireSessionWriteLock,
lockOptions: { ...lockOptions, sessionFile },
mergePromptReleasedSessionEntries,
async () =>
await runWithOwnedSessionTranscriptWritePublication(
{ sessionFile, sessionKey },
async () =>
await appendSessionTranscriptMessage({
transcriptPath: sessionFile,
message: {
role: "assistant",
content: [{ type: "text", text: "mirrored large transcript delivery" }],
provider: "openclaw",
model: "delivery-mirror",
},
}),
),
);
result = {
hasTakeover: controller.hasSessionTakeover(),
lastLine: (await readSessionFileTail(sessionFile)).trimEnd().split("\n").at(-1),
mergedEntries: mergePromptReleasedSessionEntries.mock.calls[0]?.[0],
};
await controller.dispose();
});
await controller.releaseForPrompt();
const sessionKey = "agent:main:large-linear-delivery";
await withOwnedSessionTranscriptWrites(
{
sessionFile,
sessionKey,
withSessionWriteLock: (operation, options) =>
controller.withSessionWriteLock(operation, options),
},
async () =>
await runWithOwnedSessionTranscriptWritePublication(
{ sessionFile, sessionKey },
async () =>
await appendSessionTranscriptMessage({
transcriptPath: sessionFile,
message: {
role: "assistant",
content: [{ type: "text", text: "mirrored large transcript delivery" }],
provider: "openclaw",
model: "delivery-mirror",
},
}),
),
);
const lastLine = (await fs.readFile(sessionFile, "utf8")).trimEnd().split("\n").at(-1);
expect(lastLine).toBeDefined();
expect(JSON.parse(lastLine ?? "{}")).not.toHaveProperty("parentId");
expect(mergePromptReleasedSessionEntries).toHaveBeenCalledWith([
expect.objectContaining({
type: "message",
parentId: null,
message: expect.objectContaining({ model: "delivery-mirror" }),
}),
]);
expect(controller.hasSessionTakeover()).toBe(false);
it("allows parentless delivery mirrors appended to large legacy linear transcripts", () => {
expect(result.lastLine).toBeDefined();
expect(JSON.parse(result.lastLine ?? "{}")).not.toHaveProperty("parentId");
expect(result.mergedEntries).toEqual([
expect.objectContaining({
type: "message",
parentId: null,
message: expect.objectContaining({ model: "delivery-mirror" }),
}),
]);
expect(result.hasTakeover).toBe(false);
});
});
it("refreshes the prompt fence after an owned write throws", async () => {
@@ -2972,54 +3008,59 @@ describe("embedded attempt session lock lifecycle", () => {
await controller.dispose();
});
it("validates large owned entries after migrating a large linear transcript", async () => {
const sessionFile = await createTempSessionFile();
await fs.appendFile(
sessionFile,
`${JSON.stringify({
type: "message",
id: "large-linear-user",
timestamp: new Date().toISOString(),
message: {
role: "user",
content: [{ type: "text", text: "x".repeat(7 * 1024 * 1024) }],
describe("large owned linear transcript migration", () => {
let result: {
appendedId: string;
hasTakeover: boolean;
mergedEntries: unknown;
persistedParentLink: boolean;
};
beforeAll(async () => {
const sessionFile = await createTempSessionFile();
await fs.appendFile(sessionFile, LARGE_LINEAR_TRANSCRIPT_LINE, "utf8");
const mergePromptReleasedSessionEntries = vi.fn();
const controller = await createEmbeddedAttemptSessionLockController({
acquireSessionWriteLock,
lockOptions: { ...lockOptions, sessionFile },
mergePromptReleasedSessionEntries,
});
await controller.releaseForPrompt();
const appended = await withOwnedSessionTranscriptWrites(
{
sessionFile,
withSessionWriteLock: (operation, options) =>
controller.withSessionWriteLock(operation, options),
},
})}\n`,
"utf8",
);
const mergePromptReleasedSessionEntries = vi.fn();
const controller = await createEmbeddedAttemptSessionLockController({
acquireSessionWriteLock,
lockOptions: { ...lockOptions, sessionFile },
mergePromptReleasedSessionEntries,
async () =>
await appendSessionTranscriptMessage({
transcriptPath: sessionFile,
message: {
role: "assistant",
content: [{ type: "text", text: LARGE_OWNED_TRANSCRIPT_TEXT }],
provider: "anthropic",
model: "sonnet-4.6",
},
}),
);
result = {
appendedId: appended.messageId,
hasTakeover: controller.hasSessionTakeover(),
mergedEntries: mergePromptReleasedSessionEntries.mock.calls[0]?.[0],
persistedParentLink: (await fs.readFile(sessionFile, "utf8")).includes('"parentId"'),
};
await controller.dispose();
});
await controller.releaseForPrompt();
const appended = await withOwnedSessionTranscriptWrites(
{
sessionFile,
withSessionWriteLock: (operation, options) =>
controller.withSessionWriteLock(operation, options),
},
async () =>
await appendSessionTranscriptMessage({
transcriptPath: sessionFile,
message: {
role: "assistant",
content: [{ type: "text", text: "y".repeat(3 * 1024 * 1024) }],
provider: "anthropic",
model: "sonnet-4.6",
},
}),
);
const persisted = await fs.readFile(sessionFile, "utf8");
expect(persisted).toContain('"parentId"');
expect(mergePromptReleasedSessionEntries).toHaveBeenCalledWith([
expect.objectContaining({ type: "message", id: appended.messageId }),
]);
expect(controller.hasSessionTakeover()).toBe(false);
await controller.dispose();
it("validates large owned entries after migrating a large linear transcript", () => {
expect(result.persistedParentLink).toBe(true);
expect(result.mergedEntries).toEqual([
expect.objectContaining({ type: "message", id: result.appendedId }),
]);
expect(result.hasTakeover).toBe(false);
});
});
it("serializes concurrent nested owned transcript publications", async () => {
@@ -10,6 +10,7 @@ import {
mockedGlobalHookRunner,
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import { isEmbeddedAgentRunActive, queueEmbeddedAgentMessageWithOutcome } from "./runs.js";
@@ -18,6 +19,7 @@ let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent;
describe("sessions_yield orchestration", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
@@ -8,6 +8,7 @@ import {
mockedEnsureRuntimePluginsLoaded,
mockedResolveModelAsync,
mockedRunEmbeddedAttempt,
warmRunOverflowCompactionHarness,
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
@@ -44,6 +45,7 @@ function firstAttemptInput(): Record<string, unknown> {
describe("runEmbeddedAgent usage reporting", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
beforeEach(() => {
+7 -4
View File
@@ -1,5 +1,5 @@
// Verifies plugin loading needed before agent harness selection.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
const mocks = vi.hoisted(() => ({
@@ -28,7 +28,12 @@ vi.mock("../../plugins/activation-planner.js", () => ({
describe("ensureSelectedAgentHarnessPlugin", () => {
let ensureSelectedAgentHarnessPlugin: typeof import("./runtime-plugin.js").ensureSelectedAgentHarnessPlugin;
beforeEach(async () => {
beforeAll(async () => {
vi.resetModules();
({ ensureSelectedAgentHarnessPlugin } = await import("./runtime-plugin.js"));
});
beforeEach(() => {
mocks.ensurePluginRegistryLoaded.mockReset();
mocks.resolveActivatableProviderOwnerPluginIds.mockReset();
mocks.resolveBundledProviderCompatPluginIds.mockReset();
@@ -67,8 +72,6 @@ describe("ensureSelectedAgentHarnessPlugin", () => {
({ pluginIds }: { pluginIds: readonly string[] }) =>
pluginIds.filter((pluginId) => pluginId === "memory-core"),
);
vi.resetModules();
({ ensureSelectedAgentHarnessPlugin } = await import("./runtime-plugin.js"));
});
it("loads Codex and the provider owner when an explicit runtime override forces the Codex harness", async () => {
+1
View File
@@ -118,6 +118,7 @@ export class OpenClawStdioClientTransport implements Transport {
async close(): Promise<void> {
const processToClose = this.process ?? this.closingProcess;
this.process = undefined;
this.closingProcess = processToClose;
if (processToClose) {
this.closingProcess = processToClose;
}
+9 -1
View File
@@ -2,8 +2,9 @@
* Regression coverage for model compatibility and live-model curation.
* Exercises catalog compatibility, provider modernity hooks, and live sweep selection.
*/
import path from "node:path";
import type { Api, Model } from "openclaw/plugin-sdk/llm";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const providerRuntimeMocks = vi.hoisted(() => ({
resolveProviderModernModelRef: vi.fn(),
@@ -88,10 +89,17 @@ function expectNativeStreamingSupported(overrides: Partial<Model>): void {
}
beforeEach(() => {
// Endpoint capabilities come from manifests. Keep source tests independent
// from partial dist output left by an earlier build in the same checkout.
vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(process.cwd(), "extensions"));
providerRuntimeMocks.resolveProviderModernModelRef.mockReset();
providerRuntimeMocks.resolveProviderModernModelRef.mockReturnValue(undefined);
});
afterEach(() => {
vi.unstubAllEnvs();
});
describe("normalizeModelCompat — Anthropic baseUrl", () => {
const anthropicBase = (): Model =>
({
@@ -1,9 +1,10 @@
// Verifies models.json planning applies config env vars and discovery scope.
import { beforeAll, describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createConfigRuntimeEnv } from "../config/env-vars.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import { withEnvAsync } from "../test-utils/env.js";
import { __testing as externalAuthTesting } from "./auth-profiles/external-auth.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
replaceRuntimeAuthProfileStoreSnapshots,
@@ -16,6 +17,11 @@ import {
import type { ProviderConfig } from "./models-config.providers.secrets.js";
import { encodePluginModelCatalogRelativePath } from "./plugin-model-catalog.js";
vi.mock("./provider-auth-aliases.js", () => ({
resolveProviderAuthAliasMap: () => Object.create(null) as Record<string, string>,
resolveProviderIdForAuth: (provider: string) => provider.trim().toLowerCase(),
}));
const TEST_ENV_VAR = "OPENCLAW_MODELS_CONFIG_TEST_ENV";
function createImplicitOpenRouterProvider(): ProviderConfig {
@@ -126,6 +132,59 @@ async function resolveProvidersAndCaptureDiscoveryEnv(cfg: OpenClawConfig) {
let unauthenticatedProviderWritePlan: Awaited<ReturnType<typeof planOpenClawModelsJsonWithDeps>>;
let unauthenticatedProviderParsed: { providers?: Record<string, unknown> };
async function planGoogleVertexProfileCatalog() {
const agentDir = "/tmp/openclaw-google-vertex-models-profile";
try {
externalAuthTesting.setResolveExternalAuthProfilesForTest(() => []);
replaceRuntimeAuthProfileStoreSnapshots([
{
store: { version: 1, profiles: {} },
},
{
agentDir,
store: {
version: 1,
profiles: {
"google-vertex:default": {
type: "api_key",
provider: "google-vertex",
keyRef: { source: "env", provider: "default", id: "GOOGLE_CLOUD_API_KEY" },
},
},
},
},
]);
return await planOpenClawModelsJsonWithDeps(
{
cfg: {
agents: {
defaults: {
models: {
"google-vertex/gemini-2.5-pro": {},
},
model: { primary: "google-vertex/gemini-2.5-pro" },
},
},
models: { providers: {} },
},
agentDir,
env: {},
existingRaw: "",
existingParsed: null,
},
{
resolveImplicitProviders: async () => ({
"google-vertex": createImplicitGoogleVertexProvider(),
}),
},
);
} finally {
externalAuthTesting.resetResolveExternalAuthProfilesForTest();
clearRuntimeAuthProfileStoreSnapshots();
}
}
beforeAll(async () => {
// Reused no-auth write plan proves generated providers stay serializable
// even when discovery returns auth-only provider shells.
@@ -154,6 +213,7 @@ beforeAll(async () => {
unauthenticatedProviderParsed = JSON.parse(unauthenticatedProviderWritePlan.contents) as {
providers?: Record<string, unknown>;
};
await planGoogleVertexProfileCatalog();
});
describe("models-config", () => {
@@ -466,67 +526,23 @@ describe("models-config", () => {
});
it("keeps google-vertex static catalog rows when an auth profile supplies the API key", async () => {
const agentDir = "/tmp/openclaw-google-vertex-models-profile";
try {
replaceRuntimeAuthProfileStoreSnapshots([
{
agentDir,
store: {
version: 1,
profiles: {
"google-vertex:default": {
type: "api_key",
provider: "google-vertex",
keyRef: { source: "env", provider: "default", id: "GOOGLE_CLOUD_API_KEY" },
},
},
},
},
]);
const plan = await planGoogleVertexProfileCatalog();
const plan = await planOpenClawModelsJsonWithDeps(
{
cfg: {
agents: {
defaults: {
models: {
"google-vertex/gemini-2.5-pro": {},
},
model: { primary: "google-vertex/gemini-2.5-pro" },
},
},
models: { providers: {} },
},
agentDir,
env: {},
existingRaw: "",
existingParsed: null,
},
{
resolveImplicitProviders: async () => ({
"google-vertex": createImplicitGoogleVertexProvider(),
}),
},
);
expect(plan.action).toBe("write");
if (plan.action !== "write") {
throw new Error("Expected models.json write plan");
}
const parsed = JSON.parse(plan.contents) as {
providers?: Record<
string,
{ apiKey?: string; api?: string; models?: Array<{ id?: string }> }
>;
};
expect(parsed.providers?.["google-vertex"]?.api).toBe("google-vertex");
expect(parsed.providers?.["google-vertex"]?.apiKey).toBe("GOOGLE_CLOUD_API_KEY");
expect(parsed.providers?.["google-vertex"]?.models?.map((model) => model.id)).toEqual([
"gemini-2.5-pro",
]);
} finally {
clearRuntimeAuthProfileStoreSnapshots();
expect(plan.action).toBe("write");
if (plan.action !== "write") {
throw new Error("Expected models.json write plan");
}
const parsed = JSON.parse(plan.contents) as {
providers?: Record<
string,
{ apiKey?: string; api?: string; models?: Array<{ id?: string }> }
>;
};
expect(parsed.providers?.["google-vertex"]?.api).toBe("google-vertex");
expect(parsed.providers?.["google-vertex"]?.apiKey).toBe("GOOGLE_CLOUD_API_KEY");
expect(parsed.providers?.["google-vertex"]?.models?.map((model) => model.id)).toEqual([
"gemini-2.5-pro",
]);
});
it("keeps google-vertex static catalog rows when discovery supplies the ADC marker", async () => {
@@ -31,7 +31,7 @@ vi.mock("./model-auth-env-vars.js", () => {
};
});
vi.mock("../plugin-sdk/provider-http.js", () => ({
vi.mock("./provider-attribution.js", () => ({
// Only the CN endpoint advertises native streaming usage in this contract.
resolveProviderRequestCapabilities: (params: { provider: string; baseUrl?: string }) => ({
supportsNativeStreamingUsageCompat:
@@ -3,7 +3,7 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { loadExtensions } from "./loader.js";
const tempDirs: string[] = [];
@@ -13,7 +13,9 @@ afterEach(async () => {
});
describe("loadExtensions", () => {
it("resolves plugin SDK subpaths in jiti-loaded extensions", async () => {
let result: Awaited<ReturnType<typeof loadExtensions>>;
beforeAll(async () => {
// Extensions import both public SDK helpers and runtime helper subpaths; the
// loader must route those aliases without package-manager involvement.
const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-sdk-"));
@@ -41,8 +43,10 @@ export default async function(api) {
`,
);
const result = await loadExtensions([extensionPath], dir);
result = await loadExtensions([extensionPath], dir);
});
it("resolves plugin SDK subpaths in jiti-loaded extensions", () => {
expect(result.errors).toEqual([]);
expect(result.extensions).toHaveLength(1);
expect(result.extensions[0]?.commands.has("sdk-subpath-probe")).toBe(true);
+27 -19
View File
@@ -1,6 +1,7 @@
import { spawn, type ChildProcessByStdio } from "node:child_process";
import type { Readable } from "node:stream";
import { afterEach, describe, expect, it } from "vitest";
import { type ChildProcess, spawn, type ChildProcessByStdio } from "node:child_process";
import { EventEmitter } from "node:events";
import { PassThrough, type Readable } from "node:stream";
import { afterEach, describe, expect, it, vi } from "vitest";
import { waitForChildProcess } from "./child-process.js";
describe.skipIf(process.platform === "win32")("waitForChildProcess", () => {
@@ -49,22 +50,29 @@ describe.skipIf(process.platform === "win32")("waitForChildProcess", () => {
});
it("bounds draining from a continuously writing descendant", async () => {
child = spawn(
"/bin/sh",
["-c", 'printf "HEAD\\n"; ( while :; do printf "TICK\\n"; sleep 0.03; done ) &'],
{
stdio: ["ignore", "pipe", "pipe"],
detached: true,
},
);
let output = "";
child.stdout.on("data", (chunk: Buffer) => {
output += chunk.toString();
});
vi.useFakeTimers();
try {
const stdout = new PassThrough();
const stderr = new PassThrough();
const fakeChild = Object.assign(new EventEmitter(), {
stdout,
stderr,
}) as unknown as ChildProcess;
let output = "";
stdout.on("data", (chunk: Buffer) => {
output += chunk.toString();
});
const startedAt = Date.now();
await expect(waitForChildProcess(child)).resolves.toBe(0);
expect(output).toContain("TICK");
expect(Date.now() - startedAt).toBeLessThan(2_000);
const completion = waitForChildProcess(fakeChild);
fakeChild.emit("exit", 0);
const writer = setInterval(() => stdout.write("TICK\n"), 30);
await vi.advanceTimersByTimeAsync(1_000);
await expect(completion).resolves.toBe(0);
clearInterval(writer);
expect(output).toContain("TICK");
} finally {
vi.useRealTimers();
}
});
});
@@ -2,7 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { testing as cliBackendsTesting } from "../../agents/cli-backends.js";
import {
testing as embeddedRunTesting,
@@ -254,7 +254,7 @@ function firstMockCallArg(mock: MockCallSource, label: string): unknown {
return call[0];
}
beforeEach(() => {
function setupAgentRunnerMocks(): void {
vi.useRealTimers();
registerCliBackendsForTest();
clearRuntimeConfigSnapshot();
@@ -290,7 +290,9 @@ beforeEach(() => {
model,
}),
);
});
}
beforeEach(setupAgentRunnerMocks);
afterEach(() => {
cliBackendsTesting.resetDepsForTest();
@@ -432,6 +434,17 @@ describe("runReplyAgent auto-compaction token update", () => {
return { sessionKey, stored, usageEvent };
}
beforeAll(async () => {
setupAgentRunnerMocks();
await runBaseReplyWithAgentMeta({
tmpPrefix: "openclaw-usage-warm-",
agentMeta: {
usage: { input: 10, output: 5, total: 15 },
lastCallUsage: { input: 8, output: 2, total: 10 },
},
});
});
it("updates totalTokens from lastCallUsage even without compaction", async () => {
const { sessionKey, stored } = await runBaseReplyWithAgentMeta({
tmpPrefix: "openclaw-usage-last-",
@@ -1,5 +1,5 @@
// Tests session lifecycle commands for fork, reset, restart, and cleanup.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import type { HandleCommandsParams } from "./commands-types.js";
@@ -495,10 +495,8 @@ function expectIdleTimeoutSetReply(
}
describe("/session idle and /session max-age", () => {
beforeEach(async () => {
if (!handleSessionCommand) {
({ handleSessionCommand } = await import("./commands-session.js"));
}
beforeAll(async () => {
({ handleSessionCommand } = await import("./commands-session.js"));
});
beforeEach(() => {
@@ -1483,6 +1483,22 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
};
}
beforeAll(async () => {
const sessionEntry = createSessionEntry({ thinkingLevel: "xhigh" });
await handleDirectiveOnly(
createHandleParams({
directives: parseInlineDirectives("/model opencode/claude-opus-4-7"),
allowedModelKeys: new Set([...allowedModelKeys, "opencode/claude-opus-4-7"]),
allowedModelCatalog: [
...allowedModelCatalog,
{ provider: "opencode", id: "claude-opus-4-7", name: "Claude Opus 4.7" },
],
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
}),
);
});
it("shows success message when session state is available", async () => {
const directives = parseInlineDirectives("/model openai/gpt-4o");
const sessionEntry = createSessionEntry();
@@ -20,6 +20,28 @@ installReplyRuntimeMocks(agentMocks);
describe("getReplyFromConfig fast-path runtime", () => {
beforeAll(async () => {
({ getReplyFromConfig } = await loadGetReplyModuleForTest({ cacheKey: import.meta.url }));
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
resetReplyRuntimeMocks(agentMocks);
agentMocks.runEmbeddedAgent.mockResolvedValue(makeEmbeddedTextResult("warm runtime"));
await withTempHome(async (home) => {
await getReplyFromConfig(
{
Body: "warm runtime",
BodyForAgent: "warm runtime",
RawBody: "warm runtime",
CommandBody: "warm runtime",
From: "+1001",
To: "+2000",
SessionKey: "agent:main:whatsapp:+2000",
Provider: "whatsapp",
Surface: "whatsapp",
ChatType: "direct",
},
{},
makeReplyConfig(home) as OpenClawConfig,
);
});
vi.unstubAllEnvs();
});
beforeEach(async () => {
@@ -144,10 +144,18 @@ export function installPluginContractRegistryShard(params: ContractShardParams)
installEmptyShardSuite("plugin contract registry shard");
return;
}
const pluginCache = new Map<string, Awaited<ReturnType<typeof getBundledChannelPluginAsync>>>();
beforeAll(async () => {
await Promise.all(
entries.map(async (entry) => {
pluginCache.set(entry.id, await getBundledChannelPluginAsync(entry.id));
}),
);
});
for (const entry of entries) {
describe(`${entry.id} plugin contract`, () => {
it("satisfies the base channel plugin contract", async () => {
const plugin = await getBundledChannelPluginAsync(entry.id);
it("satisfies the base channel plugin contract", () => {
const plugin = pluginCache.get(entry.id);
if (!plugin) {
throw new Error(`Missing bundled channel plugin for ${entry.id}`);
}
+81 -76
View File
@@ -170,10 +170,90 @@ function mockNodeGatewayPlanFixture(
});
}
async function buildPluginConfigExecSecretRefPlan(home: string) {
mockNodeGatewayPlanFixture({ serviceEnvironment: { OPENCLAW_PORT: "3000" } });
const pluginRoot = path.join(home, "acme-secrets");
createSecurePluginRoot(pluginRoot);
writeSecurePluginEntrypoint(path.join(pluginRoot, "secret-ref-resolver.js"));
mocks.loadPluginManifestRegistry.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "acme-secrets",
origin: "global",
rootDir: pluginRoot,
secretProviderIntegrations: {
"secret-store": {
source: "exec",
command: "${node}",
args: ["./secret-ref-resolver.js"],
passEnv: ["ACME_SECRETS_TOKEN"],
},
},
},
],
});
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "acme-plugin",
origin: "global",
configContracts: {
secretInputs: {
paths: [{ path: "apiKey", expected: "string" }],
},
},
},
],
});
return await buildGatewayInstallPlan({
env: { HOME: home, ACME_SECRETS_TOKEN: "secret-token" },
port: 3000,
runtime: "node",
config: {
plugins: {
enabled: true,
entries: {
"acme-plugin": {
enabled: true,
config: {
apiKey: {
source: "exec",
provider: "team-secrets",
id: "providers/acme-plugin/apiKey",
},
},
},
},
},
secrets: {
providers: {
"team-secrets": {
source: "exec",
pluginIntegration: {
pluginId: "acme-secrets",
integrationId: "secret-store",
},
},
},
},
},
});
}
describe("buildGatewayInstallPlan", () => {
beforeAll(async () => {
const { resolveConfigSecretTargetByPath } = await import("../secrets/target-registry.js");
resolveConfigSecretTargetByPath(["channels", "discord", "token"]);
const warmHome = fs.mkdtempSync(path.join(os.tmpdir(), "oc-plan-plugin-warm-"));
try {
await buildPluginConfigExecSecretRefPlan(warmHome);
} finally {
fs.rmSync(warmHome, { recursive: true, force: true });
}
vi.clearAllMocks();
});
// Prevent tests from reading the developer's real ~/.openclaw/.env when
@@ -614,82 +694,7 @@ describe("buildGatewayInstallPlan", () => {
});
it("includes passEnv values for plugin config exec SecretRefs", async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
OPENCLAW_PORT: "3000",
},
});
const pluginRoot = path.join(isolatedHome, "acme-secrets");
createSecurePluginRoot(pluginRoot);
writeSecurePluginEntrypoint(path.join(pluginRoot, "secret-ref-resolver.js"));
mocks.loadPluginManifestRegistry.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "acme-secrets",
origin: "global",
rootDir: pluginRoot,
secretProviderIntegrations: {
"secret-store": {
source: "exec",
command: "${node}",
args: ["./secret-ref-resolver.js"],
passEnv: ["ACME_SECRETS_TOKEN"],
},
},
},
],
});
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "acme-plugin",
origin: "global",
configContracts: {
secretInputs: {
paths: [{ path: "apiKey", expected: "string" }],
},
},
},
],
});
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv({
ACME_SECRETS_TOKEN: "secret-token",
}),
port: 3000,
runtime: "node",
config: {
plugins: {
enabled: true,
entries: {
"acme-plugin": {
enabled: true,
config: {
apiKey: {
source: "exec",
provider: "team-secrets",
id: "providers/acme-plugin/apiKey",
},
},
},
},
},
secrets: {
providers: {
"team-secrets": {
source: "exec",
pluginIntegration: {
pluginId: "acme-secrets",
integrationId: "secret-store",
},
},
},
},
},
});
const plan = await buildPluginConfigExecSecretRefPlan(isolatedHome);
expect(plan.environment.ACME_SECRETS_TOKEN).toBe("secret-token");
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
+8
View File
@@ -1511,6 +1511,14 @@ describe("doctor config flow", () => {
},
},
});
await collectDoctorWarnings({
channels: {
googlechat: {
groupPolicy: "allowlist",
accounts: { work: { groupPolicy: "allowlist" } },
},
},
});
});
beforeEach(() => {
+6 -1
View File
@@ -1,7 +1,7 @@
// Doctor security tests cover security audit checks, config findings, and repair output.
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
@@ -29,6 +29,11 @@ describe("noteSecurityWarnings gateway exposure", () => {
let prevHome: string | undefined;
let prevServiceKind: string | undefined;
beforeAll(async () => {
listReadOnlyChannelPluginsForConfigMock.mockReturnValue([]);
await noteSecurityWarnings({ gateway: { bind: "loopback" } } as OpenClawConfig);
});
beforeEach(() => {
note.mockClear();
listReadOnlyChannelPluginsForConfigMock.mockReset();
@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { describe, expect, it, vi } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { loadSessionEntry, updateSessionEntry, upsertSessionEntry } from "./session-accessor.js";
vi.mock("../config.js", async () => ({
@@ -152,6 +152,24 @@ async function waitForChild(child: ReturnType<typeof spawn>): Promise<void> {
}
describe("reply session initialization concurrency", () => {
beforeAll(async () => {
const sessionAccessorUrl = pathToFileURL(
path.resolve("src/config/sessions/session-accessor.ts"),
).href;
const child = spawn(
process.execPath,
[
"--import",
"tsx",
"--input-type=module",
"--eval",
`await import(${JSON.stringify(sessionAccessorUrl)})`,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
await waitForChild(child);
});
it("commits after same-session activity from another process", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-reply-init-"));
const sessionAccessorUrl = pathToFileURL(
@@ -2,7 +2,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import "./isolated-agent.mocks.js";
import { beforeEach, describe, expect, it } from "vitest";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import type { CliDeps } from "../cli/deps.js";
import { resolveDefaultSessionStorePath } from "../config/sessions.js";
import { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js";
@@ -49,6 +49,22 @@ async function runAnnounceTurn(params: {
}
describe("runCronIsolatedAgentTurn cron delivery awareness", () => {
beforeAll(async () => {
setupIsolatedAgentTurnMocks();
resetCompletedDirectCronDeliveriesForTests();
resetSystemEventsForTest();
await withTempCronHome(async (home) => {
const storePath = await writeDefaultAgentSessionStoreEntries({});
mockAgentPayloads([{ text: "warm runtime" }]);
await runAnnounceTurn({
home,
storePath,
sessionKey: "cron:warm-runtime",
delivery: { mode: "announce", channel: "telegram", to: "123" },
});
});
});
beforeEach(() => {
setupIsolatedAgentTurnMocks();
resetCompletedDirectCronDeliveriesForTests();
@@ -1,6 +1,6 @@
// Direct delivery tests cover isolated agent delivery through core channel targets.
import "./isolated-agent.mocks.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
import type { ChannelOutboundAdapter, ChannelOutboundContext } from "../channels/plugins/types.js";
import type { CliDeps } from "../cli/deps.js";
@@ -251,54 +251,70 @@ async function expectTelegramAnnounceDelivery({
});
}
function setupCoreChannelMocks(): void {
setupIsolatedAgentTurnMocks({ fast: true });
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "slack",
plugin: createOutboundTestPlugin({
id: "slack",
outbound: createCliDelegatingOutbound({ channel: "slack" }),
}),
source: "test",
},
{
pluginId: "discord",
plugin: createOutboundTestPlugin({
id: "discord",
outbound: createCliDelegatingOutbound({
channel: "discord",
preferFinalAssistantVisibleText: true,
}),
}),
source: "test",
},
{
pluginId: "whatsapp",
plugin: createOutboundTestPlugin({
id: "whatsapp",
outbound: createCliDelegatingOutbound({
channel: "whatsapp",
deliveryMode: "gateway",
resolveTarget: identityResolveTarget,
}),
}),
source: "test",
},
{
pluginId: "imessage",
plugin: createOutboundTestPlugin({
id: "imessage",
outbound: createCliDelegatingOutbound({ channel: "imessage" }),
}),
source: "test",
},
]),
);
}
describe("runCronIsolatedAgentTurn core-channel direct delivery", () => {
beforeEach(() => {
setupIsolatedAgentTurnMocks({ fast: true });
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "slack",
plugin: createOutboundTestPlugin({
id: "slack",
outbound: createCliDelegatingOutbound({ channel: "slack" }),
}),
source: "test",
},
{
pluginId: "discord",
plugin: createOutboundTestPlugin({
id: "discord",
outbound: createCliDelegatingOutbound({
channel: "discord",
preferFinalAssistantVisibleText: true,
}),
}),
source: "test",
},
{
pluginId: "whatsapp",
plugin: createOutboundTestPlugin({
id: "whatsapp",
outbound: createCliDelegatingOutbound({
channel: "whatsapp",
deliveryMode: "gateway",
resolveTarget: identityResolveTarget,
}),
}),
source: "test",
},
{
pluginId: "imessage",
plugin: createOutboundTestPlugin({
id: "imessage",
outbound: createCliDelegatingOutbound({ channel: "imessage" }),
}),
source: "test",
},
]),
);
beforeAll(async () => {
setupCoreChannelMocks();
const slack = CASES[0];
if (!slack) {
throw new Error("expected Slack channel case");
}
await expectCoreChannelAnnounceDelivery({
testCase: slack,
payloads: [{ text: "warm runtime" }],
assertSend: () => {},
});
clearRuntimeConfigSnapshot();
});
beforeEach(setupCoreChannelMocks);
afterEach(() => {
clearRuntimeConfigSnapshot();
});
@@ -1,6 +1,6 @@
// Hook content wrapping tests cover isolated agent message wrapping for hooks.
import "./isolated-agent.mocks.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runEmbeddedAgent } from "../agents/embedded-agent.js";
import { loadModelCatalog } from "../agents/model-catalog.js";
import { makeCfg } from "./isolated-agent.test-harness.js";
@@ -24,6 +24,19 @@ function lastEmbeddedPrompt(): string {
}
describe("runCronIsolatedAgentTurn hook content wrapping", () => {
beforeAll(async () => {
process.env.OPENCLAW_TEST_FAST = "1";
vi.spyOn(isolatedAgentRunRuntime, "resolveThinkingDefault").mockReturnValue("off");
vi.mocked(loadModelCatalog).mockResolvedValue([]);
await withTempHome(async (home) => {
await runCronTurn(home, {
jobPayload: { kind: "agentTurn", message: "warm runtime" },
message: "warm runtime",
sessionKey: "hook:gmail:warm-runtime",
});
});
});
beforeEach(() => {
process.env.OPENCLAW_TEST_FAST = "1";
vi.spyOn(isolatedAgentRunRuntime, "resolveThinkingDefault").mockReturnValue("off");
@@ -2,7 +2,7 @@
import "./isolated-agent.mocks.js";
import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as modelThinkingDefault from "../agents/model-thinking-default.js";
import type { SessionEntry } from "../config/sessions.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
@@ -24,7 +24,10 @@ import {
import { setupRunCronIsolatedAgentTurnSuite } from "./isolated-agent/run.suite-helpers.js";
import {
dispatchCronDeliveryMock,
makeCronSession,
mockRunCronFallbackPassthrough,
resetRunCronIsolatedAgentTurnHarness,
resolveCronSessionMock,
runEmbeddedAgentMock,
updateSessionStoreMock,
} from "./isolated-agent/run.test-harness.js";
@@ -61,6 +64,16 @@ function lastEmbeddedAgentCall(): {
}
describe("runCronIsolatedAgentTurn session identity", () => {
beforeAll(async () => {
resetRunCronIsolatedAgentTurnHarness();
resolveCronSessionMock.mockReturnValue(makeCronSession());
vi.spyOn(modelThinkingDefault, "resolveThinkingDefault").mockReturnValue("off");
mockRunCronFallbackPassthrough();
await withTempHome(async (home) => {
await runCronTurn(home, { jobPayload: DEFAULT_AGENT_TURN_PAYLOAD });
});
});
beforeEach(() => {
vi.spyOn(modelThinkingDefault, "resolveThinkingDefault").mockReturnValue("off");
runEmbeddedAgentMock.mockClear();
@@ -1,7 +1,7 @@
// Chat transcript parent-id tests protect gateway-injected assistant appends so
// compaction history remains connected and transcript listeners receive updates.
import fs from "node:fs";
import { describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { appendInjectedAssistantMessageToTranscript } from "./chat-transcript-inject.js";
import { createTranscriptFixtureSync } from "./chat.test-helpers.js";
@@ -37,9 +37,54 @@ function readLastTranscriptRecord(transcriptPath: string): Record<string, unknow
return JSON.parse(lines.at(-1) as string) as Record<string, unknown>;
}
function readLastTranscriptRecordFromTail(transcriptPath: string): Record<string, unknown> {
const size = fs.statSync(transcriptPath).size;
const length = Math.min(size, 64 * 1024);
const buffer = Buffer.allocUnsafe(length);
const fd = fs.openSync(transcriptPath, "r");
let bytesRead = 0;
try {
bytesRead = fs.readSync(fd, buffer, 0, length, size - length);
} finally {
fs.closeSync(fd);
}
const lines = buffer.subarray(0, bytesRead).toString("utf8").trimEnd().split(/\r?\n/);
return JSON.parse(lines.at(-1) as string) as Record<string, unknown>;
}
// Guardrail: Gateway-injected assistant transcript messages must attach to the
// current leaf with a `parentId` and must not sever compaction history.
describe("gateway chat.inject transcript writes", () => {
let oversizedDir = "";
let oversizedTranscriptPath = "";
beforeAll(() => {
const fixture = createTranscriptFixtureSync({
prefix: "openclaw-chat-inject-large-",
sessionId: "sess-1",
});
oversizedDir = fixture.dir;
oversizedTranscriptPath = fixture.transcriptPath;
fs.appendFileSync(
oversizedTranscriptPath,
`${JSON.stringify({
type: "message",
id: "legacy-large-message",
message: {
role: "assistant",
content: [{ type: "text", text: "x".repeat(9 * 1024 * 1024) }],
},
})}\n`,
"utf-8",
);
});
afterAll(() => {
if (oversizedDir) {
fs.rmSync(oversizedDir, { recursive: true, force: true });
}
});
it("appends a agent session entry that includes parentId", async () => {
const { dir, transcriptPath } = createTranscriptFixtureSync({
prefix: "openclaw-chat-inject-",
@@ -61,35 +106,15 @@ describe("gateway chat.inject transcript writes", () => {
});
it("uses raw append for oversized append-only transcripts", async () => {
const { dir, transcriptPath } = createTranscriptFixtureSync({
prefix: "openclaw-chat-inject-large-",
sessionId: "sess-1",
});
const sizeBefore = fs.statSync(oversizedTranscriptPath).size;
const messageId = await appendHelloAndRequireId(oversizedTranscriptPath);
const last = readLastTranscriptRecordFromTail(oversizedTranscriptPath);
try {
fs.appendFileSync(
transcriptPath,
`${JSON.stringify({
type: "message",
id: "legacy-large-message",
message: {
role: "assistant",
content: [{ type: "text", text: "x".repeat(9 * 1024 * 1024) }],
},
})}\n`,
"utf-8",
);
const messageId = await appendHelloAndRequireId(transcriptPath);
const last = readLastTranscriptRecord(transcriptPath);
expect(last.type).toBe("message");
expect(last).toHaveProperty("id", messageId);
expect(last).toHaveProperty("message");
expect(Object.hasOwn(last, "parentId")).toBe(false);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
expect(fs.statSync(oversizedTranscriptPath).size).toBeGreaterThan(sizeBefore);
expect(last.type).toBe("message");
expect(last).toHaveProperty("id", messageId);
expect(last).toHaveProperty("message");
expect(Object.hasOwn(last, "parentId")).toBe(false);
});
it("emits and returns the redacted injected assistant message", async () => {
+18 -3
View File
@@ -1,20 +1,34 @@
/**
* Tests gateway plugin lifecycle loading, startup, and shutdown behavior.
*/
import { afterEach, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { clearFallbackGatewayContext, createGatewaySubagentRuntime } from "./server-plugins.js";
import { installGatewayTestHooks, startServer } from "./test-helpers.server.js";
installGatewayTestHooks();
installGatewayTestHooks({ scope: "suite" });
afterEach(() => {
clearFallbackGatewayContext();
});
describe("gateway plugin fallback context lifecycle", () => {
let started: Awaited<ReturnType<typeof startServer>> | undefined;
beforeAll(async () => {
const warm = await startServer();
await warm.server.close({ reason: "warm fallback context lifecycle" });
started = await startServer();
});
afterAll(async () => {
await started?.server.close({ reason: "fallback context lifecycle cleanup" });
});
it("clears the fallback gateway context after server close", async () => {
const runtime = createGatewaySubagentRuntime();
const started = await startServer();
if (!started) {
throw new Error("expected gateway server to start");
}
try {
await expect(
@@ -22,6 +36,7 @@ describe("gateway plugin fallback context lifecycle", () => {
).resolves.toEqual({ messages: [] });
} finally {
await started.server.close({ reason: "fallback context lifecycle test done" });
started = undefined;
}
await expect(
@@ -245,6 +245,8 @@ vi.mock("./server-tailscale.js", () => ({
const { startGatewayPostAttachRuntime, startGatewaySidecars, testing } =
await import("./server-startup-post-attach.js");
const { STARTUP_UNAVAILABLE_GATEWAY_METHODS } = await import("./methods/core-descriptors.js");
const { createGatewayCloseHandler } = await import("./server-close.js");
const { createChatRunState } = await import("./server-chat-state.js");
type PostAttachParams = Parameters<typeof startGatewayPostAttachRuntime>[0];
type PostAttachRuntimeDeps = NonNullable<Parameters<typeof startGatewayPostAttachRuntime>[1]>;
@@ -1894,9 +1896,6 @@ describe("startGatewayPostAttachRuntime", () => {
const postReadySidecars = [{ stop: vi.fn() }];
const stopChannel = vi.fn(async () => {});
const pluginServices = { stop: vi.fn(async () => {}) };
const { createGatewayCloseHandler } = await import("./server-close.js");
const { createChatRunState } = await import("./server-chat-state.js");
const close = createGatewayCloseHandler({
bonjourStop: null,
tailscaleCleanup: null,
@@ -2,7 +2,7 @@
* Gateway startup web fetch bind tests.
*/
import http from "node:http";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { getFreePort, installGatewayTestHooks, startGatewayServer } from "./test-helpers.js";
import { readClientResponseBody } from "./test-http-response.js";
@@ -40,7 +40,7 @@ vi.mock("../secrets/runtime-web-tools-public-artifacts.runtime.js", async () =>
};
});
installGatewayTestHooks();
installGatewayTestHooks({ scope: "suite" });
afterEach(() => {
webFetchProviderDiscovery.resolveBundledWebFetchProvidersFromPublicArtifactsMock.mockClear();
@@ -71,53 +71,58 @@ async function writeConfig(config: OpenClawConfig): Promise<void> {
}
describe("gateway startup web fetch config", () => {
it("binds HTTP with credential-free tools.web.fetch config without fetch provider discovery", async () => {
const previousMinimal = process.env.OPENCLAW_TEST_MINIMAL_GATEWAY;
let port: number;
let previousMinimal: string | undefined;
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
beforeAll(async () => {
previousMinimal = process.env.OPENCLAW_TEST_MINIMAL_GATEWAY;
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "0";
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
try {
await writeConfig({
gateway: {
mode: "local",
bind: "loopback",
auth: { mode: "none" },
},
plugins: {
enabled: true,
allow: [],
entries: {},
},
tools: {
web: {
fetch: {
enabled: true,
maxChars: 200_000,
maxCharsCap: 2_000_000,
},
await writeConfig({
gateway: {
mode: "local",
bind: "loopback",
auth: { mode: "none" },
},
plugins: {
enabled: true,
allow: [],
entries: {},
},
tools: {
web: {
fetch: {
enabled: true,
maxChars: 200_000,
maxCharsCap: 2_000_000,
},
},
} as OpenClawConfig);
},
} as OpenClawConfig);
const port = await getFreePort();
server = await startGatewayServer(port, {
auth: { mode: "none" },
});
port = await getFreePort();
server = await startGatewayServer(port, {
auth: { mode: "none" },
});
});
const response = await requestHealthz(port);
expect(response.status).toBe(200);
expect(
webFetchProviderDiscovery.resolveBundledWebFetchProvidersFromPublicArtifactsMock,
).not.toHaveBeenCalled();
expect(webFetchProviderDiscovery.resolvePluginWebFetchProvidersMock).not.toHaveBeenCalled();
} finally {
if (server) {
await server.close();
}
if (previousMinimal === undefined) {
delete process.env.OPENCLAW_TEST_MINIMAL_GATEWAY;
} else {
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = previousMinimal;
}
afterAll(async () => {
if (server) {
await server.close();
}
if (previousMinimal === undefined) {
delete process.env.OPENCLAW_TEST_MINIMAL_GATEWAY;
} else {
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = previousMinimal;
}
});
it("binds HTTP with credential-free tools.web.fetch config without fetch provider discovery", async () => {
const response = await requestHealthz(port);
expect(response.status).toBe(200);
expect(
webFetchProviderDiscovery.resolveBundledWebFetchProvidersFromPublicArtifactsMock,
).not.toHaveBeenCalled();
expect(webFetchProviderDiscovery.resolvePluginWebFetchProvidersMock).not.toHaveBeenCalled();
});
});
+36 -11
View File
@@ -11,7 +11,11 @@ import {
import type { OpenClawConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js";
import { registerAgentRunContext, resetAgentRunContextForTest } from "../infra/agent-events.js";
import { buildGatewaySessionInfo, listSessionsFromStore } from "./session-utils.js";
import {
buildGatewaySessionInfo,
filterAndSortSessionEntries,
listSessionsFromStore,
} from "./session-utils.js";
const MAIN_SESSION_KEY = "agent:main:main";
const MAIN_SESSION_ID = "sess-main";
@@ -81,6 +85,17 @@ function createLegacyRuntimeStore(model: string): Record<string, SessionEntry> {
};
}
function buildLegacyRuntimeRow(cfg: OpenClawConfig, model: string) {
const store = createLegacyRuntimeStore(model);
return buildGatewaySessionInfo({
cfg,
storePath: "/tmp/sessions.json",
store,
key: MAIN_SESSION_KEY,
entry: store[MAIN_SESSION_KEY],
});
}
function createOpenAiPricingConfig(params: {
id: string;
label: string;
@@ -325,6 +340,20 @@ function childTranscriptEntry(sessionId: string, now: number): SessionEntry {
}
describe("listSessionsFromStore search", () => {
beforeAll(() => {
listSessionsFromStore({
cfg: createModelDefaultsConfig({ primary: "anthropic/claude-sonnet-4-6" }),
store: {
"agent:main:warm-runtime": {
sessionId: "sess-warm-runtime",
updatedAt: Date.now(),
} as SessionEntry,
},
storePath: "/tmp/openclaw-session-search-warm.json",
opts: { search: "anthropic" },
});
});
beforeAll(() => {
listSessionsFromStore({
cfg: createModelDefaultsConfig({ primary: "openai/gpt-5.4" }),
@@ -460,14 +489,14 @@ describe("listSessionsFromStore search", () => {
] as const;
for (const testCase of cases) {
const result = listSearchSessions({
const entries = filterAndSortSessionEntries({
cfg,
store,
opts: { search: testCase.search },
now,
});
expect(result.sessions.map((session) => session.key)).toEqual([testCase.expectedKey]);
expect(result.totalCount).toBe(1);
expect(entries.map(([key]) => key)).toEqual([testCase.expectedKey]);
}
});
@@ -539,14 +568,10 @@ describe("listSessionsFromStore search", () => {
expectedProvider: "vercel-ai-gateway",
},
])("$name", ({ cfg, runtimeModel, expectedProvider }) => {
const result = listSearchSessions({
cfg,
store: createLegacyRuntimeStore(runtimeModel),
opts: {},
});
const row = buildLegacyRuntimeRow(cfg, runtimeModel);
expect(result.sessions[0]?.modelProvider).toBe(expectedProvider);
expect(result.sessions[0]?.model).toBe(runtimeModel);
expect(row.modelProvider).toBe(expectedProvider);
expect(row.model).toBe(runtimeModel);
});
test("exposes unknown totals when freshness is stale or missing", () => {
@@ -240,10 +240,10 @@ describe("OpenAI-compatible image provider helper", () => {
});
it("accepts valid multi-image JSON above the generic provider JSON cap", async () => {
const imageBytes = Buffer.alloc(6 * 1024 * 1024, 1);
const imageBytes = Buffer.alloc(3 * 1024 * 1024 + 64 * 1024, 1);
postJsonRequestMock.mockResolvedValue({
response: jsonResponse({
data: Array.from({ length: 3 }, () => ({
data: Array.from({ length: 4 }, () => ({
b64_json: imageBytes.toString("base64"),
})),
}),
@@ -255,15 +255,16 @@ describe("OpenAI-compatible image provider helper", () => {
provider: "sample",
model: "sample-image",
prompt: "large",
count: 3,
count: 4,
cfg: {} as never,
});
expect(result.images).toHaveLength(3);
expect(result.images).toHaveLength(4);
expect(result.images.map((image) => image.buffer.byteLength)).toEqual([
imageBytes.byteLength,
imageBytes.byteLength,
imageBytes.byteLength,
imageBytes.byteLength,
]);
});
+5
View File
@@ -1,5 +1,6 @@
// Image runtime tests cover model-backed image routing, auth/profile handling,
// provider payload transforms, and MiniMax/Copilot special paths.
import path from "node:path";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -138,11 +139,15 @@ const { describeImageWithModel } = await import("./image.js");
describe("describeImageWithModel", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
beforeEach(() => {
// Provider endpoint policy comes from manifests. Pin source manifests so a
// prior local build cannot make this source-checkout test read partial dist output.
vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(process.cwd(), "extensions"));
vi.stubGlobal("fetch", fetchMock);
vi.clearAllMocks();
fetchMock.mockResolvedValue({
+12 -1
View File
@@ -2,7 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import {
resolveAuthProfileDatabasePath,
writePersistedAuthProfileStoreRaw,
@@ -222,6 +222,17 @@ async function seedAuditFixture(fixture: AuditFixture): Promise<void> {
describe("secrets audit", () => {
let fixture: AuditFixture;
beforeAll(async () => {
const warmFixture = await createAuditFixture();
try {
await seedAuditFixture(warmFixture);
await runSecretsAudit({ env: warmFixture.env });
} finally {
closeOpenClawAgentDatabasesForTest();
await fs.rm(warmFixture.rootDir, { recursive: true, force: true });
}
});
async function writeModelsProvider(
overrides: Partial<{
apiKey: unknown;
+1
View File
@@ -17,6 +17,7 @@ import { resolveConfigSecretTargetByPath } from "./target-registry.js";
describe("secrets configure plan helpers", () => {
beforeAll(() => {
resolveConfigSecretTargetByPath(["channels", "telegram", "botToken"]);
buildConfigureCandidates({} as OpenClawConfig);
});
it("builds configure candidates from supported configure targets", () => {
+24 -11
View File
@@ -267,22 +267,35 @@ describe("secret ref resolver", () => {
const scriptPath = await writeForkingNoOutputScript(root);
const pidPath = path.join(root, "forked.pid");
let childPid: number | undefined;
const nativeSetTimeout = globalThis.setTimeout;
const noOutputTimeouts: Array<() => void> = [];
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, delay, ...args) => {
if (delay === 1_000) {
noOutputTimeouts.push(() => callback(...args));
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, delay, ...args);
});
try {
await expect(
resolveExecSecret(scriptPath, {
env: { NODE_BINARY: process.execPath, PID_FILE: pidPath },
// The first no-output window must absorb shell spawn latency under
// parallel-suite load; the script's readiness byte then pins the
// killing silence window after the pid write.
noOutputTimeoutMs: 1000,
timeoutMs: 10_000,
}),
).rejects.toThrow('Exec provider "execmain" produced no output');
const resultPromise = resolveExecSecret(scriptPath, {
env: { NODE_BINARY: process.execPath, PID_FILE: pidPath },
// Preserve production-like startup headroom; the test fires the
// re-armed timer only after the readiness byte arrives.
noOutputTimeoutMs: 1_000,
timeoutMs: 10_000,
});
await vi.waitFor(() => {
expect(noOutputTimeouts.length).toBeGreaterThanOrEqual(2);
});
childPid = await readPidFile(pidPath);
noOutputTimeouts.at(-1)?.();
await expect(resultPromise).rejects.toThrow('Exec provider "execmain" produced no output');
expect(await waitForPidToExit(childPid, 5_000)).toBe(true);
} finally {
setTimeoutSpy.mockRestore();
killPidIfAlive(childPid);
}
});
+3
View File
@@ -633,6 +633,9 @@ function applyConfigForOpenClawTarget(
if (entry.id === "plugins.entries.minimax.config.webSearch.apiKey") {
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "minimax");
}
if (entry.id === "plugins.entries.parallel.config.webSearch.apiKey") {
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "parallel");
}
if (entry.id === "plugins.entries.tavily.config.webSearch.apiKey") {
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "tavily");
}
+34 -11
View File
@@ -30,7 +30,12 @@ let compiledCoreOpenClawTargetState: {
knownTargetIds: Set<string>;
openClawCompiledSecretTargets: CompiledTargetRegistryEntry[];
openClawTargetsById: Map<string, CompiledTargetRegistryEntry[]>;
targetsByType: Map<string, CompiledTargetRegistryEntry[]>;
planTargetsByType: Map<string, CompiledTargetRegistryEntry[]>;
} | null = null;
let compiledCoreAuthProfileTargetState: {
entries: CompiledTargetRegistryEntry[];
entriesById: Map<string, CompiledTargetRegistryEntry[]>;
} | null = null;
// Channel contract entries are process-stable; plugin install/reload is the owner of freshness.
@@ -99,18 +104,33 @@ function getCompiledCoreOpenClawTargetState() {
if (compiledCoreOpenClawTargetState) {
return compiledCoreOpenClawTargetState;
}
const openClawCompiledSecretTargets = getCoreSecretTargetRegistry()
.filter((entry) => entry.configFile === "openclaw.json")
.map(compileTargetRegistryEntry);
const compiledCoreSecretTargets = getCoreSecretTargetRegistry().map(compileTargetRegistryEntry);
const openClawCompiledSecretTargets = compiledCoreSecretTargets.filter(
(entry) => entry.configFile === "openclaw.json",
);
compiledCoreOpenClawTargetState = {
knownTargetIds: new Set(openClawCompiledSecretTargets.map((entry) => entry.id)),
knownTargetIds: new Set(compiledCoreSecretTargets.map((entry) => entry.id)),
openClawCompiledSecretTargets,
openClawTargetsById: buildConfigTargetIdIndex(openClawCompiledSecretTargets),
targetsByType: buildTargetTypeIndex(openClawCompiledSecretTargets),
planTargetsByType: buildTargetTypeIndex(compiledCoreSecretTargets),
};
return compiledCoreOpenClawTargetState;
}
function getCompiledCoreAuthProfileTargetState() {
if (compiledCoreAuthProfileTargetState) {
return compiledCoreAuthProfileTargetState;
}
const entries = getCoreSecretTargetRegistry()
.filter((entry) => entry.configFile === "auth-profiles.json")
.map(compileTargetRegistryEntry);
compiledCoreAuthProfileTargetState = {
entries,
entriesById: buildConfigTargetIdIndex(entries),
};
return compiledCoreAuthProfileTargetState;
}
function getCompiledChannelOpenClawTargets(
channelId: string,
): CompiledTargetRegistryEntry[] | null {
@@ -300,13 +320,16 @@ export function resolvePlanTargetAgainstRegistry(candidate: {
providerId?: string;
accountId?: string;
}): ResolvedPlanTarget | null {
const coreEntries = getCompiledCoreOpenClawTargetState().targetsByType.get(candidate.type);
const coreEntries = getCompiledCoreOpenClawTargetState().planTargetsByType.get(candidate.type);
if (coreEntries) {
return resolvePlanTargetAgainstEntries(candidate, coreEntries);
}
const explicitChannelId =
candidate.pathSegments[0] === "channels" ? (candidate.pathSegments[1]?.trim() ?? "") : "";
if (explicitChannelId) {
if (/[\\/:]/.test(explicitChannelId)) {
return null;
}
const channelEntries = getCompiledChannelOpenClawTargets(explicitChannelId) ?? [];
const channelTypeEntries = buildTargetTypeIndex(channelEntries).get(candidate.type);
if (channelTypeEntries) {
@@ -463,11 +486,11 @@ export function discoverAuthProfileSecretTargets(
targetIds?: Iterable<string>,
): DiscoveredConfigSecretTarget[] {
const allowedTargetIds = normalizeAllowedTargetIds(targetIds);
const registryState = getCompiledSecretTargetRegistryState();
const registryState = getCompiledCoreAuthProfileTargetState();
const discoveryEntries = resolveDiscoveryEntries({
allowedTargetIds,
defaultEntries: registryState.authProfilesCompiledSecretTargets,
entriesById: registryState.authProfilesTargetsById,
defaultEntries: registryState.entries,
entriesById: registryState.entriesById,
});
return discoverSecretTargetsFromEntries(store, discoveryEntries);
}
@@ -476,7 +499,7 @@ export function discoverAuthProfileSecretTargets(
* Lists auth-profile target entries that participate in plaintext/unresolved-ref audit.
*/
export function listAuthProfileSecretTargetEntries(): SecretTargetRegistryEntry[] {
return getCompiledSecretTargetRegistryState().compiledSecretTargetRegistry.filter(
return getCoreSecretTargetRegistry().filter(
(entry) => entry.configFile === "auth-profiles.json" && entry.includeInAudit,
);
}
@@ -121,4 +121,14 @@ describe("secret target registry fast path", () => {
expect(target?.entry.id).toBe("channels.telegram.botToken");
expect(loadPluginManifestRegistryMock).not.toHaveBeenCalled();
});
it("resolves auth-profile plan targets without loading plugin metadata", () => {
const target = resolvePlanTargetAgainstRegistry({
type: "auth-profiles.api_key.key",
pathSegments: ["profiles", "openai:default", "key"],
});
expect(target?.entry.id).toBe("auth-profiles.api_key.key");
expect(loadPluginManifestRegistryMock).not.toHaveBeenCalled();
});
});
+23 -7
View File
@@ -2,7 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
killPidIfAlive,
@@ -223,9 +223,20 @@ describe("runInstallPolicy", () => {
const forkScriptPath = await writeForkingNoOutputScript(sourceDir);
const pidPath = path.join(sourceDir, "forked.pid");
let childPid: number | undefined;
const nativeSetTimeout = globalThis.setTimeout;
const noOutputTimeouts: Array<() => void> = [];
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, delay, ...args) => {
if (delay === 1_000) {
noOutputTimeouts.push(() => callback(...args));
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, delay, ...args);
});
try {
const result = await runInstallPolicy({
const resultPromise = runInstallPolicy({
config: {
security: {
installPolicy: {
@@ -235,10 +246,9 @@ describe("runInstallPolicy", () => {
command: forkScriptPath,
env: { NODE_BINARY: process.execPath, PID_FILE: pidPath },
allowInsecurePath: true,
// The first no-output window must absorb shell spawn latency
// under parallel-suite load; the script's readiness byte then
// pins the killing silence window after the pid write.
noOutputTimeoutMs: 1000,
// Preserve production-like startup headroom; the test fires
// the re-armed timer only after the readiness byte arrives.
noOutputTimeoutMs: 1_000,
timeoutMs: 10_000,
},
},
@@ -246,11 +256,17 @@ describe("runInstallPolicy", () => {
},
request: baseRequest(sourceDir),
});
await vi.waitFor(() => {
expect(noOutputTimeouts.length).toBeGreaterThanOrEqual(2);
});
childPid = await readPidFile(pidPath);
noOutputTimeouts.at(-1)?.();
const result = await resultPromise;
expect(result?.blocked?.reason).toContain("policy command produced no output");
childPid = await readPidFile(pidPath);
expect(await waitForPidToExit(childPid, 5_000)).toBe(true);
} finally {
setTimeoutSpy.mockRestore();
killPidIfAlive(childPid);
}
},
+42 -42
View File
@@ -34,6 +34,8 @@ function readRestartIntentRow(env: NodeJS.ProcessEnv) {
describe("gateway restart benchmark script", () => {
let helpResult: ReturnType<typeof spawnSync>;
let unknownArgsResult: ReturnType<typeof spawnSync>;
let duplicateCaseResult: ReturnType<typeof spawnSync>;
beforeAll(() => {
helpResult = spawnSync(
@@ -48,6 +50,38 @@ describe("gateway restart benchmark script", () => {
},
},
);
unknownArgsResult = spawnSync(
process.execPath,
["--import", "tsx", "scripts/bench-gateway-restart.ts", "--wat"],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
NODE_NO_WARNINGS: "1",
},
},
);
duplicateCaseResult = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/bench-gateway-restart.ts",
"--case",
"skipChannels",
"--case",
"skipChannels",
],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
NODE_NO_WARNINGS: "1",
},
},
);
});
it("prints help without running benchmark cases", () => {
@@ -104,51 +138,17 @@ describe("gateway restart benchmark script", () => {
});
it("rejects unknown benchmark CLI args before checking platform or running cases", () => {
const result = spawnSync(
process.execPath,
["--import", "tsx", "scripts/bench-gateway-restart.ts", "--wat"],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
NODE_NO_WARNINGS: "1",
},
},
);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("Unknown argument: --wat");
expect(result.stderr).not.toContain("\n at ");
expect(unknownArgsResult.status).toBe(1);
expect(unknownArgsResult.stdout).toBe("");
expect(unknownArgsResult.stderr.trim()).toBe("Unknown argument: --wat");
expect(unknownArgsResult.stderr).not.toContain("\n at ");
});
it("reports duplicate benchmark cases without a stack trace", () => {
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/bench-gateway-restart.ts",
"--case",
"skipChannels",
"--case",
"skipChannels",
],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
NODE_NO_WARNINGS: "1",
},
},
);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe('Duplicate --case "skipChannels"');
expect(result.stderr).not.toContain("\n at ");
expect(duplicateCaseResult.status).toBe(1);
expect(duplicateCaseResult.stdout).toBe("");
expect(duplicateCaseResult.stderr.trim()).toBe('Duplicate --case "skipChannels"');
expect(duplicateCaseResult.stderr).not.toContain("\n at ");
});
it("guards the SIGUSR1 restart benchmark on Windows", () => {
@@ -1,5 +1,4 @@
// Issue 78851 profiler CLI tests cover argument handling before work starts.
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import {
issue78851ModelResolutionHelpRequested,
@@ -37,20 +36,9 @@ describe("issue 78851 model resolution profiler CLI", () => {
});
it("rejects invalid arguments even when help is also requested", () => {
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/perf/issue-78851-model-resolution.ts",
"--wat",
"--help",
],
{ encoding: "utf8" },
expect(() => parseIssue78851ModelResolutionOptions(["--wat", "--help"])).toThrow(
"Unknown argument: --wat",
);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Unknown argument: --wat");
});
it("rejects duplicate value flags before starting the profiler", () => {
@@ -359,6 +359,17 @@ describe("prepare-extension-package-boundary-artifacts", () => {
tempRoots.add(rootDir);
const descendantPidPath = path.join(rootDir, "descendant.pid");
let descendantPid = 0;
const nativeSetTimeout = globalThis.setTimeout;
let triggerStepTimeout: (() => void) | undefined;
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, timeout, ...args) => {
if (timeout === 2_000 && !triggerStepTimeout) {
triggerStepTimeout = () => callback(...args);
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, timeout, ...args);
});
const descendantScript = [
"process.on('SIGTERM', () => {});",
"setInterval(() => {}, 1000);",
@@ -372,17 +383,20 @@ describe("prepare-extension-package-boundary-artifacts", () => {
].join("\n");
try {
// The parent records the descendant pid immediately after spawn, so this
// needs headroom for only one Node boot under parallel-suite load.
// The parent records the descendant pid at spawn time, before it
// boots; fire the captured production timeout after that readiness proof.
const command = runNodeStep("hung-group-prep", ["--eval", parentScript], 2_000);
const expectedFailure = expect(command).rejects.toThrow(
"hung-group-prep timed out after 2000ms",
);
descendantPid = Number.parseInt(await waitForFile(descendantPidPath, 4_000), 10);
expect(triggerStepTimeout).toBeDefined();
triggerStepTimeout?.();
await expectedFailure;
await waitForDead(descendantPid, 2_000);
} finally {
setTimeoutSpy.mockRestore();
if (descendantPid && isProcessAlive(descendantPid)) {
process.kill(descendantPid, "SIGKILL");
}
+91 -30
View File
@@ -4,7 +4,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it, vi } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import {
buildGroupedTestComparison,
buildGroupedTestReport,
@@ -343,6 +343,62 @@ describe("scripts/test-group-report aggregation", () => {
}
});
it("continues allow-failures profiling after a config writes an empty JSON report", async () => {
const tempDir = makeTempDir();
try {
const result = await runReportPlans({
args: parseTestGroupReportArgs([
"--config",
"failed.config.ts",
"--config",
"passed.config.ts",
"--allow-failures",
"--no-rss",
]),
logDir: path.join(tempDir, "logs"),
reportDir: path.join(tempDir, "reports"),
runPlans: [
{ config: "failed.config.ts", forwardedArgs: [], label: "failed" },
{ config: "passed.config.ts", forwardedArgs: [], label: "passed" },
],
runVitestJsonReport: async (params: {
config: string;
label: string;
logPath: string;
reportPath: string;
}) => {
fs.mkdirSync(path.dirname(params.reportPath), { recursive: true });
fs.writeFileSync(
params.reportPath,
`${JSON.stringify({
testResults: params.label === "failed" ? [] : [{ name: "passed.test.ts" }],
})}\n`,
"utf8",
);
return {
config: params.config,
elapsedMs: 10,
label: params.label,
logPath: params.logPath,
maxRssBytes: null,
reportPath: params.reportPath,
status: params.label === "failed" ? 1 : 0,
};
},
});
expect(result.failed).toBe(true);
expect(result.exitCode).toBe(0);
expect(result.runs.map((run) => [run.label, run.status])).toStrictEqual([
["failed", 1],
["passed", 0],
]);
expect(result.runEntries.map((entry) => entry.config)).toStrictEqual(["passed"]);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("prints slow tests as soon as each config report completes", async () => {
const tempDir = makeTempDir();
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -1179,6 +1235,24 @@ describe("scripts/test-group-report child process guard", () => {
});
describe("scripts/test-group-report run plans", () => {
let serialFullSuitePlans: ReturnType<typeof resolveRunPlans> = [];
let parallelFullSuitePlans: ReturnType<typeof resolveRunPlans> = [];
beforeAll(() => {
withEnv(
{
OPENCLAW_TEST_PROJECTS_PARALLEL: undefined,
OPENCLAW_TEST_PROJECTS_LEAF_SHARDS: undefined,
},
() => {
serialFullSuitePlans = resolveRunPlans(parseTestGroupReportArgs(["--full-suite"]));
},
);
withEnv({ OPENCLAW_TEST_PROJECTS_PARALLEL: "6" }, () => {
parallelFullSuitePlans = resolveRunPlans(parseTestGroupReportArgs(["--full-suite"]));
});
});
it("isolates full-suite duration reports by default", () => {
expect(resolveReportVitestArgs(parseTestGroupReportArgs(["--full-suite"]))).toEqual([
"--isolate=true",
@@ -1258,40 +1332,27 @@ describe("scripts/test-group-report run plans", () => {
});
it("uses leaf configs for full-suite profiling without requiring parallel env", () => {
withEnv(
{
OPENCLAW_TEST_PROJECTS_PARALLEL: undefined,
OPENCLAW_TEST_PROJECTS_LEAF_SHARDS: undefined,
},
() => {
const plans = resolveRunPlans(parseTestGroupReportArgs(["--full-suite"]));
expect(plans.map((plan) => plan.config)).not.toContain(
"test/vitest/vitest.full-agentic.config.ts",
);
expect(plans.map((plan) => plan.config)).toContain(
"test/vitest/vitest.agents-tools.config.ts",
);
},
expect(serialFullSuitePlans.map((plan) => plan.config)).not.toContain(
"test/vitest/vitest.full-agentic.config.ts",
);
expect(serialFullSuitePlans.map((plan) => plan.config)).toContain(
"test/vitest/vitest.agents-tools.config.ts",
);
});
it("preserves full-suite shard file args and unique report labels", () => {
withEnv({ OPENCLAW_TEST_PROJECTS_PARALLEL: "6" }, () => {
const plans = resolveRunPlans(parseTestGroupReportArgs(["--full-suite"]));
const gatewayServerPlans = plans.filter(
(plan) => plan.config === "test/vitest/vitest.gateway-server.config.ts",
);
const gatewayServerPlans = parallelFullSuitePlans.filter(
(plan) => plan.config === "test/vitest/vitest.gateway-server.config.ts",
);
expect(gatewayServerPlans.length).toBeGreaterThan(1);
expect(new Set(gatewayServerPlans.map((plan) => plan.label)).size).toBe(
gatewayServerPlans.length,
);
expect(gatewayServerPlans.every((plan) => plan.forwardedArgs.length > 0)).toBe(true);
expect(gatewayServerPlans.flatMap((plan) => plan.forwardedArgs)).toContain(
"src/gateway/server.node-pairing-authz.test.ts",
);
});
expect(gatewayServerPlans.length).toBeGreaterThan(1);
expect(new Set(gatewayServerPlans.map((plan) => plan.label)).size).toBe(
gatewayServerPlans.length,
);
expect(gatewayServerPlans.every((plan) => plan.forwardedArgs.length > 0)).toBe(true);
expect(gatewayServerPlans.flatMap((plan) => plan.forwardedArgs)).toContain(
"src/gateway/server.node-pairing-authz.test.ts",
);
});
});
+35 -4
View File
@@ -4130,19 +4130,23 @@ describe("scripts/test-projects full-suite sharding", () => {
it("can expand full-suite shards to project configs for perf experiments", () => {
const gatewayServerConfig = "test/vitest/vitest.gateway-server.config.ts";
const toolingConfig = "test/vitest/vitest.tooling.config.ts";
const unitFastConfig = "test/vitest/vitest.unit-fast.config.ts";
const plans = leafShardPlans;
const toolingPlans = plans.filter((plan) => plan.config === toolingConfig);
const unitFastPlans = plans.filter((plan) => plan.config === unitFastConfig);
if (leafShardHasGitGatewayListing) {
expect(leafShardGatewayTreeReads).toEqual([]);
}
expect(leafShardPlans.map((plan) => plan.config)).toEqual([
"test/vitest/vitest.unit-fast.config.ts",
...unitFastPlans.map(() => unitFastConfig),
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
"test/vitest/vitest.unit-src.config.ts",
"test/vitest/vitest.unit-security.config.ts",
"test/vitest/vitest.unit-support.config.ts",
"test/vitest/vitest.boundary.config.ts",
"test/vitest/vitest.tooling.config.ts",
...toolingPlans.map(() => toolingConfig),
"test/vitest/vitest.tooling-docker.config.ts",
"test/vitest/vitest.tooling-isolated.config.ts",
"test/vitest/vitest.contracts-channel-surface.config.ts",
@@ -4235,9 +4239,36 @@ describe("scripts/test-projects full-suite sharding", () => {
expect(gatewayTargets).toContain("src/gateway/server-network-runtime.e2e.test.ts");
expect(gatewayTargets).not.toContain("src/gateway/gateway.test.ts");
expect(Math.max(...gatewayChunkSizes) - Math.min(...gatewayChunkSizes)).toBeLessThanOrEqual(1);
expect(plans.filter((plan) => plan.config !== gatewayServerConfig)).toEqual(
const unitFastTargets = unitFastPlans.flatMap((plan) => plan.forwardedArgs);
expect(unitFastPlans.length).toBeGreaterThan(10);
expect(unitFastPlans.every((plan) => plan.forwardedArgs.length <= 70)).toBe(true);
expect(unitFastTargets.length).toBeGreaterThan(1_000);
expect(new Set(unitFastTargets).size).toBe(unitFastTargets.length);
expect(unitFastTargets).toContain("extensions/canvas/src/host/server.state-dir.test.ts");
expect(unitFastTargets).not.toContain("src/utils.test.ts");
const toolingTargets = toolingPlans.flatMap((plan) => plan.forwardedArgs);
expect(toolingPlans.length).toBeGreaterThan(1);
expect(toolingPlans.every((plan) => plan.forwardedArgs.length <= 2)).toBe(true);
expect(new Set(toolingTargets).size).toBe(toolingTargets.length);
expect(toolingTargets).toContain("test/scripts/test-group-report.test.ts");
expect(toolingTargets).toContain("src/scripts/control-ui-i18n-report.test.ts");
expect(toolingTargets).not.toContain("test/scripts/docker-build-helper.test.ts");
expect(toolingTargets).not.toContain("test/scripts/openclaw-e2e-instance.test.ts");
expect(
plans.filter(
(plan) =>
plan.config !== gatewayServerConfig &&
plan.config !== toolingConfig &&
plan.config !== unitFastConfig,
),
).toEqual(
plans
.filter((plan) => plan.config !== gatewayServerConfig)
.filter(
(plan) =>
plan.config !== gatewayServerConfig &&
plan.config !== toolingConfig &&
plan.config !== unitFastConfig,
)
.map((plan) => ({
config: plan.config,
forwardedArgs: [],