mirror of
https://github.com/logos-co/logos-chatsdk-ui.git
synced 2026-08-27 14:51:07 +00:00
* feat: hold the run's failures and hand over the logs beside them A failure lived on a toast for four seconds and then left with the only copy of its message, so two in quick succession meant the first went unread. The account of the run it belonged to was no easier to reach: the log files sit in the session directory under names only the host assigns, and nothing in the app named them. Failures are retained instead, newest first, with consecutive repeats collapsed into one row and a count. The strip across the foot holds the newest and counts the ones behind it, and opening the dialog marks them seen, which quiets the strip without emptying the list. Each precondition guard names the action it refused rather than sharing one context-free sentence between call sites, and a failure the module gave no reason for says that much, because an empty reason is what a call that never reached the module leaves behind. The logs are listed one writer at a time, because each keeps its own wherever the platform gave it somewhere to write: the chat module in the instance directory it was assigned, and this view in that same directory, borrowed because a view module is assigned none of its own. Runs are grouped by the stem of the file their writer announced, so neither list picks up the other's files, and nothing reads what is in them. The fix for the borrowed folder is an instance path on LogosUiPluginContext. chat_module moves to a rev that installs a `tracing` subscriber, without which the chat core's account of the run is empty: libchat logs exclusively through `tracing`, and an event raised in a process with no subscriber is dropped before it reaches stderr. That rev also names the file it opens through get_log_path(), which is what lets this list the chat core's runs at all. * feat: notice when the chat module is gone The view found out that the module had died only by trying to use it, twenty seconds later, when a call it had already made ran out its own timeout. Left alone it never found out: the status bar went on saying Online, the composer went on accepting messages, and the failure that mattered arrived as "the chat module gave no reason". Ask instead. A probe every ten seconds calls health(), whose answer is whether the call arrived, and two unanswered in a row put the app in Error: the composer closes, the account card stops claiming a connection, and the failure names the module and points at its log, which is where the panic and its backtrace are. One unanswered probe is not enough, because a loaded machine can miss one and being wrong about this is worse than being slow. The probe passes a two-second timeout and stops once it has concluded. Acquiring the remote object blocks the caller for as long as it is given, so the default twenty would freeze the UI on every tick for the rest of the run, which is a worse fault than the one being fixed.
139 lines
5.0 KiB
C++
139 lines
5.0 KiB
C++
#include <QDir>
|
|
#include <QFile>
|
|
#include <QTemporaryDir>
|
|
#include <QTest>
|
|
|
|
#include "SessionLogFiles.h"
|
|
|
|
class TestSessionLogFiles : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void noPathHasNoRuns();
|
|
void groupsRotationsIntoOneRun();
|
|
void ordersRunsNewestFirst();
|
|
void ignoresOtherFiles();
|
|
void separatesTwoWritersSharingOneDirectory();
|
|
void reportsAnUnstampedPathOnItsOwn();
|
|
|
|
private:
|
|
// Writes `bytes` bytes into <dir>/<name>, so a run has a size to report.
|
|
static void write(const QTemporaryDir& dir, const QString& name, int bytes);
|
|
};
|
|
|
|
void TestSessionLogFiles::write(const QTemporaryDir& dir, const QString& name, int bytes)
|
|
{
|
|
QFile file(dir.filePath(name));
|
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
|
file.write(QByteArray(bytes, 'x'));
|
|
}
|
|
|
|
void TestSessionLogFiles::noPathHasNoRuns()
|
|
{
|
|
QVERIFY(listSessionLogRuns(QString()).isEmpty());
|
|
}
|
|
|
|
void TestSessionLogFiles::groupsRotationsIntoOneRun()
|
|
{
|
|
QTemporaryDir dir;
|
|
write(dir, QStringLiteral("basecamp_20260728_100000.001.log"), 10);
|
|
write(dir, QStringLiteral("basecamp_20260728_100000.002.log"), 20);
|
|
write(dir, QStringLiteral("basecamp_20260728_100000.log"), 5);
|
|
|
|
const QList<SessionLogRun> runs =
|
|
listSessionLogRuns(dir.filePath(QStringLiteral("basecamp_20260728_100000.log")));
|
|
|
|
QCOMPARE(runs.size(), 1);
|
|
QCOMPARE(runs.first().bytes, 35);
|
|
QCOMPARE(runs.first().stamp, QStringLiteral("20260728_100000"));
|
|
// Oldest rotation first, the file still being written last: reading them in
|
|
// that order reads the run in the order it happened.
|
|
QCOMPARE(runs.first().paths.size(), 3);
|
|
QVERIFY(runs.first().paths.at(0).endsWith(QStringLiteral(".001.log")));
|
|
QVERIFY(runs.first().paths.at(1).endsWith(QStringLiteral(".002.log")));
|
|
QVERIFY(runs.first().paths.at(2).endsWith(QStringLiteral("100000.log")));
|
|
}
|
|
|
|
void TestSessionLogFiles::ordersRunsNewestFirst()
|
|
{
|
|
QTemporaryDir dir;
|
|
write(dir, QStringLiteral("basecamp_20260726_080000.log"), 1);
|
|
write(dir, QStringLiteral("basecamp_20260728_100000.log"), 1);
|
|
write(dir, QStringLiteral("basecamp_20260727_090000.log"), 1);
|
|
|
|
const QList<SessionLogRun> runs =
|
|
listSessionLogRuns(dir.filePath(QStringLiteral("basecamp_20260728_100000.log")));
|
|
|
|
QCOMPARE(runs.size(), 3);
|
|
QCOMPARE(runs.at(0).stamp, QStringLiteral("20260728_100000"));
|
|
QCOMPARE(runs.at(1).stamp, QStringLiteral("20260727_090000"));
|
|
QCOMPARE(runs.at(2).stamp, QStringLiteral("20260726_080000"));
|
|
}
|
|
|
|
void TestSessionLogFiles::ignoresOtherFiles()
|
|
{
|
|
QTemporaryDir dir;
|
|
write(dir, QStringLiteral("basecamp_20260728_100000.log"), 1);
|
|
// Another app logging into the same directory, and a file that only looks
|
|
// like a run.
|
|
write(dir, QStringLiteral("logos-standalone-app_20260728_100000.log"), 1);
|
|
write(dir, QStringLiteral("basecamp_notastamp.log"), 1);
|
|
write(dir, QStringLiteral("basecamp_20260728_100000.log.bak"), 1);
|
|
|
|
const QList<SessionLogRun> runs =
|
|
listSessionLogRuns(dir.filePath(QStringLiteral("basecamp_20260728_100000.log")));
|
|
|
|
QCOMPARE(runs.size(), 1);
|
|
QCOMPARE(runs.first().paths.size(), 1);
|
|
}
|
|
|
|
// This view keeps its log in the chat module's instance directory, for want of
|
|
// one of its own, so one directory holds two writers' runs. The whole
|
|
// arrangement rests on each list being its own writer's and no one else's.
|
|
void TestSessionLogFiles::separatesTwoWritersSharingOneDirectory()
|
|
{
|
|
QTemporaryDir dir;
|
|
write(dir, QStringLiteral("chat_ui_20260728_100000.log"), 1);
|
|
write(dir, QStringLiteral("chat_ui_20260727_090000.log"), 1);
|
|
write(dir, QStringLiteral("chat_module_20260728_100000.log"), 1);
|
|
write(dir, QStringLiteral("chat_module_20260728_100000.001.log"), 1);
|
|
write(dir, QStringLiteral("chat_module_20260726_080000.log"), 1);
|
|
|
|
const QList<SessionLogRun> view =
|
|
listSessionLogRuns(dir.filePath(QStringLiteral("chat_ui_20260728_100000.log")));
|
|
const QList<SessionLogRun> core =
|
|
listSessionLogRuns(dir.filePath(QStringLiteral("chat_module_20260728_100000.log")));
|
|
|
|
QCOMPARE(view.size(), 2);
|
|
QCOMPARE(view.at(0).paths.size(), 1);
|
|
QCOMPARE(core.size(), 2);
|
|
// The module's newest run rotated once; the view's file of the same stamp is
|
|
// not part of it.
|
|
QCOMPARE(core.at(0).paths.size(), 2);
|
|
for (const SessionLogRun& run : view) {
|
|
for (const QString& path : run.paths)
|
|
QVERIFY2(path.contains(QStringLiteral("chat_ui_")), qPrintable(path));
|
|
}
|
|
for (const SessionLogRun& run : core) {
|
|
for (const QString& path : run.paths)
|
|
QVERIFY2(path.contains(QStringLiteral("chat_module_")), qPrintable(path));
|
|
}
|
|
}
|
|
|
|
void TestSessionLogFiles::reportsAnUnstampedPathOnItsOwn()
|
|
{
|
|
QTemporaryDir dir;
|
|
write(dir, QStringLiteral("session.log"), 7);
|
|
|
|
const QList<SessionLogRun> runs = listSessionLogRuns(dir.filePath(QStringLiteral("session.log")));
|
|
|
|
QCOMPARE(runs.size(), 1);
|
|
QCOMPARE(runs.first().paths.size(), 1);
|
|
QCOMPARE(runs.first().bytes, 7);
|
|
QVERIFY(runs.first().stamp.isEmpty());
|
|
}
|
|
|
|
QTEST_MAIN(TestSessionLogFiles)
|
|
#include "tst_sessionlogfiles.moc"
|