Files
wehub-resource-sync 555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

485 lines
16 KiB
TypeScript

/**
* Tests for config.ts — mem0ConfigSchema.parse() and exported constants.
*/
import { describe, it, expect } from "vitest";
import {
mem0ConfigSchema,
DEFAULT_CUSTOM_INSTRUCTIONS,
DEFAULT_CUSTOM_CATEGORIES,
} from "../config.ts";
// ---------------------------------------------------------------------------
// Exported constants
// ---------------------------------------------------------------------------
describe("DEFAULT_CUSTOM_INSTRUCTIONS", () => {
it("is a non-empty string", () => {
expect(typeof DEFAULT_CUSTOM_INSTRUCTIONS).toBe("string");
expect(DEFAULT_CUSTOM_INSTRUCTIONS.length).toBeGreaterThan(0);
});
});
describe("DEFAULT_CUSTOM_CATEGORIES", () => {
it("is a non-empty object with string values", () => {
expect(typeof DEFAULT_CUSTOM_CATEGORIES).toBe("object");
const keys = Object.keys(DEFAULT_CUSTOM_CATEGORIES);
expect(keys.length).toBeGreaterThan(0);
for (const key of keys) {
expect(typeof DEFAULT_CUSTOM_CATEGORIES[key]).toBe("string");
}
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — defaults
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — defaults", () => {
it("mode defaults to 'platform' when omitted", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.mode).toBe("platform");
});
it("userId falls back to a non-empty string when not provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(typeof cfg.userId).toBe("string");
expect(cfg.userId.length).toBeGreaterThan(0);
});
it("autoCapture defaults to true", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.autoCapture).toBe(true);
});
it("autoRecall defaults to true", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.autoRecall).toBe(true);
});
it("topK defaults to 5", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.topK).toBe(5);
});
it("searchThreshold defaults to 0.1", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.searchThreshold).toBe(0.1);
});
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS);
});
it("customCategories defaults to DEFAULT_CUSTOM_CATEGORIES", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.customCategories).toBe(DEFAULT_CUSTOM_CATEGORIES);
});
// v3.0.0: customPrompt removed, use customInstructions instead
it("customPrompt input falls back to customInstructions", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key", customPrompt: "My prompt" });
expect(cfg.customInstructions).toBe("My prompt");
});
it("oss defaults to undefined", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.oss).toBeUndefined();
});
it("skills defaults to undefined", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.skills).toBeUndefined();
});
it("allows anonymousTelemetryId", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key", anonymousTelemetryId: "123" });
expect(cfg.anonymousTelemetryId).toBe("123");
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — mode parsing
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — mode parsing", () => {
it('"oss" is not a valid mode and defaults to "platform"', () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", apiKey: "k" });
expect(cfg.mode).toBe("platform");
});
it('"open-source" stays as "open-source"', () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source" });
expect(cfg.mode).toBe("open-source");
});
it("any other string defaults to 'platform'", () => {
const cfg = mem0ConfigSchema.parse({ mode: "something-else", apiKey: "k" });
expect(cfg.mode).toBe("platform");
});
it("undefined mode defaults to 'platform'", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k" });
expect(cfg.mode).toBe("platform");
});
it("numeric mode defaults to 'platform'", () => {
const cfg = mem0ConfigSchema.parse({ mode: 42, apiKey: "k" });
expect(cfg.mode).toBe("platform");
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — userId precedence
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — userId", () => {
it("userId from config takes precedence over os.userInfo() fallback", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "test-key",
userId: "custom-user",
});
expect(cfg.userId).toBe("custom-user");
});
it("empty string userId falls back to os.userInfo()", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key", userId: "" });
// Empty string is falsy, so the fallback should kick in
expect(typeof cfg.userId).toBe("string");
expect(cfg.userId.length).toBeGreaterThan(0);
});
it("non-string userId falls back to os.userInfo()", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key", userId: 123 });
expect(typeof cfg.userId).toBe("string");
expect(cfg.userId.length).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — needsSetup
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — needsSetup", () => {
// Note: needsSetup = (mode === "platform" && !resolvedApiKey).
// resolvedApiKey can come from the config OR from ~/.mem0/config.json fallback.
// When no apiKey is provided and no config file exists, needsSetup is true.
// When ~/.mem0/config.json has a key, the fallback populates resolvedApiKey.
it("needsSetup is consistent: false when apiKey resolves, true otherwise (no apiKey in config)", () => {
const cfg = mem0ConfigSchema.parse({ mode: "platform" });
// needsSetup should be true only if NO apiKey was resolved (including from ~/.mem0/config.json)
if (cfg.apiKey) {
expect(cfg.needsSetup).toBe(false);
} else {
expect(cfg.needsSetup).toBe(true);
}
});
it("needsSetup is consistent with empty config", () => {
const cfg = mem0ConfigSchema.parse({});
if (cfg.apiKey) {
expect(cfg.needsSetup).toBe(false);
} else {
expect(cfg.needsSetup).toBe(true);
}
});
it("is false when apiKey is explicitly provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "my-api-key" });
expect(cfg.needsSetup).toBe(false);
});
it("is false when mode is 'open-source' via explicit string (no apiKey needed)", () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source" });
expect(cfg.needsSetup).toBe(false);
});
it("is false when mode is 'open-source' (no apiKey needed)", () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source" });
expect(cfg.needsSetup).toBe(false);
});
it("needsSetup is always false when apiKey is a valid string", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key-123" });
expect(cfg.apiKey).toBe("test-key-123");
expect(cfg.needsSetup).toBe(false);
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — error cases
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — error cases", () => {
it("throws on unknown keys", () => {
expect(() =>
mem0ConfigSchema.parse({ apiKey: "k", unknownKey: "value" }),
).toThrow(/unknown keys.*unknownKey/);
});
it("throws when multiple unknown keys are present", () => {
expect(() =>
mem0ConfigSchema.parse({ apiKey: "k", foo: 1, bar: 2 }),
).toThrow(/unknown keys/);
});
it("throws on null input", () => {
expect(() => mem0ConfigSchema.parse(null)).toThrow(
"openclaw-mem0 config required",
);
});
it("throws on undefined input", () => {
expect(() => mem0ConfigSchema.parse(undefined)).toThrow(
"openclaw-mem0 config required",
);
});
it("throws on string input", () => {
expect(() => mem0ConfigSchema.parse("not an object")).toThrow(
"openclaw-mem0 config required",
);
});
it("throws on number input", () => {
expect(() => mem0ConfigSchema.parse(42)).toThrow(
"openclaw-mem0 config required",
);
});
it("throws on array input", () => {
expect(() => mem0ConfigSchema.parse([1, 2, 3])).toThrow(
"openclaw-mem0 config required",
);
});
it("throws on boolean input", () => {
expect(() => mem0ConfigSchema.parse(true)).toThrow(
"openclaw-mem0 config required",
);
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — explicit overrides
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — explicit overrides", () => {
it("autoCapture can be set to false", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
autoCapture: false,
});
expect(cfg.autoCapture).toBe(false);
});
it("autoRecall can be set to false", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
autoRecall: false,
});
expect(cfg.autoRecall).toBe(false);
});
it("custom topK is used when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 });
expect(cfg.topK).toBe(20);
});
it("custom searchThreshold is used when provided", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
searchThreshold: 0.8,
});
expect(cfg.searchThreshold).toBe(0.8);
});
it("custom customInstructions override defaults", () => {
const custom = "My custom instructions";
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
customInstructions: custom,
});
expect(cfg.customInstructions).toBe(custom);
});
// v3.0.0: customPrompt renamed to customInstructions (backwards compat: customPrompt maps to customInstructions)
it("customPrompt input maps to customInstructions output", () => {
const custom = "My custom prompt";
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
customPrompt: custom,
});
expect(cfg.customInstructions).toBe(custom);
});
it("custom customCategories override defaults", () => {
const cats = { myCategory: "description" };
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
customCategories: cats,
});
expect(cfg.customCategories).toEqual(cats);
});
it("baseUrl is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
baseUrl: "https://custom.api.com",
});
expect(cfg.baseUrl).toBe("https://custom.api.com");
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — oss config
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — oss config", () => {
it("parses oss object when provided", () => {
const ossConfig = {
embedder: {
provider: "openai",
config: { model: "text-embedding-3-small" },
},
vectorStore: { provider: "qdrant", config: { host: "localhost" } },
llm: { provider: "openai", config: { model: "gpt-4" } },
historyDbPath: "/tmp/history.db",
disableHistory: false,
};
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: ossConfig });
expect(cfg.mode).toBe("open-source");
expect(cfg.oss).toEqual(ossConfig);
});
it("ignores oss when it is not a plain object", () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: "not-an-object" });
expect(cfg.oss).toBeUndefined();
});
it("ignores oss when it is an array", () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: [1, 2, 3] });
expect(cfg.oss).toBeUndefined();
});
it("ignores oss when it is null", () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: null });
expect(cfg.oss).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — skills config
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — skills config", () => {
it("parses skills object when provided", () => {
const skillsConfig = {
triage: {
enabled: true,
importanceThreshold: 3,
credentialPatterns: ["sk-", "ghp_"],
},
recall: {
enabled: true,
strategy: "smart" as const,
tokenBudget: 2000,
maxMemories: 10,
},
dream: {
enabled: true,
auto: true,
minHours: 12,
minSessions: 3,
minMemories: 15,
},
domain: "engineering",
customRules: {
include: ["tool configs"],
exclude: ["passwords"],
},
categories: {
identity: {
importance: 5,
ttl: null,
immutable: true,
},
},
};
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
skills: skillsConfig,
});
expect(cfg.skills).toEqual(skillsConfig);
});
it("skills is undefined when not provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k" });
expect(cfg.skills).toBeUndefined();
});
it("skills is undefined when set to a non-object value", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", skills: "invalid" });
expect(cfg.skills).toBeUndefined();
});
it("skills is undefined when set to an array", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", skills: [1, 2] });
expect(cfg.skills).toBeUndefined();
});
it("skills is undefined when set to null", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", skills: null });
expect(cfg.skills).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — customCategories edge cases
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — customCategories edge cases", () => {
it("non-object customCategories falls back to defaults", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
customCategories: "not-an-object",
});
expect(cfg.customCategories).toBe(DEFAULT_CUSTOM_CATEGORIES);
});
it("array customCategories falls back to defaults", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
customCategories: ["a", "b"],
});
expect(cfg.customCategories).toBe(DEFAULT_CUSTOM_CATEGORIES);
});
it("null customCategories falls back to defaults", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
customCategories: null,
});
expect(cfg.customCategories).toBe(DEFAULT_CUSTOM_CATEGORIES);
});
});
// ---------------------------------------------------------------------------
// mem0ConfigSchema.parse() — non-string apiKey
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — apiKey edge cases", () => {
// Note: When a non-string apiKey is provided, the parser treats it as
// undefined. However, readMem0ConfigFile() may still provide a fallback
// apiKey from ~/.mem0/config.json if one exists on the system.
it("non-string apiKey is not used directly from config", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: 12345 });
// The numeric value is not used directly — apiKey comes from fallback or is undefined
// Either way, the non-string value is never the resolved apiKey
expect(cfg.apiKey).not.toBe(12345);
});
it("boolean apiKey is not used directly from config", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: true });
expect(cfg.apiKey).not.toBe(true);
});
it("string apiKey takes precedence over any fallback", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "explicit-key" });
expect(cfg.apiKey).toBe("explicit-key");
expect(cfg.needsSetup).toBe(false);
});
});