@@ -46,8 +46,8 @@ test.describe('Neo.ai.services.github-workflow.toolService — sync_all dev-bran
4646 } ) ;
4747
4848 test ( 'sync_all delegates to SyncService.runFullSync when on dev' , async ( ) => {
49- let delegateCalls = 0 ;
50- const delegate = async ( ...args ) => {
49+ let delegateCalls = 0 ;
50+ const delegate = async ( ...args ) => {
5151 delegateCalls ++ ;
5252 return { message : 'sync ok' , args} ;
5353 } ;
@@ -60,33 +60,33 @@ test.describe('Neo.ai.services.github-workflow.toolService — sync_all dev-bran
6060 } ) ;
6161
6262 test ( 'sync_all REJECTS when on a feature branch (no delegate call)' , async ( ) => {
63- let delegateCalls = 0 ;
64- const delegate = async ( ) => { delegateCalls ++ ; return { message : 'should not run' } ; } ;
65- const guarded = buildDevBranchGuard ( delegate , async ( ) => 'agent/some-feature-branch' ) ;
63+ let delegateCalls = 0 ;
64+ const delegate = async ( ) => { delegateCalls ++ ; return { message : 'should not run' } ; } ;
65+ const guarded = buildDevBranchGuard ( delegate , async ( ) => 'agent/some-feature-branch' ) ;
6666
6767 await expect ( guarded ( ) ) . rejects . toThrow ( / s y n c _ a l l R E J E C T E D .* ' a g e n t \/ s o m e - f e a t u r e - b r a n c h ' .* n o t ' d e v ' / ) ;
6868 expect ( delegateCalls ) . toBe ( 0 ) ;
6969 } ) ;
7070
7171 test ( 'sync_all REJECTS when on main (no delegate call)' , async ( ) => {
72- let delegateCalls = 0 ;
73- const delegate = async ( ) => { delegateCalls ++ ; } ;
74- const guarded = buildDevBranchGuard ( delegate , async ( ) => 'main' ) ;
72+ let delegateCalls = 0 ;
73+ const delegate = async ( ) => { delegateCalls ++ ; } ;
74+ const guarded = buildDevBranchGuard ( delegate , async ( ) => 'main' ) ;
7575
7676 await expect ( guarded ( ) ) . rejects . toThrow ( / s y n c _ a l l R E J E C T E D .* ' m a i n ' .* n o t ' d e v ' / ) ;
7777 expect ( delegateCalls ) . toBe ( 0 ) ;
7878 } ) ;
7979
8080 test ( 'sync_all REJECTS on detached HEAD (empty branch name)' , async ( ) => {
8181 const delegate = async ( ) => { throw new Error ( 'should not run' ) ; } ;
82- const guarded = buildDevBranchGuard ( delegate , async ( ) => '' ) ;
82+ const guarded = buildDevBranchGuard ( delegate , async ( ) => '' ) ;
8383
8484 await expect ( guarded ( ) ) . rejects . toThrow ( / s y n c _ a l l R E J E C T E D .* ' \( d e t a c h e d \) ' / ) ;
8585 } ) ;
8686
8787 test ( 'sync_all REJECTS immediately on root mismatch from branch detector' , async ( ) => {
8888 const delegate = async ( ) => { throw new Error ( 'should not run' ) ; } ;
89- const guarded = buildDevBranchGuard ( delegate , async ( ) => {
89+ const guarded = buildDevBranchGuard ( delegate , async ( ) => {
9090 throw new Error ( 'sync_all REJECTED: Root mismatch. MCP server projectRoot...' ) ;
9191 } ) ;
9292
@@ -95,7 +95,7 @@ test.describe('Neo.ai.services.github-workflow.toolService — sync_all dev-bran
9595
9696 test ( 'sync_all REJECTS with git-error message when branch detector throws' , async ( ) => {
9797 const delegate = async ( ) => { throw new Error ( 'should not run' ) ; } ;
98- const guarded = buildDevBranchGuard ( delegate , async ( ) => {
98+ const guarded = buildDevBranchGuard ( delegate , async ( ) => {
9999 throw new Error ( 'git: not a git repository' ) ;
100100 } ) ;
101101
@@ -125,7 +125,7 @@ test.describe('Neo.ai.services.github-workflow.toolService — getConversationRo
125125 let originalPrGetConversation ;
126126
127127 test . beforeAll ( async ( ) => {
128- const mod = await import ( '../../../../../../ai/mcp/server/github-workflow/toolService.mjs' ) ;
128+ const mod = await import ( '../../../../../../ai/mcp/server/github-workflow/toolService.mjs' ) ;
129129 getConversationRouter = mod . getConversationRouter ;
130130 IssueService = ( await import ( '../../../../../../ai/services/github-workflow/IssueService.mjs' ) ) . default ;
131131 PullRequestService = ( await import ( '../../../../../../ai/services/github-workflow/PullRequestService.mjs' ) ) . default ;
@@ -237,8 +237,8 @@ test.describe('Neo.ai.services.github-workflow.toolService — write identity gu
237237 } ) ;
238238
239239 test ( 'delegates a public write when expected agent and viewer login match' , async ( ) => {
240- let delegateCalls = 0 ;
241- const guarded = buildGitHubWriteIdentityGuard ( async ( ...args ) => {
240+ let delegateCalls = 0 ;
241+ const guarded = buildGitHubWriteIdentityGuard ( async ( ...args ) => {
242242 delegateCalls ++ ;
243243 return { ok : true , args} ;
244244 } , {
@@ -256,28 +256,34 @@ test.describe('Neo.ai.services.github-workflow.toolService — write identity gu
256256 } ) ;
257257
258258 test ( 'rejects a public write on identity mismatch before delegate invocation' , async ( ) => {
259- let delegateCalls = 0 ;
260- const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
259+ let delegateCalls = 0 ;
260+ let assertionCalls = 0 ;
261+ const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
261262 delegateCalls ++ ;
262263 return { ok : true } ;
263264 } , {
264- assertExpectedIdentity : async ( ) => ( {
265- ok : false ,
266- reason : 'identity drift: authed as neo-opus-ada, expected neo-gpt' ,
267- code : 'LOGIN_MISMATCH'
268- } )
265+ assertExpectedIdentity : async ( ) => {
266+ assertionCalls ++ ;
267+ return {
268+ ok : false ,
269+ reason : 'identity drift: authed as neo-opus-ada, expected neo-gpt' ,
270+ code : 'LOGIN_MISMATCH'
271+ }
272+ }
269273 } ) ;
270274
271275 await expect ( guarded ( ) ) . rejects . toMatchObject ( {
272- code : 'GITHUB_IDENTITY_MISMATCH' ,
273- reason : 'identity drift: authed as neo-opus-ada, expected neo-gpt'
276+ code : 'GITHUB_IDENTITY_MISMATCH' ,
277+ identityClass : 'identity-mismatch' ,
278+ reason : 'identity drift: authed as neo-opus-ada, expected neo-gpt'
274279 } ) ;
275280 expect ( delegateCalls ) . toBe ( 0 ) ;
281+ expect ( assertionCalls ) . toBe ( 1 ) ;
276282 } ) ;
277283
278284 test ( 'rejects a public write when expected identity is unresolved' , async ( ) => {
279- let delegateCalls = 0 ;
280- const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
285+ let delegateCalls = 0 ;
286+ const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
281287 delegateCalls ++ ;
282288 } , {
283289 assertExpectedIdentity : async ( ) => ( {
@@ -293,28 +299,62 @@ test.describe('Neo.ai.services.github-workflow.toolService — write identity gu
293299 expect ( delegateCalls ) . toBe ( 0 ) ;
294300 } ) ;
295301
296- test ( 'rejects a public write when viewer login probe fails' , async ( ) => {
297- let delegateCalls = 0 ;
298- const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
302+ test ( 'retries transient empty-login resolution before delegating' , async ( ) => {
303+ let delegateCalls = 0 ;
304+ let assertionCalls = 0 ;
305+ const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
299306 delegateCalls ++ ;
307+ return { ok : true } ;
300308 } , {
301- assertExpectedIdentity : async ( ) => ( {
302- ok : false ,
303- reason : 'identity drift: no authed login resolved, expected neo-gpt' ,
304- code : 'NO_AUTHED_LOGIN'
305- } )
309+ assertExpectedIdentity : async ( ) => {
310+ assertionCalls ++ ;
311+ return assertionCalls === 1
312+ ? {
313+ ok : false ,
314+ reason : 'identity drift: no authed login resolved, expected neo-gpt' ,
315+ code : 'NO_AUTHED_LOGIN'
316+ }
317+ : {
318+ ok : true ,
319+ reason : null ,
320+ code : 'OK'
321+ }
322+ }
323+ } ) ;
324+
325+ await expect ( guarded ( ) ) . resolves . toEqual ( { ok : true } ) ;
326+ expect ( delegateCalls ) . toBe ( 1 ) ;
327+ expect ( assertionCalls ) . toBe ( 2 ) ;
328+ } ) ;
329+
330+ test ( 'rejects a public write when viewer login probe still fails after bounded retry' , async ( ) => {
331+ let delegateCalls = 0 ;
332+ let assertionCalls = 0 ;
333+ const guarded = buildGitHubWriteIdentityGuard ( async ( ) => {
334+ delegateCalls ++ ;
335+ } , {
336+ assertExpectedIdentity : async ( ) => {
337+ assertionCalls ++ ;
338+ return {
339+ ok : false ,
340+ reason : 'identity drift: no authed login resolved, expected neo-gpt' ,
341+ code : 'NO_AUTHED_LOGIN'
342+ }
343+ }
306344 } ) ;
307345
308346 await expect ( guarded ( ) ) . rejects . toMatchObject ( {
309- code : 'GITHUB_VIEWER_UNRESOLVED'
347+ code : 'GITHUB_VIEWER_UNRESOLVED' ,
348+ identityClass : 'identity-resolution-transient'
310349 } ) ;
311350 expect ( delegateCalls ) . toBe ( 0 ) ;
351+ expect ( assertionCalls ) . toBe ( 2 ) ;
312352 } ) ;
313353
314354 test ( 'guards public GitHub writes but leaves read and health tools untouched' , async ( ) => {
315355 const readHandler = async ( ) => ( { read : true } ) ;
316356 const writeHandler = async ( ) => ( { write : true } ) ;
317- const mapping = guardGitHubWriteTools ( {
357+ const mapping = guardGitHubWriteTools ( {
318358 get_conversation : readHandler ,
319359 healthcheck : readHandler ,
320360 manage_issue_comment : writeHandler ,
@@ -346,8 +386,8 @@ test.describe('Neo.ai.services.github-workflow.toolService — write identity gu
346386
347387 test ( 'rejects service mappings with unclassified future tools (#13252)' , ( ) => {
348388 expect ( ( ) => guardGitHubWriteTools ( {
349- get_conversation : async ( ) => { } ,
350- future_public_mutation : async ( ) => { }
389+ get_conversation : async ( ) => { } ,
390+ future_public_mutation : async ( ) => { }
351391 } ) ) . toThrow ( / M i s s i n g c l a s s i f i c a t i o n : f u t u r e _ p u b l i c _ m u t a t i o n / ) ;
352392 } ) ;
353393
0 commit comments