evOo-local/packages/adapters/src/memory-provider-factory.test.ts
Cursor Agent 016fd0c628 fix(adapters): gate memory provider runtime resolve to Space owners
Cloud/public memory providers are owner-managed infrastructure. The RPC
read path is owner-gated separately; this aligns executor, history
compaction, and thread-clear purge with the same boundary so non-owner
members cannot load credentials or reach external endpoints at runtime.

Deployment-owner local/private providers remain available to all Space
members when the deployment owner configured them.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 06:20:43 +00:00

181 lines
5.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
createMemoryProvider,
memoryProviderResolveRequest,
SpaceMemoryProviderResolver,
} from "./memory-provider-factory.js";
function resolverFor(
plaintext: string,
options: {
mode?: "cloud" | "local";
ownerUserId?: string | null;
baseUrl?: string;
memberRole?: string | null;
} = {},
) {
const prisma = {
spaceMemoryConfig: {
findUnique: vi.fn(async () => ({
userId: "config-author",
provider: "supermemory",
settings: {
mode: options.mode ?? "cloud",
baseUrl: options.baseUrl ?? "https://api.supermemory.ai",
},
defaultMemoryScope: "shared",
secret: { ciphertext: "encrypted" },
})),
},
deploymentSettings: {
findUnique: vi.fn(async () => ({ ownerUserId: options.ownerUserId ?? null })),
},
spaceMember: {
findUnique: vi.fn(async () =>
options.memberRole === null ? null : { role: options.memberRole ?? "owner" },
),
},
};
const secrets = { load: vi.fn(() => plaintext) };
return {
resolver: new SpaceMemoryProviderResolver(prisma as never, secrets as never),
secrets,
prisma,
};
}
afterEach(() => vi.unstubAllGlobals());
describe("SpaceMemoryProviderResolver", () => {
it.each([null, "another-user"])(
"disables saved local configurations without current deployment-owner authorization: %s",
async (ownerUserId) => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { resolver, secrets } = resolverFor("sm_fake_key", {
mode: "local",
baseUrl: "http://127.0.0.1:8123/internal-action#",
ownerUserId,
});
await expect(
resolver.resolve(memoryProviderResolveRequest("workspace-1", "config-author")),
).resolves.toBeNull();
expect(secrets.load).not.toHaveBeenCalled();
expect(fetchMock).not.toHaveBeenCalled();
},
);
it("retains deployment-owner-configured local memory for Space members", async () => {
const fetchMock = vi.fn().mockResolvedValue(Response.json({ results: [] }));
vi.stubGlobal("fetch", fetchMock);
const { resolver } = resolverFor("sm_fake_key", {
mode: "local",
baseUrl: "http://localhost:6767/memory/",
ownerUserId: "config-author",
memberRole: "member",
});
const configured = await resolver.resolve(
memoryProviderResolveRequest("workspace-1", "space-member"),
);
await configured!.provider.recall(
{ query: "project", scope: "isolated", botId: "bot-1", limit: 1 },
{
operationId: "op-1",
traceId: "trace-1",
spaceId: "workspace-1",
userId: "space-member",
signal: new AbortController().signal,
},
);
expect(fetchMock.mock.calls[0]?.[0]).toBe("http://localhost:6767/memory/v4/search");
});
it("blocks non-owners from loading cloud provider credentials at runtime", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { resolver, secrets } = resolverFor(JSON.stringify({ apiKey: "sm_json_key" }), {
memberRole: "member",
});
await expect(
resolver.resolve(memoryProviderResolveRequest("workspace-1", "space-member")),
).resolves.toBeNull();
expect(secrets.load).not.toHaveBeenCalled();
expect(fetchMock).not.toHaveBeenCalled();
});
it("rejects ambiguous persisted URLs even when the deployment owner configured them", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { resolver } = resolverFor("sm_fake_key", {
mode: "local",
baseUrl: "http://127.0.0.1:8123/internal-action#",
ownerUserId: "config-author",
});
await expect(
resolver.resolve(memoryProviderResolveRequest("workspace-1", "config-author")),
).rejects.toThrow(/base URL/);
expect(fetchMock).not.toHaveBeenCalled();
});
it("loads generic JSON credential payloads for Space owners", async () => {
const { resolver, prisma } = resolverFor(JSON.stringify({ apiKey: "sm_json_key" }));
const configured = await resolver.resolve(
memoryProviderResolveRequest("workspace-1", "config-author"),
);
expect(configured?.provider.describe().id).toBe("supermemory");
expect(configured?.defaultScope).toBe("shared");
expect(prisma.deploymentSettings.findUnique).not.toHaveBeenCalled();
});
it("keeps legacy raw Supermemory credentials usable after the schema migration", async () => {
const { resolver } = resolverFor("sm_legacy_key");
const configured = await resolver.resolve(
memoryProviderResolveRequest("workspace-1", "config-author"),
);
expect(configured?.provider.describe().id).toBe("supermemory");
});
it("revalidates persisted provider endpoints before using decrypted credentials", async () => {
expect(() =>
createMemoryProvider(
"supermemory",
{ mode: "local", baseUrl: "https://memory.example.com" },
{ apiKey: "sm_test_key" },
),
).toThrow(/loopback/);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ results: [] }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const provider = createMemoryProvider(
"supermemory",
{ mode: "cloud", baseUrl: "https://memory.example.com" },
{ apiKey: "sm_test_key" },
);
await provider.recall(
{ query: "project", scope: "isolated", botId: "bot-1", limit: 1 },
{
operationId: "op-1",
traceId: "trace-1",
spaceId: "workspace-1",
userId: "user-1",
signal: new AbortController().signal,
},
);
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.supermemory.ai/v4/search");
vi.unstubAllGlobals();
});
});