You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

28 KiB

Know Foolery - Development Guidelines & Coding Standards

Overview

This document establishes coding standards, development practices, and guidelines for the Know Foolery project to ensure consistency, maintainability, and quality across the codebase.

Code Quality Standards

General Code Quality Orientations

  • Type Safety: TypeScript for frontend, Go's type system for backend
  • Code Review: Mandatory peer review for all changes
  • Static Analysis: Automated code quality checks
  • Documentation: Comprehensive API and component documentation

Linting and Formatting

# .golangci.yml
run:
  timeout: 5m
  tests: true

linters:
  enable:
    - gofmt
    - goimports
    - govet
    - errcheck
    - staticcheck
    - unused
    - gosimple
    - structcheck
    - varcheck
    - ineffassign
    - deadcode
    - typecheck
    - gosec
    - misspell
    - lll
    - unconvert
    - dupl
    - goconst
    - gocyclo

linters-settings:
  lll:
    line-length: 120
  gocyclo:
    min-complexity: 15
  dupl:
    threshold: 150

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - gocyclo
        - errcheck
        - dupl
        - gosec
// .eslintrc.json
{
  "extends": [
    "plugin:solid/typescript",
    "@typescript-eslint/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "solid"],
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/prefer-const": "error",
    "solid/reactivity": "warn",
    "prefer-const": "error",
    "no-var": "error",
    "object-shorthand": "error",
    "prefer-template": "error"
  }
}

Naming Conventions

Go Code

// Files: snake_case
user_service.go
jwt_middleware.go
database_client.go

// Packages: lowercase, single word when possible
package auth
package database
package observability

// Exported types: PascalCase
type GameService struct {}
type AuthClaims struct {}
type DatabaseConfig struct {}

// Unexported types: camelCase
type userRepository struct {}
type tokenValidator struct {}

// Functions: PascalCase (exported), camelCase (unexported)
func NewGameService() *GameService {}
func (s *GameService) StartGame() error {}
func validateToken() error {}

// Constants: PascalCase or SCREAMING_SNAKE_CASE for groups
const DefaultTimeout = 30 * time.Second
const (
    StatusActive   = "active"
    StatusInactive = "inactive"
)

// Interfaces: noun or adjective + "er" suffix
type TokenValidator interface {}
type UserRepository interface {}
type ScoreCalculator interface {}

TypeScript Code

// Files: kebab-case
game-card.tsx
user-service.ts
auth-store.ts

// Components: PascalCase
export const GameCard: Component<GameCardProps> = () => {}
export const LeaderboardList: Component = () => {}

// Composables/stores: camelCase with "create"/"use" prefix
export const createGameSessionStore = () => {}
export const useAuthentication = () => {}

// Functions: camelCase
export const calculateScore = () => {}
export const validateAnswer = () => {}

// Types/Interfaces: PascalCase
interface GameSessionProps {}
type AuthenticationState = 'loading' | 'authenticated' | 'unauthenticated'

// Constants: SCREAMING_SNAKE_CASE
export const MAX_ATTEMPTS = 3
export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes

Go Development Standards

Fiber Framework Patterns

Service Structure

// Service initialization with Fiber
package main

import (
    "log"
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/cors"
    "github.com/gofiber/fiber/v3/middleware/logger"
    "github.com/gofiber/fiber/v3/middleware/recover"
)

func main() {
    // Create Fiber app with configuration
    app := fiber.New(fiber.Config{
        AppName:      "Know Foolery Game Service",
        ServerHeader: "Fiber",
        ErrorHandler: customErrorHandler,
        ReadTimeout:  time.Second * 10,
        WriteTimeout: time.Second * 10,
        IdleTimeout:  time.Second * 120,
    })

    // Global middleware
    app.Use(logger.New())
    app.Use(recover.New())
    app.Use(cors.New(cors.Config{
        AllowOrigins: "http://localhost:3000, https://app.knowfoolery.com",
        AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
        AllowHeaders: "Origin, Content-Type, Authorization",
    }))

    // Health check endpoint
    app.Get("/health", healthCheck)

    // API routes with versioning
    api := app.Group("/api/v1")
    setupGameRoutes(api)

    // Start server
    log.Fatal(app.Listen(":8080"))
}

func customErrorHandler(c *fiber.Ctx, err error) error {
    code := fiber.StatusInternalServerError
    message := "Internal Server Error"

    if e, ok := err.(*fiber.Error); ok {
        code = e.Code
        message = e.Message
    }

    return c.Status(code).JSON(fiber.Map{
        "error":   true,
        "message": message,
        "code":    code,
    })
}

Handler Patterns

// Handler structure and patterns
package handlers

import (
    "github.com/gofiber/fiber/v3"
    "knowfoolery/internal/services"
)

type GameHandler struct {
    gameService *services.GameService
    validator   *validator.Validate
}

func NewGameHandler(gameService *services.GameService) *GameHandler {
    return &GameHandler{
        gameService: gameService,
        validator:   validator.New(),
    }
}

// HTTP handler with proper error handling
func (h *GameHandler) StartGame(c *fiber.Ctx) error {
    var req StartGameRequest
    
    // Parse and validate request
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   true,
            "message": "Invalid request body",
        })
    }
    
    if err := h.validator.Struct(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   true,
            "message": "Validation failed",
            "details": err.Error(),
        })
    }
    
    // Extract user context from JWT middleware
    userID := c.Locals("user_id").(string)
    userRole := c.Locals("user_role").(string)
    
    // Business logic
    gameSession, err := h.gameService.StartGame(c.Context(), userID, req.PlayerName)
    if err != nil {
        return handleServiceError(c, err)
    }
    
    return c.Status(fiber.StatusCreated).JSON(fiber.Map{
        "success": true,
        "data":    gameSession,
    })
}

// Centralized error handling
func handleServiceError(c *fiber.Ctx, err error) error {
    switch {
    case errors.Is(err, services.ErrInvalidInput):
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   true,
            "message": "Invalid input",
            "details": err.Error(),
        })
    case errors.Is(err, services.ErrNotFound):
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error":   true,
            "message": "Resource not found",
        })
    case errors.Is(err, services.ErrUnauthorized):
        return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
            "error":   true,
            "message": "Unauthorized access",
        })
    default:
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error":   true,
            "message": "Internal server error",
        })
    }
}

// Request/Response structures with validation
type StartGameRequest struct {
    PlayerName string `json:"player_name" validate:"required,min=2,max=50,alphanum_space"`
}

type StartGameResponse struct {
    SessionID     string    `json:"session_id"`
    PlayerName    string    `json:"player_name"`
    StartTime     time.Time `json:"start_time"`
    TimeRemaining int       `json:"time_remaining_seconds"`
}

Middleware Patterns

// Custom middleware for authentication
package middleware

import (
    "github.com/gofiber/fiber/v3"
    "knowfoolery/shared/auth"
)

func JWTMiddleware(zitadelRepo auth.ZitadelRepository) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // Extract token from Authorization header
        authHeader := c.Get("Authorization")
        if authHeader == "" {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error":   true,
                "message": "Authorization header required",
            })
        }
        
        token := strings.TrimPrefix(authHeader, "Bearer ")
        if token == authHeader {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error":   true,
                "message": "Invalid authorization header format",
            })
        }
        
        // Validate token
        claims, err := zitadelRepo.ValidateToken(c.Context(), token)
        if err != nil {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error":   true,
                "message": "Invalid token",
            })
        }
        
        // Set user context
        c.Locals("user_id", claims.Subject)
        c.Locals("user_email", claims.Email)
        c.Locals("user_name", claims.Name)
        c.Locals("user_roles", claims.Roles)
        
        return c.Next()
    }
}

// Rate limiting middleware
func RateLimitMiddleware(redisClient *redis.Client) fiber.Handler {
    return func(c *fiber.Ctx) error {
        userID := c.Locals("user_id")
        clientIP := c.IP()
        
        // Per-user rate limiting
        if userID != nil {
            userKey := fmt.Sprintf("rate_limit:user:%s", userID)
            if !allowRequest(redisClient, userKey, 60, time.Minute) {
                return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                    "error":   true,
                    "message": "Rate limit exceeded for user",
                })
            }
        }
        
        // Per-IP rate limiting
        ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP)
        if !allowRequest(redisClient, ipKey, 100, time.Minute) {
            return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                "error":   true,
                "message": "Rate limit exceeded for IP",
            })
        }
        
        return c.Next()
    }
}

Database Patterns with Ent

Schema Definition Standards

// Ent schema with proper field validation and indexes
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
    "entgo.io/ent/schema/index"
    "time"
)

type Question struct {
    ent.Schema
}

func (Question) Fields() []ent.Field {
    return []ent.Field{
        field.String("theme").
            NotEmpty().
            MaxLen(100).
            Comment("Question category/theme"),
        field.Text("text").
            NotEmpty().
            MaxLen(1000).
            Comment("The actual question text"),
        field.String("answer").
            NotEmpty().
            MaxLen(500).
            Comment("Correct answer"),
        field.Text("hint").
            Optional().
            MaxLen(500).
            Comment("Optional hint text"),
        field.Enum("difficulty").
            Values("easy", "medium", "hard").
            Default("medium").
            Comment("Question difficulty level"),
        field.Bool("is_active").
            Default(true).
            Comment("Whether question is active"),
        field.Time("created_at").
            Default(time.Now).
            Immutable().
            Comment("Creation timestamp"),
        field.Time("updated_at").
            Default(time.Now).
            UpdateDefault(time.Now).
            Comment("Last update timestamp"),
    }
}

func (Question) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("theme", "is_active"),
        index.Fields("difficulty", "is_active"),
        index.Fields("created_at"),
    }
}

func (Question) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("attempts", QuestionAttempt.Type),
    }
}

Service Layer Patterns

// Service layer with proper error handling
package services

import (
    "context"
    "errors"
    "knowfoolery/internal/models"
    "knowfoolery/ent"
)

var (
    ErrInvalidInput    = errors.New("invalid input")
    ErrNotFound        = errors.New("resource not found")
    ErrUnauthorized    = errors.New("unauthorized access")
    ErrGameInProgress  = errors.New("game already in progress")
    ErrSessionExpired  = errors.New("session expired")
)

type GameService struct {
    client       *ent.Client
    questionSvc  QuestionService
    sessionSvc   SessionService
    validator    *validator.Validate
}

func NewGameService(
    client *ent.Client,
    questionSvc QuestionService,
    sessionSvc SessionService,
) *GameService {
    return &GameService{
        client:      client,
        questionSvc: questionSvc,
        sessionSvc:  sessionSvc,
        validator:   validator.New(),
    }
}

func (s *GameService) StartGame(ctx context.Context, userID, playerName string) (*models.GameSession, error) {
    // Validate input
    if err := s.validatePlayerName(playerName); err != nil {
        return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
    }
    
    // Check for existing active session
    activeSession, err := s.client.GameSession.
        Query().
        Where(gamesession.And(
            gamesession.PlayerNameEQ(playerName),
            gamesession.StatusEQ(gamesession.StatusActive),
        )).
        Only(ctx)
    
    if err != nil && !ent.IsNotFound(err) {
        return nil, fmt.Errorf("failed to check existing session: %w", err)
    }
    
    if activeSession != nil {
        return nil, ErrGameInProgress
    }
    
    // Start database transaction
    tx, err := s.client.Tx(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to start transaction: %w", err)
    }
    defer tx.Rollback()
    
    // Create new game session
    session, err := tx.GameSession.
        Create().
        SetPlayerName(playerName).
        SetTotalScore(0).
        SetQuestionsAsked(0).
        SetQuestionsCorrect(0).
        SetHintsUsed(0).
        SetStartTime(time.Now()).
        SetStatus(gamesession.StatusActive).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to create session: %w", err)
    }
    
    // Get first question
    question, err := s.questionSvc.GetRandomQuestion(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to get question: %w", err)
    }
    
    // Update session with first question
    session, err = session.Update().
        SetCurrentQuestionID(question.ID).
        SetCurrentAttempts(0).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to update session: %w", err)
    }
    
    // Commit transaction
    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("failed to commit transaction: %w", err)
    }
    
    return &models.GameSession{
        ID:            session.ID,
        PlayerName:    session.PlayerName,
        TotalScore:    session.TotalScore,
        StartTime:     session.StartTime,
        Status:        string(session.Status),
        CurrentQuestion: &models.Question{
            ID:     question.ID,
            Theme:  question.Theme,
            Text:   question.Text,
            Hint:   question.Hint,
        },
        TimeRemaining: s.calculateTimeRemaining(session.StartTime),
    }, nil
}

func (s *GameService) validatePlayerName(name string) error {
    type PlayerNameValidation struct {
        Name string `validate:"required,min=2,max=50,alphanum_space"`
    }
    
    return s.validator.Struct(&PlayerNameValidation{Name: name})
}

func (s *GameService) calculateTimeRemaining(startTime time.Time) int {
    const sessionDuration = 30 * time.Minute
    elapsed := time.Since(startTime)
    remaining := sessionDuration - elapsed
    
    if remaining < 0 {
        return 0
    }
    
    return int(remaining.Seconds())
}

TypeScript Development Standards

SolidJS Component Patterns

Component Structure

// Component with proper TypeScript types and SolidJS patterns
import type { Component } from 'solid-js'
import { createMemo, createSignal, Show } from 'solid-js'
import {
  Box,
  Button,
  Input,
  LinearProgress,
  Typography,
  Chip,
} from '@suid/material'

// Props interface with proper documentation
export interface GameCardProps {
  /** The question to display */
  question: string
  /** Theme/category of the question */
  theme: string
  /** Time remaining in seconds */
  timeRemaining: number
  /** Number of attempts left */
  attemptsLeft: number
  /** Callback when answer is submitted */
  onSubmitAnswer: (answer: string) => Promise<void>
  /** Loading state from async operations */
  loading?: boolean
}

/**
 * GameCard component displays a quiz question with input and controls
 *
 * @param props - GameCard props
 * @returns SolidJS component
 */
export const GameCard: Component<GameCardProps> = (props) => {
  const [answer, setAnswer] = createSignal('')
  const [isSubmitting, setIsSubmitting] = createSignal(false)

  const disabled = createMemo(() => props.loading || isSubmitting())
  const progressValue = createMemo(() => ((30 * 60 - props.timeRemaining) / (30 * 60)) * 100)

  const handleSubmit = async () => {
    const value = answer().trim()
    if (!value || disabled()) return

    setIsSubmitting(true)
    try {
      await props.onSubmitAnswer(value)
      setAnswer('')
    } catch (error) {
      console.error('Failed to submit answer:', error)
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <Box sx={{ p: 3, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}>
      <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
        <Chip label={props.theme} />
        <Typography variant="body2">{props.timeRemaining}s</Typography>
      </Box>
      <LinearProgress variant="determinate" value={progressValue()} />
      <Typography variant="h6" sx={{ mt: 2 }}>{props.question}</Typography>
      <Typography variant="body2" sx={{ mt: 1 }}>Attempts left: {props.attemptsLeft}/3</Typography>
      <Input
        value={answer()}
        onInput={(e) => setAnswer(e.currentTarget.value)}
        disabled={disabled()}
        placeholder="Enter your answer"
      />
      <Button onClick={handleSubmit} disabled={disabled() || answer().trim().length === 0}>
        Submit Answer
      </Button>
      <Show when={disabled()}>
        <Typography variant="caption">Submitting...</Typography>
      </Show>
    </Box>
  )
}

Solid Composable Pattern

import { createSignal } from 'solid-js'
import { GameSessionService } from '../services/game-session-service'

export const createGameSessionStore = (playerName: () => string) => {
  const service = new GameSessionService()
  const [isLoading, setIsLoading] = createSignal(false)
  const [error, setError] = createSignal<string | null>(null)
  const [sessionId, setSessionId] = createSignal<string | null>(null)

  const startGame = async () => {
    setIsLoading(true)
    setError(null)
    try {
      const session = await service.startGame(playerName())
      setSessionId(session.sessionId)
      return session
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to start game'
      setError(message)
      throw err
    } finally {
      setIsLoading(false)
    }
  }

  return {
    isLoading,
    error,
    sessionId,
    startGame,
  }
}

Testing Standards

Go Testing Patterns

// Comprehensive testing with testify and testcontainers
package services_test

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/suite"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    
    "knowfoolery/internal/services"
    "knowfoolery/ent/enttest"
)

type GameServiceTestSuite struct {
    suite.Suite
    ctx         context.Context
    gameService *services.GameService
    client      *ent.Client
}

func (suite *GameServiceTestSuite) SetupSuite() {
    suite.ctx = context.Background()
    
    // Start PostgreSQL container for integration tests
    postgresContainer, err := postgres.RunContainer(suite.ctx,
        testcontainers.WithImage("postgres:15-alpine"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
    )
    require.NoError(suite.T(), err)
    
    connStr, err := postgresContainer.ConnectionString(suite.ctx)
    require.NoError(suite.T(), err)
    
    // Create Ent client with test database
    suite.client = enttest.Open(suite.T(), "postgres", connStr)
    
    // Initialize service
    suite.gameService = services.NewGameService(
        suite.client,
        services.NewQuestionService(suite.client),
        services.NewSessionService(suite.client),
    )
}

func (suite *GameServiceTestSuite) TearDownSuite() {
    suite.client.Close()
}

func (suite *GameServiceTestSuite) SetupTest() {
    // Clean database before each test
    suite.client.GameSession.Delete().ExecX(suite.ctx)
    suite.client.Question.Delete().ExecX(suite.ctx)
    
    // Seed test data
    suite.seedTestQuestions()
}

func (suite *GameServiceTestSuite) TestStartGame_Success() {
    // Arrange
    playerName := "TestPlayer"
    
    // Act
    session, err := suite.gameService.StartGame(suite.ctx, "user123", playerName)
    
    // Assert
    require.NoError(suite.T(), err)
    assert.NotEmpty(suite.T(), session.ID)
    assert.Equal(suite.T(), playerName, session.PlayerName)
    assert.Equal(suite.T(), 0, session.TotalScore)
    assert.NotNil(suite.T(), session.CurrentQuestion)
    assert.True(suite.T(), session.TimeRemaining > 0)
}

func (suite *GameServiceTestSuite) TestStartGame_InvalidPlayerName() {
    // Test cases for validation
    testCases := []struct {
        name        string
        playerName  string
        expectedErr string
    }{
        {"empty name", "", "required"},
        {"too short", "a", "min"},
        {"too long", strings.Repeat("a", 51), "max"},
        {"invalid chars", "player@123", "alphanum_space"},
    }
    
    for _, tc := range testCases {
        suite.T().Run(tc.name, func(t *testing.T) {
            _, err := suite.gameService.StartGame(suite.ctx, "user123", tc.playerName)
            require.Error(t, err)
            assert.Contains(t, err.Error(), tc.expectedErr)
        })
    }
}

func (suite *GameServiceTestSuite) TestStartGame_ExistingActiveSession() {
    // Arrange
    playerName := "TestPlayer"
    
    // Start first game
    _, err := suite.gameService.StartGame(suite.ctx, "user123", playerName)
    require.NoError(suite.T(), err)
    
    // Act - try to start second game
    _, err = suite.gameService.StartGame(suite.ctx, "user123", playerName)
    
    // Assert
    require.Error(suite.T(), err)
    assert.Equal(suite.T(), services.ErrGameInProgress, err)
}

func (suite *GameServiceTestSuite) seedTestQuestions() {
    questions := []struct {
        theme  string
        text   string
        answer string
        hint   string
    }{
        {"Geography", "What is the capital of France?", "Paris", "City of Light"},
        {"History", "Who painted the Mona Lisa?", "Leonardo da Vinci", "Renaissance artist"},
        {"Science", "What is the largest planet?", "Jupiter", "Gas giant"},
    }
    
    for _, q := range questions {
        suite.client.Question.Create().
            SetTheme(q.theme).
            SetText(q.text).
            SetAnswer(q.answer).
            SetHint(q.hint).
            SetIsActive(true).
            ExecX(suite.ctx)
    }
}

func TestGameServiceSuite(t *testing.T) {
    suite.Run(t, new(GameServiceTestSuite))
}

// Benchmark tests
func BenchmarkGameService_StartGame(b *testing.B) {
    // Setup
    ctx := context.Background()
    client := enttest.Open(b, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    defer client.Close()
    
    gameService := services.NewGameService(client, nil, nil)
    
    // Benchmark
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        playerName := fmt.Sprintf("Player%d", i)
        _, _ = gameService.StartGame(ctx, fmt.Sprintf("user%d", i), playerName)
    }
}

TypeScript Testing Patterns

// SolidJS component testing with Vitest and Testing Library
import { render, screen, fireEvent } from '@solidjs/testing-library'
import { describe, expect, it, vi } from 'vitest'
import { GameCard, GameCardProps } from '../GameCard'

// Mock props factory
const createMockProps = (overrides: Partial<GameCardProps> = {}): GameCardProps => ({
  question: "What is the capital of France?",
  theme: "Geography",
  timeRemaining: 1800, // 30 minutes
  attemptsLeft: 3,
  onSubmitAnswer: vi.fn(),
  loading: false,
  ...overrides,
})

describe('GameCard', () => {
  describe('Rendering', () => {
    it('should render question and theme correctly', () => {
      const props = createMockProps()

      render(() => <GameCard {...props} />)

      expect(screen.getByText(props.question)).toBeInTheDocument()
      expect(screen.getByText(props.theme)).toBeInTheDocument()
    })
  })

  describe('User Interactions', () => {
    it('should call onSubmitAnswer when submit button is clicked', async () => {
      const props = createMockProps()
      const mockOnSubmitAnswer = vi.fn().mockResolvedValue(undefined)
      props.onSubmitAnswer = mockOnSubmitAnswer

      render(() => <GameCard {...props} />)

      const input = screen.getByPlaceholderText('Enter your answer')
      fireEvent.input(input, { target: { value: 'Paris' } })

      const submitButton = screen.getByRole('button', { name: 'Submit Answer' })
      fireEvent.click(submitButton)

      expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris')
    })
  })
})

Git Workflow

# Branch naming conventions
feature/add-game-session-management
bugfix/fix-timer-display-issue
hotfix/security-vulnerability-patch
refactor/improve-database-queries

# Commit message format
type(scope): description

# Types: feat, fix, docs, style, refactor, test, chore
# Examples:
feat(game): add session timeout handling
fix(auth): resolve JWT token validation issue
docs(api): update authentication endpoint documentation
test(services): add comprehensive game service tests

Development Commands

Backend Development

# Navigate to service directory first
cd backend/services/{service-name}

# Start service in development mode with hot reload
go run cmd/main.go

# Service ports are env-driven via shared bootstrap helpers:
# ADMIN_SERVICE_PORT, GAME_SESSION_PORT, GATEWAY_PORT,
# LEADERBOARD_PORT, QUESTION_BANK_PORT, USER_SERVICE_PORT

# Run tests for a specific service
go test ./... -v

# Run tests with coverage
go test ./... -cover

# Build service binary
go build -o bin/{service-name} cmd/main.go

# Generate Ent schemas (from service root)
go generate ./...

# Lint Go code
golangci-lint run

# Format Go code  
go fmt ./...
goimports -w .

# Update go.mod of all services
find . -type f -name "*.mod" -printf "%h\0" | sort -zu | xargs -0 -I{} sh -c 'cd "{}" && go mod tidy'

# Run all backend tests
go test  $(go list -f '{{.Dir}}/...' -m | xargs)

# Go vet all backend code
go vet  $(go list -f '{{.Dir}}/...' -m | xargs)

# Lint all backend code
golangci-lint run  $(go list -f '{{.Dir}}/...' -m | xargs)

Development Environment

# Start development stack with Docker Compose
cd infrastructure/dev
docker-compose up -d

# This starts:
# - PostgreSQL database (port 5432)
# - Redis cache (port 6379) 
# - Zitadel auth server (port 8080)

Frontend Development

Note: This project uses Yarn with nodeLinker: node-modules. PnP files like .pnp.cjs and .pnp.loader.mjs are not used. Status: Web commands are implemented. Tauri cross-platform commands are planned and require a future frontend/apps/cross-platform workspace.

# Web application
cd frontend
yarn install
yarn dev             # Start web development server (SolidJS)
yarn build           # Build all frontend workspaces
yarn test            # Run Vitest suites
yarn lint            # ESLint
yarn format:check    # Prettier check

# Cross-platform packaging with Tauri (desktop/mobile)
cd frontend/apps/cross-platform
yarn tauri dev       # Run Tauri app with embedded web UI
yarn tauri build     # Build release bundles