Skip to content
Open
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
29 changes: 29 additions & 0 deletions test/embedding-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// Isolate from the on-disk ~/.agentmemory/.env file. The providers resolve
// keys through config's getEnvVar/detectEmbeddingProvider, which call the
// internal getMergedEnv() -> loadEnvFile() and re-read that file every call.
// A live Voyage/OpenAI-compat key (and OPENAI_EMBEDDING_MODEL /
// OPENAI_EMBEDDING_DIMENSIONS) would otherwise leak in and break the
// "returns null when no API keys" and default-dimension assertions.
// Re-implement only the two env-reading exports so they see process.env
// alone (equivalent to loadEnvFile() returning {}); keep everything else
// original.
vi.mock("../src/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/config.js")>();
const getEnvVar = (key: string): string | undefined => process.env[key];
const detectEmbeddingProvider = (
env?: Record<string, string>,
): string | null => {
const source = env ?? (process.env as Record<string, string>);
const forced = source["EMBEDDING_PROVIDER"];
if (forced) return forced;
if (source["GEMINI_API_KEY"]) return "gemini";
if (source["OPENAI_API_KEY"]) return "openai";
if (source["VOYAGE_API_KEY"]) return "voyage";
if (source["COHERE_API_KEY"]) return "cohere";
if (source["OPENROUTER_API_KEY"]) return "openrouter";
return null;
};
return { ...actual, getEnvVar, detectEmbeddingProvider };
});

import {
createEmbeddingProvider,
withDimensionGuard,
Expand Down
35 changes: 27 additions & 8 deletions test/hook-project.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, join } from "node:path";
import { execSync } from "node:child_process";
import { resolveProject } from "../src/hooks/_project.js";

// Derive the expected basename the same way the resolver does (git toplevel
// basename) instead of hardcoding "agentmemory". The clone dir may be named
// anything (e.g. "agentmemory-lancedb"), so a hardcoded literal is not
// hermetic. Falls back to the cwd basename if not in a git repo.
function expectedRepoBasename(): string {
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: process.cwd(),
stdio: ["ignore", "pipe", "ignore"],
timeout: 500,
})
.toString()
.trim();
if (top) return basename(top);
} catch {}
return basename(process.cwd());
}

describe("resolveProject — hook project basename resolver", () => {
const originalEnv = process.env.AGENTMEMORY_PROJECT_NAME;
const repoBasename = expectedRepoBasename();

beforeEach(() => {
delete process.env.AGENTMEMORY_PROJECT_NAME;
Expand All @@ -32,35 +52,34 @@ describe("resolveProject — hook project basename resolver", () => {

it("ignores empty env override", () => {
process.env.AGENTMEMORY_PROJECT_NAME = " ";
const repoBasename = "agentmemory";
expect(resolveProject(process.cwd())).toBe(repoBasename);
});

it("returns git toplevel basename when cwd is inside a repo", () => {
const top = resolveProject(process.cwd());
expect(top).toBe("agentmemory");
expect(top).toBe(repoBasename);
});

it("returns git toplevel basename from a nested subdir", () => {
const nested = join(process.cwd(), "src", "hooks");
expect(resolveProject(nested)).toBe("agentmemory");
expect(resolveProject(nested)).toBe(repoBasename);
});

it("falls back to basename(cwd) when not in a git repo", () => {
const dir = mkdtempSync(join(tmpdir(), "amem-noproj-"));
try {
expect(resolveProject(dir)).toBe(dir.split("/").pop());
expect(resolveProject(dir)).toBe(basename(dir));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("defaults to process.cwd() when no cwd argument given", () => {
expect(resolveProject()).toBe("agentmemory");
expect(resolveProject()).toBe(repoBasename);
});

it("defaults to process.cwd() when cwd argument is empty", () => {
expect(resolveProject("")).toBe("agentmemory");
expect(resolveProject(" ")).toBe("agentmemory");
expect(resolveProject("")).toBe(repoBasename);
expect(resolveProject(" ")).toBe(repoBasename);
});
});
9 changes: 9 additions & 0 deletions test/mesh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ vi.mock("../src/logger.js", () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));

// isAllowedUrl() in src/functions/mesh.ts calls dns/promises.lookup() on any
// non-IP hostname (e.g. the *.example.com peers used below). On DNS-restricted
// hosts that real lookup hangs to the vitest timeout. Mock the dns layer to
// return a fixed PUBLIC address so isAllowedUrl's own private-IP/loopback
// logic still runs and resolves instantly without touching the network.
vi.mock("node:dns/promises", () => ({
lookup: vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]),
}));

import { registerMeshFunction } from "../src/functions/mesh.js";
import type {
MeshPeer,
Expand Down