219 lines
5.6 KiB
Go
219 lines
5.6 KiB
Go
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// MockBootMessageService implements BootMessageService for testing
|
|
type MockBootMessageService struct {
|
|
mu sync.Mutex
|
|
CreatedMessages []*models.Message
|
|
UpdatedMessages []*models.Message
|
|
createErr error
|
|
updateErr error
|
|
}
|
|
|
|
func (m *MockBootMessageService) CreateMessage(ctx context.Context, req *BootMessageRequest) (*models.Message, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.createErr != nil {
|
|
return nil, m.createErr
|
|
}
|
|
msg := &models.Message{
|
|
ID: "boot-msg-" + req.TaskID,
|
|
TaskSessionID: req.TaskSessionID,
|
|
TaskID: req.TaskID,
|
|
Content: req.Content,
|
|
AuthorType: models.MessageAuthorType(req.AuthorType),
|
|
Type: models.MessageType(req.Type),
|
|
Metadata: req.Metadata,
|
|
}
|
|
m.CreatedMessages = append(m.CreatedMessages, msg)
|
|
return msg, nil
|
|
}
|
|
|
|
func (m *MockBootMessageService) UpdateMessage(ctx context.Context, message *models.Message) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.updateErr != nil {
|
|
return m.updateErr
|
|
}
|
|
// Store a snapshot of the message at update time
|
|
snapshot := *message
|
|
metaCopy := make(map[string]interface{})
|
|
for k, v := range message.Metadata {
|
|
metaCopy[k] = v
|
|
}
|
|
snapshot.Metadata = metaCopy
|
|
m.UpdatedMessages = append(m.UpdatedMessages, &snapshot)
|
|
return nil
|
|
}
|
|
|
|
func (m *MockBootMessageService) getLastUpdatedMessage() *models.Message {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if len(m.UpdatedMessages) == 0 {
|
|
return nil
|
|
}
|
|
return m.UpdatedMessages[len(m.UpdatedMessages)-1]
|
|
}
|
|
|
|
func TestFinalizeBootMessage_Success(t *testing.T) {
|
|
mgr := newTestManager()
|
|
bootSvc := &MockBootMessageService{}
|
|
mgr.bootMessageService = bootSvc
|
|
|
|
msg := &models.Message{
|
|
ID: "boot-msg-1",
|
|
Metadata: map[string]interface{}{"status": "running"},
|
|
}
|
|
stopCh := make(chan struct{})
|
|
|
|
mgr.finalizeBootMessage(nil, msg, stopCh, nil, "exited")
|
|
|
|
// Verify stop channel was closed
|
|
select {
|
|
case <-stopCh:
|
|
// good, channel was closed
|
|
default:
|
|
t.Error("expected stopCh to be closed")
|
|
}
|
|
|
|
// Verify message was updated with final status
|
|
lastMsg := bootSvc.getLastUpdatedMessage()
|
|
if lastMsg == nil {
|
|
t.Fatal("expected boot message to be updated")
|
|
} else {
|
|
if lastMsg.Metadata["status"] != "exited" {
|
|
t.Errorf("expected status 'exited', got %v", lastMsg.Metadata["status"])
|
|
}
|
|
if lastMsg.Metadata["exit_code"] != 0 {
|
|
t.Errorf("expected exit_code 0, got %v", lastMsg.Metadata["exit_code"])
|
|
}
|
|
if lastMsg.Metadata["completed_at"] == nil {
|
|
t.Error("expected completed_at to be set")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinalizeBootMessage_Failed(t *testing.T) {
|
|
mgr := newTestManager()
|
|
bootSvc := &MockBootMessageService{}
|
|
mgr.bootMessageService = bootSvc
|
|
|
|
msg := &models.Message{
|
|
ID: "boot-msg-1",
|
|
Metadata: map[string]interface{}{"status": "running"},
|
|
}
|
|
stopCh := make(chan struct{})
|
|
|
|
mgr.finalizeBootMessage(nil, msg, stopCh, nil, "failed")
|
|
|
|
lastMsg := bootSvc.getLastUpdatedMessage()
|
|
if lastMsg == nil {
|
|
t.Fatal("expected boot message to be updated")
|
|
} else {
|
|
if lastMsg.Metadata["status"] != "failed" {
|
|
t.Errorf("expected status 'failed', got %v", lastMsg.Metadata["status"])
|
|
}
|
|
// Failed status should NOT have exit_code
|
|
if _, ok := lastMsg.Metadata["exit_code"]; ok {
|
|
t.Error("expected no exit_code for failed status")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinalizeBootMessage_NilMessage(t *testing.T) {
|
|
mgr := newTestManager()
|
|
bootSvc := &MockBootMessageService{}
|
|
mgr.bootMessageService = bootSvc
|
|
|
|
// Should not panic with nil message
|
|
mgr.finalizeBootMessage(nil, nil, nil, nil, "exited")
|
|
|
|
bootSvc.mu.Lock()
|
|
defer bootSvc.mu.Unlock()
|
|
if len(bootSvc.UpdatedMessages) != 0 {
|
|
t.Error("expected no updates for nil message")
|
|
}
|
|
}
|
|
|
|
func TestFinalizeBootMessage_NilService(t *testing.T) {
|
|
mgr := newTestManager()
|
|
// bootMessageService is nil by default
|
|
|
|
msg := &models.Message{
|
|
ID: "boot-msg-1",
|
|
Metadata: map[string]interface{}{"status": "running"},
|
|
}
|
|
|
|
// Should not panic with nil service
|
|
mgr.finalizeBootMessage(nil, msg, nil, nil, "exited")
|
|
}
|
|
|
|
func TestBootMessage_IsResumingFlag(t *testing.T) {
|
|
// Test that the is_resuming flag is correctly based on ACPSessionID presence
|
|
tests := []struct {
|
|
name string
|
|
acpSessionID string
|
|
wantResuming bool
|
|
}{
|
|
{
|
|
name: "new session (no ACP session ID)",
|
|
acpSessionID: "",
|
|
wantResuming: false,
|
|
},
|
|
{
|
|
name: "resumed session (has ACP session ID)",
|
|
acpSessionID: "acp-session-123",
|
|
wantResuming: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// The is_resuming logic is: execution.ACPSessionID != ""
|
|
isResuming := tt.acpSessionID != ""
|
|
if isResuming != tt.wantResuming {
|
|
t.Errorf("is_resuming = %v, want %v", isResuming, tt.wantResuming)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPollAgentStderr_StopsOnClose(t *testing.T) {
|
|
mgr := newTestManager()
|
|
bootSvc := &MockBootMessageService{}
|
|
mgr.bootMessageService = bootSvc
|
|
|
|
msg := &models.Message{
|
|
ID: "boot-msg-1",
|
|
Metadata: map[string]interface{}{"status": "running"},
|
|
}
|
|
stopCh := make(chan struct{})
|
|
|
|
// Start polling with nil client (will fail on each poll, but shouldn't panic)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
// Pass nil client - the pollAgentStderr will log errors but should exit on stop
|
|
// We can't use a nil client directly since it would panic on method call.
|
|
// Instead, just test that close(stopCh) causes the goroutine to exit.
|
|
close(stopCh)
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Good, goroutine exited
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("pollAgentStderr did not stop within timeout")
|
|
}
|
|
|
|
_ = msg // msg would be used in a real poll
|
|
}
|