// rules.list/upsert, queue.reorder, schedule.get/set, limiter.get/set, download.update: // the rest of D3, called directly against an in-memory store (no socket). #include #include "check.hpp" #include "rpc/dispatcher.hpp" #include "rpc/event_hub.hpp" #include "store/migrations.hpp" #include "store/queues.hpp" #include "store/sqlite.hpp" #include "store/tasks.hpp" #include "velox_proto.hpp" using namespace velox::daemon; namespace proto = velox::proto; namespace { store::TaskRow task(std::string id, std::optional queue = std::nullopt, std::int64_t pos = 0) { store::TaskRow r; r.task_id = std::move(id); r.url = "https://cdn.example/" + r.task_id; r.save_dir = "/tmp"; r.filename = r.task_id + ".bin"; r.state = "queued"; r.created_at = "2026-09-12T00:00:00Z"; r.queue_id = std::move(queue); if (r.queue_id) r.queue_position = pos; return r; } } // namespace void run() { auto db = store::Db::open(":memory:"); CHECK(db.has_value()); if (!db) return; CHECK(store::migrate_to_head(*db).has_value()); rpc::EventHub hub; rpc::VeloxDispatcher dispatcher(*db, hub); // --- rules.list starts empty; rules.upsert creates, replaces, and removes -------- { auto listed = dispatcher.on_rules_list({}); CHECK(listed.has_value()); if (listed) CHECK(listed->items.empty()); proto::RulesUpsertParams up; proto::Rule r; r.name = "ISOs to a queue"; r.enabled = true; r.priority = 5; r.match.extensions = std::vector{"iso"}; r.action.categoryId = "programs"; up.upsert = {r}; auto created = dispatcher.on_rules_upsert(up); CHECK(created.has_value()); if (created) { CHECK_EQ(created->items.size(), std::size_t{1}); CHECK(!created->items[0].ruleId.empty()); } // Replace: same ruleId, new priority. if (!created || created->items.empty()) return; std::string rule_id = created->items[0].ruleId; proto::RulesUpsertParams replace; proto::Rule r2 = created->items[0]; r2.priority = 1; replace.upsert = {r2}; auto replaced = dispatcher.on_rules_upsert(replace); CHECK(replaced.has_value()); if (replaced) { CHECK_EQ(replaced->items.size(), std::size_t{1}); CHECK_EQ(replaced->items[0].priority, std::int64_t{1}); } // Remove. proto::RulesUpsertParams rm; rm.remove = {rule_id}; auto removed = dispatcher.on_rules_upsert(rm); CHECK(removed.has_value()); if (removed) CHECK(removed->items.empty()); } // --- queue.reorder: a real permutation applies; a non-permutation is -32602 ------ { store::Tasks tasks(*db); CHECK(tasks.insert(task("q0", "main", 0)).has_value()); CHECK(tasks.insert(task("q1", "main", 1)).has_value()); CHECK(tasks.insert(task("q2", "main", 2)).has_value()); proto::QueueReorderParams p; p.queueId = "main"; p.taskIds = {"q2", "q0", "q1"}; auto r = dispatcher.on_queue_reorder(p); CHECK(r.has_value()); if (r) { CHECK(r->queue.taskIds.has_value()); if (r->queue.taskIds) { const auto& ids = *r->queue.taskIds; CHECK_EQ(ids.size(), std::size_t{3}); if (ids.size() == 3) { CHECK_EQ(ids[0], std::string("q2")); CHECK_EQ(ids[1], std::string("q0")); CHECK_EQ(ids[2], std::string("q1")); } } } proto::QueueReorderParams bad; bad.queueId = "main"; bad.taskIds = {"q0", "q1"}; // missing q2: not a permutation auto bad_r = dispatcher.on_queue_reorder(bad); CHECK(!bad_r.has_value()); if (!bad_r) CHECK(bad_r.error().code == proto::ErrorCode::InvalidParams); proto::QueueReorderParams unknown_queue; unknown_queue.queueId = "does-not-exist"; unknown_queue.taskIds = {}; auto unknown_r = dispatcher.on_queue_reorder(unknown_queue); CHECK(!unknown_r.has_value()); tasks.remove("q0"); tasks.remove("q1"); tasks.remove("q2"); } // --- schedule.get / schedule.set --------------------------------------------------- { proto::ScheduleGetParams g; g.queueId = std::string("main"); auto before = dispatcher.on_schedule_get(g); CHECK(before.has_value()); if (before) { CHECK_EQ(before->items.size(), std::size_t{1}); if (!before->items.empty()) CHECK(!before->items[0].schedule.has_value()); } proto::ScheduleSetParams set; set.queueId = "main"; proto::Schedule sched; sched.enabled = true; sched.mode = proto::ScheduleMode::Periodic; sched.startTime = std::string("22:00"); sched.stopTime = std::string("06:00"); set.schedule = sched; auto set_r = dispatcher.on_schedule_set(set); CHECK(set_r.has_value()); if (set_r) { CHECK(set_r->schedule.has_value()); CHECK(!set_r->nextRunAt.has_value()); // documented gap, not computed } auto after = dispatcher.on_schedule_get(g); CHECK(after.has_value()); if (after && !after->items.empty()) CHECK(after->items[0].schedule.has_value()); // Clearing (the wire-level "explicit null" is unreachable through the generated // parser's collapsing of absent/null — see UpdatePatch's own comment; this test // only proves set-a-value works, not clear). proto::ScheduleGetParams all; auto all_r = dispatcher.on_schedule_get(all); CHECK(all_r.has_value()); if (all_r) CHECK(!all_r->items.empty()); proto::ScheduleGetParams missing; missing.queueId = std::string("does-not-exist"); auto missing_r = dispatcher.on_schedule_get(missing); CHECK(missing_r.has_value()); if (missing_r) CHECK(missing_r->items.empty()); } // --- limiter.get / limiter.set ----------------------------------------------------- { auto before = dispatcher.on_limiter_get({}); CHECK(before.has_value()); if (before) { CHECK(!before->enabled); CHECK_EQ(before->globalBps, std::int64_t{0}); } proto::Limiter set; set.enabled = true; set.globalBps = 2'000'000; auto set_r = dispatcher.on_limiter_set(set); CHECK(set_r.has_value()); if (set_r) { CHECK(set_r->enabled); CHECK_EQ(set_r->globalBps, std::int64_t{2'000'000}); } auto after = dispatcher.on_limiter_get({}); CHECK(after.has_value()); if (after) { CHECK(after->enabled); CHECK_EQ(after->globalBps, std::int64_t{2'000'000}); } } // --- download.update: metadata fields, range validation, not-found --------------- { store::Tasks tasks(*db); CHECK(tasks.insert(task("u0")).has_value()); proto::DownloadUpdateParams p; p.taskId = "u0"; p.patch.description = std::string("a note"); p.patch.segments = 4; auto r = dispatcher.on_download_update(p); CHECK(r.has_value()); if (r) { CHECK_EQ(r->taskId, std::string("u0")); } auto row = tasks.get("u0"); CHECK(row.has_value() && row->has_value()); if (row && *row) { CHECK_EQ((*row)->description.value_or(""), std::string("a note")); CHECK_EQ((*row)->req_segments.value_or(-1), std::int64_t{4}); } proto::DownloadUpdateParams bad_range; bad_range.taskId = "u0"; bad_range.patch.segments = 999; auto bad_r = dispatcher.on_download_update(bad_range); CHECK(!bad_r.has_value()); if (!bad_r) CHECK(bad_r.error().code == proto::ErrorCode::InvalidParams); proto::DownloadUpdateParams missing; missing.taskId = "does-not-exist"; missing.patch.description = std::string("x"); auto missing_r = dispatcher.on_download_update(missing); CHECK(!missing_r.has_value()); if (!missing_r) CHECK(missing_r.error().code == proto::ErrorCode::TaskNotFound); tasks.remove("u0"); } } TEST_MAIN()