mudlet/test/functional_tests/ExperiencedPlayerGateTest.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

354 lines
16 KiB
C++
Raw Permalink Normal View History

fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
/***************************************************************************
* Copyright (C) 2026 by Vadim Peretokin - vadim.peretokin@mudlet.org *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
/*
* Locks in who Mudlet considers an experienced player. That decision gates all
* of the first-time guidance - the interface tour, the starter UI package and
* the one-line hints - so getting it wrong either buries a newcomer's
* onboarding or drops a beginner tour on top of a ten-year veteran's session.
*
* mudlet::rememberFirstLaunch() and mudlet::evaluateExperiencedPlayer() take
* their settings, profiles path and "now" as arguments, so most cases run
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
* without a mudlet instance. The last two drive a real init() instead, to pin
* the production wiring.
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
*
* Run with: ctest -R ExperiencedPlayerGateTest -V
*/
#include <QtTest/QtTest>
#include <QTimeZone>
#include "MudletInstanceCoordinator.h"
#include "mudlet.h"
// init() reads compiled-in resources, which are not registered automatically in
// a test binary that links mudlet_core statically
extern void qInitResources_mudlet();
extern void qInitResources_qm();
extern void qInitResources_additional_splash_screens();
extern void qInitResources_mudlet_fonts_common();
extern void qInitResources_mudlet_fonts_posix();
void initializeQRCResourcesForExperiencedPlayerGateTest();
class ExperiencedPlayerGateTest : public QObject
{
Q_OBJECT
private:
QByteArray mSavedXdg;
// Outlives the two live-singleton cases, which share one mudlet instance
QTemporaryDir mLiveConfig;
// Fixed, so the six month arithmetic does not depend on the day the suite runs
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
const QDateTime mNow = QDateTime(QDate(2026, 8, 5), QTime(12, 0), QTimeZone::UTC);
const QString mKey = qsl("firstLaunchDate");
QString profilesPathIn(const QString& configDir) const { return qsl("%1/profiles").arg(configDir); }
QString iniIn(const QString& configDir) const { return qsl("%1/Mudlet.ini").arg(configDir); }
QString makeProfile(const QString& configDir, const QString& name) const
{
const QString path = qsl("%1/%2").arg(profilesPathIn(configDir), name);
return QDir().mkpath(path) ? path : QString();
}
void setFirstLaunch(QSettings& settings, const QDateTime& when) const { settings.setValue(mKey, when.toUTC().toString(Qt::ISODate)); }
// setupConfig() consults portable.txt before the XDG logic
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
bool portableMarkerPresent() const
{
return QFileInfo::exists(qsl("%1/portable.txt").arg(QCoreApplication::applicationDirPath())) || QFileInfo::exists(qsl("%1/.config/mudlet/portable.txt").arg(QDir::homePath()));
}
private slots:
void initTestCase() { mSavedXdg = qgetenv("XDG_CONFIG_HOME"); }
void cleanupTestCase()
{
if (mudlet::self()) {
delete mudlet::self();
}
mSavedXdg.isNull() ? qunsetenv("XDG_CONFIG_HOME") : qputenv("XDG_CONFIG_HOME", mSavedXdg);
}
// --- a brand new installation ---
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
void test_freshInstallRecordsTodayAndIsNew()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
QVERIFY(!QDir(profilesPathIn(config.path())).exists());
mudlet::rememberFirstLaunch(settings, profilesPathIn(config.path()), mNow);
QCOMPARE(settings.value(mKey).toString(), mNow.toString(Qt::ISODate));
QVERIFY2(!mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow), "a first-ever launch must be treated as a new player");
}
void test_emptyProfilesDirectoryIsStillAFirstRun()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QVERIFY(QDir().mkpath(profilesPathIn(config.path())));
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
mudlet::rememberFirstLaunch(settings, profilesPathIn(config.path()), mNow);
QVERIFY(settings.contains(mKey));
QVERIFY(!mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow));
}
// --- the recorded date, once there is one ---
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
void test_recentlyRecordedFirstLaunchIsNew()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
setFirstLaunch(settings, mNow.addMonths(-1));
QVERIFY(!makeProfile(config.path(), qsl("Achaea")).isEmpty());
QVERIFY2(!mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow), "a month of use is not enough to be experienced, even with a profile in hand");
}
void test_oldRecordedFirstLaunchIsExperienced()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
setFirstLaunch(settings, mNow.addYears(-3));
QVERIFY(mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow));
}
void test_sixMonthBoundary()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
const QString profiles = profilesPathIn(config.path());
setFirstLaunch(settings, mNow.addMonths(-6).addDays(1));
QVERIFY2(!mudlet::evaluateExperiencedPlayer(settings, profiles, mNow), "one day short of six months is not yet experienced");
setFirstLaunch(settings, mNow.addMonths(-6));
QVERIFY2(mudlet::evaluateExperiencedPlayer(settings, profiles, mNow), "exactly six months of use is experienced");
setFirstLaunch(settings, mNow.addMonths(-6).addDays(-1));
QVERIFY(mudlet::evaluateExperiencedPlayer(settings, profiles, mNow));
}
void test_futureDatedFirstLaunchIsNotExperienced()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
setFirstLaunch(settings, mNow.addYears(1));
QVERIFY(!mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow));
}
// --- upgrading users, who have no recorded first launch ---
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
void test_upgraderWithFreshlyWrittenProfilesIsExperienced()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
for (const auto& name : {qsl("Achaea"), qsl("StickMUD"), qsl("Legends of the Jedi")}) {
const QString profile = makeProfile(config.path(), name);
QVERIFY(!profile.isEmpty());
QFile url(qsl("%1/url").arg(profile));
QVERIFY(url.open(QIODevice::WriteOnly));
url.write("achaea.com");
url.close();
QVERIFY2(QFileInfo(profile).lastModified() > mNow.addMonths(-6), "the fixture is only meaningful while the profile directory looks brand new");
}
QVERIFY2(mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow), "an installation with profiles but no recorded first launch predates the key, so it is experienced");
}
// A restored profile may or may not keep its modification times, and never
// keeps its birth time, so no timestamp is consulted
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
void test_restoredFromBackupIsExperienced()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
QVERIFY(!makeProfile(config.path(), qsl("Restored")).isEmpty());
QVERIFY(mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow));
}
void test_settingsWithoutProfilesStillCountAsEarlierUse()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
settings.setValue(qsl("pos"), QPoint(120, 80));
QVERIFY(!QDir(profilesPathIn(config.path())).exists());
mudlet::rememberFirstLaunch(settings, profilesPathIn(config.path()), mNow);
QVERIFY2(!settings.contains(mKey), "an installation with settings on file is not on its first run");
QVERIFY(mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow));
}
void test_unreadableProfilesDirectoryIsTakenAsPopulated()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
const QString profiles = profilesPathIn(config.path());
QVERIFY(!makeProfile(config.path(), qsl("Achaea")).isEmpty());
QVERIFY(QFile::setPermissions(profiles, QFileDevice::WriteOwner | QFileDevice::ExeOwner));
if (QFileInfo(profiles).isReadable()) {
QVERIFY(QFile::setPermissions(profiles, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner));
QSKIP("the profiles directory is readable despite the permissions - running as root?");
}
const bool experienced = mudlet::evaluateExperiencedPlayer(settings, profiles, mNow);
// Restore before asserting, or a failure leaves QTemporaryDir unable to clean up
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
QVERIFY(QFile::setPermissions(profiles, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner));
QVERIFY(experienced);
}
void test_upgradeDoesNotRecordAFirstLaunch()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
QVERIFY(!makeProfile(config.path(), qsl("Achaea")).isEmpty());
mudlet::rememberFirstLaunch(settings, profilesPathIn(config.path()), mNow);
QVERIFY(!settings.contains(mKey));
QVERIFY(mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow.addYears(1)));
}
void test_existingRecordIsNeverOverwritten()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
const QDateTime original = mNow.addMonths(-3);
setFirstLaunch(settings, original);
mudlet::rememberFirstLaunch(settings, profilesPathIn(config.path()), mNow);
QCOMPARE(settings.value(mKey).toString(), original.toString(Qt::ISODate));
}
void test_unparseableRecordFallsBackToTheEarlierUseCheck()
{
QTemporaryDir config;
QVERIFY(config.isValid());
QSettings settings(iniIn(config.path()), QSettings::IniFormat);
settings.setValue(mKey, qsl("not a date"));
QVERIFY(mudlet::evaluateExperiencedPlayer(settings, profilesPathIn(config.path()), mNow));
mudlet::rememberFirstLaunch(settings, profilesPathIn(config.path()), mNow);
QCOMPARE(settings.value(mKey).toString(), qsl("not a date"));
}
void test_recordedValueSurvivesAQSettingsRoundTrip()
{
QTemporaryDir config;
QVERIFY(config.isValid());
{
QSettings writer(iniIn(config.path()), QSettings::IniFormat);
mudlet::rememberFirstLaunch(writer, profilesPathIn(config.path()), mNow.addYears(-2));
}
QSettings reader(iniIn(config.path()), QSettings::IniFormat);
QVERIFY2(mudlet::evaluateExperiencedPlayer(reader, profilesPathIn(config.path()), mNow), "the recorded date must be readable back out of Mudlet.ini");
QFile ini(iniIn(config.path()));
QVERIFY(ini.open(QIODevice::ReadOnly | QIODevice::Text));
QVERIFY2(QString::fromUtf8(ini.readAll()).contains(qsl("firstLaunchDate=2024-08-05T12:00:00Z")), "the date is stored as plain ISO 8601, so it can be read and edited by hand");
}
// --- the live singleton ---
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
// Nothing else in the suite notices the init() call being moved or dropped,
// which would make every fresh install take the "used before" fallback
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
void test_initRecordsTheFirstLaunchOnAFreshInstall()
{
if (portableMarkerPresent()) {
QSKIP("portable.txt present - setupConfig() takes the portable branch");
}
QVERIFY(mLiveConfig.isValid());
Fix an empty XDG config directory hiding every profile (#9712) #### Brief overview of PR changes/additions - An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated `~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran its first-launch onboarding as though the user were new. It also stuck: the first such launch wrote `Mudlet.ini` into that directory, which then kept it winning. - The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` > exists > absent) and the stronger claim wins, with `$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a deliberate opt-in both still land there. A directory that cannot be listed counts as populated rather than empty, so a permission bit cannot re-enter the bug. - Creating `profiles/` is now the opt-in a test harness uses; the `mudlet` directory alone is not, because other tooling creates that by accident. Where both roots hold profiles, `setupConfig()` names the one it is ignoring instead of leaving those profiles apparently gone. #### Motivation for adding to Mudlet Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned rather than destroyed, but a returning user sees "5.0 wiped my profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p "$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it. #### Other info (issues closed, discussion etc) Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`, `mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the onboarding dialog. After: both profiles listed. `ConfigDirOverrideTest` covers the resolution table including the sticky `Mudlet.ini` state, both-populated, symlinked and unreadable directories; each new guard was mutation-checked. The busted suite passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles` root. Not fixed here, and pre-existing rather than 5.0 regressions: `CredentialManager` stores passwords and the OAuth reconnect token under `AppConfigLocation` while the config root is `confPath`, so exporting `XDG_CONFIG_HOME` strands them, and the plaintext-password migration reads one path, writes the other and deletes the original. Both reproduce identically on the 4.22.0 binary and need their own migration path. Assisted-by: Claude:claude-opus-5
2026-08-10 22:17:09 +02:00
// $XDG_CONFIG_HOME/mudlet/profiles is the opt-in marker, without which
// setupConfig() keeps using a legacy ~/.config/mudlet
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
const QString configDir = qsl("%1/mudlet").arg(mLiveConfig.path());
Fix an empty XDG config directory hiding every profile (#9712) #### Brief overview of PR changes/additions - An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated `~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran its first-launch onboarding as though the user were new. It also stuck: the first such launch wrote `Mudlet.ini` into that directory, which then kept it winning. - The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` > exists > absent) and the stronger claim wins, with `$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a deliberate opt-in both still land there. A directory that cannot be listed counts as populated rather than empty, so a permission bit cannot re-enter the bug. - Creating `profiles/` is now the opt-in a test harness uses; the `mudlet` directory alone is not, because other tooling creates that by accident. Where both roots hold profiles, `setupConfig()` names the one it is ignoring instead of leaving those profiles apparently gone. #### Motivation for adding to Mudlet Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned rather than destroyed, but a returning user sees "5.0 wiped my profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p "$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it. #### Other info (issues closed, discussion etc) Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`, `mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the onboarding dialog. After: both profiles listed. `ConfigDirOverrideTest` covers the resolution table including the sticky `Mudlet.ini` state, both-populated, symlinked and unreadable directories; each new guard was mutation-checked. The busted suite passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles` root. Not fixed here, and pre-existing rather than 5.0 regressions: `CredentialManager` stores passwords and the OAuth reconnect token under `AppConfigLocation` while the config root is `confPath`, so exporting `XDG_CONFIG_HOME` strands them, and the plaintext-password migration reads one path, writes the other and deletes the original. Both reproduce identically on the 4.22.0 binary and need their own migration path. Assisted-by: Claude:claude-opus-5
2026-08-10 22:17:09 +02:00
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(configDir)));
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
qputenv("XDG_CONFIG_HOME", mLiveConfig.path().toUtf8());
initializeQRCResourcesForExperiencedPlayerGateTest();
mudlet::start();
mudlet::self()->setupConfig();
QCOMPARE(mudlet::getMudletPath(enums::mainPath), configDir);
QVERIFY2(mudlet::getQSettings()->allKeys().isEmpty(), "a fresh config dir must start out with an empty Mudlet.ini - something wrote settings before init()");
Fix an empty XDG config directory hiding every profile (#9712) #### Brief overview of PR changes/additions - An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated `~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran its first-launch onboarding as though the user were new. It also stuck: the first such launch wrote `Mudlet.ini` into that directory, which then kept it winning. - The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` > exists > absent) and the stronger claim wins, with `$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a deliberate opt-in both still land there. A directory that cannot be listed counts as populated rather than empty, so a permission bit cannot re-enter the bug. - Creating `profiles/` is now the opt-in a test harness uses; the `mudlet` directory alone is not, because other tooling creates that by accident. Where both roots hold profiles, `setupConfig()` names the one it is ignoring instead of leaving those profiles apparently gone. #### Motivation for adding to Mudlet Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned rather than destroyed, but a returning user sees "5.0 wiped my profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p "$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it. #### Other info (issues closed, discussion etc) Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`, `mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the onboarding dialog. After: both profiles listed. `ConfigDirOverrideTest` covers the resolution table including the sticky `Mudlet.ini` state, both-populated, symlinked and unreadable directories; each new guard was mutation-checked. The busted suite passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles` root. Not fixed here, and pre-existing rather than 5.0 regressions: `CredentialManager` stores passwords and the OAuth reconnect token under `AppConfigLocation` while the config root is `confPath`, so exporting `XDG_CONFIG_HOME` strands them, and the plaintext-password migration reads one path, writes the other and deletes the original. Both reproduce identically on the 4.22.0 binary and need their own migration path. Assisted-by: Claude:claude-opus-5
2026-08-10 22:17:09 +02:00
QVERIFY2(QDir(mudlet::getMudletPath(enums::profilesPath)).entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty(), "the opt-in profiles/ dir has to be empty, or this is not a fresh install");
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
mudlet::self()->init();
QVERIFY2(mudlet::getQSettings()->contains(mKey), "init() must record the first launch date");
QCOMPARE(QDateTime::fromString(mudlet::getQSettings()->value(mKey).toString(), Qt::ISODate).isValid(), true);
}
// Pins the key and profiles path experiencedMudletPlayer() picks for itself,
// which the case above cannot - there both branches would answer "new".
fix: stop treating long-time Mudlet users as brand new players (#9695) #### Brief overview of PR changes/additions - `experiencedMudletPlayer()` decided veteran status from profile **directory** mtimes. The per-profile data writes (url, port, password, `profile.ini`, command history) all land straight in that directory and bump its mtime every session, so only a profile *abandoned* for six months ever looked old - the more you use Mudlet, the more certainly it called you new. All 13 profiles on the maintainer's machine classified as "brand new player". - Mudlet renewed those timestamps itself: the connection dialog rewrites `url`/`port`/`description` for the selected profile at startup, so merely launching Mudlet reset the value the gate read. - Replaced with a `firstLaunchDate` key recorded in QSettings on a genuinely fresh install (written in `init()`, before anything can create a profile or save a setting). An installation with any trace of earlier use - a profile, or any other setting already on file - has no recoverable start date and is treated as experienced; timestamps cannot recover one, since a copied or restored profile keeps its modification times only if the tool used happened to preserve them, and loses its birth time regardless. #### Motivation for adding to Mudlet In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169 "Add: UI tour to complement the Mudlet tutorial") and the starter UI package (#9454, 69cd06b1c "add: starter interface with health bars, map and chat for new players") off it, so essentially every active 4.22.0 user upgrading to 5.0 would get a beginner tour dropped on top of their session. #### Other info (issues closed, discussion etc) The heuristic dates back to ae0564e6e "Improve: revise splitscreen tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases) covers fresh install, upgrader with freshly-written profiles, settings restored without profiles, the six-month boundary, a profile restored from backup, future-dated, unparseable and unwritable records, and two live-singleton cases that pin the `init()` call site and the memoised read. Both directions were mutation-tested: dropping the `init()` call and restoring the old mtime heuristic each fail the suite. When in doubt the gate errs towards *experienced* - a veteran shown a new-user tour is a much worse outcome than a newcomer who misses it. **Test case:** Seed a HOME that looks like an existing user (one profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect - before, the 6-step "Welcome to Mudlet!" tour appears; after, it does not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets the tour, and a genuinely empty HOME records one. Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
// Runs last: experiencedMudletPlayer() memoises for the life of the process.
void test_experiencedThroughTheRealSettings()
{
if (portableMarkerPresent()) {
QSKIP("portable.txt present - setupConfig() takes the portable branch");
}
QVERIFY(mudlet::self());
auto* settings = mudlet::getQSettings();
QVERIFY(settings);
settings->remove(mKey);
QVERIFY(!makeProfile(mudlet::getMudletPath(enums::mainPath), qsl("Achaea")).isEmpty());
QVERIFY2(mudlet::self()->experiencedMudletPlayer(), "a profile with no recorded first launch must read as an experienced player");
}
};
void initializeQRCResourcesForExperiencedPlayerGateTest()
{
#ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
qInitResources_additional_splash_screens();
#endif
#ifdef INCLUDE_FONTS
qInitResources_mudlet_fonts_common();
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
qInitResources_mudlet_fonts_posix();
#endif
#endif
qInitResources_mudlet();
qInitResources_qm();
}
#include "ExperiencedPlayerGateTest.moc"
QTEST_MAIN(ExperiencedPlayerGateTest)