Files
Tom Moor ff3b3ce552 fix: Allow empty string in optional MCP fields (#12310)
* fix: Allow empty string in optional fields

* fix: Preserve empty strings for content fields in MCP tools

Address review feedback by reverting content/text fields (description, document
text, comment text) back to z.string().optional() so callers can intentionally
clear values via "". optionalString() is reserved for identifier and query
fields where "" is not a meaningful input.
2026-05-10 10:47:24 -04:00

183 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CollectionPermission, type NavigationNode } from "@shared/types";
import {
buildCollection,
buildDocument,
buildTeam,
buildUser,
} from "@server/test/factories";
import {
buildBreadcrumb,
getBreadcrumbsForDocuments,
optionalString,
} from "./util";
const node = (
id: string,
title: string,
children: NavigationNode[] = []
): NavigationNode => ({
id,
title,
url: `/doc/${id}`,
children,
});
describe("buildBreadcrumb", () => {
const structure: NavigationNode[] = [
node("a", "Onboarding", [
node("b", "Setup guide", [node("c", "Database")]),
node("d", "Glossary"),
]),
node("e", "Architecture"),
];
it("returns just the collection name for a root-level document", () => {
expect(buildBreadcrumb("a", structure, "Engineering")).toBe("Engineering");
expect(buildBreadcrumb("e", structure, "Engineering")).toBe("Engineering");
});
it("includes ancestor titles for a nested document", () => {
expect(buildBreadcrumb("b", structure, "Engineering")).toBe(
"Engineering Onboarding"
);
expect(buildBreadcrumb("c", structure, "Engineering")).toBe(
"Engineering Onboarding Setup guide"
);
});
it("excludes the document's own title from the path", () => {
const result = buildBreadcrumb("c", structure, "Engineering");
expect(result).not.toContain("Database");
});
it("falls back to the collection name when the document is not in the structure", () => {
expect(buildBreadcrumb("missing", structure, "Engineering")).toBe(
"Engineering"
);
});
it("returns just the collection name when the structure is null", () => {
expect(buildBreadcrumb("a", null, "Engineering")).toBe("Engineering");
expect(buildBreadcrumb("a", undefined, "Engineering")).toBe("Engineering");
});
it("returns just the collection name when the structure is empty", () => {
expect(buildBreadcrumb("a", [], "Engineering")).toBe("Engineering");
});
});
describe("optionalString", () => {
const schema = optionalString();
it("returns undefined when input is omitted", () => {
expect(schema.parse(undefined)).toBeUndefined();
});
it("coerces an empty string to undefined", () => {
expect(schema.parse("")).toBeUndefined();
});
it("passes through a non-empty string", () => {
expect(schema.parse("hello")).toBe("hello");
});
it("preserves whitespace-only strings", () => {
expect(schema.parse(" ")).toBe(" ");
});
});
describe("getBreadcrumbsForDocuments", () => {
it("returns the collection name for a root-level document", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
permission: CollectionPermission.ReadWrite,
name: "Engineering",
});
const doc = await buildDocument({
teamId: team.id,
collectionId: collection.id,
});
const result = await getBreadcrumbsForDocuments([doc], user);
expect(result.get(doc.id)).toBe("Engineering");
});
it("includes ancestor titles for a nested document", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
permission: CollectionPermission.ReadWrite,
name: "Engineering",
});
const parent = await buildDocument({
teamId: team.id,
collectionId: collection.id,
title: "Onboarding",
});
const child = await buildDocument({
teamId: team.id,
collectionId: collection.id,
parentDocumentId: parent.id,
});
const result = await getBreadcrumbsForDocuments([child], user);
expect(result.get(child.id)).toBe("Engineering Onboarding");
});
it("omits documents whose collection the user cannot read", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
permission: null,
name: "Secrets",
});
const doc = await buildDocument({
teamId: team.id,
collectionId: collection.id,
});
const result = await getBreadcrumbsForDocuments([doc], user);
expect(result.has(doc.id)).toBe(false);
});
it("returns an empty map for empty input", async () => {
const user = await buildUser();
const result = await getBreadcrumbsForDocuments([], user);
expect(result.size).toBe(0);
});
it("omits documents that have no collection", async () => {
const user = await buildUser();
const result = await getBreadcrumbsForDocuments(
[{ id: "doc-without-collection", collectionId: null }],
user
);
expect(result.has("doc-without-collection")).toBe(false);
});
it("resolves breadcrumbs across multiple collections in one call", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const c1 = await buildCollection({
teamId: team.id,
permission: CollectionPermission.ReadWrite,
name: "One",
});
const c2 = await buildCollection({
teamId: team.id,
permission: CollectionPermission.ReadWrite,
name: "Two",
});
const d1 = await buildDocument({ teamId: team.id, collectionId: c1.id });
const d2 = await buildDocument({ teamId: team.id, collectionId: c2.id });
const result = await getBreadcrumbsForDocuments([d1, d2], user);
expect(result.get(d1.id)).toBe("One");
expect(result.get(d2.id)).toBe("Two");
});
});