mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
025ae36a72
* fix(device-pair): verify retained subscribers before archiving the migration source Precheck remaining namespace capacity before importing legacy subscribers. Verify source and pre-existing destination keys in the shared JSON importer before reporting completion or archiving, preserving the source and warning when retention falls short. Keep runtime caps, eviction policy, and registration semantics unchanged. Prepared for draft PR maintainer review. Review-required: persistent-state retention semantics. * fix(device-pair): omit absent subscriber migration fields Keep absent account and thread fields out of normalized legacy subscribers so the strict plugin-state JSON serializer accepts ordinary subscriptions. Preserve numeric zero thread IDs and exercise all optional target combinations through the real migration contract.
208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
// Active Memory tests cover doctor contract api plugin behavior.
|
|
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, vi } from "vitest";
|
|
import {
|
|
legacyConfigRules,
|
|
normalizeCompatibilityConfig,
|
|
stateMigrations,
|
|
} from "./doctor-contract-api.js";
|
|
|
|
it("removes the retired QMD override while preserving Active Memory siblings", () => {
|
|
expect(legacyConfigRules).toEqual([
|
|
expect.objectContaining({
|
|
path: ["plugins", "entries", "active-memory", "config", "qmd"],
|
|
message: expect.stringContaining("doctor --fix"),
|
|
}),
|
|
]);
|
|
const cfg = {
|
|
plugins: {
|
|
entries: {
|
|
"active-memory": {
|
|
config: { enabled: true, qmd: { searchMode: "search" } },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = normalizeCompatibilityConfig({ cfg });
|
|
|
|
expect(result.config).toHaveProperty("plugins.entries.active-memory.config.enabled", true);
|
|
expect(result.config).not.toHaveProperty("plugins.entries.active-memory.config.qmd");
|
|
expect(result.changes).toEqual(["Removed retired Active Memory QMD search-mode configuration."]);
|
|
});
|
|
|
|
function createDoctorContext(env: NodeJS.ProcessEnv): PluginDoctorStateMigrationContext {
|
|
return {
|
|
openPluginStateKeyedStore<T>(options: OpenKeyedStoreOptions) {
|
|
return createPluginStateKeyedStoreForTests<T>("active-memory", {
|
|
...options,
|
|
env: options.env ?? env,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("active-memory doctor state migration", () => {
|
|
let stateDir = "";
|
|
let env: NodeJS.ProcessEnv;
|
|
|
|
beforeEach(async () => {
|
|
resetPluginStateStoreForTests();
|
|
stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-active-memory-doctor-"));
|
|
env = { ...process.env, OPENCLAW_STATE_DIR: stateDir };
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.useRealTimers();
|
|
resetPluginStateStoreForTests();
|
|
await fs.rm(stateDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("preserves oversized legacy opt-outs without writing or archiving on repeated repairs", async () => {
|
|
const sourcePath = path.join(stateDir, "plugins", "active-memory", "session-toggles.json");
|
|
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
|
const source = JSON.stringify({
|
|
sessions: Object.fromEntries(
|
|
Array.from({ length: 10_001 }, (_, index) => [
|
|
`session-${index}`,
|
|
{ disabled: true, updatedAt: index + 1 },
|
|
]),
|
|
),
|
|
});
|
|
await fs.writeFile(sourcePath, source);
|
|
const migration = expectDefined(stateMigrations[0], "active-memory state migration");
|
|
const params = {
|
|
config: {},
|
|
env,
|
|
stateDir,
|
|
oauthDir: path.join(stateDir, "oauth"),
|
|
context: createDoctorContext(env),
|
|
};
|
|
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
await expect(migration.migrateLegacyState(params)).resolves.toEqual({
|
|
changes: [],
|
|
warnings: [
|
|
"Skipped Active Memory session toggle migration because plugin state has room for 10000 of 10001 missing entries; left legacy source in place",
|
|
],
|
|
});
|
|
await expect(
|
|
params.context
|
|
.openPluginStateKeyedStore({ namespace: "session-toggles", maxEntries: 10_000 })
|
|
.entries(),
|
|
).resolves.toEqual([]);
|
|
await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe(source);
|
|
await expect(fs.access(`${sourcePath}.migrated`)).rejects.toThrow();
|
|
await expect(migration.detectLegacyState(params)).resolves.not.toBeNull();
|
|
}
|
|
});
|
|
|
|
it("imports legacy session opt-outs into plugin state", async () => {
|
|
const sourcePath = path.join(stateDir, "plugins", "active-memory", "session-toggles.json");
|
|
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
|
await fs.writeFile(
|
|
sourcePath,
|
|
JSON.stringify({
|
|
sessions: {
|
|
"telegram:dm:123": { disabled: true, updatedAt: 1700 },
|
|
"telegram:dm:456": { disabled: false, updatedAt: 1701 },
|
|
},
|
|
}),
|
|
);
|
|
|
|
const migration = expectDefined(stateMigrations[0], "active-memory state migration");
|
|
await expect(
|
|
migration.detectLegacyState({
|
|
config: {},
|
|
env,
|
|
stateDir,
|
|
oauthDir: path.join(stateDir, "oauth"),
|
|
context: createDoctorContext(env),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
preview: [expect.stringContaining("1 entry")],
|
|
});
|
|
|
|
const result = await migration.migrateLegacyState({
|
|
config: {},
|
|
env,
|
|
stateDir,
|
|
oauthDir: path.join(stateDir, "oauth"),
|
|
context: createDoctorContext(env),
|
|
});
|
|
|
|
expect(result.warnings).toEqual([]);
|
|
expect(result.changes).toEqual([
|
|
expect.stringContaining("Migrated 1 Active Memory session toggle entry"),
|
|
expect.stringContaining("Archived Active Memory session toggles legacy source"),
|
|
]);
|
|
await expect(fs.access(sourcePath)).rejects.toThrow();
|
|
await expect(fs.access(`${sourcePath}.migrated`)).resolves.toBeUndefined();
|
|
|
|
const entries = await createDoctorContext(env)
|
|
.openPluginStateKeyedStore({
|
|
namespace: "session-toggles",
|
|
maxEntries: 10_000,
|
|
})
|
|
.entries();
|
|
expect(entries).toMatchObject([
|
|
{
|
|
key: expect.any(String),
|
|
value: {
|
|
sessionKey: "telegram:dm:123",
|
|
disabled: true,
|
|
updatedAt: 1700,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("normalizes malformed legacy updatedAt values before importing toggles", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-07-10T00:00:00.000Z"));
|
|
const sourcePath = path.join(stateDir, "plugins", "active-memory", "session-toggles.json");
|
|
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
|
await fs.writeFile(
|
|
sourcePath,
|
|
'{"sessions":{"telegram:dm:bad":{"disabled":true,"updatedAt":1e999}}}',
|
|
);
|
|
|
|
const migration = expectDefined(stateMigrations[0], "active-memory state migration");
|
|
const result = await migration.migrateLegacyState({
|
|
config: {},
|
|
env,
|
|
stateDir,
|
|
oauthDir: path.join(stateDir, "oauth"),
|
|
context: createDoctorContext(env),
|
|
});
|
|
|
|
expect(result.warnings).toEqual([]);
|
|
const entries = await createDoctorContext(env)
|
|
.openPluginStateKeyedStore({
|
|
namespace: "session-toggles",
|
|
maxEntries: 10_000,
|
|
})
|
|
.entries();
|
|
expect(entries).toMatchObject([
|
|
{
|
|
value: {
|
|
sessionKey: "telegram:dm:bad",
|
|
disabled: true,
|
|
updatedAt: Date.parse("2026-07-10T00:00:00.000Z"),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
});
|