




















@@ -6,9 +6,13 @@ import { resetLogger, setLoggerOverride } from "../logging/logger.js";
66import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js";
77import {
88clearCurrentPluginMetadataSnapshot,
9+resolvePluginMetadataControlPlaneFingerprint,
910setCurrentPluginMetadataSnapshot,
1011} from "../plugins/current-plugin-metadata-snapshot.js";
12+import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js";
13+import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js";
1114import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
15+import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
1216import { CommandLaneTaskTimeoutError } from "../process/command-queue.js";
1317import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
1418import type { AuthProfileStore } from "./auth-profiles/types.js";
@@ -164,10 +168,7 @@ let authTempRoot = "";
164168let authTempCounter = 0;
165169166170beforeAll(() => {
167-setCurrentPluginMetadataSnapshot(loadPluginMetadataSnapshot({ config: {}, env: process.env }), {
168-config: {},
169-env: process.env,
170-});
171+setDefaultPluginMetadataSnapshot();
171172});
172173173174afterAll(() => {
@@ -181,6 +182,73 @@ function resetModelFallbackTestState(): void {
181182authSourceCheckMock.hasAnyAuthProfileStoreSource.mockReset().mockReturnValue(false);
182183}
183184185+function setDefaultPluginMetadataSnapshot(): void {
186+setCurrentPluginMetadataSnapshot(loadPluginMetadataSnapshot({ config: {}, env: process.env }), {
187+config: {},
188+env: process.env,
189+});
190+}
191+192+function createModelNormalizerSnapshot(params: {
193+manifestHash: string;
194+prefix: string;
195+}): PluginMetadataSnapshot {
196+const policyHash = resolveInstalledPluginIndexPolicyHash({});
197+const index: InstalledPluginIndex = {
198+version: 1,
199+hostContractVersion: "test-host",
200+compatRegistryVersion: "test-compat",
201+migrationVersion: 1,
202+ policyHash,
203+generatedAtMs: 0,
204+installRecords: {},
205+plugins: [
206+{
207+pluginId: "fallback-normalizer",
208+manifestPath: `/tmp/fallback-normalizer-${params.manifestHash}/openclaw.plugin.json`,
209+manifestHash: params.manifestHash,
210+source: `/tmp/fallback-normalizer-${params.manifestHash}/index.ts`,
211+rootDir: `/tmp/fallback-normalizer-${params.manifestHash}`,
212+origin: "global",
213+enabled: true,
214+startup: {
215+sidecar: false,
216+memory: false,
217+deferConfiguredChannelFullLoadUntilAfterListen: false,
218+agentHarnesses: [],
219+},
220+compat: [],
221+},
222+],
223+diagnostics: [],
224+};
225+return {
226+ policyHash,
227+configFingerprint: resolvePluginMetadataControlPlaneFingerprint(
228+{},
229+{
230+env: process.env,
231+ index,
232+ policyHash,
233+},
234+),
235+ index,
236+registryDiagnostics: [],
237+plugins: [
238+{
239+id: "fallback-normalizer",
240+modelIdNormalization: {
241+providers: {
242+demo: {
243+prefixWhenBare: params.prefix,
244+},
245+},
246+},
247+},
248+],
249+} as unknown as PluginMetadataSnapshot;
250+}
251+184252afterEach(resetModelFallbackTestState);
185253186254beforeEach(() => {
@@ -227,6 +295,31 @@ function makeProviderFallbackCfg(provider: string): OpenClawConfig {
227295});
228296}
229297298+function makeProviderOrderFallbackCfg(
299+entries: Array<[provider: string, model: string]>,
300+): OpenClawConfig {
301+return {
302+agents: {
303+defaults: {
304+model: {
305+fallbacks: [],
306+},
307+},
308+},
309+models: {
310+providers: Object.fromEntries(
311+entries.map(([provider, model]) => [
312+provider,
313+{
314+baseUrl: `https://${provider}.example.test`,
315+models: [{ id: model }],
316+},
317+]),
318+),
319+},
320+} as unknown as OpenClawConfig;
321+}
322+230323async function withTempAuthStore<T>(
231324store: AuthProfileStore,
232325run: (tempDir: string) => Promise<T>,
@@ -1969,6 +2062,82 @@ describe("runWithModelFallback", () => {
19692062]);
19702063});
197120642065+it("does not reuse provider-order-sensitive configured fallback candidates", () => {
2066+const anthropicFirst = makeProviderOrderFallbackCfg([
2067+["anthropic", "claude-sonnet-4"],
2068+["ollama", "llama3"],
2069+]);
2070+const ollamaFirst = makeProviderOrderFallbackCfg([
2071+["ollama", "llama3"],
2072+["anthropic", "claude-sonnet-4"],
2073+]);
2074+2075+expect(
2076+testing.resolveFallbackCandidates({
2077+cfg: anthropicFirst,
2078+provider: "",
2079+model: "",
2080+fallbacksOverride: [],
2081+}),
2082+).toEqual([{ provider: "anthropic", model: "claude-sonnet-4" }]);
2083+expect(
2084+testing.resolveFallbackCandidates({
2085+cfg: ollamaFirst,
2086+provider: "",
2087+model: "",
2088+fallbacksOverride: [],
2089+}),
2090+).toEqual([{ provider: "ollama", model: "llama3" }]);
2091+});
2092+2093+it("does not reuse fallback candidate cache entries across manifest normalization snapshots", () => {
2094+const cfg = makeCfg({
2095+agents: {
2096+defaults: {
2097+model: {
2098+fallbacks: [],
2099+},
2100+},
2101+},
2102+});
2103+2104+try {
2105+setCurrentPluginMetadataSnapshot(
2106+createModelNormalizerSnapshot({
2107+manifestHash: "alpha",
2108+prefix: "alpha",
2109+}),
2110+{ config: {}, env: process.env },
2111+);
2112+expect(
2113+testing.resolveFallbackCandidates({
2114+ cfg,
2115+provider: "demo",
2116+model: "demo-model",
2117+fallbacksOverride: [],
2118+}),
2119+).toEqual([{ provider: "demo", model: "alpha/demo-model" }]);
2120+2121+setCurrentPluginMetadataSnapshot(
2122+createModelNormalizerSnapshot({
2123+manifestHash: "bravo",
2124+prefix: "bravo",
2125+}),
2126+{ config: {}, env: process.env },
2127+);
2128+expect(
2129+testing.resolveFallbackCandidates({
2130+ cfg,
2131+provider: "demo",
2132+model: "demo-model",
2133+fallbacksOverride: [],
2134+}),
2135+).toEqual([{ provider: "demo", model: "bravo/demo-model" }]);
2136+} finally {
2137+setDefaultPluginMetadataSnapshot();
2138+}
2139+});
2140+19722141it("defaults provider/model when missing (regression #946)", () => {
19732142const cfg = makeCfg({
19742143agents: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。