Skip to content
Merged
3 changes: 3 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,6 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
they were actually completed, while the parent recurring task continues to
track the occurrence date. Thanks to @raphaelfaouakhiri for reporting and
fixing this.
- (#2173, #2175) Completion-anchored recurring tasks with materialized
occurrence notes now schedule the next occurrence from the day the occurrence
was actually completed. Thanks to @chmac for reporting and fixing this.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"@fullcalendar/multimonth": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@modelcontextprotocol/sdk": "^1.12.1",
"@tasknotes/model": "0.2.1",
"@tasknotes/model": "0.3.0-rc.5",
"chrono-node": "^2.7.5",
"date-fns": "^4.1.0",
"ical.js": "^2.2.1",
Expand Down
2 changes: 2 additions & 0 deletions src/services/TaskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,8 @@ export class TaskService {
parentTask,
completedStatus: this.normalizeStatusValue(newValue),
currentTimestamp,
completionDate:
updatedOccurrence.completedDate || getCurrentDateString(),
maintainDueDateOffsetInRecurring:
this.plugin.settings.maintainDueDateOffsetInRecurring,
})
Expand Down
9 changes: 8 additions & 1 deletion tests/unit/api/tasknotes-api-v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,14 @@ describe("TaskNotesApiV1", () => {
})
);

expect(api.model.validateTask(createTask()).valid).toBe(true);
expect(
api.model.validateTask(
createTask({
dateCreated: "2026-06-01T12:00:00Z",
dateModified: "2026-06-01T12:00:00Z",
})
).valid
).toBe(true);
expect(api.model.validatePatch({ timeEntries: "invalid" as never })).toEqual(
expect.objectContaining({
valid: false,
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/services/task-occurrence-materialization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ jest.mock("../../../src/utils/filenameGenerator", () => ({
}));

describe("TaskService materialized occurrences", () => {
beforeEach(() => {
const dateUtils = jest.requireMock("../../../src/utils/dateUtils");
dateUtils.getCurrentDateString.mockReturnValue("2025-01-01");
});

function createService(tasks: Record<string, TaskInfo> = {}) {
const frontmatterByPath = new Map<string, Record<string, unknown>>();
const plugin = PluginFactory.createMockPlugin({
Expand Down Expand Up @@ -454,6 +459,56 @@ describe("TaskService materialized occurrences", () => {
expect(frontmatterByPath.get(parent.path)?.skipped_instances).toBeUndefined();
});

it("advances completion-anchored parents from the actual completion date", async () => {
const dateUtils = jest.requireMock("../../../src/utils/dateUtils");
dateUtils.getCurrentDateString.mockReturnValue("2026-07-30");
const parent = TaskFactory.createTask({
title: "Weekly task",
path: "Tasks/Weekly task.md",
recurrence: "DTSTART:20260727;FREQ=WEEKLY",
recurrence_anchor: "completion",
scheduled: "2026-07-27",
complete_instances: [],
occurrence_materialization: "on_completion",
});
const occurrence = TaskFactory.createTask({
title: "Weekly task",
path: "Tasks/Weekly task 2026-07-28.md",
status: "open",
recurrence_parent: "[[Tasks/Weekly task]]",
occurrence_date: "2026-07-28",
scheduled: "2026-07-29",
});
const { taskService, frontmatterByPath } = createService({
[parent.path]: parent,
[occurrence.path]: occurrence,
});

const completedOccurrence = await taskService.updateProperty(
occurrence,
"status",
"done"
);

expect(frontmatterByPath.get(occurrence.path)).toMatchObject({
status: "done",
completedDate: "2026-07-30",
});
expect(frontmatterByPath.get(parent.path)).toMatchObject({
complete_instances: ["2026-07-28"],
recurrence: "DTSTART:20260730;FREQ=WEEKLY",
scheduled: "2026-08-06",
});

await taskService.updateProperty(completedOccurrence, "status", "open");

expect(frontmatterByPath.get(parent.path)).toMatchObject({
recurrence: "DTSTART:20260730;FREQ=WEEKLY",
scheduled: "2026-08-06",
});
expect(frontmatterByPath.get(parent.path)?.complete_instances).toBeUndefined();
});

describe("issue #2126 — occurrence filename template wiring", () => {
const filenameGenerator = jest.requireMock("../../../src/utils/filenameGenerator");

Expand Down
Loading