From c352dfe5b8f79b60d71b9e8146d7ed627d94e09c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 12 Jul 2026 09:00:40 +0000 Subject: [PATCH 1/5] build: use Go 1.25.12 toolchain --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index aba81bd..dc63bdd 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/RandomCodeSpace/unified-agent-manager go 1.25.0 -toolchain go1.25.11 +toolchain go1.25.12 require ( github.com/charmbracelet/bubbletea v1.3.10 From 8c98d102e0bab51aaedc5eab98175550d84f4012 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 12 Jul 2026 09:01:17 +0000 Subject: [PATCH 2/5] chore(deps): upgrade x/term to 0.2.2 --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index dc63bdd..c218439 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,10 @@ toolchain go1.25.12 require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/term v0.2.1 + github.com/charmbracelet/x/term v0.2.2 github.com/creack/pty v1.1.24 github.com/mattn/go-runewidth v0.0.16 + golang.org/x/sys v0.36.0 ) require ( @@ -26,6 +27,5 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index c6551b4..58b52be 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7 github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= From e3f2aaa5295113ce07cd8708ef5977bb7f8d6bd7 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 12 Jul 2026 09:01:52 +0000 Subject: [PATCH 3/5] chore(deps): upgrade go-runewidth to 0.0.24 --- go.mod | 3 ++- go.sum | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c218439..4d29612 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.2 github.com/creack/pty v1.1.24 - github.com/mattn/go-runewidth v0.0.16 + github.com/mattn/go-runewidth v0.0.24 golang.org/x/sys v0.36.0 ) @@ -18,6 +18,7 @@ require ( github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 58b52be..b1e0044 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -22,15 +24,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= From d38383fb4e7d184ac5d2d2ae06b8c0a0f4b2e65e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 12 Jul 2026 09:08:53 +0000 Subject: [PATCH 4/5] test: add fuzz and release smoke coverage --- .github/workflows/fuzz.yml | 40 ++++++++++++++ .github/workflows/release-smoke.yml | 75 ++++++++++++++++++++++++++ internal/adapter/registry_test.go | 20 +++++++ internal/app/pr_refresh_test.go | 42 +++++++++++++++ internal/app/render_cluster_test.go | 21 ++++++++ internal/session/attach_filter_test.go | 13 +++++ internal/session/liveness_test.go | 29 ++++++++++ internal/session/proto_test.go | 25 +++++++++ internal/vterm/vterm_test.go | 30 +++++++++++ 9 files changed, 295 insertions(+) create mode 100644 .github/workflows/fuzz.yml create mode 100644 .github/workflows/release-smoke.yml diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..e1565b1 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,40 @@ +name: Scheduled fuzzing + +on: + schedule: + - cron: "41 4 * * 2" + workflow_dispatch: + +permissions: + contents: read + +jobs: + fuzz: + name: fuzz-${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - package: ./internal/session + target: FuzzFrameDecoding + - package: ./internal/vterm + target: FuzzTerminalInput + - package: ./internal/displaytext + target: FuzzSanitize + - package: ./internal/session + target: FuzzAttachFiltering + - package: ./internal/session + target: FuzzStateDecoding + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Fuzz for 30 seconds + run: go test "${{ matrix.package }}" -run=^$ -fuzz="^${{ matrix.target }}$" -fuzztime=30s diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml new file mode 100644 index 0000000..6af1e4a --- /dev/null +++ b/.github/workflows/release-smoke.yml @@ -0,0 +1,75 @@ +name: Release smoke + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + snapshot: + name: GoReleaser snapshot + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Validate GoReleaser configuration + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser + version: v2.17.0 + args: check + + - name: Build snapshot archives + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser + version: v2.17.0 + args: release --snapshot --clean --skip=sign,sbom + + - name: Verify archive names and contents + shell: bash + run: | + mapfile -t archives < <(find dist -maxdepth 1 -type f -name 'uam_*.tar.gz' -print | sort) + test "${#archives[@]}" -eq 4 + for archive in "${archives[@]}"; do + basename "${archive}" | grep -Eq '^uam_.+_(linux|darwin)_(amd64|arm64)\.tar\.gz$' + tar -tzf "${archive}" | grep -Eq '(^|/)uam$' + done + linux_archive="$(find dist -maxdepth 1 -type f -name 'uam_*_linux_amd64.tar.gz' -print -quit)" + test -n "${linux_archive}" + mkdir -p /tmp/uam-release-smoke + tar -xzf "${linux_archive}" -C /tmp/uam-release-smoke + binary="$(find /tmp/uam-release-smoke -type f -name uam -print -quit)" + test -n "${binary}" + "${binary}" version | grep -E '.+' + + darwin-native: + name: Darwin-native version smoke + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache: true + + - name: Build and run version command + run: | + go build -o "${RUNNER_TEMP}/uam" ./cmd/uam + "${RUNNER_TEMP}/uam" version | grep -E '.+' diff --git a/internal/adapter/registry_test.go b/internal/adapter/registry_test.go index c4dd1e7..10fad55 100644 --- a/internal/adapter/registry_test.go +++ b/internal/adapter/registry_test.go @@ -2,6 +2,7 @@ package adapter import ( "context" + "fmt" "testing" "time" @@ -57,6 +58,25 @@ func TestRegistryListAllUsesSingleBackendSnapshot(t *testing.T) { } } +func BenchmarkRegistryListAllSharedSnapshot(b *testing.B) { + infos := make([]session.Info, 300) + for i := range infos { + infos[i] = session.Info{Name: fmt.Sprintf("uam-a-%08x", i+1), CreatedUnix: time.Now().Unix(), Alive: true} + } + backend := &adaptertest.Backend{Sessions: infos} + adapters := make([]AgentAdapter, 0, 6) + for _, name := range []string{"a", "b", "c", "d", "e", "f"} { + adapters = append(adapters, NewAgent(name, name, []CommandCandidate{{Display: "sh", Args: []string{"/bin/sh"}}}, nil, backend)) + } + registry := NewRegistryWithBackend(backend, adapters) + b.ResetTimer() + for b.Loop() { + if _, err := registry.ListAll(context.Background()); err != nil { + b.Fatal(err) + } + } +} + type fakeAdapter struct { name string available bool diff --git a/internal/app/pr_refresh_test.go b/internal/app/pr_refresh_test.go index 5cb8408..2a48e21 100644 --- a/internal/app/pr_refresh_test.go +++ b/internal/app/pr_refresh_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "path/filepath" "sync" "sync/atomic" "testing" @@ -168,3 +169,44 @@ func TestAdapterStatusMapping(t *testing.T) { } } } + +func BenchmarkRefreshPRStatuses(b *testing.B) { + sessions := make([]adapter.Session, 20) + for i := range sessions { + id := fmt.Sprintf("%08x", i+1) + sessions[i] = adapter.Session{ + ID: id, AgentType: "fake", DisplayName: id, Cwd: "/tmp", + SessionName: "uam-fake-" + id, State: adapter.Active, ProcAlive: adapter.Alive, + CreatedAt: time.Now(), PR: &adapter.PRRef{URL: fmt.Sprintf("https://github.com/o/r/pull/%d", i+1), Number: i + 1, Status: adapter.PROpen}, + } + } + st, err := store.Open(filepath.Join(b.TempDir(), "sessions.json")) + if err != nil { + b.Fatal(err) + } + fake := &svcFakeAdapter{name: "fake", available: true, sessions: sessions} + svc := NewService(st, adapter.NewRegistry([]adapter.AgentAdapter{fake})) + svc.checkPR = func(context.Context, string) (pr.Status, error) { return pr.Open, nil } + if _, _, err := svc.LoadSessions(context.Background()); err != nil { + b.Fatal(err) + } + b.ResetTimer() + for b.Loop() { + b.StopTimer() + if err := st.Update(func(cfg *store.Config) error { + for key, rec := range cfg.Sessions { + if rec.PR != nil { + rec.PR.LastChecked = time.Time{} + cfg.Sessions[key] = rec + } + } + return nil + }); err != nil { + b.Fatal(err) + } + b.StartTimer() + if err := svc.RefreshPRStatuses(context.Background()); err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/app/render_cluster_test.go b/internal/app/render_cluster_test.go index 75dbcac..0a96f82 100644 --- a/internal/app/render_cluster_test.go +++ b/internal/app/render_cluster_test.go @@ -75,6 +75,27 @@ func TestTruncateIsWidthAware(t *testing.T) { } } +func TestTruncateUnicodeGolden(t *testing.T) { + for _, tc := range []struct { + name string + in string + cols int + want string + }{ + {name: "emoji", in: "🚀🚀🚀", cols: 5, want: "🚀🚀…"}, + {name: "combining", in: "e\u0301clair", cols: 3, want: "e\u0301c…"}, + {name: "cjk", in: "你好世界", cols: 5, want: "你好…"}, + {name: "narrow", in: "anything", cols: 1, want: "…"}, + {name: "pasted-multibyte-name", in: "部署 café 🚀", cols: 10, want: "部署 café…"}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := truncate(tc.in, tc.cols); got != tc.want { + t.Fatalf("truncate(%q, %d) = %q, want %q", tc.in, tc.cols, got, tc.want) + } + }) + } +} + // F28 — truncate must not chop a multibyte rune mid-sequence (the old byte-slice // path produced mojibake on the boundary). func TestTruncateNeverEmitsMojibake(t *testing.T) { diff --git a/internal/session/attach_filter_test.go b/internal/session/attach_filter_test.go index ecbae72..d1c7a3f 100644 --- a/internal/session/attach_filter_test.go +++ b/internal/session/attach_filter_test.go @@ -21,6 +21,19 @@ func runFilter(t *testing.T, f *stdinFilter, chunks ...string) (string, bool) { return out.String(), false } +func FuzzAttachFiltering(f *testing.F) { + for _, seed := range []string{"plain text", "世界🚀", "\x02d", "\x1b[D", "\x1b]11;rgb:ffff/ffff/ffff\x1b\\"} { + f.Add(seed, true) + } + f.Fuzz(func(t *testing.T, input string, backDetach bool) { + filter := &stdinFilter{backDetach: backDetach} + out, _ := filter.filter([]byte(input)) + if len(out) > 2*len(input)+2 { + t.Fatalf("filter expanded %d input bytes to %d bytes", len(input), len(out)) + } + }) +} + func TestLeftArrowDetachesWhenNothingTyped(t *testing.T) { f := &stdinFilter{backDetach: true} out, detach := runFilter(t, f, "\x1b[D") diff --git a/internal/session/liveness_test.go b/internal/session/liveness_test.go index b311480..48475aa 100644 --- a/internal/session/liveness_test.go +++ b/internal/session/liveness_test.go @@ -2,6 +2,7 @@ package session import ( "context" + "encoding/json" "errors" "os" "os/exec" @@ -12,6 +13,34 @@ import ( "testing" ) +func FuzzStateDecoding(f *testing.F) { + for _, seed := range [][]byte{ + []byte(`{"name":"uam-fake-aabbccdd","host_pid":1,"host_start":2}`), + []byte(`{"name":"../escape","host_pid":1}`), + []byte(`null`), + []byte(`{"unknown":{"nested":true}}`), + } { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, data []byte) { + var state State + if err := json.Unmarshal(data, &state); err != nil { + return + } + encoded, err := json.Marshal(state) + if err != nil { + t.Fatal(err) + } + var decoded State + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatal(err) + } + if decoded.Name != state.Name || decoded.HostPID != state.HostPID || decoded.HostStart != state.HostStart { + t.Fatalf("state round trip changed identity: before=%+v after=%+v", state, decoded) + } + }) +} + func TestVerifyDirRejectsUnsafeRuntimeDirectories(t *testing.T) { parent := t.TempDir() diff --git a/internal/session/proto_test.go b/internal/session/proto_test.go index 87f9ac8..24f9882 100644 --- a/internal/session/proto_test.go +++ b/internal/session/proto_test.go @@ -84,3 +84,28 @@ func TestFrameWriterSerializesConcurrentFrames(t *testing.T) { t.Fatalf("trailing read error = %v, want EOF", err) } } + +func FuzzFrameDecoding(f *testing.F) { + for _, seed := range [][]byte{ + {frameDetach, 0, 0, 0, 0}, + {frameStdin, 0, 0, 0, 3, 'a', 'b', 'c'}, + {frameResize, 0, 0, 0, 4, 0, 80, 0, 24}, + {frameStdin, 0xff, 0xff, 0xff, 0xff}, + } { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, data []byte) { + kind, payload, err := readFrame(bytes.NewReader(data)) + if err != nil { + return + } + var roundTrip bytes.Buffer + if err := writeFrame(&roundTrip, kind, payload); err != nil { + t.Fatal(err) + } + gotKind, gotPayload, err := readFrame(&roundTrip) + if err != nil || gotKind != kind || !bytes.Equal(gotPayload, payload) { + t.Fatalf("frame round trip = (%d, %x, %v), want (%d, %x)", gotKind, gotPayload, err, kind, payload) + } + }) +} diff --git a/internal/vterm/vterm_test.go b/internal/vterm/vterm_test.go index 16257d8..9824783 100644 --- a/internal/vterm/vterm_test.go +++ b/internal/vterm/vterm_test.go @@ -372,6 +372,36 @@ func BenchmarkHostileCSICounts(b *testing.B) { } } +func BenchmarkOrdinaryTerminalStream(b *testing.B) { + stream := []byte("\x1b[32mready\x1b[0m> go test ./...\r\nok package 0.123s\r\n") + term := New(120, 40, 500) + b.SetBytes(int64(len(stream))) + b.ResetTimer() + for b.Loop() { + _, _ = term.Write(stream) + } +} + +func FuzzTerminalInput(f *testing.F) { + for _, seed := range [][]byte{ + []byte("plain\r\ntext"), + []byte("café 世界 🚀"), + []byte("\x1b[999999999999999999999S"), + []byte("\x1b]0;title\x07visible"), + } { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, input []byte) { + term := New(80, 24, 100) + if _, err := term.Write(input); err != nil { + t.Fatal(err) + } + if got := term.Capture(100); len(got) > 80*(24+100)*4 { + t.Fatalf("capture exceeded screen/history bound: %d bytes", len(got)) + } + }) +} + func TestScrollRegionReverseWrapAndKeypadIgnored(t *testing.T) { term := New(20, 4, 0) // Keypad mode escapes and charset designators are consumed silently. From 5b938d1e1237dcc96190143687a1da17437ceef9 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 12 Jul 2026 09:54:59 +0000 Subject: [PATCH 5/5] ci: bound Darwin lifecycle smoke tests --- .github/workflows/release-smoke.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index 6af1e4a..4590d20 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +concurrency: + group: release-smoke-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: true + jobs: snapshot: name: GoReleaser snapshot @@ -57,8 +61,9 @@ jobs: "${binary}" version | grep -E '.+' darwin-native: - name: Darwin-native version smoke + name: Darwin-native lifecycle and version smoke runs-on: macos-14 + timeout-minutes: 8 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -69,6 +74,12 @@ jobs: go-version-file: go.mod cache: true + - name: Run native session lifecycle tests + run: go test -race -count=1 -timeout=4m ./internal/session + + - name: Run stop, restart, and resume tests + run: go test -race -count=1 -timeout=2m ./internal/app + - name: Build and run version command run: | go build -o "${RUNNER_TEMP}/uam" ./cmd/uam