package executor import ( "context" "errors" "testing" "github.com/thepeterstone/claudomator/internal/event" "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_BuffersAndBlocks(t *testing.T) { ch := newStoreChannel(&fakeChannelStore{}, "task-1") 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) } q, blocked := ch.PendingQuestion() if !blocked || q != `{"text":"q"}` { t.Errorf("expected buffered question, got q=%q blocked=%v", q, blocked) } } func TestStoreChannel_PendingQuestion_DefaultNotBlocked(t *testing.T) { ch := newStoreChannel(&fakeChannelStore{}, "task-1") if q, blocked := ch.PendingQuestion(); blocked || q != "" { t.Errorf("expected no pending question, got q=%q blocked=%v", q, blocked) } } func TestStoreChannel_ReportSummary_Buffers(t *testing.T) { ch := newStoreChannel(&fakeChannelStore{}, "task-1") if _, ok := ch.ReportedSummary(); ok { t.Error("expected no summary before report") } if err := ch.ReportSummary(context.Background(), "did the thing"); err != nil { t.Fatalf("ReportSummary: %v", err) } sum, ok := ch.ReportedSummary() if !ok || sum != "did the thing" { t.Errorf("expected buffered summary, got %q ok=%v", sum, ok) } } func TestStoreChannel_SpawnSubtask_CreatesChildWithParent(t *testing.T) { store := &fakeChannelStore{} ch := newStoreChannel(store, "parent-1") 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") 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") 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) } }