refactor(dev): share postgres across main and worktrees
This commit is contained in:
parent
94c9b07bfb
commit
2c28c4cba2
16 changed files with 839 additions and 359 deletions
|
|
@ -1,221 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
dbURL = "postgres://multica:multica@localhost:5432/multica?sslmode=disable"
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dbURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to connect to database: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Create seed user
|
||||
var userID string
|
||||
err = pool.QueryRow(ctx, `
|
||||
INSERT INTO "user" (name, email, avatar_url)
|
||||
VALUES ('Jiayuan Zhang', 'jiayuan@multica.ai', NULL)
|
||||
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id
|
||||
`).Scan(&userID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
fmt.Printf("User created: %s\n", userID)
|
||||
|
||||
// Create seed workspace
|
||||
var workspaceID string
|
||||
err = pool.QueryRow(ctx, `
|
||||
INSERT INTO workspace (name, slug, description)
|
||||
VALUES ('Multica', 'multica', 'AI-native task management')
|
||||
ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id
|
||||
`).Scan(&workspaceID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create workspace: %v", err)
|
||||
}
|
||||
fmt.Printf("Workspace created: %s\n", workspaceID)
|
||||
|
||||
// Add user as owner
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO member (workspace_id, user_id, role)
|
||||
VALUES ($1, $2, 'owner')
|
||||
ON CONFLICT (workspace_id, user_id) DO NOTHING
|
||||
`, workspaceID, userID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create member: %v", err)
|
||||
}
|
||||
fmt.Println("Member created")
|
||||
|
||||
// Create some agents
|
||||
agents := []struct {
|
||||
name string
|
||||
description string
|
||||
runtimeMode string
|
||||
status string
|
||||
skills string
|
||||
tools string
|
||||
triggers string
|
||||
}{
|
||||
{
|
||||
"Deep Research Agent",
|
||||
"Performs deep research on topics using web search and analysis",
|
||||
"local", "idle",
|
||||
"# Deep Research Agent\n\nYou are a research agent that performs thorough analysis on assigned topics.\n\n## Workflow\n1. Break down the research question into sub-questions\n2. Use web search to gather information from multiple sources\n3. Cross-reference and validate findings\n4. Synthesize a comprehensive report\n5. Post the report as a comment on the issue\n\n## Output Format\nAlways produce a structured report with:\n- Executive Summary\n- Key Findings\n- Sources\n- Recommendations",
|
||||
`[{"id":"tool-1","name":"Google Search","description":"Search the web for information","auth_type":"api_key","connected":true,"config":{}},{"id":"tool-2","name":"Web Scraper","description":"Extract content from web pages","auth_type":"none","connected":true,"config":{}}]`,
|
||||
`[{"id":"trigger-1","type":"on_assign","enabled":true,"config":{}}]`,
|
||||
},
|
||||
{
|
||||
"Code Review Bot",
|
||||
"Reviews pull requests and provides feedback on code quality",
|
||||
"cloud", "idle",
|
||||
"# Code Review Bot\n\nYou review code changes and provide constructive feedback.\n\n## Review Criteria\n- Code correctness and logic\n- Performance implications\n- Security vulnerabilities\n- Code style and readability\n- Test coverage\n\n## Process\n1. Read the issue description for context\n2. Analyze code changes\n3. Post review comments on specific lines\n4. Provide an overall summary",
|
||||
`[{"id":"tool-3","name":"GitHub","description":"Access GitHub repositories and PRs","auth_type":"oauth","connected":true,"config":{}}]`,
|
||||
`[{"id":"trigger-2","type":"on_assign","enabled":true,"config":{}}]`,
|
||||
},
|
||||
{
|
||||
"Daily Standup Bot",
|
||||
"Generates daily standup summaries from recent activity",
|
||||
"cloud", "working",
|
||||
"# Daily Standup Bot\n\nGenerate a daily standup summary based on workspace activity.\n\n## Tasks\n1. Collect all issue status changes from the last 24 hours\n2. Summarize what each team member worked on\n3. Identify blocked items\n4. Post the summary to the team channel",
|
||||
`[{"id":"tool-4","name":"Slack","description":"Send messages to Slack channels","auth_type":"oauth","connected":true,"config":{"channel":"#standup"}}]`,
|
||||
`[{"id":"trigger-3","type":"scheduled","enabled":true,"config":{"cron":"0 9 * * 1-5","timezone":"Asia/Shanghai"}}]`,
|
||||
},
|
||||
{
|
||||
"Local Dev Agent",
|
||||
"A local development agent running on your machine",
|
||||
"local", "offline",
|
||||
"",
|
||||
`[]`,
|
||||
`[{"id":"trigger-4","type":"on_assign","enabled":true,"config":{}}]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range agents {
|
||||
var agentID string
|
||||
// Check if agent already exists
|
||||
err = pool.QueryRow(ctx, `
|
||||
SELECT id FROM agent WHERE workspace_id = $1 AND name = $2
|
||||
`, workspaceID, a.name).Scan(&agentID)
|
||||
if err == nil {
|
||||
fmt.Printf("Agent exists: %s (%s)\n", a.name, agentID)
|
||||
continue
|
||||
}
|
||||
err = pool.QueryRow(ctx, `
|
||||
INSERT INTO agent_runtime (workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at)
|
||||
VALUES (
|
||||
$1,
|
||||
NULL,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
'{"seed":true}'::jsonb,
|
||||
CASE WHEN $5 = 'online' THEN now() ELSE NULL END
|
||||
)
|
||||
RETURNING id
|
||||
`,
|
||||
workspaceID,
|
||||
a.name+" Runtime",
|
||||
a.runtimeMode,
|
||||
map[string]string{"cloud": "multica_agent", "local": "seed_local"}[a.runtimeMode],
|
||||
map[bool]string{true: "offline", false: "online"}[a.status == "offline"],
|
||||
map[string]string{"cloud": "Seeded cloud runtime", "local": "Seeded local runtime"}[a.runtimeMode],
|
||||
).Scan(&agentID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create runtime for agent %s: %v", a.name, err)
|
||||
continue
|
||||
}
|
||||
runtimeID := agentID
|
||||
err = pool.QueryRow(ctx, `
|
||||
INSERT INTO agent (workspace_id, name, description, runtime_mode, runtime_id, status, owner_id, skills, tools, triggers)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb)
|
||||
RETURNING id
|
||||
`, workspaceID, a.name, a.description, a.runtimeMode, runtimeID, a.status, userID, a.skills, a.tools, a.triggers).Scan(&agentID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create agent %s: %v", a.name, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Agent created: %s (%s)\n", a.name, agentID)
|
||||
}
|
||||
|
||||
// Create seed issues
|
||||
issues := []struct {
|
||||
title string
|
||||
description string
|
||||
status string
|
||||
priority string
|
||||
position float64
|
||||
}{
|
||||
{"Add multi-workspace support", "Users should be able to create and switch between multiple workspaces.", "backlog", "medium", 1},
|
||||
{"Agent long-term memory persistence", "Agents need persistent memory across sessions for better context.", "backlog", "low", 2},
|
||||
{"Design the agent config UI", "Create a configuration interface for agent settings and capabilities.", "todo", "high", 3},
|
||||
{"Implement issue list API endpoint", "Build the REST API for listing, filtering, and paginating issues.", "in_progress", "urgent", 4},
|
||||
{"Implement OAuth login flow", "Set up OAuth 2.0 with Google for user authentication.", "in_progress", "high", 5},
|
||||
{"Add WebSocket reconnection logic", "Handle disconnections gracefully with exponential backoff.", "in_review", "medium", 6},
|
||||
{"Set up CI/CD pipeline", "Configure GitHub Actions for automated testing and deployment.", "done", "high", 7},
|
||||
{"Design database schema", "Create the initial PostgreSQL schema for all entities.", "done", "urgent", 8},
|
||||
{"Implement real-time notifications", "Push notifications to users via WebSocket when issues change.", "todo", "medium", 9},
|
||||
{"Agent task queue management", "Build the task dispatching and queue management system for agents.", "todo", "high", 10},
|
||||
}
|
||||
|
||||
for _, iss := range issues {
|
||||
var issueID string
|
||||
// Check if issue already exists
|
||||
err = pool.QueryRow(ctx, `
|
||||
SELECT id FROM issue WHERE workspace_id = $1 AND title = $2
|
||||
`, workspaceID, iss.title).Scan(&issueID)
|
||||
if err == nil {
|
||||
fmt.Printf("Issue exists: %s (%s)\n", iss.title, issueID)
|
||||
continue
|
||||
}
|
||||
err = pool.QueryRow(ctx, `
|
||||
INSERT INTO issue (workspace_id, title, description, status, priority, creator_type, creator_id, position)
|
||||
VALUES ($1, $2, $3, $4, $5, 'member', $6, $7)
|
||||
RETURNING id
|
||||
`, workspaceID, iss.title, iss.description, iss.status, iss.priority, userID, iss.position).Scan(&issueID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create issue %s: %v", iss.title, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Issue created: %s (%s)\n", iss.title, issueID)
|
||||
}
|
||||
|
||||
// Create seed comment (only if not already present)
|
||||
var commentExists bool
|
||||
_ = pool.QueryRow(ctx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM comment c
|
||||
JOIN issue i ON c.issue_id = i.id
|
||||
WHERE i.workspace_id = $1 AND i.title = 'Implement issue list API endpoint'
|
||||
AND c.content = 'This is a high priority item for Q2.'
|
||||
)
|
||||
`, workspaceID).Scan(&commentExists)
|
||||
if !commentExists {
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO comment (issue_id, author_type, author_id, content, type)
|
||||
SELECT i.id, 'member', $2, 'This is a high priority item for Q2.', 'comment'
|
||||
FROM issue i WHERE i.workspace_id = $1 AND i.title = 'Implement issue list API endpoint'
|
||||
`, workspaceID, userID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create comment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nSeed data created successfully!")
|
||||
fmt.Printf("\nUser ID: %s\n", userID)
|
||||
fmt.Printf("Workspace ID: %s\n", workspaceID)
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/multica-ai/multica/server/internal/realtime"
|
||||
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -30,48 +29,48 @@ var (
|
|||
|
||||
var jwtSecret = []byte("multica-dev-secret-change-in-production")
|
||||
|
||||
const (
|
||||
integrationTestEmail = "integration-test@multica.ai"
|
||||
integrationTestName = "Integration Tester"
|
||||
integrationTestWorkspaceSlug = "integration-tests"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := context.Background()
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
dbURL = "postgres://multica:multica@localhost:5432/multica?sslmode=disable"
|
||||
}
|
||||
|
||||
pool, err := pgxpool.New(context.Background(), dbURL)
|
||||
pool, err := pgxpool.New(ctx, dbURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Skipping integration tests: could not connect to database: %v\n", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Get seed data IDs
|
||||
row := pool.QueryRow(context.Background(), `SELECT id FROM "user" WHERE email = 'jiayuan@multica.ai'`)
|
||||
row.Scan(&testUserID)
|
||||
|
||||
row = pool.QueryRow(context.Background(), `SELECT id FROM workspace WHERE slug = 'multica'`)
|
||||
row.Scan(&testWorkspaceID)
|
||||
|
||||
if testUserID == "" || testWorkspaceID == "" {
|
||||
fmt.Println("Skipping integration tests: seed data not found. Run 'go run ./cmd/seed/' first.")
|
||||
os.Exit(0)
|
||||
testUserID, testWorkspaceID, err = setupIntegrationTestFixture(ctx, pool)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to set up integration test fixture: %v\n", err)
|
||||
pool.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
queries := db.New(pool)
|
||||
hub := realtime.NewHub()
|
||||
go hub.Run()
|
||||
|
||||
_ = queries
|
||||
router := NewRouter(pool, hub)
|
||||
testServer = httptest.NewServer(router)
|
||||
defer testServer.Close()
|
||||
|
||||
// Login to get a real JWT token
|
||||
loginBody, _ := json.Marshal(map[string]string{
|
||||
"email": "jiayuan@multica.ai",
|
||||
"name": "Jiayuan Zhang",
|
||||
"email": integrationTestEmail,
|
||||
"name": integrationTestName,
|
||||
})
|
||||
resp, err := http.Post(testServer.URL+"/auth/login", "application/json", bytes.NewReader(loginBody))
|
||||
if err != nil {
|
||||
fmt.Printf("Skipping: login failed: %v\n", err)
|
||||
testServer.Close()
|
||||
pool.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
@ -85,7 +84,81 @@ func TestMain(m *testing.M) {
|
|||
json.NewDecoder(resp.Body).Decode(&loginResp)
|
||||
testToken = loginResp.Token
|
||||
|
||||
os.Exit(m.Run())
|
||||
code := m.Run()
|
||||
|
||||
if err := cleanupIntegrationTestFixture(context.Background(), pool); err != nil {
|
||||
fmt.Printf("Failed to clean up integration test fixture: %v\n", err)
|
||||
if code == 0 {
|
||||
code = 1
|
||||
}
|
||||
}
|
||||
testServer.Close()
|
||||
pool.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setupIntegrationTestFixture(ctx context.Context, pool *pgxpool.Pool) (string, string, error) {
|
||||
if err := cleanupIntegrationTestFixture(ctx, pool); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var userID string
|
||||
if err := pool.QueryRow(ctx, `
|
||||
INSERT INTO "user" (name, email)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id
|
||||
`, integrationTestName, integrationTestEmail).Scan(&userID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var workspaceID string
|
||||
if err := pool.QueryRow(ctx, `
|
||||
INSERT INTO workspace (name, slug, description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`, "Integration Tests", integrationTestWorkspaceSlug, "Temporary workspace for router integration tests").Scan(&workspaceID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO member (workspace_id, user_id, role)
|
||||
VALUES ($1, $2, 'owner')
|
||||
`, workspaceID, userID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var runtimeID string
|
||||
if err := pool.QueryRow(ctx, `
|
||||
INSERT INTO agent_runtime (
|
||||
workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at
|
||||
)
|
||||
VALUES ($1, NULL, $2, 'cloud', $3, 'online', $4, '{}'::jsonb, now())
|
||||
RETURNING id
|
||||
`, workspaceID, "Integration Test Runtime", "integration_test_runtime", "Integration test runtime").Scan(&runtimeID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO agent (
|
||||
workspace_id, name, description, runtime_mode, runtime_config,
|
||||
runtime_id, visibility, max_concurrent_tasks, owner_id, skills, tools, triggers
|
||||
)
|
||||
VALUES ($1, $2, '', 'cloud', '{}'::jsonb, $3, 'workspace', 1, $4, '', '[]'::jsonb, '[]'::jsonb)
|
||||
`, workspaceID, "Integration Test Agent", runtimeID, userID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return userID, workspaceID, nil
|
||||
}
|
||||
|
||||
func cleanupIntegrationTestFixture(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
if _, err := pool.Exec(ctx, `DELETE FROM workspace WHERE slug = $1`, integrationTestWorkspaceSlug); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `DELETE FROM "user" WHERE email = $1`, integrationTestEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper to make authenticated requests
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue