package companion import ( "context" "database/sql" "database/sql/driver" "errors" "strings" "sync" "testing" "time" ) const recordingSQLDriverName = "scum_companion_recording_sql" var ( recordingSQLDriverOnce sync.Once activeSQLRecorder *sqlRecorder ) type sqlRecorder struct { mu sync.Mutex dsn string statements []string args [][]driver.NamedValue commits int } func (recorder *sqlRecorder) append(query string, args []driver.NamedValue) { recorder.mu.Lock() defer recorder.mu.Unlock() recorder.statements = append(recorder.statements, query) recorder.args = append(recorder.args, append([]driver.NamedValue(nil), args...)) } func (recorder *sqlRecorder) committed() { recorder.mu.Lock() defer recorder.mu.Unlock() recorder.commits++ } type recordingSQLDriver struct{} type recordingSQLConn struct{ recorder *sqlRecorder } type recordingSQLTx struct{ recorder *sqlRecorder } func (recordingSQLDriver) Open(dsn string) (driver.Conn, error) { if activeSQLRecorder == nil { return nil, errors.New("recording SQL recorder is not configured") } activeSQLRecorder.dsn = dsn return &recordingSQLConn{recorder: activeSQLRecorder}, nil } func (conn *recordingSQLConn) Prepare(string) (driver.Stmt, error) { return nil, errors.New("prepare is not supported") } func (conn *recordingSQLConn) Close() error { return nil } func (conn *recordingSQLConn) Begin() (driver.Tx, error) { return &recordingSQLTx{recorder: conn.recorder}, nil } func (conn *recordingSQLConn) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) { return &recordingSQLTx{recorder: conn.recorder}, nil } func (conn *recordingSQLConn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) { conn.recorder.append(query, args) return driver.RowsAffected(1), nil } func (tx *recordingSQLTx) Commit() error { tx.recorder.committed(); return nil } func (tx *recordingSQLTx) Rollback() error { return nil } func TestSCUMSQLStoreWritesTrajectorySamplesWithoutCoordinateConversion(t *testing.T) { db, recorder := newRecordingSQLDB(t, "direct-db") store, err := NewSCUMSQLStore(db) if err != nil { t.Fatalf("create SCUM SQL store: %v", err) } if err := store.EnsureSchema(context.Background()); err != nil { t.Fatalf("ensure schema: %v", err) } z := 7.75 sampledAt := time.Date(2026, 8, 28, 7, 30, 0, 123456000, time.UTC) written, err := store.StoreTrajectorySamples(context.Background(), []TrajectorySample{{ ServerInstanceID: "server-1", SubjectType: "player", SubjectID: "76561198000000001", SteamID: "76561198000000001", UserProfileID: "profile-1", GamePlayerID: "player-1", WorldX: 123.5, WorldY: -456.25, WorldZ: &z, ObservedAt: sampledAt.Add(-time.Second), SampledAt: sampledAt, Source: "plugin.sql.scum.positions", }}) if err != nil || written != 1 { t.Fatalf("store trajectory sample: written=%d err=%v", written, err) } if recorder.commits != 1 { t.Fatalf("trajectory write did not commit once: %d", recorder.commits) } insertIndex := findStatement(recorder.statements, "INSERT INTO scum_trajectories") if insertIndex < 0 { t.Fatalf("missing trajectory insert statement: %v", recorder.statements) } insert := recorder.statements[insertIndex] if !strings.Contains(insert, "world_x") || !strings.Contains(insert, "world_y") || !strings.Contains(insert, "world_z") || strings.Contains(insert, "map_x") || strings.Contains(insert, "pixel") { t.Fatalf("trajectory SQL does not use raw world coordinate columns: %s", insert) } args := recorder.args[insertIndex] if args[13].Value != 123.5 || args[14].Value != -456.25 || args[15].Value != 7.75 { t.Fatalf("coordinates were changed before storage: %+v", args[13:16]) } } func TestTrajectorySamplesFromSCUMRowsKeepWorldCoordinates(t *testing.T) { sampledAt := time.Date(2026, 8, 28, 8, 0, 0, 0, time.UTC) positionSamples, err := TrajectorySamplesFromPositionRows("server-1", []map[string]any{ {"subjectType": "player", "subjectId": "76561198000000001", "userProfileId": "profile-1", "gamePlayerId": "player-1", "x": 10.25, "y": -20.5, "z": 3.75, "observedAt": "2026-08-28T07:59:00Z"}, {"subjectType": "base", "subjectId": "base-1", "x": 1, "y": 2, "z": 0}, }, sampledAt) if err != nil || len(positionSamples) != 1 { t.Fatalf("position samples=%+v err=%v", positionSamples, err) } if positionSamples[0].WorldX != 10.25 || positionSamples[0].WorldY != -20.5 || positionSamples[0].WorldZ == nil || *positionSamples[0].WorldZ != 3.75 || positionSamples[0].Source != "plugin.sql.scum.positions" { t.Fatalf("position coordinates were not preserved: %+v", positionSamples[0]) } vehicleSamples, err := TrajectorySamplesFromVehicleRows("server-1", []map[string]any{{"vehicleId": "vehicle-1", "entityId": "entity-1", "className": "BPC_Laika_C", "label": "Laika", "x": "400.5", "y": 200, "z": 0, "lastAccessTime": "2026-08-28T07:58:00Z"}}, sampledAt) if err != nil || len(vehicleSamples) != 1 { t.Fatalf("vehicle samples=%+v err=%v", vehicleSamples, err) } if vehicleSamples[0].SubjectType != "vehicle" || vehicleSamples[0].WorldX != 400.5 || vehicleSamples[0].WorldY != 200 || vehicleSamples[0].ClassName != "BPC_Laika_C" || vehicleSamples[0].Source != "plugin.sql.scum.vehicles" { t.Fatalf("vehicle coordinates were not preserved: %+v", vehicleSamples[0]) } } func TestOpenSCUMSQLStoreFromEnvUsesSharedPlatformDSNName(t *testing.T) { recorder := &sqlRecorder{} activeSQLRecorder = recorder previousOpenSQL := openSQL t.Cleanup(func() { openSQL = previousOpenSQL }) openSQL = func(driverName, dsn string) (*sql.DB, error) { if driverName != "mysql" { t.Fatalf("unexpected SQL driver: %s", driverName) } return sql.Open(recordingSQLDriverName, dsn) } t.Setenv(PlatformMySQLDSNEnvironment, "platform:platform@tcp(127.0.0.1:3306)/platform?parseTime=true") store, err := OpenSCUMSQLStoreFromEnv(PlatformMySQLDSNEnvironment) if err != nil { t.Fatalf("open store from shared env: %v", err) } if err := store.EnsureSchema(context.Background()); err != nil { t.Fatalf("ensure schema from shared env: %v", err) } store.db.Close() if recorder.dsn != "platform:platform@tcp(127.0.0.1:3306)/platform?parseTime=true" { t.Fatalf("store did not use the shared platform DSN environment: %q", recorder.dsn) } } func newRecordingSQLDB(t *testing.T, dsn string) (*sql.DB, *sqlRecorder) { t.Helper() recordingSQLDriverOnce.Do(func() { sql.Register(recordingSQLDriverName, recordingSQLDriver{}) }) recorder := &sqlRecorder{} activeSQLRecorder = recorder db, err := sql.Open(recordingSQLDriverName, dsn) if err != nil { t.Fatalf("open recording SQL db: %v", err) } return db, recorder } func findStatement(statements []string, prefix string) int { for index, statement := range statements { if strings.Contains(statement, prefix) { return index } } return -1 }