From 6999b3478b4b46681d1796a964cee189d2853116 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 14 Mar 2026 16:17:02 +0100 Subject: [PATCH 1/2] fix(migrations): traverse nested template choices --- .../consolidateFileExistsBehavior.test.ts | 98 ++++++++++++++++++- .../consolidateFileExistsBehavior.ts | 48 +++------ 2 files changed, 108 insertions(+), 38 deletions(-) diff --git a/src/migrations/consolidateFileExistsBehavior.test.ts b/src/migrations/consolidateFileExistsBehavior.test.ts index 750ae66f..adb80417 100644 --- a/src/migrations/consolidateFileExistsBehavior.test.ts +++ b/src/migrations/consolidateFileExistsBehavior.test.ts @@ -1,3 +1,4 @@ +import { CommandType } from "../types/macros/CommandType"; import { describe, expect, it } from "vitest"; import migration from "./consolidateFileExistsBehavior"; @@ -51,7 +52,46 @@ describe("consolidateFileExistsBehavior migration", () => { }); }); - it("normalizes nested macro command template choices", async () => { + it("normalizes template choices nested inside Macro choice commands", async () => { + const plugin = { + settings: { + choices: [ + { + id: "macro-choice", + name: "Macro Choice", + type: "Macro", + macro: { + id: "macro-1", + name: "Macro", + commands: [ + { + type: CommandType.NestedChoice, + choice: { + id: "template-choice", + name: "Template", + type: "Template", + setFileExistsBehavior: true, + fileExistsMode: "Append duplicate suffix", + }, + }, + ], + }, + }, + ], + macros: [], + }, + } as any; + + await migration.migrate(plugin); + + expect( + plugin.settings.choices[0].macro.commands[0].choice, + ).toMatchObject({ + fileExistsBehavior: { kind: "apply", mode: "duplicateSuffix" }, + }); + }); + + it("normalizes nested macro command template choices in legacy macros", async () => { const plugin = { settings: { choices: [], @@ -83,4 +123,60 @@ describe("consolidateFileExistsBehavior migration", () => { fileExistsBehavior: { kind: "prompt" }, }); }); + + it("normalizes template choices nested in conditional macro branches", async () => { + const plugin = { + settings: { + choices: [], + macros: [ + { + id: "macro-1", + name: "Macro", + commands: [ + { + type: CommandType.Conditional, + thenCommands: [ + { + type: CommandType.NestedChoice, + choice: { + id: "template-then", + name: "Then Template", + type: "Template", + setFileExistsBehavior: true, + fileExistsMode: "Append duplicate suffix", + }, + }, + ], + elseCommands: [ + { + type: CommandType.NestedChoice, + choice: { + id: "template-else", + name: "Else Template", + type: "Template", + setFileExistsBehavior: false, + fileExistsMode: "Overwrite the file", + }, + }, + ], + }, + ], + }, + ], + }, + } as any; + + await migration.migrate(plugin); + + expect( + plugin.settings.macros[0].commands[0].thenCommands[0].choice, + ).toMatchObject({ + fileExistsBehavior: { kind: "apply", mode: "duplicateSuffix" }, + }); + expect( + plugin.settings.macros[0].commands[0].elseCommands[0].choice, + ).toMatchObject({ + fileExistsBehavior: { kind: "prompt" }, + }); + }); }); diff --git a/src/migrations/consolidateFileExistsBehavior.ts b/src/migrations/consolidateFileExistsBehavior.ts index e07a84f2..478a541e 100644 --- a/src/migrations/consolidateFileExistsBehavior.ts +++ b/src/migrations/consolidateFileExistsBehavior.ts @@ -1,53 +1,27 @@ import type QuickAdd from "src/main"; -import type IChoice from "src/types/choices/IChoice"; -import type { IMacro } from "src/types/macros/IMacro"; import { deepClone } from "src/utils/deepClone"; import { isTemplateChoice, normalizeTemplateChoice, } from "./helpers/normalizeTemplateFileExistsBehavior"; -import { isMultiChoice } from "./helpers/isMultiChoice"; -import { isNestedChoiceCommand } from "./helpers/isNestedChoiceCommand"; +import { walkAllChoices } from "./helpers/choice-traversal"; import type { Migration } from "./Migrations"; -function normalizeChoices(choices: IChoice[]): IChoice[] { - for (const choice of choices) { - if (isMultiChoice(choice)) { - choice.choices = normalizeChoices(choice.choices); - } - - if (isTemplateChoice(choice)) { - normalizeTemplateChoice(choice); - } - } - - return choices; -} - -function normalizeMacros(macros: IMacro[]): IMacro[] { - for (const macro of macros) { - if (!Array.isArray(macro.commands)) continue; - - for (const command of macro.commands) { - if (isNestedChoiceCommand(command) && isTemplateChoice(command.choice)) { - normalizeTemplateChoice(command.choice); - } - } - } - - return macros; -} - const consolidateFileExistsBehavior: Migration = { description: "Re-run template file collision normalization for users with older migration state", migrate: async (plugin: QuickAdd): Promise => { - const choicesCopy = deepClone(plugin.settings.choices); - const macrosCopy = deepClone((plugin.settings as any).macros || []); - - plugin.settings.choices = deepClone(normalizeChoices(choicesCopy)); - (plugin.settings as any).macros = normalizeMacros(macrosCopy); + plugin.settings.choices = deepClone(plugin.settings.choices); + (plugin.settings as any).macros = deepClone( + (plugin.settings as any).macros || [], + ); + + walkAllChoices(plugin, (choice) => { + if (isTemplateChoice(choice)) { + normalizeTemplateChoice(choice); + } + }); }, }; From 9ee2acf418e3dc9562d3e547360ccecc1d0b67f1 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 14 Mar 2026 16:24:14 +0100 Subject: [PATCH 2/2] fix(migrations): harden file exists backfill --- .../consolidateFileExistsBehavior.test.ts | 42 +++++++++++++++++++ .../consolidateFileExistsBehavior.ts | 13 ++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/migrations/consolidateFileExistsBehavior.test.ts b/src/migrations/consolidateFileExistsBehavior.test.ts index adb80417..a57d7ced 100644 --- a/src/migrations/consolidateFileExistsBehavior.test.ts +++ b/src/migrations/consolidateFileExistsBehavior.test.ts @@ -89,6 +89,12 @@ describe("consolidateFileExistsBehavior migration", () => { ).toMatchObject({ fileExistsBehavior: { kind: "apply", mode: "duplicateSuffix" }, }); + expect( + plugin.settings.choices[0].macro.commands[0].choice.fileExistsMode, + ).toBeUndefined(); + expect( + plugin.settings.choices[0].macro.commands[0].choice.setFileExistsBehavior, + ).toBeUndefined(); }); it("normalizes nested macro command template choices in legacy macros", async () => { @@ -122,6 +128,12 @@ describe("consolidateFileExistsBehavior migration", () => { expect(plugin.settings.macros[0].commands[0].choice).toMatchObject({ fileExistsBehavior: { kind: "prompt" }, }); + expect( + plugin.settings.macros[0].commands[0].choice.fileExistsMode, + ).toBeUndefined(); + expect( + plugin.settings.macros[0].commands[0].choice.setFileExistsBehavior, + ).toBeUndefined(); }); it("normalizes template choices nested in conditional macro branches", async () => { @@ -173,10 +185,40 @@ describe("consolidateFileExistsBehavior migration", () => { ).toMatchObject({ fileExistsBehavior: { kind: "apply", mode: "duplicateSuffix" }, }); + expect( + plugin.settings.macros[0].commands[0].thenCommands[0].choice + .fileExistsMode, + ).toBeUndefined(); + expect( + plugin.settings.macros[0].commands[0].thenCommands[0].choice + .setFileExistsBehavior, + ).toBeUndefined(); expect( plugin.settings.macros[0].commands[0].elseCommands[0].choice, ).toMatchObject({ fileExistsBehavior: { kind: "prompt" }, }); + expect( + plugin.settings.macros[0].commands[0].elseCommands[0].choice + .fileExistsMode, + ).toBeUndefined(); + expect( + plugin.settings.macros[0].commands[0].elseCommands[0].choice + .setFileExistsBehavior, + ).toBeUndefined(); + }); + + it("treats malformed persisted choice and macro collections as empty arrays", async () => { + const plugin = { + settings: { + choices: { invalid: true }, + macros: "invalid", + }, + } as any; + + await migration.migrate(plugin); + + expect(plugin.settings.choices).toEqual([]); + expect(plugin.settings.macros).toEqual([]); }); }); diff --git a/src/migrations/consolidateFileExistsBehavior.ts b/src/migrations/consolidateFileExistsBehavior.ts index 478a541e..f3314426 100644 --- a/src/migrations/consolidateFileExistsBehavior.ts +++ b/src/migrations/consolidateFileExistsBehavior.ts @@ -12,10 +12,15 @@ const consolidateFileExistsBehavior: Migration = { "Re-run template file collision normalization for users with older migration state", migrate: async (plugin: QuickAdd): Promise => { - plugin.settings.choices = deepClone(plugin.settings.choices); - (plugin.settings as any).macros = deepClone( - (plugin.settings as any).macros || [], - ); + const choices = Array.isArray(plugin.settings.choices) + ? plugin.settings.choices + : []; + const macros = Array.isArray((plugin.settings as any).macros) + ? (plugin.settings as any).macros + : []; + + plugin.settings.choices = deepClone(choices); + (plugin.settings as any).macros = deepClone(macros); walkAllChoices(plugin, (choice) => { if (isTemplateChoice(choice)) {