From 64b1f5fbf498b4b87d3e60e42e61a071f1f4d787 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 5 May 2026 19:48:12 +0100 Subject: [PATCH] test: speed up changed test paths --- src/agents/model-fallback-observation.ts | 4 ++ src/agents/model-fallback.probe.test.ts | 76 +++++++++------------ src/agents/model-fallback.ts | 4 ++ src/commands/models.list.e2e.test.ts | 6 +- src/config/sessions/transcript-append.ts | 15 +++- src/plugin-activation-boundary.test.ts | 5 ++ src/plugins/bundled-plugin-metadata.test.ts | 3 +- 7 files changed, 63 insertions(+), 50 deletions(-) diff --git a/src/agents/model-fallback-observation.ts b/src/agents/model-fallback-observation.ts index 13c557ce9ed..35c8a9d5a95 100644 --- a/src/agents/model-fallback-observation.ts +++ b/src/agents/model-fallback-observation.ts @@ -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; diff --git a/src/agents/model-fallback.probe.test.ts b/src/agents/model-fallback.probe.test.ts index 945ff05c932..4fb65847258 100644 --- a/src/agents/model-fallback.probe.test.ts +++ b/src/agents/model-fallback.probe.test.ts @@ -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"); - }); }); diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index f4421ce46bd..0ff9ba8b31c 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -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(params: { let lastError: unknown; const cooldownProbeUsedProviders = new Set(); const observeDecision = async (decision: ModelFallbackDecisionParams) => { + if (!params.onFallbackStep && !isModelFallbackDecisionLogEnabled()) { + return; + } const fallbackStep = logModelFallbackDecision(decision); if (fallbackStep) { await params.onFallbackStep?.(fallbackStep); diff --git a/src/commands/models.list.e2e.test.ts b/src/commands/models.list.e2e.test.ts index 5c612cc0422..4fcbe60b040 100644 --- a/src/commands/models.list.e2e.test.ts +++ b/src/commands/models.list.e2e.test.ts @@ -190,6 +190,8 @@ async function loadSourceConfigSnapshotForTest(fallback: unknown): Promise { 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(); diff --git a/src/config/sessions/transcript-append.ts b/src/config/sessions/transcript-append.ts index 8b505183728..7b4959d3f35 100644 --- a/src/config/sessions/transcript-append.ts +++ b/src/config/sessions/transcript-append.ts @@ -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 | null = + null; + +async function loadCurrentSessionVersion(): Promise { + 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(); const output: string[] = []; let previousId: string | null = null; @@ -138,7 +146,7 @@ async function migrateLinearTranscriptToParentLinked(transcriptPath: string): Pr } const record = parsed as Record; 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(), diff --git a/src/plugin-activation-boundary.test.ts b/src/plugin-activation-boundary.test.ts index fbbedae6ec6..851d3168777 100644 --- a/src/plugin-activation-boundary.test.ts +++ b/src/plugin-activation-boundary.test.ts @@ -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", () => ({ diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 08608d899d3..2faa4ae7c38 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -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",