diff --git a/src/engine/MacroChoiceEngine.entry.test.ts b/src/engine/MacroChoiceEngine.entry.test.ts index 1604f8b1..e46a1901 100644 --- a/src/engine/MacroChoiceEngine.entry.test.ts +++ b/src/engine/MacroChoiceEngine.entry.test.ts @@ -325,6 +325,188 @@ describe("MacroChoiceEngine user script variable propagation", () => { expect(engine["params"].variables.override).toBe(1); expect(engine["choiceExecutor"].variables).toBe(providedVariables); }); + + it("treats `params.variables = {...}` as replacing the backing map", async () => { + const assignScript: IUserScript = { + id: "assign-script", + name: "Assign Script", + type: CommandType.UserScript, + path: "assign-script.js", + settings: {}, + }; + + const assignChoice: IMacroChoice = { + id: "macro-assign", + name: "Macro assign", + type: "Macro", + command: false, + runOnStartup: false, + macro: { + id: "macro-assign", + name: "Macro assign", + commands: [assignScript], + } as IMacro, + }; + + mockGetUserScript.mockImplementationOnce(() => { + return Promise.resolve(async (params: { variables: Record }) => { + params.variables = { foo: "bar" }; + }); + }); + + variables.set("old", "value"); + + const engine = new MacroChoiceEngine( + app, + plugin, + assignChoice, + choiceExecutor, + variables, + ); + + await engine.run(); + + expect(choiceExecutor.variables.get("foo")).toBe("bar"); + expect(choiceExecutor.variables.has("old")).toBe(false); + }); + + it("does not clear variables when assigned the same backing map", async () => { + const assignSameScript: IUserScript = { + id: "assign-same-script", + name: "Assign Same Script", + type: CommandType.UserScript, + path: "assign-same-script.js", + settings: {}, + }; + + const assignSameChoice: IMacroChoice = { + id: "macro-assign-same", + name: "Macro assign same", + type: "Macro", + command: false, + runOnStartup: false, + macro: { + id: "macro-assign-same", + name: "Macro assign same", + commands: [assignSameScript], + } as IMacro, + }; + + mockGetUserScript.mockImplementationOnce(() => { + return Promise.resolve(async (params: { variables: Record }) => { + params.variables = params.variables; + params.variables.added = 2; + }); + }); + + variables.set("old", "value"); + + const engine = new MacroChoiceEngine( + app, + plugin, + assignSameChoice, + choiceExecutor, + variables, + ); + + await engine.run(); + + expect(choiceExecutor.variables.get("old")).toBe("value"); + expect(choiceExecutor.variables.get("added")).toBe(2); + }); + + it("ignores invalid reassignment of params.variables without clearing", async () => { + const invalidAssignScript: IUserScript = { + id: "invalid-assign-script", + name: "Invalid assign Script", + type: CommandType.UserScript, + path: "invalid-assign-script.js", + settings: {}, + }; + + const invalidAssignChoice: IMacroChoice = { + id: "macro-invalid-assign", + name: "Macro invalid assign", + type: "Macro", + command: false, + runOnStartup: false, + macro: { + id: "macro-invalid-assign", + name: "Macro invalid assign", + commands: [invalidAssignScript], + } as IMacro, + }; + + mockGetUserScript.mockImplementationOnce(() => { + return Promise.resolve(async (params: { variables: any }) => { + params.variables = 123; + params.variables.added = "ok"; + }); + }); + + variables.set("old", "value"); + + const engine = new MacroChoiceEngine( + app, + plugin, + invalidAssignChoice, + choiceExecutor, + variables, + ); + + await engine.run(); + + expect(choiceExecutor.variables.get("old")).toBe("value"); + expect(choiceExecutor.variables.get("added")).toBe("ok"); + }); + + it("skips non-string Map keys when assigning params.variables", async () => { + const mapAssignScript: IUserScript = { + id: "map-assign-script", + name: "Map assign Script", + type: CommandType.UserScript, + path: "map-assign-script.js", + settings: {}, + }; + + const mapAssignChoice: IMacroChoice = { + id: "macro-map-assign", + name: "Macro map assign", + type: "Macro", + command: false, + runOnStartup: false, + macro: { + id: "macro-map-assign", + name: "Macro map assign", + commands: [mapAssignScript], + } as IMacro, + }; + + mockGetUserScript.mockImplementationOnce(() => { + return Promise.resolve(async (params: { variables: any }) => { + params.variables = new Map([ + [1, "nope"], + ["foo", "bar"], + ]); + }); + }); + + variables.set("old", "value"); + + const engine = new MacroChoiceEngine( + app, + plugin, + mapAssignChoice, + choiceExecutor, + variables, + ); + + await engine.run(); + + expect(choiceExecutor.variables.get("foo")).toBe("bar"); + expect(choiceExecutor.variables.has("old")).toBe(false); + expect(choiceExecutor.variables.has(1 as any)).toBe(false); + }); }); describe("MacroChoiceEngine choice command cancellation", () => { diff --git a/src/engine/MacroChoiceEngine.ts b/src/engine/MacroChoiceEngine.ts index 7f809b0e..5cf41283 100644 --- a/src/engine/MacroChoiceEngine.ts +++ b/src/engine/MacroChoiceEngine.ts @@ -85,15 +85,44 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine { choiceExecutor: IChoiceExecutor, sharedVariables: Map ) { - return { + const variablesProxy = createVariablesProxy(sharedVariables); + + const params = { app, quickAddApi: QuickAddApi.GetApi(app, plugin, choiceExecutor), - variables: createVariablesProxy(sharedVariables), obsidian, abort: (message?: string) => { throw new MacroAbortError(message); }, - }; + } as unknown as typeof this.params; + + // Backward compatibility: some scripts assign `QuickAdd.variables = {...}` + // or `params.variables = {...}`. + // Treat that as replacing the backing Map so templates can consume them. + Object.defineProperty(params, "variables", { + get: () => variablesProxy, + set: (next: unknown) => { + if (next === sharedVariables || next === variablesProxy) return; + + const entries = + next instanceof Map + ? Array.from(next.entries()).filter(([key]) => typeof key === "string") + : next && typeof next === "object" + ? Object.entries(next as Record) + : null; + + // Invalid assignments are ignored to avoid wiping the backing store. + if (!entries) return; + + sharedVariables.clear(); + + entries?.forEach(([key, value]) => sharedVariables.set(key, value)); + }, + enumerable: true, + configurable: false, + }); + + return params; } private initSharedVariables(