daemon: D3 (partial) — category.upsert/remove, queue.upsert, download.remove/addBatch/provideAuth
Closes a bounded, high-value slice of the remaining D3 stubs. settings.*/rules.*/ limiter.*/schedule.*/grabber.*/media.*/capture.*/queue.reorder/download.refreshUrl/ download.update stay deferred — reasons noted individually in deferrals.md (settings.* specifically: a real, large field<->key<->JSON-type mapping table across ~43 fields / ~50 SettingKeys, not started rather than rushed). category.upsert/remove: store/categories.hpp gains get/upsert/remove. upsert generates an id when absent and always ignores the payload's `builtin` (preserved from the existing row on replace, false on create); saveDir goes through the same fs::resolve_target canonicalize-and-root-check as download.add. remove refuses a builtin at both layers (dispatcher's -32602 pre-check; the store's own "DELETE ... AND builtin = 0" as defense in depth) and reassigns member tasks to reassignTo (default "general") inside one transaction before deleting the row. queue.upsert: store/queues.hpp gains get/upsert (set_state already existed from D4b). Same create-generates-id pattern; a create always starts 'stopped', a replace keeps the queue's current run state (upsert edits config, not run state — that's queue.start/stop). Also fixed in passing: on_complete has been a real column since migration 0001 but Queues::list/get never projected it onto Queue.onComplete. download.remove: cancels with discard_partial=true (always drops the .veloxpart pair — unlike download.cancel, which keeps them, the row is gone either way), deletes the finished file only when deleteFile is true and the task was complete (best-effort), deletes the row (segments cascade via the FK), and publishes event.task.removed (closing the last open note under D5). download.addBatch: on_download_add's body is now a shared add_one(), called once per item after merging each item's unset fields against params.defaults. download.provideAuth: forwards to EnginePort::provide_auth via a new TaskActionPort::provide_auth. `remember`/persisting to the Secret Service is accepted but not acted on — nothing in this build talks to libsecret yet. Verified against real veloxd + tools/testserver: category create/replace/ remove-with-reassignment, queue create/replace-keeps-run-state, a batch add sharing defaults.saveDir, and download.remove with deleteFile actually deleting the file and the task then 404ing download.get with -32010. Full ctest: 39/39. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// store/categories + store/queues: the two D3 handlers GUI's category panel and queue
|
||||
// view need against a real daemon.
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "check.hpp"
|
||||
@@ -73,6 +75,105 @@ void run() {
|
||||
CHECK_EQ(ids[2], std::string("t0"));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Categories::upsert: create generates an id, builtin is never settable ------
|
||||
{
|
||||
Categories categories(*db);
|
||||
|
||||
velox::proto::Category in;
|
||||
in.name = "ISOs";
|
||||
in.saveDir = "/tmp/isos";
|
||||
in.extensions = {"iso"};
|
||||
in.builtin = true; // ignored on create: a client cannot mint a builtin category
|
||||
auto created = categories.upsert(in);
|
||||
CHECK(created.has_value());
|
||||
if (created) {
|
||||
CHECK(!created->categoryId.empty());
|
||||
CHECK(!created->builtin);
|
||||
|
||||
// A replace keeps builtin=false too, and can rename/re-point.
|
||||
velox::proto::Category patch = *created;
|
||||
patch.name = "ISO Images";
|
||||
patch.builtin = true; // still ignored
|
||||
auto replaced = categories.upsert(patch);
|
||||
CHECK(replaced.has_value());
|
||||
if (replaced) {
|
||||
CHECK_EQ(replaced->categoryId, created->categoryId);
|
||||
CHECK_EQ(replaced->name, std::string("ISO Images"));
|
||||
CHECK(!replaced->builtin);
|
||||
}
|
||||
|
||||
// A builtin category is untouched by remove(), and its tasks are not
|
||||
// reassigned away from it — the store enforces this even without the
|
||||
// dispatcher's own -32602 pre-check.
|
||||
auto builtin_attempt = categories.remove("general", std::nullopt);
|
||||
CHECK(builtin_attempt.has_value());
|
||||
if (builtin_attempt) CHECK(!builtin_attempt->removed);
|
||||
|
||||
// remove() reassigns member tasks (default target: "general") and deletes
|
||||
// the row.
|
||||
Tasks tasks(*db);
|
||||
TaskRow r;
|
||||
r.task_id = "cat-owner";
|
||||
r.url = "https://example.com/x";
|
||||
r.save_dir = "/tmp";
|
||||
r.filename = "x";
|
||||
r.created_at = "2026-09-11T00:00:00Z";
|
||||
r.category_id = created->categoryId;
|
||||
CHECK(tasks.insert(r).has_value());
|
||||
|
||||
auto removed = categories.remove(created->categoryId, std::nullopt);
|
||||
CHECK(removed.has_value());
|
||||
if (removed) {
|
||||
CHECK(removed->removed);
|
||||
CHECK_EQ(removed->reassigned_task_ids.size(), std::size_t{1});
|
||||
CHECK_EQ(removed->reassigned_task_ids[0], std::string("cat-owner"));
|
||||
}
|
||||
auto owner = tasks.get("cat-owner");
|
||||
CHECK(owner.has_value() && owner->has_value());
|
||||
if (owner && *owner)
|
||||
CHECK_EQ((*owner)->category_id.value_or(""), std::string("general"));
|
||||
|
||||
// Gone: a second remove() finds nothing.
|
||||
auto gone = categories.remove(created->categoryId, std::nullopt);
|
||||
CHECK(gone.has_value());
|
||||
if (gone) CHECK(!gone->removed);
|
||||
|
||||
tasks.remove("cat-owner");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Queues::upsert: create generates an id; replace keeps the run state --------
|
||||
{
|
||||
Queues queues(*db);
|
||||
|
||||
velox::proto::Queue in;
|
||||
in.name = "Nightly";
|
||||
in.state = velox::proto::QueueState::Running; // ignored on create: always 'stopped'
|
||||
in.maxConcurrent = 3;
|
||||
auto created = queues.upsert(in);
|
||||
CHECK(created.has_value());
|
||||
if (created) {
|
||||
CHECK(!created->queueId.empty());
|
||||
CHECK(created->state == velox::proto::QueueState::Stopped);
|
||||
|
||||
CHECK(queues.set_state(created->queueId, "running").has_value());
|
||||
|
||||
velox::proto::Queue patch = *created;
|
||||
patch.name = "Nightly Batch";
|
||||
patch.maxConcurrent = 5;
|
||||
patch.state = velox::proto::QueueState::Stopped; // ignored on replace too
|
||||
auto replaced = queues.upsert(patch);
|
||||
CHECK(replaced.has_value());
|
||||
if (replaced) {
|
||||
CHECK_EQ(replaced->name, std::string("Nightly Batch"));
|
||||
CHECK_EQ(replaced->maxConcurrent, std::int64_t{5});
|
||||
// Run state survived the config edit — still 'running' from set_state above,
|
||||
// not reset by the payload's (ignored) 'stopped'.
|
||||
CHECK(replaced->state == velox::proto::QueueState::Running);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_MAIN()
|
||||
|
||||
Reference in New Issue
Block a user