mirror of https://github.com/openclaw/openclaw.git
Memory: harden readFile ENOENT handling
This commit is contained in:
parent
f3f47886ba
commit
ec4198954a
|
|
@ -12,11 +12,16 @@ let searchImpl: () => Promise<unknown[]> = async () => [
|
|||
source: "memory" as const,
|
||||
},
|
||||
];
|
||||
let readFileImpl: () => Promise<unknown> = async () => "";
|
||||
type MemoryReadParams = { relPath: string; from?: number; lines?: number };
|
||||
type MemoryReadResult = { text: string; path: string };
|
||||
let readFileImpl: (params: MemoryReadParams) => Promise<MemoryReadResult> = async (params) => ({
|
||||
text: "",
|
||||
path: params.relPath,
|
||||
});
|
||||
|
||||
const stubManager = {
|
||||
search: vi.fn(async () => await searchImpl()),
|
||||
readFile: vi.fn(async () => await readFileImpl()),
|
||||
readFile: vi.fn(async (params: MemoryReadParams) => await readFileImpl(params)),
|
||||
status: () => ({
|
||||
backend,
|
||||
files: 1,
|
||||
|
|
@ -59,7 +64,7 @@ beforeEach(() => {
|
|||
source: "memory" as const,
|
||||
},
|
||||
];
|
||||
readFileImpl = async () => ""; // default: return empty string
|
||||
readFileImpl = async (params: MemoryReadParams) => ({ text: "", path: params.relPath });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
@ -170,7 +175,7 @@ describe("memory tools", () => {
|
|||
});
|
||||
|
||||
it("does not throw when memory_get fails", async () => {
|
||||
readFileImpl = async () => {
|
||||
readFileImpl = async (_params: MemoryReadParams) => {
|
||||
throw new Error("path required");
|
||||
};
|
||||
|
||||
|
|
@ -191,7 +196,7 @@ describe("memory tools", () => {
|
|||
});
|
||||
|
||||
it("returns empty text without error when file does not exist (ENOENT)", async () => {
|
||||
readFileImpl = async () => {
|
||||
readFileImpl = async (_params: MemoryReadParams) => {
|
||||
return { text: "", path: "memory/2026-02-19.md" };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { MemoryIndexManager } from "./index.js";
|
||||
import { resetEmbeddingMocks } from "./embedding.test-mocks.js";
|
||||
import { getRequiredMemoryIndexManager } from "./test-manager-helpers.js";
|
||||
|
||||
function createMemorySearchCfg(options: {
|
||||
workspaceDir: string;
|
||||
indexPath: string;
|
||||
}): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: options.workspaceDir,
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "mock-embed",
|
||||
store: { path: options.indexPath, vector: { enabled: false } },
|
||||
cache: { enabled: false },
|
||||
query: { minScore: 0, hybrid: { enabled: false } },
|
||||
sync: { watch: false, onSessionStart: false, onSearch: false },
|
||||
},
|
||||
},
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
describe("MemoryIndexManager.readFile", () => {
|
||||
let workspaceDir: string;
|
||||
let indexPath: string;
|
||||
let manager: MemoryIndexManager | null = null;
|
||||
|
||||
beforeEach(async () => {
|
||||
resetEmbeddingMocks();
|
||||
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-read-"));
|
||||
indexPath = path.join(workspaceDir, "index.sqlite");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (manager) {
|
||||
await manager.close();
|
||||
manager = null;
|
||||
}
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns empty text when the requested file does not exist", async () => {
|
||||
manager = await getRequiredMemoryIndexManager({
|
||||
cfg: createMemorySearchCfg({ workspaceDir, indexPath }),
|
||||
agentId: "main",
|
||||
});
|
||||
|
||||
const relPath = "memory/2099-01-01.md";
|
||||
const result = await manager.readFile({ relPath });
|
||||
expect(result).toEqual({ text: "", path: relPath });
|
||||
});
|
||||
|
||||
it("returns content slices when the file exists", async () => {
|
||||
const relPath = "memory/2026-02-20.md";
|
||||
const absPath = path.join(workspaceDir, relPath);
|
||||
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
||||
await fs.writeFile(absPath, ["line 1", "line 2", "line 3"].join("\n"), "utf-8");
|
||||
|
||||
manager = await getRequiredMemoryIndexManager({
|
||||
cfg: createMemorySearchCfg({ workspaceDir, indexPath }),
|
||||
agentId: "main",
|
||||
});
|
||||
|
||||
const result = await manager.readFile({ relPath, from: 2, lines: 1 });
|
||||
expect(result).toEqual({ text: "line 2", path: relPath });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Stats } from "node:fs";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { type FSWatcher } from "chokidar";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type {
|
||||
MemoryEmbeddingProbeResult,
|
||||
MemoryProviderStatus,
|
||||
MemorySearchManager,
|
||||
MemorySearchResult,
|
||||
MemorySource,
|
||||
MemorySyncProgressUpdate,
|
||||
} from "./types.js";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
createEmbeddingProvider,
|
||||
|
|
@ -20,14 +29,6 @@ import { isMemoryPath, normalizeExtraMemoryPaths } from "./internal.js";
|
|||
import { MemoryManagerEmbeddingOps } from "./manager-embedding-ops.js";
|
||||
import { searchKeyword, searchVector } from "./manager-search.js";
|
||||
import { extractKeywords } from "./query-expansion.js";
|
||||
import type {
|
||||
MemoryEmbeddingProbeResult,
|
||||
MemoryProviderStatus,
|
||||
MemorySearchManager,
|
||||
MemorySearchResult,
|
||||
MemorySource,
|
||||
MemorySyncProgressUpdate,
|
||||
} from "./types.js";
|
||||
const SNIPPET_MAX_CHARS = 700;
|
||||
const VECTOR_TABLE = "chunks_vec";
|
||||
const FTS_TABLE = "chunks_fts";
|
||||
|
|
@ -36,6 +37,15 @@ const BATCH_FAILURE_LIMIT = 2;
|
|||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
function isFileMissingError(err: unknown): err is NodeJS.ErrnoException & { code: "ENOENT" } {
|
||||
return Boolean(
|
||||
err &&
|
||||
typeof err === "object" &&
|
||||
"code" in err &&
|
||||
(err as Partial<NodeJS.ErrnoException>).code === "ENOENT",
|
||||
);
|
||||
}
|
||||
|
||||
const INDEX_CACHE = new Map<string, MemoryIndexManager>();
|
||||
|
||||
export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements MemorySearchManager {
|
||||
|
|
@ -437,15 +447,11 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
|||
if (!absPath.endsWith(".md")) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
let stat;
|
||||
let stat: Stats;
|
||||
try {
|
||||
stat = await fs.lstat(absPath);
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
"code" in err &&
|
||||
(err as NodeJS.ErrnoException).code === "ENOENT"
|
||||
) {
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return { text: "", path: relPath };
|
||||
}
|
||||
throw err;
|
||||
|
|
@ -453,7 +459,15 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
|||
if (stat.isSymbolicLink() || !stat.isFile()) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
const content = await fs.readFile(absPath, "utf-8");
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(absPath, "utf-8");
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return { text: "", path: relPath };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!params.from && !params.lines) {
|
||||
return { text: content, path: relPath };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue