diff --git a/cli.ts b/cli.ts index c969b755..c59cf212 100644 --- a/cli.ts +++ b/cli.ts @@ -659,7 +659,21 @@ export async function runImportMarkdown( // Parse each file for memory entries (lines starting with "- ") for (const { filePath, scope: discoveredScope } of mdFiles) { foundFiles++; - let content = await fsPromises.readFile(filePath, "utf-8"); + let content: string; + try { + const stats = await fsPromises.stat(filePath); + if (!stats.isFile()) { + // Skip non-file entries (e.g. a directory named "YYYY-MM-DD.md") + console.warn(` [skip] not a file: ${filePath}`); + skipped++; + continue; + } + content = await fsPromises.readFile(filePath, "utf-8"); + } catch (err) { + console.warn(` [skip] unreadable: ${filePath} — ${err}`); + skipped++; + continue; + } // Strip UTF-8 BOM (e.g. from Windows Notepad-saved files) content = content.replace(/^\uFEFF/, ""); // Normalize line endings: handle both CRLF (\r\n) and LF (\n) diff --git a/scripts/ci-test-manifest.mjs b/scripts/ci-test-manifest.mjs index 77bc1d98..d412fe27 100644 --- a/scripts/ci-test-manifest.mjs +++ b/scripts/ci-test-manifest.mjs @@ -20,6 +20,7 @@ export const CI_TEST_MANIFEST = [ { group: "core-regression", runner: "node", file: "test/strip-envelope-metadata.test.mjs", args: ["--test"] }, { group: "cli-smoke", runner: "node", file: "test/cli-smoke.mjs" }, { group: "cli-smoke", runner: "node", file: "test/functional-e2e.mjs" }, + { group: "cli-smoke", runner: "node", file: "test/import-markdown/import-markdown.test.mjs", args: ["--test"] }, { group: "core-regression", runner: "node", file: "test/retriever-rerank-regression.mjs" }, { group: "core-regression", runner: "node", file: "test/smart-memory-lifecycle.mjs" }, { group: "core-regression", runner: "node", file: "test/smart-extractor-branches.mjs" }, diff --git a/scripts/verify-ci-test-manifest.mjs b/scripts/verify-ci-test-manifest.mjs index 1a7d652a..c15fce4e 100644 --- a/scripts/verify-ci-test-manifest.mjs +++ b/scripts/verify-ci-test-manifest.mjs @@ -21,6 +21,7 @@ const EXPECTED_BASELINE = [ { group: "core-regression", runner: "node", file: "test/strip-envelope-metadata.test.mjs", args: ["--test"] }, { group: "cli-smoke", runner: "node", file: "test/cli-smoke.mjs" }, { group: "cli-smoke", runner: "node", file: "test/functional-e2e.mjs" }, + { group: "cli-smoke", runner: "node", file: "test/import-markdown/import-markdown.test.mjs", args: ["--test"] }, { group: "core-regression", runner: "node", file: "test/retriever-rerank-regression.mjs" }, { group: "core-regression", runner: "node", file: "test/smart-memory-lifecycle.mjs" }, { group: "core-regression", runner: "node", file: "test/smart-extractor-branches.mjs" }, diff --git a/test/import-markdown/import-markdown.test.mjs b/test/import-markdown/import-markdown.test.mjs index e2deb247..b2995449 100644 --- a/test/import-markdown/import-markdown.test.mjs +++ b/test/import-markdown/import-markdown.test.mjs @@ -416,6 +416,39 @@ describe("import-markdown CLI", () => { ); }); }); + + describe("skip non-file .md entries", () => { + it("skips a directory named YYYY-MM-DD.md without aborting import", async () => { + const wsDir = await setupWorkspace("nonfile-test"); + // Create memory/ subdirectory first + await mkdir(join(wsDir, "memory"), { recursive: true }); + // Create a real .md file + await writeFile( + join(wsDir, "memory", "2026-04-11.md"), + "- Real file entry\n", + "utf-8", + ); + // Create a directory that looks like a .md file (the bug scenario) + const fakeDir = join(wsDir, "memory", "2026-04-12.md"); + await mkdir(fakeDir, { recursive: true }); + + const ctx = { embedder: mockEmbedder, store: mockStore }; + let threw = false; + try { + const { imported, skipped } = await runImportMarkdown(ctx, { + openclawHome: testWorkspaceDir, + workspaceGlob: "nonfile-test", + }); + // Should have imported the real file (1 entry from "- Real file entry") + assert.strictEqual(imported, 1, "should import the real .md file"); + assert.ok(skipped >= 0); + } catch (err) { + threw = true; + throw new Error(`Import aborted on .md directory: ${err}`); + } + assert.ok(!threw, "import should not abort when encountering .md directory"); + }); + }); }); // ────────────────────────────────────────────────────────────────────────────── Test runner helper ──────────────────────────────────────────────────────────────────────────────