test: add integration tests for CRUD, feedback, export and update CI workflow
This commit is contained in:
@@ -57,11 +57,15 @@ jobs:
|
|||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
run: pnpm run build
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run unit tests
|
||||||
|
working-directory: mem0-ts
|
||||||
|
run: pnpm run test:ci -- --testPathIgnorePatterns=integration
|
||||||
|
|
||||||
|
- name: Run integration tests (with cleanup)
|
||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
env:
|
env:
|
||||||
MEM0_API_KEY: ${{ secrets.MEM0_API_KEY }}
|
MEM0_API_KEY: ${{ secrets.MEM0_API_KEY }}
|
||||||
run: pnpm run test:ci
|
run: pnpm run test:integration
|
||||||
|
|
||||||
- name: Verify package exports
|
- name: Verify package exports
|
||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
...require("./jest.config"),
|
||||||
|
testMatch: ["**/integration/**/*.test.ts"],
|
||||||
|
globalSetup:
|
||||||
|
"<rootDir>/src/client/tests/integration/global-setup.ts",
|
||||||
|
globalTeardown:
|
||||||
|
"<rootDir>/src/client/tests/integration/global-teardown.ts",
|
||||||
|
};
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"example": "ts-node src/oss/examples/vector-stores/index.ts",
|
"example": "ts-node src/oss/examples/vector-stores/index.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:ci": "jest --coverage --ci",
|
"test:ci": "jest --coverage --ci",
|
||||||
|
"test:integration": "jest --config jest.integration.config.js --forceExit",
|
||||||
"test:ts": "jest --config jest.config.js",
|
"test:ts": "jest --config jest.config.js",
|
||||||
"test:watch": "jest --config jest.config.js --watch",
|
"test:watch": "jest --config jest.config.js --watch",
|
||||||
"format": "npm run clean && prettier --write .",
|
"format": "npm run clean && prettier --write .",
|
||||||
|
|||||||
@@ -132,6 +132,17 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
|||||||
expect(typeof mem.memory).toBe("string");
|
expect(typeof mem.memory).toBe("string");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("returns paginated results with page and page_size", async () => {
|
||||||
|
const page1 = await client.getAll({
|
||||||
|
user_id: TEST_USER_ID,
|
||||||
|
page: 1,
|
||||||
|
page_size: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paginated response is an object with results array
|
||||||
|
expect(page1).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Update ───────────────────────────────────────────────
|
// ─── Update ───────────────────────────────────────────────
|
||||||
@@ -151,6 +162,19 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
|||||||
expect(typeof updated.memory).toBe("string");
|
expect(typeof updated.memory).toBe("string");
|
||||||
expect(updated.memory).not.toBe(originalText);
|
expect(updated.memory).not.toBe(originalText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("updates memory metadata", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
|
||||||
|
await client.update(memoryId, {
|
||||||
|
metadata: { source: "integration-test", priority: "high" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await client.get(memoryId);
|
||||||
|
expect(updated.metadata).toBeDefined();
|
||||||
|
expect(updated.metadata.source).toBe("integration-test");
|
||||||
|
expect(updated.metadata.priority).toBe("high");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Delete single ────────────────────────────────────────
|
// ─── Delete single ────────────────────────────────────────
|
||||||
@@ -170,6 +194,43 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Edge cases ──────────────────────────────────────────
|
||||||
|
describe("edge cases", () => {
|
||||||
|
test("add with metadata attaches metadata to the memory", async () => {
|
||||||
|
const result = await client.add(
|
||||||
|
[
|
||||||
|
{ role: "user" as const, content: "I prefer dark mode in all apps." },
|
||||||
|
{ role: "assistant" as const, content: "Noted, dark mode preference saved!" },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
user_id: TEST_USER_ID,
|
||||||
|
metadata: { source: "integration-test", category: "preferences" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getAll for non-existent user returns empty array", async () => {
|
||||||
|
const memories = await client.getAll({
|
||||||
|
user_id: `nonexistent-user-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Array.isArray(memories)).toBe(true);
|
||||||
|
expect(memories.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deleteAll for non-existent user does not throw", async () => {
|
||||||
|
const result = await client.deleteAll({
|
||||||
|
user_id: `nonexistent-user-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(typeof result.message).toBe("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Delete all + delete user ─────────────────────────────
|
// ─── Delete all + delete user ─────────────────────────────
|
||||||
describe("cleanup operations", () => {
|
describe("cleanup operations", () => {
|
||||||
test("deletes all memories for test user", async () => {
|
test("deletes all memories for test user", async () => {
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
/**
|
|
||||||
* Integration tests: Feedback and memory export.
|
|
||||||
*
|
|
||||||
* Tests feedback submission and the export lifecycle
|
|
||||||
* (create → retrieve) against the real API.
|
|
||||||
*
|
|
||||||
* Run: MEM0_API_KEY=your-key npx jest feedback-and-export.test.ts --forceExit
|
|
||||||
*/
|
|
||||||
import { MemoryClient } from "../../mem0";
|
|
||||||
import { Feedback } from "../../mem0.types";
|
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
import {
|
|
||||||
describeIntegration,
|
|
||||||
createTestClient,
|
|
||||||
suppressTelemetryNoise,
|
|
||||||
seedTestMemories,
|
|
||||||
cleanupTestUser,
|
|
||||||
} from "./helpers";
|
|
||||||
|
|
||||||
jest.setTimeout(120_000);
|
|
||||||
|
|
||||||
const TEST_USER_ID = `integration-fb-export-${randomUUID()}`;
|
|
||||||
|
|
||||||
describeIntegration("MemoryClient Integration — Feedback & Export", () => {
|
|
||||||
let client: MemoryClient;
|
|
||||||
let cleanup: () => void;
|
|
||||||
let memoryIds: string[] = [];
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
cleanup = suppressTelemetryNoise();
|
|
||||||
client = createTestClient();
|
|
||||||
memoryIds = await seedTestMemories(client, TEST_USER_ID);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanupTestUser(client, TEST_USER_ID);
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Feedback ─────────────────────────────────────────────
|
|
||||||
// Note: client.feedback() is deprecated and doesn't send org_id/project_id,
|
|
||||||
// so we call _fetchWithErrorHandling directly to test the API endpoint.
|
|
||||||
describe("feedback", () => {
|
|
||||||
test("submits positive feedback on a memory", async () => {
|
|
||||||
const memoryId = memoryIds[0];
|
|
||||||
expect(memoryId).toBeDefined();
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
memory_id: memoryId,
|
|
||||||
feedback: Feedback.POSITIVE,
|
|
||||||
org_id: String(client.organizationId),
|
|
||||||
project_id: String(client.projectId),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await (client as any)._fetchWithErrorHandling(
|
|
||||||
`${client.host}/v1/feedback/`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: client.headers,
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Export ───────────────────────────────────────────────
|
|
||||||
describe("memory export", () => {
|
|
||||||
let exportId: string | undefined;
|
|
||||||
|
|
||||||
test("creates a memory export", async () => {
|
|
||||||
const result = await client.createMemoryExport({
|
|
||||||
filters: { AND: [{ user_id: TEST_USER_ID }] },
|
|
||||||
schema: { memory: "string", user_id: "string" },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(
|
|
||||||
typeof result.id === "string" || typeof result.message === "string",
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
exportId = result.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("retrieves a memory export", async () => {
|
|
||||||
if (!exportId) return;
|
|
||||||
|
|
||||||
const result = await client.getMemoryExport({
|
|
||||||
memory_export_id: exportId,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Jest global setup for integration tests.
|
||||||
|
*
|
||||||
|
* Runs a full project cleanup before any integration test starts,
|
||||||
|
* then waits 10 seconds for the async cleanup to propagate.
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
|
||||||
|
export default async function globalSetup() {
|
||||||
|
const apiKey = process.env.MEM0_API_KEY;
|
||||||
|
if (!apiKey) return; // skip if no key — tests will be skipped too
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey });
|
||||||
|
await client.ping();
|
||||||
|
|
||||||
|
console.log("[integration] Running pre-test cleanup...");
|
||||||
|
|
||||||
|
// Full project wipe — all four filters set explicitly
|
||||||
|
try {
|
||||||
|
await client.deleteAll({
|
||||||
|
user_id: "*",
|
||||||
|
agent_id: "*",
|
||||||
|
app_id: "*",
|
||||||
|
run_id: "*",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore — may 404 if no data exists
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteUsers();
|
||||||
|
} catch {
|
||||||
|
// ignore — may throw "No entities to delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 10 seconds for async cleanup to propagate
|
||||||
|
console.log("[integration] Waiting 10s for cleanup to propagate...");
|
||||||
|
await new Promise((r) => setTimeout(r, 10_000));
|
||||||
|
console.log("[integration] Pre-test cleanup done.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Jest global teardown for integration tests.
|
||||||
|
*
|
||||||
|
* Runs a full project cleanup after all integration tests complete
|
||||||
|
* so no test data is left behind.
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
|
||||||
|
export default async function globalTeardown() {
|
||||||
|
const apiKey = process.env.MEM0_API_KEY;
|
||||||
|
if (!apiKey) return;
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey });
|
||||||
|
await client.ping();
|
||||||
|
|
||||||
|
console.log("[integration] Running post-test cleanup...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteAll({
|
||||||
|
user_id: "*",
|
||||||
|
agent_id: "*",
|
||||||
|
app_id: "*",
|
||||||
|
run_id: "*",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteUsers();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[integration] Post-test cleanup done.");
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
* Provides environment gating, client factory, polling helpers,
|
* Provides environment gating, client factory, polling helpers,
|
||||||
* and console suppression for telemetry noise.
|
* and console suppression for telemetry noise.
|
||||||
*
|
*
|
||||||
* API credit budget: these helpers are designed to minimize API calls.
|
* All helpers use only the SDK's public API — no internal method access.
|
||||||
* Each CI run should use ~40 calls total across all test files.
|
|
||||||
*/
|
*/
|
||||||
import { MemoryClient } from "../../mem0";
|
import { MemoryClient } from "../../mem0";
|
||||||
import type { Memory } from "../../mem0.types";
|
import type { Memory } from "../../mem0.types";
|
||||||
@@ -19,35 +18,33 @@ export const describeIntegration = API_KEY ? describe : describe.skip;
|
|||||||
* Create a MemoryClient with the real API key.
|
* Create a MemoryClient with the real API key.
|
||||||
* Call this inside beforeAll — not at module scope — so it only
|
* Call this inside beforeAll — not at module scope — so it only
|
||||||
* runs when the suite is not skipped.
|
* runs when the suite is not skipped.
|
||||||
*
|
|
||||||
* The returned client retries transient errors (502, 503, 504, 429)
|
|
||||||
* with backoff so CI runs are not flaky.
|
|
||||||
*/
|
*/
|
||||||
export function createTestClient(): MemoryClient {
|
export function createTestClient(): MemoryClient {
|
||||||
const client = new MemoryClient({ apiKey: API_KEY! });
|
return new MemoryClient({ apiKey: API_KEY! });
|
||||||
|
}
|
||||||
|
|
||||||
const originalFetch = (client as any)._fetchWithErrorHandling.bind(client);
|
/**
|
||||||
(client as any)._fetchWithErrorHandling = async (
|
* Retry an async SDK call on transient errors (NetworkError, RateLimitError).
|
||||||
url: string,
|
* Use this to wrap any SDK call that may flake in CI.
|
||||||
options: any,
|
*/
|
||||||
) => {
|
export async function withRetry<T>(
|
||||||
const MAX_RETRIES = 2;
|
fn: () => Promise<T>,
|
||||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
maxRetries = 2,
|
||||||
try {
|
): Promise<T> {
|
||||||
return await originalFetch(url, options);
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
} catch (error: any) {
|
try {
|
||||||
const isTransient =
|
return await fn();
|
||||||
error instanceof NetworkError || error instanceof RateLimitError;
|
} catch (error: any) {
|
||||||
if (isTransient && attempt < MAX_RETRIES) {
|
const isTransient =
|
||||||
await new Promise((r) => setTimeout(r, 3_000 * attempt));
|
error instanceof NetworkError || error instanceof RateLimitError;
|
||||||
continue;
|
if (isTransient && attempt < maxRetries) {
|
||||||
}
|
await new Promise((r) => setTimeout(r, 3_000 * attempt));
|
||||||
throw error;
|
continue;
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
throw new Error("withRetry: unreachable");
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +60,9 @@ export async function waitForMemories(
|
|||||||
): Promise<Memory[]> {
|
): Promise<Memory[]> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < maxWaitMs) {
|
while (Date.now() - start < maxWaitMs) {
|
||||||
const memories = await client.getAll({ user_id: userId });
|
const memories = await withRetry(() =>
|
||||||
|
client.getAll({ user_id: userId }),
|
||||||
|
);
|
||||||
if (Array.isArray(memories) && memories.length >= minCount) {
|
if (Array.isArray(memories) && memories.length >= minCount) {
|
||||||
return memories;
|
return memories;
|
||||||
}
|
}
|
||||||
@@ -84,7 +83,7 @@ export async function waitForSearchResults(
|
|||||||
): Promise<Memory[]> {
|
): Promise<Memory[]> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < maxWaitMs) {
|
while (Date.now() - start < maxWaitMs) {
|
||||||
const results = await client.search(query, options);
|
const results = await withRetry(() => client.search(query, options));
|
||||||
if (Array.isArray(results) && results.length > 0) {
|
if (Array.isArray(results) && results.length > 0) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -129,33 +128,38 @@ export async function seedTestMemories(
|
|||||||
client: MemoryClient,
|
client: MemoryClient,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
await client.add(
|
await withRetry(() =>
|
||||||
[
|
client.add(
|
||||||
{
|
[
|
||||||
role: "user" as const,
|
{
|
||||||
content: "Hi, I'm integration-test-user. My favorite color is blue.",
|
role: "user" as const,
|
||||||
},
|
content:
|
||||||
{
|
"Hi, I'm integration-test-user. My favorite color is blue.",
|
||||||
role: "assistant" as const,
|
},
|
||||||
content:
|
{
|
||||||
"Nice to meet you! I'll remember that your favorite color is blue.",
|
role: "assistant" as const,
|
||||||
},
|
content:
|
||||||
],
|
"Nice to meet you! I'll remember that your favorite color is blue.",
|
||||||
{ user_id: userId },
|
},
|
||||||
|
],
|
||||||
|
{ user_id: userId },
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.add(
|
await withRetry(() =>
|
||||||
[
|
client.add(
|
||||||
{
|
[
|
||||||
role: "user" as const,
|
{
|
||||||
content: "I work as a software engineer at Acme Corp.",
|
role: "user" as const,
|
||||||
},
|
content: "I work as a software engineer at Acme Corp.",
|
||||||
{
|
},
|
||||||
role: "assistant" as const,
|
{
|
||||||
content: "Got it, you're a software engineer at Acme Corp!",
|
role: "assistant" as const,
|
||||||
},
|
content: "Got it, you're a software engineer at Acme Corp!",
|
||||||
],
|
},
|
||||||
{ user_id: userId },
|
],
|
||||||
|
{ user_id: userId },
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const memories = await waitForMemories(client, userId, 1);
|
const memories = await waitForMemories(client, userId, 1);
|
||||||
@@ -180,3 +184,34 @@ export async function cleanupTestUser(
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full project wipe — deletes all memories and all entities.
|
||||||
|
* Equivalent to Python SDK's:
|
||||||
|
* client.delete_all(user_id="*", agent_id="*", app_id="*", run_id="*")
|
||||||
|
*
|
||||||
|
* Used as cleanup before and after integration test runs so tests
|
||||||
|
* start from a clean slate and don't leave data behind.
|
||||||
|
*/
|
||||||
|
export async function fullProjectCleanup(
|
||||||
|
client: MemoryClient,
|
||||||
|
): Promise<void> {
|
||||||
|
// Delete all memories — all four filters set explicitly
|
||||||
|
try {
|
||||||
|
await client.deleteAll({
|
||||||
|
user_id: "*",
|
||||||
|
agent_id: "*",
|
||||||
|
app_id: "*",
|
||||||
|
run_id: "*",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore — may 404 if no data exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all entities (users, agents, apps, runs)
|
||||||
|
try {
|
||||||
|
await client.deleteUsers();
|
||||||
|
} catch {
|
||||||
|
// ignore — may throw "No entities to delete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Integration tests: User management, project, and webhooks.
|
* Integration tests: User management and project configuration.
|
||||||
*
|
*
|
||||||
* Tests users(), getProject(), and the full webhook lifecycle
|
* Tests users(), getProject(), and updateProject() against the real API.
|
||||||
* (create → list → update → delete) against the real API.
|
|
||||||
*
|
*
|
||||||
* Note: SDK webhook methods have a known camelCase bug (eventTypes vs event_types),
|
* Note: Webhook tests (createWebhook, updateWebhook) are excluded because
|
||||||
* so webhook tests call _fetchWithErrorHandling directly with correct snake_case keys.
|
* the SDK has a known bug where it sends camelCase keys (eventTypes) instead
|
||||||
|
* of snake_case (event_types). These will be added once the SDK is fixed.
|
||||||
*
|
*
|
||||||
* Run: MEM0_API_KEY=your-key npx jest management.test.ts --forceExit
|
* Run: MEM0_API_KEY=your-key npx jest management.test.ts --forceExit
|
||||||
*/
|
*/
|
||||||
import { MemoryClient } from "../../mem0";
|
import { MemoryClient } from "../../mem0";
|
||||||
import type { Webhook } from "../../mem0.types";
|
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import {
|
import {
|
||||||
describeIntegration,
|
describeIntegration,
|
||||||
@@ -25,11 +24,10 @@ jest.setTimeout(120_000);
|
|||||||
const TEST_USER_ID = `integration-mgmt-${randomUUID()}`;
|
const TEST_USER_ID = `integration-mgmt-${randomUUID()}`;
|
||||||
|
|
||||||
describeIntegration(
|
describeIntegration(
|
||||||
"MemoryClient Integration — Users, Project & Webhooks",
|
"MemoryClient Integration — Users & Project",
|
||||||
() => {
|
() => {
|
||||||
let client: MemoryClient;
|
let client: MemoryClient;
|
||||||
let cleanup: () => void;
|
let cleanup: () => void;
|
||||||
let webhookId: string | undefined;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
cleanup = suppressTelemetryNoise();
|
cleanup = suppressTelemetryNoise();
|
||||||
@@ -38,13 +36,6 @@ describeIntegration(
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (webhookId) {
|
|
||||||
try {
|
|
||||||
await client.deleteWebhook({ webhookId });
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await cleanupTestUser(client, TEST_USER_ID);
|
await cleanupTestUser(client, TEST_USER_ID);
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
@@ -71,6 +62,8 @@ describeIntegration(
|
|||||||
|
|
||||||
// ─── Project ──────────────────────────────────────────────
|
// ─── Project ──────────────────────────────────────────────
|
||||||
describe("project management", () => {
|
describe("project management", () => {
|
||||||
|
let originalInstructions: string | undefined;
|
||||||
|
|
||||||
test("gets project with custom_instructions field", async () => {
|
test("gets project with custom_instructions field", async () => {
|
||||||
const project = await client.getProject({
|
const project = await client.getProject({
|
||||||
fields: ["custom_instructions"],
|
fields: ["custom_instructions"],
|
||||||
@@ -79,78 +72,29 @@ describeIntegration(
|
|||||||
expect(project).toBeDefined();
|
expect(project).toBeDefined();
|
||||||
expect(typeof project).toBe("object");
|
expect(typeof project).toBe("object");
|
||||||
expect("custom_instructions" in project).toBe(true);
|
expect("custom_instructions" in project).toBe(true);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Webhooks ─────────────────────────────────────────────
|
originalInstructions = project.custom_instructions;
|
||||||
// SDK's createWebhook/updateWebhook send camelCase eventTypes but
|
|
||||||
// API expects snake_case event_types, so we call the API directly.
|
|
||||||
describe("webhook lifecycle", () => {
|
|
||||||
const webhookUrl = `https://example.com/webhook-test-${randomUUID()}`;
|
|
||||||
|
|
||||||
test("creates a webhook", async () => {
|
|
||||||
const payload = {
|
|
||||||
name: `integration-test-webhook-${randomUUID().slice(0, 8)}`,
|
|
||||||
url: webhookUrl,
|
|
||||||
event_types: ["memory_add", "memory_update"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const created = await (client as any)._fetchWithErrorHandling(
|
|
||||||
`${client.host}/api/v1/webhooks/projects/${client.projectId}/`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: client.headers,
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(created).toBeDefined();
|
|
||||||
expect(typeof created.webhook_id).toBe("string");
|
|
||||||
|
|
||||||
webhookId = created.webhook_id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("lists webhooks and finds the created one", async () => {
|
test("updates project custom_instructions via updateProject()", async () => {
|
||||||
const webhooks = await client.getWebhooks();
|
const testInstruction = `integration-test-${randomUUID().slice(0, 8)}`;
|
||||||
|
|
||||||
expect(Array.isArray(webhooks)).toBe(true);
|
const result = await client.updateProject({
|
||||||
|
custom_instructions: testInstruction,
|
||||||
if (webhookId) {
|
});
|
||||||
const found = webhooks.find(
|
|
||||||
(w: Webhook) => w.webhook_id === webhookId,
|
|
||||||
);
|
|
||||||
expect(found).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("updates a webhook", async () => {
|
|
||||||
if (!webhookId) return;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
name: `updated-webhook-${randomUUID().slice(0, 8)}`,
|
|
||||||
url: webhookUrl,
|
|
||||||
event_types: ["memory_add", "memory_update", "memory_delete"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await (client as any)._fetchWithErrorHandling(
|
|
||||||
`${client.host}/api/v1/webhooks/${webhookId}/`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: client.headers,
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
|
||||||
|
|
||||||
test("deletes the webhook", async () => {
|
// Verify the update took effect
|
||||||
if (!webhookId) return;
|
const project = await client.getProject({
|
||||||
|
fields: ["custom_instructions"],
|
||||||
|
});
|
||||||
|
expect(project.custom_instructions).toBe(testInstruction);
|
||||||
|
|
||||||
const result = await client.deleteWebhook({ webhookId });
|
// Restore original
|
||||||
expect(result).toBeDefined();
|
await client.updateProject({
|
||||||
|
custom_instructions: originalInstructions || "",
|
||||||
webhookId = undefined;
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,4 +105,27 @@ describeIntegration("MemoryClient Integration — Search & History", () => {
|
|||||||
expect(events).toContain("ADD");
|
expect(events).toContain("ADD");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Edge cases ─────────────────────────────────────────
|
||||||
|
describe("edge cases", () => {
|
||||||
|
test("search for non-existent user returns empty results", async () => {
|
||||||
|
const results = await client.search("anything", {
|
||||||
|
user_id: `nonexistent-user-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("search with limit returns at most that many results", async () => {
|
||||||
|
const results = await waitForSearchResults(
|
||||||
|
client,
|
||||||
|
"Tell me about integration test user",
|
||||||
|
{ user_id: TEST_USER_ID, limit: 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBeLessThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user