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.

39 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.

Quality Assurance

Testing Strategy

  • Unit Testing: Go testing framework + Jest for frontend
  • Integration Testing: testcontainers for database testing
  • E2E Testing: Playwright for complete user journeys
  • Performance Testing: k6 for load and stress testing
  • Security Testing: Automated security scanning and penetration testing

Code Quality

  • 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

Code Organization

Project Structure Standards

knowfoolery/
├── backend/
│   ├── services/                    # Microservices
│   │   ├── {service-name}/
│   │   │   ├── cmd/
│   │   │   │   └── main.go         # Service entry point
│   │   │   ├── internal/
│   │   │   │   ├── handlers/       # Fiber HTTP handlers
│   │   │   │   ├── services/       # Business logic
│   │   │   │   ├── middleware/     # Service-specific middleware
│   │   │   │   └── models/         # Ent models and schemas
│   │   │   ├── api/                # API definitions (OpenAPI/gRPC)
│   │   │   ├── config/             # Configuration management
│   │   │   ├── tests/              # Service-specific tests
│   │   │   ├── Dockerfile          # Container definition
│   │   │   └── go.mod              # Go module
│   │   └── shared/                 # Shared packages
│   │       ├── auth/               # JWT middleware & Zitadel integration
│   │       ├── database/           # Ent database client
│   │       ├── observability/      # Metrics and tracing
│   │       ├── security/           # Security utilities
│   │       └── utils/              # Common utilities
├── frontend/
│   ├── packages/
│   │   ├── ui-components/          # Shared Gluestack UI components
│   │   ├── shared-logic/           # Business logic
│   │   ├── shared-types/           # TypeScript types
│   │   └── shared-utils/           # Utility functions
│   ├── apps/
│   │   ├── web/                    # React web app
│   │   ├── mobile/                 # React Native app
│   │   └── desktop/                # Wails desktop app
└── infrastructure/                 # DevOps and deployment

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-context.tsx

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

// Hooks: camelCase with "use" prefix
export const useGameSession = () => {}
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

React Component Patterns

Component Structure

// Component with proper TypeScript types and patterns
import React, { useState, useEffect, useCallback } from 'react'
import {
  Card,
  VStack,
  HStack,
  Text,
  Input,
  Button,
  Badge,
  Progress
} from '@gluestack-ui/themed'
import { useGameSession } from '../hooks/useGameSession'
import { useTimer } from '../hooks/useTimer'

// 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
  /** Current player score */
  currentScore: number
  /** Loading state */
  isLoading?: boolean
  /** Callback when answer is submitted */
  onSubmitAnswer: (answer: string) => Promise<void>
  /** Callback when hint is requested */
  onRequestHint: () => Promise<void>
  /** Callback when time expires */
  onTimeExpire?: () => void
}

/**
 * GameCard component displays a quiz question with input and controls
 * 
 * @param props - GameCard props
 * @returns React component
 */
export const GameCard: React.FC<GameCardProps> = ({
  question,
  theme,
  timeRemaining,
  attemptsLeft,
  currentScore,
  isLoading = false,
  onSubmitAnswer,
  onRequestHint,
  onTimeExpire
}) => {
  // Local state with proper typing
  const [answer, setAnswer] = useState<string>('')
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
  
  // Custom hooks
  const { formatTime } = useTimer()
  
  // Memoized handlers
  const handleSubmit = useCallback(async () => {
    if (!answer.trim() || isSubmitting) return
    
    setIsSubmitting(true)
    try {
      await onSubmitAnswer(answer.trim())
      setAnswer('') // Clear input on successful submission
    } catch (error) {
      console.error('Failed to submit answer:', error)
      // Error handling would be managed by parent component
    } finally {
      setIsSubmitting(false)
    }
  }, [answer, isSubmitting, onSubmitAnswer])
  
  const handleHintRequest = useCallback(async () => {
    if (isLoading) return
    
    try {
      await onRequestHint()
    } catch (error) {
      console.error('Failed to request hint:', error)
    }
  }, [isLoading, onRequestHint])
  
  // Effects
  useEffect(() => {
    if (timeRemaining === 0 && onTimeExpire) {
      onTimeExpire()
    }
  }, [timeRemaining, onTimeExpire])
  
  // Computed values
  const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100
  const isTimeWarning = timeRemaining <= 300 // 5 minutes
  const isTimeCritical = timeRemaining <= 60  // 1 minute
  
  return (
    <Card size="lg" variant="elevated" m="$4">
      <VStack space="md" p="$4">
        {/* Header with theme and timer */}
        <HStack justifyContent="space-between" alignItems="center">
          <Badge 
            size="sm" 
            variant="solid" 
            action="info"
            testID="theme-badge"
          >
            <Text color="$white" fontSize="$sm" fontWeight="$semibold">
              {theme}
            </Text>
          </Badge>
          <HStack space="sm" alignItems="center">
            <Text 
              fontSize="$sm" 
              color={isTimeCritical ? "$error500" : isTimeWarning ? "$warning500" : "$textLight600"}
              testID="timer-display"
            >
              ⏱️ {formatTime(timeRemaining)}
            </Text>
          </HStack>
        </HStack>
        
        {/* Progress indicator */}
        <Progress 
          value={progressValue} 
          size="sm"
          testID="session-progress"
        >
          <ProgressFilledTrack />
        </Progress>
        
        {/* Question */}
        <Text 
          fontSize="$lg" 
          fontWeight="$semibold" 
          lineHeight="$xl"
          testID="question-text"
        >
          {question}
        </Text>
        
        {/* Answer input */}
        <Input 
          size="lg" 
          isDisabled={isLoading || isSubmitting}
          testID="answer-input"
        >
          <InputField
            placeholder="Enter your answer..."
            value={answer}
            onChangeText={setAnswer}
            autoCapitalize="none"
            autoCorrect={false}
            onSubmitEditing={handleSubmit}
            returnKeyType="send"
          />
        </Input>
        
        {/* Game stats */}
        <HStack justifyContent="space-between" alignItems="center">
          <Text fontSize="$sm" color="$textLight600" testID="attempts-counter">
            Attempts left: {attemptsLeft}/3
          </Text>
          <Text fontSize="$sm" color="$textLight600" testID="score-display">
            Score: {currentScore} points
          </Text>
        </HStack>
        
        {/* Action buttons */}
        <HStack space="sm">
          <Button
            size="lg"
            variant="solid"
            action="primary"
            flex={1}
            onPress={handleSubmit}
            isDisabled={!answer.trim() || isLoading || isSubmitting}
            testID="submit-button"
          >
            <ButtonText>
              {isSubmitting ? 'Submitting...' : 'Submit Answer'}
            </ButtonText>
          </Button>
          <Button
            size="lg"
            variant="outline"
            action="secondary"
            onPress={handleHintRequest}
            isDisabled={isLoading || isSubmitting}
            testID="hint-button"
          >
            <ButtonText>💡 Hint</ButtonText>
          </Button>
        </HStack>
      </VStack>
    </Card>
  )
}

// Default export for dynamic imports
export default GameCard

Custom Hooks Pattern

// Custom hook with proper TypeScript types
import { useState, useEffect, useCallback, useRef } from 'react'
import { GameSessionService } from '../services/game-session-service'

interface UseGameSessionOptions {
  playerName: string
  onSessionEnd?: (finalScore: number) => void
  onError?: (error: Error) => void
}

interface GameSessionState {
  sessionId: string | null
  currentQuestion: Question | null
  score: number
  attemptsLeft: number
  timeRemaining: number
  isLoading: boolean
  error: string | null
}

interface GameSessionActions {
  startGame: () => Promise<void>
  submitAnswer: (answer: string) => Promise<boolean>
  requestHint: () => Promise<string>
  endGame: () => Promise<void>
}

export const useGameSession = (
  options: UseGameSessionOptions
): [GameSessionState, GameSessionActions] => {
  const [state, setState] = useState<GameSessionState>({
    sessionId: null,
    currentQuestion: null,
    score: 0,
    attemptsLeft: 3,
    timeRemaining: 30 * 60, // 30 minutes
    isLoading: false,
    error: null,
  })
  
  const timerRef = useRef<NodeJS.Timeout | null>(null)
  const gameService = useRef(new GameSessionService())
  
  // Timer effect
  useEffect(() => {
    if (state.sessionId && state.timeRemaining > 0) {
      timerRef.current = setInterval(() => {
        setState(prev => {
          const newTimeRemaining = prev.timeRemaining - 1
          if (newTimeRemaining <= 0 && options.onSessionEnd) {
            options.onSessionEnd(prev.score)
          }
          return { ...prev, timeRemaining: Math.max(0, newTimeRemaining) }
        })
      }, 1000)
    }
    
    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current)
      }
    }
  }, [state.sessionId, state.timeRemaining, options.onSessionEnd])
  
  // Actions
  const startGame = useCallback(async () => {
    setState(prev => ({ ...prev, isLoading: true, error: null }))
    
    try {
      const session = await gameService.current.startGame(options.playerName)
      setState(prev => ({
        ...prev,
        sessionId: session.sessionId,
        currentQuestion: session.firstQuestion,
        score: 0,
        attemptsLeft: 3,
        timeRemaining: session.timeRemaining,
        isLoading: false,
      }))
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Failed to start game'
      setState(prev => ({ ...prev, error: errorMessage, isLoading: false }))
      options.onError?.(error instanceof Error ? error : new Error(errorMessage))
    }
  }, [options.playerName, options.onError])
  
  const submitAnswer = useCallback(async (answer: string): Promise<boolean> => {
    if (!state.sessionId || !state.currentQuestion) {
      throw new Error('No active game session')
    }
    
    setState(prev => ({ ...prev, isLoading: true }))
    
    try {
      const result = await gameService.current.submitAnswer(
        state.sessionId,
        state.currentQuestion.id,
        answer
      )
      
      setState(prev => ({
        ...prev,
        score: result.newScore,
        attemptsLeft: result.attemptsLeft,
        currentQuestion: result.nextQuestion,
        isLoading: false,
      }))
      
      return result.isCorrect
    } catch (error) {
      setState(prev => ({ ...prev, isLoading: false }))
      throw error
    }
  }, [state.sessionId, state.currentQuestion])
  
  const requestHint = useCallback(async (): Promise<string> => {
    if (!state.sessionId || !state.currentQuestion) {
      throw new Error('No active game session')
    }
    
    const hint = await gameService.current.requestHint(
      state.sessionId,
      state.currentQuestion.id
    )
    
    return hint
  }, [state.sessionId, state.currentQuestion])
  
  const endGame = useCallback(async () => {
    if (!state.sessionId) return
    
    await gameService.current.endGame(state.sessionId)
    
    setState({
      sessionId: null,
      currentQuestion: null,
      score: 0,
      attemptsLeft: 3,
      timeRemaining: 30 * 60,
      isLoading: false,
      error: null,
    })
    
    if (timerRef.current) {
      clearInterval(timerRef.current)
    }
  }, [state.sessionId])
  
  return [
    state,
    {
      startGame,
      submitAnswer,
      requestHint,
      endGame,
    },
  ]
}

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

// React component testing with Jest and Testing Library
import React from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { GluestackUIProvider } from '@gluestack-ui/themed'
import { GameCard, GameCardProps } from '../GameCard'

// Test wrapper for Gluestack UI
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <GluestackUIProvider>{children}</GluestackUIProvider>
)

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

describe('GameCard', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })
  
  describe('Rendering', () => {
    it('should render question and theme correctly', () => {
      const props = createMockProps()
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('question-text')).toHaveTextContent(props.question)
      expect(screen.getByTestId('theme-badge')).toHaveTextContent(props.theme)
    })
    
    it('should display timer in correct format', () => {
      const props = createMockProps({ timeRemaining: 125 }) // 2:05
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05')
    })
    
    it('should show attempts and score correctly', () => {
      const props = createMockProps({ attemptsLeft: 2, currentScore: 5 })
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('attempts-counter')).toHaveTextContent('Attempts left: 2/3')
      expect(screen.getByTestId('score-display')).toHaveTextContent('Score: 5 points')
    })
  })
  
  describe('User Interactions', () => {
    it('should call onSubmitAnswer when submit button is clicked', async () => {
      const props = createMockProps()
      const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined)
      props.onSubmitAnswer = mockOnSubmitAnswer
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      // Type an answer
      const input = screen.getByTestId('answer-input')
      fireEvent.changeText(input, 'Paris')
      
      // Click submit
      const submitButton = screen.getByTestId('submit-button')
      fireEvent.press(submitButton)
      
      await waitFor(() => {
        expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris')
      })
    })
    
    it('should not submit empty answer', () => {
      const props = createMockProps()
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const submitButton = screen.getByTestId('submit-button')
      expect(submitButton).toBeDisabled()
    })
    
    it('should call onRequestHint when hint button is clicked', async () => {
      const props = createMockProps()
      const mockOnRequestHint = jest.fn().mockResolvedValue('City of Light')
      props.onRequestHint = mockOnRequestHint
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const hintButton = screen.getByTestId('hint-button')
      fireEvent.press(hintButton)
      
      await waitFor(() => {
        expect(mockOnRequestHint).toHaveBeenCalled()
      })
    })
  })
  
  describe('Loading States', () => {
    it('should disable inputs when loading', () => {
      const props = createMockProps({ isLoading: true })
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('answer-input')).toBeDisabled()
      expect(screen.getByTestId('submit-button')).toBeDisabled()
      expect(screen.getByTestId('hint-button')).toBeDisabled()
    })
  })
  
  describe('Timer States', () => {
    it('should show warning color when time is low', () => {
      const props = createMockProps({ timeRemaining: 300 }) // 5 minutes
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const timer = screen.getByTestId('timer-display')
      expect(timer).toHaveStyle({ color: expect.stringContaining('warning') })
    })
    
    it('should show critical color when time is very low', () => {
      const props = createMockProps({ timeRemaining: 30 }) // 30 seconds
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const timer = screen.getByTestId('timer-display')
      expect(timer).toHaveStyle({ color: expect.stringContaining('error') })
    })
  })
})

// Hook testing
import { renderHook, act } from '@testing-library/react'
import { useGameSession } from '../hooks/useGameSession'

// Mock the service
jest.mock('../services/game-session-service')

describe('useGameSession', () => {
  const mockGameService = {
    startGame: jest.fn(),
    submitAnswer: jest.fn(),
    requestHint: jest.fn(),
    endGame: jest.fn(),
  }
  
  beforeEach(() => {
    jest.clearAllMocks()
    // Set up service mock
    ;(GameSessionService as jest.Mock).mockImplementation(() => mockGameService)
  })
  
  it('should start game successfully', async () => {
    mockGameService.startGame.mockResolvedValue({
      sessionId: 'session123',
      firstQuestion: { id: 'q1', text: 'Test question' },
      timeRemaining: 1800,
    })
    
    const { result } = renderHook(() => 
      useGameSession({ playerName: 'TestPlayer' })
    )
    
    await act(async () => {
      await result.current[1].startGame()
    })
    
    expect(result.current[0].sessionId).toBe('session123')
    expect(result.current[0].currentQuestion).toBeDefined()
  })
  
  it('should handle errors during game start', async () => {
    const mockError = new Error('Failed to start game')
    mockGameService.startGame.mockRejectedValue(mockError)
    
    const onError = jest.fn()
    const { result } = renderHook(() => 
      useGameSession({ playerName: 'TestPlayer', onError })
    )
    
    await act(async () => {
      await result.current[1].startGame()
    })
    
    expect(result.current[0].error).toBe('Failed to start game')
    expect(onError).toHaveBeenCalledWith(mockError)
  })
})

Code Quality Standards

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": [
    "@react-native",
    "@typescript-eslint/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "react-hooks"],
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/prefer-const": "error",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "prefer-const": "error",
    "no-var": "error",
    "object-shorthand": "error",
    "prefer-template": "error"
  }
}

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

This comprehensive development guidelines document ensures consistent, maintainable, and high-quality code across the Know Foolery project.