Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}
},
"scripts": {
"build": "bunx tsc && mkdir -p dist/web && cp -r src/web/* dist/web/",
"build": "bunx tsc && bun scripts/copy-web-assets.mjs",
"dev": "tsc --watch",
"typecheck": "tsc --noEmit",
"format": "prettier --write \"src/**/*.{ts,js,css,html}\"",
Expand Down
4 changes: 4 additions & 0 deletions scripts/copy-web-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { cpSync, mkdirSync } from "node:fs";

mkdirSync("dist/web", { recursive: true });
cpSync("src/web", "dist/web", { recursive: true });
8 changes: 5 additions & 3 deletions tests/config-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ describe("project-scoped config resolution", () => {
let readSpy: ReturnType<typeof spyOn>;
let existsSpy: ReturnType<typeof spyOn>;

const normalizePath = (p: unknown) => String(p).replace(/\\/g, "/");

afterEach(() => {
readSpy?.mockRestore();
existsSpy?.mockRestore();
Expand All @@ -15,7 +17,7 @@ describe("project-scoped config resolution", () => {

it("uses global config when no project config exists", () => {
existsSpy = spyOn(fs, "existsSync").mockImplementation((p) => {
const path = String(p);
const path = normalizePath(p);
return path.includes(".config/opencode/opencode-mem");
});
readSpy = spyOn(fs, "readFileSync").mockReturnValue(
Expand All @@ -28,7 +30,7 @@ describe("project-scoped config resolution", () => {
it("project config overrides global config", () => {
existsSpy = spyOn(fs, "existsSync").mockReturnValue(true);
readSpy = spyOn(fs, "readFileSync").mockImplementation((p) => {
const path = String(p);
const path = normalizePath(p);
if (path.includes(".opencode/opencode-mem")) {
return JSON.stringify({
opencodeProvider: "openai",
Expand All @@ -48,7 +50,7 @@ describe("project-scoped config resolution", () => {
it("shallow merge: project adds fields, global fields preserved when not overridden", () => {
existsSpy = spyOn(fs, "existsSync").mockReturnValue(true);
readSpy = spyOn(fs, "readFileSync").mockImplementation((p) => {
const path = String(p);
const path = normalizePath(p);
if (path.includes(".opencode/opencode-mem")) {
return JSON.stringify({ opencodeProvider: "anthropic" }) as any;
}
Expand Down
37 changes: 37 additions & 0 deletions tests/helpers/temp-dir.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { rmSync } from "node:fs";

const RETRYABLE_REMOVE_CODES = new Set(["EBUSY", "ENOTEMPTY", "EPERM"]);

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function isRetryableRemoveError(error) {
return RETRYABLE_REMOVE_CODES.has(error?.code);
}

export async function removeDirWithRetries(dir, attempts = 20) {
let lastError;

for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
rmSync(dir, { recursive: true, force: true });
return;
} catch (error) {
lastError = error;
if (!isRetryableRemoveError(error) || attempt === attempts - 1) {
throw error;
}
await sleep(50 * (attempt + 1));
}
}

throw lastError;
}

export async function removeTempDirs(tempDirs) {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) await removeDirWithRetries(dir);
}
}
7 changes: 4 additions & 3 deletions tests/profile-write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* by testing the underlying manager directly (no live plugin context needed).
*/
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { connectionManager } from "../src/services/sqlite/connection-manager.js";
import { removeDirWithRetries } from "./helpers/temp-dir.mjs";

// We patch CONFIG.storagePath before importing the manager so the DB lands in tmp.
let tmpDir: string;
Expand All @@ -28,9 +29,9 @@ describe("UserProfileManager – explicit preference writes", () => {
tmpDir = mkdtempSync(join(tmpdir(), "opencode-mem-test-"));
});

afterEach(() => {
afterEach(async () => {
connectionManager.closeAll();
rmSync(tmpDir, { recursive: true, force: true });
await removeDirWithRetries(tmpDir);
});

it("creates a profile with an explicit preference when none exists", async () => {
Expand Down
14 changes: 9 additions & 5 deletions tests/vector-backends/exact-scan-backend.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { afterEach, describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getDatabase } from "../../src/services/sqlite/sqlite-bootstrap.js";
import { ExactScanBackend } from "../../src/services/vector-backends/exact-scan-backend.js";
import { removeTempDirs } from "../helpers/temp-dir.mjs";

const Database = getDatabase();

describe("ExactScanBackend", () => {
const tempDirs: string[] = [];
const databases: Array<{ close: () => void }> = [];

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
afterEach(async () => {
while (databases.length > 0) {
databases.pop()?.close();
}
await removeTempDirs(tempDirs);
});

it("returns nearest vectors in similarity order", () => {
Expand All @@ -41,6 +43,7 @@ describe("ExactScanBackend", () => {
const tempDir = mkdtempSync(join(tmpdir(), "exact-scan-backend-"));
tempDirs.push(tempDir);
const db = new Database(join(tempDir, "test.db"));
databases.push(db);

db.run(`CREATE TABLE memories (id TEXT PRIMARY KEY, vector BLOB, tags_vector BLOB)`);

Expand Down Expand Up @@ -76,6 +79,7 @@ describe("ExactScanBackend", () => {
const tempDir = mkdtempSync(join(tmpdir(), "exact-scan-backend-empty-"));
tempDirs.push(tempDir);
const db = new Database(join(tempDir, "test.db"));
databases.push(db);
db.run(`CREATE TABLE memories (id TEXT PRIMARY KEY, vector BLOB, tags_vector BLOB)`);

const backend = new ExactScanBackend();
Expand Down
15 changes: 10 additions & 5 deletions tests/vector-backends/migration-fallback.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
import { afterEach, describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getDatabase } from "../../src/services/sqlite/sqlite-bootstrap.js";
import { ExactScanBackend } from "../../src/services/vector-backends/exact-scan-backend.js";
import { VectorSearch } from "../../src/services/sqlite/vector-search.js";
import { removeTempDirs } from "../helpers/temp-dir.mjs";
import { connectionManager } from "../../src/services/sqlite/connection-manager.js";

const Database = getDatabase();

describe("migration with backend abstraction", () => {
const tempDirs: string[] = [];
const databases: Array<{ close: () => void }> = [];

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
afterEach(async () => {
connectionManager.closeAll();
while (databases.length > 0) {
databases.pop()?.close();
}
await removeTempDirs(tempDirs);
});

it("rebuilds and searches memories without direct hnsw manager calls", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "migration-backend-"));
tempDirs.push(tempDir);
const dbPath = join(tempDir, "test.db");
const db = new Database(dbPath);
databases.push(db);
db.run(`
CREATE TABLE memories (
id TEXT PRIMARY KEY,
Expand Down
102 changes: 55 additions & 47 deletions tests/vector-backends/usearch-backend.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { afterEach, describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getDatabase } from "../../src/services/sqlite/sqlite-bootstrap.js";
import { USearchBackend } from "../../src/services/vector-backends/usearch-backend.js";
import { removeTempDirs } from "../helpers/temp-dir.mjs";

const Database = getDatabase();
const canLoadUSearch = await import("usearch").then(() => true).catch(() => false);
const itIfUSearchAvailable = canLoadUSearch ? it : it.skip;

describe("USearchBackend", () => {
const tempDirs: string[] = [];
const databases: Array<{ close: () => void }> = [];

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
afterEach(async () => {
while (databases.length > 0) {
databases.pop()?.close();
}
await removeTempDirs(tempDirs);
});

it("creates and searches an in-memory index", async () => {
itIfUSearchAvailable("creates and searches an in-memory index", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "usearch-backend-"));
tempDirs.push(baseDir);

Expand All @@ -38,7 +42,7 @@ describe("USearchBackend", () => {
expect(result.map((x) => x.id)).toEqual(["a", "c"]);
});

it("supports public insert and search path", async () => {
itIfUSearchAvailable("supports public insert and search path", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "usearch-backend-public-"));
tempDirs.push(baseDir);

Expand Down Expand Up @@ -72,50 +76,54 @@ describe("USearchBackend", () => {
expect(result.map((x) => x.id)).toEqual(["alpha"]);
});

it("updates an existing id instead of failing on duplicate insert", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "usearch-backend-upsert-"));
tempDirs.push(baseDir);

const shard = {
id: 1,
scope: "project" as const,
scopeHash: "hash",
shardIndex: 0,
dbPath: join(baseDir, "test.db"),
vectorCount: 1,
isActive: true,
createdAt: Date.now(),
};

const backend = new USearchBackend({ baseDir, dimensions: 4 });
await backend.insert({
id: "alpha",
vector: new Float32Array([0, 1, 0, 0]),
shard,
kind: "content",
});
await backend.insert({
id: "alpha",
vector: new Float32Array([1, 0, 0, 0]),
shard,
kind: "content",
});

const result = await backend.search({
db: null,
shard,
kind: "content",
queryVector: new Float32Array([1, 0, 0, 0]),
limit: 1,
});

expect(result.map((x) => x.id)).toEqual(["alpha"]);
});
itIfUSearchAvailable(
"updates an existing id instead of failing on duplicate insert",
async () => {
const baseDir = mkdtempSync(join(tmpdir(), "usearch-backend-upsert-"));
tempDirs.push(baseDir);

const shard = {
id: 1,
scope: "project" as const,
scopeHash: "hash",
shardIndex: 0,
dbPath: join(baseDir, "test.db"),
vectorCount: 1,
isActive: true,
createdAt: Date.now(),
};

const backend = new USearchBackend({ baseDir, dimensions: 4 });
await backend.insert({
id: "alpha",
vector: new Float32Array([0, 1, 0, 0]),
shard,
kind: "content",
});
await backend.insert({
id: "alpha",
vector: new Float32Array([1, 0, 0, 0]),
shard,
kind: "content",
});

const result = await backend.search({
db: null,
shard,
kind: "content",
queryVector: new Float32Array([1, 0, 0, 0]),
limit: 1,
});

expect(result.map((x) => x.id)).toEqual(["alpha"]);
}
);

it("rebuilds an index from sqlite rows", async () => {
itIfUSearchAvailable("rebuilds an index from sqlite rows", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "usearch-backend-rebuild-"));
tempDirs.push(baseDir);
const db = new Database(join(baseDir, "test.db"));
databases.push(db);
db.run(`CREATE TABLE memories (id TEXT PRIMARY KEY, vector BLOB, tags_vector BLOB)`);
db.prepare(`INSERT INTO memories (id, vector, tags_vector) VALUES (?, ?, ?)`).run(
"alpha",
Expand Down
16 changes: 11 additions & 5 deletions tests/vector-search-backend-integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { afterEach, describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ExactScanBackend } from "../src/services/vector-backends/exact-scan-backend.js";
import { getDatabase } from "../src/services/sqlite/sqlite-bootstrap.js";
import { VectorSearch } from "../src/services/sqlite/vector-search.js";
import type { VectorBackend } from "../src/services/vector-backends/types.js";
import { removeTempDirs } from "./helpers/temp-dir.mjs";
import { connectionManager } from "../src/services/sqlite/connection-manager.js";

const Database = getDatabase();

Expand All @@ -27,19 +29,22 @@ function createFailingBackend(): VectorBackend {

describe("vector search backend integration", () => {
const tempDirs: string[] = [];
const databases: Array<{ close: () => void }> = [];

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
afterEach(async () => {
connectionManager.closeAll();
while (databases.length > 0) {
databases.pop()?.close();
}
await removeTempDirs(tempDirs);
});

it("searches inserted memories and preserves ranking semantics", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "vector-search-integration-"));
tempDirs.push(tempDir);
const dbPath = join(tempDir, "test.db");
const db = new Database(dbPath);
databases.push(db);

db.run(`
CREATE TABLE memories (
Expand Down Expand Up @@ -124,6 +129,7 @@ describe("vector search backend integration", () => {
tempDirs.push(tempDir);
const dbPath = join(tempDir, "test.db");
const db = new Database(dbPath);
databases.push(db);

db.run(`
CREATE TABLE memories (
Expand Down
Loading