test: stabilize startup session migration flake (#97370)

* test: stabilize startup session migration flake

* test: cover voice call doctor session ids
This commit is contained in:
Dallin Romney
2026-06-28 04:29:41 -07:00
committed by GitHub
parent 39c4c02653
commit 08d15ec32d
3 changed files with 96 additions and 52 deletions
@@ -1,10 +1,7 @@
/**
* Gateway startup session migration tests.
*/
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { runStartupSessionMigration } from "./server-startup-session-migration.js";
function makeLog() {
@@ -29,47 +26,6 @@ function firstLogMessage(log: ReturnType<typeof vi.fn>, label: string): string {
}
describe("runStartupSessionMigration", () => {
it("discovers plugin-owned agents during direct gateway startup", async () => {
await withTempDir({ prefix: "openclaw-startup-migration-" }, async (tempDir) => {
const storeTemplate = path.join(tempDir, "stores", "{agentId}", "sessions.json");
const voiceStorePath = path.join(tempDir, "stores", "voice", "sessions.json");
fs.mkdirSync(path.dirname(voiceStorePath), { recursive: true });
fs.writeFileSync(
voiceStorePath,
JSON.stringify({
"voice:15550001111": { sessionId: "legacy-voice", updatedAt: 1 },
}),
);
const cfg = {
session: { store: storeTemplate },
agents: { list: [{ id: "main", default: true }] },
plugins: {
entries: { "voice-call": { config: { agentId: "voice" } } },
},
} as ReturnType<typeof makeCfg>;
const log = makeLog();
await runStartupSessionMigration({
cfg,
env: {
...process.env,
HOME: tempDir,
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
OPENCLAW_STATE_DIR: path.join(tempDir, "state"),
},
log,
});
const store = JSON.parse(fs.readFileSync(voiceStorePath, "utf8")) as Record<
string,
{ sessionId?: string }
>;
expect(store["agent:voice:voice:15550001111"]?.sessionId).toBe("legacy-voice");
expect(store["voice:15550001111"]).toBeUndefined();
expect(log.info).toHaveBeenCalledOnce();
});
});
it("logs changes when orphaned keys are canonicalized", async () => {
const log = makeLog();
const migrate = vi.fn().mockResolvedValue({
+64 -8
View File
@@ -1,7 +1,7 @@
// Tests migration cleanup for orphaned state keys.
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import {
@@ -9,6 +9,16 @@ import {
sessionStoreTextMayNeedCanonicalization,
} from "./state-migrations.js";
const listPluginDoctorSessionStoreAgentIdsMock = vi.hoisted(() => vi.fn((): string[] => []));
vi.mock("../plugins/doctor-contract-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/doctor-contract-registry.js")>();
return {
...actual,
listPluginDoctorSessionStoreAgentIds: listPluginDoctorSessionStoreAgentIdsMock,
};
});
function writeStore(storePath: string, store: Record<string, unknown>): void {
fs.mkdirSync(path.dirname(storePath), { recursive: true });
fs.writeFileSync(storePath, JSON.stringify(store));
@@ -55,14 +65,24 @@ function sharedMainOpsConfig(sharedStorePath: string): OpenClawConfig {
} as OpenClawConfig;
}
async function migrateFixtureState(stateDir: string, cfg: OpenClawConfig = OPS_WORK_CONFIG) {
async function migrateFixtureState(
stateDir: string,
cfg: OpenClawConfig = OPS_WORK_CONFIG,
additionalAgentIds?: readonly string[],
) {
return migrateOrphanedSessionKeys({
cfg,
env: { OPENCLAW_STATE_DIR: stateDir },
additionalAgentIds,
});
}
describe("migrateOrphanedSessionKeys", () => {
beforeEach(() => {
listPluginDoctorSessionStoreAgentIdsMock.mockReset();
listPluginDoctorSessionStoreAgentIdsMock.mockReturnValue([]);
});
it("recognizes canonical stores without parsing them for migration", () => {
const raw = JSON.stringify({
"agent:main:discord:channel:123": { sessionId: "channel", updatedAt: 1 },
@@ -238,6 +258,7 @@ describe("migrateOrphanedSessionKeys", () => {
const result = await migrateOrphanedSessionKeys({
cfg,
env: { OPENCLAW_STATE_DIR: stateDir },
additionalAgentIds: ["voice"],
});
const store = readStore(voiceStorePath);
@@ -254,6 +275,41 @@ describe("migrateOrphanedSessionKeys", () => {
});
});
it("discovers plugin-owned agents through doctor contracts", async () => {
await withStateFixture(async ({ tmpDir, stateDir }) => {
listPluginDoctorSessionStoreAgentIdsMock.mockReturnValue(["voice"]);
const storeTemplate = path.join(tmpDir, "stores", "{agentId}", "sessions.json");
const voiceStorePath = path.join(tmpDir, "stores", "voice", "sessions.json");
writeStore(voiceStorePath, {
"voice:15550001111": { sessionId: "legacy-voice", updatedAt: 2000 },
});
const cfg = {
session: { store: storeTemplate },
agents: { list: [{ id: "main", default: true }] },
plugins: {
entries: {
"voice-call": { config: { agentId: "voice" } },
},
},
} as OpenClawConfig;
const result = await migrateFixtureState(stateDir, cfg);
expect(listPluginDoctorSessionStoreAgentIdsMock).toHaveBeenCalledWith({
config: cfg,
env: { OPENCLAW_STATE_DIR: stateDir },
pluginIds: ["voice-call"],
});
const store = readStore(voiceStorePath);
expect(requireStoreEntry(store, "agent:voice:voice:15550001111").sessionId).toBe(
"legacy-voice",
);
expect(store["voice:15550001111"]).toBeUndefined();
expect(result.changes).toHaveLength(1);
expect(result.warnings).toHaveLength(0);
});
});
it.each([
{ scope: undefined, canonicalMainKey: "agent:voice:main" },
{ scope: "global" as const, canonicalMainKey: "global" },
@@ -278,7 +334,7 @@ describe("migrateOrphanedSessionKeys", () => {
},
} as OpenClawConfig;
const result = await migrateFixtureState(stateDir, cfg);
const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
const store = readStore(voiceStorePath);
expect(requireStoreEntry(store, "agent:main:main").sessionId).toBe("explicit-foreign");
@@ -310,7 +366,7 @@ describe("migrateOrphanedSessionKeys", () => {
},
} as OpenClawConfig;
const result = await migrateFixtureState(stateDir, cfg);
const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
const store = readStore(sharedStorePath);
expect(requireStoreEntry(store, "agent:main:main").sessionId).toBe("ambiguous-main");
@@ -336,7 +392,7 @@ describe("migrateOrphanedSessionKeys", () => {
},
} as OpenClawConfig;
const result = await migrateFixtureState(stateDir, cfg);
const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
const store = readStore(sharedStorePath);
expect(requireStoreEntry(store, "agent:main:work").sessionId).toBe("ambiguous-main");
@@ -370,8 +426,8 @@ describe("migrateOrphanedSessionKeys", () => {
},
} as OpenClawConfig;
const result = await migrateFixtureState(stateDir, cfg);
const rerun = await migrateFixtureState(stateDir, cfg);
const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
const rerun = await migrateFixtureState(stateDir, cfg, ["voice"]);
expect(result.changes).toHaveLength(0);
expect(result.warnings).toEqual([
@@ -457,7 +513,7 @@ describe("migrateOrphanedSessionKeys", () => {
},
} as OpenClawConfig;
const result = await migrateFixtureState(stateDir, cfg);
const result = await migrateFixtureState(stateDir, cfg, ["voice"]);
expect(result.changes).toHaveLength(0);
expect(result.warnings).toEqual([
@@ -10,9 +10,11 @@ import {
clearPluginDoctorContractRegistryCache,
listPluginDoctorLegacyConfigRules,
listPluginDoctorSessionRouteStateOwners,
listPluginDoctorSessionStoreAgentIds,
} from "./doctor-contract-registry.js";
const tempDirs: string[] = [];
const repoRoot = path.resolve(import.meta.dirname, "../..");
function makeTempDir(): string {
const dir = fs.mkdtempSync(
@@ -306,4 +308,34 @@ describe("doctor contract registry load-path plugins", () => {
},
]);
});
it("loads session-store agent IDs from the real Voice Call doctor contract", () => {
const stateDir = makeTempDir();
const pluginRoot = path.join(repoRoot, "extensions", "voice-call");
const config = {
plugins: {
load: { paths: [pluginRoot] },
entries: {
"voice-call": {
enabled: true,
config: {
agentId: "Voice",
numbers: {
"+15550001111": { agentId: "Cards" },
"+15550002222": {},
},
},
},
},
},
} as OpenClawConfig;
expect(
listPluginDoctorSessionStoreAgentIds({
config,
env: makeHermeticDoctorEnv(stateDir),
pluginIds: ["voice-call"],
}),
).toEqual(["cards", "voice"]);
});
});