diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4e7b68fa..fc92454c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,11 +45,13 @@ jobs: - name: Download dependencies run: go mod download + - name: Install linters + run: make install-lint-tools + - name: Lint - uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6 + uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v9 with: - install-mode: goinstall - version: v1.64.8 + install-mode: "none" test: name: Test diff --git a/.golangci.yml b/.golangci.yml index 91a0a89c..f3f7dc4c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,41 +1,59 @@ +version: "2" run: - timeout: 5m modules-download-mode: readonly - -linters-settings: - gci: - sections: - - standard - - default - - prefix(github.com/Use-Tusk/tusk-cli) - gofmt: - simplify: true - goimports: - local-prefixes: github.com/Use-Tusk/tusk-cli - gocritic: - disabled-checks: - - singleCaseSwitch - revive: - rules: - - name: exported - disabled: true - linters: - enable-all: false - disable-all: true + default: none enable: - - staticcheck - errcheck - - gosimple + - gocritic + - gosec - govet - - unused - ineffassign - - gosec - - gocritic + - misspell - revive + - staticcheck + - unused + settings: + gocritic: + disabled-checks: + - singleCaseSwitch + staticcheck: + checks: + - all + - -ST1000 + - -ST1003 + - -ST1005 + - -ST1016 + - -ST1020 + - -ST1021 + - -ST1022 + revive: + rules: + - name: exported + disabled: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: - gofumpt - - ineffassign - - misspell - -issues: - exclude-use-default: false + settings: + gci: + sections: + - standard + - default + - prefix(github.com/Use-Tusk/tusk-cli) + gofmt: + simplify: true + goimports: + local-prefixes: + - github.com/Use-Tusk/tusk-cli + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index a0f44296..eca30e72 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ GOMOD=$(GOCMD) mod BINARY_NAME=tusk BINARY_UNIX=$(BINARY_NAME)_unix +# Tool versions +GOLANGCI_LINT_VERSION=v2.11.4 + .PHONY: all build build-ci build-linux test test-ci clean deps install-buf install-lint-tools setup setup-ci run fmt lint help @@ -54,7 +57,7 @@ install-buf: install-lint-tools: @echo "📦 Installing linting tools..." GOTOOLCHAIN=local go install mvdan.cc/gofumpt@latest - GOTOOLCHAIN=local go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + GOTOOLCHAIN=local go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) @echo "✅ Linting tools installed" setup: install-buf deps install-lint-tools diff --git a/cmd/setup.go b/cmd/setup.go index 41687140..9c400f61 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -95,7 +95,7 @@ func getAnthropicAPIConfig() (*APIConfig, error) { if envKey := os.Getenv("ANTHROPIC_API_KEY"); envKey != "" { // In non-interactive mode (CI/scripts), default to BYOK to avoid hanging - if !term.IsTerminal(int(os.Stdin.Fd())) { + if !term.IsTerminal(int(os.Stdin.Fd())) { //nolint:gosec // file descriptor fits in int return &APIConfig{ Mode: agent.APIModeDirect, APIKey: envKey, diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 6675975c..f508535c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -200,7 +200,7 @@ func (a *Agent) trackInterrupted(phaseName string, phasesCompleted int) { // Run executes the agent with TUI or in headless mode func (a *Agent) Run(parentCtx context.Context) error { // Create cancellable context - a.ctx, a.cancel = context.WithCancel(parentCtx) + a.ctx, a.cancel = context.WithCancel(parentCtx) //nolint:gosec // cancel is invoked via defer below defer a.cancel() if a.logger != nil { @@ -1659,31 +1659,31 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note sb.WriteString("## Discovered Information\n\n") if state != nil && (state.ProjectType != "" || state.PackageManager != "" || state.EntryPoint != "") { if state.ServiceName != "" { - sb.WriteString(fmt.Sprintf("- **Service Name**: %s\n", state.ServiceName)) + fmt.Fprintf(&sb, "- **Service Name**: %s\n", state.ServiceName) } if state.ProjectType != "" { - sb.WriteString(fmt.Sprintf("- **Project Type**: %s\n", state.ProjectType)) + fmt.Fprintf(&sb, "- **Project Type**: %s\n", state.ProjectType) } if state.PackageManager != "" { - sb.WriteString(fmt.Sprintf("- **Package Manager**: %s\n", state.PackageManager)) + fmt.Fprintf(&sb, "- **Package Manager**: %s\n", state.PackageManager) } if state.ModuleSystem != "" { - sb.WriteString(fmt.Sprintf("- **Module System**: %s\n", state.ModuleSystem)) + fmt.Fprintf(&sb, "- **Module System**: %s\n", state.ModuleSystem) } if state.EntryPoint != "" { - sb.WriteString(fmt.Sprintf("- **Entry Point**: %s\n", state.EntryPoint)) + fmt.Fprintf(&sb, "- **Entry Point**: %s\n", state.EntryPoint) } if state.StartCommand != "" { - sb.WriteString(fmt.Sprintf("- **Start Command**: `%s`\n", state.StartCommand)) + fmt.Fprintf(&sb, "- **Start Command**: `%s`\n", state.StartCommand) } if state.Port != "" { - sb.WriteString(fmt.Sprintf("- **Port**: %s\n", state.Port)) + fmt.Fprintf(&sb, "- **Port**: %s\n", state.Port) } if state.HealthEndpoint != "" { - sb.WriteString(fmt.Sprintf("- **Health Endpoint**: %s\n", state.HealthEndpoint)) + fmt.Fprintf(&sb, "- **Health Endpoint**: %s\n", state.HealthEndpoint) } if state.DockerType != "" && state.DockerType != "none" { - sb.WriteString(fmt.Sprintf("- **Docker**: %s\n", state.DockerType)) + fmt.Fprintf(&sb, "- **Docker**: %s\n", state.DockerType) } sb.WriteString("\n") } else { @@ -1695,7 +1695,7 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note sb.WriteString("The following packages are used but not instrumented by the SDK.\n") sb.WriteString("Recording/replay may not capture these calls:\n\n") for _, warning := range state.CompatibilityWarnings { - sb.WriteString(fmt.Sprintf("- ⚠️ %s\n", warning)) + fmt.Fprintf(&sb, "- ⚠️ %s\n", warning) } sb.WriteString("\n") } @@ -1725,10 +1725,10 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note sb.WriteString("- ✓ Authenticated with Tusk Cloud\n") } if state.GitRepoOwner != "" && state.GitRepoName != "" { - sb.WriteString(fmt.Sprintf("- ✓ Repository detected: %s/%s\n", state.GitRepoOwner, state.GitRepoName)) + fmt.Fprintf(&sb, "- ✓ Repository detected: %s/%s\n", state.GitRepoOwner, state.GitRepoName) } if state.CloudServiceID != "" { - sb.WriteString(fmt.Sprintf("- ✓ Cloud service created (ID: %s)\n", state.CloudServiceID)) + fmt.Fprintf(&sb, "- ✓ Cloud service created (ID: %s)\n", state.CloudServiceID) } if state.ApiKeyCreated { sb.WriteString("- ✓ API key created\n") @@ -1741,13 +1741,13 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note sb.WriteString("None yet.\n\n") } else { for _, phase := range completedPhases { - sb.WriteString(fmt.Sprintf("- ✓ %s\n", phase)) + fmt.Fprintf(&sb, "- ✓ %s\n", phase) } sb.WriteString("\n") } if currentPhase != "" { - sb.WriteString(fmt.Sprintf("## Current Phase\n\n%s (in progress)\n\n", currentPhase)) + fmt.Fprintf(&sb, "## Current Phase\n\n%s (in progress)\n\n", currentPhase) } if state != nil && (len(state.Errors) > 0 || len(state.Warnings) > 0) { @@ -1755,9 +1755,9 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note sb.WriteString("## Errors Encountered\n\n") for _, err := range state.Errors { if err.Fatal { - sb.WriteString(fmt.Sprintf("- ❌ [%s] %s (fatal)\n", err.Phase, err.Message)) + fmt.Fprintf(&sb, "- ❌ [%s] %s (fatal)\n", err.Phase, err.Message) } else { - sb.WriteString(fmt.Sprintf("- ⚠️ [%s] %s\n", err.Phase, err.Message)) + fmt.Fprintf(&sb, "- ⚠️ [%s] %s\n", err.Phase, err.Message) } } sb.WriteString("\n") @@ -1765,7 +1765,7 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note if len(state.Warnings) > 0 { sb.WriteString("## Warnings\n\n") for _, w := range state.Warnings { - sb.WriteString(fmt.Sprintf("- %s\n", w)) + fmt.Fprintf(&sb, "- %s\n", w) } sb.WriteString("\n") } @@ -1777,7 +1777,7 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note sb.WriteString("\n\n") } - sb.WriteString(fmt.Sprintf("---\nLast updated: %s\n", time.Now().Format(time.RFC3339))) + fmt.Fprintf(&sb, "---\nLast updated: %s\n", time.Now().Format(time.RFC3339)) return os.WriteFile(a.progressFilePath(), []byte(sb.String()), 0o600) } diff --git a/internal/agent/phases.go b/internal/agent/phases.go index 66106363..cffea0f6 100644 --- a/internal/agent/phases.go +++ b/internal/agent/phases.go @@ -228,7 +228,7 @@ func (pm *PhaseManager) UpdateState(results map[string]interface{}) { // StateAsContext returns the current state as a string for the prompt func (pm *PhaseManager) StateAsContext() string { - data, _ := json.MarshalIndent(pm.state, "", " ") + data, _ := json.MarshalIndent(pm.state, "", " ") //nolint:gosec // intentional serialization of agent state result := string(data) // Include previous progress if available @@ -1027,10 +1027,10 @@ func eligibilityCheckPhase() *Phase { } manifest, err := tools.FetchManifestFromURL(url) if err != nil { - extra.WriteString(fmt.Sprintf("**%s**: Failed to fetch manifest - %s\n\n", lang, err)) + fmt.Fprintf(&extra, "**%s**: Failed to fetch manifest - %s\n\n", lang, err) continue } - extra.WriteString(fmt.Sprintf("**%s Manifest**:\n```json\n%s\n```\n\n", lang, manifest)) + fmt.Fprintf(&extra, "**%s Manifest**:\n```json\n%s\n```\n\n", lang, manifest) } // Add user guidance if provided diff --git a/internal/agent/tools/abort.go b/internal/agent/tools/abort.go index bec7f645..34acd4dd 100644 --- a/internal/agent/tools/abort.go +++ b/internal/agent/tools/abort.go @@ -104,7 +104,7 @@ func ResetPhaseProgress(workDir string) func(json.RawMessage) (string, error) { } newContent := strings.Join(newLines, "\n") - if err := os.WriteFile(progressPath, []byte(newContent), 0o600); err != nil { + if err := os.WriteFile(progressPath, []byte(newContent), 0o600); err != nil { //nolint:gosec // path is constructed internally for agent progress tracking return "", err } diff --git a/internal/agent/tools/filesystem.go b/internal/agent/tools/filesystem.go index 1ffe57af..886c8ca2 100644 --- a/internal/agent/tools/filesystem.go +++ b/internal/agent/tools/filesystem.go @@ -225,7 +225,7 @@ func (ft *FilesystemTools) PatchFile(input json.RawMessage) (string, error) { modified += "\n" } - if err := os.WriteFile(fullPath, []byte(modified), 0o600); err != nil { + if err := os.WriteFile(fullPath, []byte(modified), 0o600); err != nil { //nolint:gosec // agent tool deliberately writes to user-specified paths return "", fmt.Errorf("failed to write file: %w", err) } diff --git a/internal/agent/ui_headless.go b/internal/agent/ui_headless.go index 94b50b67..1f2915ce 100644 --- a/internal/agent/ui_headless.go +++ b/internal/agent/ui_headless.go @@ -102,7 +102,7 @@ func (u *HeadlessUI) AgentText(text string, streaming bool) { if strings.TrimSpace(text) != "" { width := 90 if utils.IsTerminal() { - if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 { + if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 { //nolint:gosec // file descriptor fits in int width = max(w-4, 40) } } diff --git a/internal/analytics/notice.go b/internal/analytics/notice.go index 7889ffb5..ba5a340a 100644 --- a/internal/analytics/notice.go +++ b/internal/analytics/notice.go @@ -29,7 +29,7 @@ func ShowFirstRunNotice(cmd *cobra.Command) bool { } // Skip if not a TTY (piped output) - if !term.IsTerminal(int(os.Stdout.Fd())) { + if !term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // file descriptor fits in int return false } diff --git a/internal/api/client.go b/internal/api/client.go index 86f82044..5a24c684 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -145,7 +145,7 @@ func buildAuthenticatedRequest( } func (c *TuskClient) executeRequest(httpReq *http.Request) ([]byte, *http.Response, error) { - httpResp, err := c.httpClient.Do(httpReq) + httpResp, err := c.httpClient.Do(httpReq) //nolint:gosec // request URL is configured by the CLI, not user-controlled input if err != nil { return nil, nil, fmt.Errorf("http error: %w", err) } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 3f9afa6c..47ca078f 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -100,7 +100,7 @@ func (a *Authenticator) SaveTokenFile() error { if err := os.MkdirAll(dir, 0o700); err != nil { return fmt.Errorf("cannot create config dir %q: %w", dir, err) } - b, err := json.MarshalIndent(a.Token, "", " ") + b, err := json.MarshalIndent(a.Token, "", " ") //nolint:gosec // intentional persistence of auth token to user config dir if err != nil { return err } @@ -113,11 +113,11 @@ func (a *Authenticator) SaveTokenFile() error { func openBrowser(link string) error { switch runtime.GOOS { case "darwin": - return exec.Command("open", link).Start() + return exec.Command("open", link).Start() //nolint:gosec // opening URL in browser is intentional case "windows": - return exec.Command("rundll32", "url.dll,FileProtocolHandler", link).Start() + return exec.Command("rundll32", "url.dll,FileProtocolHandler", link).Start() //nolint:gosec // opening URL in browser is intentional default: - return exec.Command("xdg-open", link).Start() + return exec.Command("xdg-open", link).Start() //nolint:gosec // opening URL in browser is intentional } } diff --git a/internal/runner/agent_writer.go b/internal/runner/agent_writer.go index 238511ec..7623a49f 100644 --- a/internal/runner/agent_writer.go +++ b/internal/runner/agent_writer.go @@ -103,10 +103,10 @@ func (w *AgentWriter) WriteIndex(totalTests int, passedTests int) error { var sb strings.Builder sb.WriteString("# Tusk Drift Agent Deviation Report\n\n") - sb.WriteString(fmt.Sprintf("Run: %s\n", time.Now().Format("2006-01-02 15:04:05"))) - sb.WriteString(fmt.Sprintf("Tests: %d total, %d passed, %d failed\n", totalTests, passedTests, failedTests)) + fmt.Fprintf(&sb, "Run: %s\n", time.Now().Format("2006-01-02 15:04:05")) + fmt.Fprintf(&sb, "Tests: %d total, %d passed, %d failed\n", totalTests, passedTests, failedTests) if w.baseBranch != "" { - sb.WriteString(fmt.Sprintf("Base Branch: %s\n", w.baseBranch)) + fmt.Fprintf(&sb, "Base Branch: %s\n", w.baseBranch) } // Deviations table @@ -125,15 +125,15 @@ func (w *AgentWriter) WriteIndex(totalTests int, passedTests int) error { sb.WriteString("| # | Test ID | Endpoint | Failure Reason | File |\n") sb.WriteString("|---|---------|----------|----------------|------|\n") for i, d := range deviations { - sb.WriteString(fmt.Sprintf("| %d | %s | %s %s | %s | %s |\n", - i+1, d.testID, d.method, d.path, d.failureType, d.fileName)) + fmt.Fprintf(&sb, "| %d | %s | %s %s | %s | %s |\n", + i+1, d.testID, d.method, d.path, d.failureType, d.fileName) } } if len(passed) > 0 { sb.WriteString("\n## Passing Tests\n") for _, p := range passed { - sb.WriteString(fmt.Sprintf("- %s: %s %s\n", p.testID, p.method, p.path)) + fmt.Fprintf(&sb, "- %s: %s %s\n", p.testID, p.method, p.path) } } @@ -176,15 +176,15 @@ func buildFrontmatter(test Test, result TestResult, server *Server, failureType var sb strings.Builder sb.WriteString("---\n") - sb.WriteString(fmt.Sprintf("deviation_id: %s\n", result.TestID)) - sb.WriteString(fmt.Sprintf("endpoint: %s %s\n", test.Method, test.Path)) - sb.WriteString(fmt.Sprintf("method: %s\n", test.Method)) - sb.WriteString(fmt.Sprintf("path: %s\n", test.Path)) - sb.WriteString(fmt.Sprintf("failure_type: %s\n", failureType)) - sb.WriteString(fmt.Sprintf("status_expected: %d\n", statusExpected)) - sb.WriteString(fmt.Sprintf("status_actual: %d\n", statusActual)) - sb.WriteString(fmt.Sprintf("has_mock_not_found: %t\n", hasMockNotFound)) - sb.WriteString(fmt.Sprintf("duration_ms: %d\n", result.Duration)) + fmt.Fprintf(&sb, "deviation_id: %s\n", result.TestID) + fmt.Fprintf(&sb, "endpoint: %s %s\n", test.Method, test.Path) + fmt.Fprintf(&sb, "method: %s\n", test.Method) + fmt.Fprintf(&sb, "path: %s\n", test.Path) + fmt.Fprintf(&sb, "failure_type: %s\n", failureType) + fmt.Fprintf(&sb, "status_expected: %d\n", statusExpected) + fmt.Fprintf(&sb, "status_actual: %d\n", statusActual) + fmt.Fprintf(&sb, "has_mock_not_found: %t\n", hasMockNotFound) + fmt.Fprintf(&sb, "duration_ms: %d\n", result.Duration) sb.WriteString("---\n\n") return sb.String() @@ -200,7 +200,7 @@ func buildDeviationBody(test Test, result TestResult, server *Server) string { // Request section sb.WriteString("## Request\n") - sb.WriteString(fmt.Sprintf("%s %s\n", test.Request.Method, test.Request.Path)) + fmt.Fprintf(&sb, "%s %s\n", test.Request.Method, test.Request.Path) sb.WriteString("Body:\n") sb.WriteString(formatBodyForAgent(test.Request.Body)) sb.WriteString("\n\n") @@ -232,9 +232,9 @@ func buildDeviationBody(test Test, result TestResult, server *Server) string { } if statusExpected != statusActual { - sb.WriteString(fmt.Sprintf("Status: %d -> %d (CHANGED)\n", statusExpected, statusActual)) + fmt.Fprintf(&sb, "Status: %d -> %d (CHANGED)\n", statusExpected, statusActual) } else { - sb.WriteString(fmt.Sprintf("Status: %d (OK)\n", statusExpected)) + fmt.Fprintf(&sb, "Status: %d (OK)\n", statusExpected) } // Body diff @@ -271,12 +271,12 @@ func buildDeviationBody(test Test, result TestResult, server *Server) string { for _, ev := range matchEvents { opName := matchEventOperationName(ev) quality, scope := matchLevelToStrings(ev.MatchLevel) - sb.WriteString(fmt.Sprintf("| %d | %s | %s | %s | |\n", idx, opName, quality, scope)) + fmt.Fprintf(&sb, "| %d | %s | %s | %s | |\n", idx, opName, quality, scope) idx++ } for _, ev := range mockNotFoundEvents { opName := mockNotFoundOperationName(ev) - sb.WriteString(fmt.Sprintf("| %d | %s | MOCK NOT FOUND | — | No matching recording |\n", idx, opName)) + fmt.Fprintf(&sb, "| %d | %s | MOCK NOT FOUND | — | No matching recording |\n", idx, opName) idx++ } sb.WriteString("\n") @@ -287,12 +287,12 @@ func buildDeviationBody(test Test, result TestResult, server *Server) string { sb.WriteString("## Mock Not Found Events\n") for _, ev := range mockNotFoundEvents { opName := mockNotFoundOperationName(ev) - sb.WriteString(fmt.Sprintf("- %s\n", opName)) + fmt.Fprintf(&sb, "- %s\n", opName) if ev.SpanName != "" { - sb.WriteString(fmt.Sprintf(" Request: %s\n", ev.SpanName)) + fmt.Fprintf(&sb, " Request: %s\n", ev.SpanName) } if ev.StackTrace != "" { - sb.WriteString(fmt.Sprintf(" Stack: %s\n", ev.StackTrace)) + fmt.Fprintf(&sb, " Stack: %s\n", ev.StackTrace) } sb.WriteString(" This outbound call had no matching recording.\n") } @@ -383,7 +383,7 @@ func formatTruncatedDiff(expected, actual any) string { aStr := string(aBytes) var sb strings.Builder - sb.WriteString(fmt.Sprintf("Body diff too large to display (%dKB expected, %dKB actual).\n\n", len(eBytes)/1024, len(aBytes)/1024)) + fmt.Fprintf(&sb, "Body diff too large to display (%dKB expected, %dKB actual).\n\n", len(eBytes)/1024, len(aBytes)/1024) sb.WriteString("### Expected (truncated)\n") if len(eStr) > 1000 { sb.WriteString(eStr[:1000]) diff --git a/internal/runner/comparison.go b/internal/runner/comparison.go index 47ebf932..4102438f 100644 --- a/internal/runner/comparison.go +++ b/internal/runner/comparison.go @@ -22,7 +22,7 @@ func (e *Executor) compareAndGenerateResult(test Test, actualResp *http.Response // Extract decodedType from the server span's output schema // This ensures we parse the actual response the same way we parsed the expected value - var decodedType core.DecodedType = core.DecodedType_DECODED_TYPE_UNSPECIFIED + decodedType := core.DecodedType_DECODED_TYPE_UNSPECIFIED for _, span := range test.Spans { if span.IsRootSpan && span.OutputSchema != nil && span.OutputSchema.Properties != nil { bodySchema := span.OutputSchema.Properties["body"] diff --git a/internal/runner/coverage.go b/internal/runner/coverage.go index d04954b4..a17defb3 100644 --- a/internal/runner/coverage.go +++ b/internal/runner/coverage.go @@ -655,7 +655,7 @@ func WriteCoverageLCOV(path string, aggregate CoverageSnapshot) error { linesHit := 0 for _, line := range lineNums { count := fileData.Lines[strconv.Itoa(line)] - b.WriteString(fmt.Sprintf("DA:%d,%d\n", line, count)) + fmt.Fprintf(&b, "DA:%d,%d\n", line, count) linesFound++ if count > 0 { linesHit++ @@ -680,7 +680,7 @@ func WriteCoverageLCOV(path string, aggregate CoverageSnapshot) error { if i < info.Covered { count = 1 } - b.WriteString(fmt.Sprintf("BRDA:%d,0,%d,%d\n", line, i, count)) + fmt.Fprintf(&b, "BRDA:%d,0,%d,%d\n", line, i, count) branchesFound++ if count > 0 { branchesHit++ @@ -688,11 +688,11 @@ func WriteCoverageLCOV(path string, aggregate CoverageSnapshot) error { } } - b.WriteString(fmt.Sprintf("LF:%d\n", linesFound)) - b.WriteString(fmt.Sprintf("LH:%d\n", linesHit)) + fmt.Fprintf(&b, "LF:%d\n", linesFound) + fmt.Fprintf(&b, "LH:%d\n", linesHit) if branchesFound > 0 { - b.WriteString(fmt.Sprintf("BRF:%d\n", branchesFound)) - b.WriteString(fmt.Sprintf("BRH:%d\n", branchesHit)) + fmt.Fprintf(&b, "BRF:%d\n", branchesFound) + fmt.Fprintf(&b, "BRH:%d\n", branchesHit) } b.WriteString("end_of_record\n") } diff --git a/internal/runner/executor.go b/internal/runner/executor.go index 63d2b324..bdf067a9 100644 --- a/internal/runner/executor.go +++ b/internal/runner/executor.go @@ -1083,7 +1083,7 @@ func OutputResultsSummary(results []TestResult, format string, quiet bool) error // Priority: server/root span > earliest non-server span. // Root span is preferred for inbound-level determinism (e.g., caching keys derived from time). func GetFirstSpanTimestamp(spans []*core.Span) (float64, string) { - var firstTimestamp float64 = math.MaxFloat64 + firstTimestamp := math.MaxFloat64 var serverSpanTimestamp float64 = 0 var foundNonServerSpan bool diff --git a/internal/runner/service.go b/internal/runner/service.go index 7cd1e074..084206d2 100644 --- a/internal/runner/service.go +++ b/internal/runner/service.go @@ -431,13 +431,13 @@ func (e *Executor) setupServiceLogging() error { logsDir = testLogsDir } - if err := os.MkdirAll(logsDir, 0o750); err != nil { + if err := os.MkdirAll(logsDir, 0o750); err != nil { //nolint:gosec // logsDir is configured by the user return fmt.Errorf("failed to create logs directory: %w", err) } timestamp := time.Now().Format("20060102-150405") logPath := filepath.Join(logsDir, fmt.Sprintf("tusk-replay-%s.log", timestamp)) - logFile, err := os.Create(logPath) // #nosec G304 + logFile, err := os.Create(logPath) //nolint:gosec // logsDir is configured by the user if err != nil { return fmt.Errorf("failed to create service log file: %w", err) } diff --git a/internal/runner/validate_executor.go b/internal/runner/validate_executor.go index 56a24be7..1255c6eb 100644 --- a/internal/runner/validate_executor.go +++ b/internal/runner/validate_executor.go @@ -62,7 +62,7 @@ func (ve *ValidateExecutor) validateSingleTrace(ctx context.Context, test *Test) start := time.Now() // Run the test using existing executor logic - testResult, runErr := ve.Executor.RunSingleTest(*test) + testResult, runErr := ve.RunSingleTest(*test) result := ValidationResult{ TraceID: test.TraceID, diff --git a/internal/tui/components/details_panel.go b/internal/tui/components/details_panel.go index fcf83bd9..8fc4119b 100644 --- a/internal/tui/components/details_panel.go +++ b/internal/tui/components/details_panel.go @@ -74,6 +74,6 @@ func (dp *DetailsPanel) View() string { // CopyAllContent copies all panel content to the clipboard func (dp *DetailsPanel) CopyAllContent() tea.Cmd { - text := dp.ContentPanel.GetRawContent() - return dp.ContentPanel.CopyText(text) + text := dp.GetRawContent() + return dp.CopyText(text) } diff --git a/internal/tui/components/log_panel.go b/internal/tui/components/log_panel.go index 9ebf28a5..1bf6ebf3 100644 --- a/internal/tui/components/log_panel.go +++ b/internal/tui/components/log_panel.go @@ -45,7 +45,7 @@ func (lp *LogPanelComponent) View(width, height int) string { defer lp.logMutex.Unlock() // Only rebuild content if something actually changed - currentWrapWidth := lp.ContentPanel.GetViewportWidth() - 2 + currentWrapWidth := lp.GetViewportWidth() - 2 if currentWrapWidth <= 0 { currentWrapWidth = 70 } @@ -138,7 +138,7 @@ func (lp *LogPanelComponent) SetOffset(x, y int) { // rebuildContent rebuilds the viewport content from logs func (lp *LogPanelComponent) rebuildContent(gotoBottom bool) { - wrapWidth := lp.ContentPanel.GetViewportWidth() - 2 + wrapWidth := lp.GetViewportWidth() - 2 if wrapWidth <= 0 { wrapWidth = 70 } @@ -150,25 +150,25 @@ func (lp *LogPanelComponent) rebuildContent(gotoBottom bool) { if logs, exists := lp.testLogs[lp.currentTestID]; exists { sourceLogs = logs } else { - lp.ContentPanel.UpdateContentLines([]string{"No logs available for this test yet..."}) + lp.UpdateContentLines([]string{"No logs available for this test yet..."}) if gotoBottom { - lp.ContentPanel.GotoBottom() + lp.GotoBottom() } return } } - lp.ContentPanel.UpdateContentLines(utils.WrapLines(sourceLogs, wrapWidth)) + lp.UpdateContentLines(utils.WrapLines(sourceLogs, wrapWidth)) if gotoBottom { - lp.ContentPanel.GotoBottom() + lp.GotoBottom() } } // updateTitle updates the panel title based on current state func (lp *LogPanelComponent) updateTitle() { if lp.currentTestID == "" { - lp.ContentPanel.SetTitle("Logs") + lp.SetTitle("Logs") } else { title := "Test Logs" if len(lp.currentTestID) > 35 { @@ -176,12 +176,12 @@ func (lp *LogPanelComponent) updateTitle() { } else { title += ": " + lp.currentTestID } - lp.ContentPanel.SetTitle(title) + lp.SetTitle(title) } } // CopyAllLogs copies all currently visible logs to the clipboard func (lp *LogPanelComponent) CopyAllLogs() tea.Cmd { text := lp.GetRawLogs() - return lp.ContentPanel.CopyText(text) + return lp.CopyText(text) } diff --git a/internal/tui/onboard-cloud/actions_test.go b/internal/tui/onboard-cloud/actions_test.go index fb5183fd..7377d688 100644 --- a/internal/tui/onboard-cloud/actions_test.go +++ b/internal/tui/onboard-cloud/actions_test.go @@ -382,7 +382,7 @@ func TestCreateApiKey_Success(t *testing.T) { w.Header().Set("Content-Type", "application/protobuf") resp := &backend.CreateApiKeyResponse{ Response: &backend.CreateApiKeyResponse_Success{ - Success: &backend.CreateApiKeyResponseSuccess{ + Success: &backend.CreateApiKeyResponseSuccess{ //nolint:gosec // test fixture, not a real credential ApiKeyId: "new-api-key-id", ApiKey: "tusk_api_key_12345", }, @@ -396,7 +396,7 @@ func TestCreateApiKey_Success(t *testing.T) { setupTestEnvironment(t, server.URL) setupTestAuth(t) - model := &Model{ + model := &Model{ //nolint:gosec // test fixture, not a real credential ApiKeyName: "Test API Key", } @@ -433,7 +433,7 @@ func TestCreateApiKey_NotAuthorized(t *testing.T) { setupTestEnvironment(t, server.URL) setupTestAuth(t) - model := &Model{ + model := &Model{ //nolint:gosec // test fixture, not a real credential ApiKeyName: "Test API Key", } @@ -462,7 +462,7 @@ func TestCreateApiKey_InvalidResponse(t *testing.T) { setupTestEnvironment(t, server.URL) setupTestAuth(t) - model := &Model{ + model := &Model{ //nolint:gosec // test fixture, not a real credential ApiKeyName: "Test API Key", } @@ -571,7 +571,7 @@ func TestCreateApiKey_MissingConfig(t *testing.T) { setupTestAuth(t) - model := &Model{ + model := &Model{ //nolint:gosec // test fixture, not a real credential ApiKeyName: "Test API Key", } @@ -686,7 +686,7 @@ func TestCreateApiKey_RequestBody(t *testing.T) { w.Header().Set("Content-Type", "application/protobuf") resp := &backend.CreateApiKeyResponse{ Response: &backend.CreateApiKeyResponse_Success{ - Success: &backend.CreateApiKeyResponseSuccess{ + Success: &backend.CreateApiKeyResponseSuccess{ //nolint:gosec // test fixture, not a real credential ApiKeyId: "new-api-key-id", ApiKey: "tusk_api_key_12345", }, diff --git a/internal/tui/onboard-cloud/helpers_test.go b/internal/tui/onboard-cloud/helpers_test.go index 42cebbef..7406117b 100644 --- a/internal/tui/onboard-cloud/helpers_test.go +++ b/internal/tui/onboard-cloud/helpers_test.go @@ -730,7 +730,7 @@ func setupGitRepoWithRemote(t *testing.T, remoteURL string) string { cmd.Dir = tmpDir _ = cmd.Run() - cmd = exec.Command("git", "remote", "add", "origin", remoteURL) + cmd = exec.Command("git", "remote", "add", "origin", remoteURL) //nolint:gosec // test helper, remoteURL is test-controlled cmd.Dir = tmpDir err = cmd.Run() require.NoError(t, err) diff --git a/internal/tui/onboard-cloud/steps.go b/internal/tui/onboard-cloud/steps.go index f73ec5f7..966f4511 100644 --- a/internal/tui/onboard-cloud/steps.go +++ b/internal/tui/onboard-cloud/steps.go @@ -543,20 +543,20 @@ func (ReviewStep) Description(m *Model) string { summary.WriteString("Here's what was configured:\n\n") - summary.WriteString(fmt.Sprintf("📦 Repository: %s/%s\n\n", m.GitRepoOwner, m.GitRepoName)) + fmt.Fprintf(&summary, "📦 Repository: %s/%s\n\n", m.GitRepoOwner, m.GitRepoName) - summary.WriteString(fmt.Sprintf("💻 Service ID: %s\n\n", m.ServiceID)) + fmt.Fprintf(&summary, "💻 Service ID: %s\n\n", m.ServiceID) summary.WriteString("🔑 API Key\n") switch { case m.HasApiKey: summary.WriteString(" ✓ Already configured (TUSK_API_KEY environment variable)\n\n") case m.ApiKey != "": - summary.WriteString(fmt.Sprintf(" ✓ Created new API key (%s)\n", m.ApiKeyName)) + fmt.Fprintf(&summary, " ✓ Created new API key (%s)\n", m.ApiKeyName) summary.WriteString(" Remember to set TUSK_API_KEY in your environment\n\n") default: - summary.WriteString(fmt.Sprintf(" ⊘ Skipped (you can create one later at %s)\n\n", - styles.LinkStyle.Render("https://app.usetusk.ai/app/settings/api-keys"))) + fmt.Fprintf(&summary, " ⊘ Skipped (you can create one later at %s)\n\n", + styles.LinkStyle.Render("https://app.usetusk.ai/app/settings/api-keys")) } summary.WriteString("⚙️ Recording Configuration\n") @@ -564,11 +564,11 @@ func (ReviewStep) Description(m *Model) string { if samplingMode == "" { samplingMode = "adaptive" } - summary.WriteString(fmt.Sprintf(" • Sampling mode: %s\n", samplingMode)) + fmt.Fprintf(&summary, " • Sampling mode: %s\n", samplingMode) samplingRate, _ := strconv.ParseFloat(m.SamplingRate, 64) - summary.WriteString(fmt.Sprintf(" • Base sampling rate: %.2f (%.0f%% of requests)\n", samplingRate, samplingRate*100)) - summary.WriteString(fmt.Sprintf(" • Export spans: %t\n", m.ExportSpans)) - summary.WriteString(fmt.Sprintf(" • Record environment variables: %t\n\n", m.EnableEnvVarRecording)) + fmt.Fprintf(&summary, " • Base sampling rate: %.2f (%.0f%% of requests)\n", samplingRate, samplingRate*100) + fmt.Fprintf(&summary, " • Export spans: %t\n", m.ExportSpans) + fmt.Fprintf(&summary, " • Record environment variables: %t\n\n", m.EnableEnvVarRecording) summary.WriteString("All settings have been saved to .tusk/config.yaml.\n") summary.WriteString("\nPress [enter] to continue...") diff --git a/internal/tui/onboard/compose_override.go b/internal/tui/onboard/compose_override.go index 04063357..1a76be21 100644 --- a/internal/tui/onboard/compose_override.go +++ b/internal/tui/onboard/compose_override.go @@ -54,10 +54,10 @@ func (m *Model) createDockerComposeOverrideFile() error { var buf strings.Builder buf.WriteString("# Tusk Drift override file for Docker Compose\n") - buf.WriteString(fmt.Sprintf("# Environment variables for service: %s\n", targetService)) + fmt.Fprintf(&buf, "# Environment variables for service: %s\n", targetService) buf.WriteString("# Please double check that this is the correct service.\n\n") buf.WriteString("services:\n") - buf.WriteString(fmt.Sprintf(" %s:\n", targetService)) + fmt.Fprintf(&buf, " %s:\n", targetService) buf.WriteString(" environment:\n") buf.WriteString(" TUSK_DRIFT_MODE: ${TUSK_DRIFT_MODE:-REPLAY}\n") buf.WriteString(" TUSK_MOCK_HOST: ${TUSK_MOCK_HOST:-host.docker.internal}\n") diff --git a/internal/tui/onboard/manifest.go b/internal/tui/onboard/manifest.go index af63215e..8de2ebda 100644 --- a/internal/tui/onboard/manifest.go +++ b/internal/tui/onboard/manifest.go @@ -52,11 +52,11 @@ func formatManifestForDisplay(manifest *Manifest, projectType string) string { if projectType == "python" { sdkName = "Python" } - sb.WriteString(fmt.Sprintf("Tusk Drift %s SDK (v%s) currently supports:\n", sdkName, manifest.SDKVersion)) + fmt.Fprintf(&sb, "Tusk Drift %s SDK (v%s) currently supports:\n", sdkName, manifest.SDKVersion) for _, inst := range manifest.Instrumentations { versions := formatVersions(inst.SupportedVersions) - sb.WriteString(fmt.Sprintf(" • %s: %s\n", inst.PackageName, versions)) + fmt.Fprintf(&sb, " • %s: %s\n", inst.PackageName, versions) } return sb.String() diff --git a/internal/tui/onboard/view.go b/internal/tui/onboard/view.go index 8fd0e93d..e8adbf11 100644 --- a/internal/tui/onboard/view.go +++ b/internal/tui/onboard/view.go @@ -225,7 +225,7 @@ func (m *Model) confirmOutroText() string { if m.DockerType == dockerTypeCompose { b.WriteString("A docker-compose.tusk-override.yml file will be created.\n\n") } - b.WriteString(fmt.Sprintf("Save this configuration to %s/%s? (y/n)\n", configDir, configFile)) + fmt.Fprintf(&b, "Save this configuration to %s/%s? (y/n)\n", configDir, configFile) return b.String() } @@ -241,25 +241,25 @@ func (m *Model) currentFooter() string { func (m *Model) summary() string { var b strings.Builder if strings.TrimSpace(m.ServiceName) != "" { - b.WriteString(fmt.Sprintf("Service: %s\n", styles.SuccessStyle.Render(m.ServiceName))) + fmt.Fprintf(&b, "Service: %s\n", styles.SuccessStyle.Render(m.ServiceName)) } if strings.TrimSpace(m.ServicePort) != "" { - b.WriteString(fmt.Sprintf("Port: %s\n", styles.SuccessStyle.Render(m.ServicePort))) + fmt.Fprintf(&b, "Port: %s\n", styles.SuccessStyle.Render(m.ServicePort)) } if strings.TrimSpace(m.StartCmd) != "" { - b.WriteString(fmt.Sprintf("Start: %s\n", styles.SuccessStyle.Render(m.StartCmd))) + fmt.Fprintf(&b, "Start: %s\n", styles.SuccessStyle.Render(m.StartCmd)) } if strings.TrimSpace(m.StopCmd) != "" { - b.WriteString(fmt.Sprintf("Stop: %s\n", styles.SuccessStyle.Render(m.StopCmd))) + fmt.Fprintf(&b, "Stop: %s\n", styles.SuccessStyle.Render(m.StopCmd)) } if strings.TrimSpace(m.ReadinessCmd) != "" { - b.WriteString(fmt.Sprintf("Readiness command: %s\n", styles.SuccessStyle.Render(m.ReadinessCmd))) + fmt.Fprintf(&b, "Readiness command: %s\n", styles.SuccessStyle.Render(m.ReadinessCmd)) } if strings.TrimSpace(m.ReadinessTimeout) != "" { - b.WriteString(fmt.Sprintf("Timeout: %s\n", styles.SuccessStyle.Render(m.ReadinessTimeout))) + fmt.Fprintf(&b, "Timeout: %s\n", styles.SuccessStyle.Render(m.ReadinessTimeout)) } if strings.TrimSpace(m.ReadinessInterval) != "" { - b.WriteString(fmt.Sprintf("Interval: %s\n", styles.SuccessStyle.Render(m.ReadinessInterval))) + fmt.Fprintf(&b, "Interval: %s\n", styles.SuccessStyle.Render(m.ReadinessInterval)) } if b.Len() > 0 { b.WriteString("\n") diff --git a/internal/tui/test_executor.go b/internal/tui/test_executor.go index 62b3730c..6c012676 100644 --- a/internal/tui/test_executor.go +++ b/internal/tui/test_executor.go @@ -385,14 +385,14 @@ func (h *tuiSlogHandler) Handle(_ context.Context, r slog.Record) error { b.WriteString(" ") b.WriteString(a.Key) b.WriteString("=") - b.WriteString(fmt.Sprintf("%v", a.Value.Any())) + fmt.Fprintf(&b, "%v", a.Value.Any()) } // Append record attrs r.Attrs(func(a slog.Attr) bool { b.WriteString(" ") b.WriteString(a.Key) b.WriteString("=") - b.WriteString(fmt.Sprintf("%v", a.Value.Any())) + fmt.Fprintf(&b, "%v", a.Value.Any()) return true }) _, err := h.writer.Write([]byte(b.String())) diff --git a/internal/utils/ci.go b/internal/utils/ci.go index 2d4a439b..e907b102 100644 --- a/internal/utils/ci.go +++ b/internal/utils/ci.go @@ -31,8 +31,8 @@ func CIWarning(message string) { // https://buildkite.com/docs/agent/v3/cli-annotate ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "buildkite-agent", "annotate", message, "--style", "warning") - _ = cmd.Run() // best-effort, ignore errors + cmd := exec.CommandContext(ctx, "buildkite-agent", "annotate", message, "--style", "warning") //nolint:gosec // intentional invocation of buildkite-agent for CI annotation + _ = cmd.Run() // best-effort, ignore errors case os.Getenv("GITLAB_CI") == "true": // GitLab has no annotation API — use ANSI yellow to stand out in job logs diff --git a/internal/utils/parse_test.go b/internal/utils/parse_test.go index ca9bb197..130c0592 100644 --- a/internal/utils/parse_test.go +++ b/internal/utils/parse_test.go @@ -232,9 +232,10 @@ func TestParseSpansFromFile_MapsOTelSpanKindsToProto(t *testing.T) { // After mapping: OTel SERVER (1) → Proto SERVER (2), OTel CLIENT (2) → Proto CLIENT (3) var server, client *core.Span for _, s := range spans { - if s.Name == "server" { + switch s.Name { + case "server": server = s - } else if s.Name == "client" { + case "client": client = s } } @@ -286,9 +287,10 @@ func TestParseSpansFromFile_DoesNotMapProtoSpanKinds(t *testing.T) { // Values should remain unchanged since they're already Proto format var server, client *core.Span for _, s := range spans { - if s.Name == "server" { + switch s.Name { + case "server": server = s - } else if s.Name == "client" { + case "client": client = s } } @@ -338,9 +340,10 @@ func TestParseSpansFromFile_FixesEnvVarsSnapshotSpan(t *testing.T) { var server, env *core.Span for _, s := range spans { - if s.Name == "server" { + switch s.Name { + case "server": server = s - } else if s.Name == "ENV_VARS_SNAPSHOT" { + case "ENV_VARS_SNAPSHOT": env = s } }