@@ -3,11 +3,13 @@ import { GroupPermissionResource } from "@app/lib/resources/group_permission_res
33import { withTransaction } from "@app/lib/utils/sql_utils" ;
44import baseLogger from "@app/logger/logger" ;
55import { restoreEditorlessAgentAuthors } from "@app/migrations/20260922_restore_editorless_agent_authors" ;
6+ import * as searchIndexationClient from "@app/temporal/es_indexation/client" ;
67import { AgentConfigurationFactory } from "@app/tests/utils/AgentConfigurationFactory" ;
78import { createResourceTest } from "@app/tests/utils/generic_resource_tests" ;
89import { MembershipFactory } from "@app/tests/utils/MembershipFactory" ;
910import { UserFactory } from "@app/tests/utils/UserFactory" ;
1011import type { AgentConfigurationType } from "@app/types/assistant/agent" ;
12+ import { Err , Ok } from "@app/types/shared/result" ;
1113import type { UserType } from "@app/types/user" ;
1214import assert from "assert" ;
1315import { afterEach , describe , expect , it , vi } from "vitest" ;
@@ -50,8 +52,8 @@ async function editorIds(
5052describe ( "restoreEditorlessAgentAuthors" , ( ) => {
5153 it ( "restores only editorless agents, with a dry run and idempotent rerun" , async ( ) => {
5254 const launchSearchIndexation = vi
53- . spyOn ( AgentResource , "launchSearchIndexation " )
54- . mockResolvedValue ( ) ;
55+ . spyOn ( searchIndexationClient , "launchIndexAgentSearchWorkflow " )
56+ . mockResolvedValue ( new Ok ( undefined ) ) ;
5557 const {
5658 authenticator : auth ,
5759 user,
@@ -117,10 +119,10 @@ describe("restoreEditorlessAgentAuthors", () => {
117119 otherEditor . sId ,
118120 ] ) ;
119121 expect ( await editorIds ( auth , resourceWithGlobalEditor ) ) . toEqual ( [ ] ) ;
120- expect ( launchSearchIndexation ) . toHaveBeenCalledExactlyOnceWith (
121- expect . anything ( ) ,
122- [ editorlessAgent . sId ]
123- ) ;
122+ expect ( launchSearchIndexation ) . toHaveBeenCalledExactlyOnceWith ( {
123+ workspaceId : workspace . sId ,
124+ agentId : editorlessAgent . sId ,
125+ } ) ;
124126
125127 await expect (
126128 restoreEditorlessAgentAuthors ( {
@@ -135,6 +137,95 @@ describe("restoreEditorlessAgentAuthors", () => {
135137 } ) ;
136138 } ) ;
137139
140+ it ( "retries failed search workflow launches" , async ( ) => {
141+ const launchSearchIndexation = vi
142+ . spyOn ( searchIndexationClient , "launchIndexAgentSearchWorkflow" )
143+ . mockResolvedValue ( new Ok ( undefined ) ) ;
144+ const { authenticator : auth , workspace } = await createResourceTest ( {
145+ role : "admin" ,
146+ } ) ;
147+ const agent = await AgentConfigurationFactory . createTestAgent ( auth , {
148+ name : "Editorless agent" ,
149+ } ) ;
150+ await replaceEditors ( auth , agent , [ ] ) ;
151+ launchSearchIndexation . mockReset ( ) ;
152+ launchSearchIndexation
153+ . mockResolvedValueOnce ( new Err ( new Error ( "Temporal unavailable" ) ) )
154+ . mockResolvedValueOnce ( new Err ( new Error ( "Temporal unavailable" ) ) )
155+ . mockResolvedValueOnce ( new Ok ( undefined ) ) ;
156+
157+ await restoreEditorlessAgentAuthors ( {
158+ execute : true ,
159+ logger,
160+ wId : workspace . sId ,
161+ } ) ;
162+
163+ expect ( launchSearchIndexation ) . toHaveBeenCalledTimes ( 3 ) ;
164+ } ) ;
165+
166+ it ( "waits for every in-flight repair before propagating a failure" , async ( ) => {
167+ vi . spyOn (
168+ searchIndexationClient ,
169+ "launchIndexAgentSearchWorkflow"
170+ ) . mockResolvedValue ( new Ok ( undefined ) ) ;
171+ const {
172+ authenticator : auth ,
173+ user,
174+ workspace,
175+ } = await createResourceTest ( { role : "admin" } ) ;
176+ const firstAgent = await AgentConfigurationFactory . createTestAgent ( auth , {
177+ name : "First editorless agent" ,
178+ } ) ;
179+ const secondAgent = await AgentConfigurationFactory . createTestAgent ( auth , {
180+ name : "Second editorless agent" ,
181+ } ) ;
182+ const firstResource = await replaceEditors ( auth , firstAgent , [ ] ) ;
183+ const secondResource = await replaceEditors ( auth , secondAgent , [ ] ) ;
184+
185+ const originalGrantToUser = GroupPermissionResource . grantToUser ;
186+ let releaseSecondGrant : ( ) => void = ( ) => undefined ;
187+ const secondGrantGate = new Promise < void > ( ( resolve ) => {
188+ releaseSecondGrant = resolve ;
189+ } ) ;
190+ let markSecondGrantStarted : ( ) => void = ( ) => undefined ;
191+ const secondGrantStarted = new Promise < void > ( ( resolve ) => {
192+ markSecondGrantStarted = resolve ;
193+ } ) ;
194+ vi . spyOn ( GroupPermissionResource , "grantToUser" ) . mockImplementation (
195+ async ( grantAuth , grant ) => {
196+ if ( grant . resourceId === firstResource . id ) {
197+ throw new Error ( "Failed first repair" ) ;
198+ }
199+ if ( grant . resourceId === secondResource . id ) {
200+ markSecondGrantStarted ( ) ;
201+ await secondGrantGate ;
202+ }
203+ return originalGrantToUser . call (
204+ GroupPermissionResource ,
205+ grantAuth ,
206+ grant
207+ ) ;
208+ }
209+ ) ;
210+
211+ let settled = false ;
212+ const run = restoreEditorlessAgentAuthors ( {
213+ execute : true ,
214+ logger,
215+ wId : workspace . sId ,
216+ } ) . finally ( ( ) => {
217+ settled = true ;
218+ } ) ;
219+ await secondGrantStarted ;
220+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
221+ expect ( settled ) . toBe ( false ) ;
222+
223+ releaseSecondGrant ( ) ;
224+ await expect ( run ) . rejects . toThrow ( "Failed first repair" ) ;
225+ expect ( await editorIds ( auth , firstResource ) ) . toEqual ( [ ] ) ;
226+ expect ( await editorIds ( auth , secondResource ) ) . toEqual ( [ user . sId ] ) ;
227+ } ) ;
228+
138229 it ( "rejects an unknown workspace scope" , async ( ) => {
139230 await expect (
140231 restoreEditorlessAgentAuthors ( {
0 commit comments