fix(ts-sdk): stop forcing pg on installs (optional ranged pg peer, optional natural) (#7450)
This commit is contained in:
@@ -5,6 +5,12 @@ description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector
|
|||||||
|
|
||||||
[pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension.
|
[pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension.
|
||||||
|
|
||||||
|
The TypeScript SDK loads the `pg` driver only when you use this store, so install it alongside `mem0ai`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install pg
|
||||||
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
|
|||||||
+13
-4
@@ -100,7 +100,8 @@
|
|||||||
"tsup": "^8.3.0",
|
"tsup": "^8.3.0",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"iovalkey": "^0.3.3",
|
"iovalkey": "^0.3.3",
|
||||||
"@mochow/mochow-sdk-node": "^2.1.5"
|
"@mochow/mochow-sdk-node": "^2.1.5",
|
||||||
|
"@types/jest": "^29.5.14"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.18.0",
|
"axios": "^1.18.0",
|
||||||
@@ -127,8 +128,7 @@
|
|||||||
"@qdrant/js-client-rest": "^1.18.0",
|
"@qdrant/js-client-rest": "^1.18.0",
|
||||||
"@supabase/supabase-js": "^2.49.1",
|
"@supabase/supabase-js": "^2.49.1",
|
||||||
"@turbopuffer/turbopuffer": "^2.0.0",
|
"@turbopuffer/turbopuffer": "^2.0.0",
|
||||||
"@types/jest": "29.5.14",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/pg": "8.11.0",
|
|
||||||
"@upstash/vector": "^1.2.3",
|
"@upstash/vector": "^1.2.3",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"cassandra-driver": "4.8.0",
|
"cassandra-driver": "4.8.0",
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
"weaviate-client": "^3.0.0",
|
"weaviate-client": "^3.0.0",
|
||||||
"ollama": "^0.5.14",
|
"ollama": "^0.5.14",
|
||||||
"oracledb": "^6.5.0 || ^7.0.0",
|
"oracledb": "^6.5.0 || ^7.0.0",
|
||||||
"pg": "8.11.3",
|
"pg": "^8.11.3",
|
||||||
"redis": "^4.6.13",
|
"redis": "^4.6.13",
|
||||||
"@elastic/elasticsearch": "^9.0.0",
|
"@elastic/elasticsearch": "^9.0.0",
|
||||||
"iovalkey": "^0.3.3",
|
"iovalkey": "^0.3.3",
|
||||||
@@ -257,6 +257,15 @@
|
|||||||
},
|
},
|
||||||
"oracledb": {
|
"oracledb": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"pg": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/pg": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"natural": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Generated
+6
-42
@@ -109,11 +109,8 @@ importers:
|
|||||||
'@turbopuffer/turbopuffer':
|
'@turbopuffer/turbopuffer':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
'@types/jest':
|
|
||||||
specifier: 29.5.14
|
|
||||||
version: 29.5.14
|
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: 8.11.0
|
specifier: ^8.11.0
|
||||||
version: 8.11.0
|
version: 8.11.0
|
||||||
'@upstash/vector':
|
'@upstash/vector':
|
||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
@@ -167,8 +164,8 @@ importers:
|
|||||||
specifier: ^6.5.0 || ^7.0.0
|
specifier: ^6.5.0 || ^7.0.0
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
pg:
|
pg:
|
||||||
specifier: 8.11.3
|
specifier: ^8.11.3
|
||||||
version: 8.11.3
|
version: 8.21.0
|
||||||
redis:
|
redis:
|
||||||
specifier: ^4.6.13
|
specifier: ^4.6.13
|
||||||
version: 4.7.1
|
version: 4.7.1
|
||||||
@@ -191,6 +188,9 @@ importers:
|
|||||||
'@types/better-sqlite3':
|
'@types/better-sqlite3':
|
||||||
specifier: ^7.6.13
|
specifier: ^7.6.13
|
||||||
version: 7.6.13
|
version: 7.6.13
|
||||||
|
'@types/jest':
|
||||||
|
specifier: ^29.5.14
|
||||||
|
version: 29.5.14
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.7.6
|
specifier: ^22.7.6
|
||||||
version: 22.19.21
|
version: 22.19.21
|
||||||
@@ -2228,10 +2228,6 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
buffer-writer@2.0.0:
|
|
||||||
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
@@ -3846,9 +3842,6 @@ packages:
|
|||||||
package-json-from-dist@1.0.1:
|
package-json-from-dist@1.0.1:
|
||||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
packet-reader@1.0.0:
|
|
||||||
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
|
|
||||||
|
|
||||||
pad-left@2.1.0:
|
pad-left@2.1.0:
|
||||||
resolution: {integrity: sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==}
|
resolution: {integrity: sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3920,15 +3913,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==}
|
resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
pg@8.11.3:
|
|
||||||
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
|
|
||||||
engines: {node: '>= 8.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
pg-native: '>=3.0.1'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
pg-native:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
pg@8.21.0:
|
pg@8.21.0:
|
||||||
resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==}
|
resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==}
|
||||||
engines: {node: '>= 16.0.0'}
|
engines: {node: '>= 16.0.0'}
|
||||||
@@ -7593,8 +7577,6 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer-writer@2.0.0: {}
|
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@@ -9409,8 +9391,6 @@ snapshots:
|
|||||||
|
|
||||||
package-json-from-dist@1.0.1: {}
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
packet-reader@1.0.0: {}
|
|
||||||
|
|
||||||
pad-left@2.1.0:
|
pad-left@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
repeat-string: 1.6.1
|
repeat-string: 1.6.1
|
||||||
@@ -9458,10 +9438,6 @@ snapshots:
|
|||||||
|
|
||||||
pg-numeric@1.0.2: {}
|
pg-numeric@1.0.2: {}
|
||||||
|
|
||||||
pg-pool@3.14.0(pg@8.11.3):
|
|
||||||
dependencies:
|
|
||||||
pg: 8.11.3
|
|
||||||
|
|
||||||
pg-pool@3.14.0(pg@8.21.0):
|
pg-pool@3.14.0(pg@8.21.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
pg: 8.21.0
|
pg: 8.21.0
|
||||||
@@ -9486,18 +9462,6 @@ snapshots:
|
|||||||
postgres-interval: 3.0.0
|
postgres-interval: 3.0.0
|
||||||
postgres-range: 1.1.4
|
postgres-range: 1.1.4
|
||||||
|
|
||||||
pg@8.11.3:
|
|
||||||
dependencies:
|
|
||||||
buffer-writer: 2.0.0
|
|
||||||
packet-reader: 1.0.0
|
|
||||||
pg-connection-string: 2.13.0
|
|
||||||
pg-pool: 3.14.0(pg@8.11.3)
|
|
||||||
pg-protocol: 1.14.0
|
|
||||||
pg-types: 2.2.0
|
|
||||||
pgpass: 1.0.5
|
|
||||||
optionalDependencies:
|
|
||||||
pg-cloudflare: 1.4.0
|
|
||||||
|
|
||||||
pg@8.21.0:
|
pg@8.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pg-connection-string: 2.13.0
|
pg-connection-string: 2.13.0
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { Client as ClientType, ClientConfig } from "pg";
|
import type { Client as ClientType, ClientConfig } from "pg";
|
||||||
import pkg from "pg";
|
|
||||||
const { Client, escapeIdentifier } = pkg;
|
|
||||||
import { VectorStore } from "./base";
|
import { VectorStore } from "./base";
|
||||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||||
|
import { loadPeer } from "../utils/load_peer";
|
||||||
|
|
||||||
|
function escapeIdentifier(name: string): string {
|
||||||
|
return `"${name.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;
|
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;
|
||||||
|
|
||||||
@@ -212,7 +215,7 @@ function buildClientConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PGVector implements VectorStore {
|
export class PGVector implements VectorStore {
|
||||||
private client: ClientType;
|
private client!: ClientType;
|
||||||
private collectionName: string;
|
private collectionName: string;
|
||||||
private useDiskann: boolean;
|
private useDiskann: boolean;
|
||||||
private useHnsw: boolean;
|
private useHnsw: boolean;
|
||||||
@@ -234,13 +237,6 @@ export class PGVector implements VectorStore {
|
|||||||
? ""
|
? ""
|
||||||
: validateIdentifier(config.dbname || "vector_store", "dbname");
|
: validateIdentifier(config.dbname || "vector_store", "dbname");
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
this.client = new Client(
|
|
||||||
buildClientConfig(
|
|
||||||
config,
|
|
||||||
this.useDirectConnection ? undefined : "postgres",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
this.initialize().catch(console.error);
|
this.initialize().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +253,18 @@ export class PGVector implements VectorStore {
|
|||||||
|
|
||||||
private async _doInitialize(): Promise<void> {
|
private async _doInitialize(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const pg = await loadPeer(
|
||||||
|
"pg",
|
||||||
|
"PGVector vector store",
|
||||||
|
() => import("pg"),
|
||||||
|
);
|
||||||
|
const { Client } = pg.default ?? pg;
|
||||||
|
this.client = new Client(
|
||||||
|
buildClientConfig(
|
||||||
|
this.config,
|
||||||
|
this.useDirectConnection ? undefined : "postgres",
|
||||||
|
),
|
||||||
|
);
|
||||||
await this.client.connect();
|
await this.client.connect();
|
||||||
|
|
||||||
if (!this.useDirectConnection) {
|
if (!this.useDirectConnection) {
|
||||||
@@ -345,6 +353,7 @@ export class PGVector implements VectorStore {
|
|||||||
ids: string[],
|
ids: string[],
|
||||||
payloads: Record<string, any>[],
|
payloads: Record<string, any>[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await this.initialize();
|
||||||
const values = vectors.map((vector, i) => ({
|
const values = vectors.map((vector, i) => ({
|
||||||
id: ids[i],
|
id: ids[i],
|
||||||
vector: `[${vector.join(",")}]`,
|
vector: `[${vector.join(",")}]`,
|
||||||
@@ -368,6 +377,7 @@ export class PGVector implements VectorStore {
|
|||||||
topK: number = 5,
|
topK: number = 5,
|
||||||
filters?: SearchFilters,
|
filters?: SearchFilters,
|
||||||
): Promise<VectorStoreResult[] | null> {
|
): Promise<VectorStoreResult[] | null> {
|
||||||
|
await this.initialize();
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
conditions,
|
conditions,
|
||||||
@@ -406,6 +416,7 @@ export class PGVector implements VectorStore {
|
|||||||
topK: number = 5,
|
topK: number = 5,
|
||||||
filters?: SearchFilters,
|
filters?: SearchFilters,
|
||||||
): Promise<VectorStoreResult[]> {
|
): Promise<VectorStoreResult[]> {
|
||||||
|
await this.initialize();
|
||||||
const queryVector = `[${query.join(",")}]`;
|
const queryVector = `[${query.join(",")}]`;
|
||||||
const {
|
const {
|
||||||
conditions,
|
conditions,
|
||||||
@@ -435,6 +446,7 @@ export class PGVector implements VectorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(vectorId: string): Promise<VectorStoreResult | null> {
|
async get(vectorId: string): Promise<VectorStoreResult | null> {
|
||||||
|
await this.initialize();
|
||||||
const result = await this.client.query(
|
const result = await this.client.query(
|
||||||
`SELECT id, payload FROM ${this.col()} WHERE id = $1`,
|
`SELECT id, payload FROM ${this.col()} WHERE id = $1`,
|
||||||
[vectorId],
|
[vectorId],
|
||||||
@@ -453,6 +465,7 @@ export class PGVector implements VectorStore {
|
|||||||
vector: number[],
|
vector: number[],
|
||||||
payload: Record<string, any>,
|
payload: Record<string, any>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await this.initialize();
|
||||||
const vectorStr = `[${vector.join(",")}]`;
|
const vectorStr = `[${vector.join(",")}]`;
|
||||||
await this.client.query(
|
await this.client.query(
|
||||||
`
|
`
|
||||||
@@ -465,12 +478,14 @@ export class PGVector implements VectorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(vectorId: string): Promise<void> {
|
async delete(vectorId: string): Promise<void> {
|
||||||
|
await this.initialize();
|
||||||
await this.client.query(`DELETE FROM ${this.col()} WHERE id = $1`, [
|
await this.client.query(`DELETE FROM ${this.col()} WHERE id = $1`, [
|
||||||
vectorId,
|
vectorId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCol(): Promise<void> {
|
async deleteCol(): Promise<void> {
|
||||||
|
await this.initialize();
|
||||||
await this.client.query(`DROP TABLE IF EXISTS ${this.col()}`);
|
await this.client.query(`DROP TABLE IF EXISTS ${this.col()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +502,7 @@ export class PGVector implements VectorStore {
|
|||||||
filters?: SearchFilters,
|
filters?: SearchFilters,
|
||||||
topK: number = 100,
|
topK: number = 100,
|
||||||
): Promise<[VectorStoreResult[], number]> {
|
): Promise<[VectorStoreResult[], number]> {
|
||||||
|
await this.initialize();
|
||||||
const {
|
const {
|
||||||
conditions,
|
conditions,
|
||||||
values: filterValues,
|
values: filterValues,
|
||||||
@@ -525,10 +541,11 @@ export class PGVector implements VectorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this.client.end();
|
await this.client?.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserId(): Promise<string> {
|
async getUserId(): Promise<string> {
|
||||||
|
await this.initialize();
|
||||||
const result = await this.client.query(
|
const result = await this.client.query(
|
||||||
"SELECT user_id FROM memory_migrations LIMIT 1",
|
"SELECT user_id FROM memory_migrations LIMIT 1",
|
||||||
);
|
);
|
||||||
@@ -549,6 +566,7 @@ export class PGVector implements VectorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUserId(userId: string): Promise<void> {
|
async setUserId(userId: string): Promise<void> {
|
||||||
|
await this.initialize();
|
||||||
await this.client.query("DELETE FROM memory_migrations");
|
await this.client.query("DELETE FROM memory_migrations");
|
||||||
await this.client.query(
|
await this.client.query(
|
||||||
"INSERT INTO memory_migrations (user_id) VALUES ($1)",
|
"INSERT INTO memory_migrations (user_id) VALUES ($1)",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
jest.mock("pg", () => {
|
||||||
|
throw new Error("Cannot find module 'pg'");
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("natural", () => {
|
||||||
|
throw new Error("Cannot find module 'natural'");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mem0ai/oss without pg or natural installed", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mem0ai/oss loads", async () => {
|
||||||
|
await expect(import("../src")).resolves.toHaveProperty("Memory");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PGVector explains how to install pg", async () => {
|
||||||
|
const { PGVector } = await import("../src/vector_stores/pgvector");
|
||||||
|
const store = new PGVector({
|
||||||
|
connectionString: "postgresql://localhost:5432/db",
|
||||||
|
embeddingModelDims: 3,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
await expect(store.initialize()).rejects.toThrow(
|
||||||
|
"The 'pg' package is required to use the PGVector vector store. Install it with: npm install pg",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("BM25 lemmatization falls back to the built-in stemmer", async () => {
|
||||||
|
const { lemmatizeForBm25 } = await import("../src/utils/lemmatization");
|
||||||
|
|
||||||
|
expect(lemmatizeForBm25("The dogs were running")).toBe("dog runn running");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user