diff --git a/internal/ai/gemini/issue_content_generator.go b/internal/ai/gemini/issue_content_generator.go index 5509cd3..fd9d5e8 100644 --- a/internal/ai/gemini/issue_content_generator.go +++ b/internal/ai/gemini/issue_content_generator.go @@ -142,6 +142,11 @@ func (s *GeminiIssueContentGenerator) GenerateIssueContent(ctx context.Context, log.Debug("calling gemini API for issue content", "prompt_length", len(prompt)) + if request.SkipConfirmation { + s.wrapper.SetSkipConfirmation(true) + defer s.wrapper.SetSkipConfirmation(false) + } + var usage *models.TokenUsage var responseText string diff --git a/internal/ai/gemini/issue_content_generator_test.go b/internal/ai/gemini/issue_content_generator_test.go index 683b95a..c0b95d6 100644 --- a/internal/ai/gemini/issue_content_generator_test.go +++ b/internal/ai/gemini/issue_content_generator_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/thomas-vilte/matecommit/internal/ai" "github.com/thomas-vilte/matecommit/internal/config" "github.com/thomas-vilte/matecommit/internal/models" "google.golang.org/genai" @@ -443,3 +444,69 @@ func TestGenerateIssueContent_HappyPath(t *testing.T) { assert.Contains(t, result.Labels, "fix") }) } + +// TestGenerateIssueContent_SkipConfirmation verifies that a request with +// SkipConfirmation bypasses the cost confirmation prompt (used for internal +// orchestration calls like template auto-selection), while a normal request +// still goes through it — so the user isn't asked twice for one logical +// "generate an issue" action. +func TestGenerateIssueContent_SkipConfirmation(t *testing.T) { + tmpHome, err := os.MkdirTemp("", "matecommit-test-issue-skip-*") + assert.NoError(t, err) + defer func() { + if err := os.RemoveAll(tmpHome); err != nil { + return + } + }() + oldHome := os.Getenv("HOME") + _ = os.Setenv("HOME", tmpHome) + defer func() { + if err := os.Setenv("HOME", oldHome); err != nil { + return + } + }() + + ctx := context.Background() + cfg := &config.Config{ + AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, + AIConfig: config.AIConfig{Models: map[config.AI]config.Model{config.AIGemini: "gemini-pro"}}, + } + + confirmationCalls := 0 + onConfirmation := func(ai.ConfirmationResult) (string, bool) { + confirmationCalls++ + return "current", true + } + + gen, err := NewGeminiIssueContentGenerator(ctx, cfg, onConfirmation) + assert.NoError(t, err) + + expectedJSON := `{"title": "Issue Title", "description": "Issue Description", "labels": ["fix"]}` + gen.generateFn = func(ctx context.Context, mName string, p string) (interface{}, *models.TokenUsage, error) { + return &genai.GenerateContentResponse{ + Candidates: []*genai.Candidate{ + {Content: &genai.Content{Parts: []*genai.Part{{Text: expectedJSON}}}}, + }, + }, &models.TokenUsage{TotalTokens: 30}, nil + } + + t.Run("normal request triggers confirmation", func(t *testing.T) { + _, err := gen.GenerateIssueContent(ctx, models.IssueGenerationRequest{Description: "unique-prompt-1"}) + assert.NoError(t, err) + assert.Equal(t, 1, confirmationCalls, "confirmation should have been requested") + }) + + t.Run("SkipConfirmation bypasses the prompt", func(t *testing.T) { + confirmationCalls = 0 + _, err := gen.GenerateIssueContent(ctx, models.IssueGenerationRequest{Description: "unique-prompt-2", SkipConfirmation: true}) + assert.NoError(t, err) + assert.Equal(t, 0, confirmationCalls, "confirmation should have been skipped") + }) + + t.Run("wrapper returns to normal confirmation behavior afterwards", func(t *testing.T) { + confirmationCalls = 0 + _, err := gen.GenerateIssueContent(ctx, models.IssueGenerationRequest{Description: "unique-prompt-3"}) + assert.NoError(t, err) + assert.Equal(t, 1, confirmationCalls, "confirmation should be re-enabled after a SkipConfirmation call") + }) +} diff --git a/internal/models/issue_generation.go b/internal/models/issue_generation.go index cec0e26..0d9f58b 100644 --- a/internal/models/issue_generation.go +++ b/internal/models/issue_generation.go @@ -23,6 +23,12 @@ type IssueGenerationRequest struct { // AvailableLabels is the list of labels available in the repository (optional) AvailableLabels []string + + // SkipConfirmation bypasses the cost confirmation prompt for this call. + // Used for small internal orchestration calls (e.g. template + // auto-selection) that shouldn't interrupt the user twice for what + // they experience as a single logical action. + SkipConfirmation bool } // IssueGenerationResult contains the result of an issue's content generation. diff --git a/internal/services/issue_generator_service.go b/internal/services/issue_generator_service.go index ba9a18c..6500c55 100644 --- a/internal/services/issue_generator_service.go +++ b/internal/services/issue_generator_service.go @@ -458,6 +458,9 @@ If no template fits perfectly, choose "Custom Issue" or the most generic one.`, request := models.IssueGenerationRequest{ Description: prompt, Language: "en", + // This is a small internal call to pick a template, not the user's + // actual issue content — don't make them confirm cost for it too. + SkipConfirmation: true, } result, err := s.ai.GenerateIssueContent(ctx, request)