// store/tasks + store/settings: insert / get / list (filter, sort, page) and the // TaskSummary projection. #include #include "check.hpp" #include "store/migrations.hpp" #include "store/settings.hpp" #include "store/sqlite.hpp" #include "store/tasks.hpp" using namespace velox::daemon::store; namespace proto = velox::proto; namespace { TaskRow row(std::string id, std::string name, std::string state, std::string created, std::optional size = std::nullopt, std::optional cat = std::nullopt) { TaskRow r; r.task_id = std::move(id); r.url = "https://example.com/" + name; r.save_dir = "/home/u/Downloads"; r.filename = std::move(name); r.state = std::move(state); r.created_at = std::move(created); r.size_bytes = size; r.category_id = std::move(cat); return r; } } // namespace void run() { auto db = Db::open(":memory:"); CHECK(db.has_value()); if (!db) return; CHECK(migrate_to_head(*db).has_value()); Tasks tasks(*db); // --- insert + get round trip ------------------------------------------------ { auto r = row("t1", "ubuntu.iso", "downloading", "2026-09-10T10:00:00Z", 6228541440LL, "programs"); r.downloaded_bytes = 1000000; r.eff_segments = 8; r.resumable = true; CHECK(tasks.insert(r).has_value()); auto got = tasks.get("t1"); CHECK(got.has_value() && got->has_value()); if (got && *got) { CHECK_EQ((*got)->filename, std::string("ubuntu.iso")); CHECK_EQ((*got)->downloaded_bytes, 1000000); CHECK((*got)->size_bytes.has_value() && *(*got)->size_bytes == 6228541440LL); CHECK((*got)->resumable); CHECK((*got)->category_id.value_or("") == "programs"); } auto missing = tasks.get("nope"); CHECK(missing.has_value() && !missing->has_value()); } // --- a duplicate id is rejected by the primary key ----------------------- CHECK(!tasks.insert(row("t1", "again", "new", "2026-09-10T10:01:00Z")).has_value()); // --- projection to TaskSummary, including the error block -------------- { auto r = row("terr", "broken.zip", "failed", "2026-09-10T09:00:00Z"); r.error_code = "not_found"; r.error_message = "server returned 404"; r.error_http_status = 404; r.error_retryable = false; r.error_attempt = 3; CHECK(tasks.insert(r).has_value()); auto got = tasks.get("terr"); CHECK(got && *got); const auto s = to_summary(**got); CHECK(s.state == proto::TaskState::Failed); CHECK(s.error.has_value()); if (s.error) { CHECK(s.error->code == proto::TaskErrorCode::NotFound); CHECK(s.error->httpStatus.value_or(0) == 404); CHECK(s.error->attempt.value_or(0) == 3); } // a non-error task has no error block auto ok = tasks.get("t1"); CHECK(!to_summary(**ok).error.has_value()); } // --- list: total + paging + default newest-first sort ------------------ { for (int i = 0; i < 20; ++i) { char id[8]; std::snprintf(id, sizeof(id), "p%02d", i); char ts[24]; std::snprintf(ts, sizeof(ts), "2026-09-11T%02d:00:00Z", i); CHECK(tasks.insert(row(id, std::string("f") + id, "queued", ts)).has_value()); } auto page = tasks.list(std::nullopt, std::nullopt, 0, 5); CHECK(page.has_value()); if (page) { CHECK_EQ(page->total, 22); // 20 + t1 + terr CHECK_EQ(page->rows.size(), 5u); // newest first: p19 (11:00) before p18 ... CHECK_EQ(page->rows.front().task_id, std::string("p19")); } auto page2 = tasks.list(std::nullopt, std::nullopt, 20, 500); CHECK(page2.has_value()); if (page2) CHECK_EQ(page2->rows.size(), 2u); // the tail } // --- list: filter by state --------------------------------------------- { proto::TaskFilter f; f.states = std::vector{proto::TaskState::Queued}; auto page = tasks.list(f, std::nullopt, 0, 500); CHECK(page.has_value()); if (page) { CHECK_EQ(page->total, 20); for (const auto& r : page->rows) CHECK_EQ(r.state, std::string("queued")); } } // --- list: filter by category + case-insensitive query --------------- { proto::TaskFilter f; f.categoryId = "programs"; auto page = tasks.list(f, std::nullopt, 0, 500); CHECK(page.has_value() && page->total == 1); proto::TaskFilter q; q.query = "UBUNTU"; // matches "ubuntu.iso" case-insensitively auto page2 = tasks.list(q, std::nullopt, 0, 500); CHECK(page2.has_value() && page2->total == 1); if (page2 && !page2->rows.empty()) CHECK_EQ(page2->rows.front().task_id, std::string("t1")); } // --- list: explicit sort by filename ascending ---------------------- { proto::TaskSort s; s.field = proto::TaskSortField::Filename; s.direction = proto::TaskSortDirection::Asc; auto page = tasks.list(std::nullopt, s, 0, 3); CHECK(page.has_value()); if (page && page->rows.size() >= 2) CHECK(page->rows[0].filename <= page->rows[1].filename); } // --- set_state + pause_reason, and remove ------------------------- { CHECK(tasks.set_state("t1", "paused", std::string("user")).value_or(false)); auto g = tasks.get("t1"); CHECK(g && *g && (*g)->state == "paused" && (*g)->pause_reason.value_or("") == "user"); CHECK(tasks.set_state("t1", "downloading", std::nullopt).value_or(false)); g = tasks.get("t1"); CHECK(g && *g && !(*g)->pause_reason.has_value()); // cleared when not paused CHECK(!tasks.set_state("ghost", "paused", std::nullopt).value_or(true)); CHECK(tasks.remove("terr").value_or(false)); CHECK(!tasks.remove("terr").value_or(true)); CHECK(tasks.count().value_or(-1) == 21); } // --- settings: default fallback, override, typed reads --------------- { Settings settings(*db); CHECK_EQ(settings.get_int("connection.maxConcurrentDownloads"), 5); // built-in default CHECK(settings.set_raw("connection.maxConcurrentDownloads", "9").has_value()); CHECK_EQ(settings.get_int("connection.maxConcurrentDownloads"), 9); // override wins CHECK_EQ(settings.get_int("connection.maxActiveSegments"), 32); auto roots = settings.get_string_array("saveTo.allowedRoots"); CHECK_EQ(roots.size(), 1u); if (!roots.empty()) CHECK_EQ(roots.front(), std::string("~/Downloads")); auto ov = settings.overrides(); CHECK(ov.has_value() && ov->count("connection.maxConcurrentDownloads") == 1); } } TEST_MAIN()