mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 00:44:49 +00:00
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import os from "node:os";
|
|
import path from "node:path";
|
|
import { createCapturedPluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { resolveHomePath } from "./helpers.js";
|
|
import pluginEntry from "./index.js";
|
|
import { HERMES_REASON_INCLUDE_SECRETS } from "./items.js";
|
|
import { buildHermesMigrationProvider } from "./provider.js";
|
|
import { cleanupTempRoots, makeContext, makeTempRoot, writeFile } from "./test/provider-helpers.js";
|
|
|
|
function itemById(
|
|
items: Array<{ id: string; [key: string]: unknown }>,
|
|
id: string,
|
|
): { id: string; [key: string]: unknown } | undefined {
|
|
return items.find((item) => item.id === id);
|
|
}
|
|
|
|
describe("Hermes migration provider", () => {
|
|
afterEach(async () => {
|
|
await cleanupTempRoots();
|
|
});
|
|
|
|
it("registers the Hermes migration provider through the plugin entry", () => {
|
|
const captured = createCapturedPluginRegistration();
|
|
pluginEntry.register(captured.api);
|
|
expect(captured.migrationProviders.map((provider) => provider.id)).toEqual(["hermes"]);
|
|
});
|
|
|
|
it("resolves tilde source paths against the OS home when OPENCLAW_HOME is set", () => {
|
|
const previous = process.env.OPENCLAW_HOME;
|
|
process.env.OPENCLAW_HOME = path.join(path.sep, "tmp", "openclaw-home");
|
|
try {
|
|
expect(resolveHomePath("~/.hermes")).toBe(path.join(os.homedir(), ".hermes"));
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.OPENCLAW_HOME;
|
|
} else {
|
|
process.env.OPENCLAW_HOME = previous;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("detects Hermes sources supported by planning", async () => {
|
|
const root = await makeTempRoot();
|
|
const source = path.join(root, "hermes");
|
|
await writeFile(path.join(source, "SOUL.md"), "# Hermes soul\n");
|
|
|
|
const provider = buildHermesMigrationProvider();
|
|
const detected = await provider.detect?.(
|
|
makeContext({
|
|
source,
|
|
stateDir: path.join(root, "state"),
|
|
workspaceDir: path.join(root, "workspace"),
|
|
}),
|
|
);
|
|
|
|
expect(detected?.found).toBe(true);
|
|
expect(detected?.source).toBe(source);
|
|
expect(detected?.confidence).toBe("high");
|
|
});
|
|
|
|
it("detects archive-only Hermes sources", async () => {
|
|
const root = await makeTempRoot();
|
|
const source = path.join(root, "hermes");
|
|
await writeFile(path.join(source, "logs", "run.log"), "log line\n");
|
|
|
|
const provider = buildHermesMigrationProvider();
|
|
const detected = await provider.detect?.(
|
|
makeContext({
|
|
source,
|
|
stateDir: path.join(root, "state"),
|
|
workspaceDir: path.join(root, "workspace"),
|
|
}),
|
|
);
|
|
|
|
expect(detected?.found).toBe(true);
|
|
expect(detected?.source).toBe(source);
|
|
expect(detected?.confidence).toBe("high");
|
|
});
|
|
|
|
it("rejects missing Hermes sources before planning", async () => {
|
|
const root = await makeTempRoot();
|
|
const source = path.join(root, "missing-hermes");
|
|
|
|
const provider = buildHermesMigrationProvider();
|
|
|
|
await expect(
|
|
provider.plan(
|
|
makeContext({
|
|
source,
|
|
stateDir: path.join(root, "state"),
|
|
workspaceDir: path.join(root, "workspace"),
|
|
}),
|
|
),
|
|
).rejects.toThrow(`Hermes state was not found at ${source}`);
|
|
});
|
|
|
|
it("plans model, workspace, memory, skill, and secret items without importing secrets by default", async () => {
|
|
const root = await makeTempRoot();
|
|
const source = path.join(root, "hermes");
|
|
const workspaceDir = path.join(root, "workspace");
|
|
const stateDir = path.join(root, "state");
|
|
await writeFile(
|
|
path.join(source, "config.yaml"),
|
|
"model:\n provider: openai\n model: gpt-5.4\n",
|
|
);
|
|
await writeFile(path.join(source, ".env"), "OPENAI_API_KEY=sk-hermes\n");
|
|
await writeFile(path.join(source, "SOUL.md"), "# Hermes soul\n");
|
|
await writeFile(path.join(source, "memories", "MEMORY.md"), "remember this\n");
|
|
await writeFile(path.join(source, "skills", "Ship It", "SKILL.md"), "# Ship It\n");
|
|
await writeFile(path.join(workspaceDir, "SOUL.md"), "# Existing soul\n");
|
|
|
|
const provider = buildHermesMigrationProvider();
|
|
const plan = await provider.plan(
|
|
makeContext({
|
|
source,
|
|
stateDir,
|
|
workspaceDir,
|
|
model: "anthropic/claude-sonnet-4.6",
|
|
}),
|
|
);
|
|
|
|
expect(plan.summary.total).toBe(8);
|
|
expect(plan.summary.conflicts).toBe(2);
|
|
expect(plan.summary.sensitive).toBe(1);
|
|
expect(itemById(plan.items, "config:default-model")?.status).toBe("conflict");
|
|
expect(itemById(plan.items, "config:memory")?.status).toBe("planned");
|
|
expect(itemById(plan.items, "config:memory-plugin-slot")?.status).toBe("planned");
|
|
expect(itemById(plan.items, "config:model-providers")?.status).toBe("planned");
|
|
expect(itemById(plan.items, "workspace:SOUL.md")?.status).toBe("conflict");
|
|
const memory = itemById(plan.items, "memory:MEMORY.md");
|
|
expect(memory?.action).toBe("append");
|
|
expect(memory?.status).toBe("planned");
|
|
expect(itemById(plan.items, "skill:ship-it")?.status).toBe("planned");
|
|
const secret = itemById(plan.items, "secret:openai");
|
|
expect(secret?.sensitive).toBe(true);
|
|
expect(secret?.status).toBe("skipped");
|
|
expect(secret?.reason).toBe(HERMES_REASON_INCLUDE_SECRETS);
|
|
expect(plan.warnings).toEqual([
|
|
"Secrets were detected but skipped. Re-run with --include-secrets to import supported API keys.",
|
|
"Conflicts were found. Re-run with --overwrite to replace conflicting targets after item-level backups.",
|
|
]);
|
|
});
|
|
});
|