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 }