From cdd36e9164d078dea365f28d19834c7cc0a87d7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:13:52 +0000 Subject: [PATCH] Add --filtered-integrity flag to logs command Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/context_cancellation_test.go | 4 +- pkg/cli/logs_ci_scenario_test.go | 1 + pkg/cli/logs_command.go | 5 +- pkg/cli/logs_download_test.go | 4 +- pkg/cli/logs_filtering_test.go | 82 +++++++++++++++++++++++++- pkg/cli/logs_json_stderr_order_test.go | 4 +- pkg/cli/logs_orchestrator.go | 40 ++++++++++++- 7 files changed, 131 insertions(+), 9 deletions(-) diff --git a/pkg/cli/context_cancellation_test.go b/pkg/cli/context_cancellation_test.go index 612cf9f45b4..df9fb11e711 100644 --- a/pkg/cli/context_cancellation_test.go +++ b/pkg/cli/context_cancellation_test.go @@ -71,7 +71,7 @@ func TestDownloadWorkflowLogsWithCancellation(t *testing.T) { cancel() // Try to download logs with a cancelled context - err := DownloadWorkflowLogs(ctx, "", 10, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "", "") + err := DownloadWorkflowLogs(ctx, "", 10, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "", "", false) // Should return context.Canceled error assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled") @@ -111,7 +111,7 @@ func TestDownloadWorkflowLogsTimeoutRespected(t *testing.T) { start := time.Now() // Use a workflow name that doesn't exist to avoid actual network calls - _ = DownloadWorkflowLogs(ctx, "nonexistent-workflow-12345", 100, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 1, "", "") + _ = DownloadWorkflowLogs(ctx, "nonexistent-workflow-12345", 100, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 1, "", "", false) elapsed := time.Since(start) // Should complete within reasonable time (give 5 seconds buffer for test overhead) diff --git a/pkg/cli/logs_ci_scenario_test.go b/pkg/cli/logs_ci_scenario_test.go index 6ab304abff4..607d4f5d6f2 100644 --- a/pkg/cli/logs_ci_scenario_test.go +++ b/pkg/cli/logs_ci_scenario_test.go @@ -50,6 +50,7 @@ func TestLogsJSONOutputWithNoRuns(t *testing.T) { 10, // timeout "summary.json", // summaryFile "", // safeOutputType + false, // filteredIntegrity ) // Restore stdout and read output diff --git a/pkg/cli/logs_command.go b/pkg/cli/logs_command.go index c2e67126f44..56f1076ea04 100644 --- a/pkg/cli/logs_command.go +++ b/pkg/cli/logs_command.go @@ -73,6 +73,7 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` logs --safe-output create-issue # Filter logs with create_issue messages ` + string(constants.CLIExtensionPrefix) + ` logs --ref main # Filter logs by branch or tag ` + string(constants.CLIExtensionPrefix) + ` logs --ref feature-xyz # Filter logs by feature branch + ` + string(constants.CLIExtensionPrefix) + ` logs --filtered-integrity # Filter logs with DIFC integrity-filtered items in gateway logs # Run ID range filtering ` + string(constants.CLIExtensionPrefix) + ` logs --after-run-id 1000 # Filter runs after run ID 1000 @@ -138,6 +139,7 @@ Examples: repoOverride, _ := cmd.Flags().GetString("repo") summaryFile, _ := cmd.Flags().GetString("summary-file") safeOutputType, _ := cmd.Flags().GetString("safe-output") + filteredIntegrity, _ := cmd.Flags().GetBool("filtered-integrity") // Resolve relative dates to absolute dates for GitHub CLI now := time.Now() @@ -172,7 +174,7 @@ Examples: logsCommandLog.Printf("Executing logs download: workflow=%s, count=%d, engine=%s", workflowName, count, engine) - return DownloadWorkflowLogs(cmd.Context(), workflowName, count, startDate, endDate, outputDir, engine, ref, beforeRunID, afterRunID, repoOverride, verbose, toolGraph, noStaged, firewallOnly, noFirewall, parse, jsonOutput, timeout, summaryFile, safeOutputType) + return DownloadWorkflowLogs(cmd.Context(), workflowName, count, startDate, endDate, outputDir, engine, ref, beforeRunID, afterRunID, repoOverride, verbose, toolGraph, noStaged, firewallOnly, noFirewall, parse, jsonOutput, timeout, summaryFile, safeOutputType, filteredIntegrity) }, } @@ -191,6 +193,7 @@ Examples: logsCmd.Flags().Bool("firewall", false, "Filter to only runs with firewall enabled") logsCmd.Flags().Bool("no-firewall", false, "Filter to only runs without firewall enabled") logsCmd.Flags().String("safe-output", "", "Filter to runs containing a specific safe output type (e.g., create-issue, missing-tool, missing-data)") + logsCmd.Flags().Bool("filtered-integrity", false, "Filter to runs with DIFC integrity-filtered items in the gateway logs") logsCmd.Flags().Bool("parse", false, "Run JavaScript parsers on agent logs and firewall logs, writing Markdown to log.md and firewall.md") addJSONFlag(logsCmd) logsCmd.Flags().Int("timeout", 0, "Download timeout in seconds (0 = no timeout)") diff --git a/pkg/cli/logs_download_test.go b/pkg/cli/logs_download_test.go index bf5feb031e7..307edac3441 100644 --- a/pkg/cli/logs_download_test.go +++ b/pkg/cli/logs_download_test.go @@ -21,7 +21,7 @@ func TestDownloadWorkflowLogs(t *testing.T) { // Test the DownloadWorkflowLogs function // This should either fail with auth error (if not authenticated) // or succeed with no results (if authenticated but no workflows match) - err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "") + err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "", false) // If GitHub CLI is authenticated, the function may succeed but find no results // If not authenticated, it should return an auth error @@ -257,7 +257,7 @@ func TestDownloadWorkflowLogsWithEngineFilter(t *testing.T) { if !tt.expectError { // For valid engines, test that the function can be called without panic // It may still fail with auth errors, which is expected - err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", tt.engine, "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "") + err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", tt.engine, "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "", false) // Clean up any created directories os.RemoveAll("./test-logs") diff --git a/pkg/cli/logs_filtering_test.go b/pkg/cli/logs_filtering_test.go index fb467d2b54c..c11e44034cd 100644 --- a/pkg/cli/logs_filtering_test.go +++ b/pkg/cli/logs_filtering_test.go @@ -17,7 +17,7 @@ func TestLogsCommandFlags(t *testing.T) { cmd := NewLogsCommand() // Check that all expected flags are present - expectedFlags := []string{"count", "start-date", "end-date", "output", "engine", "ref", "before-run-id", "after-run-id"} + expectedFlags := []string{"count", "start-date", "end-date", "output", "engine", "ref", "before-run-id", "after-run-id", "filtered-integrity"} for _, flagName := range expectedFlags { flag := cmd.Flags().Lookup(flagName) @@ -444,3 +444,83 @@ func TestFindAgentLogFile(t *testing.T) { } }) } + +// TestRunHasDifcFilteredItems verifies the DIFC filtered-integrity filter helper. +func TestRunHasDifcFilteredItems(t *testing.T) { + const gatewayWithDifc = `{"timestamp":"2025-01-01T00:00:00Z","type":"DIFC_FILTERED","server_id":"github","tool_name":"create_issue","reason":"integrity"}` + "\n" + const gatewayWithoutDifc = `{"timestamp":"2025-01-01T00:00:00Z","event":"tool_call","server_name":"github","tool_name":"list_issues","duration":10}` + "\n" + + tests := []struct { + name string + fileContent string + filePath func(dir string) string + want bool + }{ + { + name: "gateway.jsonl with DIFC_FILTERED event", + fileContent: gatewayWithDifc, + filePath: func(dir string) string { return filepath.Join(dir, "gateway.jsonl") }, + want: true, + }, + { + name: "gateway.jsonl without DIFC_FILTERED events", + fileContent: gatewayWithoutDifc, + filePath: func(dir string) string { return filepath.Join(dir, "gateway.jsonl") }, + want: false, + }, + { + name: "mcp-logs/gateway.jsonl with DIFC_FILTERED event", + fileContent: gatewayWithDifc, + filePath: func(dir string) string { return filepath.Join(dir, "mcp-logs", "gateway.jsonl") }, + want: true, + }, + { + name: "no gateway logs present", + fileContent: "", + filePath: nil, // no file created + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := testutil.TempDir(t, "difc-filter-*") + + if tt.filePath != nil { + path := tt.filePath(dir) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + if err := os.WriteFile(path, []byte(tt.fileContent), 0644); err != nil { + t.Fatalf("failed to write gateway file: %v", err) + } + } + + got, err := runHasDifcFilteredItems(dir, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("runHasDifcFilteredItems() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestFilteredIntegrityFlag verifies the --filtered-integrity flag is registered correctly. +func TestFilteredIntegrityFlag(t *testing.T) { + cmd := NewLogsCommand() + + flag := cmd.Flags().Lookup("filtered-integrity") + if flag == nil { + t.Fatal("Expected flag 'filtered-integrity' not found in logs command") + } + + if flag.DefValue != "false" { + t.Errorf("Expected 'filtered-integrity' default to be 'false', got: %s", flag.DefValue) + } + + if flag.Usage == "" { + t.Error("Expected 'filtered-integrity' flag to have usage text") + } +} diff --git a/pkg/cli/logs_json_stderr_order_test.go b/pkg/cli/logs_json_stderr_order_test.go index f622b21772b..b3f77c9f7bb 100644 --- a/pkg/cli/logs_json_stderr_order_test.go +++ b/pkg/cli/logs_json_stderr_order_test.go @@ -58,6 +58,7 @@ func TestLogsJSONOutputBeforeStderr(t *testing.T) { 10, // timeout "summary.json", // summaryFile "", // safeOutputType + false, // filteredIntegrity ) // Close writers first @@ -177,7 +178,8 @@ func TestLogsJSONAndStderrRedirected(t *testing.T) { true, // jsonOutput 10, "summary.json", - "", // safeOutputType + "", // safeOutputType + false, // filteredIntegrity ) // Close the writer diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index d6654f4ad02..b1893a08e8d 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -41,8 +41,8 @@ func getMaxConcurrentDownloads() int { } // DownloadWorkflowLogs downloads and analyzes workflow logs with metrics -func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, startDate, endDate, outputDir, engine, ref string, beforeRunID, afterRunID int64, repoOverride string, verbose bool, toolGraph bool, noStaged bool, firewallOnly bool, noFirewall bool, parse bool, jsonOutput bool, timeout int, summaryFile string, safeOutputType string) error { - logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType) +func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, startDate, endDate, outputDir, engine, ref string, beforeRunID, afterRunID int64, repoOverride string, verbose bool, toolGraph bool, noStaged bool, firewallOnly bool, noFirewall bool, parse bool, jsonOutput bool, timeout int, summaryFile string, safeOutputType string, filteredIntegrity bool) error { + logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s, filteredIntegrity=%v", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType, filteredIntegrity) // Ensure .github/aw/logs/.gitignore exists on every invocation if err := ensureLogsGitignore(); err != nil { @@ -309,6 +309,23 @@ func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, s } } + // Apply filtered-integrity filtering if --filtered-integrity flag is specified + if filteredIntegrity { + hasFiltered, checkErr := runHasDifcFilteredItems(result.LogsPath, verbose) + if checkErr != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check DIFC filtered items for run %d: %v", result.Run.DatabaseID, checkErr))) + continue + } + + if !hasFiltered { + logsOrchestratorLog.Printf("Skipping run %d: no DIFC filtered items found", result.Run.DatabaseID) + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skipping run %d: no DIFC integrity-filtered items found in gateway logs", result.Run.DatabaseID))) + } + continue + } + } + // Update run with metrics and path run := result.Run run.TokenUsage = result.Metrics.TokenUsage @@ -869,3 +886,22 @@ func runContainsSafeOutputType(runDir string, safeOutputType string, verbose boo return false, nil } + +// runHasDifcFilteredItems checks if a run's gateway logs contain any DIFC_FILTERED events. +// It parses the gateway logs (falling back to rpc-messages.jsonl when gateway.jsonl is absent) +// and returns true when at least one DIFC integrity- or secrecy-filtered event is present. +func runHasDifcFilteredItems(runDir string, verbose bool) (bool, error) { + logsOrchestratorLog.Printf("Checking run for DIFC filtered items: dir=%s", runDir) + + gatewayMetrics, err := parseGatewayLogs(runDir, verbose) + if err != nil { + // No gateway log file present — not an error for workflows without MCP + return false, nil + } + + if gatewayMetrics == nil { + return false, nil + } + + return gatewayMetrics.TotalFiltered > 0, nil +}