fix(memory): support symlinks in memory file discovery
walkDir now follows symbolic links using fs.stat() to determine if the target is a file or directory. This enables: - Symlinked markdown files inside memory/ to be indexed - Symlinked directories inside memory/ to be traversed - Graceful handling of dangling symlinks (silently skipped) Fixes an issue where dirent.isFile() returned false for symlinks, causing them to be excluded from memory search indexing. Cross-platform notes: - Works on Linux/macOS/Windows (symlink support varies by platform) - Windows may require elevated permissions to create symlinks, but reading existing symlinks works without special privileges
This commit is contained in:
parent
2930ebfd43
commit
048e7494bd
@ -1,6 +1,73 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { chunkMarkdown } from "./internal.js";
|
||||
import { chunkMarkdown, listMemoryFiles } from "./internal.js";
|
||||
|
||||
async function makeTempDir(prefix = "moltbot-memory-test-"): Promise<string> {
|
||||
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
describe("listMemoryFiles", () => {
|
||||
it("includes symlinked markdown files", async () => {
|
||||
const tempDir = await makeTempDir();
|
||||
const memoryDir = path.join(tempDir, "memory");
|
||||
await fs.mkdir(memoryDir);
|
||||
|
||||
// Create a real file outside memory/
|
||||
const realFile = path.join(tempDir, "notes.md");
|
||||
await fs.writeFile(realFile, "# Notes\nSome content");
|
||||
|
||||
// Create a symlink inside memory/ pointing to the real file
|
||||
const symlinkPath = path.join(memoryDir, "linked-notes.md");
|
||||
await fs.symlink(realFile, symlinkPath);
|
||||
|
||||
const files = await listMemoryFiles(tempDir);
|
||||
expect(files).toContain(symlinkPath);
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
it("includes files inside symlinked directories", async () => {
|
||||
const tempDir = await makeTempDir();
|
||||
const memoryDir = path.join(tempDir, "memory");
|
||||
await fs.mkdir(memoryDir);
|
||||
|
||||
// Create a real directory with a markdown file outside memory/
|
||||
const realDir = path.join(tempDir, "external-notes");
|
||||
await fs.mkdir(realDir);
|
||||
await fs.writeFile(path.join(realDir, "doc.md"), "# Doc");
|
||||
|
||||
// Create a symlinked directory inside memory/
|
||||
const symlinkDir = path.join(memoryDir, "linked-dir");
|
||||
await fs.symlink(realDir, symlinkDir);
|
||||
|
||||
const files = await listMemoryFiles(tempDir);
|
||||
expect(files).toContain(path.join(symlinkDir, "doc.md"));
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
it("skips dangling symlinks gracefully", async () => {
|
||||
const tempDir = await makeTempDir();
|
||||
const memoryDir = path.join(tempDir, "memory");
|
||||
await fs.mkdir(memoryDir);
|
||||
|
||||
// Create a symlink pointing to a non-existent file
|
||||
const danglingSymlink = path.join(memoryDir, "dangling.md");
|
||||
await fs.symlink("/non/existent/path.md", danglingSymlink);
|
||||
|
||||
// Should not throw and should return empty (no valid files)
|
||||
const files = await listMemoryFiles(tempDir);
|
||||
expect(files).not.toContain(danglingSymlink);
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(tempDir, { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkMarkdown", () => {
|
||||
it("splits overly long lines into max-sized chunks", () => {
|
||||
|
||||
@ -50,11 +50,27 @@ async function walkDir(dir: string, files: string[]) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
|
||||
let isDir = entry.isDirectory();
|
||||
let isFile = entry.isFile();
|
||||
|
||||
// For symlinks, resolve the target type via fs.stat (follows symlink)
|
||||
if (entry.isSymbolicLink()) {
|
||||
try {
|
||||
const stat = await fs.stat(full);
|
||||
isDir = stat.isDirectory();
|
||||
isFile = stat.isFile();
|
||||
} catch {
|
||||
// Dangling symlink or inaccessible target - skip silently
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
await walkDir(full, files);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
if (!isFile) continue;
|
||||
if (!entry.name.endsWith(".md")) continue;
|
||||
files.push(full);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user