2026-03-24 17:28:59 -04:00
|
|
|
#include <gtest/gtest.h>
|
2026-05-22 07:56:23 -04:00
|
|
|
#include <logos_json.h>
|
2026-03-24 17:28:59 -04:00
|
|
|
#include <sstream>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include "client/output.h"
|
|
|
|
|
|
|
|
|
|
// Helper to capture stdout
|
|
|
|
|
class CaptureStdout {
|
|
|
|
|
public:
|
|
|
|
|
CaptureStdout() {
|
|
|
|
|
oldBuf = std::cout.rdbuf(buffer.rdbuf());
|
|
|
|
|
}
|
|
|
|
|
~CaptureStdout() {
|
|
|
|
|
std::cout.rdbuf(oldBuf);
|
|
|
|
|
}
|
|
|
|
|
std::string str() const { return buffer.str(); }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
std::stringstream buffer;
|
|
|
|
|
std::streambuf* oldBuf;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class CaptureStderr {
|
|
|
|
|
public:
|
|
|
|
|
CaptureStderr() {
|
|
|
|
|
oldBuf = std::cerr.rdbuf(buffer.rdbuf());
|
|
|
|
|
}
|
|
|
|
|
~CaptureStderr() {
|
|
|
|
|
std::cerr.rdbuf(oldBuf);
|
|
|
|
|
}
|
|
|
|
|
std::string str() const { return buffer.str(); }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
std::stringstream buffer;
|
|
|
|
|
std::streambuf* oldBuf;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class OutputTest : public ::testing::Test {
|
|
|
|
|
protected:
|
|
|
|
|
// Force JSON mode for predictable output in tests
|
|
|
|
|
Output jsonOutput{true};
|
|
|
|
|
Output humanOutput{false}; // Note: in test environment, TTY detection varies
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── JSON Mode Tests ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, IsJsonMode_TrueWhenForced)
|
|
|
|
|
{
|
|
|
|
|
EXPECT_TRUE(jsonOutput.isJsonMode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintSuccess_JsonObject)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap data{{"status", "ok"}, {"module", "waku"}};
|
2026-03-24 17:28:59 -04:00
|
|
|
jsonOutput.printSuccess(data);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["status"].get<std::string>(), "ok");
|
|
|
|
|
EXPECT_EQ(doc["module"].get<std::string>(), "waku");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintSuccess_JsonArray)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosList data = nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "waku"}},
|
|
|
|
|
LogosMap{{"name", "chat"}}
|
|
|
|
|
});
|
2026-03-24 17:28:59 -04:00
|
|
|
jsonOutput.printSuccess(data);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_array());
|
|
|
|
|
EXPECT_EQ(doc.size(), 2u);
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintSuccess_String)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-21 14:05:18 -04:00
|
|
|
jsonOutput.printSuccess(std::string("All good"));
|
2026-03-24 17:28:59 -04:00
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["status"].get<std::string>(), "ok");
|
|
|
|
|
EXPECT_EQ(doc["message"].get<std::string>(), "All good");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintError_Json)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
jsonOutput.printError("MODULE_NOT_FOUND", "Module 'foo' not found.",
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap{{"known_modules", LogosList::array({"waku", "chat"})}});
|
2026-03-24 17:28:59 -04:00
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["status"].get<std::string>(), "error");
|
|
|
|
|
EXPECT_EQ(doc["code"].get<std::string>(), "MODULE_NOT_FOUND");
|
|
|
|
|
EXPECT_EQ(doc["message"].get<std::string>(), "Module 'foo' not found.");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintModuleList_Json)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosList modules = nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "waku"}, {"version", "0.1.0"}, {"status", "loaded"}, {"uptime_seconds", 8040}},
|
|
|
|
|
LogosMap{{"name", "chat"}, {"version", "0.2.0"}, {"status", "crashed"}}
|
|
|
|
|
});
|
2026-03-24 17:28:59 -04:00
|
|
|
jsonOutput.printModuleList(modules);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_array());
|
|
|
|
|
EXPECT_EQ(doc.size(), 2u);
|
|
|
|
|
EXPECT_EQ(doc[0]["name"].get<std::string>(), "waku");
|
|
|
|
|
EXPECT_EQ(doc[1]["status"].get<std::string>(), "crashed");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintStatus_Json_Running)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap status{
|
|
|
|
|
{"daemon", LogosMap{
|
|
|
|
|
{"status", "running"}, {"pid", 12345}, {"uptime_seconds", 3600}, {"version", "1.0"}
|
|
|
|
|
}},
|
|
|
|
|
{"modules_summary", LogosMap{{"loaded", 2}, {"crashed", 0}, {"not_loaded", 1}}},
|
|
|
|
|
{"modules", nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "waku"}, {"status", "loaded"}},
|
|
|
|
|
LogosMap{{"name", "chat"}, {"status", "loaded"}},
|
|
|
|
|
LogosMap{{"name", "delivery"}, {"status", "not_loaded"}}
|
|
|
|
|
})}
|
2026-03-24 17:28:59 -04:00
|
|
|
};
|
|
|
|
|
jsonOutput.printStatus(status);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["daemon"]["status"].get<std::string>(), "running");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintStatus_Json_NotRunning)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap status{{"daemon", LogosMap{{"status", "not_running"}}}};
|
2026-03-24 17:28:59 -04:00
|
|
|
jsonOutput.printStatus(status);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["daemon"]["status"].get<std::string>(), "not_running");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintModuleInfo_Json)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap info{
|
2026-03-24 17:28:59 -04:00
|
|
|
{"name", "chat"},
|
|
|
|
|
{"version", "0.2.0"},
|
|
|
|
|
{"status", "loaded"},
|
|
|
|
|
{"pid", 23457},
|
|
|
|
|
{"uptime_seconds", 8040},
|
2026-05-22 07:56:23 -04:00
|
|
|
{"dependencies", nlohmann::json::array({"waku", "store"})},
|
|
|
|
|
{"methods", nlohmann::json::array({
|
|
|
|
|
LogosMap{
|
2026-03-24 17:28:59 -04:00
|
|
|
{"name", "send_message"},
|
2026-06-04 11:42:16 -03:00
|
|
|
{"parameters", nlohmann::json::array({LogosMap{{"name", "text"}, {"type", "QString"}}})},
|
|
|
|
|
{"returnType", "QString"}
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
2026-05-22 07:56:23 -04:00
|
|
|
})}
|
2026-03-24 17:28:59 -04:00
|
|
|
};
|
|
|
|
|
jsonOutput.printModuleInfo(info);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["name"].get<std::string>(), "chat");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 11:42:16 -03:00
|
|
|
// Human-mode: the methods loop must read the real getPluginMethods keys
|
|
|
|
|
// (returnType / parameters) and render a multi-line description with each
|
|
|
|
|
// line preserved.
|
|
|
|
|
TEST_F(OutputTest, PrintModuleInfo_Human_MethodsAndDescriptions)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
LogosMap info{
|
|
|
|
|
{"name", "chat"},
|
|
|
|
|
{"version", "0.2.0"},
|
|
|
|
|
{"status", "loaded"},
|
|
|
|
|
{"methods", nlohmann::json::array({
|
|
|
|
|
LogosMap{
|
|
|
|
|
{"name", "send_message"},
|
|
|
|
|
{"returnType", "QString"},
|
|
|
|
|
{"parameters", nlohmann::json::array({LogosMap{{"name", "text"}, {"type", "QString"}}})},
|
|
|
|
|
{"description", "Sends a message.\nUses the active channel."}
|
|
|
|
|
}
|
|
|
|
|
})}
|
|
|
|
|
};
|
|
|
|
|
humanOutput.printModuleInfo(info);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
|
|
|
|
// Signature is built from returnType + parameters (the keys this fixes).
|
|
|
|
|
EXPECT_NE(out.find("send_message(text: QString) -> QString"), std::string::npos);
|
|
|
|
|
// Multi-line description: both lines rendered, line breaks preserved.
|
|
|
|
|
EXPECT_NE(out.find("Sends a message."), std::string::npos);
|
|
|
|
|
EXPECT_NE(out.find("Uses the active channel."), std::string::npos);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:17:03 -03:00
|
|
|
// Human-mode: events render in their own section as `name(params)` with NO
|
|
|
|
|
// return type (events are fire-and-forget), plus their multi-line description.
|
|
|
|
|
TEST_F(OutputTest, PrintModuleInfo_Human_Events)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
LogosMap info{
|
|
|
|
|
{"name", "chat"},
|
|
|
|
|
{"version", "0.2.0"},
|
|
|
|
|
{"status", "loaded"},
|
|
|
|
|
{"events", nlohmann::json::array({
|
|
|
|
|
LogosMap{
|
|
|
|
|
{"name", "versionReady"},
|
|
|
|
|
{"parameters", nlohmann::json::array({LogosMap{{"name", "version"}, {"type", "QString"}}})},
|
|
|
|
|
{"description", "Fired when ready.\nCarries the version."}
|
|
|
|
|
}
|
|
|
|
|
})}
|
|
|
|
|
};
|
|
|
|
|
humanOutput.printModuleInfo(info);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
|
|
|
|
EXPECT_NE(out.find("Events:"), std::string::npos);
|
|
|
|
|
// Signature has params but no `-> returnType` (events are void).
|
|
|
|
|
EXPECT_NE(out.find("versionReady(version: QString)"), std::string::npos);
|
|
|
|
|
EXPECT_EQ(out.find("versionReady(version: QString) ->"), std::string::npos);
|
|
|
|
|
// Multi-line description: both lines rendered, line breaks preserved.
|
|
|
|
|
EXPECT_NE(out.find("Fired when ready."), std::string::npos);
|
|
|
|
|
EXPECT_NE(out.find("Carries the version."), std::string::npos);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 17:28:59 -04:00
|
|
|
TEST_F(OutputTest, PrintEvent_Ndjson)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap event{
|
2026-03-24 17:28:59 -04:00
|
|
|
{"timestamp", "2026-03-23T14:30:01Z"},
|
|
|
|
|
{"module", "chat"},
|
|
|
|
|
{"event", "chat-message"},
|
2026-05-22 07:56:23 -04:00
|
|
|
{"data", LogosMap{{"from", "alice"}, {"text", "hello"}}}
|
2026-03-24 17:28:59 -04:00
|
|
|
};
|
|
|
|
|
jsonOutput.printEvent(event);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
|
|
|
|
EXPECT_NE(out.find("{"), std::string::npos);
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["event"].get<std::string>(), "chat-message");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintStats_Json)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosList stats = nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "waku"}, {"pid", 23456}, {"cpu_percent", 2.1}, {"memory_mb", 48.3}},
|
|
|
|
|
LogosMap{{"name", "chat"}, {"pid", 23457}, {"cpu_percent", 0.4}, {"memory_mb", 22.1}}
|
|
|
|
|
});
|
2026-03-24 17:28:59 -04:00
|
|
|
jsonOutput.printStats(stats);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_array());
|
|
|
|
|
EXPECT_EQ(doc.size(), 2u);
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Human Mode Tests ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintModuleList_Human)
|
|
|
|
|
{
|
|
|
|
|
Output out(false);
|
|
|
|
|
out.setJsonMode(false);
|
|
|
|
|
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosList modules = nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "waku"}, {"version", "0.1.0"}, {"status", "loaded"}, {"uptime_seconds", 8040}}
|
|
|
|
|
});
|
2026-03-24 17:28:59 -04:00
|
|
|
out.printModuleList(modules);
|
|
|
|
|
std::string output = cap.str();
|
|
|
|
|
EXPECT_FALSE(output.empty());
|
2026-06-29 20:46:22 -03:00
|
|
|
// Version is rendered with a leading "v".
|
|
|
|
|
EXPECT_NE(output.find("v0.1.0"), std::string::npos) << output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Regression for #59: a non-empty version must show in the human table, and a
|
|
|
|
|
// name longer than the old fixed 12-char column must not collide with it
|
|
|
|
|
// (the bug rendered "delivery_modulev" — name immediately followed by "v").
|
|
|
|
|
TEST_F(OutputTest, PrintModuleList_Human_LongNameDoesNotCollideWithVersion)
|
|
|
|
|
{
|
|
|
|
|
Output out(false);
|
|
|
|
|
out.setJsonMode(false);
|
|
|
|
|
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
LogosList modules = nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "capability_module"}, {"version", "1.0.0"}, {"status", "loaded"}}
|
|
|
|
|
});
|
|
|
|
|
out.printModuleList(modules);
|
|
|
|
|
std::string output = cap.str();
|
|
|
|
|
|
|
|
|
|
EXPECT_NE(output.find("capability_module"), std::string::npos) << output;
|
|
|
|
|
EXPECT_NE(output.find("v1.0.0"), std::string::npos) << output;
|
|
|
|
|
// The name and version must be separated by whitespace, never fused.
|
|
|
|
|
EXPECT_EQ(output.find("capability_modulev1.0.0"), std::string::npos)
|
|
|
|
|
<< "name collided with version column:\n" << output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A module that declares no version renders "-", not a bare "v".
|
|
|
|
|
TEST_F(OutputTest, PrintModuleList_Human_MissingVersionShowsDash)
|
|
|
|
|
{
|
|
|
|
|
Output out(false);
|
|
|
|
|
out.setJsonMode(false);
|
|
|
|
|
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
LogosList modules = nlohmann::json::array({
|
|
|
|
|
LogosMap{{"name", "no_version_module"}, {"status", "not_loaded"}}
|
|
|
|
|
});
|
|
|
|
|
out.printModuleList(modules);
|
|
|
|
|
std::string output = cap.str();
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(output.find("no_version_modulev"), std::string::npos)
|
|
|
|
|
<< "empty version must not render as a bare 'v':\n" << output;
|
|
|
|
|
EXPECT_NE(output.find("-"), std::string::npos) << output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// module-info human output shows the version with a "v" prefix when present.
|
|
|
|
|
TEST_F(OutputTest, PrintModuleInfo_Human_RendersVersion)
|
|
|
|
|
{
|
|
|
|
|
Output out(false);
|
|
|
|
|
out.setJsonMode(false);
|
|
|
|
|
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
LogosMap info{{"name", "chat"}, {"version", "0.2.0"}, {"status", "loaded"}};
|
|
|
|
|
out.printModuleInfo(info);
|
|
|
|
|
std::string output = cap.str();
|
|
|
|
|
EXPECT_NE(output.find("Version: v0.2.0"), std::string::npos) << output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// module-info with no version shows "-" rather than a bare "v".
|
|
|
|
|
TEST_F(OutputTest, PrintModuleInfo_Human_MissingVersionShowsDash)
|
|
|
|
|
{
|
|
|
|
|
Output out(false);
|
|
|
|
|
out.setJsonMode(false);
|
|
|
|
|
|
|
|
|
|
CaptureStdout cap;
|
|
|
|
|
LogosMap info{{"name", "chat"}, {"status", "not_loaded"}};
|
|
|
|
|
out.printModuleInfo(info);
|
|
|
|
|
std::string output = cap.str();
|
|
|
|
|
EXPECT_NE(output.find("Version: -"), std::string::npos) << output;
|
|
|
|
|
EXPECT_EQ(output.find("Version: v"), std::string::npos) << output;
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintReload_Json_Success)
|
|
|
|
|
{
|
|
|
|
|
CaptureStdout cap;
|
2026-05-22 07:56:23 -04:00
|
|
|
LogosMap result{
|
2026-03-24 17:28:59 -04:00
|
|
|
{"action", "reload"}, {"module", "chat"}, {"version", "0.2.0"},
|
|
|
|
|
{"status", "loaded"}, {"pid", 51203}, {"previous_status", "crashed"}
|
|
|
|
|
};
|
|
|
|
|
jsonOutput.printReload(result);
|
|
|
|
|
|
|
|
|
|
std::string out = cap.str();
|
2026-05-22 07:56:23 -04:00
|
|
|
nlohmann::json doc = nlohmann::json::parse(out);
|
|
|
|
|
ASSERT_TRUE(doc.is_object());
|
|
|
|
|
EXPECT_EQ(doc["module"].get<std::string>(), "chat");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(OutputTest, PrintError_Human)
|
|
|
|
|
{
|
|
|
|
|
CaptureStderr cap;
|
|
|
|
|
Output out(false);
|
2026-08-03 21:49:15 -03:00
|
|
|
out.printError("NO_DAEMON", "No running logosctl daemon.");
|
2026-03-24 17:28:59 -04:00
|
|
|
// Just verify it doesn't crash; actual output depends on TTY detection
|
|
|
|
|
}
|