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
3 changes: 2 additions & 1 deletion sdk/typescript/src/knowledge-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { tmpdir } from "node:os";
import { basename, extname, join, resolve } from "node:path";
import { unzipSync } from "fflate";
import { expandHome } from "./runtime.js";

const SUPPORTED_EXTENSIONS = new Set([
".md",
Expand All @@ -37,7 +38,7 @@ export async function prepareKnowledgeBase(
signal?.throwIfAborted();
if (!requested.trim())
throw new Error("Knowledge base paths cannot be empty.");
const path = resolve(requested);
const path = resolve(expandHome(requested));
const metadata = await lstat(path);
if (metadata.isSymbolicLink()) {
throw new Error(`Knowledge base paths cannot be symbolic links: ${path}`);
Expand Down
37 changes: 36 additions & 1 deletion sdk/typescript/tests-ts/knowledge-base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ import {
symlink,
writeFile,
} from "node:fs/promises";
import * as os from "node:os";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, expect, mock, test } from "bun:test";
import { strToU8, zipSync } from "fflate";
import { prepareKnowledgeBase } from "../src/knowledge-base.js";

const temporaryDirectories: string[] = [];
const testPosix = process.platform === "win32" ? test.skip : test;
// `mock.module` mutates the live `node:os` namespace, so the original exports
// have to be snapshotted before the first mock to be restorable afterwards.
const nodeOs = { ...os };
const realHomeDirectory = os.homedir();

afterEach(async () => {
await Promise.all(
Expand Down Expand Up @@ -105,6 +110,36 @@ describe("scan knowledge bases", () => {
}
});

test("expands ~ in requested paths and leaves absolute and ~user paths alone", async () => {
const home = await temporaryDirectory();
const documents = join(home, "docs");
await mkdir(documents, { recursive: true });
await writeFile(join(documents, "scope.md"), "Review the payment service.");
mock.module("node:os", () => ({ ...nodeOs, homedir: () => home }));
try {
const expanded = await prepareKnowledgeBase(["~/docs"]);
temporaryDirectories.push(expanded.path);
expect(expanded.sources).toEqual([documents]);

const bare = await prepareKnowledgeBase(["~"]);
temporaryDirectories.push(bare.path);
expect(bare.sources).toEqual([home]);

const absolute = await prepareKnowledgeBase([documents]);
temporaryDirectories.push(absolute.path);
expect(absolute.sources).toEqual([documents]);

// Another account's home cannot be resolved portably, so `~other` stays a
// literal path segment under the working directory.
await expect(prepareKnowledgeBase(["~other/docs"])).rejects.toThrow(
/~other/u,
);
} finally {
mock.module("node:os", () => nodeOs);
}
expect(os.homedir()).toBe(realHomeDirectory);
});

test("extracts searchable text from PDFs and DOCX documents", async () => {
const root = await temporaryDirectory();
await writeFile(
Expand Down