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
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public async Task<ToolResult> 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,
Expand Down
21 changes: 18 additions & 3 deletions src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

namespace Orbit.Application.Habits.Commands;

/// <param name="InheritParentFrequency">
/// When true and <see cref="FrequencyUnit"/>/<see cref="FrequencyQuantity"/> 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".
/// </param>
public record CreateSubHabitCommand(
Guid UserId,
Guid ParentHabitId,
Expand All @@ -19,7 +25,8 @@ public record CreateSubHabitCommand(
DateOnly? DueDate = null,
HabitCommandOptions? Options = null,
IReadOnlyList<Guid>? TagIds = null,
string? Emoji = null) : IRequest<Result<Guid>>;
string? Emoji = null,
bool InheritParentFrequency = false) : IRequest<Result<Guid>>;

public class CreateSubHabitCommandHandler(
IGenericRepository<Habit> habitRepository,
Expand Down Expand Up @@ -61,11 +68,19 @@ public async Task<Result<Guid>> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,56 @@ public async Task Handle_ForeignTagId_ReturnsFailureWithoutCreating()
await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>());
}

[Fact]
public async Task Handle_OneTimeChildUnderRecurringParent_DoesNotInheritParentFrequency()
{
var parent = CreateParentHabit();
_habitRepo.FindOneTrackedAsync(
Arg.Any<Expression<Func<Habit, bool>>>(),
Arg.Any<Func<IQueryable<Habit>, IQueryable<Habit>>?>(),
Arg.Any<CancellationToken>())
.Returns(parent);
_habitRepo.FindAsync(
Arg.Any<Expression<Func<Habit, bool>>>(),
Arg.Any<CancellationToken>())
.Returns(new List<Habit> { 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<Habit>(h => h.FrequencyUnit == null && h.FrequencyQuantity == null),
Arg.Any<CancellationToken>());
}

[Fact]
public async Task Handle_InheritParentFrequencyWithNoOverride_UsesParentCadence()
{
var parent = CreateParentHabit();
_habitRepo.FindOneTrackedAsync(
Arg.Any<Expression<Func<Habit, bool>>>(),
Arg.Any<Func<IQueryable<Habit>, IQueryable<Habit>>?>(),
Arg.Any<CancellationToken>())
.Returns(parent);
_habitRepo.FindAsync(
Arg.Any<Expression<Func<Habit, bool>>>(),
Arg.Any<CancellationToken>())
.Returns(new List<Habit> { 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<Habit>(h => h.FrequencyUnit == FrequencyUnit.Day && h.FrequencyQuantity == 1),
Arg.Any<CancellationToken>());
}

[Fact]
public async Task Handle_ExceedsMaxDepth_ReturnsFailure()
{
Expand Down
Loading