Files
viettranx 3b4004946e chore: apply go fix syntax upgrades (min/max, switch patterns)
Apply Go 1.21+ min/max builtins and improved switch pattern matching
across test files and utilities. No functional changes.
2026-04-15 11:24:57 +07:00

369 lines
9.4 KiB
Go

package acp
import (
"context"
"regexp"
"strings"
"sync"
"testing"
"time"
)
// --- cappedBuffer tests ---
func TestCappedBuffer_BasicWrite(t *testing.T) {
cb := &cappedBuffer{max: 20}
n, err := cb.Write([]byte("hello"))
if err != nil {
t.Fatalf("Write error: %v", err)
}
if n != 5 {
t.Errorf("expected n=5, got %d", n)
}
if cb.String() != "hello" {
t.Errorf("expected 'hello', got %q", cb.String())
}
}
func TestCappedBuffer_ExactMax(t *testing.T) {
cb := &cappedBuffer{max: 5}
cb.Write([]byte("hello")) // exactly 5
if cb.String() != "hello" {
t.Errorf("expected 'hello', got %q", cb.String())
}
}
func TestCappedBuffer_SmallOverflow(t *testing.T) {
// buf has 4 bytes, new 3-byte write causes overflow=2
// overflow < buf.Len() → truncate from front: keep buf[2:] + new data
cb := &cappedBuffer{max: 5}
cb.Write([]byte("abcd")) // buf="abcd" (4 bytes)
cb.Write([]byte("xyz")) // overflow = 4+3-5 = 2 < 4 → keep buf[2:]="cd", append "xyz"
got := cb.String()
if len(got) > 5 {
t.Errorf("expected len ≤ 5, got %d: %q", len(got), got)
}
// Tail should end with the new data
if !strings.HasSuffix(got, "xyz") {
t.Errorf("expected suffix 'xyz', got %q", got)
}
}
func TestCappedBuffer_LargeOverflow(t *testing.T) {
// Write that is larger than max alone
cb := &cappedBuffer{max: 5}
cb.Write([]byte("hello world 123456789")) // 21 bytes, max 5
got := cb.String()
if len(got) > 5 {
t.Errorf("expected len ≤ 5, got %d: %q", len(got), got)
}
// Should contain the last 5 bytes of "hello world 123456789" = "56789"
if got != "56789" {
t.Errorf("expected '56789', got %q", got)
}
}
func TestCappedBuffer_OverflowExceedsExistingBuf(t *testing.T) {
// overflow >= buf.Len() branch: reset buf, truncate p to last max bytes
cb := &cappedBuffer{max: 3}
cb.Write([]byte("ab")) // buf="ab" (2)
cb.Write([]byte("xyz")) // overflow = 2+3-3 = 2 >= buf.Len(2) → reset buf, write "xyz"
got := cb.String()
if got != "xyz" {
t.Errorf("expected 'xyz', got %q", got)
}
}
func TestCappedBuffer_MultipleSmallWrites(t *testing.T) {
cb := &cappedBuffer{max: 10}
for range 5 {
cb.Write([]byte("ab"))
}
got := cb.String()
if len(got) > 10 {
t.Errorf("expected len ≤ 10, got %d", len(got))
}
}
func TestCappedBuffer_ZeroMax(t *testing.T) {
cb := &cappedBuffer{max: 0}
// cappedBuffer with max=0 truncates all data: p becomes p[len(p)-0:] = []
// bytes.Buffer.Write([]) returns n=0
_, err := cb.Write([]byte("data"))
if err != nil {
t.Fatalf("Write error: %v", err)
}
if cb.String() != "" {
t.Errorf("expected empty buffer with max=0, got %q", cb.String())
}
}
func TestCappedBuffer_ConcurrentWrites(t *testing.T) {
cb := &cappedBuffer{max: 100}
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
cb.Write([]byte("data"))
})
}
wg.Wait()
s := cb.String()
if len(s) > 100 {
t.Errorf("expected len ≤ 100 after concurrent writes, got %d", len(s))
}
}
// --- allowedTerminalBinaries ---
func TestAllowedTerminalBinaries_Contains(t *testing.T) {
allowed := []string{
"sh", "bash", "zsh", "node", "python3", "go", "git",
"ls", "cat", "grep", "rg", "find", "curl", "docker",
"npm", "pip", "jq",
}
for _, bin := range allowed {
if !allowedTerminalBinaries[bin] {
t.Errorf("expected %q to be in allowed binaries", bin)
}
}
}
func TestAllowedTerminalBinaries_NotContains(t *testing.T) {
denied := []string{"rm", "dd", "mkfs", "fsck", "shutdown", "reboot", "nc", "netcat"}
for _, bin := range denied {
if allowedTerminalBinaries[bin] {
t.Errorf("expected %q to NOT be in allowed binaries", bin)
}
}
}
// --- terminalOutput / releaseTerminal / killTerminal via ToolBridge ---
func makeRunningTerminal(t *testing.T, id string) *Terminal {
t.Helper()
term := &Terminal{
id: id,
output: &cappedBuffer{max: 1024},
exited: make(chan struct{}),
cancel: func() {},
}
return term
}
func makeExitedTerminal(t *testing.T, id string, code int) *Terminal {
t.Helper()
term := &Terminal{
id: id,
output: &cappedBuffer{max: 1024},
exited: make(chan struct{}),
exitCode: code,
cancel: func() {},
}
close(term.exited) // pre-exited
return term
}
func TestTerminalOutput_Running(t *testing.T) {
tb, _ := newTestBridge(t)
term := makeRunningTerminal(t, "t1")
term.output.Write([]byte("some output"))
tb.terminals.Store("t1", term)
resp, err := tb.terminalOutput(TerminalOutputRequest{TerminalID: "t1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.Output != "some output" {
t.Errorf("expected 'some output', got %q", resp.Output)
}
if resp.ExitStatus != nil {
t.Error("expected nil ExitStatus for running terminal")
}
}
func TestTerminalOutput_Exited(t *testing.T) {
tb, _ := newTestBridge(t)
term := makeExitedTerminal(t, "t2", 42)
term.output.Write([]byte("done"))
tb.terminals.Store("t2", term)
resp, err := tb.terminalOutput(TerminalOutputRequest{TerminalID: "t2"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.ExitStatus == nil {
t.Fatal("expected non-nil ExitStatus for exited terminal")
}
if *resp.ExitStatus != 42 {
t.Errorf("expected exit code 42, got %d", *resp.ExitStatus)
}
}
func TestTerminalOutput_NotFound(t *testing.T) {
tb, _ := newTestBridge(t)
_, err := tb.terminalOutput(TerminalOutputRequest{TerminalID: "missing"})
if err == nil {
t.Error("expected error for missing terminal")
}
}
func TestReleaseTerminal_ExistingCancels(t *testing.T) {
tb, _ := newTestBridge(t)
cancelled := false
var mu sync.Mutex
term := makeRunningTerminal(t, "t3")
term.cancel = func() {
mu.Lock()
cancelled = true
mu.Unlock()
}
tb.terminals.Store("t3", term)
resp, err := tb.releaseTerminal(ReleaseTerminalRequest{TerminalID: "t3"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
mu.Lock()
wasCancelled := cancelled
mu.Unlock()
if !wasCancelled {
t.Error("expected cancel to be called on release")
}
// Terminal should be removed
if _, ok := tb.terminals.Load("t3"); ok {
t.Error("expected terminal to be removed after release")
}
}
func TestReleaseTerminal_NotFound(t *testing.T) {
tb, _ := newTestBridge(t)
resp, err := tb.releaseTerminal(ReleaseTerminalRequest{TerminalID: "none"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response even for missing terminal")
}
}
func TestKillTerminal_Cancels(t *testing.T) {
tb, _ := newTestBridge(t)
killed := false
var mu sync.Mutex
term := makeRunningTerminal(t, "tk1")
term.cancel = func() {
mu.Lock()
killed = true
mu.Unlock()
}
tb.terminals.Store("tk1", term)
resp, err := tb.killTerminal(KillTerminalRequest{TerminalID: "tk1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
mu.Lock()
wasKilled := killed
mu.Unlock()
if !wasKilled {
t.Error("expected cancel to be called on kill")
}
}
func TestKillTerminal_NotFound(t *testing.T) {
tb, _ := newTestBridge(t)
_, err := tb.killTerminal(KillTerminalRequest{TerminalID: "missing"})
if err == nil {
t.Error("expected error for missing terminal")
}
}
func TestWaitForExit_Exited(t *testing.T) {
tb, _ := newTestBridge(t)
term := makeExitedTerminal(t, "tw1", 0)
tb.terminals.Store("tw1", term)
resp, err := tb.waitForExit(context.Background(), WaitForTerminalExitRequest{TerminalID: "tw1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.ExitStatus != 0 {
t.Errorf("expected exit 0, got %d", resp.ExitStatus)
}
}
func TestWaitForExit_ContextCancel(t *testing.T) {
tb, _ := newTestBridge(t)
term := makeRunningTerminal(t, "tw2") // never exits
tb.terminals.Store("tw2", term)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
_, err := tb.waitForExit(ctx, WaitForTerminalExitRequest{TerminalID: "tw2"})
if err == nil {
t.Fatal("expected context cancellation error")
}
}
func TestWaitForExit_NotFound(t *testing.T) {
tb, _ := newTestBridge(t)
_, err := tb.waitForExit(context.Background(), WaitForTerminalExitRequest{TerminalID: "missing"})
if err == nil {
t.Error("expected error for missing terminal")
}
}
// --- createTerminal security: deny patterns and binary allowlist ---
func TestCreateTerminal_AllowlistDenied(t *testing.T) {
tb, _ := newTestBridge(t)
_, err := tb.createTerminal(CreateTerminalRequest{Command: "rm", Args: []string{"-rf", "/"}})
if err == nil {
t.Fatal("expected error for disallowed binary")
}
if !strings.Contains(err.Error(), "not in allowed binary list") {
t.Errorf("expected allowlist error, got %q", err.Error())
}
}
func TestCreateTerminal_DenyPattern(t *testing.T) {
pat := regexp.MustCompile(`rm -rf`)
tb, _ := newTestBridge(t, WithDenyPatterns([]*regexp.Regexp{pat}))
// "bash" is allowed but the combined command matches deny pattern
_, err := tb.createTerminal(CreateTerminalRequest{
Command: "bash",
Args: []string{"-c", "rm -rf /tmp/test"},
})
if err == nil {
t.Fatal("expected deny pattern to block command")
}
if !strings.Contains(err.Error(), "denied by safety policy") {
t.Errorf("expected safety policy error, got %q", err.Error())
}
}
func TestCreateTerminal_InvalidCwd(t *testing.T) {
tb, _ := newTestBridge(t)
// Path traversal in cwd should be denied
_, err := tb.createTerminal(CreateTerminalRequest{
Command: "ls",
Cwd: "../../etc",
})
if err == nil {
t.Fatal("expected error for cwd outside workspace")
}
}