diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index a4d9aba..79d4d4c 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -1893,6 +1893,67 @@ func TestReportOverviewTextShowsIncidentAndAuthorityCues(t *testing.T) { } } +func TestReportOverviewTextWrapsAuthoritySummary(t *testing.T) { + tools := map[string]int{} + for i := 0; i < 10; i++ { + tools[fmt.Sprintf("terminal-very-long-authority-tool-%02d", i)] = 1 + } + sessions := []Session{{ + Name: "wide-authority", + Health: 90, + Metrics: Metrics{ + SourceTool: "codex_cli", + ModelUsed: "gpt-5.1", + ToolUsage: tools, + ToolAuthority: map[string]int{ + ToolAuthorityReadOnlyFiles: 11, + ToolAuthorityTestOrBuild: 12, + ToolAuthorityWriteFiles: 13, + ToolAuthorityPackageInstall: 14, + ToolAuthorityNetworkAccess: 15, + ToolAuthorityShellExec: 16, + ToolAuthorityGitWrite: 17, + ToolAuthorityExternalPublish: 18, + ToolAuthorityUnknown: 19, + }, + HighestAuthority: ToolAuthorityUnknown, + }, + }} + + out := ReportOverview(ComputeOverview(sessions), sessions) + for _, want := range []string{ + "Tool authority", + "Highest category", + "Authority category counts", + "High-authority tools", + } { + if !strings.Contains(out, want) { + t.Fatalf("text overview missing %q:\n%s", want, out) + } + } + + var authorityLines []string + inAuthority := false + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, "── Tool authority ──") { + inAuthority = true + continue + } + if inAuthority && strings.TrimSpace(line) == "" { + break + } + if inAuthority { + authorityLines = append(authorityLines, line) + if got := utf8.RuneCountInString(line); got > 100 { + t.Fatalf("authority line too wide (%d): %q\n%s", got, line, out) + } + } + } + if len(authorityLines) < 5 { + t.Fatalf("expected wrapped authority evidence lines, got %d:\n%s", len(authorityLines), out) + } +} + func TestReportOverviewTextTruncatesUnicodeSafely(t *testing.T) { longName := "x" + strings.Repeat("项目", 24) + "-异常会话" longTool := "x" + strings.Repeat("工具", 32) diff --git a/internal/engine/report.go b/internal/engine/report.go index f74c268..786738c 100644 --- a/internal/engine/report.go +++ b/internal/engine/report.go @@ -1159,10 +1159,14 @@ func ReportOverview(ov Overview, sessions []Session) string { wf(" %s: %s", i18n.T("report_highest_authority"), authority.Highest) } if len(authority.Counts) > 0 { - wf(" %s: %s", i18n.T("report_authority_category_counts"), textAuthorityCounts(authority.Counts)) + for _, line := range textWrappedKeyValues(i18n.T("report_authority_category_counts"), textAuthorityCountValues(authority.Counts), 96) { + wf(" %s", line) + } } if len(authority.HighTools) > 0 { - wf(" %s: %s", i18n.T("report_high_authority_tools"), textCell(strings.Join(authority.HighTools, ", "), 68)) + for _, line := range textWrappedKeyValues(i18n.T("report_high_authority_tools"), textToolValues(authority.HighTools), 96) { + wf(" %s", line) + } } w("") } @@ -1225,12 +1229,52 @@ func ReportOverview(ov Overview, sessions []Session) string { return b.String() } -func textAuthorityCounts(items []authorityCount) string { +func textAuthorityCountValues(items []authorityCount) []string { parts := make([]string, 0, len(items)) for _, item := range items { parts = append(parts, fmt.Sprintf("%s=%d", item.Category, item.Count)) } - return strings.Join(parts, ", ") + return parts +} + +func textToolValues(items []string) []string { + parts := make([]string, 0, len(items)) + for _, item := range items { + parts = append(parts, textCell(item, 40)) + } + return parts +} + +func textWrappedKeyValues(label string, values []string, limit int) []string { + if len(values) == 0 { + return []string{label + ":"} + } + prefix := label + ": " + continuation := strings.Repeat(" ", utf8.RuneCountInString(label)+2) + lines := make([]string, 0, 1) + current := prefix + for _, value := range values { + separator := "" + if current != prefix && current != continuation { + separator = ", " + } + next := separator + value + if utf8.RuneCountInString(current)+utf8.RuneCountInString(next) > limit && current != prefix && current != continuation { + lines = append(lines, current) + current = continuation + value + continue + } + if utf8.RuneCountInString(current)+utf8.RuneCountInString(next) > limit { + valueLimit := limit - utf8.RuneCountInString(current) + if valueLimit < 4 { + valueLimit = 4 + } + next = textCell(value, valueLimit) + } + current += next + } + lines = append(lines, current) + return lines } func textCell(value string, limit int) string {