remove port monitoring and automatic tunneling features
Remove port_monitor, TunnelManager, and /port-events handler to eliminate
automatic port tunneling functionality that bridges outtie to innie environments.
Sketch got confused when I asked it to change how this works; removing
and re-adding was easier!
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s78f868b27a44cb2bk
diff --git a/loop/agent.go b/loop/agent.go
index f01e601..8f96924 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -152,8 +152,7 @@
// CompactConversation compacts the current conversation by generating a summary
// and restarting the conversation with that summary as the initial context
CompactConversation(ctx context.Context) error
- // GetPortMonitor returns the port monitor instance for accessing port events
- GetPortMonitor() *PortMonitor
+
// SkabandAddr returns the skaband address if configured
SkabandAddr() string
}
@@ -460,9 +459,6 @@
// Track outstanding tool calls by ID with their names
outstandingToolCalls map[string]string
-
- // Port monitoring
- portMonitor *PortMonitor
}
// NewIterator implements CodingAgent.
@@ -1071,8 +1067,8 @@
stateMachine: NewStateMachine(),
workingDir: config.WorkingDir,
outsideHTTP: config.OutsideHTTP,
- portMonitor: NewPortMonitor(),
- mcpManager: mcp.NewMCPManager(),
+
+ mcpManager: mcp.NewMCPManager(),
}
return agent
}
@@ -1526,12 +1522,6 @@
}
func (a *Agent) Loop(ctxOuter context.Context) {
- // Start port monitoring when the agent loop begins
- // Only monitor ports when running in a container
- if a.IsInContainer() {
- a.portMonitor.Start(ctxOuter)
- }
-
// Set up cleanup when context is done
defer func() {
if a.mcpManager != nil {
@@ -2540,11 +2530,6 @@
return nil
}
-// GetPortMonitor returns the port monitor instance for accessing port events
-func (a *Agent) GetPortMonitor() *PortMonitor {
- return a.portMonitor
-}
-
// SkabandAddr returns the skaband address if configured
func (a *Agent) SkabandAddr() string {
if a.config.SkabandClient != nil {
diff --git a/loop/port_monitor.go b/loop/port_monitor.go
deleted file mode 100644
index 105e1e7..0000000
--- a/loop/port_monitor.go
+++ /dev/null
@@ -1,344 +0,0 @@
-package loop
-
-import (
- "context"
- "fmt"
- "log/slog"
- "net"
- "os"
- "os/exec"
- "strconv"
- "strings"
- "sync"
- "time"
-)
-
-// PortEvent represents a port change event
-type PortEvent struct {
- Type string `json:"type"` // "opened" or "closed"
- Port string `json:"port"` // "proto:address:port" format
- Timestamp time.Time `json:"timestamp"` // when the event occurred
-}
-
-// PortMonitor handles periodic monitoring of listening ports in containers
-type PortMonitor struct {
- mu sync.Mutex // protects following
- lastPorts string // last netstat/ss output for comparison
- events []PortEvent // circular buffer of recent port events
- maxEvents int // maximum events to keep in buffer
-}
-
-// NewPortMonitor creates a new PortMonitor instance
-func NewPortMonitor() *PortMonitor {
- return &PortMonitor{
- maxEvents: 100, // keep last 100 events
- events: make([]PortEvent, 0, 100),
- }
-}
-
-// Start begins periodic port monitoring in a background goroutine
-func (pm *PortMonitor) Start(ctx context.Context) {
- go func() {
- ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds
- defer ticker.Stop()
-
- // Get initial port state
- pm.updatePortState(ctx)
-
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- pm.updatePortState(ctx)
- }
- }
- }()
-}
-
-// updatePortState runs ss and checks for changes in listening ports
-// Falls back to /proc/net/tcp* parsing if ss is not available
-func (pm *PortMonitor) updatePortState(ctx context.Context) {
- var currentPorts string
- var err error
-
- // Try ss command first
- cmd := exec.CommandContext(ctx, "ss", "-lntu")
- output, err := cmd.Output()
- if err != nil {
- // ss command failed, try /proc filesystem fallback
- slog.DebugContext(ctx, "ss command failed, trying /proc fallback", "error", err)
- currentPorts, err = pm.getListeningPortsFromProc()
- if err != nil {
- // Both methods failed - log and return
- slog.DebugContext(ctx, "Failed to get listening ports", "ss_error", err)
- return
- }
- } else {
- currentPorts = string(output)
- }
-
- pm.mu.Lock()
- defer pm.mu.Unlock()
-
- // Check if ports have changed
- if pm.lastPorts != "" && pm.lastPorts != currentPorts {
- // Ports have changed, log the difference
- slog.InfoContext(ctx, "Container port changes detected",
- slog.String("previous_ports", pm.lastPorts),
- slog.String("current_ports", currentPorts))
-
- // Parse and compare the port lists for more detailed logging
- pm.logPortDifferences(ctx, pm.lastPorts, currentPorts)
- }
-
- pm.lastPorts = currentPorts
-}
-
-// logPortDifferences parses ss output and logs specific port changes
-func (pm *PortMonitor) logPortDifferences(ctx context.Context, oldPorts, newPorts string) {
- oldPortSet := parseSSPorts(oldPorts)
- newPortSet := parseSSPorts(newPorts)
- now := time.Now()
-
- // Find newly opened ports
- for port := range newPortSet {
- if !oldPortSet[port] {
- slog.InfoContext(ctx, "New port detected", slog.String("port", port))
- pm.addEvent(PortEvent{
- Type: "opened",
- Port: port,
- Timestamp: now,
- })
- }
- }
-
- // Find closed ports
- for port := range oldPortSet {
- if !newPortSet[port] {
- slog.InfoContext(ctx, "Port closed", slog.String("port", port))
- pm.addEvent(PortEvent{
- Type: "closed",
- Port: port,
- Timestamp: now,
- })
- }
- }
-}
-
-// addEvent adds a port event to the circular buffer (must be called with mutex held)
-func (pm *PortMonitor) addEvent(event PortEvent) {
- // If buffer is full, remove oldest event
- if len(pm.events) >= pm.maxEvents {
- // Shift all events left by 1 to remove oldest
- copy(pm.events, pm.events[1:])
- pm.events = pm.events[:len(pm.events)-1]
- }
- // Add new event
- pm.events = append(pm.events, event)
-}
-
-// GetRecentEvents returns a copy of recent port events since the given timestamp
-func (pm *PortMonitor) GetRecentEvents(since time.Time) []PortEvent {
- pm.mu.Lock()
- defer pm.mu.Unlock()
-
- // Find events since the given timestamp
- var result []PortEvent
- for _, event := range pm.events {
- if event.Timestamp.After(since) {
- result = append(result, event)
- }
- }
- return result
-}
-
-// UpdatePortState is a public wrapper for updatePortState for testing purposes
-func (pm *PortMonitor) UpdatePortState(ctx context.Context) {
- pm.updatePortState(ctx)
-}
-
-// GetAllRecentEvents returns a copy of all recent port events
-func (pm *PortMonitor) GetAllRecentEvents() []PortEvent {
- pm.mu.Lock()
- defer pm.mu.Unlock()
-
- // Return a copy of all events
- result := make([]PortEvent, len(pm.events))
- copy(result, pm.events)
- return result
-}
-
-// parseSSPorts extracts listening ports from ss -lntu output
-// Returns a map with "proto:address:port" as keys
-// ss output format: Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
-func parseSSPorts(output string) map[string]bool {
- ports := make(map[string]bool)
- lines := strings.Split(output, "\n")
-
- for _, line := range lines {
- fields := strings.Fields(line)
- if len(fields) < 5 {
- continue
- }
-
- // Skip header line and non-LISTEN states
- if fields[0] == "Netid" || fields[1] != "LISTEN" {
- continue
- }
-
- proto := fields[0]
- localAddr := fields[4] // Local Address:Port
- portKey := fmt.Sprintf("%s:%s", proto, localAddr)
- ports[portKey] = true
- }
-
- return ports
-}
-
-// getListeningPortsFromProc reads /proc/net/tcp and /proc/net/tcp6 to find listening ports
-// Returns output in a format similar to ss -lntu
-func (pm *PortMonitor) getListeningPortsFromProc() (string, error) {
- var result strings.Builder
- result.WriteString("Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
-
- // Parse IPv4 listening ports
- if err := pm.parseProc("/proc/net/tcp", "tcp", &result); err != nil {
- return "", fmt.Errorf("failed to parse /proc/net/tcp: %w", err)
- }
-
- // Parse IPv6 listening ports
- if err := pm.parseProc("/proc/net/tcp6", "tcp", &result); err != nil {
- // IPv6 might not be available, log but don't fail
- slog.Debug("Failed to parse /proc/net/tcp6", "error", err)
- }
-
- // Parse UDP ports
- if err := pm.parseProc("/proc/net/udp", "udp", &result); err != nil {
- slog.Debug("Failed to parse /proc/net/udp", "error", err)
- }
-
- if err := pm.parseProc("/proc/net/udp6", "udp", &result); err != nil {
- slog.Debug("Failed to parse /proc/net/udp6", "error", err)
- }
-
- return result.String(), nil
-}
-
-// parseProc parses a /proc/net/* file for listening sockets
-func (pm *PortMonitor) parseProc(filename, protocol string, result *strings.Builder) error {
- data, err := os.ReadFile(filename)
- if err != nil {
- return err
- }
-
- lines := strings.Split(string(data), "\n")
- for i, line := range lines {
- if i == 0 || strings.TrimSpace(line) == "" {
- continue // Skip header and empty lines
- }
-
- fields := strings.Fields(line)
- if len(fields) < 4 {
- continue
- }
-
- // Parse socket state (4th field, index 3)
- stateHex := fields[3]
- state, err := strconv.ParseInt(stateHex, 16, 32)
- if err != nil {
- continue
- }
-
- // Check if socket is in LISTEN state (0x0A for TCP) or bound state for UDP
- isListening := false
- if protocol == "tcp" && state == 0x0A {
- isListening = true
- } else if protocol == "udp" && state == 0x07 {
- // UDP sockets in state 0x07 (TCP_CLOSE) are bound/listening
- isListening = true
- }
-
- if !isListening {
- continue
- }
-
- // Parse local address (2nd field, index 1)
- localAddr := fields[1]
- addr, port, err := pm.parseAddress(localAddr, strings.Contains(filename, "6"))
- if err != nil {
- continue
- }
-
- // Format similar to ss output
- result.WriteString(fmt.Sprintf("%s LISTEN 0 0 %s:%d 0.0.0.0:*\n",
- protocol, addr, port))
- }
-
- return nil
-}
-
-// parseAddress parses hex-encoded address:port from /proc/net files
-func (pm *PortMonitor) parseAddress(addrPort string, isIPv6 bool) (string, int, error) {
- parts := strings.Split(addrPort, ":")
- if len(parts) != 2 {
- return "", 0, fmt.Errorf("invalid address:port format: %s", addrPort)
- }
-
- // Parse port (stored in hex, big-endian)
- portHex := parts[1]
- port, err := strconv.ParseInt(portHex, 16, 32)
- if err != nil {
- return "", 0, fmt.Errorf("invalid port hex: %s", portHex)
- }
-
- // Parse IP address
- addrHex := parts[0]
- var addr string
-
- if isIPv6 {
- // IPv6: 32 hex chars representing 16 bytes
- if len(addrHex) != 32 {
- return "", 0, fmt.Errorf("invalid IPv6 address hex length: %d", len(addrHex))
- }
- // Convert hex to IPv6 address
- var ipBytes [16]byte
- for i := 0; i < 16; i++ {
- b, err := strconv.ParseInt(addrHex[i*2:(i+1)*2], 16, 8)
- if err != nil {
- return "", 0, fmt.Errorf("invalid IPv6 hex: %s", addrHex)
- }
- ipBytes[i] = byte(b)
- }
- // /proc stores IPv6 in little-endian 32-bit chunks, need to reverse each chunk
- for i := 0; i < 16; i += 4 {
- ipBytes[i], ipBytes[i+1], ipBytes[i+2], ipBytes[i+3] = ipBytes[i+3], ipBytes[i+2], ipBytes[i+1], ipBytes[i]
- }
- addr = net.IP(ipBytes[:]).String()
- } else {
- // IPv4: 8 hex chars representing 4 bytes in little-endian
- if len(addrHex) != 8 {
- return "", 0, fmt.Errorf("invalid IPv4 address hex length: %d", len(addrHex))
- }
- // Parse as little-endian 32-bit integer
- addrInt, err := strconv.ParseInt(addrHex, 16, 64)
- if err != nil {
- return "", 0, fmt.Errorf("invalid IPv4 hex: %s", addrHex)
- }
- // Convert to IP address (reverse byte order for little-endian)
- addr = fmt.Sprintf("%d.%d.%d.%d",
- addrInt&0xFF,
- (addrInt>>8)&0xFF,
- (addrInt>>16)&0xFF,
- (addrInt>>24)&0xFF)
- }
-
- // Handle special addresses
- if addr == "0.0.0.0" {
- addr = "*"
- } else if addr == "::" {
- addr = "*"
- }
-
- return addr, int(port), nil
-}
diff --git a/loop/port_monitor_test.go b/loop/port_monitor_test.go
deleted file mode 100644
index e0f7ff1..0000000
--- a/loop/port_monitor_test.go
+++ /dev/null
@@ -1,388 +0,0 @@
-package loop
-
-import (
- "context"
- "os"
- "strings"
- "testing"
- "time"
-)
-
-// TestPortMonitoring tests the port monitoring functionality
-func TestPortMonitoring(t *testing.T) {
- // Test with ss output format
- ssOutput := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
-tcp LISTEN 0 1024 127.0.0.1:40975 0.0.0.0:*
-tcp LISTEN 0 4096 *:22 *:*
-tcp LISTEN 0 4096 *:80 *:*
-udp UNCONN 0 0 127.0.0.1:123 0.0.0.0:*
-`
-
- expected := map[string]bool{
- "tcp:127.0.0.1:40975": true,
- "tcp:*:22": true,
- "tcp:*:80": true,
- }
-
- result := parseSSPorts(ssOutput)
-
- // Check that all expected ports are found
- for port := range expected {
- if !result[port] {
- t.Errorf("Expected port %s not found in ss parsed output", port)
- }
- }
-
- // Check that UDP port is not included (since it's UNCONN, not LISTEN)
- if result["udp:127.0.0.1:123"] {
- t.Errorf("UDP UNCONN port should not be included in listening ports")
- }
-
- // Check that no extra ports are found
- for port := range result {
- if !expected[port] {
- t.Errorf("Unexpected port %s found in parsed output", port)
- }
- }
-}
-
-// TestPortMonitoringLogDifferences tests the port difference logging
-func TestPortMonitoringLogDifferences(t *testing.T) {
- ctx := context.Background()
-
- oldPorts := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
-tcp LISTEN 0 4096 *:22 *:*
-tcp LISTEN 0 1024 127.0.0.1:8080 0.0.0.0:*
-`
-
- newPorts := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
-tcp LISTEN 0 4096 *:22 *:*
-tcp LISTEN 0 1024 127.0.0.1:9090 0.0.0.0:*
-`
-
- // Create a port monitor to test the logPortDifferences method
- pm := NewPortMonitor()
-
- // This test mainly ensures the method doesn't panic and processes the differences
- // The actual logging output would need to be captured via a test logger to verify fully
- pm.logPortDifferences(ctx, oldPorts, newPorts)
-
- // Test with no differences
- pm.logPortDifferences(ctx, oldPorts, oldPorts)
-}
-
-// TestPortMonitorCreation tests creating a new port monitor
-func TestPortMonitorCreation(t *testing.T) {
- pm := NewPortMonitor()
- if pm == nil {
- t.Error("NewPortMonitor() returned nil")
- }
-
- // Verify initial state
- pm.mu.Lock()
- defer pm.mu.Unlock()
- if pm.lastPorts != "" {
- t.Error("NewPortMonitor() should have empty lastPorts initially")
- }
-}
-
-// TestParseSSPortsEdgeCases tests edge cases in ss output parsing
-func TestParseSSPortsEdgeCases(t *testing.T) {
- tests := []struct {
- name string
- output string
- expected map[string]bool
- }{
- {
- name: "empty output",
- output: "",
- expected: map[string]bool{},
- },
- {
- name: "header only",
- output: "Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess",
- expected: map[string]bool{},
- },
- {
- name: "non-listen states filtered out",
- output: "tcp ESTAB 0 0 127.0.0.1:8080 127.0.0.1:45678\nudp UNCONN 0 0 127.0.0.1:123 0.0.0.0:*",
- expected: map[string]bool{},
- },
- {
- name: "insufficient fields",
- output: "tcp LISTEN 0",
- expected: map[string]bool{},
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- result := parseSSPorts(test.output)
- if len(result) != len(test.expected) {
- t.Errorf("Expected %d ports, got %d", len(test.expected), len(result))
- }
- for port := range test.expected {
- if !result[port] {
- t.Errorf("Expected port %s not found", port)
- }
- }
- for port := range result {
- if !test.expected[port] {
- t.Errorf("Unexpected port %s found", port)
- }
- }
- })
- }
-}
-
-// TestPortEventStorage tests the new event storage functionality
-func TestPortEventStorage(t *testing.T) {
- pm := NewPortMonitor()
-
- // Initially should have no events
- allEvents := pm.GetAllRecentEvents()
- if len(allEvents) != 0 {
- t.Errorf("Expected 0 events initially, got %d", len(allEvents))
- }
-
- // Simulate port changes that would add events
- ctx := context.Background()
- oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*"
- newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*"
-
- pm.logPortDifferences(ctx, oldPorts, newPorts)
-
- // Should now have events
- allEvents = pm.GetAllRecentEvents()
- if len(allEvents) != 2 {
- t.Errorf("Expected 2 events (1 opened, 1 closed), got %d", len(allEvents))
- }
-
- // Check event types
- foundOpened := false
- foundClosed := false
- for _, event := range allEvents {
- if event.Type == "opened" && event.Port == "tcp:0.0.0.0:9090" {
- foundOpened = true
- }
- if event.Type == "closed" && event.Port == "tcp:0.0.0.0:8080" {
- foundClosed = true
- }
- }
-
- if !foundOpened {
- t.Error("Expected to find 'opened' event for port tcp:0.0.0.0:9090")
- }
- if !foundClosed {
- t.Error("Expected to find 'closed' event for port tcp:0.0.0.0:8080")
- }
-}
-
-// TestPortEventFiltering tests the time-based filtering
-func TestPortEventFiltering(t *testing.T) {
- pm := NewPortMonitor()
- ctx := context.Background()
-
- // Record time before adding events
- beforeTime := time.Now()
- time.Sleep(1 * time.Millisecond) // Small delay to ensure timestamp difference
-
- // Add some events
- oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*"
- newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*"
- pm.logPortDifferences(ctx, oldPorts, newPorts)
-
- // Get events since beforeTime - should get all events
- recentEvents := pm.GetRecentEvents(beforeTime)
- if len(recentEvents) != 2 {
- t.Errorf("Expected 2 recent events, got %d", len(recentEvents))
- }
-
- // Get events since now - should get no events
- nowTime := time.Now()
- recentEvents = pm.GetRecentEvents(nowTime)
- if len(recentEvents) != 0 {
- t.Errorf("Expected 0 recent events since now, got %d", len(recentEvents))
- }
-}
-
-// TestParseAddress tests the hex address parsing for /proc/net files
-func TestParseAddress(t *testing.T) {
- pm := NewPortMonitor()
-
- tests := []struct {
- name string
- addrPort string
- isIPv6 bool
- expectIP string
- expectPort int
- expectErr bool
- }{
- {
- name: "IPv4 localhost:80",
- addrPort: "0100007F:0050", // 127.0.0.1:80 in little-endian hex
- isIPv6: false,
- expectIP: "127.0.0.1",
- expectPort: 80,
- expectErr: false,
- },
- {
- name: "IPv4 any:22",
- addrPort: "00000000:0016", // 0.0.0.0:22
- isIPv6: false,
- expectIP: "*",
- expectPort: 22,
- expectErr: false,
- },
- {
- name: "IPv4 high port",
- addrPort: "0100007F:1F90", // 127.0.0.1:8080
- isIPv6: false,
- expectIP: "127.0.0.1",
- expectPort: 8080,
- expectErr: false,
- },
- {
- name: "IPv6 any port 22",
- addrPort: "00000000000000000000000000000000:0016", // [::]:22
- isIPv6: true,
- expectIP: "*",
- expectPort: 22,
- expectErr: false,
- },
- {
- name: "Invalid format - no colon",
- addrPort: "0100007F0050",
- isIPv6: false,
- expectErr: true,
- },
- {
- name: "Invalid port hex",
- addrPort: "0100007F:ZZZZ",
- isIPv6: false,
- expectErr: true,
- },
- {
- name: "Invalid IPv4 hex length",
- addrPort: "0100:0050",
- isIPv6: false,
- expectErr: true,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- ip, port, err := pm.parseAddress(test.addrPort, test.isIPv6)
- if test.expectErr {
- if err == nil {
- t.Errorf("Expected error but got none")
- }
- return
- }
-
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- return
- }
-
- if ip != test.expectIP {
- t.Errorf("Expected IP %s, got %s", test.expectIP, ip)
- }
-
- if port != test.expectPort {
- t.Errorf("Expected port %d, got %d", test.expectPort, port)
- }
- })
- }
-}
-
-// TestParseProcData tests parsing of mock /proc/net data
-func TestParseProcData(t *testing.T) {
- pm := NewPortMonitor()
-
- // Test TCP data with listening sockets
- tcpData := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
- 0: 0100007F:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1
- 1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 2
- 2: 0100007F:1F90 0200007F:C350 01 00000000:00000000 00:00000000 00000000 0 0 3`
-
- var result strings.Builder
- result.WriteString("Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
-
- // Create temp file with test data
- tmpFile := "/tmp/test_tcp"
- err := os.WriteFile(tmpFile, []byte(tcpData), 0o644)
- if err != nil {
- t.Fatalf("Failed to create temp file: %v", err)
- }
- defer os.Remove(tmpFile)
-
- err = pm.parseProc(tmpFile, "tcp", &result)
- if err != nil {
- t.Fatalf("parseProc failed: %v", err)
- }
-
- output := result.String()
- t.Logf("Generated output:\n%s", output)
-
- // Should contain listening ports (state 0A = LISTEN)
- if !strings.Contains(output, "127.0.0.1:80") {
- t.Error("Expected to find 127.0.0.1:80 in output")
- }
- if !strings.Contains(output, "*:22") {
- t.Error("Expected to find *:22 in output")
- }
- // Should not contain established connection (state 01)
- if strings.Contains(output, "127.0.0.1:8080") {
- t.Error("Should not find established connection 127.0.0.1:8080 in output")
- }
-}
-
-// TestGetListeningPortsFromProcFallback tests the complete /proc fallback
-func TestGetListeningPortsFromProcFallback(t *testing.T) {
- pm := NewPortMonitor()
-
- // This test verifies the method runs without error
- // The actual files may or may not exist, but it should handle both cases gracefully
- output, err := pm.getListeningPortsFromProc()
- if err != nil {
- t.Logf("getListeningPortsFromProc failed (may be expected if /proc/net files don't exist): %v", err)
- // Don't fail the test - this might be expected in some environments
- return
- }
-
- t.Logf("Generated /proc fallback output:\n%s", output)
-
- // Should at least have a header
- if !strings.Contains(output, "Netid State") {
- t.Error("Expected header in /proc fallback output")
- }
-}
-
-// TestUpdatePortStateWithFallback tests updatePortState with both ss and /proc fallback
-func TestUpdatePortStateWithFallback(t *testing.T) {
- pm := NewPortMonitor()
- ctx := context.Background()
-
- // Call updatePortState - should try ss first, then fall back to /proc if ss fails
- pm.updatePortState(ctx)
-
- // The method should complete without panicking
- // We can't easily test the exact behavior without mocking, but we can ensure it runs
- // Check if any port state was captured
- pm.mu.Lock()
- lastPorts := pm.lastPorts
- pm.mu.Unlock()
-
- t.Logf("Captured port state (length %d):", len(lastPorts))
- if len(lastPorts) > 0 {
- t.Logf("First 200 chars: %s", lastPorts[:min(200, len(lastPorts))])
- }
-}
-
-func min(a, b int) int {
- if a < b {
- return a
- }
- return b
-}
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 7f31401..b6e2259 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -307,44 +307,6 @@
io.WriteString(w, "{}\n")
})
- // Handler for /port-events - returns recent port change events
- s.mux.HandleFunc("/port-events", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
-
- // Get the 'since' query parameter for filtering events
- sinceParam := r.URL.Query().Get("since")
- var events []loop.PortEvent
-
- // Get port monitor from agent
- portMonitor := agent.GetPortMonitor()
- if portMonitor == nil {
- // Return empty array if port monitor not available
- events = []loop.PortEvent{}
- } else if sinceParam != "" {
- // Parse the since timestamp
- sinceTime, err := time.Parse(time.RFC3339, sinceParam)
- if err != nil {
- http.Error(w, fmt.Sprintf("Invalid 'since' timestamp format: %v", err), http.StatusBadRequest)
- return
- }
- events = portMonitor.GetRecentEvents(sinceTime)
- } else {
- // Return all recent events
- events = portMonitor.GetAllRecentEvents()
- }
-
- // Encode and return the events
- if err := json.NewEncoder(w).Encode(events); err != nil {
- slog.ErrorContext(r.Context(), "Error encoding port events response", slog.Any("err", err))
- http.Error(w, "Internal server error", http.StatusInternalServerError)
- }
- })
-
// Handler for /messages?start=N&end=M (start/end are optional)
s.mux.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index adda4e3..f6ec8c7 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -3,7 +3,6 @@
import (
"bufio"
"context"
- "encoding/json"
"net/http"
"net/http/httptest"
"slices"
@@ -261,10 +260,9 @@
m.retryNumber++
}
-func (m *mockAgent) GetPortMonitor() *loop.PortMonitor { return loop.NewPortMonitor() }
-func (m *mockAgent) SkabandAddr() string { return m.skabandAddr }
-func (m *mockAgent) LinkToGitHub() bool { return false }
-func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
+func (m *mockAgent) SkabandAddr() string { return m.skabandAddr }
+func (m *mockAgent) LinkToGitHub() bool { return false }
+func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
// TestSSEStream tests the SSE stream endpoint
func TestSSEStream(t *testing.T) {
@@ -512,76 +510,6 @@
t.Log("Mock CompactConversation works correctly")
}
-// TestPortEventsEndpoint tests the /port-events HTTP endpoint
-func TestPortEventsEndpoint(t *testing.T) {
- // Create a mock agent that implements the CodingAgent interface
- agent := &mockAgent{
- branchPrefix: "sketch/",
- }
-
- // Create a server with the mock agent
- server, err := server.New(agent, nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- // Test GET /port-events
- req, err := http.NewRequest("GET", "/port-events", nil)
- if err != nil {
- t.Fatalf("Failed to create request: %v", err)
- }
-
- rr := httptest.NewRecorder()
- server.ServeHTTP(rr, req)
-
- // Should return 200 OK
- if status := rr.Code; status != http.StatusOK {
- t.Errorf("Expected status code %d, got %d", http.StatusOK, status)
- }
-
- // Should return JSON content type
- contentType := rr.Header().Get("Content-Type")
- if contentType != "application/json" {
- t.Errorf("Expected Content-Type application/json, got %s", contentType)
- }
-
- // Should return valid JSON (empty array since mock returns no events)
- var events []any
- if err := json.Unmarshal(rr.Body.Bytes(), &events); err != nil {
- t.Errorf("Failed to parse JSON response: %v", err)
- }
-
- // Should be empty array for mock agent
- if len(events) != 0 {
- t.Errorf("Expected empty events array, got %d events", len(events))
- }
-}
-
-// TestPortEventsEndpointMethodNotAllowed tests that non-GET requests are rejected
-func TestPortEventsEndpointMethodNotAllowed(t *testing.T) {
- agent := &mockAgent{
- branchPrefix: "sketch/",
- }
- server, err := server.New(agent, nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- // Test POST /port-events (should be rejected)
- req, err := http.NewRequest("POST", "/port-events", nil)
- if err != nil {
- t.Fatalf("Failed to create request: %v", err)
- }
-
- rr := httptest.NewRecorder()
- server.ServeHTTP(rr, req)
-
- // Should return 405 Method Not Allowed
- if status := rr.Code; status != http.StatusMethodNotAllowed {
- t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, status)
- }
-}
-
func TestParsePortProxyHost(t *testing.T) {
tests := []struct {
name string