diff --git a/apps/web/hooks/use-document-mutations.test.ts b/apps/web/hooks/use-document-mutations.test.ts new file mode 100644 index 00000000..1ec4f211 --- /dev/null +++ b/apps/web/hooks/use-document-mutations.test.ts @@ -0,0 +1,177 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test" + +type SpaceSettingsResult = + | { containerTag: string; entityContext: string | null; name: string | null } + | Error + +type DocumentRequest = { + body?: Record +} + +let spaceSettingsResult: SpaceSettingsResult = new Error( + "space settings result not configured", +) +const documentRequests: { route: string; options: DocumentRequest }[] = [] +const fileRequestBodies: (FormData | null)[] = [] + +const fetchQuery = mock(async () => { + if (spaceSettingsResult instanceof Error) throw spaceSettingsResult + return spaceSettingsResult +}) + +const originalFetch = globalThis.fetch +const uploadFetch = mock( + async ( + _input: Parameters[0], + init?: Parameters[1], + ): Promise => { + fileRequestBodies.push(init?.body instanceof FormData ? init.body : null) + return Response.json({ id: "document-1" }) + }, +) +globalThis.fetch = Object.assign(uploadFetch, { + preconnect: originalFetch.preconnect, +}) + +mock.module("@tanstack/react-query", () => ({ + useMutation: (options: { + mutationFn: (variables: unknown) => Promise + }) => ({ mutateAsync: options.mutationFn }), + useQuery: () => ({}), + useQueryClient: () => ({ fetchQuery }), +})) + +mock.module("sonner", () => ({ + toast: { + error: () => {}, + success: () => {}, + warning: () => {}, + }, +})) + +mock.module("@lib/api", () => ({ + $fetch: async (route: string, options: DocumentRequest = {}) => { + documentRequests.push({ route, options }) + return { data: { id: "document-1" }, error: null } + }, +})) + +mock.module("@lib/auth-context", () => ({ + useAuth: () => ({ user: { name: "Ada" } }), +})) + +mock.module("@/lib/analytics", () => ({ + analytics: { + documentAdded: () => {}, + documentDeleted: () => {}, + documentEdited: () => {}, + documentsBulkDeleted: () => {}, + }, +})) + +const { useDocumentMutations } = await import("./use-document-mutations") + +afterAll(() => { + globalThis.fetch = originalFetch + mock.restore() +}) + +const defaultEntityContext = + "This is Ada, saving items in a personal knowledge management system. This may be websites, links, notes, journals, PDFs, etc. Understand the user from it into a graph." + +async function saveNote() { + const { noteMutation } = useDocumentMutations() + await noteMutation.mutateAsync({ content: "A note", project: "space-1" }) + return documentRequests[0] +} + +async function uploadFile() { + const { fileMutation } = useDocumentMutations() + await fileMutation.mutateAsync({ + fileEntries: [ + { + id: "file-1", + file: new File(["contents"], "note.txt", { type: "text/plain" }), + }, + ], + project: "space-1", + }) + return fileRequestBodies[0] +} + +describe("document mutation entity context", () => { + beforeEach(() => { + spaceSettingsResult = new Error("space settings result not configured") + documentRequests.length = 0 + fileRequestBodies.length = 0 + fetchQuery.mockClear() + uploadFetch.mockClear() + }) + + it("omits the default when the space has a configured context", async () => { + spaceSettingsResult = { + containerTag: "space-1", + entityContext: "Remember work decisions", + name: "Work", + } + + const request = await saveNote() + + expect(request).toEqual({ + route: "@post/documents", + options: { + body: { + content: "A note", + containerTags: ["space-1"], + metadata: { sm_source: "consumer" }, + }, + }, + }) + expect(fetchQuery).toHaveBeenCalledTimes(1) + }) + + it("includes the default after settings confirm the context is empty", async () => { + spaceSettingsResult = { + containerTag: "space-1", + entityContext: null, + name: "Personal", + } + + const request = await saveNote() + + expect(request?.options.body).toEqual({ + content: "A note", + containerTags: ["space-1"], + entityContext: defaultEntityContext, + metadata: { sm_source: "consumer" }, + }) + }) + + it("still saves but omits the context when settings cannot be read", async () => { + spaceSettingsResult = new Error("settings unavailable") + + const request = await saveNote() + + expect(request).toEqual({ + route: "@post/documents", + options: { + body: { + content: "A note", + containerTags: ["space-1"], + metadata: { sm_source: "consumer" }, + }, + }, + }) + expect(fetchQuery).toHaveBeenCalledTimes(1) + }) + + it("omits the context from file uploads when settings cannot be read", async () => { + spaceSettingsResult = new Error("settings unavailable") + + const formData = await uploadFile() + + expect(formData?.get("containerTags")).toBe('["space-1"]') + expect(formData?.get("entityContext")).toBeNull() + expect(uploadFetch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/web/hooks/use-document-mutations.ts b/apps/web/hooks/use-document-mutations.ts index 7f0e6384..a467e103 100644 --- a/apps/web/hooks/use-document-mutations.ts +++ b/apps/web/hooks/use-document-mutations.ts @@ -230,7 +230,7 @@ export function useDocumentMutations({ const defaultEntityContext = `This is ${user?.name ?? "a user"}, saving items in a personal knowledge management system. This may be websites, links, notes, journals, PDFs, etc. Understand the user from it into a graph.` - // Skip when the space has its own context — sending one would overwrite the stored value. + // Only send the default after settings confirm the context is empty; otherwise it could overwrite the stored value. const resolveEntityContext = async ( project: string, ): Promise => { @@ -242,7 +242,7 @@ export function useDocumentMutations({ }) return settings?.entityContext ? undefined : defaultEntityContext } catch { - return defaultEntityContext + return undefined } }