diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.cpp index 5185557..aee3103 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.cpp @@ -3,11 +3,20 @@ #include "HyperTwistBootstrap/HyperTwistContractLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" #include "HAL/FileManager.h" +#include "JsonObjectConverter.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" namespace HyperTwistTrainingPersistenceLibraryInternal { + template + FString SerializeStructToJson(const TStruct& Value) + { + FString Json; + FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0); + return Json; + } + FString SanitizePathToken(const FString& Value) { FString Sanitized = Value; @@ -31,6 +40,22 @@ namespace HyperTwistTrainingPersistenceLibraryInternal : RepositoryPath; } + FString ResolveVerificationPath(const FString& RepositoryPath) + { + if (RepositoryPath.IsEmpty()) + { + return FPaths::Combine( + FPaths::ProjectSavedDir(), + TEXT("HyperTwist"), + TEXT("training-repository-roundtrip.json") + ); + } + + return FPaths::IsRelative(RepositoryPath) + ? FPaths::Combine(FPaths::ProjectSavedDir(), RepositoryPath) + : RepositoryPath; + } + FString ResolveTimerExportPath( const FString& UserId, const FString& DeckId, @@ -55,6 +80,14 @@ namespace HyperTwistTrainingPersistenceLibraryInternal ? FPaths::Combine(FPaths::ProjectSavedDir(), ExportPath) : ExportPath; } + + void AppendVerificationNote(TArray& Notes, const FString& Note) + { + if (!Note.IsEmpty()) + { + Notes.Add(Note); + } + } } FString UHyperTwistTrainingPersistenceLibrary::GetDefaultTrainingRepositoryPath() @@ -67,6 +100,16 @@ FString UHyperTwistTrainingPersistenceLibrary::ResolveTrainingRepositoryPath(con return HyperTwistTrainingPersistenceLibraryInternal::ResolvePath(RepositoryPath); } +FString UHyperTwistTrainingPersistenceLibrary::GetDefaultTrainingRepositoryVerificationPath() +{ + return HyperTwistTrainingPersistenceLibraryInternal::ResolveVerificationPath(FString()); +} + +FString UHyperTwistTrainingPersistenceLibrary::ResolveTrainingRepositoryVerificationPath(const FString& RepositoryPath) +{ + return HyperTwistTrainingPersistenceLibraryInternal::ResolveVerificationPath(RepositoryPath); +} + FString UHyperTwistTrainingPersistenceLibrary::GetDefaultTrainingTimerExportPath(const FString& UserId, const FString& DeckId) { return HyperTwistTrainingPersistenceLibraryInternal::ResolveTimerExportPath(UserId, DeckId, FString()); @@ -149,6 +192,146 @@ bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingRepositoryStateFromFile( return true; } +FHyperTwistTrainingRepositoryRoundTripVerification +UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& RepositoryPath +) +{ + FHyperTwistTrainingRepositoryRoundTripVerification Verification; + Verification.ResolvedPath = ResolveTrainingRepositoryVerificationPath(RepositoryPath); + + const FHyperTwistTrainingRepositoryState ExpectedRepositoryState = + UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(RepositoryState); + Verification.ExpectedRunRecordCount = ExpectedRepositoryState.RunRecords.Num(); + Verification.ExpectedLearnerStateCount = ExpectedRepositoryState.LearnerStates.Num(); + Verification.ExpectedReviewPlanCount = ExpectedRepositoryState.ReviewPlans.Num(); + Verification.ExpectedStoredSessionTemplateCount = ExpectedRepositoryState.StoredSessionTemplates.Num(); + Verification.ExpectedMethodDrillFavoriteCount = ExpectedRepositoryState.StoredMethodDrillFavorites.Num(); + Verification.ExpectedMethodDrillBatchCount = ExpectedRepositoryState.StoredMethodDrillBatches.Num(); + Verification.ExpectedCoachActionPlanCount = ExpectedRepositoryState.StoredCoachActionPlans.Num(); + Verification.ExpectedCoachSessionQueueCount = ExpectedRepositoryState.StoredCoachSessionQueues.Num(); + Verification.ExpectedCoachSessionQueueHistoryCount = + ExpectedRepositoryState.StoredCoachSessionQueueHistory.Num(); + Verification.ExpectedIntegrityReport = + UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(ExpectedRepositoryState); + + FString SavedPath; + Verification.bSaveSucceeded = SaveTrainingRepositoryStateToFile( + ExpectedRepositoryState, + Verification.ResolvedPath, + SavedPath + ); + Verification.ResolvedPath = SavedPath; + if (!Verification.bSaveSucceeded) + { + HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote( + Verification.VerificationNotes, + TEXT("roundtrip-save-failed") + ); + return Verification; + } + + FHyperTwistTrainingRepositoryState LoadedRepositoryState; + FString LoadedPath; + Verification.bLoadSucceeded = LoadTrainingRepositoryStateFromFile( + Verification.ResolvedPath, + LoadedRepositoryState, + LoadedPath + ); + Verification.ResolvedPath = LoadedPath; + if (!Verification.bLoadSucceeded) + { + HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote( + Verification.VerificationNotes, + TEXT("roundtrip-load-failed") + ); + return Verification; + } + + Verification.LoadedRunRecordCount = LoadedRepositoryState.RunRecords.Num(); + Verification.LoadedLearnerStateCount = LoadedRepositoryState.LearnerStates.Num(); + Verification.LoadedReviewPlanCount = LoadedRepositoryState.ReviewPlans.Num(); + Verification.LoadedStoredSessionTemplateCount = LoadedRepositoryState.StoredSessionTemplates.Num(); + Verification.LoadedMethodDrillFavoriteCount = LoadedRepositoryState.StoredMethodDrillFavorites.Num(); + Verification.LoadedMethodDrillBatchCount = LoadedRepositoryState.StoredMethodDrillBatches.Num(); + Verification.LoadedCoachActionPlanCount = LoadedRepositoryState.StoredCoachActionPlans.Num(); + Verification.LoadedCoachSessionQueueCount = LoadedRepositoryState.StoredCoachSessionQueues.Num(); + Verification.LoadedCoachSessionQueueHistoryCount = + LoadedRepositoryState.StoredCoachSessionQueueHistory.Num(); + Verification.LoadedIntegrityReport = + UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(LoadedRepositoryState); + + Verification.bRepositoryCountsMatch = + Verification.ExpectedRunRecordCount == Verification.LoadedRunRecordCount + && Verification.ExpectedLearnerStateCount == Verification.LoadedLearnerStateCount + && Verification.ExpectedReviewPlanCount == Verification.LoadedReviewPlanCount + && Verification.ExpectedStoredSessionTemplateCount == Verification.LoadedStoredSessionTemplateCount + && Verification.ExpectedMethodDrillFavoriteCount == Verification.LoadedMethodDrillFavoriteCount + && Verification.ExpectedMethodDrillBatchCount == Verification.LoadedMethodDrillBatchCount + && Verification.ExpectedCoachActionPlanCount == Verification.LoadedCoachActionPlanCount + && Verification.ExpectedCoachSessionQueueCount == Verification.LoadedCoachSessionQueueCount + && Verification.ExpectedCoachSessionQueueHistoryCount + == Verification.LoadedCoachSessionQueueHistoryCount; + + if (!Verification.bRepositoryCountsMatch) + { + HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote( + Verification.VerificationNotes, + TEXT("roundtrip-count-mismatch") + ); + } + + const FString ExpectedRepositoryJson = + UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(ExpectedRepositoryState); + const FString LoadedRepositoryJson = + UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(LoadedRepositoryState); + Verification.bSerializedStateMatch = + !ExpectedRepositoryJson.IsEmpty() + && !LoadedRepositoryJson.IsEmpty() + && ExpectedRepositoryJson == LoadedRepositoryJson; + if (!Verification.bSerializedStateMatch) + { + HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote( + Verification.VerificationNotes, + TEXT("roundtrip-serialized-state-mismatch") + ); + } + + const FString ExpectedIntegrityJson = + HyperTwistTrainingPersistenceLibraryInternal::SerializeStructToJson(Verification.ExpectedIntegrityReport); + const FString LoadedIntegrityJson = + HyperTwistTrainingPersistenceLibraryInternal::SerializeStructToJson(Verification.LoadedIntegrityReport); + Verification.bIntegrityReportMatch = + !ExpectedIntegrityJson.IsEmpty() + && !LoadedIntegrityJson.IsEmpty() + && ExpectedIntegrityJson == LoadedIntegrityJson; + if (!Verification.bIntegrityReportMatch) + { + HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote( + Verification.VerificationNotes, + TEXT("roundtrip-integrity-report-mismatch") + ); + } + + if (Verification.LoadedIntegrityReport.HasIntegrityIssues()) + { + HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote( + Verification.VerificationNotes, + TEXT("roundtrip-loaded-state-still-has-integrity-issues") + ); + } + + Verification.bDidRoundTripPass = + Verification.bSaveSucceeded + && Verification.bLoadSucceeded + && Verification.bRepositoryCountsMatch + && Verification.bSerializedStateMatch + && Verification.bIntegrityReportMatch + && !Verification.LoadedIntegrityReport.HasIntegrityIssues(); + return Verification; +} + bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingTimerExportPacketFromFile( const FString& UserId, const FString& DeckId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index de1aa7d..b173732 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -114,6 +114,19 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRuntimeLibrary:: return FHyperTwistTrainingRepositoryIntegrityReport(); } +FHyperTwistTrainingRepositoryRoundTripVerification +UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRepositoryRoundTripVerification( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveRepositoryRoundTripVerification(); + } + + return FHyperTwistTrainingRepositoryRoundTripVerification(); +} + FHyperTwistTrainingDeckHistorySummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingDeckHistorySummary(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -811,6 +824,19 @@ bool UHyperTwistTrainingRuntimeLibrary::LoadTrainingRepositoryStateFromDefaultPa return false; } +FHyperTwistTrainingRepositoryRoundTripVerification +UHyperTwistTrainingRuntimeLibrary::VerifyTrainingRepositoryRoundTripAtDefaultPath( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->VerifyTrainingRepositoryRoundTripAtDefaultPath(); + } + + return FHyperTwistTrainingRepositoryRoundTripVerification(); +} + bool UHyperTwistTrainingRuntimeLibrary::SaveTrainingTimerExportPacketToDefaultPath( UObject* WorldContextObject, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index e4de8ff..19770a9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -53,6 +53,12 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingSubsystem::GetAc return ActiveRepositoryIntegrityReport; } +FHyperTwistTrainingRepositoryRoundTripVerification +UHyperTwistTrainingSubsystem::GetActiveRepositoryRoundTripVerification() const +{ + return ActiveRepositoryRoundTripVerification; +} + FHyperTwistTrainingDeckHistorySummary UHyperTwistTrainingSubsystem::GetActiveDeckHistorySummary() const { return ActiveDeckHistorySummary; @@ -574,6 +580,17 @@ bool UHyperTwistTrainingSubsystem::LoadTrainingRepositoryStateFromDefaultPath(FS return true; } +FHyperTwistTrainingRepositoryRoundTripVerification +UHyperTwistTrainingSubsystem::VerifyTrainingRepositoryRoundTripAtDefaultPath() +{ + ActiveRepositoryRoundTripVerification = + UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip( + TrainingRepositoryState, + FString() + ); + return ActiveRepositoryRoundTripVerification; +} + bool UHyperTwistTrainingSubsystem::SaveTrainingTimerExportPacketToDefaultPath( const FString& UserId, const FString& DeckId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h index 54e96ea..bb67cac 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h @@ -17,6 +17,12 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Persistence") static FString ResolveTrainingRepositoryPath(const FString& RepositoryPath); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Persistence") + static FString GetDefaultTrainingRepositoryVerificationPath(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Persistence") + static FString ResolveTrainingRepositoryVerificationPath(const FString& RepositoryPath); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Persistence") static FString GetDefaultTrainingTimerExportPath(const FString& UserId, const FString& DeckId); @@ -48,6 +54,12 @@ public: FString& OutResolvedPath ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Persistence") + static FHyperTwistTrainingRepositoryRoundTripVerification VerifyTrainingRepositoryStateRoundTrip( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& RepositoryPath + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Persistence") static bool LoadTrainingTimerExportPacketFromFile( const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 3631798..8851835 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -45,6 +45,11 @@ public: UObject* WorldContextObject ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingRepositoryRoundTripVerification GetActiveTrainingRepositoryRoundTripVerification( + UObject* WorldContextObject + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingDeckHistorySummary GetActiveTrainingDeckHistorySummary(UObject* WorldContextObject); @@ -317,6 +322,11 @@ public: FString& OutResolvedPath ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingRepositoryRoundTripVerification VerifyTrainingRepositoryRoundTripAtDefaultPath( + UObject* WorldContextObject + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static bool SaveTrainingTimerExportPacketToDefaultPath( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index e6a65eb..d44575b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -38,6 +38,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingRepositoryIntegrityReport GetActiveRepositoryIntegrityReport() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + FHyperTwistTrainingRepositoryRoundTripVerification GetActiveRepositoryRoundTripVerification() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingDeckHistorySummary GetActiveDeckHistorySummary() const; @@ -260,6 +263,9 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") bool LoadTrainingRepositoryStateFromDefaultPath(FString& OutResolvedPath); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + FHyperTwistTrainingRepositoryRoundTripVerification VerifyTrainingRepositoryRoundTripAtDefaultPath(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") bool SaveTrainingTimerExportPacketToDefaultPath( const FString& UserId, @@ -423,6 +429,9 @@ private: UPROPERTY() FHyperTwistTrainingRepositoryIntegrityReport ActiveRepositoryIntegrityReport; + UPROPERTY() + FHyperTwistTrainingRepositoryRoundTripVerification ActiveRepositoryRoundTripVerification; + UPROPERTY() FHyperTwistTrainingDeckHistorySummary ActiveDeckHistorySummary; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 6398827..4297ce0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -4087,3 +4087,98 @@ struct FHyperTwistTrainingRepositoryIntegrityReport || MissingCoachSessionQueueHistorySourceSessionCount > 0; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingRepositoryRoundTripVerification +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ResolvedPath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSaveSucceeded = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLoadSucceeded = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRepositoryCountsMatch = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSerializedStateMatch = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bIntegrityReportMatch = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bDidRoundTripPass = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedRunRecordCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedRunRecordCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedLearnerStateCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedLearnerStateCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedReviewPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedReviewPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedStoredSessionTemplateCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedStoredSessionTemplateCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedMethodDrillFavoriteCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedMethodDrillFavoriteCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedMethodDrillBatchCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedMethodDrillBatchCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedCoachActionPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedCoachActionPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedCoachSessionQueueCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedCoachSessionQueueCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpectedCoachSessionQueueHistoryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LoadedCoachSessionQueueHistoryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingRepositoryIntegrityReport ExpectedIntegrityReport; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingRepositoryIntegrityReport LoadedIntegrityReport; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray VerificationNotes; + + bool IsStructurallyValid() const + { + return !ResolvedPath.IsEmpty() && bSaveSucceeded && bLoadSucceeded; + } +};