From d12287c962aa3d97ba2e524160e60419b475e83b Mon Sep 17 00:00:00 2001 From: Shrijith Venkatramana Date: Thu, 2 Jul 2026 14:55:46 +0000 Subject: [PATCH 1/2] adaptive review draft LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%) --- config/ai_connectors.go | 7 ++++ internal/appui/setup.go | 11 +++--- internal/appui/setup_flow.go | 19 ++++++++- internal/appui/ui_connectors_test.go | 26 +++++++++++++ setup/connectors.go | 32 +++++++++++++++ setup/connectors_test.go | 58 ++++++++++++++++++++++++++++ setup/constants.go | 3 +- setup/types.go | 3 ++ ui/types.go | 8 +++- 9 files changed, 158 insertions(+), 9 deletions(-) diff --git a/config/ai_connectors.go b/config/ai_connectors.go index 14aa70b..910e16f 100644 --- a/config/ai_connectors.go +++ b/config/ai_connectors.go @@ -95,6 +95,13 @@ func RenderManagedAIConnectorsSection(connectors []uicfg.ConnectorRemote, sectio builder.WriteString("provider_name = ") builder.WriteString(strconv.Quote(connector.ProviderName)) builder.WriteString("\n") + role := strings.TrimSpace(connector.Role) + if role == "" { + role = "leader" + } + builder.WriteString("role = ") + builder.WriteString(strconv.Quote(role)) + builder.WriteString("\n") builder.WriteString("connector_name = ") builder.WriteString(strconv.Quote(connector.ConnectorName)) builder.WriteString("\n") diff --git a/internal/appui/setup.go b/internal/appui/setup.go index a2b8329..02e9981 100644 --- a/internal/appui/setup.go +++ b/internal/appui/setup.go @@ -7,11 +7,12 @@ import ( ) const ( - cloudAPIURL = setuptpl.CloudAPIURL - geminiKeysURL = setuptpl.GeminiKeysURL - defaultGeminiModel = setuptpl.DefaultGeminiModel - setupTimeout = 5 * time.Minute - issuesURL = setuptpl.IssuesURL + cloudAPIURL = setuptpl.CloudAPIURL + geminiKeysURL = setuptpl.GeminiKeysURL + defaultGeminiModel = setuptpl.DefaultGeminiModel + defaultGeminiHelperModel = setuptpl.DefaultGeminiHelperModel + setupTimeout = 5 * time.Minute + issuesURL = setuptpl.IssuesURL ) type setupResult = setuptpl.SetupResult diff --git a/internal/appui/setup_flow.go b/internal/appui/setup_flow.go index 7fca987..5c1352a 100644 --- a/internal/appui/setup_flow.go +++ b/internal/appui/setup_flow.go @@ -115,9 +115,22 @@ func RunSetup(c *cli.Context) error { return setupError(slog, fmt.Errorf("failed to create AI connector: %w", err)) } fmt.Printf(" %s✅ Gemini connector created%s %s(model: %s)%s\n", clr(cGreen), clr(cReset), clr(cDim), defaultGeminiModel, clr(cReset)) - fmt.Println() slog.write("gemini connector created") + // Helper connector powers Adaptive Review out of the box, reusing the + // same free key. Best-effort: the leader connector above is already + // committed, so a failure here shouldn't fail the whole setup — the org + // just stays leader-only until they configure a helper connector later. + slog.write("creating gemini helper connector") + if err := createGeminiHelperConnector(result, geminiKey, selectedAPIURL); err != nil { + slog.write("warning: failed to create helper connector: %v", err) + fmt.Printf(" %s⚠ Could not create Helper connector%s %s(Adaptive Review will stay leader-only for now)%s\n", clr(cYellow), clr(cReset), clr(cDim), clr(cReset)) + } else { + fmt.Printf(" %s✅ Helper connector created%s %s(model: %s)%s\n", clr(cGreen), clr(cReset), clr(cDim), defaultGeminiHelperModel, clr(cReset)) + slog.write("gemini helper connector created") + } + fmt.Println() + if !skipLogin { if err := writeConfig(result, selectedAPIURL); err != nil { return setupError(slog, fmt.Errorf("failed to write config: %w", err)) @@ -406,6 +419,10 @@ func createGeminiConnector(result *setupResult, geminiKey string, apiURL string) return setuptpl.CreateGeminiConnector(result, geminiKey, apiURL) } +func createGeminiHelperConnector(result *setupResult, geminiKey string, apiURL string) error { + return setuptpl.CreateGeminiHelperConnector(result, geminiKey, apiURL) +} + func writeConfig(result *setupResult, apiURL string) error { return setuptpl.WriteConfigWithOptions(result, setuptpl.WriteConfigOptions{APIURL: strings.TrimSpace(apiURL)}) } diff --git a/internal/appui/ui_connectors_test.go b/internal/appui/ui_connectors_test.go index 3ca7a91..031e5a4 100644 --- a/internal/appui/ui_connectors_test.go +++ b/internal/appui/ui_connectors_test.go @@ -28,6 +28,25 @@ org_id = "42" APIKey: "gkey", SelectedModel: "gemini-2.5-flash", DisplayOrder: 1, + Role: "leader", + }, + { + ID: 8, + ProviderName: "gemini", + ConnectorName: "Gemini Flash-Lite (Helper)", + APIKey: "gkey2", + SelectedModel: "gemini-2.5-flash-lite", + DisplayOrder: 1, + Role: "helper", + }, + { + ID: 9, + ProviderName: "gemini", + ConnectorName: "Legacy connector with no role", + APIKey: "gkey3", + SelectedModel: "gemini-2.5-flash", + DisplayOrder: 2, + Role: "", }, } @@ -56,6 +75,13 @@ org_id = "42" if !strings.Contains(updated, `[[ai_connectors]]`) || !strings.Contains(updated, `provider_name = "gemini"`) { t.Fatalf("connector data not written to managed section") } + + if strings.Count(updated, `role = "leader"`) != 2 { + t.Fatalf("expected two connectors (explicit leader + empty-role fallback) rendered with role=\"leader\", got: %s", updated) + } + if !strings.Contains(updated, `role = "helper"`) { + t.Fatalf("expected the helper connector to render role=\"helper\", got: %s", updated) + } } func TestPersistAuthTokensToConfigUpdatesExistingTokens(t *testing.T) { diff --git a/setup/connectors.go b/setup/connectors.go index 61ccd93..c9292da 100644 --- a/setup/connectors.go +++ b/setup/connectors.go @@ -69,6 +69,7 @@ func CreateGeminiConnector(result *SetupResult, geminiKey string, apiURL string) ConnectorName: "Gemini Flash", SelectedModel: DefaultGeminiModel, DisplayOrder: 0, + Role: "leader", } client := network.NewSetupClient(30 * time.Second) @@ -82,3 +83,34 @@ func CreateGeminiConnector(result *SetupResult, geminiKey string, apiURL string) return nil } + +// CreateGeminiHelperConnector creates a Gemini Flash-Lite helper connector in +// LiveReview, reusing the same free Gemini key as the leader connector. This +// powers Adaptive Review (see LiveReview's leader/helper model feature) out +// of the box for onboarded orgs, with no second key prompt. +func CreateGeminiHelperConnector(result *SetupResult, geminiKey string, apiURL string) error { + apiURL = strings.TrimSpace(apiURL) + if apiURL == "" { + apiURL = CloudAPIURL + } + + reqBody := CreateConnectorRequest{ + ProviderName: "gemini", + APIKey: geminiKey, + ConnectorName: "Gemini Flash-Lite (Helper)", + SelectedModel: DefaultGeminiHelperModel, + DisplayOrder: 0, + Role: "helper", + } + + client := network.NewSetupClient(30 * time.Second) + resp, err := network.SetupCreateConnector(client, apiURL, result.OrgID, reqBody, result.AccessToken) + if err != nil { + return fmt.Errorf("failed to create helper connector: %w", err) + } + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + return fmt.Errorf("create helper connector returned %d: %s", resp.StatusCode, redactConnectorErrorBody(resp.Body, geminiKey)) + } + + return nil +} diff --git a/setup/connectors_test.go b/setup/connectors_test.go index 4d635d0..4565baf 100644 --- a/setup/connectors_test.go +++ b/setup/connectors_test.go @@ -1,6 +1,10 @@ package setup import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" "strings" "testing" ) @@ -27,3 +31,57 @@ func TestRedactConnectorErrorBody_EmptyBody(t *testing.T) { t.Fatalf("expected empty body marker, got %q", redacted) } } + +func TestCreateGeminiHelperConnector_SendsHelperRoleAndLiteModel(t *testing.T) { + var gotBody CreateConnectorRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read request body: %v", err) + } + if err := json.Unmarshal(body, &gotBody); err != nil { + t.Fatalf("failed to unmarshal request body: %v", err) + } + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + result := &SetupResult{OrgID: "1", AccessToken: "test-token"} + if err := CreateGeminiHelperConnector(result, "test-gemini-key", server.URL); err != nil { + t.Fatalf("CreateGeminiHelperConnector() error = %v", err) + } + + if gotBody.Role != "helper" { + t.Errorf("expected role=%q, got %q", "helper", gotBody.Role) + } + if gotBody.SelectedModel != DefaultGeminiHelperModel { + t.Errorf("expected selected_model=%q, got %q", DefaultGeminiHelperModel, gotBody.SelectedModel) + } + if gotBody.APIKey != "test-gemini-key" { + t.Errorf("expected the same key passed to the leader connector to be reused, got %q", gotBody.APIKey) + } +} + +func TestCreateGeminiConnector_SendsLeaderRole(t *testing.T) { + var gotBody CreateConnectorRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &gotBody) + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + result := &SetupResult{OrgID: "1", AccessToken: "test-token"} + if err := CreateGeminiConnector(result, "test-gemini-key", server.URL); err != nil { + t.Fatalf("CreateGeminiConnector() error = %v", err) + } + + if gotBody.Role != "leader" { + t.Errorf("expected role=%q, got %q", "leader", gotBody.Role) + } + if gotBody.SelectedModel != DefaultGeminiModel { + t.Errorf("expected selected_model=%q, got %q", DefaultGeminiModel, gotBody.SelectedModel) + } +} diff --git a/setup/constants.go b/setup/constants.go index 1311786..942120c 100644 --- a/setup/constants.go +++ b/setup/constants.go @@ -4,6 +4,7 @@ const ( CloudAPIURL = "https://livereview.hexmos.com" HexmosSigninBase = "https://hexmos.com/signin" GeminiKeysURL = "https://aistudio.google.com/api-keys" - DefaultGeminiModel = "gemini-2.5-flash" + DefaultGeminiModel = "gemini-2.5-flash" + DefaultGeminiHelperModel = "gemini-2.5-flash-lite" IssuesURL = "https://github.com/HexmosTech/git-lrc/issues/new" ) diff --git a/setup/types.go b/setup/types.go index d44fa7f..420fa2c 100644 --- a/setup/types.go +++ b/setup/types.go @@ -97,4 +97,7 @@ type CreateConnectorRequest struct { SelectedModel string `json:"selected_model"` DisplayOrder int `json:"display_order"` BaseURL string `json:"base_url,omitempty"` + // Role is "leader" or "helper" (see LiveReview's Adaptive Review + // feature). Omitted/empty defaults to "leader" server-side. + Role string `json:"role,omitempty"` } diff --git a/ui/types.go b/ui/types.go index eb1300d..e7fdb45 100644 --- a/ui/types.go +++ b/ui/types.go @@ -32,8 +32,12 @@ type ConnectorRemote struct { GCPLocation string `json:"gcp_location"` SelectedModel string `json:"selected_model"` DisplayOrder int `json:"display_order"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + // Role is "leader" or "helper" (LiveReview's Adaptive Review feature). + // The backend always includes this on connector responses; empty here + // just means an old/unexpected payload, and should be treated as "leader". + Role string `json:"role"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } type AuthRefreshRequest struct { From d957c96d54057255a16d5e5b0c0194921110fa46 Mon Sep 17 00:00:00 2001 From: Lince Mathew Date: Fri, 3 Jul 2026 21:03:04 +0530 Subject: [PATCH 2/2] Refactor Connector Creation Logic LiveReview Pre-Commit Check: ran (iter:3, coverage:0%) --- setup/connectors.go | 62 +++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/setup/connectors.go b/setup/connectors.go index c9292da..d742a32 100644 --- a/setup/connectors.go +++ b/setup/connectors.go @@ -25,12 +25,17 @@ func redactConnectorErrorBody(body []byte, secrets ...string) string { return msg } -// ValidateGeminiKey checks the key against LiveReview's validate-key endpoint. -func ValidateGeminiKey(result *SetupResult, geminiKey string, apiURL string) (bool, string, error) { +func defaultAPIURL(apiURL string) string { apiURL = strings.TrimSpace(apiURL) if apiURL == "" { - apiURL = CloudAPIURL + return CloudAPIURL } + return apiURL +} + +// ValidateGeminiKey checks the key against LiveReview's validate-key endpoint. +func ValidateGeminiKey(result *SetupResult, geminiKey string, apiURL string) (bool, string, error) { + apiURL = defaultAPIURL(apiURL) reqBody := ValidateKeyRequest{ Provider: "gemini", @@ -56,13 +61,27 @@ func ValidateGeminiKey(result *SetupResult, geminiKey string, apiURL string) (bo return valResp.Valid, valResp.Message, nil } -// CreateGeminiConnector creates a Gemini AI connector in LiveReview. -func CreateGeminiConnector(result *SetupResult, geminiKey string, apiURL string) error { - apiURL = strings.TrimSpace(apiURL) - if apiURL == "" { - apiURL = CloudAPIURL +// createConnector posts a connector creation request to LiveReview, sharing +// the URL defaulting, network call, and error handling used by every +// connector-creation flow (leader, helper, ...). errLabel is used to phrase +// error messages, e.g. "connector" or "helper connector". +func createConnector(result *SetupResult, apiURL string, reqBody CreateConnectorRequest, geminiKey string, errLabel string) error { + apiURL = defaultAPIURL(apiURL) + + client := network.NewSetupClient(30 * time.Second) + resp, err := network.SetupCreateConnector(client, apiURL, result.OrgID, reqBody, result.AccessToken) + if err != nil { + return fmt.Errorf("failed to create %s: %w", errLabel, err) + } + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + return fmt.Errorf("create %s returned %d: %s", errLabel, resp.StatusCode, redactConnectorErrorBody(resp.Body, geminiKey)) } + return nil +} + +// CreateGeminiConnector creates a Gemini AI connector in LiveReview. +func CreateGeminiConnector(result *SetupResult, geminiKey string, apiURL string) error { reqBody := CreateConnectorRequest{ ProviderName: "gemini", APIKey: geminiKey, @@ -72,16 +91,7 @@ func CreateGeminiConnector(result *SetupResult, geminiKey string, apiURL string) Role: "leader", } - client := network.NewSetupClient(30 * time.Second) - resp, err := network.SetupCreateConnector(client, apiURL, result.OrgID, reqBody, result.AccessToken) - if err != nil { - return fmt.Errorf("failed to create connector: %w", err) - } - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { - return fmt.Errorf("create connector returned %d: %s", resp.StatusCode, redactConnectorErrorBody(resp.Body, geminiKey)) - } - - return nil + return createConnector(result, apiURL, reqBody, geminiKey, "connector") } // CreateGeminiHelperConnector creates a Gemini Flash-Lite helper connector in @@ -89,11 +99,6 @@ func CreateGeminiConnector(result *SetupResult, geminiKey string, apiURL string) // powers Adaptive Review (see LiveReview's leader/helper model feature) out // of the box for onboarded orgs, with no second key prompt. func CreateGeminiHelperConnector(result *SetupResult, geminiKey string, apiURL string) error { - apiURL = strings.TrimSpace(apiURL) - if apiURL == "" { - apiURL = CloudAPIURL - } - reqBody := CreateConnectorRequest{ ProviderName: "gemini", APIKey: geminiKey, @@ -103,14 +108,5 @@ func CreateGeminiHelperConnector(result *SetupResult, geminiKey string, apiURL s Role: "helper", } - client := network.NewSetupClient(30 * time.Second) - resp, err := network.SetupCreateConnector(client, apiURL, result.OrgID, reqBody, result.AccessToken) - if err != nil { - return fmt.Errorf("failed to create helper connector: %w", err) - } - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { - return fmt.Errorf("create helper connector returned %d: %s", resp.StatusCode, redactConnectorErrorBody(resp.Body, geminiKey)) - } - - return nil + return createConnector(result, apiURL, reqBody, geminiKey, "helper connector") }