Files
openclaw/extensions/migrate-hermes/model.apply.test.ts
T
Peter Steinberger 8616c0c374 refactor: finish shared test helper migrations (#120996)
* test: finish shared helper migrations

* test: fix helper migration CI

* style: fix test import ordering

* test(acpx): restore deferred void types

* test: fix helper migrations after rebase
2026-08-09 06:00:06 -07:00

228 lines
7.1 KiB
TypeScript

// Migrate Hermes tests cover model.apply plugin behavior.
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
import {
resolvePreferredOpenClawTmpDir,
tempWorkspace,
type TempWorkspace,
} from "openclaw/plugin-sdk/temp-path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
HERMES_REASON_DEFAULT_MODEL_CONFIGURED,
HERMES_REASON_MODEL_PROVIDER_CONFLICT,
} from "./items.js";
import { buildHermesMigrationProvider } from "./provider.js";
import { makeConfigRuntime, makeContext, writeFile } from "./test/provider-helpers.js";
let testWorkspace: TempWorkspace;
function defaultModelItem(status: "migrated" | "conflict") {
return {
id: "config:default-model",
kind: "config",
action: "update",
target: "agents.defaults.model",
status,
...(status === "conflict" ? { reason: HERMES_REASON_DEFAULT_MODEL_CONFIGURED } : {}),
details: { model: "openai/gpt-5.4" },
};
}
describe("Hermes migration model apply", () => {
beforeEach(async () => {
testWorkspace = await tempWorkspace({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: "openclaw-migrate-hermes-",
});
});
afterEach(async () => {
await testWorkspace.cleanup();
});
it("updates only the primary model when applying over object-form model config", async () => {
const root = testWorkspace.dir;
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
await writeFile(
path.join(source, "config.yaml"),
"model:\n provider: openai\n model: gpt-5.4\n",
);
const existingConfig = {
agents: {
defaults: {
workspace: workspaceDir,
model: {
primary: "anthropic/claude-sonnet-4.6",
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
timeoutMs: 120_000,
},
},
},
} as OpenClawConfig;
let writtenConfig: OpenClawConfig | undefined;
const provider = buildHermesMigrationProvider({
runtime: makeConfigRuntime(existingConfig, (next) => {
writtenConfig = next;
}),
});
const result = await provider.apply(
makeContext({
source,
stateDir,
workspaceDir,
overwrite: true,
model: existingConfig.agents?.defaults?.model,
reportDir,
}),
);
expect(result.items).toEqual([defaultModelItem("migrated")]);
expect(writtenConfig?.agents?.defaults?.model).toEqual({
primary: "openai/gpt-5.4",
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
timeoutMs: 120_000,
});
});
it("updates the default-agent model override when applying with overwrite", async () => {
const root = testWorkspace.dir;
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
await writeFile(
path.join(source, "config.yaml"),
"model:\n provider: openai\n model: gpt-5.4\n",
);
const existingConfig = {
agents: {
defaults: {
workspace: workspaceDir,
model: {
primary: "google/gemini-3-pro",
fallbacks: ["openai/gpt-5.4"],
},
},
list: [
{
id: "main",
default: true,
model: {
primary: "anthropic/claude-sonnet-4.6",
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
},
},
],
},
} as OpenClawConfig;
let writtenConfig: OpenClawConfig | undefined;
const provider = buildHermesMigrationProvider({
runtime: makeConfigRuntime(existingConfig, (next) => {
writtenConfig = next;
}),
});
const result = await provider.apply(
makeContext({
source,
stateDir,
workspaceDir,
config: existingConfig,
overwrite: true,
reportDir,
}),
);
expect(result.items).toEqual([defaultModelItem("migrated")]);
expect(writtenConfig?.agents?.list?.[0]?.model).toEqual({
primary: "openai/gpt-5.4",
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
});
expect(writtenConfig?.agents?.defaults?.model).toEqual(existingConfig.agents?.defaults?.model);
});
it("reports late-created default models as conflicts without overwriting", async () => {
const root = testWorkspace.dir;
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
await writeFile(
path.join(source, "config.yaml"),
"model:\n provider: openai\n model: gpt-5.4\n",
);
const lateConfig = {
agents: {
defaults: {
workspace: workspaceDir,
model: "anthropic/claude-sonnet-4.6",
},
},
} as OpenClawConfig;
const provider = buildHermesMigrationProvider({
runtime: makeConfigRuntime(lateConfig),
});
const ctx = makeContext({ source, stateDir, workspaceDir, reportDir });
const plan = await provider.plan(ctx);
const result = await provider.apply(ctx, plan);
expect(result.items).toEqual([defaultModelItem("conflict")]);
expect(result.summary.conflicts).toBe(1);
expect(lateConfig.agents?.defaults?.model).toBe("anthropic/claude-sonnet-4.6");
});
it("does not apply a custom default after its provider develops a late conflict", async () => {
const root = testWorkspace.dir;
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
await writeFile(
path.join(source, "config.yaml"),
[
"model:",
" provider: custom:acme",
" default: imported-model",
"providers:",
" acme:",
" base_url: https://new.example.test/v1",
" transport: openai_chat",
"",
].join("\n"),
);
const lateConfig = {
agents: { defaults: { workspace: workspaceDir } },
models: {
providers: {
acme: {
baseUrl: "https://old.example.test/v1",
api: "openai-completions",
models: [],
},
},
},
} as OpenClawConfig;
const provider = buildHermesMigrationProvider({ runtime: makeConfigRuntime(lateConfig) });
const ctx = makeContext({ source, stateDir, workspaceDir, reportDir });
const plan = await provider.plan(ctx);
const result = await provider.apply(ctx, plan);
expect(result.items.find((item) => item.id === "config:model-provider:acme")?.status).toBe(
"conflict",
);
expect(result.items.find((item) => item.id === "config:default-model")).toEqual(
expect.objectContaining({
status: "conflict",
reason: HERMES_REASON_MODEL_PROVIDER_CONFLICT,
}),
);
expect(lateConfig.agents?.defaults?.model).toBeUndefined();
});
});