diff --git a/cmd/verifyexamples/verifier.go b/cmd/verifyexamples/verifier.go index 4e207c26..88ad7ec6 100644 --- a/cmd/verifyexamples/verifier.go +++ b/cmd/verifyexamples/verifier.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "strings" + "unicode/utf8" "github.com/microsoft/agent-framework-go/agent" ) @@ -139,5 +140,11 @@ func truncate(text string, maxLength int) string { if len(text) <= maxLength { return text } - return text[:maxLength] + "... (truncated)" + // Back off to a rune boundary so truncation never splits a multi-byte rune + // (examples print °C, emoji, etc.) and emits invalid UTF-8 into logs/CSV. + end := maxLength + for end > 0 && !utf8.RuneStart(text[end]) { + end-- + } + return text[:end] + "... (truncated)" } diff --git a/cmd/verifyexamples/verifier_test.go b/cmd/verifyexamples/verifier_test.go index e1b84fa6..ae8e84c7 100644 --- a/cmd/verifyexamples/verifier_test.go +++ b/cmd/verifyexamples/verifier_test.go @@ -4,7 +4,9 @@ package main import ( "context" + "strings" "testing" + "unicode/utf8" ) func TestVerifyDeterministicOutput(t *testing.T) { @@ -33,3 +35,18 @@ func TestVerifyRequiresAIAgentForSemanticChecks(t *testing.T) { t.Fatalf("Failures = %#v, want one", result.Failures) } } + +func TestTruncateDoesNotSplitRunes(t *testing.T) { + // "°" is two bytes (0xC2 0xB0); truncating at byte 1 must not split it. + got := truncate("a°cdef", 2) + if !utf8.ValidString(got) { + t.Fatalf("truncate produced invalid UTF-8: %q", got) + } + if !strings.HasPrefix(got, "a") || strings.ContainsRune(got, '�') { + t.Fatalf("truncate = %q, want a clean rune-boundary cut", got) + } + // Short input is returned unchanged. + if truncate("ok", 10) != "ok" { + t.Fatalf("short input was modified") + } +}