@@ -705,4 +705,84 @@ describe("expansion round counter survives a clarification round-trip", () => {
705705 } ) ;
706706 expect ( second . kind ) . toBe ( "attach" ) ;
707707 } ) ;
708+
709+ // AIW-284: research asking only for repositories the workspace already holds
710+ // used to park the whole run on "Which additional repository is required?",
711+ // which no human can answer, because everything named was already there. Same
712+ // limitation as the test above: expandResearchWorkspace is an inline closure in
713+ // agent.ts, so this drives the exported validator and mirrors the call site's
714+ // state transitions rather than executing the loop itself.
715+ it ( "continues without a clarification and still burns a round when every requested repository is attached" , ( ) => {
716+ const ctx = makeCtx ( {
717+ sandboxId : "sbx-research" ,
718+ workspaceManifest : { version : 2 , repositories : [ ] } ,
719+ selectedRepositories : [
720+ {
721+ provider : "github" ,
722+ repoPath : "acme/service" ,
723+ defaultBranch : "main" ,
724+ selectedRationale : "symptom" ,
725+ } ,
726+ {
727+ provider : "gitlab" ,
728+ repoPath : "acme/shared/contracts" ,
729+ defaultBranch : "main" ,
730+ selectedRationale : "imports" ,
731+ } ,
732+ ] ,
733+ } ) ;
734+
735+ const requests = [
736+ {
737+ provider : "gitlab" as const ,
738+ repoPath : "acme/shared/contracts" ,
739+ rationale : "need the schema" ,
740+ } ,
741+ {
742+ provider : "github" as const ,
743+ repoPath : "acme/service" ,
744+ rationale : "need the caller" ,
745+ } ,
746+ ] ;
747+ const decision = validateRepositoryExpansionRequests ( {
748+ requests,
749+ catalog,
750+ attached : ctx . selectedRepositories ,
751+ completedRounds : ctx . repositoryExpansion . rounds ,
752+ } ) ;
753+
754+ // The run keeps going: no clarification, and nothing to clone.
755+ expect ( decision ) . toEqual ( { kind : "already_attached" } ) ;
756+
757+ // The call site advances the durable counter exactly as the attach path
758+ // does, so a model that keeps re-asking is bounded instead of looping.
759+ ctx . repositoryExpansion = {
760+ rounds : ctx . repositoryExpansion . rounds + 1 ,
761+ priorRequests : [ ...ctx . repositoryExpansion . priorRequests , ...requests ] ,
762+ } ;
763+ expect ( ctx . repositoryExpansion . rounds ) . toBe ( 1 ) ;
764+ expect ( ctx . repositoryExpansion . priorRequests ) . toEqual ( requests ) ;
765+
766+ // Round two repeats the no-op, and round three hits the expansion limit,
767+ // which IS a question a human can act on.
768+ expect (
769+ validateRepositoryExpansionRequests ( {
770+ requests,
771+ catalog,
772+ attached : ctx . selectedRepositories ,
773+ completedRounds : 1 ,
774+ } ) ,
775+ ) . toEqual ( { kind : "already_attached" } ) ;
776+ expect (
777+ validateRepositoryExpansionRequests ( {
778+ requests,
779+ catalog,
780+ attached : ctx . selectedRepositories ,
781+ completedRounds : 2 ,
782+ } ) ,
783+ ) . toMatchObject ( {
784+ kind : "clarification_needed" ,
785+ questions : [ expect . stringContaining ( "maximum of 2" ) ] ,
786+ } ) ;
787+ } ) ;
708788} ) ;
0 commit comments