refactor(integrations): rename dsh-mem0 to deepseek-plugin, strands-mem0 to mem0-strands (#7098)

This commit is contained in:
Kartik
2026-08-24 19:21:23 +05:30
committed by GitHub
parent dc82354e14
commit b717e38785
43 changed files with 127 additions and 127 deletions
@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Offline mock of the Mem0 SDK so these tests never touch the network.
const mockSearch = vi.fn();
const mockAdd = vi.fn();
vi.mock("mem0ai", () => ({
MemoryClient: class {
search = mockSearch;
add = mockAdd;
},
}));
// The real `@deepseek-ai/dsh-tools` runtime transitively imports harness peer
// packages the host provides at runtime but which aren't installed here. For
// these unit tests we only need `defineTool` to hand back the definition it was
// given, so the registered tool's `execute`/`name` can be exercised directly.
vi.mock("@deepseek-ai/dsh-tools", () => ({
defineTool: (options: unknown) => options,
}));
import { apply, type Config } from "../src/index.ts";
interface RegisteredTool {
name: string;
execute(args: unknown, exec: unknown): Promise<unknown>;
}
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
const tools = new Map<string, RegisteredTool>();
const ctx = {
tools: { register: (t: RegisteredTool) => tools.set(t.name, t) },
};
apply(ctx as never, config);
return tools;
}
let savedKey: string | undefined;
beforeEach(() => {
savedKey = process.env.MEM0_API_KEY;
mockSearch.mockReset();
mockAdd.mockReset();
});
afterEach(() => {
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
else process.env.MEM0_API_KEY = savedKey;
});
describe("apply() config validation", () => {
it("throws when no apiKey is set and MEM0_API_KEY is absent", () => {
delete process.env.MEM0_API_KEY;
expect(() => applyAndCollect({ userId: "u" } as Config)).toThrow(/apiKey|MEM0_API_KEY/);
});
it("throws when userId is missing", () => {
expect(() => applyAndCollect({ apiKey: "k", userId: "" } as Config)).toThrow(/userId/);
});
it("registers both memory tools", () => {
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
expect([...tools.keys()].sort()).toEqual(["add_memory", "search_memory"]);
});
});
describe("search_memory tool", () => {
it("returns a formatted list scoped to the configured user", async () => {
mockSearch.mockResolvedValue({
results: [{ id: "m1", memory: "Likes tea", categories: ["preference"] }],
});
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("search_memory")!.execute({ query: "drink" }, {});
expect(out).toContain("Likes tea");
expect(out).toContain("[mem0:m1]");
expect(mockSearch).toHaveBeenCalledWith("drink", {
filters: { user_id: "u" },
topK: 10,
});
});
it("honors a per-call userId override and limit", async () => {
mockSearch.mockResolvedValue({ results: [] });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
await tools.get("search_memory")!.execute({ query: "x", userId: "alice", limit: 3 }, {});
expect(mockSearch).toHaveBeenCalledWith("x", {
filters: { user_id: "alice" },
topK: 3,
});
});
it("returns a graceful failure line instead of rejecting on error", async () => {
mockSearch.mockRejectedValue(new Error("network down"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("search_memory")!.execute({ query: "x" }, {});
expect(out).toContain("search_memory failed");
expect(out).toContain("network down");
});
});
describe("add_memory tool", () => {
it("reports the write as queued on the async PENDING response, with camelCase scope + source", async () => {
// The real /v3/memories/add/ response — not an array of memories. The SDK
// camel-cases response keys, so it surfaces as `eventId`, not `event_id`.
mockAdd.mockResolvedValue({ eventId: "evt-123", status: "PENDING" });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "remember this" }, {});
expect(out).toContain("queued");
expect(out).toContain("evt-123");
expect(out).not.toContain("No new distinct memory");
expect(mockAdd).toHaveBeenCalledWith(
[{ role: "user", content: "remember this" }],
{ userId: "u", source: "DEEPSEEK_HARNESS" },
);
});
it("renders a list when the backend returns memories", async () => {
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("Stored 1 memory");
expect(out).toContain("[mem0:m1]");
});
it("returns a graceful failure line on error", async () => {
mockAdd.mockRejectedValue(new Error("boom"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("add_memory failed");
expect(out).toContain("boom");
});
});
@@ -0,0 +1,71 @@
import { describe, it, expect } from "vitest";
import {
formatAge,
formatMemoryCompact,
formatMemoryList,
formatAddResult,
} from "../src/formatting.ts";
describe("formatAge", () => {
it("formats minutes, hours, and days", () => {
expect(formatAge(new Date(Date.now() - 30 * 60_000))).toBe("30m ago");
expect(formatAge(new Date(Date.now() - 3 * 3_600_000))).toBe("3h ago");
expect(formatAge(new Date(Date.now() - 5 * 86_400_000))).toBe("5d ago");
});
});
describe("formatMemoryCompact", () => {
it("renders one line with category, text, and id", () => {
const line = formatMemoryCompact({
id: "abc-123",
memory: "User prefers dark mode",
categories: ["preference"],
createdAt: new Date(),
});
expect(line).toContain("[preference]");
expect(line).toContain("User prefers dark mode");
expect(line).toContain("[mem0:abc-123]");
});
it("falls back to uncategorized and (empty)", () => {
expect(formatMemoryCompact({ id: "x" })).toContain("[uncategorized]");
expect(formatMemoryCompact({ id: "x" })).toContain("(empty)");
});
});
describe("formatMemoryList", () => {
it("numbers multiple memories", () => {
const output = formatMemoryList([
{ id: "id-1", memory: "Fact one", categories: ["insight"] },
{ id: "id-2", memory: "Fact two", categories: ["convention"] },
]);
expect(output).toContain("1.");
expect(output).toContain("2.");
});
it("returns a plain message when there are no memories", () => {
expect(formatMemoryList([])).toBe("No memories found.");
});
});
describe("formatAddResult", () => {
it("reports queued for the async PENDING response, with the event id", () => {
// SDK camel-cases response keys, so the real shape is `eventId`.
const out = formatAddResult({ eventId: "evt-9", status: "PENDING" });
expect(out).toContain("queued");
expect(out).toContain("evt-9");
});
it("reports the stored count when the backend returns memories", () => {
expect(formatAddResult([{ id: "1", memory: "A" }])).toContain("Stored 1 memory");
expect(formatAddResult([{ id: "1" }, { id: "2" }])).toContain("Stored 2 memories");
});
it("unwraps a { results: [...] } envelope", () => {
expect(formatAddResult({ results: [{ id: "1", memory: "A" }] })).toContain("Stored 1 memory");
});
it("handles an empty result", () => {
expect(formatAddResult([])).toBe("Memory stored.");
});
});
@@ -0,0 +1,30 @@
import { describe, it, expect } from "vitest";
import { truncateOutput, MAX_OUTPUT_LINES } from "../src/output.ts";
describe("truncateOutput", () => {
it("passes small output through untouched", () => {
expect(truncateOutput("a\nb\nc")).toBe("a\nb\nc");
});
it("caps output at MAX_OUTPUT_LINES and appends a notice", () => {
const many = Array.from({ length: MAX_OUTPUT_LINES + 50 }, (_, i) => `line ${i}`).join("\n");
const out = truncateOutput(many);
expect(out.split("\n").length).toBeLessThanOrEqual(MAX_OUTPUT_LINES + 3);
expect(out).toContain("[Output truncated:");
expect(out).toContain(`of ${MAX_OUTPUT_LINES + 50} lines`);
});
it("caps output that is few lines but very large by bytes", () => {
const huge = "x".repeat(60_000);
const out = truncateOutput(huge);
expect(out.length).toBeLessThan(huge.length);
expect(out).toContain("[Output truncated:");
});
it("reports both reasons when the line cap and the byte cap fire together", () => {
const wide = Array.from({ length: MAX_OUTPUT_LINES + 50 }, () => "x".repeat(300)).join("\n");
const out = truncateOutput(wide);
expect(out).toContain(`of ${MAX_OUTPUT_LINES + 50} lines`);
expect(out).toContain("cut at 50KB");
});
});
@@ -0,0 +1,44 @@
import { describe, it, expect } from "vitest";
import { resolveSearchFilters, resolveAddParams } from "../src/scoping.ts";
describe("resolveSearchFilters (snake_case, for filters)", () => {
it("falls back to the configured default userId", () => {
expect(resolveSearchFilters({}, "default-user")).toEqual({ user_id: "default-user" });
});
it("lets a per-call userId override the default", () => {
expect(resolveSearchFilters({ userId: "alice" }, "default-user")).toEqual({
user_id: "alice",
});
});
it("treats a blank/whitespace userId as absent and falls back", () => {
expect(resolveSearchFilters({ userId: " " }, "default-user")).toEqual({
user_id: "default-user",
});
});
it("includes snake_case agent/run scope only when provided", () => {
expect(
resolveSearchFilters({ userId: "alice", agentId: "agent-1", runId: "run-9" }, "d"),
).toEqual({ user_id: "alice", agent_id: "agent-1", run_id: "run-9" });
});
it("omits blank agent/run scope", () => {
const f = resolveSearchFilters({ agentId: " ", runId: "run-9" }, "default");
expect(f).toEqual({ user_id: "default", run_id: "run-9" });
expect(f).not.toHaveProperty("agent_id");
});
});
describe("resolveAddParams (camelCase, for top-level add params)", () => {
it("uses camelCase keys and falls back to the default userId", () => {
expect(resolveAddParams({}, "default-user")).toEqual({ userId: "default-user" });
});
it("includes camelCase agent/run scope only when provided", () => {
expect(
resolveAddParams({ userId: "alice", agentId: "agent-1", runId: "run-9" }, "d"),
).toEqual({ userId: "alice", agentId: "agent-1", runId: "run-9" });
});
});