From c5628982e39447a61c3242ddf646fbac70214ef4 Mon Sep 17 00:00:00 2001 From: creatang Date: Tue, 7 Apr 2026 18:14:40 +0800 Subject: [PATCH] test(tui): improve coverage for bridge and infra helpers --- internal/tui/components/components_test.go | 50 +++ internal/tui/infra/infra_test.go | 206 ++++++++++ internal/tui/services/runtime_bridge_test.go | 378 +++++++++++++++++++ internal/tui/services/services_test.go | 27 ++ 4 files changed, 661 insertions(+) diff --git a/internal/tui/components/components_test.go b/internal/tui/components/components_test.go index a4a38432..51482565 100644 --- a/internal/tui/components/components_test.go +++ b/internal/tui/components/components_test.go @@ -137,3 +137,53 @@ func TestRenderSessionRow(t *testing.T) { t.Fatalf("expected updated-at label in row, got %q", row) } } + +func TestViewHelperBranches(t *testing.T) { + if got := fallback("primary", "fallback"); got != "primary" { + t.Fatalf("expected primary fallback value, got %q", got) + } + if got := fallback("", "fallback"); got != "fallback" { + t.Fatalf("expected fallback value, got %q", got) + } + + if got := trimMiddle("abcdef", 0); got != "" { + t.Fatalf("expected empty string for non-positive limit, got %q", got) + } + if got := trimMiddle("abcdef", 3); got != "abc" { + t.Fatalf("expected hard truncate for short limit, got %q", got) + } + if got := trimMiddle("abcdefghij", 7); got != "ab...ij" { + t.Fatalf("expected middle trim output, got %q", got) + } + + if got := trimRunes("abcdef", 3); got != "abcdef" { + t.Fatalf("expected original text when limit < 4, got %q", got) + } + if got := trimRunes("abcdef", 5); got != "ab..." { + t.Fatalf("expected rune-safe ellipsis trim, got %q", got) + } + + if got := clamp(-1, 0, 10); got != 0 { + t.Fatalf("expected clamp to min, got %d", got) + } + if got := clamp(20, 0, 10); got != 10 { + t.Fatalf("expected clamp to max, got %d", got) + } + if got := clamp(6, 0, 10); got != 6 { + t.Fatalf("expected clamp to keep in-range value, got %d", got) + } +} + +func TestNormalizeBlockRightEdgeBlankContent(t *testing.T) { + blank := " \n\t" + if got := NormalizeBlockRightEdge(blank, 20); got != blank { + t.Fatalf("expected blank content passthrough, got %q", got) + } +} + +func TestCompactStatusTextWithLimit(t *testing.T) { + text := "\n first useful line \nsecond line" + if got := CompactStatusText(text, 9); got != "fir...ine" { + t.Fatalf("expected first non-empty line compacted with ellipsis, got %q", got) + } +} diff --git a/internal/tui/infra/infra_test.go b/internal/tui/infra/infra_test.go index b677f89c..1e9775ec 100644 --- a/internal/tui/infra/infra_test.go +++ b/internal/tui/infra/infra_test.go @@ -1,12 +1,17 @@ package infra import ( + "context" "encoding/binary" "os" "path/filepath" + goruntime "runtime" "strings" "testing" "unicode/utf16" + "unicode/utf8" + + "neo-code/internal/config" ) func TestShellArgs(t *testing.T) { @@ -16,6 +21,12 @@ func TestShellArgs(t *testing.T) { if got := ShellArgs("sh", "pwd"); len(got) != 3 || got[0] != "sh" || got[2] != "pwd" { t.Fatalf("unexpected sh args: %+v", got) } + if got := ShellArgs("powershell", "Get-Location"); len(got) != 4 || got[0] != "powershell" { + t.Fatalf("unexpected powershell args: %+v", got) + } + if got := ShellArgs("pwsh", "Get-Location"); len(got) != 4 || got[0] != "powershell" { + t.Fatalf("unexpected pwsh args: %+v", got) + } if got := ShellArgs("unknown", "git status"); len(got) != 4 || got[0] != "powershell" { t.Fatalf("expected powershell fallback, got %+v", got) } @@ -46,6 +57,47 @@ func TestDecodeWorkspaceOutputUTF16LE(t *testing.T) { } } +func TestDecodeWorkspaceOutputUTF16BE(t *testing.T) { + utf16Data := utf16.Encode([]rune("UTF16 BE")) + buf := make([]byte, 2+len(utf16Data)*2) + buf[0], buf[1] = 0xFE, 0xFF + for i, word := range utf16Data { + binary.BigEndian.PutUint16(buf[2+i*2:], word) + } + + got := DecodeWorkspaceOutput(buf) + if !strings.Contains(got, "UTF16 BE") { + t.Fatalf("expected decoded utf16 big-endian content, got %q", got) + } +} + +func TestDecodeWorkspaceOutputHeuristicsAndEdges(t *testing.T) { + evenWithoutBOM := []byte{0x61, 0x00, 0x62, 0x00} + got := DecodeWorkspaceOutput(evenWithoutBOM) + if !strings.Contains(got, "ab") { + t.Fatalf("expected utf16 heuristic decode result to contain ab, got %q", got) + } + + if got := DecodeWorkspaceOutput([]byte{0xE4, 0xBD, 0xA0}); utf8.ValidString(got) && strings.TrimSpace(got) == "" { + t.Fatalf("expected odd-length raw bytes to keep readable content, got %q", got) + } + + if got := decodeUTF16([]byte{0x61}, true); got != "a" { + t.Fatalf("expected short utf16 input to return raw text, got %q", got) + } + if got := decodeUTF16([]byte{0x61, 0x00, 0x62}, true); !strings.Contains(got, "a") { + t.Fatalf("expected odd-length utf16 input to decode after trimming, got %q", got) + } +} + +func TestDecodedTextScore(t *testing.T) { + printable := decodedTextScore("hello world") + replacement := decodedTextScore(string([]rune{'\uFFFD'})) + if printable <= replacement { + t.Fatalf("expected printable text score > replacement score, got printable=%d replacement=%d", printable, replacement) + } +} + func TestCollectWorkspaceFiles(t *testing.T) { root := t.TempDir() mustWrite := func(rel string) { @@ -77,6 +129,38 @@ func TestCollectWorkspaceFiles(t *testing.T) { } } +func TestCollectWorkspaceFilesLimitAndErrors(t *testing.T) { + root := t.TempDir() + mustWrite := func(rel string) { + t.Helper() + path := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir %s: %v", rel, err) + } + if err := os.WriteFile(path, []byte(rel), 0o644); err != nil { + t.Fatalf("write %s: %v", rel, err) + } + } + mustWrite("b.txt") + mustWrite("a.txt") + + files, err := CollectWorkspaceFiles(root, 1) + if err != nil { + t.Fatalf("CollectWorkspaceFiles(limit=1) error = %v", err) + } + if len(files) != 1 { + t.Fatalf("expected exactly one file due to limit, got %v", files) + } + if files[0] != "a.txt" && files[0] != "b.txt" { + t.Fatalf("unexpected limited file list: %v", files) + } + + _, err = CollectWorkspaceFiles(filepath.Join(root, "missing"), 10) + if err == nil { + t.Fatalf("expected missing root to produce walk error") + } +} + func TestCopyTextUsesInjectedWriter(t *testing.T) { original := clipboardWriteAll t.Cleanup(func() { clipboardWriteAll = original }) @@ -130,3 +214,125 @@ func TestCachedMarkdownRendererCacheEviction(t *testing.T) { t.Fatalf("expected single cache entry after eviction, got order=%d cache=%d", renderer.CacheOrderCount(), renderer.CacheCount()) } } + +func TestCachedMarkdownRendererDefaultsAndSetMax(t *testing.T) { + renderer := NewCachedMarkdownRenderer("", -1, "(empty)") + if renderer.style != "dark" { + t.Fatalf("expected default style dark, got %q", renderer.style) + } + if renderer.maxCacheEntries != 0 { + t.Fatalf("expected negative max cache to normalize to 0, got %d", renderer.maxCacheEntries) + } + + renderer.SetMaxCacheEntries(2) + if _, err := renderer.Render("one", 20); err != nil { + t.Fatalf("Render(one) error = %v", err) + } + if _, err := renderer.Render("two", 20); err != nil { + t.Fatalf("Render(two) error = %v", err) + } + if _, err := renderer.Render("three", 20); err != nil { + t.Fatalf("Render(three) error = %v", err) + } + if renderer.CacheCount() != 2 { + t.Fatalf("expected cache eviction to keep 2 entries, got %d", renderer.CacheCount()) + } + + renderer.SetMaxCacheEntries(1) + if renderer.CacheCount() != 1 || renderer.CacheOrderCount() != 1 { + t.Fatalf("expected cache trim to one entry, got cache=%d order=%d", renderer.CacheCount(), renderer.CacheOrderCount()) + } + + renderer.SetMaxCacheEntries(-1) + if renderer.CacheCount() != 0 || renderer.CacheOrderCount() != 0 { + t.Fatalf("expected cache trim to zero after negative max, got cache=%d order=%d", renderer.CacheCount(), renderer.CacheOrderCount()) + } +} + +func TestCachedMarkdownRendererCacheDisabledAndWidthFloor(t *testing.T) { + renderer := NewCachedMarkdownRenderer("dark", 0, "(empty)") + if _, err := renderer.Render("same", 1); err != nil { + t.Fatalf("Render(width=1) error = %v", err) + } + if _, err := renderer.Render("same", 15); err != nil { + t.Fatalf("Render(width=15) error = %v", err) + } + if renderer.CacheCount() != 0 { + t.Fatalf("expected disabled cache to keep zero entries, got %d", renderer.CacheCount()) + } + if renderer.RendererCount() != 1 { + t.Fatalf("expected render width floor to reuse one renderer, got %d", renderer.RendererCount()) + } +} + +func TestDefaultWorkspaceCommandExecutor(t *testing.T) { + workdir := t.TempDir() + shellName, successCmd, noOutputCmd, failCmd, sleepCmd := workspaceExecutorCommands() + cfg := config.Config{ + Workdir: workdir, + Shell: shellName, + ToolTimeoutSec: 1, + } + + if _, err := DefaultWorkspaceCommandExecutor(context.Background(), cfg, "", " "); err == nil { + t.Fatalf("expected empty command to fail") + } + + output, err := DefaultWorkspaceCommandExecutor(context.Background(), cfg, "", successCmd) + if err != nil { + t.Fatalf("expected success command to pass, got error %v (output=%q)", err, output) + } + if !strings.Contains(strings.ToLower(output), "ok") { + t.Fatalf("expected success output to contain ok, got %q", output) + } + + output, err = DefaultWorkspaceCommandExecutor(context.Background(), cfg, workdir, noOutputCmd) + if err != nil { + t.Fatalf("expected no-output command to pass, got error %v (output=%q)", err, output) + } + if output != "(no output)" { + t.Fatalf("expected no-output placeholder, got %q", output) + } + + output, err = DefaultWorkspaceCommandExecutor(context.Background(), cfg, workdir, failCmd) + if err == nil { + t.Fatalf("expected failing command to return error, output=%q", output) + } + if strings.TrimSpace(output) == "" { + t.Fatalf("expected failing command to return sanitized output") + } + + output, err = DefaultWorkspaceCommandExecutor(context.Background(), cfg, workdir, sleepCmd) + if err == nil || !strings.Contains(err.Error(), "timed out") { + t.Fatalf("expected timeout error, got err=%v output=%q", err, output) + } +} + +func TestDefaultWorkspaceCommandExecutorUsesDefaultTimeout(t *testing.T) { + workdir := t.TempDir() + shellName, successCmd, _, _, _ := workspaceExecutorCommands() + cfg := config.Config{ + Workdir: workdir, + Shell: shellName, + ToolTimeoutSec: 0, + } + + if output, err := DefaultWorkspaceCommandExecutor(context.Background(), cfg, "", successCmd); err != nil || !strings.Contains(strings.ToLower(output), "ok") { + t.Fatalf("expected default timeout path to execute command, output=%q err=%v", output, err) + } +} + +func workspaceExecutorCommands() (shell string, success string, noOutput string, fail string, sleep string) { + if goruntime.GOOS == "windows" { + return "powershell", + "Write-Output 'OK'", + "$null = 1", + "Write-Error 'failed'; exit 2", + "Start-Sleep -Seconds 2" + } + return "bash", + "printf 'OK\\n'", + "true", + "echo failed 1>&2; exit 2", + "sleep 2" +} diff --git a/internal/tui/services/runtime_bridge_test.go b/internal/tui/services/runtime_bridge_test.go index 8ca2f2aa..56f3fc5e 100644 --- a/internal/tui/services/runtime_bridge_test.go +++ b/internal/tui/services/runtime_bridge_test.go @@ -1,6 +1,7 @@ package services import ( + "fmt" "testing" "time" @@ -139,3 +140,380 @@ func TestMapRunSnapshot(t *testing.T) { t.Fatalf("unexpected run snapshot mapping: context=%+v tools=%+v usage=%+v", context, tools, usage) } } + +func TestRuntimeBridgeParsersTypedAndNilInputs(t *testing.T) { + ctx, ok := ParseRunContextPayload(RuntimeRunContextPayload{ + Provider: " openai ", + Model: " gpt-5.4 ", + }) + if !ok || ctx.Provider != "openai" || ctx.Model != "gpt-5.4" { + t.Fatalf("expected typed run context payload to parse, got %+v ok=%v", ctx, ok) + } + + var nilRunContext *RuntimeRunContextPayload + if _, ok := ParseRunContextPayload(nilRunContext); ok { + t.Fatalf("expected nil run context pointer to fail parsing") + } + if _, ok := ParseRunContextPayload(map[string]any{"Provider": " ", "Model": " "}); ok { + t.Fatalf("expected empty run context map to fail parsing") + } + + tool, ok := ParseToolStatusPayload(map[string]any{ + "ToolCallID": 12345, + "ToolName": " filesystem ", + "Status": " failed ", + "Message": " boom ", + "DurationMS": "88", + }) + if !ok || tool.ToolCallID != "12345" || tool.ToolName != "filesystem" || tool.DurationMS != 88 { + t.Fatalf("unexpected tool status parse result: %+v ok=%v", tool, ok) + } + + var nilToolStatus *RuntimeToolStatusPayload + if _, ok := ParseToolStatusPayload(nilToolStatus); ok { + t.Fatalf("expected nil tool status pointer to fail parsing") + } + if _, ok := ParseToolStatusPayload(map[string]any{"ToolCallID": " ", "ToolName": ""}); ok { + t.Fatalf("expected empty tool status payload to fail parsing") + } + + usage, ok := ParseUsagePayload(map[string]any{ + "Delta": map[string]any{ + "InputTokens": "1", + "OutputTokens": float64(2), + "TotalTokens": int64(3), + }, + "Run": &RuntimeUsageSnapshot{ + InputTokens: 4, + OutputTokens: 5, + TotalTokens: 9, + }, + "Session": RuntimeUsageSnapshot{ + InputTokens: 10, + OutputTokens: 20, + TotalTokens: 30, + }, + }) + if !ok || usage.Delta.TotalTokens != 3 || usage.Run.TotalTokens != 9 || usage.Session.TotalTokens != 30 { + t.Fatalf("unexpected usage payload parse result: %+v ok=%v", usage, ok) + } + + var nilUsage *RuntimeUsagePayload + if _, ok := ParseUsagePayload(nilUsage); ok { + t.Fatalf("expected nil usage pointer to fail parsing") + } + if _, ok := ParseUsagePayload(RuntimeUsagePayload{}); ok { + t.Fatalf("expected zero usage payload to fail parsing") + } +} + +func TestRuntimeBridgeSnapshotParsers(t *testing.T) { + session, ok := ParseSessionContextSnapshot(map[string]any{ + "SessionID": " session-1 ", + "Provider": " openai ", + "Model": " gpt-5.4 ", + "Workdir": " /repo ", + "Mode": " act ", + }) + if !ok || session.SessionID != "session-1" || session.Workdir != "/repo" { + t.Fatalf("unexpected session snapshot parse result: %+v ok=%v", session, ok) + } + + var nilSession *RuntimeSessionContextSnapshot + if _, ok := ParseSessionContextSnapshot(nilSession); ok { + t.Fatalf("expected nil session snapshot pointer to fail parsing") + } + if _, ok := ParseSessionContextSnapshot(RuntimeSessionContextSnapshot{}); ok { + t.Fatalf("expected empty session snapshot to fail parsing") + } + + usage, ok := ParseUsageSnapshot(map[string]any{ + "InputTokens": "11", + "OutputTokens": float64(22), + "TotalTokens": int32(33), + }) + if !ok || usage.TotalTokens != 33 { + t.Fatalf("unexpected usage snapshot parse result: %+v ok=%v", usage, ok) + } + if _, ok := ParseUsageSnapshot(RuntimeUsageSnapshot{}); ok { + t.Fatalf("expected empty usage snapshot to fail parsing") + } +} + +func TestRuntimeBridgeParseRunSnapshot(t *testing.T) { + now := time.Now().UTC().Truncate(time.Second) + existing := RuntimeToolStateSnapshot{ + ToolCallID: "call-2", + ToolName: "tool-b", + Status: "failed", + } + snapshot, ok := ParseRunSnapshot(map[string]any{ + "RunID": " run-9 ", + "SessionID": " session-9 ", + "Context": map[string]any{ + "RunID": "run-9", + "SessionID": "session-9", + "Provider": " openai ", + "Model": " gpt-5.4 ", + "Workdir": " /repo ", + "Mode": " act ", + }, + "ToolStates": []any{ + map[string]any{ + "ToolCallID": "call-1", + "ToolName": "tool-a", + "Status": "succeeded", + "Message": "ok", + "DurationMS": "41", + "UpdatedAt": now, + }, + &existing, + "ignored", + (*RuntimeToolStateSnapshot)(nil), + }, + "Usage": map[string]any{ + "InputTokens": 1, + "OutputTokens": 2, + "TotalTokens": 3, + }, + "SessionUsage": RuntimeUsageSnapshot{ + InputTokens: 10, + OutputTokens: 20, + TotalTokens: 30, + }, + }) + if !ok { + t.Fatalf("expected run snapshot to parse") + } + if snapshot.RunID != "run-9" || snapshot.SessionID != "session-9" { + t.Fatalf("unexpected run/session ids: %+v", snapshot) + } + if len(snapshot.ToolStates) != 2 || snapshot.ToolStates[0].DurationMS != 41 { + t.Fatalf("unexpected parsed tool states: %+v", snapshot.ToolStates) + } + if !snapshot.ToolStates[0].UpdatedAt.Equal(now) { + t.Fatalf("expected parsed updated-at timestamp, got %v", snapshot.ToolStates[0].UpdatedAt) + } + if snapshot.Context.Provider != "openai" { + t.Fatalf("expected context provider from map, got %q", snapshot.Context.Provider) + } + + var nilSnapshot *RuntimeRunSnapshot + if _, ok := ParseRunSnapshot(nilSnapshot); ok { + t.Fatalf("expected nil run snapshot pointer to fail parsing") + } + if _, ok := ParseRunSnapshot(map[string]any{"RunID": " ", "SessionID": ""}); ok { + t.Fatalf("expected empty run snapshot ids to fail parsing") + } + if _, ok := ParseRunSnapshot(42); ok { + t.Fatalf("expected unsupported run snapshot type to fail parsing") + } +} + +func TestRuntimeBridgeMapSnapshotHelpers(t *testing.T) { + context := MapSessionContextSnapshot(RuntimeSessionContextSnapshot{ + SessionID: " session-2 ", + Provider: " openai ", + Model: " gpt-5.4-mini ", + Workdir: " /workspace ", + Mode: " plan ", + }) + if context.SessionID != "session-2" || context.Provider != "openai" || context.Workdir != "/workspace" { + t.Fatalf("unexpected mapped session context: %+v", context) + } + + current := TokenUsageVM{ + RunInputTokens: 1, + RunOutputTokens: 2, + RunTotalTokens: 3, + SessionInputTokens: 4, + SessionOutputTokens: 5, + SessionTotalTokens: 9, + } + mapped := MapUsageSnapshot(RuntimeUsageSnapshot{ + InputTokens: 100, + OutputTokens: 200, + TotalTokens: 300, + }, current) + if mapped.RunTotalTokens != 3 || mapped.SessionTotalTokens != 300 { + t.Fatalf("unexpected mapped usage snapshot: %+v", mapped) + } +} + +func TestRuntimeBridgeMergeToolStatesCaseInsensitiveAndDefaultLimit(t *testing.T) { + now := time.Now() + replaced := MergeToolStates([]ToolStateVM{ + { + ToolCallID: "Call-1", + ToolName: "Tool-A", + Status: tuistate.ToolLifecycleRunning, + UpdatedAt: now, + }, + }, ToolStateVM{ + ToolCallID: "call-1", + ToolName: "tool-a", + Status: tuistate.ToolLifecycleSucceeded, + UpdatedAt: now.Add(time.Second), + }, 0) + if len(replaced) != 1 || replaced[0].Status != tuistate.ToolLifecycleSucceeded { + t.Fatalf("expected case-insensitive duplicate replacement, got %+v", replaced) + } + + var states []ToolStateVM + for i := 0; i < 16; i++ { + states = append(states, ToolStateVM{ + ToolCallID: fmt.Sprintf("call-%d", i), + ToolName: "tool", + Status: tuistate.ToolLifecycleRunning, + }) + } + states = MergeToolStates(states, ToolStateVM{ + ToolCallID: "call-16", + ToolName: "tool", + Status: tuistate.ToolLifecycleRunning, + }, 0) + if len(states) != 16 { + t.Fatalf("expected default limit to keep 16 states, got %d", len(states)) + } + if states[0].ToolCallID != "call-1" || states[len(states)-1].ToolCallID != "call-16" { + t.Fatalf("expected oldest state to be evicted with default limit, got %+v", states) + } +} + +func TestRuntimeBridgeInternalHelpers(t *testing.T) { + if got := mapToolLifecycleStatus("planned"); got != tuistate.ToolLifecyclePlanned { + t.Fatalf("expected planned status, got %q", got) + } + if got := mapToolLifecycleStatus(" RUNNING "); got != tuistate.ToolLifecycleRunning { + t.Fatalf("expected running status, got %q", got) + } + if got := mapToolLifecycleStatus("succeeded"); got != tuistate.ToolLifecycleSucceeded { + t.Fatalf("expected succeeded status, got %q", got) + } + if got := mapToolLifecycleStatus("failed"); got != tuistate.ToolLifecycleFailed { + t.Fatalf("expected failed status, got %q", got) + } + if got := mapToolLifecycleStatus("unknown"); got != tuistate.ToolLifecycleRunning { + t.Fatalf("expected unknown status fallback to running, got %q", got) + } + + if got := parseRunContextSnapshotFromAny(RuntimeRunContextSnapshot{RunID: "r1"}); got.RunID != "r1" { + t.Fatalf("unexpected direct run context snapshot parse result: %+v", got) + } + if got := parseRunContextSnapshotFromAny((*RuntimeRunContextSnapshot)(nil)); got != (RuntimeRunContextSnapshot{}) { + t.Fatalf("expected nil run context pointer to produce zero value, got %+v", got) + } + if got := parseRunContextSnapshotFromAny(map[string]any{"RunID": "r2", "Provider": "openai"}); got.RunID != "r2" { + t.Fatalf("unexpected map run context snapshot parse result: %+v", got) + } + + original := []RuntimeToolStateSnapshot{{ToolCallID: "call-1"}} + cloned := parseToolStatesFromAny(original) + if len(cloned) != 1 || cloned[0].ToolCallID != "call-1" { + t.Fatalf("unexpected direct tool state parse result: %+v", cloned) + } + cloned[0].ToolCallID = "modified" + if original[0].ToolCallID != "call-1" { + t.Fatalf("expected parseToolStatesFromAny to return a copied slice") + } + + parsedStates := parseToolStatesFromAny([]any{ + map[string]any{"ToolCallID": "call-2", "ToolName": "tool"}, + RuntimeToolStateSnapshot{ToolCallID: "call-3"}, + (*RuntimeToolStateSnapshot)(nil), + "ignored", + }) + if len(parsedStates) != 2 { + t.Fatalf("expected two parsed tool states from mixed input, got %+v", parsedStates) + } + if got := parseToolStatesFromAny("unsupported"); got != nil { + t.Fatalf("expected unsupported tool state input to return nil, got %+v", got) + } + + if _, ok := parseToolStateFromAny((*RuntimeToolStateSnapshot)(nil)); ok { + t.Fatalf("expected nil tool state pointer to fail parsing") + } + if _, ok := parseToolStateFromAny(false); ok { + t.Fatalf("expected unsupported tool state type to fail parsing") + } +} + +func TestRuntimeBridgeReadMapHelpers(t *testing.T) { + now := time.Now().UTC().Truncate(time.Second) + m := map[string]any{ + "string": " value ", + "int": 12, + "int64": int64(13), + "int32": int32(14), + "float64": float64(15.9), + "float32": float32(16.7), + "numericStr": " 17 ", + "badStr": "x", + "time": now, + "nil": nil, + } + + if got := readMapString(m, "string"); got != "value" { + t.Fatalf("unexpected readMapString result: %q", got) + } + if got := readMapString(m, "int"); got != "12" { + t.Fatalf("expected non-string value to be stringified, got %q", got) + } + if got := readMapString(m, "missing"); got != "" { + t.Fatalf("expected missing key to return empty string, got %q", got) + } + + if got := readMapInt(m, "int"); got != 12 { + t.Fatalf("unexpected readMapInt int value: %d", got) + } + if got := readMapInt(m, "int64"); got != 13 { + t.Fatalf("unexpected readMapInt int64 value: %d", got) + } + if got := readMapInt(m, "int32"); got != 14 { + t.Fatalf("unexpected readMapInt int32 value: %d", got) + } + if got := readMapInt(m, "float64"); got != 15 { + t.Fatalf("unexpected readMapInt float64 value: %d", got) + } + if got := readMapInt(m, "float32"); got != 16 { + t.Fatalf("unexpected readMapInt float32 value: %d", got) + } + if got := readMapInt(m, "numericStr"); got != 17 { + t.Fatalf("unexpected readMapInt string value: %d", got) + } + if got := readMapInt(m, "badStr"); got != 0 { + t.Fatalf("expected invalid numeric string to return 0, got %d", got) + } + if got := readMapInt(m, "nil"); got != 0 { + t.Fatalf("expected nil value to return 0, got %d", got) + } + + if got := readMapInt64(m, "int"); got != 12 { + t.Fatalf("unexpected readMapInt64 int value: %d", got) + } + if got := readMapInt64(m, "int64"); got != 13 { + t.Fatalf("unexpected readMapInt64 int64 value: %d", got) + } + if got := readMapInt64(m, "int32"); got != 14 { + t.Fatalf("unexpected readMapInt64 int32 value: %d", got) + } + if got := readMapInt64(m, "float64"); got != 15 { + t.Fatalf("unexpected readMapInt64 float64 value: %d", got) + } + if got := readMapInt64(m, "float32"); got != 16 { + t.Fatalf("unexpected readMapInt64 float32 value: %d", got) + } + if got := readMapInt64(m, "numericStr"); got != 17 { + t.Fatalf("unexpected readMapInt64 string value: %d", got) + } + if got := readMapInt64(m, "badStr"); got != 0 { + t.Fatalf("expected invalid int64 string to return 0, got %d", got) + } + + if got := readMapTime(m, "time"); !got.Equal(now) { + t.Fatalf("unexpected readMapTime value: %v", got) + } + if got := readMapTime(m, "string"); !got.IsZero() { + t.Fatalf("expected non-time value to return zero time, got %v", got) + } +} diff --git a/internal/tui/services/services_test.go b/internal/tui/services/services_test.go index 83b7a4ab..cde184c9 100644 --- a/internal/tui/services/services_test.go +++ b/internal/tui/services/services_test.go @@ -184,3 +184,30 @@ func TestFileServices(t *testing.T) { t.Fatalf("expected empty resolved path for blank input, got %q", resolved) } } + +func TestSuggestFileMatchesBranches(t *testing.T) { + candidates := []string{ + "internal/tui/update.go", + "docs/internal-arch.md", + "README.md", + } + + if got := SuggestFileMatches("arch", candidates, 2); len(got) != 1 || got[0] != "docs/internal-arch.md" { + t.Fatalf("expected contains-match branch, got %v", got) + } + if got := SuggestFileMatches("", candidates, 2); len(got) != 2 { + t.Fatalf("expected empty query to return prefix-priority items, got %v", got) + } + if got := SuggestFileMatches("any", candidates, 0); got != nil { + t.Fatalf("expected zero limit to return nil, got %v", got) + } + if got := SuggestFileMatches("any", nil, 2); got != nil { + t.Fatalf("expected nil candidates to return nil, got %v", got) + } +} + +func TestResolveWorkspaceDirectoryInvalidPath(t *testing.T) { + if resolved := ResolveWorkspaceDirectory("\x00"); resolved != "" { + t.Fatalf("expected invalid path to resolve as empty string, got %q", resolved) + } +}