@@ -9,15 +9,21 @@ const { mockAssertConnectorSyncPayload, mockExecuteSync, mockTask } = vi.hoisted
99 mockTask : vi . fn ( ( config ) => config ) ,
1010} ) )
1111
12- vi . mock ( '@trigger.dev/sdk' , ( ) => ( { task : mockTask } ) )
12+ vi . mock ( '@trigger.dev/sdk' , ( ) => ( {
13+ task : mockTask ,
14+ AbortTaskRunError : class AbortTaskRunError extends Error { } ,
15+ } ) )
1316vi . mock ( '@/lib/knowledge/connectors/queue' , ( ) => ( {
1417 assertConnectorSyncPayload : mockAssertConnectorSyncPayload ,
1518} ) )
1619vi . mock ( '@/lib/knowledge/connectors/sync-engine' , ( ) => ( {
1720 executeSync : mockExecuteSync ,
1821} ) )
1922
20- import { executeConnectorSyncJob } from '@/background/knowledge-connector-sync'
23+ import {
24+ classifyConnectorSyncResult ,
25+ executeConnectorSyncJob ,
26+ } from '@/background/knowledge-connector-sync'
2127
2228const BILLING_ATTRIBUTION = {
2329 actorUserId : 'external-admin' ,
@@ -40,7 +46,9 @@ describe('knowledge connector sync worker', () => {
4046 docsUpdated : 0 ,
4147 docsDeleted : 0 ,
4248 docsUnchanged : 0 ,
49+ docsSkipped : 0 ,
4350 docsFailed : 0 ,
51+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
4452 } )
4553 } )
4654
@@ -103,4 +111,148 @@ describe('knowledge connector sync worker', () => {
103111 dispatchToken : undefined ,
104112 } )
105113 } )
114+
115+ it ( 'fails visibly without retrying an already-persisted partial sync' , async ( ) => {
116+ mockAssertConnectorSyncPayload . mockReturnValue ( {
117+ connectorId : 'connector-1' ,
118+ requestId : 'request-1' ,
119+ billingAttribution : BILLING_ATTRIBUTION ,
120+ } )
121+ mockExecuteSync . mockResolvedValue ( {
122+ docsAdded : 2 ,
123+ docsUpdated : 0 ,
124+ docsDeleted : 0 ,
125+ docsUnchanged : 0 ,
126+ docsSkipped : 3 ,
127+ docsFailed : 1 ,
128+ processingDispatch : { requested : 2 , accepted : 1 , failed : 1 } ,
129+ } )
130+
131+ await expect (
132+ executeConnectorSyncJob ( {
133+ connectorId : 'connector-1' ,
134+ requestId : 'request-1' ,
135+ billingAttribution : BILLING_ATTRIBUTION ,
136+ } )
137+ ) . rejects . toThrow ( 'Connector sync partially failed' )
138+ } )
139+
140+ it ( 'does not turn intentionally skipped source files into a task failure' , ( ) => {
141+ expect (
142+ classifyConnectorSyncResult ( {
143+ docsAdded : 0 ,
144+ docsUpdated : 0 ,
145+ docsDeleted : 0 ,
146+ docsUnchanged : 0 ,
147+ docsSkipped : 4 ,
148+ docsFailed : 0 ,
149+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
150+ } )
151+ ) . toBe ( 'completed' )
152+ } )
153+
154+ it ( 'reports superseded and non-runnable jobs as skipped control flow' , ( ) => {
155+ const baseResult = {
156+ docsAdded : 0 ,
157+ docsUpdated : 0 ,
158+ docsDeleted : 0 ,
159+ docsUnchanged : 0 ,
160+ docsSkipped : 0 ,
161+ docsFailed : 0 ,
162+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
163+ }
164+
165+ expect ( classifyConnectorSyncResult ( { ...baseResult , skipReason : 'sync_superseded' } ) ) . toBe (
166+ 'skipped'
167+ )
168+ expect (
169+ classifyConnectorSyncResult ( { ...baseResult , skipReason : 'connector_not_syncable' } )
170+ ) . toBe ( 'skipped' )
171+ } )
172+
173+ it ( 'never treats a provider error that collides with a skip reason as control flow' , ( ) => {
174+ expect (
175+ classifyConnectorSyncResult ( {
176+ docsAdded : 0 ,
177+ docsUpdated : 0 ,
178+ docsDeleted : 0 ,
179+ docsUnchanged : 0 ,
180+ docsSkipped : 0 ,
181+ docsFailed : 0 ,
182+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
183+ error : 'sync_in_progress' ,
184+ } )
185+ ) . toBe ( 'failed' )
186+ } )
187+
188+ it ( 'classifies a persisted connector error as a failed task' , ( ) => {
189+ expect (
190+ classifyConnectorSyncResult ( {
191+ docsAdded : 0 ,
192+ docsUpdated : 0 ,
193+ docsDeleted : 0 ,
194+ docsUnchanged : 0 ,
195+ docsSkipped : 0 ,
196+ docsFailed : 0 ,
197+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
198+ error : 'provider unavailable' ,
199+ } )
200+ ) . toBe ( 'failed' )
201+ } )
202+
203+ it ( 'fails the Trigger run for a persisted connector error without a whole-sync retry' , async ( ) => {
204+ mockAssertConnectorSyncPayload . mockReturnValue ( {
205+ connectorId : 'connector-1' ,
206+ requestId : 'request-1' ,
207+ billingAttribution : BILLING_ATTRIBUTION ,
208+ } )
209+ mockExecuteSync . mockResolvedValue ( {
210+ docsAdded : 0 ,
211+ docsUpdated : 0 ,
212+ docsDeleted : 0 ,
213+ docsUnchanged : 0 ,
214+ docsSkipped : 0 ,
215+ docsFailed : 0 ,
216+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
217+ error : 'provider unavailable' ,
218+ } )
219+
220+ await expect (
221+ executeConnectorSyncJob ( {
222+ connectorId : 'connector-1' ,
223+ requestId : 'request-1' ,
224+ billingAttribution : BILLING_ATTRIBUTION ,
225+ } )
226+ ) . rejects . toThrow ( 'Connector sync failed for connector-1: provider unavailable' )
227+ } )
228+
229+ it ( 'returns an explicit skipped outcome for a superseded task' , async ( ) => {
230+ mockAssertConnectorSyncPayload . mockReturnValue ( {
231+ connectorId : 'connector-1' ,
232+ requestId : 'request-1' ,
233+ billingAttribution : BILLING_ATTRIBUTION ,
234+ } )
235+ mockExecuteSync . mockResolvedValue ( {
236+ docsAdded : 0 ,
237+ docsUpdated : 0 ,
238+ docsDeleted : 0 ,
239+ docsUnchanged : 0 ,
240+ docsSkipped : 0 ,
241+ docsFailed : 0 ,
242+ processingDispatch : { requested : 0 , accepted : 0 , failed : 0 } ,
243+ skipReason : 'dispatch_superseded' ,
244+ } )
245+
246+ await expect (
247+ executeConnectorSyncJob ( {
248+ connectorId : 'connector-1' ,
249+ requestId : 'request-1' ,
250+ billingAttribution : BILLING_ATTRIBUTION ,
251+ } )
252+ ) . resolves . toMatchObject ( {
253+ success : false ,
254+ outcome : 'skipped' ,
255+ skipReason : 'dispatch_superseded' ,
256+ } )
257+ } )
106258} )
0 commit comments