


























11// Tests migration cleanup for orphaned state keys.
22import fs from "node:fs";
33import path from "node:path";
4-import { describe, expect, it, vi } from "vitest";
4+import { beforeEach, describe, expect, it, vi } from "vitest";
55import type { OpenClawConfig } from "../config/config.js";
66import { withTempDir } from "../test-helpers/temp-dir.js";
77import {
88migrateOrphanedSessionKeys,
99sessionStoreTextMayNeedCanonicalization,
1010} from "./state-migrations.js";
111112+const listPluginDoctorSessionStoreAgentIdsMock = vi.hoisted(() => vi.fn((): string[] => []));
13+14+vi.mock("../plugins/doctor-contract-registry.js", async (importOriginal) => {
15+const actual = await importOriginal<typeof import("../plugins/doctor-contract-registry.js")>();
16+return {
17+ ...actual,
18+listPluginDoctorSessionStoreAgentIds: listPluginDoctorSessionStoreAgentIdsMock,
19+};
20+});
21+1222function writeStore(storePath: string, store: Record<string, unknown>): void {
1323fs.mkdirSync(path.dirname(storePath), { recursive: true });
1424fs.writeFileSync(storePath, JSON.stringify(store));
@@ -55,14 +65,24 @@ function sharedMainOpsConfig(sharedStorePath: string): OpenClawConfig {
5565} as OpenClawConfig;
5666}
576758-async function migrateFixtureState(stateDir: string, cfg: OpenClawConfig = OPS_WORK_CONFIG) {
68+async function migrateFixtureState(
69+stateDir: string,
70+cfg: OpenClawConfig = OPS_WORK_CONFIG,
71+additionalAgentIds?: readonly string[],
72+) {
5973return migrateOrphanedSessionKeys({
6074 cfg,
6175env: { OPENCLAW_STATE_DIR: stateDir },
76+ additionalAgentIds,
6277});
6378}
64796580describe("migrateOrphanedSessionKeys", () => {
81+beforeEach(() => {
82+listPluginDoctorSessionStoreAgentIdsMock.mockReset();
83+listPluginDoctorSessionStoreAgentIdsMock.mockReturnValue([]);
84+});
85+6686it("recognizes canonical stores without parsing them for migration", () => {
6787const raw = JSON.stringify({
6888"agent:main:discord:channel:123": { sessionId: "channel", updatedAt: 1 },
@@ -238,6 +258,7 @@ describe("migrateOrphanedSessionKeys", () => {
238258const result = await migrateOrphanedSessionKeys({
239259 cfg,
240260env: { OPENCLAW_STATE_DIR: stateDir },
261+additionalAgentIds: ["voice"],
241262});
242263243264const store = readStore(voiceStorePath);
@@ -254,6 +275,41 @@ describe("migrateOrphanedSessionKeys", () => {
254275});
255276});
256277278+it("discovers plugin-owned agents through doctor contracts", async () => {
279+await withStateFixture(async ({ tmpDir, stateDir }) => {
280+listPluginDoctorSessionStoreAgentIdsMock.mockReturnValue(["voice"]);
281+const storeTemplate = path.join(tmpDir, "stores", "{agentId}", "sessions.json");
282+const voiceStorePath = path.join(tmpDir, "stores", "voice", "sessions.json");
283+writeStore(voiceStorePath, {
284+"voice:15550001111": { sessionId: "legacy-voice", updatedAt: 2000 },
285+});
286+const cfg = {
287+session: { store: storeTemplate },
288+agents: { list: [{ id: "main", default: true }] },
289+plugins: {
290+entries: {
291+"voice-call": { config: { agentId: "voice" } },
292+},
293+},
294+} as OpenClawConfig;
295+296+const result = await migrateFixtureState(stateDir, cfg);
297+298+expect(listPluginDoctorSessionStoreAgentIdsMock).toHaveBeenCalledWith({
299+config: cfg,
300+env: { OPENCLAW_STATE_DIR: stateDir },
301+pluginIds: ["voice-call"],
302+});
303+const store = readStore(voiceStorePath);
304+expect(requireStoreEntry(store, "agent:voice:voice:15550001111").sessionId).toBe(
305+"legacy-voice",
306+);
307+expect(store["voice:15550001111"]).toBeUndefined();
308+expect(result.changes).toHaveLength(1);
309+expect(result.warnings).toHaveLength(0);
310+});
311+});
312+257313it.each([
258314{ scope: undefined, canonicalMainKey: "agent:voice:main" },
259315{ scope: "global" as const, canonicalMainKey: "global" },
@@ -278,7 +334,7 @@ describe("migrateOrphanedSessionKeys", () => {
278334},
279335} as OpenClawConfig;
280336281-const result = await migrateFixtureState(stateDir, cfg);
337+const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
282338283339const store = readStore(voiceStorePath);
284340expect(requireStoreEntry(store, "agent:main:main").sessionId).toBe("explicit-foreign");
@@ -310,7 +366,7 @@ describe("migrateOrphanedSessionKeys", () => {
310366},
311367} as OpenClawConfig;
312368313-const result = await migrateFixtureState(stateDir, cfg);
369+const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
314370315371const store = readStore(sharedStorePath);
316372expect(requireStoreEntry(store, "agent:main:main").sessionId).toBe("ambiguous-main");
@@ -336,7 +392,7 @@ describe("migrateOrphanedSessionKeys", () => {
336392},
337393} as OpenClawConfig;
338394339-const result = await migrateFixtureState(stateDir, cfg);
395+const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
340396341397const store = readStore(sharedStorePath);
342398expect(requireStoreEntry(store, "agent:main:work").sessionId).toBe("ambiguous-main");
@@ -370,8 +426,8 @@ describe("migrateOrphanedSessionKeys", () => {
370426},
371427} as OpenClawConfig;
372428373-const result = await migrateFixtureState(stateDir, cfg);
374-const rerun = await migrateFixtureState(stateDir, cfg);
429+const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
430+const rerun = await migrateFixtureState(stateDir, cfg, ["voice"]);
375431376432expect(result.changes).toHaveLength(0);
377433expect(result.warnings).toEqual([
@@ -457,7 +513,7 @@ describe("migrateOrphanedSessionKeys", () => {
457513},
458514} as OpenClawConfig;
459515460-const result = await migrateFixtureState(stateDir, cfg);
516+const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
461517462518expect(result.changes).toHaveLength(0);
463519expect(result.warnings).toEqual([
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。