diff --git a/src/Orbit.Application/Chat/Tools/Implementations/CreateSubHabitTool.cs b/src/Orbit.Application/Chat/Tools/Implementations/CreateSubHabitTool.cs index f4d1cbaf9..a364ce34d 100644 --- a/src/Orbit.Application/Chat/Tools/Implementations/CreateSubHabitTool.cs +++ b/src/Orbit.Application/Chat/Tools/Implementations/CreateSubHabitTool.cs @@ -99,6 +99,7 @@ public async Task ExecuteAsync(JsonElement args, Guid userId, Cancel IsBadHabit: isBadHabit, DueDate: dueDate, Emoji: emoji, + InheritParentFrequency: true, Options: new Orbit.Application.Habits.Commands.HabitCommandOptions( Days: days, DueTime: dueTime, diff --git a/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs b/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs index 67129b9af..97730c112 100644 --- a/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs @@ -8,6 +8,12 @@ namespace Orbit.Application.Habits.Commands; +/// +/// When true and / are unset, the child +/// inherits the parent's cadence instead of becoming a one-time task. Only the AI chat tool (which +/// treats an unspecified frequency as "match the parent") opts into this; the REST API always sends +/// the user's explicit choice, where an unset frequency means the user picked "one-time". +/// public record CreateSubHabitCommand( Guid UserId, Guid ParentHabitId, @@ -19,7 +25,8 @@ public record CreateSubHabitCommand( DateOnly? DueDate = null, HabitCommandOptions? Options = null, IReadOnlyList? TagIds = null, - string? Emoji = null) : IRequest>; + string? Emoji = null, + bool InheritParentFrequency = false) : IRequest>; public class CreateSubHabitCommandHandler( IGenericRepository habitRepository, @@ -61,11 +68,19 @@ public async Task> Handle(CreateSubHabitCommand request, Cancellati ? 0 : siblings.Max(h => h.Position ?? -1) + 1; + var frequencyUnit = request.FrequencyUnit; + var frequencyQuantity = request.FrequencyQuantity; + if (request.InheritParentFrequency) + { + frequencyUnit ??= parent.FrequencyUnit; + frequencyQuantity ??= parent.FrequencyQuantity; + } + var childResult = Habit.Create(new HabitCreateParams( request.UserId, request.Title, - request.FrequencyUnit ?? parent.FrequencyUnit, - request.FrequencyQuantity ?? parent.FrequencyQuantity, + frequencyUnit, + frequencyQuantity, childDueDate, request.Description, Emoji: request.Emoji, diff --git a/tests/Orbit.Application.Tests/Commands/Habits/CreateSubHabitCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/CreateSubHabitCommandHandlerTests.cs index 3366986f8..1e30194ae 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/CreateSubHabitCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/CreateSubHabitCommandHandlerTests.cs @@ -129,6 +129,56 @@ public async Task Handle_ForeignTagId_ReturnsFailureWithoutCreating() await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); } + [Fact] + public async Task Handle_OneTimeChildUnderRecurringParent_DoesNotInheritParentFrequency() + { + var parent = CreateParentHabit(); + _habitRepo.FindOneTrackedAsync( + Arg.Any>>(), + Arg.Any, IQueryable>?>(), + Arg.Any()) + .Returns(parent); + _habitRepo.FindAsync( + Arg.Any>>(), + Arg.Any()) + .Returns(new List { parent }.AsReadOnly()); + + var command = new CreateSubHabitCommand(UserId, parent.Id, "Child Task", null); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + await _habitRepo.Received(1).AddAsync( + Arg.Is(h => h.FrequencyUnit == null && h.FrequencyQuantity == null), + Arg.Any()); + } + + [Fact] + public async Task Handle_InheritParentFrequencyWithNoOverride_UsesParentCadence() + { + var parent = CreateParentHabit(); + _habitRepo.FindOneTrackedAsync( + Arg.Any>>(), + Arg.Any, IQueryable>?>(), + Arg.Any()) + .Returns(parent); + _habitRepo.FindAsync( + Arg.Any>>(), + Arg.Any()) + .Returns(new List { parent }.AsReadOnly()); + + var command = new CreateSubHabitCommand( + UserId, parent.Id, "Child Task", null, + InheritParentFrequency: true); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + await _habitRepo.Received(1).AddAsync( + Arg.Is(h => h.FrequencyUnit == FrequencyUnit.Day && h.FrequencyQuantity == 1), + Arg.Any()); + } + [Fact] public async Task Handle_ExceedsMaxDepth_ReturnsFailure() {