package pluginsdk import ( "context" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // dataRecordingHost is a fake Go-native Host implementation whose Host data // API (ADR 0043) accessors are backed by in-memory fixtures, used to prove // grpcHostServer -> grpcHostClient reachability for the read RPCs added in // task-03. State/secrets/emit are inherited (unimplemented) from // UnimplementedHostData since these tests only exercise the data accessors. type dataRecordingHost struct { UnimplementedHostData tasks map[string]Task taskList []Task taskPageInfo *PageInfo sessions []Session sessionPage *PageInfo codeStats []SessionCodeStats codeStatsPage *PageInfo workspaces []Workspace workflows []Workflow workflowSteps []WorkflowStep agentProfiles []AgentProfile repositories []Repository messages []Message messagePage *PageInfo utilityText string lastTaskFilter TaskFilter lastSessionFilter SessionFilter lastMessageFilter MessageFilter lastWorkspaceID string lastWorkflowID string lastUtilityPrompt string createdTask Task lastCreateInput CreateTaskInput updatedTask Task lastUpdateInput UpdateTaskInput messageDispatch MessageDispatch lastSendTask string lastSendSession string lastSendText string } func (h *dataRecordingHost) GetState(context.Context, string, string, string) (map[string]any, bool, error) { return nil, false, nil } func (h *dataRecordingHost) SetState(context.Context, string, string, string, map[string]any) error { return nil } func (h *dataRecordingHost) DeleteState(context.Context, string, string, string) error { return nil } func (h *dataRecordingHost) ListState(context.Context, string, string) ([]StateEntry, error) { return nil, nil } func (h *dataRecordingHost) GetConfig(context.Context) (map[string]any, error) { return nil, nil } func (h *dataRecordingHost) GetSecret(context.Context, string) (string, bool, error) { return "", false, nil } func (h *dataRecordingHost) SetSecret(context.Context, string, string) error { return nil } func (h *dataRecordingHost) DeleteSecret(context.Context, string) error { return nil } func (h *dataRecordingHost) RevealSecret(context.Context, string) (string, error) { return "", nil } func (h *dataRecordingHost) EmitEvent(context.Context, string, map[string]any) error { return nil } func (h *dataRecordingHost) Tasks() TaskReader { return dataRecordingTaskReader{h} } func (h *dataRecordingHost) Sessions() SessionReader { return dataRecordingSessionReader{h} } func (h *dataRecordingHost) Workspaces() WorkspaceReader { return dataRecordingWorkspaceReader{h} } func (h *dataRecordingHost) Workflows() WorkflowReader { return dataRecordingWorkflowReader{h} } func (h *dataRecordingHost) AgentProfiles() AgentProfileReader { return dataRecordingAgentProfileReader{h} } func (h *dataRecordingHost) Repositories() RepositoryReader { return dataRecordingRepositoryReader{h} } func (h *dataRecordingHost) Messages() MessageReader { return dataRecordingMessageReader{h} } func (h *dataRecordingHost) InvokeUtilityAgent(_ context.Context, prompt string) (string, error) { h.lastUtilityPrompt = prompt return h.utilityText, nil } type dataRecordingTaskReader struct{ h *dataRecordingHost } func (r dataRecordingTaskReader) List(_ context.Context, filter TaskFilter, _ Page) ([]Task, *PageInfo, error) { r.h.lastTaskFilter = filter return r.h.taskList, r.h.taskPageInfo, nil } func (r dataRecordingTaskReader) Get(_ context.Context, id string) (*Task, error) { task, ok := r.h.tasks[id] if !ok { return nil, status.Errorf(codes.NotFound, "task %q not found", id) } return &task, nil } func (r dataRecordingTaskReader) Create(_ context.Context, in CreateTaskInput) (*Task, error) { r.h.lastCreateInput = in task := r.h.createdTask return &task, nil } func (r dataRecordingTaskReader) Update(_ context.Context, in UpdateTaskInput) (*Task, error) { r.h.lastUpdateInput = in task := r.h.updatedTask return &task, nil } type dataRecordingSessionReader struct{ h *dataRecordingHost } func (r dataRecordingSessionReader) List(_ context.Context, filter SessionFilter, _ Page) ([]Session, *PageInfo, error) { r.h.lastSessionFilter = filter return r.h.sessions, r.h.sessionPage, nil } func (r dataRecordingSessionReader) CodeStats(_ context.Context, filter SessionFilter, _ Page) ([]SessionCodeStats, *PageInfo, error) { r.h.lastSessionFilter = filter return r.h.codeStats, r.h.codeStatsPage, nil } type dataRecordingWorkspaceReader struct{ h *dataRecordingHost } func (r dataRecordingWorkspaceReader) List(context.Context, Page) ([]Workspace, *PageInfo, error) { return r.h.workspaces, nil, nil } type dataRecordingWorkflowReader struct{ h *dataRecordingHost } func (r dataRecordingWorkflowReader) List(_ context.Context, workspaceID string, _ Page) ([]Workflow, *PageInfo, error) { r.h.lastWorkspaceID = workspaceID return r.h.workflows, nil, nil } func (r dataRecordingWorkflowReader) ListSteps(_ context.Context, workflowID string) ([]WorkflowStep, error) { r.h.lastWorkflowID = workflowID return r.h.workflowSteps, nil } type dataRecordingAgentProfileReader struct{ h *dataRecordingHost } func (r dataRecordingAgentProfileReader) List(context.Context, Page) ([]AgentProfile, *PageInfo, error) { return r.h.agentProfiles, nil, nil } type dataRecordingRepositoryReader struct{ h *dataRecordingHost } func (r dataRecordingRepositoryReader) List(_ context.Context, workspaceID string, _ Page) ([]Repository, *PageInfo, error) { r.h.lastWorkspaceID = workspaceID return r.h.repositories, nil, nil } type dataRecordingMessageReader struct{ h *dataRecordingHost } func (r dataRecordingMessageReader) List(_ context.Context, filter MessageFilter, _ Page) ([]Message, *PageInfo, error) { r.h.lastMessageFilter = filter return r.h.messages, r.h.messagePage, nil } func (r dataRecordingMessageReader) Send(_ context.Context, taskID, sessionID, text string) (*MessageDispatch, error) { r.h.lastSendTask = taskID r.h.lastSendSession = sessionID r.h.lastSendText = text dispatch := r.h.messageDispatch return &dispatch, nil } func TestHostData_TasksListAndGet(t *testing.T) { impl := &dataRecordingHost{ tasks: map[string]Task{ "task-1": {ID: "task-1", Title: "Fix the bug", State: "todo", Repositories: []TaskRepository{{ ID: "tr-1", RepositoryID: "repo-1", BaseBranch: "main", Position: 0, CheckoutBranch: "feature/fix", }}}, }, taskList: []Task{{ID: "task-1", Title: "Fix the bug", State: "todo", Repositories: []TaskRepository{{ ID: "tr-1", RepositoryID: "repo-1", BaseBranch: "main", Position: 0, CheckoutBranch: "feature/fix", }}}}, taskPageInfo: &PageInfo{NextCursor: "next-1", HasMore: true}, } host := dialHostOverBufconn(t, impl) tasks, pageInfo, err := host.Tasks().List(context.Background(), TaskFilter{States: []string{"todo"}}, Page{Limit: 10}) require.NoError(t, err) require.Equal(t, []Task{{ID: "task-1", Title: "Fix the bug", State: "todo", Repositories: []TaskRepository{{ ID: "tr-1", RepositoryID: "repo-1", BaseBranch: "main", Position: 0, CheckoutBranch: "feature/fix", }}}}, tasks) require.Equal(t, &PageInfo{NextCursor: "next-1", HasMore: true}, pageInfo) require.Equal(t, TaskFilter{States: []string{"todo"}}, impl.lastTaskFilter) task, err := host.Tasks().Get(context.Background(), "task-1") require.NoError(t, err) require.Equal(t, "Fix the bug", task.Title) require.Equal(t, "feature/fix", task.Repositories[0].CheckoutBranch) _, err = host.Tasks().Get(context.Background(), "missing") require.Error(t, err) require.Equal(t, codes.NotFound, status.Code(err)) } // TestHostData_TaskWritesAndMessage proves the write RPCs (CreateTask, // UpdateTask, SendMessage) round-trip inputs and results through // grpcHostClient -> proto -> grpcHostServer, including the optional-pointer // fields on the update mask. func TestHostData_TaskWritesAndMessage(t *testing.T) { impl := &dataRecordingHost{ createdTask: Task{ID: "task-new", Title: "Investigate crash", State: "TODO"}, updatedTask: Task{ID: "task-1", Title: "Renamed", State: "IN_PROGRESS"}, messageDispatch: MessageDispatch{SessionID: "session-1", Status: "queued"}, } host := dialHostOverBufconn(t, impl) stepID := "step-2" created, err := host.Tasks().Create(context.Background(), CreateTaskInput{ WorkspaceID: "ws-1", WorkflowID: "wf-1", WorkflowStepID: &stepID, Title: "Investigate crash", Description: "stack trace attached", StartAgent: true, }) require.NoError(t, err) require.Equal(t, "task-new", created.ID) require.Equal(t, "ws-1", impl.lastCreateInput.WorkspaceID) require.NotNil(t, impl.lastCreateInput.WorkflowStepID) require.Equal(t, "step-2", *impl.lastCreateInput.WorkflowStepID) require.True(t, impl.lastCreateInput.StartAgent) title := "Renamed" state := "IN_PROGRESS" updated, err := host.Tasks().Update(context.Background(), UpdateTaskInput{ID: "task-1", Title: &title, State: &state}) require.NoError(t, err) require.Equal(t, "IN_PROGRESS", updated.State) require.Equal(t, "task-1", impl.lastUpdateInput.ID) require.NotNil(t, impl.lastUpdateInput.Title) require.Equal(t, "Renamed", *impl.lastUpdateInput.Title) require.Nil(t, impl.lastUpdateInput.Description, "an unset field must stay nil across the wire") dispatch, err := host.Messages().Send(context.Background(), "task-1", "session-1", "rerun the tests") require.NoError(t, err) require.Equal(t, "session-1", dispatch.SessionID) require.Equal(t, "queued", dispatch.Status) require.Equal(t, "task-1", impl.lastSendTask) require.Equal(t, "session-1", impl.lastSendSession) require.Equal(t, "rerun the tests", impl.lastSendText) } func TestHostData_SessionsListAndCodeStats(t *testing.T) { impl := &dataRecordingHost{ sessions: []Session{{ID: "session-1", TaskID: "task-1", State: "running"}}, codeStats: []SessionCodeStats{{SessionID: "session-1", LinesAddedCommitted: 10, CommittedLinesAvailable: true}}, } host := dialHostOverBufconn(t, impl) sessions, _, err := host.Sessions().List(context.Background(), SessionFilter{TaskIDs: []string{"task-1"}}, Page{}) require.NoError(t, err) require.Equal(t, impl.sessions, sessions) require.Equal(t, SessionFilter{TaskIDs: []string{"task-1"}}, impl.lastSessionFilter) stats, _, err := host.Sessions().CodeStats(context.Background(), SessionFilter{TaskIDs: []string{"task-1"}}, Page{}) require.NoError(t, err) require.Equal(t, impl.codeStats, stats) } func TestHostData_Workspaces(t *testing.T) { impl := &dataRecordingHost{workspaces: []Workspace{{ID: "ws-1", Name: "Acme"}}} host := dialHostOverBufconn(t, impl) workspaces, _, err := host.Workspaces().List(context.Background(), Page{}) require.NoError(t, err) require.Equal(t, impl.workspaces, workspaces) } func TestHostData_InvokeUtilityAgent(t *testing.T) { impl := &dataRecordingHost{utilityText: "the completion"} host := dialHostOverBufconn(t, impl) text, err := host.InvokeUtilityAgent(context.Background(), "do the thing") require.NoError(t, err) require.Equal(t, "the completion", text) require.Equal(t, "do the thing", impl.lastUtilityPrompt) } func TestHostData_Messages(t *testing.T) { impl := &dataRecordingHost{ messages: []Message{{ ID: "m1", SessionID: "s1", TaskID: "t1", TurnID: "turn1", AuthorType: "user", Content: "hello", Type: "message", CreatedAt: "2026-07-20T09:00:00Z", }}, messagePage: &PageInfo{NextCursor: "2", HasMore: true}, } host := dialHostOverBufconn(t, impl) since := "2026-07-20T00:00:00Z" filter := MessageFilter{SessionIDs: []string{"s1"}, TaskIDs: []string{"t1"}, Since: &since, Types: []string{"message"}} messages, pageInfo, err := host.Messages().List(context.Background(), filter, Page{Limit: 10}) require.NoError(t, err) require.Equal(t, impl.messages, messages) require.Equal(t, &PageInfo{NextCursor: "2", HasMore: true}, pageInfo) // Filter (including the optional Since pointer) round-trips through proto. require.Equal(t, filter, impl.lastMessageFilter) require.NotNil(t, impl.lastMessageFilter.Since) require.Equal(t, since, *impl.lastMessageFilter.Since) } func TestHostData_WorkflowsAndSteps(t *testing.T) { impl := &dataRecordingHost{ workflows: []Workflow{{ID: "wf-1", WorkspaceID: "ws-1", Name: "Default"}}, workflowSteps: []WorkflowStep{{ID: "step-1", WorkflowID: "wf-1", Name: "Review"}}, } host := dialHostOverBufconn(t, impl) workflows, _, err := host.Workflows().List(context.Background(), "ws-1", Page{}) require.NoError(t, err) require.Equal(t, impl.workflows, workflows) require.Equal(t, "ws-1", impl.lastWorkspaceID) steps, err := host.Workflows().ListSteps(context.Background(), "wf-1") require.NoError(t, err) require.Equal(t, impl.workflowSteps, steps) require.Equal(t, "wf-1", impl.lastWorkflowID) } func TestHostData_AgentProfiles(t *testing.T) { impl := &dataRecordingHost{agentProfiles: []AgentProfile{{ID: "profile-1", DisplayName: "Claude"}}} host := dialHostOverBufconn(t, impl) profiles, _, err := host.AgentProfiles().List(context.Background(), Page{}) require.NoError(t, err) require.Equal(t, impl.agentProfiles, profiles) } func TestHostData_Repositories(t *testing.T) { impl := &dataRecordingHost{repositories: []Repository{{ ID: "repo-1", WorkspaceID: "ws-1", Name: "team/kandev", SourceType: "provider", ProviderID: "example-vcs", ProviderRepositoryID: "repo-42", ProviderHost: "code.example.test", OwnerOrProject: "team", ProviderName: "kandev", RemoteURL: "https://code.example.test/scm/team/kandev.git", }}} host := dialHostOverBufconn(t, impl) repos, _, err := host.Repositories().List(context.Background(), "ws-1", Page{}) require.NoError(t, err) require.Equal(t, impl.repositories, repos) require.Equal(t, "ws-1", impl.lastWorkspaceID) } // TestHostData_UnimplementedHostData_ReturnsUnimplemented proves that a Host // implementation embedding UnimplementedHostData (task-03's default for // task-04 to override) compiles against the Host interface and, over the // wire, returns a gRPC Unimplemented status for every Host data API RPC — // so a plugin author calling an unwired accessor gets a clear error rather // than a panic or a zero-value success. func TestHostData_UnimplementedHostData_ReturnsUnimplemented(t *testing.T) { impl := &recordingHost{} host := dialHostOverBufconn(t, impl) _, _, err := host.Tasks().List(context.Background(), TaskFilter{}, Page{}) require.Error(t, err) require.Equal(t, codes.Unimplemented, status.Code(err)) _, err = host.Tasks().Get(context.Background(), "task-1") require.Error(t, err) require.Equal(t, codes.Unimplemented, status.Code(err)) _, _, err = host.Sessions().CodeStats(context.Background(), SessionFilter{}, Page{}) require.Error(t, err) require.Equal(t, codes.Unimplemented, status.Code(err)) _, err = host.Tasks().Create(context.Background(), CreateTaskInput{Title: "x"}) require.Error(t, err) require.Equal(t, codes.Unimplemented, status.Code(err)) _, err = host.Tasks().Update(context.Background(), UpdateTaskInput{ID: "task-1"}) require.Error(t, err) require.Equal(t, codes.Unimplemented, status.Code(err)) _, err = host.Messages().Send(context.Background(), "task-1", "", "body") require.Error(t, err) require.Equal(t, codes.Unimplemented, status.Code(err)) }