GroveEngine/planTI/scenario_12_datanode.md
StillHammer 094ab28865 feat: Add integration test scenarios 11-13 for IO and DataNode systems
Added three new integration test scenario documents:
- Scenario 11: IO System Stress Test - Tests IntraIO pub/sub with pattern matching, batching, backpressure, and thread safety
- Scenario 12: DataNode Integration Test - Tests IDataTree with hot-reload, persistence, hashing, and performance on 1000+ nodes
- Scenario 13: Cross-System Integration - Tests IO + DataNode working together with config hot-reload chains and concurrent access

Also includes comprehensive DataNode system architecture analysis documentation.

These scenarios complement the existing test suite by covering the IO communication layer and data management systems that were previously untested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 15:09:39 +08:00

33 KiB

Scénario 12: DataNode Integration Test

Priorité: SHOULD HAVE Phase: 2 (SHOULD HAVE) Durée estimée: ~4 minutes Effort implémentation: ~5-7 heures


🎯 Objectif

Valider que le système DataNode (IDataTree/JsonDataTree) fonctionne correctement pour tous les cas d'usage:

  • Tree navigation (exact match, pattern matching, queries)
  • Hot-reload system (file watch, callbacks, isolation)
  • Persistence (save/load, data integrity)
  • Hash system (data hash, tree hash, change detection)
  • Read-only enforcement (config/ vs data/ vs runtime/)
  • Type safety et defaults
  • Performance avec large trees (1000+ nodes)

Note: Le DataNode est le système central de configuration et persistence du moteur.


📋 Description

Setup Initial

  1. Créer un IDataTree avec structure complète:
    • config/ - Configuration read-only avec 500 nodes
    • data/ - Persistence read-write avec 300 nodes
    • runtime/ - State temporaire avec 200 nodes
  2. Total: ~1000 nodes dans l'arbre
  3. Fichiers JSON sur disque pour config/ et data/

Test Séquence

Test 1: Tree Navigation & Exact Matching (30s)

  1. Créer hiérarchie: config/units/tanks/heavy_mk1
  2. Tester navigation:
    • getChild("units")getChild("tanks")getChild("heavy_mk1")
    • getChildrenByName("heavy_mk1") - direct children only
    • getPath() - verify full path
  3. Vérifier:
    • Nodes trouvés correctement
    • Path correct: "config/units/tanks/heavy_mk1"
    • getChild retourne nullptr si non trouvé

Test 2: Pattern Matching (Wildcards) (30s)

  1. Créer nodes:
    • config/units/tanks/heavy_mk1
    • config/units/tanks/heavy_mk2
    • config/units/infantry/heavy_trooper
    • config/units/aircraft/light_fighter
  2. Tester patterns:
    • getChildrenByNameMatch("*heavy*") → 3 matches
    • getChildrenByNameMatch("tanks/*") → 2 matches
    • getChildrenByNameMatch("*_mk*") → 2 matches
  3. Vérifier tous les matches corrects

Test 3: Property-Based Queries (30s)

  1. Créer nodes avec propriétés:
    • heavy_mk1: armor=150, speed=30, cost=1000
    • heavy_mk2: armor=180, speed=25, cost=1200
    • light_fighter: armor=50, speed=120, cost=800
  2. Query predicates:
    • queryByProperty("armor", val > 100) → 2 units
    • queryByProperty("speed", val > 50) → 1 unit
    • queryByProperty("cost", val <= 1000) → 2 units
  3. Vérifier résultats des queries

Test 4: Hot-Reload System (60s)

  1. Créer config/gameplay.json sur disque
  2. Charger dans tree avec onTreeReloaded callback
  3. Modifier fichier sur disque (changer valeur)
  4. Appeler checkForChanges() → devrait détecter changement
  5. Appeler reloadIfChanged() → callback déclenché
  6. Vérifier:
    • Callback appelé exactement 1 fois
    • Nouvelles valeurs chargées
    • Anciens nodes remplacés

Test 5: Hot-Reload Isolation (30s)

  1. Charger 2 fichiers: config/units.json et config/maps.json
  2. Modifier seulement units.json
  3. Vérifier:
    • checkForChanges() détecte seulement units.json
    • Reload ne touche pas maps.json
    • Callback reçoit info sur quel fichier changé

Test 6: Persistence (Save/Load) (60s)

  1. Créer structure data/:
    • data/player/stats - {kills: 42, deaths: 3}
    • data/player/inventory - {gold: 1000, items: [...]}
    • data/world/time - {day: 5, hour: 14}
  2. Appeler saveData() → écrit sur disque
  3. Vérifier fichiers créés:
    • data/player/stats.json
    • data/player/inventory.json
    • data/world/time.json
  4. Charger dans nouveau tree
  5. Vérifier data identique (deep comparison)

Test 7: Selective Save (30s)

  1. Modifier seulement data/player/stats
  2. Appeler saveNode("data/player/stats")
  3. Vérifier:
    • Seulement stats.json écrit
    • Autres fichiers non modifiés (mtime identique)

Test 8: Hash System (Data Hash) (30s)

  1. Créer node avec data: {value: 42}
  2. Calculer getDataHash()
  3. Modifier data: {value: 43}
  4. Recalculer hash
  5. Vérifier hashes différents

Test 9: Hash System (Tree Hash) (30s)

  1. Créer arbre:
    root
      ├─ child1 {data: 1}
      └─ child2 {data: 2}
    
  2. Calculer getTreeHash()
  3. Modifier child1 data
  4. Recalculer tree hash
  5. Vérifier hashes différents (propagation)

Test 10: Read-Only Enforcement (30s)

  1. Tenter setChild() sur node config/
  2. Devrait throw exception
  3. Vérifier:
    • Exception levée
    • Message descriptif
    • Config/ non modifié

Test 11: Type Safety & Defaults (20s)

  1. Créer node: {armor: 150, name: "Tank"}
  2. Tester accès:
    • getInt("armor") → 150
    • getInt("missing", 100) → 100 (default)
    • getString("name") → "Tank"
    • getBool("active", true) → true (default)
    • getDouble("speed") → throw ou default

Test 12: Deep Tree Performance (30s)

  1. Créer tree avec 1000 nodes:
    • 10 catégories
    • 10 subcatégories each
    • 10 items each
  2. Mesurer temps:
    • Pattern matching "*" (tous nodes): < 100ms
    • Query by property: < 50ms
    • Tree hash calculation: < 200ms
  3. Vérifier performance acceptable

🏗️ Implémentation

Test Module Structure

// DataNodeTestModule.h
class DataNodeTestModule : public IModule {
public:
    void initialize(std::shared_ptr<IDataNode> config) override;
    void process(float deltaTime) override;
    std::shared_ptr<IDataNode> getState() const override;
    void setState(std::shared_ptr<IDataNode> state) override;
    bool isIdle() const override { return true; }

    // Test helpers
    void createTestTree();
    void testNavigation();
    void testPatternMatching();
    void testQueries();
    void testHotReload();
    void testPersistence();
    void testHashes();
    void testReadOnly();
    void testTypeAccess();
    void testPerformance();

private:
    std::shared_ptr<IDataTree> tree;
    int reloadCallbackCount = 0;
};

Test Principal

// test_12_datanode.cpp
#include "helpers/TestMetrics.h"
#include "helpers/TestAssertions.h"
#include "helpers/TestReporter.h"
#include <fstream>

int main() {
    TestReporter reporter("DataNode Integration Test");
    TestMetrics metrics;

    // === SETUP ===
    std::filesystem::create_directories("test_data/config");
    std::filesystem::create_directories("test_data/data");

    // Créer IDataTree
    auto tree = std::make_shared<JsonDataTree>("test_data");

    // ========================================================================
    // TEST 1: Tree Navigation & Exact Matching
    // ========================================================================
    std::cout << "\n=== TEST 1: Tree Navigation & Exact Matching ===\n";

    // Créer hiérarchie
    auto configRoot = tree->getConfigRoot();
    auto units = std::make_shared<JsonDataNode>("units", nlohmann::json::object());
    auto tanks = std::make_shared<JsonDataNode>("tanks", nlohmann::json::object());
    auto heavyMk1 = std::make_shared<JsonDataNode>("heavy_mk1", nlohmann::json{
        {"armor", 150},
        {"speed", 30},
        {"cost", 1000}
    });

    tanks->setChild("heavy_mk1", heavyMk1);
    units->setChild("tanks", tanks);
    configRoot->setChild("units", units);

    // Navigation
    auto foundUnits = configRoot->getChild("units");
    ASSERT_TRUE(foundUnits != nullptr, "Should find units node");

    auto foundTanks = foundUnits->getChild("tanks");
    ASSERT_TRUE(foundTanks != nullptr, "Should find tanks node");

    auto foundHeavy = foundTanks->getChild("heavy_mk1");
    ASSERT_TRUE(foundHeavy != nullptr, "Should find heavy_mk1 node");

    // Path
    std::string path = foundHeavy->getPath();
    std::cout << "Path: " << path << "\n";
    ASSERT_TRUE(path.find("heavy_mk1") != std::string::npos, "Path should contain node name");

    // Not found
    auto notFound = foundTanks->getChild("does_not_exist");
    ASSERT_TRUE(notFound == nullptr, "Should return nullptr for missing child");

    reporter.addAssertion("navigation_exact", true);
    std::cout << "✓ TEST 1 PASSED\n";

    // ========================================================================
    // TEST 2: Pattern Matching (Wildcards)
    // ========================================================================
    std::cout << "\n=== TEST 2: Pattern Matching ===\n";

    // Ajouter plus de nodes
    auto heavyMk2 = std::make_shared<JsonDataNode>("heavy_mk2", nlohmann::json{
        {"armor", 180},
        {"speed", 25},
        {"cost", 1200}
    });
    tanks->setChild("heavy_mk2", heavyMk2);

    auto infantry = std::make_shared<JsonDataNode>("infantry", nlohmann::json::object());
    auto heavyTrooper = std::make_shared<JsonDataNode>("heavy_trooper", nlohmann::json{
        {"armor", 120},
        {"speed", 15},
        {"cost", 500}
    });
    infantry->setChild("heavy_trooper", heavyTrooper);
    units->setChild("infantry", infantry);

    auto aircraft = std::make_shared<JsonDataNode>("aircraft", nlohmann::json::object());
    auto lightFighter = std::make_shared<JsonDataNode>("light_fighter", nlohmann::json{
        {"armor", 50},
        {"speed", 120},
        {"cost", 800}
    });
    aircraft->setChild("light_fighter", lightFighter);
    units->setChild("aircraft", aircraft);

    // Pattern: *heavy*
    auto heavyUnits = configRoot->getChildrenByNameMatch("*heavy*");
    std::cout << "Pattern '*heavy*' matched: " << heavyUnits.size() << " nodes\n";
    for (const auto& node : heavyUnits) {
        std::cout << "  - " << node->getName() << "\n";
    }
    // Should match: heavy_mk1, heavy_mk2, heavy_trooper
    ASSERT_EQ(heavyUnits.size(), 3, "Should match 3 'heavy' units");
    reporter.addMetric("pattern_heavy_count", heavyUnits.size());

    // Pattern: *_mk*
    auto mkUnits = configRoot->getChildrenByNameMatch("*_mk*");
    std::cout << "Pattern '*_mk*' matched: " << mkUnits.size() << " nodes\n";
    // Should match: heavy_mk1, heavy_mk2
    ASSERT_EQ(mkUnits.size(), 2, "Should match 2 '_mk' units");

    reporter.addAssertion("pattern_matching", true);
    std::cout << "✓ TEST 2 PASSED\n";

    // ========================================================================
    // TEST 3: Property-Based Queries
    // ========================================================================
    std::cout << "\n=== TEST 3: Property-Based Queries ===\n";

    // Query: armor > 100
    auto armoredUnits = configRoot->queryByProperty("armor",
        [](const IDataValue& val) {
            return val.isNumber() && val.asInt() >= 100;
        });

    std::cout << "Units with armor >= 100: " << armoredUnits.size() << "\n";
    for (const auto& node : armoredUnits) {
        int armor = node->getInt("armor");
        std::cout << "  - " << node->getName() << " (armor=" << armor << ")\n";
        ASSERT_GE(armor, 100, "Armor should be >= 100");
    }
    // Should match: heavy_mk1 (150), heavy_mk2 (180), heavy_trooper (120)
    ASSERT_EQ(armoredUnits.size(), 3, "Should find 3 armored units");

    // Query: speed > 50
    auto fastUnits = configRoot->queryByProperty("speed",
        [](const IDataValue& val) {
            return val.isNumber() && val.asInt() > 50;
        });

    std::cout << "Units with speed > 50: " << fastUnits.size() << "\n";
    // Should match: light_fighter (120)
    ASSERT_EQ(fastUnits.size(), 1, "Should find 1 fast unit");

    reporter.addAssertion("property_queries", true);
    std::cout << "✓ TEST 3 PASSED\n";

    // ========================================================================
    // TEST 4: Hot-Reload System
    // ========================================================================
    std::cout << "\n=== TEST 4: Hot-Reload System ===\n";

    // Créer fichier config
    nlohmann::json gameplayConfig = {
        {"difficulty", "normal"},
        {"maxPlayers", 4},
        {"timeLimit", 3600}
    };

    std::ofstream configFile("test_data/config/gameplay.json");
    configFile << gameplayConfig.dump(2);
    configFile.close();

    // Charger dans tree
    tree->loadConfigFile("gameplay.json");

    // Setup callback
    int callbackCount = 0;
    tree->onTreeReloaded([&callbackCount]() {
        callbackCount++;
        std::cout << "  → Reload callback triggered (count=" << callbackCount << ")\n";
    });

    // Vérifier contenu initial
    auto gameplay = configRoot->getChild("gameplay");
    ASSERT_TRUE(gameplay != nullptr, "gameplay node should exist");
    std::string difficulty = gameplay->getString("difficulty");
    ASSERT_EQ(difficulty, "normal", "Initial difficulty should be 'normal'");

    std::cout << "Initial difficulty: " << difficulty << "\n";

    // Modifier fichier
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    gameplayConfig["difficulty"] = "hard";
    gameplayConfig["maxPlayers"] = 8;

    std::ofstream configFile2("test_data/config/gameplay.json");
    configFile2 << gameplayConfig.dump(2);
    configFile2.close();

    // Force file timestamp update
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    // Check for changes
    bool hasChanges = tree->checkForChanges();
    std::cout << "Has changes: " << (hasChanges ? "YES" : "NO") << "\n";
    ASSERT_TRUE(hasChanges, "Should detect file modification");

    // Reload
    bool reloaded = tree->reloadIfChanged();
    std::cout << "Reloaded: " << (reloaded ? "YES" : "NO") << "\n";
    ASSERT_TRUE(reloaded, "Should reload changed file");

    // Vérifier callback
    ASSERT_EQ(callbackCount, 1, "Callback should be called exactly once");

    // Vérifier nouvelles valeurs
    gameplay = configRoot->getChild("gameplay");
    difficulty = gameplay->getString("difficulty");
    int maxPlayers = gameplay->getInt("maxPlayers");

    std::cout << "After reload - difficulty: " << difficulty << ", maxPlayers: " << maxPlayers << "\n";
    ASSERT_EQ(difficulty, "hard", "Difficulty should be updated to 'hard'");
    ASSERT_EQ(maxPlayers, 8, "maxPlayers should be updated to 8");

    reporter.addAssertion("hot_reload", true);
    reporter.addMetric("reload_callback_count", callbackCount);
    std::cout << "✓ TEST 4 PASSED\n";

    // ========================================================================
    // TEST 5: Hot-Reload Isolation
    // ========================================================================
    std::cout << "\n=== TEST 5: Hot-Reload Isolation ===\n";

    // Créer second fichier
    nlohmann::json mapsConfig = {
        {"defaultMap", "desert"},
        {"mapCount", 10}
    };

    std::ofstream mapsFile("test_data/config/maps.json");
    mapsFile << mapsConfig.dump(2);
    mapsFile.close();

    tree->loadConfigFile("maps.json");

    // Modifier seulement gameplay.json
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    gameplayConfig["difficulty"] = "extreme";

    std::ofstream configFile3("test_data/config/gameplay.json");
    configFile3 << gameplayConfig.dump(2);
    configFile3.close();

    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    // Check changes
    hasChanges = tree->checkForChanges();
    ASSERT_TRUE(hasChanges, "Should detect gameplay.json change");

    // Verify maps.json not affected
    auto maps = configRoot->getChild("maps");
    std::string defaultMap = maps->getString("defaultMap");
    ASSERT_EQ(defaultMap, "desert", "maps.json should not be affected");

    reloaded = tree->reloadIfChanged();
    ASSERT_TRUE(reloaded, "Should reload only changed file");

    // Verify maps still intact
    maps = configRoot->getChild("maps");
    defaultMap = maps->getString("defaultMap");
    ASSERT_EQ(defaultMap, "desert", "maps.json should still be 'desert' after isolated reload");

    reporter.addAssertion("reload_isolation", true);
    std::cout << "✓ TEST 5 PASSED\n";

    // ========================================================================
    // TEST 6: Persistence (Save/Load)
    // ========================================================================
    std::cout << "\n=== TEST 6: Persistence (Save/Load) ===\n";

    auto dataRoot = tree->getDataRoot();

    // Créer structure data/
    auto player = std::make_shared<JsonDataNode>("player", nlohmann::json::object());
    auto stats = std::make_shared<JsonDataNode>("stats", nlohmann::json{
        {"kills", 42},
        {"deaths", 3},
        {"level", 15}
    });
    auto inventory = std::make_shared<JsonDataNode>("inventory", nlohmann::json{
        {"gold", 1000},
        {"items", nlohmann::json::array({"sword", "shield", "potion"})}
    });

    player->setChild("stats", stats);
    player->setChild("inventory", inventory);
    dataRoot->setChild("player", player);

    auto world = std::make_shared<JsonDataNode>("world", nlohmann::json::object());
    auto time = std::make_shared<JsonDataNode>("time", nlohmann::json{
        {"day", 5},
        {"hour", 14},
        {"minute", 30}
    });
    world->setChild("time", time);
    dataRoot->setChild("world", world);

    // Save all data
    tree->saveData();

    // Vérifier fichiers créés
    ASSERT_TRUE(std::filesystem::exists("test_data/data/player/stats.json"),
                "stats.json should exist");
    ASSERT_TRUE(std::filesystem::exists("test_data/data/player/inventory.json"),
                "inventory.json should exist");
    ASSERT_TRUE(std::filesystem::exists("test_data/data/world/time.json"),
                "time.json should exist");

    std::cout << "Files saved successfully\n";

    // Créer nouveau tree et charger
    auto tree2 = std::make_shared<JsonDataTree>("test_data");
    tree2->loadDataDirectory();

    auto dataRoot2 = tree2->getDataRoot();
    auto player2 = dataRoot2->getChild("player");
    ASSERT_TRUE(player2 != nullptr, "player node should load");

    auto stats2 = player2->getChild("stats");
    int kills = stats2->getInt("kills");
    int deaths = stats2->getInt("deaths");

    std::cout << "Loaded: kills=" << kills << ", deaths=" << deaths << "\n";
    ASSERT_EQ(kills, 42, "kills should be preserved");
    ASSERT_EQ(deaths, 3, "deaths should be preserved");

    reporter.addAssertion("persistence", true);
    std::cout << "✓ TEST 6 PASSED\n";

    // ========================================================================
    // TEST 7: Selective Save
    // ========================================================================
    std::cout << "\n=== TEST 7: Selective Save ===\n";

    // Get mtime of inventory.json before
    auto inventoryPath = std::filesystem::path("test_data/data/player/inventory.json");
    auto mtimeBefore = std::filesystem::last_write_time(inventoryPath);

    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    // Modify only stats
    stats->setInt("kills", 100);

    // Save only stats
    tree->saveNode("data/player/stats");

    // Check inventory.json not modified
    auto mtimeAfter = std::filesystem::last_write_time(inventoryPath);

    ASSERT_EQ(mtimeBefore, mtimeAfter, "inventory.json should not be modified");

    // Load stats and verify
    auto tree3 = std::make_shared<JsonDataTree>("test_data");
    tree3->loadDataDirectory();
    auto stats3 = tree3->getDataRoot()->getChild("player")->getChild("stats");
    int newKills = stats3->getInt("kills");

    ASSERT_EQ(newKills, 100, "Selective save should update only stats");

    reporter.addAssertion("selective_save", true);
    std::cout << "✓ TEST 7 PASSED\n";

    // ========================================================================
    // TEST 8: Hash System (Data Hash)
    // ========================================================================
    std::cout << "\n=== TEST 8: Hash System (Data Hash) ===\n";

    auto testNode = std::make_shared<JsonDataNode>("test", nlohmann::json{
        {"value", 42}
    });

    std::string hash1 = testNode->getDataHash();
    std::cout << "Hash 1: " << hash1.substr(0, 16) << "...\n";

    // Modify data
    testNode->setInt("value", 43);

    std::string hash2 = testNode->getDataHash();
    std::cout << "Hash 2: " << hash2.substr(0, 16) << "...\n";

    ASSERT_TRUE(hash1 != hash2, "Hashes should differ after data change");

    reporter.addAssertion("data_hash", true);
    std::cout << "✓ TEST 8 PASSED\n";

    // ========================================================================
    // TEST 9: Hash System (Tree Hash)
    // ========================================================================
    std::cout << "\n=== TEST 9: Hash System (Tree Hash) ===\n";

    auto root = std::make_shared<JsonDataNode>("root", nlohmann::json::object());
    auto child1 = std::make_shared<JsonDataNode>("child1", nlohmann::json{{"data", 1}});
    auto child2 = std::make_shared<JsonDataNode>("child2", nlohmann::json{{"data", 2}});

    root->setChild("child1", child1);
    root->setChild("child2", child2);

    std::string treeHash1 = root->getTreeHash();
    std::cout << "Tree Hash 1: " << treeHash1.substr(0, 16) << "...\n";

    // Modify child1
    child1->setInt("data", 999);

    std::string treeHash2 = root->getTreeHash();
    std::cout << "Tree Hash 2: " << treeHash2.substr(0, 16) << "...\n";

    ASSERT_TRUE(treeHash1 != treeHash2, "Tree hash should change when child changes");

    reporter.addAssertion("tree_hash", true);
    std::cout << "✓ TEST 9 PASSED\n";

    // ========================================================================
    // TEST 10: Read-Only Enforcement
    // ========================================================================
    std::cout << "\n=== TEST 10: Read-Only Enforcement ===\n";

    auto readOnlyNode = configRoot->getChild("gameplay");

    bool exceptionThrown = false;
    try {
        auto newChild = std::make_shared<JsonDataNode>("illegal", nlohmann::json{{"bad", true}});
        readOnlyNode->setChild("illegal", newChild);
    } catch (const std::runtime_error& e) {
        std::cout << "✓ Exception thrown: " << e.what() << "\n";
        exceptionThrown = true;
    }

    ASSERT_TRUE(exceptionThrown, "Should throw exception when modifying read-only node");

    reporter.addAssertion("readonly_enforcement", true);
    std::cout << "✓ TEST 10 PASSED\n";

    // ========================================================================
    // TEST 11: Type Safety & Defaults
    // ========================================================================
    std::cout << "\n=== TEST 11: Type Safety & Defaults ===\n";

    auto typeNode = std::make_shared<JsonDataNode>("types", nlohmann::json{
        {"armor", 150},
        {"name", "Tank"},
        {"active", true},
        {"speed", 30.5}
    });

    int armor = typeNode->getInt("armor");
    ASSERT_EQ(armor, 150, "getInt should return correct value");

    int missing = typeNode->getInt("missing", 100);
    ASSERT_EQ(missing, 100, "getInt with default should return default");

    std::string name = typeNode->getString("name");
    ASSERT_EQ(name, "Tank", "getString should return correct value");

    bool active = typeNode->getBool("active");
    ASSERT_EQ(active, true, "getBool should return correct value");

    bool defaultBool = typeNode->getBool("nothere", false);
    ASSERT_EQ(defaultBool, false, "getBool with default should return default");

    double speed = typeNode->getDouble("speed");
    ASSERT_EQ(speed, 30.5, "getDouble should return correct value");

    reporter.addAssertion("type_safety", true);
    std::cout << "✓ TEST 11 PASSED\n";

    // ========================================================================
    // TEST 12: Deep Tree Performance
    // ========================================================================
    std::cout << "\n=== TEST 12: Deep Tree Performance ===\n";

    auto perfRoot = std::make_shared<JsonDataNode>("perf", nlohmann::json::object());

    // Create 1000 nodes: 10 x 10 x 10
    int nodeCount = 0;
    for (int cat = 0; cat < 10; cat++) {
        auto category = std::make_shared<JsonDataNode>("cat_" + std::to_string(cat),
                                                        nlohmann::json::object());

        for (int sub = 0; sub < 10; sub++) {
            auto subcategory = std::make_shared<JsonDataNode>("sub_" + std::to_string(sub),
                                                               nlohmann::json::object());

            for (int item = 0; item < 10; item++) {
                auto itemNode = std::make_shared<JsonDataNode>("item_" + std::to_string(item),
                                                                nlohmann::json{
                                                                    {"id", nodeCount},
                                                                    {"value", nodeCount * 10}
                                                                });
                subcategory->setChild("item_" + std::to_string(item), itemNode);
                nodeCount++;
            }

            category->setChild("sub_" + std::to_string(sub), subcategory);
        }

        perfRoot->setChild("cat_" + std::to_string(cat), category);
    }

    std::cout << "Created " << nodeCount << " nodes\n";
    ASSERT_EQ(nodeCount, 1000, "Should create 1000 nodes");

    // Pattern matching: find all items
    auto start = std::chrono::high_resolution_clock::now();
    auto allItems = perfRoot->getChildrenByNameMatch("item_*");
    auto end = std::chrono::high_resolution_clock::now();

    float patternTime = std::chrono::duration<float, std::milli>(end - start).count();
    std::cout << "Pattern matching found " << allItems.size() << " items in " << patternTime << "ms\n";

    ASSERT_EQ(allItems.size(), 1000, "Should find all 1000 items");
    ASSERT_LT(patternTime, 100.0f, "Pattern matching should be < 100ms");
    reporter.addMetric("pattern_time_ms", patternTime);

    // Query by property
    start = std::chrono::high_resolution_clock::now();
    auto queryResults = perfRoot->queryByProperty("value",
        [](const IDataValue& val) {
            return val.isNumber() && val.asInt() > 5000;
        });
    end = std::chrono::high_resolution_clock::now();

    float queryTime = std::chrono::duration<float, std::milli>(end - start).count();
    std::cout << "Query found " << queryResults.size() << " results in " << queryTime << "ms\n";

    ASSERT_LT(queryTime, 50.0f, "Query should be < 50ms");
    reporter.addMetric("query_time_ms", queryTime);

    // Tree hash
    start = std::chrono::high_resolution_clock::now();
    std::string treeHash = perfRoot->getTreeHash();
    end = std::chrono::high_resolution_clock::now();

    float hashTime = std::chrono::duration<float, std::milli>(end - start).count();
    std::cout << "Tree hash calculated in " << hashTime << "ms\n";

    ASSERT_LT(hashTime, 200.0f, "Tree hash should be < 200ms");
    reporter.addMetric("treehash_time_ms", hashTime);

    reporter.addAssertion("performance", true);
    std::cout << "✓ TEST 12 PASSED\n";

    // ========================================================================
    // CLEANUP
    // ========================================================================
    std::filesystem::remove_all("test_data");

    // ========================================================================
    // RAPPORT FINAL
    // ========================================================================

    metrics.printReport();
    reporter.printFinalReport();

    return reporter.getExitCode();
}

📊 Métriques Collectées

Métrique Description Seuil
pattern_heavy_count Nodes matchés par pattern "heavy" 3
reload_callback_count Callbacks déclenchés lors reload 1
pattern_time_ms Temps pattern matching 1000 nodes < 100ms
query_time_ms Temps property query 1000 nodes < 50ms
treehash_time_ms Temps calcul tree hash 1000 nodes < 200ms

Critères de Succès

MUST PASS

  1. Navigation exacte fonctionne (getChild, getPath)
  2. Pattern matching trouve tous les matches
  3. Property queries retournent résultats corrects
  4. Hot-reload détecte changements fichier
  5. Hot-reload callback déclenché
  6. Hot-reload isolation (un fichier modifié n'affecte pas autres)
  7. Persistence save/load préserve data
  8. Selective save modifie seulement node ciblé
  9. Data hash change quand data modifié
  10. Tree hash change quand children modifiés
  11. Read-only nodes throw exception si modifiés
  12. Type access avec defaults fonctionne
  13. Performance acceptable sur 1000 nodes

NICE TO HAVE

  1. Pattern matching < 50ms (optimal)
  2. Query < 25ms (optimal)
  3. Tree hash < 100ms (optimal)

🐛 Cas d'Erreur Attendus

Erreur Cause Action
Pattern pas match Regex incorrecte FAIL - fix wildcard conversion
Query vide Predicate trop strict WARN - vérifier logique
Hot-reload pas détecté File watch bug FAIL - fix checkForChanges()
Callback pas appelé onTreeReloaded bug FAIL - fix callback system
Persistence data corrompu JSON malformé FAIL - add validation
Hash identiques Hash calculation bug FAIL - fix getDataHash()
Read-only pas enforced isReadOnly check manquant FAIL - add check
Type mismatch crash Pas de default handling FAIL - add try/catch
Performance > seuils Algorithme O(n²) FAIL - optimize

📝 Output Attendu

================================================================================
TEST: DataNode Integration Test
================================================================================

=== TEST 1: Tree Navigation & Exact Matching ===
Path: config/units/tanks/heavy_mk1
✓ TEST 1 PASSED

=== TEST 2: Pattern Matching ===
Pattern '*heavy*' matched: 3 nodes
  - heavy_mk1
  - heavy_mk2
  - heavy_trooper
Pattern '*_mk*' matched: 2 nodes
✓ TEST 2 PASSED

=== TEST 3: Property-Based Queries ===
Units with armor >= 100: 3
  - heavy_mk1 (armor=150)
  - heavy_mk2 (armor=180)
  - heavy_trooper (armor=120)
Units with speed > 50: 1
✓ TEST 3 PASSED

=== TEST 4: Hot-Reload System ===
Initial difficulty: normal
Has changes: YES
Reloaded: YES
  → Reload callback triggered (count=1)
After reload - difficulty: hard, maxPlayers: 8
✓ TEST 4 PASSED

=== TEST 5: Hot-Reload Isolation ===
✓ TEST 5 PASSED

=== TEST 6: Persistence (Save/Load) ===
Files saved successfully
Loaded: kills=42, deaths=3
✓ TEST 6 PASSED

=== TEST 7: Selective Save ===
✓ TEST 7 PASSED

=== TEST 8: Hash System (Data Hash) ===
Hash 1: 5d41402abc4b2a76...
Hash 2: 7c6a180b36896a0e...
✓ TEST 8 PASSED

=== TEST 9: Hash System (Tree Hash) ===
Tree Hash 1: a1b2c3d4e5f6g7h8...
Tree Hash 2: 9i8j7k6l5m4n3o2p...
✓ TEST 9 PASSED

=== TEST 10: Read-Only Enforcement ===
✓ Exception thrown: Cannot modify read-only node 'gameplay'
✓ TEST 10 PASSED

=== TEST 11: Type Safety & Defaults ===
✓ TEST 11 PASSED

=== TEST 12: Deep Tree Performance ===
Created 1000 nodes
Pattern matching found 1000 items in 45.3ms
Query found 500 results in 23.7ms
Tree hash calculated in 134.2ms
✓ TEST 12 PASSED

================================================================================
METRICS
================================================================================
  Pattern heavy count:     3
  Reload callback count:   1
  Pattern time:            45.3ms         (threshold: < 100ms) ✓
  Query time:              23.7ms         (threshold: < 50ms)  ✓
  Tree hash time:          134.2ms        (threshold: < 200ms) ✓

================================================================================
ASSERTIONS
================================================================================
  ✓ navigation_exact
  ✓ pattern_matching
  ✓ property_queries
  ✓ hot_reload
  ✓ reload_isolation
  ✓ persistence
  ✓ selective_save
  ✓ data_hash
  ✓ tree_hash
  ✓ readonly_enforcement
  ✓ type_safety
  ✓ performance

Result: ✅ PASSED (12/12 tests)

================================================================================

📅 Planning

Jour 1 (4h):

  • Setup JsonDataTree avec test directory
  • Implémenter tests 1-6 (navigation, patterns, queries, hot-reload, persistence)

Jour 2 (3h):

  • Implémenter tests 7-12 (selective save, hashes, readonly, types, performance)
  • Debug + validation

Prochaine étape: scenario_13_cross_system.md