11import { beforeEach , describe , expect , test , vi } from "vitest"
22
33const mocks = vi . hoisted ( ( ) => ( {
4- auditRecord : vi . fn ( ) ,
5- deleteObject : vi . fn ( ) ,
6- findFirstGemini : vi . fn ( ) ,
7- findFirstOpenai : vi . fn ( ) ,
8- findOrFail : vi . fn ( ) ,
9- insertReturning : vi . fn ( ) ,
10- loggerWarn : vi . fn ( ) ,
11- queueAdd : vi . fn ( ) ,
12- txDeleteWhere : vi . fn ( ) ,
4+ create : vi . fn ( ) ,
5+ delete : vi . fn ( ) ,
136} ) )
147
158vi . mock ( "@/lib/safe-action" , ( ) => {
@@ -24,33 +17,35 @@ vi.mock("@/features/common/schema", () => ({
2417 workspaceIdrequestParams : [ ] ,
2518} ) )
2619
27- vi . mock ( "@/lib/log" , ( ) => ( {
28- logger : { warn : mocks . loggerWarn } ,
29- } ) )
30-
31- vi . mock ( "@chatbotx.io/business/audit" , ( ) => ( {
32- auditService : { record : mocks . auditRecord } ,
20+ vi . mock ( "@chatbotx.io/business" , ( ) => ( {
21+ aiFileService : {
22+ create : mocks . create ,
23+ delete : mocks . delete ,
24+ } ,
3325} ) )
3426
3527vi . mock ( "@chatbotx.io/business/errors" , ( ) => ( {
36- ChatbotXException : class ChatbotXException extends Error { } ,
37- } ) )
38-
39- vi . mock ( "@chatbotx.io/filesystem" , ( ) => ( {
40- uploader : { deleteObject : mocks . deleteObject } ,
28+ ChatbotXException : class ChatbotXException extends Error {
29+ code = "systemError"
30+ httpStatusCode = 400
31+
32+ constructor ( message : string , code ?: string , httpStatusCode ?: number ) {
33+ super ( message )
34+ this . name = "ChatbotXException"
35+ if ( code ) {
36+ this . code = code
37+ }
38+ if ( httpStatusCode ) {
39+ this . httpStatusCode = httpStatusCode
40+ }
41+ }
42+ } ,
4143} ) )
4244
4345vi . mock ( "@chatbotx.io/utils" , ( ) => ( {
44- createId : ( ) => "file-1" ,
4546 zodBigintAsString : ( ) => "mocked-schema" ,
4647} ) )
4748
48- vi . mock ( "@chatbotx.io/worker-config" , ( ) => ( {
49- HeavyJobAction : { processAIFile : "processAIFile" } ,
50- getHeavyJobOptions : ( ) => ( { } ) ,
51- heavyQueue : { add : mocks . queueAdd } ,
52- } ) )
53-
5449vi . mock ( "next-intl/server" , ( ) => ( {
5550 getTranslations : vi . fn ( async ( ) => ( key : string ) => key ) ,
5651} ) )
@@ -59,33 +54,11 @@ vi.mock("../src/features/ai-files/schema", () => ({
5954 createAIFileRequest : { } ,
6055} ) )
6156
62- vi . mock ( "@chatbotx.io/database/schema" , ( ) => ( {
63- aiEmbeddingModel : { id : "id" } ,
64- aiFileModel : { id : "id" } ,
65- } ) )
66-
67- vi . mock ( "@chatbotx.io/database/client" , ( ) => ( {
68- db : {
69- delete : vi . fn ( ( ) => ( { where : mocks . txDeleteWhere } ) ) ,
70- insert : vi . fn ( ( ) => ( {
71- values : vi . fn ( ( ) => ( { returning : mocks . insertReturning } ) ) ,
72- } ) ) ,
73- query : {
74- integrationGeminiModel : { findFirst : mocks . findFirstGemini } ,
75- integrationOpenaiModel : { findFirst : mocks . findFirstOpenai } ,
76- } ,
77- transaction : vi . fn ( async ( callback : ( tx : unknown ) => unknown ) =>
78- callback ( { delete : vi . fn ( ( ) => ( { where : mocks . txDeleteWhere } ) ) } ) ,
79- ) ,
80- } ,
81- eq : vi . fn ( ( field : unknown , value : unknown ) => ( { field, value } ) ) ,
82- findOrFail : mocks . findOrFail ,
83- } ) )
84-
57+ const { ChatbotXException } = await import ( "@chatbotx.io/business/errors" )
8558const { createAIFileAction } = await import (
8659 "@/features/ai-files/actions/create-ai-file.action"
8760)
88- const { deleteAIFile } = await import (
61+ const { deleteAIFileAction } = await import (
8962 "@/features/ai-files/actions/delete-ai-file.action"
9063)
9164
@@ -98,74 +71,97 @@ const workspaceId = "workspace-1"
9871
9972beforeEach ( ( ) => {
10073 vi . clearAllMocks ( )
101- mocks . findFirstOpenai . mockResolvedValue ( { id : "openai-1" } )
102- mocks . findFirstGemini . mockResolvedValue ( undefined )
103- mocks . insertReturning . mockResolvedValue ( [ { id : "file-1" } ] )
104- mocks . findOrFail . mockResolvedValue ( { id : "file-1" , path : "path/to/file" } )
10574} )
10675
107- describe ( "Knowledge tab audit messages" , ( ) => {
108- test ( "allows creating a Knowledge with Gemini as the only provider" , async ( ) => {
109- mocks . findFirstOpenai . mockResolvedValue ( undefined )
110- mocks . findFirstGemini . mockResolvedValue ( { id : "gemini-1" } )
111-
112- await (
113- createAIFileAction as unknown as ActionHandler < { name : string } , [ string ] >
114- ) ( {
115- parsedInput : { name : "manual.pdf" } ,
116- bindArgsParsedInputs : [ workspaceId ] ,
117- } )
118-
119- expect ( mocks . insertReturning ) . toHaveBeenCalled ( )
120- expect ( mocks . queueAdd ) . toHaveBeenCalled ( )
121- } )
76+ describe ( "createAIFileAction" , ( ) => {
77+ test ( "forwards workspaceId + parsedInput to aiFileService.create" , async ( ) => {
78+ mocks . create . mockResolvedValue ( { id : "file-1" } )
12279
123- test ( "createAIFileAction logs created a new Knowledge by id" , async ( ) => {
12480 await (
125- createAIFileAction as unknown as ActionHandler < { name : string } , [ string ] >
81+ createAIFileAction as unknown as ActionHandler <
82+ { name : string ; path : string ; mimeType : string ; size : number } ,
83+ [ string ]
84+ >
12685 ) ( {
127- parsedInput : { name : "manual.pdf" } ,
86+ parsedInput : {
87+ name : "manual.pdf" ,
88+ path : "path/to/file" ,
89+ mimeType : "application/pdf" ,
90+ size : 100 ,
91+ } ,
12892 bindArgsParsedInputs : [ workspaceId ] ,
12993 } )
13094
131- expect ( mocks . auditRecord ) . toHaveBeenCalledWith ( {
95+ expect ( mocks . create ) . toHaveBeenCalledWith ( {
13296 workspaceId,
133- action : "create" ,
134- detail : "created a new Knowledge (#file-1)" ,
97+ name : "manual.pdf" ,
98+ path : "path/to/file" ,
99+ mimeType : "application/pdf" ,
100+ size : 100 ,
135101 } )
136- expect ( mocks . queueAdd ) . toHaveBeenCalledWith (
137- "processAIFile" ,
138- {
139- type : "processAIFile" ,
140- data : { aiFileId : "file-1" } ,
141- } ,
142- { jobId : "heavy-ai-file-file-1" } ,
143- )
144102 } )
145103
146- test ( "deleteAIFile logs deleted a Knowledge by id" , async ( ) => {
147- await deleteAIFile ( { workspaceId, id : "file-1" } )
104+ test ( "translates a noEmbeddingProvider service error" , async ( ) => {
105+ mocks . create . mockRejectedValue (
106+ new ChatbotXException (
107+ "AI file requires an embedding provider" ,
108+ "noEmbeddingProvider" ,
109+ 400 ,
110+ ) ,
111+ )
148112
149- expect ( mocks . auditRecord ) . toHaveBeenCalledWith ( {
150- workspaceId,
151- action : "delete" ,
152- detail : "deleted a Knowledge (#file-1)" ,
153- } )
113+ await expect (
114+ (
115+ createAIFileAction as unknown as ActionHandler <
116+ { name : string ; path : string ; mimeType : string ; size : number } ,
117+ [ string ]
118+ >
119+ ) ( {
120+ parsedInput : {
121+ name : "manual.pdf" ,
122+ path : "path/to/file" ,
123+ mimeType : "application/pdf" ,
124+ size : 100 ,
125+ } ,
126+ bindArgsParsedInputs : [ workspaceId ] ,
127+ } ) ,
128+ ) . rejects . toMatchObject ( { message : "noEmbeddingProvider" } )
129+ } )
130+
131+ test ( "rethrows other service errors untranslated" , async ( ) => {
132+ mocks . create . mockRejectedValue ( new Error ( "db exploded" ) )
133+
134+ await expect (
135+ (
136+ createAIFileAction as unknown as ActionHandler <
137+ { name : string ; path : string ; mimeType : string ; size : number } ,
138+ [ string ]
139+ >
140+ ) ( {
141+ parsedInput : {
142+ name : "manual.pdf" ,
143+ path : "path/to/file" ,
144+ mimeType : "application/pdf" ,
145+ size : 100 ,
146+ } ,
147+ bindArgsParsedInputs : [ workspaceId ] ,
148+ } ) ,
149+ ) . rejects . toMatchObject ( { message : "db exploded" } )
154150 } )
151+ } )
155152
156- test ( "does not log the legacy AI Agent knowledge base message" , async ( ) => {
153+ describe ( "deleteAIFileAction" , ( ) => {
154+ test ( "forwards workspaceId + id to aiFileService.delete" , async ( ) => {
157155 await (
158- createAIFileAction as unknown as ActionHandler < { name : string } , [ string ] >
156+ deleteAIFileAction as unknown as ActionHandler <
157+ undefined ,
158+ [ string , string ]
159+ >
159160 ) ( {
160- parsedInput : { name : "manual.pdf" } ,
161- bindArgsParsedInputs : [ workspaceId ] ,
161+ parsedInput : undefined ,
162+ bindArgsParsedInputs : [ workspaceId , "file-1" ] ,
162163 } )
163- await deleteAIFile ( { workspaceId, id : "file-1" } )
164164
165- for ( const call of mocks . auditRecord . mock . calls ) {
166- expect ( call [ 0 ] . detail ) . not . toContain (
167- "updated the AI Agent knowledge base" ,
168- )
169- }
165+ expect ( mocks . delete ) . toHaveBeenCalledWith ( { workspaceId, id : "file-1" } )
170166 } )
171167} )
0 commit comments