cli: velox — add / ls / pause / resume / rm, with --json (build step 8, pulled forward)

The scriptable client, built now rather than last: it is how the daemon
gets exercised before the GUI is pointed at it (AGENT-DAEMON.md).

- src/client — synchronous blocking RPC over the Unix socket: resolve
  $XDG_RUNTIME_DIR/velox/velox.sock, connect, session.hello, one framed
  request/reply per call. Distinguishes transport failure (exit 3) from
  a daemon-returned error (exit 1).
- src/main — subcommands add/ls/pause/resume/rm; --json prints the raw
  JSON-RPC result or error; --dir/--out/--segments on add;
  --delete-file on rm. Usage errors exit 2.

ls works end to end against veloxd today (empty table). add and the
bulk verbs reach the daemon and surface its "not implemented" (-32603)
cleanly until the store lands — the plumbing is done, the commands
light up as handlers do.

Test velox.client: the real Client against an in-process UdsServer —
no-daemon path, session.hello, download.list, and a not-implemented
method surfacing as an RPC error rather than a transport error.
ASan+UBSan clean; full tree (21 tests, incl. conformance) green.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
2026-09-10 15:27:20 +04:00
co-authored by Claude Sonnet 5
parent e60d6669d8
commit 0c7ce1437c
7 changed files with 478 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
// The CLI's Client against a real in-process daemon RPC server.
#include <sys/stat.h>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <thread>
#include "check.hpp"
#include "client.hpp"
#include "rpc/dispatcher.hpp"
#include "rpc/event_loop.hpp"
#include "rpc/uds_server.hpp"
namespace rpc = velox::daemon::rpc;
using velox::cli::CallError;
using velox::cli::Client;
void run() {
// connect() with no daemon -> a kConnect error, not a crash.
{
::setenv("XDG_RUNTIME_DIR", "/tmp/velox-cli-test-nonexistent-xyz", 1);
Client c;
auto err = c.connect();
CHECK(err.has_value());
if (err) CHECK_EQ(err->code, CallError::kConnect);
}
// Point XDG_RUNTIME_DIR at a fresh temp dir; the client derives
// <XDG_RUNTIME_DIR>/velox/velox.sock and the server binds exactly that.
char tmpl[] = "/tmp/velox-cli-test-XXXXXX";
const char* xdg = ::mkdtemp(tmpl);
CHECK(xdg != nullptr);
if (xdg == nullptr) return;
::setenv("XDG_RUNTIME_DIR", xdg, 1);
const std::string velox_dir = std::string(xdg) + "/velox";
::mkdir(velox_dir.c_str(), 0700);
const std::string server_sock = velox_dir + "/velox.sock";
rpc::EventLoop loop;
rpc::VeloxDispatcher dispatcher;
rpc::UdsServer server(loop, dispatcher, server_sock);
const auto ec = server.start();
CHECK(!ec);
if (ec) return;
std::thread th([&loop] { loop.run(); });
{
Client c;
auto err = c.connect();
CHECK(!err.has_value());
if (err) {
loop.stop();
th.join();
return;
}
CHECK_EQ(c.hello_result().value("transport", ""), std::string("uds"));
auto ls = c.call("download.list", nlohmann::json::object());
CHECK(ls.has_value());
if (ls) {
CHECK_EQ(ls->value("total", -1), 0);
CHECK(ls->at("items").is_array());
}
// A not-yet-implemented method surfaces the daemon's error, not a transport error.
auto add = c.call("download.add", {{"url", "https://example.com/x"}});
CHECK(!add.has_value());
if (!add) CHECK_EQ(add.error().code, -32603);
}
loop.stop();
th.join();
::unlink(server_sock.c_str());
}
TEST_MAIN()