Files
Peter Steinberger 75dbe52e3e refactor: one code path behind doctor legacy-state migrations (#120716)
* refactor(doctor): prefer manifest route-state owners

* refactor(doctor): unify config repair declarations

* refactor(doctor): unify legacy state migrations

* fix(doctor): satisfy migration pipeline guards

* fix(plugin-sdk): keep doctor adapter inside boundary
2026-08-08 18:23:15 -07:00

133 lines
5.1 KiB
TypeScript

// Whatsapp tests cover setup entry plugin behavior.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { buildLegacyMigrationPreview } from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { describe, expect, it, vi } from "vitest";
import { stateMigrations } from "./doctor-contract-api.js";
import * as legacySessionSurfaceApi from "./legacy-session-surface-api.js";
import setupEntry from "./setup-entry.js";
import * as setupPluginApi from "./setup-plugin-api.js";
import { detectWhatsAppLegacyStateMigrations } from "./src/state-migrations.js";
vi.mock("baileys", () => {
throw new Error("setup plugin load must not load Baileys");
});
vi.mock("./src/setup-finalize.js", () => {
throw new Error("setup status load must not load finalize");
});
const setupEntryLoadOptions = {
createLoaderForTest: (() => (specifier: string) => {
if (/[\\/]setup-plugin-api\.[jt]s$/u.test(specifier)) {
return setupPluginApi;
}
if (/[\\/]legacy-session-surface-api\.[jt]s$/u.test(specifier)) {
return legacySessionSurfaceApi;
}
throw new Error(`unexpected setup entry module load: ${specifier}`);
}) as never,
};
describe("whatsapp setup entry", () => {
it("loads setup entry metadata without importing runtime dependencies", () => {
expect(setupEntry.kind).toBe("bundled-channel-setup-entry");
expect(setupEntry.features).toEqual({ legacySessionSurfaces: true });
});
it("loads the setup plugin without installing runtime dependencies", () => {
const whatsappSetupPlugin = setupEntry.loadSetupPlugin(setupEntryLoadOptions);
expect(whatsappSetupPlugin.id).toBe("whatsapp");
});
it("loads the legacy session helper without importing runtime dependencies", () => {
const legacySessionSurface = setupEntry.loadLegacySessionSurface?.(setupEntryLoadOptions);
if (!legacySessionSurface) {
throw new Error("expected WhatsApp legacy session surface");
}
expect(Object.keys(legacySessionSurface).toSorted()).toEqual([
"canonicalizeLegacySessionKey",
"isLegacyGroupSessionKey",
]);
expect(legacySessionSurface.canonicalizeLegacySessionKey).toBeTypeOf("function");
expect(legacySessionSurface.isLegacyGroupSessionKey).toBeTypeOf("function");
});
it("plans migration for every Baileys auth category while preserving other shared-root files", async () => {
const oauthDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-wa-legacy-migration-"));
const authFiles = [
"creds.json",
"creds.json.bak",
"pre-key-1.json",
"session-contact.json",
"sender-key-group.json",
"sender-key-memory-group.json",
"app-state-sync-key-contact.json",
"app-state-sync-version-contact.json",
"lid-mapping-15551234567.json",
"device-list-15551234567.json",
"tctoken-15551234567.json",
"identity-key-15551234567.json",
];
try {
for (const file of [...authFiles, "oauth.json", "google-oauth.json", "notes.txt"]) {
fs.writeFileSync(path.join(oauthDir, file), "{}", "utf-8");
}
fs.mkdirSync(path.join(oauthDir, "nested"));
fs.writeFileSync(path.join(oauthDir, "nested", "session-keep.json"), "{}", "utf-8");
fs.symlinkSync(path.join(oauthDir, "notes.txt"), path.join(oauthDir, "session-linked.json"));
const migrations = detectWhatsAppLegacyStateMigrations({ oauthDir });
expect(migrations.map((migration) => path.basename(migration.sourcePath)).toSorted()).toEqual(
authFiles.toSorted(),
);
expect(
migrations
.map((migration) => ({
kind: migration.kind,
label: migration.label,
sourcePath: migration.sourcePath,
targetPath: migration.targetPath,
namespace: null,
}))
.toSorted((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
).toEqual(
authFiles
.map((fileName) => ({
kind: "move",
label: `WhatsApp auth ${fileName}`,
sourcePath: path.join(oauthDir, fileName),
targetPath: path.join(oauthDir, "whatsapp", "default", fileName),
namespace: null,
}))
.toSorted((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
);
await expect(
stateMigrations[0]?.detectLegacyState({
config: {},
env: {},
oauthDir,
stateDir: oauthDir,
context: { openPluginStateKeyedStore: vi.fn() } as never,
}),
).resolves.toEqual({ preview: migrations.map(buildLegacyMigrationPreview) });
for (const migration of migrations) {
expect(migration.targetPath).toBe(
path.join(oauthDir, "whatsapp", "default", path.basename(migration.sourcePath)),
);
}
} finally {
fs.rmSync(oauthDir, { recursive: true, force: true });
}
});
it("loads the delegated setup wizard without importing runtime dependencies", async () => {
const { whatsappSetupWizard } = await import("./src/setup-surface.js");
expect(whatsappSetupWizard.channel).toBe("whatsapp");
});
});