From 06deb2f27dc17e3863123a5e49e296f5c466355b Mon Sep 17 00:00:00 2001 From: greymoth <246701683+greymoth-jp@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:31:19 +0900 Subject: [PATCH] fix: align check summary table for wide-character skill names The skill summary table aligns columns by terminal display width (padRight uses runewidth.StringWidth), but the dynamic Skill column width was computed with utf8.RuneCountInString and truncateName cut by rune count. A wide character (CJK, fullwidth) is one rune but two display cells, so wide skill names overflowed the column and pushed every later column to the right. Measure the column and truncate by display width to match padRight. --- cmd/waza/cmd_check.go | 19 ++++++++++------- cmd/waza/cmd_check_test.go | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/cmd/waza/cmd_check.go b/cmd/waza/cmd_check.go index 7ca358f5..29e773b4 100644 --- a/cmd/waza/cmd_check.go +++ b/cmd/waza/cmd_check.go @@ -9,7 +9,6 @@ import ( "path/filepath" "strings" "time" - "unicode/utf8" "github.com/mattn/go-runewidth" @@ -246,15 +245,17 @@ func printCheckSummaryTable(w interface{ Write([]byte) (int, error) }, reports [ const maxNameWidth = 25 const minNameWidth = 10 - // Compute dynamic column width from the longest skill name. + // Compute dynamic column width (in terminal display cells) from the longest + // skill name. Display width, rather than the rune count, is used so the + // column stays aligned with padRight for wide characters (CJK, emoji). nameWidth := len("Skill") for _, r := range reports { n := r.skillName if n == "" { n = "unnamed" } - if runeLen := utf8.RuneCountInString(n); runeLen > nameWidth { - nameWidth = runeLen + if w := runewidth.StringWidth(n); w > nameWidth { + nameWidth = w } } if nameWidth > maxNameWidth { @@ -337,13 +338,15 @@ func printCheckSummaryTable(w interface{ Write([]byte) (int, error) }, reports [ fmt.Fprintf(w, "\n") //nolint:errcheck } -// truncateName shortens a name to maxLen runes, replacing the last rune with "…" if needed. +// truncateName shortens a name so that its terminal display width is at most +// maxLen cells, appending "…" when truncation happens. Display width is used +// (rather than the rune count) so that wide characters such as CJK and emoji +// stay aligned with padRight in the summary table. func truncateName(name string, maxLen int) string { - runes := []rune(name) - if len(runes) <= maxLen { + if runewidth.StringWidth(name) <= maxLen { return name } - return string(runes[:maxLen-1]) + "…" + return runewidth.Truncate(name, maxLen, "…") } // padRight pads s with spaces so its terminal display width reaches width. diff --git a/cmd/waza/cmd_check_test.go b/cmd/waza/cmd_check_test.go index ab215af1..aa6dd920 100644 --- a/cmd/waza/cmd_check_test.go +++ b/cmd/waza/cmd_check_test.go @@ -10,6 +10,7 @@ import ( "testing" "unicode/utf8" + "github.com/mattn/go-runewidth" "github.com/microsoft/waza/cmd/waza/tokens" "github.com/microsoft/waza/internal/scaffold" "github.com/microsoft/waza/internal/scoring" @@ -692,6 +693,48 @@ func TestPrintCheckSummaryTable_DynamicWidth(t *testing.T) { } } +func TestPrintCheckSummaryTable_WideCharAlignment(t *testing.T) { + // A skill name with fullwidth (CJK) characters must not break column + // alignment. The table pads by terminal display width (see padRight), so the + // dynamic name column has to be measured by display width too; otherwise a + // wide name overflows its column and shifts every following column right. + reports := []*readinessReport{ + {skillName: "日本語スキル", complianceLevel: "Medium-High", tokenCount: 100, tokenLimit: 500}, + {skillName: "azure-ai", complianceLevel: "Medium-High", tokenCount: 100, tokenLimit: 500}, + } + var buf bytes.Buffer + printCheckSummaryTable(&buf, reports) + + // The display column at which the second column begins must be identical on + // the header row ("Compliance") and on every data row ("Medium-High"). + displayCol := func(line, token string) (int, bool) { + idx := strings.Index(line, token) + if idx < 0 { + return 0, false + } + return runewidth.StringWidth(line[:idx]), true + } + + lines := strings.Split(buf.String(), "\n") + var want int + found := false + for _, line := range lines { + if col, ok := displayCol(line, "Compliance"); ok { + want, found = col, true + break + } + } + require.True(t, found, "header row with the Compliance column was not found") + + for _, line := range lines { + if !strings.Contains(line, "Medium-High") { + continue + } + col, _ := displayCol(line, "Medium-High") + assert.Equal(t, want, col, "second column misaligned for row %q", line) + } +} + func TestCheckCommandJSONOutput(t *testing.T) { tmpDir := t.TempDir() skillContent := `---