Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/commands/CmdDone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,17 @@ int CmdDone::execute(std::string&) {

updateRecurrenceMask(task);

// Auto-complete all pending/waiting descendants (no prompt).
// Auto-complete all pending/waiting descendants (no prompt, no hooks).
// RAII guard ensures hooks are re-enabled even if tdb2.modify() throws.
struct HooksGuard {
Hooks& hooks;
bool saved;
explicit HooksGuard(Hooks& h, bool val) : hooks(h), saved(h.enable(val)) {}
~HooksGuard() { hooks.enable(saved); }
HooksGuard(const HooksGuard&) = delete;
HooksGuard& operator=(const HooksGuard&) = delete;
};
HooksGuard hooksGuard(Context::getContext().hooks, false);
auto desc = Context::getContext().tdb2.descendants(task.get("uuid"));
for (auto& d : desc) {
if (d.getStatus() == Task::pending || d.getStatus() == Task::waiting) {
Expand Down
35 changes: 35 additions & 0 deletions test/hooks.on-modify.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,41 @@ def test_onmodify_revert_changes(self):
hook.assertTriggeredCount(1)
hook.assertExitcode(0)

def test_recursive_done_suppresses_child_hooks(self):
"""on-modify hook fires once for parent only when recursively completing descendants."""
hookname = "on-modify-accept"
self.t.hooks.add_default(hookname, log=True)

# Create a parent → child → grandchild tree to exercise multi-level recursion.
self.t("add Parent Task")
all_tasks = self.t.export()
parent_uuid = next(t["uuid"] for t in all_tasks if t["description"] == "Parent Task")

self.t(f"add Child Task parent_id:{parent_uuid}")
all_tasks = self.t.export()
child_uuid = next(t["uuid"] for t in all_tasks if t["description"] == "Child Task")

self.t(f"add Grandchild Task parent_id:{child_uuid}")

# Complete the parent — should recursively complete child and grandchild.
self.t(f"{parent_uuid} done")

# Hook must have fired exactly once (for the parent only, not descendants).
hook = self.t.hooks[hookname]
hook.assertTriggeredCount(1)

# The single hook invocation must be for the parent task, not a descendant.
logs = hook.get_logs()
hooked_uuid = logs["input"]["json"][1]["uuid"] # second JSON = new task
self.assertEqual(hooked_uuid, parent_uuid)

# All three tasks must be completed in the DB.
tasks = self.t.export("+COMPLETED")
descriptions = [t["description"] for t in tasks]
self.assertIn("Parent Task", descriptions)
self.assertIn("Child Task", descriptions)
self.assertIn("Grandchild Task", descriptions)

def test_onmodify_escaped_backslash(self):
"""on-modify-accept - a well-behaved, successful, on-modify hook."""
# Create a task with a slash-escaped tab in it, avoiding TaskWarrior to ensure
Expand Down
Loading