mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-25 22:31:46 -03:00
v2.3.6: fix UUID loss during launcher updates
Players were losing character data (inventory, armor, backpack) after each launcher update because config.json corruption wiped the UUID mapping. Same username, new UUID = server treats as new player. Fix: UUIDs now stored in separate uuid-store.json that saveConfig() can never touch. Added safety check to refuse destructive writes when config file exists but loads empty. Includes 28 regression tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
523
test-uuid-persistence.js
Normal file
523
test-uuid-persistence.js
Normal file
@@ -0,0 +1,523 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* UUID Persistence Tests
|
||||
*
|
||||
* Simulates the exact conditions that caused character data loss:
|
||||
* - Config file corruption during updates
|
||||
* - File locks making config temporarily unreadable
|
||||
* - Username re-entry after config wipe
|
||||
*
|
||||
* Run: node test-uuid-persistence.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Use a temp directory so we don't mess with real config
|
||||
const TEST_DIR = path.join(os.tmpdir(), 'hytale-uuid-test-' + Date.now());
|
||||
const CONFIG_FILE = path.join(TEST_DIR, 'config.json');
|
||||
const CONFIG_BACKUP = path.join(TEST_DIR, 'config.json.bak');
|
||||
const CONFIG_TEMP = path.join(TEST_DIR, 'config.json.tmp');
|
||||
const UUID_STORE_FILE = path.join(TEST_DIR, 'uuid-store.json');
|
||||
|
||||
// Track test results
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const failures = [];
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
passed++;
|
||||
console.log(` ✓ ${message}`);
|
||||
} else {
|
||||
failed++;
|
||||
failures.push(message);
|
||||
console.log(` ✗ FAIL: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
if (actual === expected) {
|
||||
passed++;
|
||||
console.log(` ✓ ${message}`);
|
||||
} else {
|
||||
failed++;
|
||||
failures.push(`${message} (expected: ${expected}, got: ${actual})`);
|
||||
console.log(` ✗ FAIL: ${message} (expected: "${expected}", got: "${actual}")`);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
cleanup();
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Inline the config functions so we can override paths
|
||||
// (We can't require config.js directly because it uses hardcoded getAppDir())
|
||||
// ============================================================================
|
||||
|
||||
function validateConfig(config) {
|
||||
if (!config || typeof config !== 'object') return false;
|
||||
if (config.userUuids !== undefined && typeof config.userUuids !== 'object') return false;
|
||||
if (config.username !== undefined && (typeof config.username !== 'string')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
if (data.trim()) {
|
||||
const config = JSON.parse(data);
|
||||
if (validateConfig(config)) return config;
|
||||
console.warn('[Config] Primary config invalid structure, trying backup...');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Config] Failed to load primary config:', err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_BACKUP)) {
|
||||
const data = fs.readFileSync(CONFIG_BACKUP, 'utf8');
|
||||
if (data.trim()) {
|
||||
const config = JSON.parse(data);
|
||||
if (validateConfig(config)) {
|
||||
console.log('[Config] Recovered from backup successfully');
|
||||
try { fs.writeFileSync(CONFIG_FILE, data, 'utf8'); } catch (e) {}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveConfig(update) {
|
||||
const maxRetries = 3;
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (!fs.existsSync(TEST_DIR)) fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
|
||||
const currentConfig = loadConfig();
|
||||
|
||||
// SAFETY CHECK: refuse to save if file exists but loaded empty
|
||||
if (Object.keys(currentConfig).length === 0 && fs.existsSync(CONFIG_FILE)) {
|
||||
const fileSize = fs.statSync(CONFIG_FILE).size;
|
||||
if (fileSize > 2) {
|
||||
console.error(`[Config] REFUSING to save — loaded empty but file exists (${fileSize} bytes). Retrying...`);
|
||||
const delay = attempt * 50; // shorter delay for tests
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < delay) {}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const newConfig = { ...currentConfig, ...update };
|
||||
const data = JSON.stringify(newConfig, null, 2);
|
||||
|
||||
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
||||
const verification = JSON.parse(fs.readFileSync(CONFIG_TEMP, 'utf8'));
|
||||
if (!validateConfig(verification)) throw new Error('Validation failed');
|
||||
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
const currentData = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
if (currentData.trim()) fs.writeFileSync(CONFIG_BACKUP, currentData, 'utf8');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
||||
return true;
|
||||
} catch (err) {
|
||||
try { if (fs.existsSync(CONFIG_TEMP)) fs.unlinkSync(CONFIG_TEMP); } catch (e) {}
|
||||
if (attempt >= maxRetries) throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadUuidStore() {
|
||||
try {
|
||||
if (fs.existsSync(UUID_STORE_FILE)) {
|
||||
const data = fs.readFileSync(UUID_STORE_FILE, 'utf8');
|
||||
if (data.trim()) return JSON.parse(data);
|
||||
}
|
||||
} catch (err) {}
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveUuidStore(store) {
|
||||
const tmpFile = UUID_STORE_FILE + '.tmp';
|
||||
fs.writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf8');
|
||||
fs.renameSync(tmpFile, UUID_STORE_FILE);
|
||||
}
|
||||
|
||||
function migrateUuidStoreIfNeeded() {
|
||||
if (fs.existsSync(UUID_STORE_FILE)) return;
|
||||
const config = loadConfig();
|
||||
if (config.userUuids && Object.keys(config.userUuids).length > 0) {
|
||||
console.log('[UUID Store] Migrating', Object.keys(config.userUuids).length, 'UUIDs');
|
||||
saveUuidStore(config.userUuids);
|
||||
}
|
||||
}
|
||||
|
||||
function getUuidForUser(username) {
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
if (!username || !username.trim()) throw new Error('Username required');
|
||||
|
||||
const displayName = username.trim();
|
||||
const normalizedLookup = displayName.toLowerCase();
|
||||
|
||||
migrateUuidStoreIfNeeded();
|
||||
|
||||
// 1. Check UUID store (source of truth)
|
||||
const uuidStore = loadUuidStore();
|
||||
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
|
||||
if (storeKey) {
|
||||
const existingUuid = uuidStore[storeKey];
|
||||
if (storeKey !== displayName) {
|
||||
delete uuidStore[storeKey];
|
||||
uuidStore[displayName] = existingUuid;
|
||||
saveUuidStore(uuidStore);
|
||||
}
|
||||
// Sync to config (non-critical)
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const configUuids = config.userUuids || {};
|
||||
const configKey = Object.keys(configUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||
if (!configKey || configUuids[configKey] !== existingUuid) {
|
||||
if (configKey) delete configUuids[configKey];
|
||||
configUuids[displayName] = existingUuid;
|
||||
saveConfig({ userUuids: configUuids });
|
||||
}
|
||||
} catch (e) {}
|
||||
return existingUuid;
|
||||
}
|
||||
|
||||
// 2. Fallback: check config.json
|
||||
const config = loadConfig();
|
||||
const userUuids = config.userUuids || {};
|
||||
const configKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||
if (configKey) {
|
||||
const recoveredUuid = userUuids[configKey];
|
||||
uuidStore[displayName] = recoveredUuid;
|
||||
saveUuidStore(uuidStore);
|
||||
return recoveredUuid;
|
||||
}
|
||||
|
||||
// 3. New user — generate UUID
|
||||
const newUuid = uuidv4();
|
||||
uuidStore[displayName] = newUuid;
|
||||
saveUuidStore(uuidStore);
|
||||
userUuids[displayName] = newUuid;
|
||||
saveConfig({ userUuids });
|
||||
return newUuid;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OLD CODE (before fix) — for comparison testing
|
||||
// ============================================================================
|
||||
|
||||
function getUuidForUser_OLD(username) {
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
if (!username || !username.trim()) throw new Error('Username required');
|
||||
const displayName = username.trim();
|
||||
const normalizedLookup = displayName.toLowerCase();
|
||||
|
||||
const config = loadConfig();
|
||||
const userUuids = config.userUuids || {};
|
||||
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||
|
||||
if (existingKey) {
|
||||
return userUuids[existingKey];
|
||||
}
|
||||
|
||||
// New user
|
||||
const newUuid = uuidv4();
|
||||
userUuids[displayName] = newUuid;
|
||||
saveConfig({ userUuids });
|
||||
return newUuid;
|
||||
}
|
||||
|
||||
function saveConfig_OLD(update) {
|
||||
// OLD saveConfig without safety check
|
||||
if (!fs.existsSync(TEST_DIR)) fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
const currentConfig = loadConfig();
|
||||
// NO SAFETY CHECK — this is the bug
|
||||
const newConfig = { ...currentConfig, ...update };
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('UUID PERSISTENCE TESTS — Simulating update corruption scenarios');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 1: Normal flow — UUID stays consistent
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 1: Normal flow — UUID stays consistent ---');
|
||||
setup();
|
||||
|
||||
const uuid1 = getUuidForUser('SpecialK');
|
||||
const uuid2 = getUuidForUser('SpecialK');
|
||||
const uuid3 = getUuidForUser('specialk'); // case insensitive
|
||||
|
||||
assertEqual(uuid1, uuid2, 'Same username returns same UUID');
|
||||
assertEqual(uuid1, uuid3, 'Case-insensitive lookup returns same UUID');
|
||||
assert(uuid1.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i), 'UUID is valid v4 format');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 2: Simulate update corruption (THE BUG) — old code
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 2: OLD CODE — Config wipe during update loses UUID ---');
|
||||
setup();
|
||||
|
||||
// Setup: player has UUID
|
||||
const oldConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true };
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(oldConfig, null, 2), 'utf8');
|
||||
|
||||
const uuidBefore = getUuidForUser_OLD('SpecialK');
|
||||
assertEqual(uuidBefore, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID correct before corruption');
|
||||
|
||||
// Simulate: config.json gets corrupted (loadConfig returns {} because file locked)
|
||||
// This simulates what happens when saveConfig reads an empty/locked file
|
||||
fs.writeFileSync(CONFIG_FILE, '', 'utf8'); // Simulate corruption: empty file
|
||||
|
||||
// Old saveConfig behavior: reads empty, merges with update, saves
|
||||
// This wipes userUuids
|
||||
saveConfig_OLD({ hasLaunchedBefore: true });
|
||||
|
||||
const configAfterCorruption = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||
assert(!configAfterCorruption.userUuids, 'OLD CODE: userUuids wiped after corruption');
|
||||
assert(!configAfterCorruption.username, 'OLD CODE: username wiped after corruption');
|
||||
|
||||
// Player re-enters name, gets NEW UUID (character data lost!)
|
||||
const uuidAfterOld = getUuidForUser_OLD('SpecialK');
|
||||
assert(uuidAfterOld !== uuidBefore, 'OLD CODE: UUID changed after corruption (BUG!)');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 3: NEW CODE — Config wipe during update, UUID survives via uuid-store
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 3: NEW CODE — Config wipe + UUID survives via uuid-store ---');
|
||||
setup();
|
||||
|
||||
// Setup: player has UUID (stored in both config.json AND uuid-store.json)
|
||||
const initialConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true };
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(initialConfig, null, 2), 'utf8');
|
||||
|
||||
// First call migrates to uuid-store
|
||||
const uuidFirst = getUuidForUser('SpecialK');
|
||||
assertEqual(uuidFirst, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID correct before corruption');
|
||||
assert(fs.existsSync(UUID_STORE_FILE), 'uuid-store.json created');
|
||||
|
||||
// Simulate: config.json gets wiped (same as the update bug)
|
||||
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');
|
||||
|
||||
// Verify config is empty
|
||||
const wipedConfig = loadConfig();
|
||||
assert(!wipedConfig.userUuids || Object.keys(wipedConfig.userUuids).length === 0, 'Config wiped — no userUuids');
|
||||
assert(!wipedConfig.username, 'Config wiped — no username');
|
||||
|
||||
// Player re-enters same name → UUID recovered from uuid-store!
|
||||
const uuidAfterNew = getUuidForUser('SpecialK');
|
||||
assertEqual(uuidAfterNew, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'NEW CODE: UUID preserved after config wipe!');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 4: saveConfig safety check — refuses to overwrite good data with empty
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 4: saveConfig safety check — blocks destructive writes ---');
|
||||
setup();
|
||||
|
||||
// Setup: valid config file with data
|
||||
const goodConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true, installPath: 'C:\\Games\\Hytale' };
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(goodConfig, null, 2), 'utf8');
|
||||
|
||||
// Make the file temporarily unreadable by writing garbage (simulates file lock/corruption)
|
||||
const originalContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
fs.writeFileSync(CONFIG_FILE, 'NOT VALID JSON!!!', 'utf8');
|
||||
|
||||
// Try to save — should refuse because file exists but can't be parsed
|
||||
let saveThrew = false;
|
||||
try {
|
||||
saveConfig({ someNewField: true });
|
||||
} catch (e) {
|
||||
saveThrew = true;
|
||||
}
|
||||
|
||||
// The file should still have the garbage (not overwritten with { someNewField: true })
|
||||
const afterContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
|
||||
// Restore original for backup recovery test
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(goodConfig, null, 2), 'utf8');
|
||||
|
||||
// Note: with invalid JSON, loadConfig returns {} and safety check triggers
|
||||
// The save may eventually succeed on retry if the file becomes readable
|
||||
// What matters is that it doesn't blindly overwrite
|
||||
assert(afterContent !== '{\n "someNewField": true\n}', 'Safety check prevented blind overwrite of corrupted file');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 5: Backup recovery — config.json corrupted, recovered from .bak
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 5: Backup recovery — auto-recover from .bak ---');
|
||||
setup();
|
||||
|
||||
// Create config and backup
|
||||
const validConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' } };
|
||||
fs.writeFileSync(CONFIG_BACKUP, JSON.stringify(validConfig, null, 2), 'utf8');
|
||||
fs.writeFileSync(CONFIG_FILE, 'CORRUPTED', 'utf8');
|
||||
|
||||
const recovered = loadConfig();
|
||||
assertEqual(recovered.username, 'SpecialK', 'Username recovered from backup');
|
||||
assert(recovered.userUuids && recovered.userUuids['SpecialK'] === 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID recovered from backup');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 6: Full update simulation — the exact scenario from player report
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 6: Full update simulation (player report scenario) ---');
|
||||
setup();
|
||||
|
||||
// Step 1: Player installs v2.3.4, sets username, plays game
|
||||
console.log(' Step 1: Player sets up profile...');
|
||||
saveConfig({ username: 'Special K', hasLaunchedBefore: true });
|
||||
const originalUuid = getUuidForUser('Special K');
|
||||
console.log(` Original UUID: ${originalUuid}`);
|
||||
|
||||
// Step 2: v2.3.5 auto-update — new app launches
|
||||
console.log(' Step 2: Simulating v2.3.5 update...');
|
||||
|
||||
// Simulate the 3 saveConfig calls that happen during startup
|
||||
// But first, simulate config being temporarily locked (returns empty)
|
||||
const preUpdateContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
fs.writeFileSync(CONFIG_FILE, '', 'utf8'); // Simulate: file empty during write (race condition)
|
||||
|
||||
// These are the 3 calls from: profileManager.init, migrateUserDataToCentralized, handleFirstLaunchCheck
|
||||
// With our safety check, they should NOT wipe the data
|
||||
try { saveConfig({ hasLaunchedBefore: true }); } catch (e) { /* expected — safety check blocks it */ }
|
||||
|
||||
// Simulate file becomes readable again (antivirus releases lock)
|
||||
fs.writeFileSync(CONFIG_FILE, preUpdateContent, 'utf8');
|
||||
|
||||
// Step 3: Player re-enters username (because UI might show empty)
|
||||
console.log(' Step 3: Player re-enters username...');
|
||||
const postUpdateUuid = getUuidForUser('Special K');
|
||||
console.log(` Post-update UUID: ${postUpdateUuid}`);
|
||||
|
||||
assertEqual(postUpdateUuid, originalUuid, 'UUID survived the full update cycle!');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 7: Multiple users — UUIDs stay independent
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 7: Multiple users — UUIDs stay independent ---');
|
||||
setup();
|
||||
|
||||
const uuidAlice = getUuidForUser('Alice');
|
||||
const uuidBob = getUuidForUser('Bob');
|
||||
const uuidCharlie = getUuidForUser('Charlie');
|
||||
|
||||
assert(uuidAlice !== uuidBob, 'Alice and Bob have different UUIDs');
|
||||
assert(uuidBob !== uuidCharlie, 'Bob and Charlie have different UUIDs');
|
||||
|
||||
// Wipe config, all should survive
|
||||
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');
|
||||
|
||||
assertEqual(getUuidForUser('Alice'), uuidAlice, 'Alice UUID survived config wipe');
|
||||
assertEqual(getUuidForUser('Bob'), uuidBob, 'Bob UUID survived config wipe');
|
||||
assertEqual(getUuidForUser('Charlie'), uuidCharlie, 'Charlie UUID survived config wipe');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 8: UUID store deleted — recovery from config.json
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 8: UUID store deleted — recovery from config.json ---');
|
||||
setup();
|
||||
|
||||
// Create UUID via normal flow (saves to both stores)
|
||||
const uuidOriginal = getUuidForUser('TestPlayer');
|
||||
|
||||
// Delete uuid-store.json (simulates user manually deleting it or disk issue)
|
||||
fs.unlinkSync(UUID_STORE_FILE);
|
||||
assert(!fs.existsSync(UUID_STORE_FILE), 'uuid-store.json deleted');
|
||||
|
||||
// UUID should be recovered from config.json
|
||||
const uuidRecovered = getUuidForUser('TestPlayer');
|
||||
assertEqual(uuidRecovered, uuidOriginal, 'UUID recovered from config.json after uuid-store deletion');
|
||||
assert(fs.existsSync(UUID_STORE_FILE), 'uuid-store.json recreated after recovery');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 9: Both stores deleted — new UUID generated (fresh install)
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 9: Both stores deleted — new UUID (fresh install) ---');
|
||||
setup();
|
||||
|
||||
const uuidFresh = getUuidForUser('NewPlayer');
|
||||
|
||||
// Delete both
|
||||
fs.unlinkSync(UUID_STORE_FILE);
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
|
||||
const uuidAfterWipe = getUuidForUser('NewPlayer');
|
||||
assert(uuidAfterWipe !== uuidFresh, 'New UUID generated when both stores are gone (expected for true fresh install)');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TEST 10: Worst case — config.json wiped AND uuid-store.json exists
|
||||
// Simulates the EXACT player-reported scenario with new code
|
||||
// --------------------------------------------------------------------------
|
||||
console.log('\n--- Test 10: Exact player scenario with new code ---');
|
||||
setup();
|
||||
|
||||
// Player has been playing for a while
|
||||
saveConfig({
|
||||
username: 'Special K',
|
||||
hasLaunchedBefore: true,
|
||||
installPath: 'C:\\Games\\Hytale',
|
||||
version_client: '2026.02.19-1a311a592',
|
||||
version_branch: 'release',
|
||||
userUuids: { 'Special K': '11111111-2222-4333-9444-555555555555' }
|
||||
});
|
||||
|
||||
// First call creates uuid-store.json
|
||||
const originalUuid10 = getUuidForUser('Special K');
|
||||
assertEqual(originalUuid10, '11111111-2222-4333-9444-555555555555', 'Original UUID loaded');
|
||||
|
||||
// BOOM: Update happens, config.json completely wiped
|
||||
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');
|
||||
|
||||
// Username lost — player has to re-enter
|
||||
const loadedUsername = loadConfig().username;
|
||||
assert(!loadedUsername, 'Username is gone from config (simulating what player saw)');
|
||||
|
||||
// Player types "Special K" again in settings
|
||||
saveConfig({ username: 'Special K' });
|
||||
|
||||
// Player clicks Play — getUuidForUser called
|
||||
const recoveredUuid10 = getUuidForUser('Special K');
|
||||
assertEqual(recoveredUuid10, '11111111-2222-4333-9444-555555555555', 'UUID recovered — character data preserved!');
|
||||
|
||||
// ============================================================================
|
||||
// RESULTS
|
||||
// ============================================================================
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) {
|
||||
console.log('\nFailures:');
|
||||
failures.forEach(f => console.log(` ✗ ${f}`));
|
||||
}
|
||||
console.log('='.repeat(70));
|
||||
|
||||
cleanup();
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user