Files
goclaw/internal/agent/media_filename_test.go
viettranx 1ac08155b0 feat(trace): reliable stop/abort with ctx-aware streams and 2-phase router
Makes the Stop button on the traces page actually stop running traces.
Seven-phase implementation across provider HTTP, agent router, trace
persistence, WS events, tool exec, i18n, and integration tests.

- Provider HTTP+SSE ctx-aware: close socket on cancel via CtxBody wrapper
- Router 2-phase abort: CAS state machine, 3s grace, force-mark fallback
- Trace retry: 3 inline retries + 10-max retry queue, stale recovery 10min
- trace.status WS event: real-time UI updates (invalidates query on receive)
- Tool exec: process-group kill (SIGTERM→3s→SIGKILL), Rod page ctx watch
- i18n: 6 abort toast variants in en/vi/zh
- Integration: 9 scenarios, -race clean

Fixes tenant-ctx loss in forceMarkTraceAborted and retry worker broadcast
(caught by code-reviewer: C1/C2). Stale threshold intentionally 10min
because start_time-based; last_span_at migration is a follow-up.
2026-04-14 18:28:31 +07:00

134 lines
3.5 KiB
Go

package agent
import (
"regexp"
"strings"
"testing"
"unicode/utf8"
)
func TestSanitizeFilename(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
// Latin / Vietnamese
{"vietnamese_basic", "Báo cáo Q4.pdf", "bao-cao-q4"},
{"vietnamese_dong_lower", "đường.doc", "duong"},
{"vietnamese_dong_upper", "Đường.doc", "duong"},
{"english_simple", "Quarterly Report.pdf", "quarterly-report"},
{"english_numbers", "report-2025_v2.pdf", "report-2025-v2"},
{"double_dot", "report.final.pdf", "report-final"},
// Non-Latin preserved
{"cjk_jp", "猫の写真.png", "猫の写真"},
{"cjk_zh", "报告.pdf", "报告"},
{"arabic", "تقرير.pdf", "تقرير"},
// Degenerate / unsafe
{"empty", "", ""},
{"whitespace_only", " ", ""},
// path_traversal: filepath.Base strips dirs → "passwd" is safe slug (no /, no ..).
{"path_traversal", "../../etc/passwd", "passwd"},
{"dotfile", ".env", ""},
{"dot_only", ".", ""},
{"dotdot_only", "..", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := sanitizeFilename(tc.in)
if got != tc.want {
t.Fatalf("sanitizeFilename(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestSanitizeFilenameGuarantees(t *testing.T) {
// Path-traversal-in-middle and unsafe runes must never survive in output.
inputs := []string{
"a/b/c.png",
"a\\b\\c.png",
"name:with:colons.txt",
"quote\"mark.pdf",
"pipe|file.md",
"question?.md",
"star*.md",
"angle<>.md",
"with\x00null.pdf",
"猫/の写真.png",
}
unsafe := regexp.MustCompile(`[/\\:*?"<>|\x00-\x1f]`)
for _, in := range inputs {
out := sanitizeFilename(in)
if unsafe.MatchString(out) {
t.Errorf("sanitizeFilename(%q) = %q contains unsafe char", in, out)
}
if strings.Contains(out, "..") {
t.Errorf("sanitizeFilename(%q) = %q contains ..", in, out)
}
}
}
func TestSanitizeFilenameLengthCap(t *testing.T) {
// Latin: 200-char ASCII stem.
long := strings.Repeat("a", 200) + ".pdf"
out := sanitizeFilename(long)
if n := utf8.RuneCountInString(out); n > maxFilenameRunes {
t.Errorf("Latin cap: got %d runes, want <= %d", n, maxFilenameRunes)
}
// CJK: 100 CJK runes (multi-byte).
cjkLong := strings.Repeat("猫", 100) + ".png"
cjkOut := sanitizeFilename(cjkLong)
if n := utf8.RuneCountInString(cjkOut); n > maxFilenameRunes {
t.Errorf("CJK cap: got %d runes, want <= %d", n, maxFilenameRunes)
}
}
func TestSanitizeFilenameNoLeadingTrailingDash(t *testing.T) {
cases := []string{
" hello .pdf",
"---weird---.pdf",
"$$$money$$$.pdf",
}
for _, in := range cases {
out := sanitizeFilename(in)
if out == "" {
continue
}
if strings.HasPrefix(out, "-") || strings.HasSuffix(out, "-") {
t.Errorf("sanitizeFilename(%q) = %q has leading/trailing dash", in, out)
}
}
}
func TestShortID(t *testing.T) {
got := shortID(8)
if len(got) != 8 {
t.Fatalf("shortID(8) len = %d, want 8", len(got))
}
if !regexp.MustCompile(`^[0-9a-f]{8}$`).MatchString(got) {
t.Fatalf("shortID(8) = %q, not lowercase hex", got)
}
// Two calls should (almost always) differ — 32-bit entropy.
if got == shortID(8) {
// Retry once — if still equal, something is wrong.
if got == shortID(8) {
t.Fatalf("shortID(8) produced identical output twice in a row: %q", got)
}
}
}
func TestShortIDPanicsOnInvalidInput(t *testing.T) {
for _, n := range []int{0, -2, 3, 7} {
func() {
defer func() {
if r := recover(); r == nil {
t.Errorf("shortID(%d) did not panic", n)
}
}()
_ = shortID(n)
}()
}
}