test: speed up changed test paths

This commit is contained in:
Peter Steinberger
2026-05-05 19:48:12 +01:00
parent 7d5ca3064a
commit 64b1f5fbf4
7 changed files with 63 additions and 50 deletions

View File

@@ -6,6 +6,10 @@ import type { FailoverReason } from "./pi-embedded-helpers.js";
const decisionLog = createSubsystemLogger("model-fallback").child("decision");
export function isModelFallbackDecisionLogEnabled(): boolean {
return decisionLog.isEnabled("warn");
}
function buildErrorObservationFields(error?: string): {
errorPreview?: string;
errorHash?: string;

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "node:crypto";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -280,7 +281,7 @@ describe("runWithModelFallback probe logic", () => {
setLoggerOverride({
level: "trace",
consoleLevel: "silent",
file: path.join(os.tmpdir(), `openclaw-model-fallback-probe-${Date.now()}.log`),
file: path.join(os.tmpdir(), `openclaw-model-fallback-probe-${randomUUID()}.log`),
});
const run = vi.fn().mockResolvedValue("probed-ok");
@@ -410,19 +411,26 @@ describe("runWithModelFallback probe logic", () => {
expectPrimaryProbeSuccess(result, run, "recovered");
});
it("attempts non-primary fallbacks during rate-limit cooldown after primary probe failure", async () => {
await expectProbeFailureFallsBack({
reason: "rate_limit",
it.each([
{
label: "rate-limit",
reason: "rate_limit" as const,
probeError: Object.assign(new Error("rate limited"), { status: 429 }),
});
});
it("attempts non-primary fallbacks during overloaded cooldown after primary probe failure", async () => {
await expectProbeFailureFallsBack({
reason: "overloaded",
},
{
label: "overloaded",
reason: "overloaded" as const,
probeError: Object.assign(new Error("service overloaded"), { status: 503 }),
});
});
},
])(
"attempts non-primary fallbacks during $label cooldown after primary probe failure",
async ({ reason, probeError }) => {
await expectProbeFailureFallsBack({
reason,
probeError,
});
},
);
it("keeps walking remaining fallbacks after an abort-wrapped RESOURCE_EXHAUSTED probe failure", async () => {
const cfg = makeCfg({
@@ -556,38 +564,22 @@ describe("runWithModelFallback probe logic", () => {
expect(_probeThrottleInternals.lastProbeAttempt.has("key-0")).toBe(true);
});
it("handles non-finite soonest safely (treats as probe-worthy)", async () => {
it("handles missing or non-finite soonest safely (treats as probe-worthy)", async () => {
const cfg = makeCfg();
// Return Infinity — should be treated as "probe" per the guard
mockedGetSoonestCooldownExpiry.mockReturnValue(Infinity);
for (const [label, soonest] of [
["infinity", Infinity],
["nan", Number.NaN],
["null", null],
] as const) {
_probeThrottleInternals.lastProbeAttempt.clear();
mockedGetSoonestCooldownExpiry.mockReturnValue(soonest);
const run = vi.fn().mockResolvedValue("ok-infinity");
const run = vi.fn().mockResolvedValue(`ok-${label}`);
const result = await runPrimaryCandidate(cfg, run);
expectPrimaryProbeSuccess(result, run, "ok-infinity");
});
it("handles NaN soonest safely (treats as probe-worthy)", async () => {
const cfg = makeCfg();
mockedGetSoonestCooldownExpiry.mockReturnValue(Number.NaN);
const run = vi.fn().mockResolvedValue("ok-nan");
const result = await runPrimaryCandidate(cfg, run);
expectPrimaryProbeSuccess(result, run, "ok-nan");
});
it("handles null soonest safely (treats as probe-worthy)", async () => {
const cfg = makeCfg();
mockedGetSoonestCooldownExpiry.mockReturnValue(null);
const run = vi.fn().mockResolvedValue("ok-null");
const result = await runPrimaryCandidate(cfg, run);
expectPrimaryProbeSuccess(result, run, "ok-null");
const result = await runPrimaryCandidate(cfg, run);
expectPrimaryProbeSuccess(result, run, `ok-${label}`);
}
});
it("single candidate skips with rate_limit and exhausts candidates", async () => {
@@ -700,8 +692,4 @@ describe("runWithModelFallback probe logic", () => {
expectPrimaryProbeSuccess(result, run, "billing-probe-ok");
});
it("skips billing-cooldowned primary with fallbacks when far from cooldown expiry", async () => {
await expectPrimarySkippedAfterLongCooldown("billing");
});
});

View File

@@ -26,6 +26,7 @@ import {
} from "./failover-policy.js";
import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
import {
isModelFallbackDecisionLogEnabled,
logModelFallbackDecision,
type ModelFallbackDecisionParams,
type ModelFallbackStepFields,
@@ -815,6 +816,9 @@ export async function runWithModelFallback<T>(params: {
let lastError: unknown;
const cooldownProbeUsedProviders = new Set<string>();
const observeDecision = async (decision: ModelFallbackDecisionParams) => {
if (!params.onFallbackStep && !isModelFallbackDecisionLogEnabled()) {
return;
}
const fallbackStep = logModelFallbackDecision(decision);
if (fallbackStep) {
await params.onFallbackStep?.(fallbackStep);

View File

@@ -190,6 +190,8 @@ async function loadSourceConfigSnapshotForTest(fallback: unknown): Promise<unkno
beforeEach(() => {
previousExitCode = process.exitCode;
process.exitCode = undefined;
modelRegistryState.models = [];
modelRegistryState.available = [];
modelRegistryState.getAllError = undefined;
modelRegistryState.getAvailableError = undefined;
modelRegistryState.findError = undefined;
@@ -510,7 +512,9 @@ describe("models list/status", () => {
loadProviderCatalogModelsForList.mockResolvedValueOnce([MOONSHOT_MODEL]);
const runtime = makeRuntime();
await modelsListCommand({ all: true, provider: "moonshot", json: true }, runtime);
await withEnvAsync({ KIMI_API_KEY: undefined, MOONSHOT_API_KEY: undefined }, () =>
modelsListCommand({ all: true, provider: "moonshot", json: true }, runtime),
);
const payload = parseJsonLog(runtime);
expect(loadModelCatalog).not.toHaveBeenCalled();

View File

@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { StringDecoder } from "node:string_decoder";
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
import {
acquireSessionWriteLock,
type SessionWriteLockAcquireTimeoutConfig,
@@ -12,6 +11,14 @@ import {
const TRANSCRIPT_APPEND_SCAN_CHUNK_BYTES = 64 * 1024;
const SESSION_MANAGER_APPEND_MAX_BYTES = 8 * 1024 * 1024;
let piCodingAgentModulePromise: Promise<typeof import("@mariozechner/pi-coding-agent")> | null =
null;
async function loadCurrentSessionVersion(): Promise<number> {
piCodingAgentModulePromise ??= import("@mariozechner/pi-coding-agent");
return (await piCodingAgentModulePromise).CURRENT_SESSION_VERSION;
}
type TranscriptLeafInfo = {
leafId?: string;
hasParentLinkedEntries: boolean;
@@ -117,6 +124,7 @@ async function migrateLinearTranscriptToParentLinked(transcriptPath: string): Pr
leafId?: string;
}> {
const raw = await fs.readFile(transcriptPath, "utf-8");
const currentSessionVersion = await loadCurrentSessionVersion();
const existingIds = new Set<string>();
const output: string[] = [];
let previousId: string | null = null;
@@ -138,7 +146,7 @@ async function migrateLinearTranscriptToParentLinked(transcriptPath: string): Pr
}
const record = parsed as Record<string, unknown>;
if (record.type === "session") {
output.push(JSON.stringify({ ...record, version: CURRENT_SESSION_VERSION }));
output.push(JSON.stringify({ ...record, version: currentSessionVersion }));
continue;
}
const id = normalizeEntryId(record.id) ?? generateEntryId(existingIds);
@@ -170,10 +178,11 @@ async function ensureTranscriptHeader(
if (stat?.isFile() && stat.size > 0) {
return;
}
const currentSessionVersion = await loadCurrentSessionVersion();
await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
const header = {
type: "session",
version: CURRENT_SESSION_VERSION,
version: currentSessionVersion,
id: params.sessionId ?? randomUUID(),
timestamp: new Date().toISOString(),
cwd: params.cwd ?? process.cwd(),

View File

@@ -80,6 +80,11 @@ const facadeMockHelpers = vi.hoisted(() => {
vi.mock("./plugins/plugin-registry.js", () => ({
loadPluginManifestRegistryForPluginRegistry,
loadPluginRegistrySnapshotWithMetadata: () => ({
source: "derived",
snapshot: { plugins: [] },
diagnostics: [],
}),
}));
vi.mock("./secrets/channel-env-vars.js", () => ({

View File

@@ -51,7 +51,6 @@ const EXPECTED_EMPTY_CONFIG_GATEWAY_STARTUP_PLUGIN_IDS = [
"acpx",
"browser",
"device-pair",
"discord",
"file-transfer",
"memory-core",
"phone-control",
@@ -470,7 +469,7 @@ describe("bundled plugin metadata", () => {
expect(
resolveGatewayStartupPluginIdsFromRegistry({
config: {},
env: process.env,
env: {},
index,
manifestRegistry,
platform: "linux",