@@ -2068,9 +2068,11 @@ func TestResumeProjectAssistantRunPersistsAssistantTextBeforeNextPause(t *testin
20682068 firstCall := chatStreamingCall {Index : 0 , ID : "call-first-write" , Type : "function" }
20692069 firstCall .Function .Name = projectToolWriteFile
20702070 firstCall .Function .Arguments = `{"path":"src/App.tsx","content":"first\n"}`
2071- secondCall := chatStreamingCall {Index : 0 , ID : "call-second-write" , Type : "function" }
2072- secondCall .Function .Name = projectToolWriteFile
2073- secondCall .Function .Arguments = `{"path":"src/Other.tsx","content":"second\n"}`
2071+ // Approving the first write grants every write until the next commit,
2072+ // so the next pause has to come from a tool that still always asks.
2073+ secondCall := chatStreamingCall {Index : 0 , ID : "call-second-runtime" , Type : "function" }
2074+ secondCall .Function .Name = projectToolDeployProjectRuntime
2075+ secondCall .Function .Arguments = `{"targetRef":"runtime-a","image":"ghcr.io/demo/app:latest","port":8080,"intent":"preview"}`
20742076 model := & repositoryFlowEinoChatModel {Steps : []repositoryFlowEinoModelStep {
20752077 {Message : einoschema .AssistantMessage ("" , projectEinoToolCallsFromStreamingForTest ([]chatStreamingCall {firstCall }))},
20762078 {Message : einoschema .AssistantMessage ("First change applied. " , projectEinoToolCallsFromStreamingForTest ([]chatStreamingCall {secondCall }))},
@@ -2163,6 +2165,79 @@ func TestResumeProjectAssistantRunPersistsAssistantTextBeforeNextPause(t *testin
21632165 }
21642166}
21652167
2168+ func TestResumeProjectAssistantRunAllowingWriteDoesNotRePromptLaterWrites (t * testing.T ) {
2169+ messages := store .NewMemoryStore ()
2170+ workspaces := workspace .NewFileStore (t .TempDir ())
2171+ server := NewWithWorkspace (nil , messages , workspaces , "" , false )
2172+ project := projectWithRepository ("demo-repo" , "demo" , "github" )
2173+ project .Name = "demo"
2174+ id := identity {tenantPath : "root:org-a:ws-1" , orgUUID : "org-a" , workspaceUUID : "ws-1" }
2175+ messageScope := projectMessageScope (id .orgUUID , id .workspaceUUID , project .Name )
2176+ workspaceScope := projectWorkspaceScope (id , project .Name )
2177+
2178+ firstCall := chatStreamingCall {Index : 0 , ID : "call-first-write" , Type : "function" }
2179+ firstCall .Function .Name = projectToolWriteFile
2180+ firstCall .Function .Arguments = `{"path":"src/App.tsx","content":"first\n"}`
2181+ secondCall := chatStreamingCall {Index : 0 , ID : "call-second-write" , Type : "function" }
2182+ secondCall .Function .Name = projectToolWriteFile
2183+ secondCall .Function .Arguments = `{"path":"src/Other.tsx","content":"second\n"}`
2184+ model := & repositoryFlowEinoChatModel {Steps : []repositoryFlowEinoModelStep {
2185+ {Message : einoschema .AssistantMessage ("" , projectEinoToolCallsFromStreamingForTest ([]chatStreamingCall {firstCall }))},
2186+ {Message : einoschema .AssistantMessage ("Applied both changes. " , projectEinoToolCallsFromStreamingForTest ([]chatStreamingCall {secondCall }))},
2187+ {Message : einoschema .AssistantMessage ("All done." , nil )},
2188+ }}
2189+ setProjectAssistantModelForTest (server , model )
2190+ settings := projectLLMSettings {Provider : defaultProjectLLMProvider , BaseURL : defaultProjectLLMBaseURL , Model : "test-model" , APIKey : "test-key" }
2191+ client := asclient .NewFromDynamic (projectSettingsDynamicClient {secret : projectLLMSettingsSecret (settings )})
2192+ if err := appendProjectUserMessage (context .Background (), messages , messageScope , "write files" ); err != nil {
2193+ t .Fatalf ("appendProjectUserMessage returned error: %v" , err )
2194+ }
2195+
2196+ _ , err := server .generateProjectAssistantStream (
2197+ httptest .NewRequest (http .MethodPost , "/" , nil ),
2198+ id ,
2199+ client ,
2200+ project ,
2201+ projectAssistantStreamCallbacks {},
2202+ )
2203+ var permissionErr * projectAssistantPermissionRequiredError
2204+ if ! errors .As (err , & permissionErr ) {
2205+ t .Fatalf ("generateProjectAssistantStream error = %v, want permission required" , err )
2206+ }
2207+
2208+ resp , err := server .resumeProjectAssistantRunWithRepositoryAndClient (
2209+ context .Background (),
2210+ httptest .NewRequest (http .MethodPost , "/" , nil ),
2211+ id ,
2212+ client ,
2213+ project ,
2214+ & ProjectRepositoryView {Ref : "demo-repo" , Name : "demo" , Status : projectRepositoryStatusReady },
2215+ permissionErr .RunID ,
2216+ projectAssistantResumeRequest {
2217+ RequestID : permissionErr .RequestID ,
2218+ Decision : string (projectAssistantPermissionAllow ),
2219+ },
2220+ )
2221+ if err != nil {
2222+ t .Fatalf ("resumeProjectAssistantRun returned error: %v" , err )
2223+ }
2224+ if resp .Status != store .AssistantRunStatusCompleted {
2225+ t .Fatalf ("resume status = %q, want %q (second write should auto-approve)" , resp .Status , store .AssistantRunStatusCompleted )
2226+ }
2227+
2228+ files , err := workspaces .ListFiles (context .Background (), workspaceScope , workspace.ListOptions {})
2229+ if err != nil {
2230+ t .Fatalf ("ListFiles returned error: %v" , err )
2231+ }
2232+ written := map [string ]bool {}
2233+ for _ , f := range files .Files {
2234+ written [f .Path ] = true
2235+ }
2236+ if ! written ["src/App.tsx" ] || ! written ["src/Other.tsx" ] {
2237+ t .Fatalf ("written files = %v, want both src/App.tsx and src/Other.tsx" , written )
2238+ }
2239+ }
2240+
21662241func TestResumeProjectAssistantRunRejectsStaleRepositoryBinding (t * testing.T ) {
21672242 var sawCommit bool
21682243 mcp := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
0 commit comments