feat(tasks): add child task entry defaults

This commit is contained in:
Ashley Venn 2026-06-24 11:54:25 -07:00
parent 92c05d8c1e
commit eba197d5ae
4 changed files with 199 additions and 0 deletions

View file

@ -47,6 +47,8 @@ Execution notes:
Recommended Codex level: medium Recommended Codex level: medium
Status: Completed
Tasks: Tasks:
Support row-style child entry data: Support row-style child entry data:
@ -72,6 +74,16 @@ Acceptance criteria:
- Tests cover inherited project and overridden project. - Tests cover inherited project and overridden project.
- Tests cover reward `not set` remaining distinct from very low reward. - Tests cover reward `not set` remaining distinct from very low reward.
Execution notes:
- Added `ChildTaskEntry` for row-style child creation data.
- Child entries support title, nullable priority, reward, difficulty, duration, and optional project override.
- Child entries inherit the parent project when no override is supplied.
- Missing priority remains null so children without explicit priority can preserve row insertion order.
- Missing reward remains `RewardLevel.notSet`, distinct from `RewardLevel.veryLow`.
- Added tests for explicit child fields, project inheritance/override, reward not-set behavior, and no-priority row order.
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite. BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite.
## Chunk 7.3 — Child-task edge-case regression test suite ## Chunk 7.3 — Child-task edge-case regression test suite

View file

@ -47,6 +47,8 @@ Execution notes:
Recommended Codex level: medium Recommended Codex level: medium
Status: Completed
Tasks: Tasks:
Support row-style child entry data: Support row-style child entry data:
@ -72,6 +74,16 @@ Acceptance criteria:
- Tests cover inherited project and overridden project. - Tests cover inherited project and overridden project.
- Tests cover reward `not set` remaining distinct from very low reward. - Tests cover reward `not set` remaining distinct from very low reward.
Execution notes:
- Added `ChildTaskEntry` for row-style child creation data.
- Child entries support title, nullable priority, reward, difficulty, duration, and optional project override.
- Child entries inherit the parent project when no override is supplied.
- Missing priority remains null so children without explicit priority can preserve row insertion order.
- Missing reward remains `RewardLevel.notSet`, distinct from `RewardLevel.veryLow`.
- Added tests for explicit child fields, project inheritance/override, reward not-set behavior, and no-priority row order.
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite. BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite.
## Chunk 7.3 — Child-task edge-case regression test suite ## Chunk 7.3 — Child-task edge-case regression test suite

View file

@ -6,6 +6,77 @@
import 'models.dart'; import 'models.dart';
/// Row-style input for creating one child task.
///
/// Priority is nullable on purpose. A null priority means the user did not set
/// one, so callers should preserve row insertion order instead of treating the
/// child as medium priority.
class ChildTaskEntry {
const ChildTaskEntry({
required this.id,
required this.title,
required this.createdAt,
this.priority,
this.reward = RewardLevel.notSet,
this.difficulty = DifficultyLevel.notSet,
this.durationMinutes,
this.projectId,
this.updatedAt,
});
/// Caller-generated child id.
final String id;
/// User-entered child title.
final String title;
/// Creation timestamp supplied by the caller.
final DateTime createdAt;
/// Optional explicit child priority.
final PriorityLevel? priority;
/// Optional explicit child reward. Missing reward stays `notSet`.
final RewardLevel reward;
/// Optional explicit child difficulty.
final DifficultyLevel difficulty;
/// Optional child duration estimate.
final int? durationMinutes;
/// Optional project override. Null means inherit the parent project.
final String? projectId;
/// Optional update timestamp.
final DateTime? updatedAt;
/// Convert this entry into a child [Task] owned by [parent].
Task toTask({
required Task parent,
}) {
final trimmedTitle = title.trim();
if (trimmedTitle.isEmpty) {
throw ArgumentError.value(title, 'title', 'Title is required.');
}
return Task(
id: id,
title: trimmedTitle,
projectId: projectId ?? parent.projectId,
type: TaskType.flexible,
status: TaskStatus.backlog,
priority: priority,
reward: reward,
difficulty: difficulty,
durationMinutes: durationMinutes,
parentTaskId: parent.id,
createdAt: createdAt,
updatedAt: updatedAt ?? createdAt,
);
}
}
/// Read-only parent/child projection over a task list. /// Read-only parent/child projection over a task list.
/// ///
/// This is intentionally not a scheduler. It answers ownership questions such /// This is intentionally not a scheduler. It answers ownership questions such

View file

@ -112,6 +112,110 @@ void main() {
expect(unchangedParent.scheduledStart, isNull); expect(unchangedParent.scheduledStart, isNull);
}); });
}); });
group('Child task entry defaults', () {
final now = DateTime(2026, 6, 19, 12);
test('child creation supports explicit row fields', () {
final parent = task(
id: 'parent',
title: 'Clean kitchen',
createdAt: now,
).copyWith(projectId: 'home');
final child = ChildTaskEntry(
id: 'child',
title: ' Clear sink ',
createdAt: now,
priority: PriorityLevel.high,
reward: RewardLevel.high,
difficulty: DifficultyLevel.hard,
durationMinutes: 20,
).toTask(parent: parent);
expect(child.id, 'child');
expect(child.title, 'Clear sink');
expect(child.parentTaskId, parent.id);
expect(child.projectId, parent.projectId);
expect(child.priority, PriorityLevel.high);
expect(child.reward, RewardLevel.high);
expect(child.difficulty, DifficultyLevel.hard);
expect(child.durationMinutes, 20);
expect(child.status, TaskStatus.backlog);
});
test('child inherits parent project unless project override is supplied',
() {
final parent = task(
id: 'parent',
title: 'Plan trip',
createdAt: now,
).copyWith(projectId: 'travel');
final inherited = ChildTaskEntry(
id: 'inherited',
title: 'Book train',
createdAt: now,
).toTask(parent: parent);
final overridden = ChildTaskEntry(
id: 'overridden',
title: 'Ask partner',
createdAt: now,
projectId: 'relationships',
).toTask(parent: parent);
expect(inherited.projectId, 'travel');
expect(overridden.projectId, 'relationships');
});
test('missing reward remains not set and distinct from very low reward',
() {
final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
final noReward = ChildTaskEntry(
id: 'no-reward',
title: 'Clear sink',
createdAt: now,
).toTask(parent: parent);
final veryLowReward = ChildTaskEntry(
id: 'very-low',
title: 'Wipe counter',
createdAt: now,
reward: RewardLevel.veryLow,
).toTask(parent: parent);
expect(noReward.reward, RewardLevel.notSet);
expect(veryLowReward.reward, RewardLevel.veryLow);
expect(noReward.reward, isNot(veryLowReward.reward));
});
test('children without priority preserve row insertion order', () {
final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
final first = ChildTaskEntry(
id: 'first',
title: 'Clear sink',
createdAt: now,
).toTask(parent: parent);
final second = ChildTaskEntry(
id: 'second',
title: 'Wipe counter',
createdAt: now,
).toTask(parent: parent);
final third = ChildTaskEntry(
id: 'third',
title: 'Sweep floor',
createdAt: now,
).toTask(parent: parent);
final view = ChildTaskView(tasks: [parent, first, second, third]);
expect(first.priority, isNull);
expect(second.priority, isNull);
expect(third.priority, isNull);
expect(view.childrenOf(parent).map((child) => child.id), [
'first',
'second',
'third',
]);
});
});
} }
Task task({ Task task({