All files / web/src/test/journey-simulator EphemeralDatabase.ts

81.92% Statements 136/166
83.33% Branches 5/6
71.42% Functions 5/7
81.92% Lines 136/166

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 1671x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 66x 66x 1x 1x 1x 1x 1x 1x 36x     36x 36x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 1x 1x 1x 1x 1x 1x 1x 1x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                         1x 1x 1x 1x 1x 1x 1x                  
/**
 * Ephemeral Database Factory
 *
 * Creates in-memory SQLite databases for testing.
 * Each database is fully isolated with all migrations applied.
 */
 
import Database from 'better-sqlite3'
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import * as schema from '@/db/schema'
 
/**
 * Type alias for the database client with schema
 */
export type TestDatabase = BetterSQLite3Database<typeof schema>
 
// Module-level variable to hold the "current" ephemeral database for mocking
let currentEphemeralDb: TestDatabase | null = null
 
/**
 * Sets the current ephemeral database for mocking purposes.
 * Call this in beforeEach to make the mock work.
 */
export function setCurrentEphemeralDb(db: TestDatabase | null): void {
  currentEphemeralDb = db
}
 
/**
 * Gets the current ephemeral database.
 * Used by the vi.mock to provide the db to modules being tested.
 */
export function getCurrentEphemeralDb(): TestDatabase {
  if (!currentEphemeralDb) {
    throw new Error('No ephemeral database set. Call setCurrentEphemeralDb() in beforeEach.')
  }
  return currentEphemeralDb
}
 
/**
 * Result of creating an ephemeral database
 */
export interface EphemeralDbResult {
  /** Drizzle database client */
  db: TestDatabase
  /** Underlying better-sqlite3 instance */
  sqlite: Database.Database
  /** Cleanup function to close the database */
  cleanup: () => void
}
 
/**
 * Creates an ephemeral in-memory database for testing.
 * Applies all migrations and returns a Drizzle instance.
 *
 * @returns Database instance with cleanup function
 */
export function createEphemeralDatabase(): EphemeralDbResult {
  // Create in-memory SQLite database
  const sqlite = new Database(':memory:')
 
  // Enable foreign keys (required for cascading deletes)
  sqlite.pragma('foreign_keys = ON')
 
  // Create Drizzle instance with schema
  const db = drizzle(sqlite, { schema })
 
  // Apply all migrations
  migrate(db, { migrationsFolder: './drizzle' })
 
  return {
    db,
    sqlite,
    cleanup: () => {
      sqlite.close()
    },
  }
}
 
/**
 * Creates a test user and player in the database.
 *
 * @param db - The ephemeral database instance
 * @param playerId - Optional custom player ID (defaults to 'test-student')
 * @returns The created user and player IDs
 */
export async function createTestStudent(
  db: TestDatabase,
  playerId: string = 'test-student'
): Promise<{ userId: string; playerId: string }> {
  const userId = `user-${playerId}`
  const now = new Date()
 
  // Create user
  await db.insert(schema.users).values({
    id: userId,
    guestId: `guest-${playerId}-${Date.now()}`,
    createdAt: now,
  })
 
  // Create player
  await db.insert(schema.players).values({
    id: playerId,
    userId,
    name: 'Test Student',
    emoji: '🧪',
    color: '#4F46E5',
    isActive: true,
    createdAt: now,
  })
 
  // Initialize curriculum position
  await db.insert(schema.playerCurriculum).values({
    playerId,
    currentLevel: 1,
    currentPhaseId: 'L1.add.+1.direct',
    createdAt: now,
    updatedAt: now,
  })
 
  return { userId, playerId }
}
 
/**
 * Creates skill mastery records for a player.
 *
 * @param db - The ephemeral database instance
 * @param playerId - The player ID
 * @param skillIds - Array of skill IDs to initialize
 * @param isPracticing - Whether skills should be marked as practicing (default: true)
 */
export async function initializeSkillMastery(
  db: TestDatabase,
  playerId: string,
  skillIds: string[],
  isPracticing: boolean = true
): Promise<void> {
  const now = new Date()

  for (const skillId of skillIds) {
    await db.insert(schema.playerSkillMastery).values({
      playerId,
      skillId,
      isPracticing,
      practiceLevel: isPracticing ? 'visual' : 'none',
      lastHadHelp: false,
      createdAt: now,
      lastPracticedAt: null,
    })
  }
}
 
/**
 * Resets the database by clearing all tables.
 * Useful for running multiple scenarios in a single test file.
 *
 * @param db - The ephemeral database instance
 */
export async function resetDatabase(db: TestDatabase): Promise<void> {
  // Delete in reverse order of dependencies
  await db.delete(schema.sessionPlans)
  await db.delete(schema.playerSkillMastery)
  await db.delete(schema.playerCurriculum)
  await db.delete(schema.players)
  await db.delete(schema.users)
}