/ components / execd / pkg / runtime / command_status_test.go
command_status_test.go
  1  // Copyright 2025 Alibaba Group Holding Ltd.
  2  //
  3  // Licensed under the Apache License, Version 2.0 (the "License");
  4  // you may not use this file except in compliance with the License.
  5  // You may obtain a copy of the License at
  6  //
  7  //     http://www.apache.org/licenses/LICENSE-2.0
  8  //
  9  // Unless required by applicable law or agreed to in writing, software
 10  // distributed under the License is distributed on an "AS IS" BASIS,
 11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  // See the License for the specific language governing permissions and
 13  // limitations under the License.
 14  
 15  package runtime
 16  
 17  import (
 18  	"context"
 19  	"os"
 20  	"path/filepath"
 21  	"strings"
 22  	"testing"
 23  	"time"
 24  
 25  	"github.com/stretchr/testify/require"
 26  )
 27  
 28  func TestGetCommandStatus_NotFound(t *testing.T) {
 29  	c := NewController("", "")
 30  
 31  	_, err := c.GetCommandStatus("missing")
 32  	require.Error(t, err, "expected error for missing session")
 33  }
 34  
 35  func TestGetCommandStatus_Running(t *testing.T) {
 36  	c := NewController("", "")
 37  
 38  	var session string
 39  	req := &ExecuteCodeRequest{
 40  		Language: BackgroundCommand,
 41  		Code:     "sleep 2",
 42  		Hooks: ExecuteResultHook{
 43  			OnExecuteInit:     func(id string) { session = id },
 44  			OnExecuteComplete: func(time.Duration) {},
 45  		},
 46  	}
 47  
 48  	ctx, cancel := context.WithCancel(context.Background())
 49  	require.NoError(t, c.runBackgroundCommand(ctx, cancel, req))
 50  	require.NotEmpty(t, session, "session should be set by OnExecuteInit")
 51  
 52  	// Poll until status is registered (runBackgroundCommand stores kernel asynchronously).
 53  	deadline := time.Now().Add(5 * time.Second)
 54  	var (
 55  		status *CommandStatus
 56  		err    error
 57  	)
 58  	for time.Now().Before(deadline) {
 59  		status, err = c.GetCommandStatus(session)
 60  		if err == nil {
 61  			break
 62  		}
 63  		if strings.Contains(err.Error(), "not found") {
 64  			time.Sleep(50 * time.Millisecond)
 65  			continue
 66  		}
 67  		require.NoError(t, err, "GetCommandStatus unexpected error")
 68  	}
 69  	require.NoError(t, err, "GetCommandStatus error after retry")
 70  
 71  	require.NotNil(t, status)
 72  	require.True(t, status.Running, "expected running=true")
 73  	require.Nil(t, status.ExitCode, "expected exitCode to be nil while running")
 74  	require.Nil(t, status.FinishedAt, "expected finishedAt to be nil while running")
 75  	require.False(t, status.StartedAt.IsZero(), "expected startedAt to be set")
 76  	t.Log(status)
 77  }
 78  
 79  func TestSeekBackgroundCommandOutput_Completed(t *testing.T) {
 80  	c := NewController("", "")
 81  
 82  	tmpDir := t.TempDir()
 83  	session := "sess-done"
 84  	stdoutPath := filepath.Join(tmpDir, session+".stdout")
 85  
 86  	stdoutContent := "hello stdout"
 87  	require.NoError(t, os.WriteFile(stdoutPath, []byte(stdoutContent), 0o644))
 88  
 89  	started := time.Now().Add(-2 * time.Second)
 90  	finished := time.Now()
 91  	exitCode := 0
 92  	kernel := &commandKernel{
 93  		pid:          456,
 94  		stdoutPath:   stdoutPath,
 95  		isBackground: true,
 96  		startedAt:    started,
 97  		finishedAt:   &finished,
 98  		exitCode:     &exitCode,
 99  		errMsg:       "",
100  		running:      false,
101  	}
102  	c.storeCommandKernel(session, kernel)
103  
104  	output, cursor, err := c.SeekBackgroundCommandOutput(session, 0)
105  	require.NoError(t, err, "GetCommandOutput error")
106  
107  	require.Greater(t, cursor, int64(0), "expected cursor>=0")
108  	require.Equal(t, stdoutContent, string(output))
109  }
110  
111  func TestSeekBackgroundCommandOutput_WithRunBackgroundCommand(t *testing.T) {
112  	c := NewController("", "")
113  
114  	expected := "line1\nline2\n"
115  	var session string
116  	req := &ExecuteCodeRequest{
117  		Language: BackgroundCommand,
118  		Code:     "printf 'line1\nline2\n'",
119  		Hooks: ExecuteResultHook{
120  			OnExecuteInit:     func(id string) { session = id },
121  			OnExecuteComplete: func(executionTime time.Duration) {},
122  			// other hooks unused in this test
123  		},
124  	}
125  
126  	ctx, cancel := context.WithCancel(context.Background())
127  	require.NoError(t, c.runBackgroundCommand(ctx, cancel, req))
128  	require.NotEmpty(t, session, "session should be set by OnExecuteInit")
129  
130  	var (
131  		output []byte
132  		cursor int64
133  		err    error
134  	)
135  
136  	deadline := time.Now().Add(5 * time.Second)
137  	for time.Now().Before(deadline) {
138  		output, cursor, err = c.SeekBackgroundCommandOutput(session, 0)
139  		if err == nil && len(output) > 0 {
140  			break
141  		}
142  		time.Sleep(100 * time.Millisecond)
143  	}
144  	require.NoError(t, err, "SeekBackgroundCommandOutput error")
145  	require.Equal(t, expected, string(output))
146  	require.GreaterOrEqual(t, cursor, int64(len(expected)), "cursor should advance to end of file")
147  
148  	// incremental seek from current cursor should return empty data and same-or-higher cursor
149  	output2, cursor2, err := c.SeekBackgroundCommandOutput(session, cursor)
150  	require.NoError(t, err, "SeekBackgroundCommandOutput (second call) error")
151  	require.Empty(t, output2, "expected no new output")
152  	require.GreaterOrEqual(t, cursor2, cursor, "cursor should not move backwards")
153  }