Files
openclaw/extensions/device-pair/doctor-contract-api.test.ts
Yiğit ERDOĞAN ff83e3efe8 test(extensions): close cached agent databases before removing fixture state dirs (#126352)
Doctor migrations and auth-profile writes open per-agent and shared state
databases under the fixture's temporary directory. Clearing the plugin
state store or the runtime auth snapshots does not release those handles,
so Windows fails the directory removal with EBUSY while Linux unlinks the
open files and stays green.

Close the cached databases before each removal, matching the ordering the
zalouser and zalo fixtures already use.
2026-08-21 07:22:57 -07:00

123 lines
4.0 KiB
TypeScript

// Device Pair tests cover doctor migration of legacy notify state.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
import type {
OpenKeyedStoreOptions,
PluginDoctorStateMigrationContext,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { stateMigrations } from "./doctor-contract-api.js";
import {
DEVICE_PAIR_NOTIFY_LEGACY_STATE_FILE,
DEVICE_PAIR_NOTIFY_SUBSCRIBER_MAX_ENTRIES,
DEVICE_PAIR_NOTIFY_SUBSCRIBER_NAMESPACE,
notifySubscriberStoreKey,
type NotifySubscription,
} from "./notify-state.js";
function createDoctorContext(env: NodeJS.ProcessEnv): PluginDoctorStateMigrationContext {
return {
openPluginStateKeyedStore<T>(options: OpenKeyedStoreOptions) {
return createPluginStateKeyedStoreForTests<T>("device-pair", {
...options,
env: options.env ?? env,
});
},
};
}
describe("device-pair doctor notify migration", () => {
let stateDir = "";
let env: NodeJS.ProcessEnv;
beforeEach(async () => {
resetPluginStateStoreForTests();
stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-device-pair-doctor-"));
env = { ...process.env, OPENCLAW_STATE_DIR: stateDir };
});
afterEach(async () => {
resetPluginStateStoreForTests();
await fs.rm(stateDir, { recursive: true, force: true });
});
function migrationParams() {
return {
config: {},
env,
stateDir,
oauthDir: path.join(stateDir, "oauth"),
context: createDoctorContext(env),
};
}
it("imports legacy notify subscribers into plugin state", async () => {
const sourcePath = path.join(stateDir, DEVICE_PAIR_NOTIFY_LEGACY_STATE_FILE);
const subscriber: NotifySubscription = {
to: "chat-123",
accountId: "telegram-default",
messageThreadId: 271,
mode: "persistent",
addedAtMs: 1,
};
await fs.writeFile(
sourcePath,
JSON.stringify({
subscribers: [subscriber],
notifiedRequestIds: { stale: Date.now() },
}),
"utf8",
);
const migration = expectDefined(stateMigrations[0], "device-pair state migration");
await expect(migration.detectLegacyState(migrationParams())).resolves.toMatchObject({
preview: [expect.stringContaining("Device Pair notify subscribers")],
});
const result = await migration.migrateLegacyState(migrationParams());
expect(result.warnings).toEqual([]);
expect(result.changes).toEqual([
"Migrated Device Pair notify subscribers -> plugin state (1 imported, 0 already present)",
expect.stringContaining("Archived Device Pair notify-state legacy source"),
]);
await expect(fs.access(sourcePath)).rejects.toThrow();
await expect(fs.access(`${sourcePath}.migrated`)).resolves.toBeUndefined();
await expect(
createDoctorContext(env)
.openPluginStateKeyedStore<NotifySubscription>({
namespace: DEVICE_PAIR_NOTIFY_SUBSCRIBER_NAMESPACE,
maxEntries: DEVICE_PAIR_NOTIFY_SUBSCRIBER_MAX_ENTRIES,
})
.lookup(notifySubscriberStoreKey(subscriber)),
).resolves.toEqual(subscriber);
});
it("ignores legacy notify files that only contain cache state", async () => {
const sourcePath = path.join(stateDir, DEVICE_PAIR_NOTIFY_LEGACY_STATE_FILE);
await fs.writeFile(
sourcePath,
JSON.stringify({
subscribers: [],
notifiedRequestIds: { cached: Date.now() },
}),
"utf8",
);
const migration = expectDefined(stateMigrations[0], "device-pair state migration");
await expect(migration.detectLegacyState(migrationParams())).resolves.toBeNull();
await expect(migration.migrateLegacyState(migrationParams())).resolves.toEqual({
changes: [],
warnings: [],
});
await expect(fs.access(sourcePath)).resolves.toBeUndefined();
});
});