Files
kandev/apps/backend/internal/office/runtime/handler_taskless_annotation_test.go
T

134 lines
4.5 KiB
Go

package runtime
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
settingsstore "github.com/kandev/kandev/internal/agent/settings/store"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/office/agents"
"github.com/kandev/kandev/internal/office/models"
"github.com/kandev/kandev/internal/office/repository/sqlite"
)
// tasklessAnnotationHandlerHarness wires the runtime handler around a
// taskless run (empty task_id claim) with comment/task dependencies, so
// POST /runtime/comments can be exercised over HTTP rather than by calling
// Actions.PostComment directly — the wire path has its own request parsing
// and audit-event logging that the Actions-level tests never reach.
type tasklessAnnotationHandlerHarness struct {
router *gin.Engine
token string
comments *handlerCommentWriter
tasks *handlerTaskCreator
runEvents *recordingRunEvents
}
func newTasklessAnnotationHandlerHarness(t *testing.T, claimWorkspaceID string) *tasklessAnnotationHandlerHarness {
t.Helper()
gin.SetMode(gin.TestMode)
db, err := sqlx.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if _, _, err := settingsstore.Provide(db, db, nil); err != nil {
t.Fatalf("settings store init: %v", err)
}
repo, err := sqlite.NewWithDB(db, db, nil)
if err != nil {
t.Fatalf("new repo: %v", err)
}
agentSvc := agents.NewAgentService(repo, logger.Default(), nil)
agentSvc.SetAuth(agents.NewAgentAuth("taskless-annotation-test-key"))
agent := &models.AgentInstance{
ID: "agent-1",
WorkspaceID: "ws-agent-home",
Name: "Coordinator",
Role: models.AgentRoleCEO,
}
if err := repo.CreateAgentInstance(context.Background(), agent); err != nil {
t.Fatalf("create agent: %v", err)
}
capabilityJSON, err := MarshalCapabilities(Capabilities{CanPostComments: true})
if err != nil {
t.Fatalf("marshal capabilities: %v", err)
}
token, err := agentSvc.MintRuntimeJWT("agent-1", "", claimWorkspaceID, "run-1", "sess-1", capabilityJSON)
if err != nil {
t.Fatalf("mint runtime token: %v", err)
}
comments := &handlerCommentWriter{}
tasks := &handlerTaskCreator{}
runEvents := &recordingRunEvents{}
router := gin.New()
RegisterRoutes(router.Group(""), NewHandler(
agentSvc,
NewActions(ActionDependencies{Comments: comments, Tasks: tasks, Agents: agentSvc}),
nil,
runEvents,
nil,
nil,
nil,
))
return &tasklessAnnotationHandlerHarness{
router: router, token: token, comments: comments, tasks: tasks, runEvents: runEvents,
}
}
func (h *tasklessAnnotationHandlerHarness) postComment(t *testing.T, taskID, body string) *httptest.ResponseRecorder {
t.Helper()
payload, err := json.Marshal(map[string]string{"task_id": taskID, "body": body})
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/runtime/comments", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+h.token)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
h.router.ServeHTTP(resp, req)
return resp
}
func TestRuntimeHandler_TasklessRunAnnotatesWorkspaceTaskOverHTTP(t *testing.T) {
h := newTasklessAnnotationHandlerHarness(t, "ws-1")
resp := h.postComment(t, "task-x", "blocker found")
if resp.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d; body=%s", resp.Code, http.StatusCreated, resp.Body.String())
}
if len(h.comments.comments) != 1 {
t.Fatalf("comments = %d, want 1", len(h.comments.comments))
}
comment := h.comments.comments[0]
if comment.TaskID != "task-x" || comment.AuthorID != "agent-1" || comment.AuthorType != "agent" {
t.Fatalf("comment identity = %#v", comment)
}
assertActionRunEvent(t, h.runEvents, "post_comment", "task", "task-x")
}
func TestRuntimeHandler_PostCommentAuditEventNamesTrimmedTarget(t *testing.T) {
h := newTasklessAnnotationHandlerHarness(t, "ws-1")
resp := h.postComment(t, " task-x ", "blocker found")
if resp.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d; body=%s", resp.Code, http.StatusCreated, resp.Body.String())
}
if len(h.comments.comments) != 1 || h.comments.comments[0].TaskID != "task-x" {
t.Fatalf("comments = %#v, want one comment on trimmed task-x", h.comments.comments)
}
// The audit event must name the task the comment actually landed on,
// not the padded value the caller supplied.
assertActionRunEvent(t, h.runEvents, "post_comment", "task", "task-x")
}