mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
174 lines
6.2 KiB
Go
174 lines
6.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestAgentBuilderInstructionsConstrainModelsToRuntimeCatalog(t *testing.T) {
|
|
for _, requirement := range []string{
|
|
"AVAILABLE RUNTIME MODELS",
|
|
"Never use a model label as the id",
|
|
"never invent a model id",
|
|
} {
|
|
if !strings.Contains(agentBuilderInstructions, requirement) {
|
|
t.Fatalf("agent builder instructions missing model constraint %q", requirement)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateAgentBuilderSessionCreatesIsolatedHiddenBuilder(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
withAgentBuilderFlag(t, testHandler, true)
|
|
t.Cleanup(func() {
|
|
_, _ = testPool.Exec(context.Background(), `
|
|
DELETE FROM agent
|
|
WHERE workspace_id = $1 AND kind = 'system' AND system_key LIKE 'agent_builder:%'
|
|
`, testWorkspaceID)
|
|
})
|
|
|
|
create := func(model string) CreateAgentBuilderSessionResponse {
|
|
w := httptest.NewRecorder()
|
|
testHandler.CreateAgentBuilderSession(w, newRequest(http.MethodPost, "/api/agent-builder/sessions", map[string]any{
|
|
"runtime_id": testRuntimeID,
|
|
"model": model,
|
|
}))
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("CreateAgentBuilderSession: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var response CreateAgentBuilderSessionResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if response.SessionID == "" || response.BuilderAgentID == "" {
|
|
t.Fatalf("missing builder identifiers: %+v", response)
|
|
}
|
|
return response
|
|
}
|
|
|
|
first := create("builder-model-a")
|
|
second := create("builder-model-b")
|
|
if first.BuilderAgentID == second.BuilderAgentID {
|
|
t.Fatalf("builder sessions unexpectedly shared an agent: %s", first.BuilderAgentID)
|
|
}
|
|
if first.SessionID == second.SessionID {
|
|
t.Fatalf("each creation flow must receive a fresh chat session")
|
|
}
|
|
|
|
var kind, systemKey, firstModel string
|
|
if err := testPool.QueryRow(context.Background(), `
|
|
SELECT kind, system_key, model FROM agent WHERE id = $1
|
|
`, first.BuilderAgentID).Scan(&kind, &systemKey, &firstModel); err != nil {
|
|
t.Fatalf("load builder agent: %v", err)
|
|
}
|
|
if kind != "system" || !strings.HasPrefix(systemKey, "agent_builder:") {
|
|
t.Fatalf("unexpected builder identity kind=%q system_key=%q", kind, systemKey)
|
|
}
|
|
if firstModel != "builder-model-a" {
|
|
t.Fatalf("first builder model was mutated: got %q", firstModel)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
testHandler.ListAgents(w, newRequest(http.MethodGet, "/api/agents", nil))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("ListAgents: %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var listed []AgentResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &listed); err != nil {
|
|
t.Fatalf("decode agent list: %v", err)
|
|
}
|
|
for _, agent := range listed {
|
|
if agent.ID == first.BuilderAgentID {
|
|
t.Fatalf("system builder leaked into the user-facing agent list")
|
|
}
|
|
}
|
|
|
|
// Knowing the ID must not expose system infrastructure through the public
|
|
// Agent detail/update/archive loaders.
|
|
w = httptest.NewRecorder()
|
|
req := withURLParams(newRequest(http.MethodGet, "/api/agents/"+first.BuilderAgentID, nil), "id", first.BuilderAgentID)
|
|
testHandler.GetAgent(w, req)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("GetAgent(system): expected 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Deleting the private Builder chat also removes its session-scoped hidden
|
|
// Agent, so completed/cancelled flows do not accumulate infrastructure rows.
|
|
w = httptest.NewRecorder()
|
|
req = withURLParams(newRequest(http.MethodDelete, "/api/chat/sessions/"+first.SessionID, nil), "sessionId", first.SessionID)
|
|
req = withChatTestWorkspaceCtx(t, req)
|
|
testHandler.DeleteChatSession(w, req)
|
|
if w.Code != http.StatusNoContent {
|
|
t.Fatalf("DeleteChatSession(builder): expected 204, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var remaining int
|
|
if err := testPool.QueryRow(context.Background(), `SELECT count(*) FROM agent WHERE id = $1`, first.BuilderAgentID).Scan(&remaining); err != nil {
|
|
t.Fatalf("count deleted builder: %v", err)
|
|
}
|
|
if remaining != 0 {
|
|
t.Fatalf("builder agent survived chat deletion")
|
|
}
|
|
}
|
|
|
|
func TestCreateAgentBuilderSessionRequiresReleaseFlag(t *testing.T) {
|
|
withAgentBuilderFlag(t, testHandler, false)
|
|
w := httptest.NewRecorder()
|
|
testHandler.CreateAgentBuilderSession(w, newRequest(http.MethodPost, "/api/agent-builder/sessions", map[string]any{
|
|
"runtime_id": testRuntimeID,
|
|
}))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("CreateAgentBuilderSession without flag: expected 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateAgentAttachesSkillsInCreateTransaction(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
ctx := context.Background()
|
|
var skillID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO skill (workspace_id, name, description, content, config, created_by)
|
|
VALUES ($1, 'Atomic Create Skill', '', '# Atomic', '{}'::jsonb, $2)
|
|
RETURNING id
|
|
`, testWorkspaceID, testUserID).Scan(&skillID); err != nil {
|
|
t.Fatalf("create skill fixture: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = testPool.Exec(context.Background(), `DELETE FROM agent WHERE workspace_id = $1 AND name = 'Atomic Skill Agent'`, testWorkspaceID)
|
|
_, _ = testPool.Exec(context.Background(), `DELETE FROM skill WHERE id = $1`, skillID)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
testHandler.CreateAgent(w, newRequest(http.MethodPost, "/api/agents", map[string]any{
|
|
"name": "Atomic Skill Agent",
|
|
"runtime_id": testRuntimeID,
|
|
"skill_ids": []string{skillID},
|
|
}))
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("CreateAgent: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var response AgentResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(response.Skills) != 1 || response.Skills[0].ID != skillID {
|
|
t.Fatalf("create response did not include attached skill: %+v", response.Skills)
|
|
}
|
|
var introSessions int
|
|
if err := testPool.QueryRow(ctx, `
|
|
SELECT count(*) FROM chat_session WHERE agent_id = $1 AND is_agent_intro = true
|
|
`, response.ID).Scan(&introSessions); err != nil {
|
|
t.Fatalf("count welcome chat sessions: %v", err)
|
|
}
|
|
if introSessions != 1 {
|
|
t.Fatalf("welcome chat sessions = %d, want 1", introSessions)
|
|
}
|
|
}
|