fix(agents): tolerate replaced thinking catalog owners

This commit is contained in:
Dallin Romney
2026-08-21 09:47:39 -07:00
parent 4343b38ce7
commit 86d156523f
2 changed files with 54 additions and 6 deletions
@@ -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");
+1 -1
View File
@@ -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;