Files
nezha_domains/integration/agentcompat/internal/scenario/held_terminal_test.go
T
2026-07-20 04:51:20 +00:00

258 lines
8.2 KiB
Go

//go:build linux
package scenario
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
)
func TestHeldTerminalCommandSubmitsWithLineFeed(t *testing.T) {
command := heldTerminalCommand("marker-session")
require.Equal(t, byte('\n'), command[len(command)-1])
require.NotContains(t, command, "exit\\n")
}
func TestHeldTerminalProofRejectsEchoOnlyExactTokens(t *testing.T) {
proof := newHeldTerminalProof("marker-session")
proof.Consume(client.Frame{Type: client.FrameText, Payload: []byte("printf 'compat-size='; stty size; printf 'marker-session\\n'; read -r held_terminal_release; exit\\n\r\ncompat-size=43 132\r\nmarker-session\r\n")})
require.False(t, proof.Complete())
}
func TestHeldTerminalProofAcceptsMarkerAndExactSizeAcrossFrames(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
// When
proof.Consume(client.Frame{Type: client.FrameText, Payload: []byte("prefix\r\n\x1emarker-session|43 ")})
proof.Consume(client.Frame{Type: client.FrameBinary, Payload: []byte("132\x1f\r\n")})
// Then
require.True(t, proof.Complete())
require.Equal(t, uint32(43), proof.Rows())
require.Equal(t, uint32(132), proof.Columns())
}
func TestHeldTerminalProofIgnoresMarkerInEchoedCommand(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
// When
proof.Consume(client.Frame{Type: client.FrameText, Payload: []byte("printf '\\036%s%s|' 'marker-' 'session'; stty size; printf '\\037'\r\n")})
proof.Consume(client.Frame{Type: client.FrameText, Payload: []byte("\x1emarker-session|43 132\x1f\r\n")})
// Then
require.True(t, proof.Complete())
require.Equal(t, uint32(43), proof.Rows())
require.Equal(t, uint32(132), proof.Columns())
}
func TestHeldTerminalProofRejectsWrongMarkerAndWrongSize(t *testing.T) {
// Given
wrongMarker := newHeldTerminalProof("marker-session")
wrongSize := newHeldTerminalProof("marker-session")
// When
wrongMarker.Consume(client.Frame{Type: client.FrameText, Payload: []byte("\x1emarker-other|43 132\x1f\r\n")})
wrongSize.Consume(client.Frame{Type: client.FrameText, Payload: []byte("\x1emarker-session|42 132\x1f\r\n")})
// Then
require.False(t, wrongMarker.Complete())
require.False(t, wrongSize.Complete())
}
func TestHeldTerminalProofAcceptsValidRecordAfterWrongMarkerInSameFrame(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
frame := []byte("\x1emarker-other|43 132\x1f\x1emarker-session|43 132\x1f")
// When
proof.Consume(client.Frame{Type: client.FrameText, Payload: frame})
// Then
require.True(t, proof.Complete())
}
func TestHeldTerminalProofAcceptsValidRecordAfterMalformedRecordInSameFrame(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
frame := []byte("\x1emarker-session|43 nope\x1f\x1emarker-session|43 132\x1f")
// When
proof.Consume(client.Frame{Type: client.FrameBinary, Payload: frame})
// Then
require.True(t, proof.Complete())
}
func TestHeldTerminalProofScansMultipleInvalidRecordsBeforeValidRecord(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
frame := []byte("noise\x1emarker-other|43 132\x1f\x1emarker-session|43 nope\x1f\x1emarker-session|43 132\x1f")
// When
proof.Consume(client.Frame{Type: client.FrameText, Payload: frame})
// Then
require.True(t, proof.Complete())
}
func TestHeldTerminalProofAcceptsFramedRecordAcrossFrames(t *testing.T) {
proof := newHeldTerminalProof("marker-session")
proof.Consume(client.Frame{Type: client.FrameBinary, Payload: []byte("prefix\x1emarker-session|43 ")})
proof.Consume(client.Frame{Type: client.FrameBinary, Payload: []byte("132\x1f\r\n")})
require.True(t, proof.Complete())
}
func TestHeldTerminalProofRejectsMalformedOrUnclosedRecord(t *testing.T) {
malformed := newHeldTerminalProof("marker-session")
unclosed := newHeldTerminalProof("marker-session")
malformed.Consume(client.Frame{Type: client.FrameBinary, Payload: []byte("\x1emarker-session|43 nope\x1f")})
unclosed.Consume(client.Frame{Type: client.FrameBinary, Payload: []byte("\x1emarker-session|43 132")})
require.False(t, malformed.Complete())
require.False(t, unclosed.Complete())
require.Equal(t, []byte("\x1emarker-session|43 132"), unclosed.buffer)
}
func TestHeldTerminalProofAcceptsEchoThenFramedRecord(t *testing.T) {
proof := newHeldTerminalProof("marker-session")
proof.Consume(client.Frame{Type: client.FrameText, Payload: []byte("printf '\\036%s%s|' 'marker-' 'session'; stty size; printf '\\037' exit\\n\r\n")})
require.False(t, proof.Complete())
proof.Consume(client.Frame{Type: client.FrameBinary, Payload: []byte("\x1emarker-session|43 132\x1f")})
require.True(t, proof.Complete())
}
func TestHeldTerminalProofRejectsClosedPumpBeforeProof(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
// When
err := proof.Closed(errors.New("pump closed"))
// Then
require.Error(t, err)
require.ErrorContains(t, err, "pump closed")
}
func TestHeldTerminalProofBoundsAccumulator(t *testing.T) {
// Given
proof := newHeldTerminalProof("marker-session")
// When
proof.Consume(client.Frame{Type: client.FrameText, Payload: make([]byte, heldTerminalProofLimit+1)})
// Then
require.LessOrEqual(t, len(proof.buffer), heldTerminalProofLimit)
}
func TestHeldTerminalResponseRequiresExactSessionServerIdentity(t *testing.T) {
// Given
response := terminalCreateResponse{SessionID: "session", ServerID: 9}
// When
err := validateHeldTerminalResponse(response, 8)
// Then
require.ErrorIs(t, err, ErrHeldTerminalProtocol)
require.NoError(t, validateHeldTerminalResponse(response, 9))
}
func TestHeldTerminalInputRejectsMissingResourcesAndMismatchedReadiness(t *testing.T) {
// Given
plan := heldTestPlan(t)
input := heldTerminalInput{Plan: plan, Readiness: agent.Readiness{ServerID: 7, UUID: "agent"}}
// When
err := validateHeldTerminalInput(context.Background(), input)
// Then
require.ErrorIs(t, err, ErrInvalidHeldTerminalInput)
}
func TestHeldTerminalInputRejectsMissingPATClientBeforeRemoteMutation(t *testing.T) {
err := validateHeldPATClient(nil)
require.ErrorIs(t, err, ErrInvalidHeldPATClient)
}
func TestHeldTerminalCommandKeepsShellHeldUntilInput(t *testing.T) {
// Given
command := heldTerminalCommand("marker-session")
// Then
require.Contains(t, command, "marker-")
require.Contains(t, command, "session")
require.Contains(t, command, "stty size")
require.Contains(t, command, "read -r")
require.Contains(t, command, "exit")
require.Contains(t, command, "\\036")
require.Contains(t, command, "\\037")
require.NotEqual(t, "\n", command)
}
func TestHeldTerminalCleanupOwnerDoesNotUseCanceledWaiter(t *testing.T) {
// Given
cleanupStarted := make(chan struct{})
cleanupRelease := make(chan struct{})
lifecycle := heldTestLifecycle(t, "held-terminal-session")
stack := newHeldCleanupStack()
require.NoError(t, stack.Push(heldCleanupAction{name: "blocked", cleanup: func(cleanupContext context.Context) error {
require.NoError(t, cleanupContext.Err())
close(cleanupStarted)
<-cleanupRelease
return nil
}}))
session := newHeldTerminalSessionForTest(lifecycle, stack)
// When
canceled, cancel := context.WithCancel(context.Background())
cancel()
first := make(chan error, 1)
go func() { first <- session.Close(canceled) }()
<-cleanupStarted
// Then
require.ErrorIs(t, <-first, context.Canceled)
close(cleanupRelease)
require.NoError(t, session.Close(context.Background()))
}
func TestHeldTerminalCleanupStackReleasesHoldBeforeTransport(t *testing.T) {
// Given
stack := newHeldCleanupStack()
var order []string
require.NoError(t, stack.Push(heldCleanupAction{name: "absence", cleanup: func(context.Context) error {
order = append(order, "absence")
return nil
}}))
require.NoError(t, stack.Push(heldCleanupAction{name: "transport", cleanup: func(context.Context) error {
order = append(order, "transport")
return nil
}}))
require.NoError(t, stack.Push(heldCleanupAction{name: "release", cleanup: func(context.Context) error {
order = append(order, "release")
return nil
}}))
// When
require.NoError(t, stack.Run(context.Background()))
// Then
require.Equal(t, []string{"release", "transport", "absence"}, order)
}