mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
@@ -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({
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user