package executor import ( "context" "errors" "testing" "github.com/thepeterstone/claudomator/internal/event" "github.com/thepeterstone/claudomator/internal/storage" "github.com/thepeterstone/claudomator/internal/task" ) type fakeChannelStore struct { createdTasks []*task.Task createdEvents []*event.Event createTaskErr error } func (f *fakeChannelStore) CreateTask(t *task.Task) error { if f.createTaskErr != nil { return f.createTaskErr } f.createdTasks = append(f.createdTasks, t) return nil } func (f *fakeChannelStore) CreateEvent(e *event.Event) error { f.createdEvents = append(f.createdEvents, e) return nil } func TestStoreChannel_AskUser_ReturnsBlocked(t *testing.T) { ch := newStoreChannel(&fakeChannelStore{}, "task-1", &storage.Execution{}) answer, err := ch.AskUser(context.Background(), `{"text":"q"}`) if !errors.Is(err, ErrAgentBlocked) { t.Errorf("expected ErrAgentBlocked, got %v", err) } if answer != "" { t.Errorf("expected empty answer, got %q", answer) } } func TestStoreChannel_ReportSummary_BuffersOntoExec(t *testing.T) { e := &storage.Execution{} ch := newStoreChannel(&fakeChannelStore{}, "task-1", e) if err := ch.ReportSummary(context.Background(), "did the thing"); err != nil { t.Fatalf("ReportSummary: %v", err) } if e.Summary != "did the thing" { t.Errorf("expected exec.Summary set, got %q", e.Summary) } } func TestStoreChannel_ReportSummary_NilExec(t *testing.T) { ch := newStoreChannel(&fakeChannelStore{}, "task-1", nil) if err := ch.ReportSummary(context.Background(), "x"); err != nil { t.Errorf("expected nil err with nil exec, got %v", err) } } func TestStoreChannel_SpawnSubtask_CreatesChildWithParent(t *testing.T) { store := &fakeChannelStore{} ch := newStoreChannel(store, "parent-1", &storage.Execution{}) id, err := ch.SpawnSubtask(context.Background(), SubtaskSpec{ Name: "child", Instructions: "do it", Model: "sonnet", MaxBudgetUSD: 1.5, }) if err != nil { t.Fatalf("SpawnSubtask: %v", err) } if id == "" { t.Error("expected non-empty subtask ID") } if len(store.createdTasks) != 1 { t.Fatalf("expected 1 created task, got %d", len(store.createdTasks)) } ct := store.createdTasks[0] if ct.ParentTaskID != "parent-1" { t.Errorf("ParentTaskID: got %q want parent-1", ct.ParentTaskID) } if ct.ID != id { t.Errorf("returned id %q != created task id %q", id, ct.ID) } if ct.Agent.Instructions != "do it" || ct.Agent.Model != "sonnet" || ct.Agent.MaxBudgetUSD != 1.5 { t.Errorf("agent config not propagated: %+v", ct.Agent) } if ct.State != task.StatePending { t.Errorf("expected PENDING child, got %s", ct.State) } } func TestStoreChannel_SpawnSubtask_PropagatesError(t *testing.T) { store := &fakeChannelStore{createTaskErr: errors.New("boom")} ch := newStoreChannel(store, "parent-1", &storage.Execution{}) if _, err := ch.SpawnSubtask(context.Background(), SubtaskSpec{Name: "x"}); err == nil { t.Error("expected error from CreateTask") } } func TestStoreChannel_RecordProgress_EmitsAgentMessage(t *testing.T) { store := &fakeChannelStore{} ch := newStoreChannel(store, "task-1", &storage.Execution{}) if err := ch.RecordProgress(context.Background(), "halfway done"); err != nil { t.Fatalf("RecordProgress: %v", err) } if len(store.createdEvents) != 1 { t.Fatalf("expected 1 event, got %d", len(store.createdEvents)) } e := store.createdEvents[0] if e.Kind != event.KindAgentMessage || e.Actor != event.ActorAgent { t.Errorf("got kind=%v actor=%v want agent_message/agent", e.Kind, e.Actor) } if e.TaskID != "task-1" { t.Errorf("TaskID: got %q want task-1", e.TaskID) } }