package app import ( "strings" "testing" "github.com/hjbdev/patterm/internal/mcp" "github.com/hjbdev/patterm/internal/preset" ) func TestCanonicalizeTerminalText(t *testing.T) { cases := []struct { name string in string want string }{ { name: "ansi osc and controls", in: "\x1b]0;title\x07\x1b[31mred\x1b[0m\x00\nok", want: "red\nok", }, { name: "noisy harness stream", in: "\x1b]0;noise\x07\x1b[31mStatus: running 12s\x1b[0m\nStatus: running 13s\n╭────╮\n│ │\nDownloading 10%\rDownloading 100%\nFINAL: deploy ready\n", want: "Status: running [time]\nDownloading [count]\nFINAL: deploy ready", }, { name: "repeated blank collapse", in: "one\n\n\n two\n \n\t\nthree", want: "one\n\n two\n\nthree", }, { name: "border only box drawing removal", in: "╭────────╮\n│ │\nimportant\n╰────────╯", want: "important", }, { name: "carriage return progress coalesces final frame", in: "Downloading 10%\rDownloading 20%\rDownloading 100%\nDone", want: "Downloading [count]\nDone", }, { name: "volatile timer duplicate collapse", in: "Status: running 12s\nStatus: running 13s\nStatus: running 01:23", want: "Status: running [time]", }, { name: "duplicate status row collapse", in: "⠋ Building 1/4\n⠙ Building 2/4\n⠹ Building 3/4\nready", want: "Building [count]\nready", }, { name: "preserve meaningful indented code and tables", in: " if elapsed == 12s {\n return value\n }\n| name | value |\n| a | 1 |", want: " if elapsed == 12s {\n return value\n }\n| name | value |\n| a | 1 |", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, truncated, _ := canonicalizeTerminalText(tc.in, 120) if truncated { t.Fatalf("unexpected truncation") } if got != tc.want { t.Fatalf("got %q want %q", got, tc.want) } }) } } func TestCanonicalizeTerminalTextMaxLines(t *testing.T) { got, truncated, dropped := canonicalizeTerminalText("one\ntwo\nthree", 2) if !truncated { t.Fatalf("expected truncation") } if dropped == 0 { t.Fatalf("expected dropped bytes") } if got != "two\nthree" { t.Fatalf("got %q", got) } } func TestGetProcessOutputStreamCanonicalByDefault(t *testing.T) { sess := NewSession(t.TempDir(), "test") c := newChildEntry("p1", "proc", KindCommand, nil, nil, "", "", "") addChild(sess, c) c.recordWrite([]byte("\x1b[31mStatus: running 12s\x1b[0m\nStatus: running 13s\nresult\n")) host := newToolHost(sess, nil, nil, preset.Set{}, nil, 80, 24) out, err := host.GetProcessOutput("", mcp.ProcessOutputArgs{ProcessID: c.ID, Mode: "stream"}) if err != nil { t.Fatal(err) } if !out.Canonicalized { t.Fatalf("expected canonicalized output") } if out.Content != "Status: running [time]\nresult" { t.Fatalf("content = %q", out.Content) } if out.Cursor != nil || out.Rows != 0 || out.Cols != 0 || out.ScreenVersion != 0 || out.IdleMS != 0 { t.Fatalf("default output should be metadata-light: %#v", out) } } func TestGetProcessOutputRawReturnsStreamBytes(t *testing.T) { sess := NewSession(t.TempDir(), "test") c := newChildEntry("p1", "proc", KindCommand, nil, nil, "", "", "") addChild(sess, c) c.recordWrite([]byte("\x1b[31mred\x1b[0m")) host := newToolHost(sess, nil, nil, preset.Set{}, nil, 80, 24) out, err := host.GetProcessOutput("", mcp.ProcessOutputArgs{ProcessID: c.ID, Mode: "grid", Raw: true}) if err != nil { t.Fatal(err) } if out.Mode != "stream" { t.Fatalf("raw grid mode should report stream semantics, got %q", out.Mode) } if out.Canonicalized { t.Fatalf("raw output should not be canonicalized") } if out.Content != "\x1b[31mred\x1b[0m" { t.Fatalf("content = %q", out.Content) } if out.NewOffset != int64(len(out.Content)) { t.Fatalf("new_offset=%d want %d", out.NewOffset, len(out.Content)) } } func TestGetProcessOutputCanonicalAfterRawRead(t *testing.T) { sess := NewSession(t.TempDir(), "test") c := newChildEntry("p1", "proc", KindCommand, nil, nil, "", "", "") addChild(sess, c) c.recordWrite([]byte("\x1b[31mStatus: running 12s\x1b[0m\nStatus: running 13s\nDownloading 10%\rDownloading 100%\nFINAL: deploy ready\n")) host := newToolHost(sess, nil, nil, preset.Set{}, nil, 80, 24) if _, err := host.GetProcessOutput("", mcp.ProcessOutputArgs{ProcessID: c.ID, Mode: "stream", Raw: true}); err != nil { t.Fatal(err) } out, err := host.GetProcessOutput("", mcp.ProcessOutputArgs{ProcessID: c.ID, Mode: "stream", MaxLines: 20}) if err != nil { t.Fatal(err) } if out.Content != "Status: running [time]\nDownloading [count]\nFINAL: deploy ready" { t.Fatalf("content = %q", out.Content) } } func TestGetProcessOutputIncludeMetaRestoresFields(t *testing.T) { sess := NewSession(t.TempDir(), "test") c := newChildEntry("p1", "proc", KindCommand, nil, nil, "", "", "") addChild(sess, c) c.recordWrite([]byte("ok")) host := newToolHost(sess, nil, nil, preset.Set{}, nil, 80, 24) out, err := host.GetProcessOutput("", mcp.ProcessOutputArgs{ProcessID: c.ID, Mode: "stream", IncludeMeta: true}) if err != nil { t.Fatal(err) } if out.ScreenVersion == 0 { t.Fatalf("screen_version missing with include_meta: %#v", out) } if !strings.Contains(out.Content, "ok") { t.Fatalf("content = %q", out.Content) } }