Repository: obsidian-tasks-group/obsidian-tasks (ts)

Issue:
Recurrence code is not taking account of 'next symbol' setting
### Please check that this issue hasn't been reported before.

- [X] I searched previous [Bug Reports](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22+sort%3Acomments-desc) didn't find any similar reports.

### Expected Behavior

When I have set up custom statuses to go through a 3-way set of symbols like this:

`!` -> `D` -> `X` -> `!`

![image](https://user-images.githubusercontent.com/4840096/213404510-250a2b4c-c7bc-46a2-89f3-a174da33b11a.png)

And I have a recurring task like this:

```text
- [D] #task Important non-recurring task 🔁 every day 📅 2023-01-28
```

I expect the new lines to look like this - with the symbol `!` as that is the one that follows `X`

```text
- [!] #task Important non-recurring task 🔁 every day 📅 2023-01-29
- [X] #task Important non-recurring task 🔁 every day 📅 2023-01-28 ✅ 2023-01-19
```




### Current behaviour

The new lines look like this:


```text
- [D] #task Important non-recurring task 🔁 every day 📅 2023-01-29
- [X] #task Important non-recurring task 🔁 every day 📅 2023-01-28 ✅ 2023-01-19
```

### Steps to reproduce

1. Set up these settings (see next comment for settings file that can be used):

![image](https://user-images.githubusercontent.com/4840096/213405160-94cfd2a2-25ce-4c7c-8f0a-11dcc0ad9822.png)

2. Paste this line:


```text
- [D] #task Important non-recurring task 🔁 every day 📅 2023-01-28
```

3. toggle the task


### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.1.12

### Tasks Plugin Version

Commit d98f427b7536ed36684fb5cbb855eba4829c9ad9 which was squashed in to fbd3c38570b3afa533734feb8760cf04c30bd3f9

### Checks

- [ ] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

_No response_

Pull request:
fix: Correct several bugs in next status with recurrence
<!--- Provide a general summary of your changes in the Title above -->

# Description

Fixed:

- When the chain of statuses is 3 or more long (such as `! -> D -> X -> !` and a `D` task is toggled, the next recurrence starts with `!` now. Previously it started with `D`.
    - This was logged in #1552
- `Status.isCompleted()` now checks if `Status.DONE` instead of being determined by the symbol being `x` or `X`. So statuses like `1 -> 2 -> 3 -> 1` handle completion correctly.
- There were situations where the next recurrence could be empty `[]` (and so a broken task).

Coding changes:

- Introduce types `StatusCollection` and `StatusCollectionEntry` - to replace `Array<[string, string, string]>`
- StatusCollectionEntry now has a 4th value - the status type (such as `IN_PROGRESS`)

Documentation:

- A consequence of the changes is a fixed status type in the docs

## Motivation and Context

Fixes #1552 - and 2 other bugs found in testing this.

## How has this been tested?

- Unit tests
- Manual exploratory testing

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)


Patch:
diff --git a/docs/how-to/set-up-custom-statuses.md b/docs/how-to/set-up-custom-statuses.md
index 70abc0dc..3ff91595 100644
--- a/docs/how-to/set-up-custom-statuses.md
+++ b/docs/how-to/set-up-custom-statuses.md
@@ -76,7 +76,7 @@ Suppose that you wanted to create a set of 3 statuses that cycle between each ot
 | Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `!` | Important | `D` | `TODO` | Yes |
-| `D` | Doing - Important | `X` | `TODO` | Yes |
+| `D` | Doing - Important | `X` | `IN_PROGRESS` | Yes |
 | `X` | Done - Important | `!` | `DONE` | Yes |
 
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
diff --git a/src/Config/SettingsTab.ts b/src/Config/SettingsTab.ts
index b6547ddd..cbc76e1e 100644
--- a/src/Config/SettingsTab.ts
+++ b/src/Config/SettingsTab.ts
@@ -3,6 +3,7 @@ import { StatusConfiguration, StatusType } from '../StatusConfiguration';
 import type TasksPlugin from '../main';
 import { StatusRegistry } from '../StatusRegistry';
 import { Status } from '../Status';
+import type { StatusCollection } from '../StatusCollection';
 import type { HeadingState } from './Settings';
 import { getSettings, isFeatureEnabled, updateGeneralSetting, updateSettings } from './Settings';
 import { StatusSettings } from './StatusSettings';
@@ -548,7 +549,7 @@ function createRowForTaskStatus(
 }
 
 async function addCustomStatesToSettings(
-    supportedStatuses: Array<[string, string, string]>,
+    supportedStatuses: StatusCollection,
     statusSettings: StatusSettings,
     settings: SettingsTab,
 ) {
diff --git a/src/Config/StatusSettings.ts b/src/Config/StatusSettings.ts
index a2b948b7..756a26d1 100644
--- a/src/Config/StatusSettings.ts
+++ b/src/Config/StatusSettings.ts
@@ -1,6 +1,7 @@
 import type { StatusConfiguration } from '../StatusConfiguration';
 import type { StatusRegistry } from '../StatusRegistry';
 import { Status } from '../Status';
+import type { StatusCollection } from '../StatusCollection';
 
 /**
  * Class for encapsulating the settings that control custom statuses.
@@ -112,7 +113,7 @@ export class StatusSettings {
      */
     public static bulkAddStatusCollection(
         statusSettings: StatusSettings,
-        supportedStatuses: Array<[string, string, string]>,
+        supportedStatuses: StatusCollection,
     ): string[] {
         const notices: string[] = [];
         supportedStatuses.forEach((importedStatus) => {
diff --git a/src/Config/StatusSettingsHelpers.ts b/src/Config/StatusSettingsHelpers.ts
index 1b6f23da..a22baf8d 100644
--- a/src/Config/StatusSettingsHelpers.ts
+++ b/src/Config/StatusSettingsHelpers.ts
@@ -4,6 +4,8 @@
  * be written for its contents.
  */
 
+import type { StatusCollection } from '../StatusCollection';
+
 /**
  * Status supported by the Minimal theme. {@link https://github.com/kepano/obsidian-minimal}
  * Values recognised by Tasks are excluded.
@@ -11,27 +13,27 @@
  * @see {@link StatusSettings.bulkAddStatusCollection}
  */
 export function minimalSupportedStatuses() {
-    const zzz: Array<[string, string, string]> = [
-        ['>', 'Forwarded', 'x'],
-        ['<', 'Schedule', 'x'],
-        ['?', 'Question', 'x'],
-        // ['/', 'Incomplete', 'x'], This is used for In Progress
-        ['!', 'Important', 'x'],
-        ['"', 'Quote', 'x'],
-        ['-', 'Canceled', 'x'],
-        ['*', 'Star', 'x'],
-        ['l', 'Location', 'x'],
-        ['i', 'Info', 'x'],
-        ['S', 'Amount/savings/money', 'x'],
-        ['I', 'Idea/lightbulb', 'x'],
-        ['f', 'Fire', 'x'],
-        ['k', 'Key', 'x'],
-        ['u', 'Up', 'x'],
-        ['d', 'Down', 'x'],
-        ['w', 'Win', 'x'],
-        ['p', 'Pros', 'x'],
-        ['c', 'Cons', 'x'],
-        ['b', 'Bookmark', 'x'],
+    const zzz: StatusCollection = [
+        ['>', 'Forwarded', 'x', 'TODO'],
+        ['<', 'Schedule', 'x', 'TODO'],
+        ['?', 'Question', 'x', 'TODO'],
+        // ['/', 'Incomplete', 'x', 'IN_PROGRESS'], This is used for In Progress
+        ['!', 'Important', 'x', 'TODO'],
+        ['"', 'Quote', 'x', 'TODO'],
+        ['-', 'Canceled', 'x', 'CANCELLED'],
+        ['*', 'Star', 'x', 'TODO'],
+        ['l', 'Location', 'x', 'TODO'],
+        ['i', 'Info', 'x', 'TODO'],
+        ['S', 'Amount/savings/money', 'x', 'TODO'],
+        ['I', 'Idea/lightbulb', 'x', 'TODO'],
+        ['f', 'Fire', 'x', 'TODO'],
+        ['k', 'Key', 'x', 'TODO'],
+        ['u', 'Up', 'x', 'TODO'],
+        ['d', 'Down', 'x', 'TODO'],
+        ['w', 'Win', 'x', 'TODO'],
+        ['p', 'Pros', 'x', 'TODO'],
+        ['c', 'Cons', 'x', 'TODO'],
+        ['b', 'Bookmark', 'x', 'TODO'],
     ];
     return zzz;
 }
@@ -43,30 +45,30 @@ export function minimalSupportedStatuses() {
  * @see {@link StatusSettings.bulkAddStatusCollection}
  */
 export function itsSupportedStatuses() {
-    const zzz: Array<[string, string, string]> = [
-        //['X', 'Checked', 'x'],
-        ['>', 'Forward', 'x'],
-        ['D', 'Deferred/Scheduled', 'x'],
-        //['-', 'Cancelled/Non-Task', 'x'],
-        ['?', 'Question', 'x'],
-        ['!', 'Important', 'x'],
-        ['+', 'Add', 'x'],
-        //['/', 'Half Done', 'x'],
-        ['R', 'Research', 'x'],
-        ['i', 'Idea', 'x'],
-        ['B', 'Brainstorm', 'x'],
-        ['P', 'Pro', 'x'],
-        ['C', 'Con', 'x'],
-        ['I', 'Info', 'x'],
-        ['Q', 'Quote', 'x'],
-        ['N', 'Note', 'x'],
-        ['b', 'Bookmark', 'x'],
-        ['p', 'Paraphrase', 'x'],
-        ['E', 'Example', 'x'],
-        ['L', 'Location', 'x'],
-        ['A', 'Answer', 'x'],
-        ['r', 'Reward', 'x'],
-        ['c', 'Choice', 'x'],
+    const zzz: StatusCollection = [
+        //['X', 'Checked', 'x', 'TODO'],
+        ['>', 'Forward', 'x', 'TODO'],
+        ['D', 'Deferred/Scheduled', 'x', 'TODO'],
+        //['-', 'Cancelled/Non-Task', 'x', 'CANCELLED'],
+        ['?', 'Question', 'x', 'TODO'],
+        ['!', 'Important', 'x', 'TODO'],
+        ['+', 'Add', 'x', 'TODO'],
+        //['/', 'Half Done', 'x', 'IN_PROGRESS'],
+        ['R', 'Research', 'x', 'TODO'],
+        ['i', 'Idea', 'x', 'TODO'],
+        ['B', 'Brainstorm', 'x', 'TODO'],
+        ['P', 'Pro', 'x', 'TODO'],
+        ['C', 'Con', 'x', 'TODO'],
+        ['I', 'Info', 'x', 'TODO'],
+        ['Q', 'Quote', 'x', 'TODO'],
+        ['N', 'Note', 'x', 'TODO'],
+        ['b', 'Bookmark', 'x', 'TODO'],
+        ['p', 'Paraphrase', 'x', 'TODO'],
+        ['E', 'Example', 'x', 'TODO'],
+        ['L', 'Location', 'x', 'TODO'],
+        ['A', 'Answer', 'x', 'TODO'],
+        ['r', 'Reward', 'x', 'TODO'],
+        ['c', 'Choice', 'x', 'TODO'],
     ];
     return zzz;
 }
diff --git a/src/Status.ts b/src/Status.ts
index 1111f305..fd6e84ca 100644
--- a/src/Status.ts
+++ b/src/Status.ts
@@ -1,4 +1,5 @@
 import { StatusConfiguration, StatusType } from './StatusConfiguration';
+import type { StatusCollectionEntry } from './StatusCollection';
 
 /**
  * Tracks the possible states that a task can be in.
@@ -191,12 +192,11 @@ export class Status {
     /**
      * Helper function for bulk-importing settings from arrays of strings.
      *
-     * The type is deduced, for a few common status indicators.
-     * @param imported An array of indicator, name, next indicator
+     * @param imported An array of indicator, name, next indicator, status type
      */
-    static createFromImportedValue(imported: [string, string, string]) {
+    static createFromImportedValue(imported: StatusCollectionEntry) {
         const indicator = imported[0];
-        const type = Status.getTypeForUnknownIndicator(indicator);
+        const type = Status.getTypeFromStatusTypeString(imported[3]);
         return new Status(new StatusConfiguration(indicator, imported[1], imported[2], false, type));
     }
 
@@ -208,7 +208,7 @@ export class Status {
      * @memberof Status
      */
     public isCompleted(): boolean {
-        return this.indicator === 'x' || this.indicator === 'X';
+        return this.type === StatusType.DONE;
     }
 
     /**
diff --git a/src/StatusCollection.ts b/src/StatusCollection.ts
new file mode 100644
index 00000000..41b16e4c
--- /dev/null
+++ b/src/StatusCollection.ts
@@ -0,0 +1,11 @@
+/**
+ * The type used for a single entry in bulk imports of pre-created sets of statuses, such as for Themes or CSS Snippets.
+ * The values are: indicator, name, next indicator, status type (must be one of the values in {@link StatusType}
+ */
+export type StatusCollectionEntry = [string, string, string, string];
+
+/**
+ * The type used for bulk imports of pre-created sets of statuses, such as for Themes or CSS Snippets.
+ * See {@link Status.createFromImportedValue}
+ */
+export type StatusCollection = Array<StatusCollectionEntry>;
diff --git a/src/StatusRegistry.ts b/src/StatusRegistry.ts
index 412ba290..f9ed199e 100644
--- a/src/StatusRegistry.ts
+++ b/src/StatusRegistry.ts
@@ -1,5 +1,5 @@
 import { Status } from './Status';
-import { StatusConfiguration } from './StatusConfiguration';
+import { StatusConfiguration, StatusType } from './StatusConfiguration';
 import type { Task } from './Task';
 
 /**
@@ -149,6 +149,7 @@ export class StatusRegistry {
      *
      * @return {*}  {Status}
      * @memberof StatusRegistry
+     * @see getNextStatusOrCreate
      */
     public getNextStatus(status: Status): Status {
         if (status.nextStatusIndicator !== '') {
@@ -160,6 +161,24 @@ export class StatusRegistry {
         return Status.EMPTY;
     }
 
+    /**
+     * Return the next status if it exists, and if not, create a new
+     * TODO status using the requested next symbol.
+     *
+     * @return {*}  {Status}
+     * @memberof StatusRegistry
+     * @see getNextStatus
+     */
+    public getNextStatusOrCreate(status: Status): Status {
+        const nextStatus = this.getNextStatus(status);
+        if (nextStatus.type !== StatusType.EMPTY) {
+            return nextStatus;
+        }
+        // status is configured to advance to a symbol that is not registered.
+        // So we go ahead and create it anyway - we just cannot give it a meaningful name.
+        return Status.createUnknownStatus(status.nextStatusIndicator);
+    }
+
     /**
      * Find any statuses in the given tasks that are not known to this registry.
      * This can be used to add all unknown status types to the settings,
diff --git a/src/Task.ts b/src/Task.ts
index e58aad8c..fef37327 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -4,7 +4,7 @@ import type { TaskLayoutComponent } from './TaskLayout';
 import { Recurrence } from './Recurrence';
 import { getSettings } from './Config/Settings';
 import { StatusRegistry } from './StatusRegistry';
-import { Status } from './Status';
+import type { Status } from './Status';
 import { Urgency } from './Urgency';
 import { DateField } from './Query/Filter/DateField';
 import { renderTaskLine } from './TaskLineRenderer';
@@ -514,10 +514,7 @@ export class Task {
      * task is not recurring, it will return `[toggled]`.
      */
     public toggle(): Task[] {
-        let newStatus = StatusRegistry.getInstance().getNextStatus(this.status);
-        if (newStatus === Status.EMPTY) {
-            newStatus = Status.createUnknownStatus(this.status.nextStatusIndicator);
-        }
+        const newStatus = StatusRegistry.getInstance().getNextStatusOrCreate(this.status);
 
         let newDoneDate = null;
 
@@ -549,9 +546,11 @@ export class Task {
         const newTasks: Task[] = [];
 
         if (nextOccurrence !== null) {
+            const nextStatus = StatusRegistry.getInstance().getNextStatusOrCreate(newStatus);
             const nextTask = new Task({
                 ...this,
                 ...nextOccurrence,
+                status: nextStatus,
                 // New occurrences cannot have the same block link.
                 // And random block links don't help.
                 blockLink: '',


Test patch:
diff --git a/tests/Config/StatusSettings.test.ts b/tests/Config/StatusSettings.test.ts
index 44f4956d..1ddefe14 100644
--- a/tests/Config/StatusSettings.test.ts
+++ b/tests/Config/StatusSettings.test.ts
@@ -3,6 +3,7 @@ import { StatusSettings } from '../../src/Config/StatusSettings';
 import { Status } from '../../src/Status';
 import { StatusConfiguration } from '../../src/StatusConfiguration';
 import { StatusRegistry } from '../../src/StatusRegistry';
+import type { StatusCollection } from '../../src/StatusCollection';
 
 describe('StatusSettings', () => {
     it('verify default status settings', () => {
@@ -52,14 +53,14 @@ describe('StatusSettings', () => {
 
     it('should bulk-add new statuses, reporting errors', () => {
         // Arrange
-        const newStatuses: Array<[string, string, string]> = [
-            ['>', 'Forwarded', 'x'],
-            ['<', 'Schedule', 'x'],
-            ['?', 'Question', 'x'],
-            ['-', 'Dropped - should not be added as duplicate of core Cancelled', 'x'],
-            ['>', 'Forwarded', 'x'], // is a duplicate so should not be added
-            ['<', 'Duplicate - should not be added as duplicate of Schedule above', 'x'],
-            ['', 'Empty - should not be added as no status character', 'x'],
+        const newStatuses: StatusCollection = [
+            ['>', 'Forwarded', 'x', 'TODO'],
+            ['<', 'Schedule', 'x', 'TODO'],
+            ['?', 'Question', 'x', 'TODO'],
+            ['-', 'Dropped - should not be added as duplicate of core Cancelled', 'x', 'CANCELLED'],
+            ['>', 'Forwarded', 'x', 'TODO'], // is a duplicate so should not be added
+            ['<', 'Duplicate - should not be added as duplicate of Schedule above', 'x', 'TODO'],
+            ['', 'Empty - should not be added as no status character', 'x', 'TODO'],
         ];
         const settings = new StatusSettings();
 
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
index 73be963a..e0fb76d5 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
@@ -3,7 +3,7 @@
 | Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `!` | Important | `D` | `TODO` | Yes |
-| `D` | Doing - Important | `X` | `TODO` | Yes |
+| `D` | Doing - Important | `X` | `IN_PROGRESS` | Yes |
 | `X` | Done - Important | `!` | `DONE` | Yes |
 
 
diff --git a/tests/DocsSamplesForStatuses.test.ts b/tests/DocsSamplesForStatuses.test.ts
index a0b34ac8..0a4dd426 100644
--- a/tests/DocsSamplesForStatuses.test.ts
+++ b/tests/DocsSamplesForStatuses.test.ts
@@ -11,6 +11,7 @@ import * as FilterParser from '../src/Query/FilterParser';
 import { Group } from '../src/Query/Group';
 import { StatusNameField } from '../src/Query/Filter/StatusNameField';
 import { StatusTypeField } from '../src/Query/Filter/StatusTypeField';
+import type { StatusCollection } from '../src/StatusCollection';
 import { TaskBuilder } from './TestingTools/TaskBuilder';
 
 class MarkdownTable {
@@ -80,7 +81,7 @@ function verifyStatusesAsMarkdownTable(statuses: Status[]) {
     table.verify();
 }
 
-function constructStatuses(importedStatuses: Array<[string, string, string]>) {
+function constructStatuses(importedStatuses: StatusCollection) {
     const statuses: Status[] = [];
     importedStatuses.forEach((importedStatus) => {
         statuses.push(Status.createFromImportedValue(importedStatus));
@@ -106,10 +107,10 @@ describe('DefaultStatuses', () => {
     });
 
     it('important-cycle', () => {
-        const importantCycle: Array<[string, string, string]> = [
-            ['!', 'Important', 'D'],
-            ['D', 'Doing - Important', 'X'],
-            ['X', 'Done - Important', '!'],
+        const importantCycle: StatusCollection = [
+            ['!', 'Important', 'D', 'TODO'],
+            ['D', 'Doing - Important', 'X', 'IN_PROGRESS'],
+            ['X', 'Done - Important', '!', 'DONE'],
         ];
         verifyStatusesAsMarkdownTable(constructStatuses(importantCycle));
     });
diff --git a/tests/Status.test.ts b/tests/Status.test.ts
index 7365457c..473ca576 100644
--- a/tests/Status.test.ts
+++ b/tests/Status.test.ts
@@ -4,6 +4,7 @@
 import moment from 'moment';
 import { Status } from '../src/Status';
 import { StatusConfiguration, StatusType } from '../src/StatusConfiguration';
+import type { StatusCollectionEntry } from '../src/StatusCollection';
 
 jest.mock('obsidian');
 window.moment = moment;
@@ -106,7 +107,7 @@ describe('Status', () => {
     });
 
     it('should construct a Status from a core imported value', () => {
-        const imported: [string, string, string] = ['/', 'in progress', 'x'];
+        const imported: StatusCollectionEntry = ['/', 'in progress', 'x', 'IN_PROGRESS'];
         const status = Status.createFromImportedValue(imported);
         expect(status.indicator).toEqual('/');
         expect(status.name).toEqual('in progress');
@@ -116,12 +117,12 @@ describe('Status', () => {
     });
 
     it('should construct a Status from a custom imported value', () => {
-        const imported: [string, string, string] = ['P', 'Pro', 'C'];
+        const imported: StatusCollectionEntry = ['P', 'Pro', 'C', 'NON_TASK'];
         const status = Status.createFromImportedValue(imported);
         expect(status.indicator).toEqual('P');
         expect(status.name).toEqual('Pro');
         expect(status.nextStatusIndicator).toEqual('C');
-        expect(status.type).toEqual(StatusType.TODO);
+        expect(status.type).toEqual(StatusType.NON_TASK);
         expect(status.availableAsCommand).toEqual(false);
     });
 });
diff --git a/tests/StatusRegistry.test.ts b/tests/StatusRegistry.test.ts
index 886fe7b8..95e28979 100644
--- a/tests/StatusRegistry.test.ts
+++ b/tests/StatusRegistry.test.ts
@@ -92,6 +92,26 @@ describe('StatusRegistry', () => {
         expect(statusRegistry.getNextStatus(statusD).indicator).toEqual('a');
     });
 
+    it('should return EMPTY if next status does not exist', () => {
+        const statusRegistry = new StatusRegistry();
+        const status = new Status(new StatusConfiguration('P', 'Pro', 'C', false));
+        const nextStatus = statusRegistry.getNextStatus(status);
+        expect(nextStatus.type).toEqual(StatusType.EMPTY);
+    });
+
+    it('should construct a TODO on request if next status does not exist', () => {
+        // Arrange
+        const statusRegistry = new StatusRegistry();
+        const status = new Status(new StatusConfiguration('P', 'Pro', 'C', false));
+        const nextStatus = statusRegistry.getNextStatusOrCreate(status);
+
+        // Assert
+        expect(nextStatus.indicator).toEqual('C');
+        expect(nextStatus.name).toEqual('Unknown');
+        expect(nextStatus.nextStatusIndicator).toEqual('x');
+        expect(nextStatus.type).toEqual(StatusType.TODO);
+    });
+
     it('should handle adding custom StatusConfiguration', () => {
         // Arrange
         const statusRegistry = new StatusRegistry();
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index cd88c9c7..3fa89bb8 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -6,6 +6,8 @@ import type { Moment } from 'moment';
 import { Status } from '../src/Status';
 import { Priority, Task } from '../src/Task';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
+import type { StatusCollection } from '../src/StatusCollection';
+import { StatusRegistry } from '../src/StatusRegistry';
 import { fromLine } from './TestHelpers';
 import { TaskBuilder } from './TestingTools/TaskBuilder';
 import { RecurrenceBuilder } from './TestingTools/RecurrenceBuilder';
@@ -459,6 +461,30 @@ describe('to string', () => {
 });
 
 describe('toggle done', () => {
+    beforeAll(() => {
+        const statuses: StatusCollection = [
+            // A custom set of 3 statuses that form a cycle.
+            // The last one has a conventional symbol, 'X' that is recognised as DONE.fix
+            ['!', 'Important', 'D', 'TODO'],
+            ['D', 'Doing - Important', 'X', 'IN_PROGRESS'],
+            ['X', 'Done - Important', '!', 'DONE'],
+            // A set that uses an unconventional symbol for DONE
+            ['1', 'Status 1', '2', 'TODO'],
+            ['2', 'Status 2', '3', 'IN_PROGRESS'],
+            ['3', 'Status 3', '1', 'DONE'],
+            // A set where the DONE task goes to an unknown symbol
+            ['a', 'Status a', 'b', 'TODO'],
+            ['b', 'Status b', 'c', 'DONE'], // c is not known
+        ];
+        statuses.forEach((s) => {
+            StatusRegistry.getInstance().add(Status.createFromImportedValue(s));
+        });
+    });
+
+    afterAll(() => {
+        StatusRegistry.getInstance().clearStatuses();
+    });
+
     it('retains the block link', () => {
         // Arrange
         const line = '- [ ] this is a task 📅 2021-09-12 ^my-precious';
@@ -499,11 +525,16 @@ describe('toggle done', () => {
     });
 
     type RecurrenceCase = {
+        // inputs:
         interval: string;
+        indicator?: string;
         due?: string;
         scheduled?: string;
         start?: string;
         today?: string;
+        // results:
+        doneIndicator?: string; // the indicator of the completed task
+        nextIndicator?: string; // the indicator of the recurrence
         nextDue?: string;
         nextScheduled?: string;
         nextStart?: string;
@@ -765,21 +796,67 @@ describe('toggle done', () => {
             today: '2020-02-29', // is a leap year
             nextStart: '2022-02-28',
         },
+        // ==================================
+        // Test toggling with custom statuses.
+        // See the available statuses, which were set up in the beforeAll() function above.
+        // ==================================
+        {
+            interval: 'every day',
+            indicator: 'D',
+            due: '2023-01-19',
+            doneIndicator: 'X',
+            nextIndicator: '!',
+            nextDue: '2023-01-20',
+        },
+        {
+            interval: 'every day',
+            indicator: '2',
+            doneIndicator: '3', // 2 toggles to 3
+            nextIndicator: '1', // and the new task should be 1
+        },
+        {
+            interval: 'every day',
+            indicator: 'a',
+            doneIndicator: 'b', // a toggles to b
+            nextIndicator: 'c', // b says it toggles to c: , but c does not exist: check that it has been chosen anyway
+        },
     ];
 
-    test.concurrent.each<RecurrenceCase>(recurrenceCases)(
+    // This was calling test.concurrent.each() to run the tests in parallel, but I couldn't
+    // get it to run beforeAll() before running the tests.
+    // https://github.com/facebook/jest/issues/7997#issuecomment-796965078
+    test.each<RecurrenceCase>(recurrenceCases)(
         'recurs correctly (%j)',
-        ({ interval, due, scheduled, start, today, nextDue, nextScheduled, nextStart, nextInterval }) => {
+        ({
+            // inputs:
+            interval,
+            indicator,
+            due,
+            scheduled,
+            start,
+            today,
+            // results:
+            doneIndicator,
+            nextIndicator,
+            nextDue,
+            nextScheduled,
+            nextStart,
+            nextInterval,
+        }) => {
             const todaySpy = jest.spyOn(Date, 'now').mockReturnValue(moment(today).valueOf());
 
             // If this test fails, the RecurrenceCase had no expected new dates set, and so
             // is accidentally not doing any testing.
             const atLeaseOneExpectationSupplied =
-                nextStart !== undefined || nextDue !== undefined || nextScheduled !== undefined;
+                nextStart !== undefined ||
+                nextDue !== undefined ||
+                nextScheduled !== undefined ||
+                nextIndicator !== undefined ||
+                doneIndicator !== undefined;
             expect(atLeaseOneExpectationSupplied).toStrictEqual(true);
 
             const line = [
-                '- [ ] I am task',
+                `- [${indicator ?? ' '}] I am task`,
                 `🔁 ${interval}`,
                 !!scheduled && `⏳ ${scheduled}`,
                 !!due && `📅 ${due}`,
@@ -794,8 +871,11 @@ describe('toggle done', () => {
 
             const tasks = task!.toggle();
             expect(tasks.length).toEqual(2);
+            const doneTask: Task = tasks[1];
             const nextTask: Task = tasks[0];
 
+            expect(doneTask.status.indicator).toEqual(doneIndicator ?? 'x');
+            expect(nextTask.status.indicator).toEqual(nextIndicator ?? ' ');
             expect({
                 nextDue: nextTask.dueDate?.format('YYYY-MM-DD'),
                 nextScheduled: nextTask.scheduledDate?.format('YYYY-MM-DD'),