mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(agents): tolerate replaced thinking catalog owners
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
// Boundary proof for the turn-path thinking fallback: manifest first, then a provider-scoped
|
||||
// static catalog, then scoped live discovery only for runtime-only models (e.g. Ollama).
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginMetadataSnapshot } from "../config/plugin-auto-enable.test-helpers.js";
|
||||
import { PreparedModelRuntimeOwnerNotPublishedError } from "./prepared-model-runtime.errors.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "./prepared-model-runtime.types.js";
|
||||
|
||||
const manifestCatalogMock = vi.fn((..._args: unknown[]): Array<Record<string, unknown>> => []);
|
||||
const scopedStaticMock = vi.fn(
|
||||
@@ -16,6 +19,13 @@ const scopedLiveMock = vi.fn(
|
||||
}),
|
||||
);
|
||||
const publishedSnapshotMock = vi.fn((..._args: unknown[]) => undefined as unknown);
|
||||
const preparedSnapshotMock = vi.fn<
|
||||
(input: { agentDir: string }) => Promise<PreparedModelRuntimeSnapshot>
|
||||
>(async (input) => {
|
||||
throw new PreparedModelRuntimeOwnerNotPublishedError(
|
||||
`not published for test (${input.agentDir})`,
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock("./model-catalog.js", () => ({
|
||||
loadManifestModelCatalog: (...args: unknown[]) => manifestCatalogMock(...args),
|
||||
@@ -27,11 +37,8 @@ vi.mock("./prepared-model-runtime.js", async (importOriginal) => {
|
||||
...actual,
|
||||
getPreparedModelRuntimeSnapshot: (...args: unknown[]) => publishedSnapshotMock(...args),
|
||||
// No published lifecycle owner: force the scoped read-only builders to run.
|
||||
prepareModelRuntimeSnapshot: vi.fn(async (input: { agentDir: string }) => {
|
||||
throw new actual.PreparedModelRuntimeOwnerNotPublishedError(
|
||||
`not published for test (${input.agentDir})`,
|
||||
);
|
||||
}),
|
||||
prepareModelRuntimeSnapshot: (...args: Parameters<typeof preparedSnapshotMock>) =>
|
||||
preparedSnapshotMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -54,6 +61,11 @@ describe("loadProviderScopedThinkingCatalog", () => {
|
||||
scopedStaticMock.mockResolvedValue({ entries: [], routeVariants: [] });
|
||||
scopedLiveMock.mockResolvedValue({ entries: [], routeVariants: [] });
|
||||
publishedSnapshotMock.mockReturnValue(undefined);
|
||||
preparedSnapshotMock.mockImplementation(async (input) => {
|
||||
throw new PreparedModelRuntimeOwnerNotPublishedError(
|
||||
`not published for test (${input.agentDir})`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the published prepared generation over partial manifest compatibility", async () => {
|
||||
@@ -130,6 +142,42 @@ describe("loadProviderScopedThinkingCatalog", () => {
|
||||
expect(scopedLiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the scoped catalog while a published owner has the replaced config", async () => {
|
||||
preparedSnapshotMock.mockResolvedValue({
|
||||
agentDir: "/tmp/model-catalog-test",
|
||||
activeProjectKeys: [],
|
||||
config: { skills: { entries: { marker: { enabled: false } } } },
|
||||
authModes: {},
|
||||
metadataSnapshot: createPluginMetadataSnapshot({
|
||||
config: {},
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
}),
|
||||
allowGatewaySubagentBinding: false,
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
configuredRuntimeModels: [],
|
||||
inlineProviderModels: [],
|
||||
createStores: () => {
|
||||
throw new Error("stores are outside this catalog fallback test");
|
||||
},
|
||||
});
|
||||
scopedStaticMock.mockResolvedValue({
|
||||
entries: [{ provider: "acme", id: "replacement-model", reasoning: true }],
|
||||
routeVariants: [],
|
||||
});
|
||||
const { loadProviderScopedThinkingCatalog } = await import("./prepared-model-catalog.js");
|
||||
|
||||
const catalog = await loadProviderScopedThinkingCatalog({
|
||||
config: { skills: { entries: { marker: { enabled: true } } } },
|
||||
provider: "acme",
|
||||
model: "replacement-model",
|
||||
});
|
||||
|
||||
expect(catalog).toEqual([
|
||||
expect.objectContaining({ provider: "acme", id: "replacement-model", reasoning: true }),
|
||||
]);
|
||||
expect(scopedStaticMock).toHaveBeenCalledWith(expect.anything(), ["acme"]);
|
||||
});
|
||||
|
||||
it("runs provider-scoped live discovery for runtime-only models and keeps their thinking", async () => {
|
||||
scopedLiveMock.mockResolvedValue({ entries: [ollamaEntry], routeVariants: [] });
|
||||
const { loadProviderScopedThinkingCatalog } = await import("./prepared-model-catalog.js");
|
||||
|
||||
@@ -332,7 +332,7 @@ async function loadScopedReadOnlyModelCatalog(
|
||||
try {
|
||||
const prepared = await prepareModelRuntimeSnapshot(candidate);
|
||||
if (!preparedModelRuntimeConfigsMatch(prepared.config, candidate.config)) {
|
||||
throw new PreparedModelCatalogConfigReplacedError(candidate.agentDir);
|
||||
continue;
|
||||
}
|
||||
if (isPreparedModelCatalogFull(prepared.modelCatalog)) {
|
||||
return prepared.modelCatalog;
|
||||
|
||||
Reference in New Issue
Block a user