Skip to content

Commit 26a7cf5

Browse files
authored
fix: preserve macro user script variable updates (#999)
* fix: sync macro user script variables * fix: avoid overwriting executor map updates * refactor: proxy macro variables map * test: strengthen variables proxy coverage * chore: include value in proxy property descriptor * fix: preserve executor vars when custom map provided * fix: expose hasOwnProperty shim on variables proxy * fix: clarify variable map merge and set AI outputs into map * chore: tidy proxy test expectation * docs: add macro variables proxy guide * refactor: extract macro variable wiring helpers * chore: remove macro variables proxy dev doc * test: add proxy edge case coverage
1 parent 9b4f177 commit 26a7cf5

6 files changed

Lines changed: 355 additions & 38 deletions

File tree

src/engine/MacroChoiceEngine.entry.test.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import { MacroChoiceEngine } from "./MacroChoiceEngine";
33
import type QuickAdd from "../main";
44
import type { IChoiceExecutor } from "../IChoiceExecutor";
@@ -211,6 +211,122 @@ describe("MacroChoiceEngine user script entry handling", () => {
211211
});
212212
});
213213

214+
describe("MacroChoiceEngine user script variable propagation", () => {
215+
const app = {} as App;
216+
const plugin = {} as unknown as QuickAdd;
217+
218+
let choiceExecutor: IChoiceExecutor;
219+
let variables: Map<string, unknown>;
220+
let macroChoice: IMacroChoice;
221+
let logs: Array<number | undefined>;
222+
let getApiSpy: ReturnType<typeof vi.spyOn>;
223+
224+
beforeEach(() => {
225+
vi.clearAllMocks();
226+
mockGetUserScript.mockReset();
227+
mockInitializeUserScriptSettings.mockReset();
228+
mockSuggest.mockReset();
229+
230+
getApiSpy = vi
231+
.spyOn(QuickAddApi, "GetApi")
232+
.mockReturnValue({} as unknown as ReturnType<typeof QuickAddApi.GetApi>);
233+
234+
logs = [];
235+
variables = new Map<string, unknown>();
236+
choiceExecutor = {
237+
execute: vi.fn(),
238+
variables,
239+
};
240+
241+
const makeScript = (index: number): IUserScript => ({
242+
id: `script-${index}`,
243+
name: `Script ${index}`,
244+
type: CommandType.UserScript,
245+
path: `script-${index}.js`,
246+
settings: {},
247+
});
248+
249+
macroChoice = {
250+
id: "macro-sequence",
251+
name: "Macro sequence",
252+
type: "Macro",
253+
command: false,
254+
runOnStartup: false,
255+
macro: {
256+
id: "macro-sequence",
257+
name: "Macro sequence",
258+
commands: [
259+
makeScript(1),
260+
makeScript(2),
261+
makeScript(3),
262+
makeScript(4),
263+
],
264+
} as IMacro,
265+
};
266+
267+
mockGetUserScript.mockImplementation((command: IUserScript) => {
268+
const nextValueByPath: Record<string, number> = {
269+
"script-1.js": 1,
270+
"script-2.js": 2,
271+
"script-3.js": 3,
272+
"script-4.js": 3,
273+
};
274+
275+
const nextValue = nextValueByPath[command.path ?? ""] ?? 0;
276+
277+
return Promise.resolve(async (params: { variables: Record<string, unknown> }) => {
278+
logs.push(params.variables.target as number | undefined);
279+
if (nextValue !== 0) {
280+
params.variables.target = nextValue;
281+
}
282+
});
283+
});
284+
});
285+
286+
afterEach(() => {
287+
getApiSpy.mockRestore();
288+
});
289+
290+
it("propagates variable mutations across sequential user scripts", async () => {
291+
const engine = new MacroChoiceEngine(
292+
app,
293+
plugin,
294+
macroChoice,
295+
choiceExecutor,
296+
variables,
297+
);
298+
299+
await engine.run();
300+
301+
expect(logs).toEqual([undefined, 1, 2, 3]);
302+
expect(choiceExecutor.variables.get("target")).toBe(3);
303+
});
304+
305+
it("merges existing executor variables into provided map without losing state", async () => {
306+
const executorVariables = new Map<string, unknown>([
307+
["keep", "executor"],
308+
]);
309+
const providedVariables = new Map<string, unknown>([
310+
["override", 1],
311+
]);
312+
313+
const engine = new MacroChoiceEngine(
314+
app,
315+
plugin,
316+
macroChoice,
317+
{
318+
...choiceExecutor,
319+
variables: executorVariables,
320+
},
321+
providedVariables,
322+
);
323+
324+
expect(engine["params"].variables.keep).toBe("executor");
325+
expect(engine["params"].variables.override).toBe(1);
326+
expect(engine["choiceExecutor"].variables).toBe(providedVariables);
327+
});
328+
});
329+
214330
describe("MacroChoiceEngine choice command cancellation", () => {
215331
const app = {} as App;
216332
let plugin: QuickAdd & { getChoiceById: ReturnType<typeof vi.fn> };

src/engine/MacroChoiceEngine.ts

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import type { ScriptCondition } from "../types/macros/Conditional/types";
4747
import { evaluateCondition } from "./helpers/conditionalEvaluator";
4848
import { handleMacroAbort } from "../utils/macroAbortHandler";
4949
import { buildOpenFileOptions } from "./helpers/openFileOptions";
50+
import { createVariablesProxy } from "../utils/variablesProxy";
5051

5152
type ConditionalScriptRunner = () => Promise<unknown>;
5253

@@ -78,6 +79,42 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
7879
private userScriptCommand: IUserScript | null;
7980
private conditionalScriptCache = new Map<string, ConditionalScriptRunner>();
8081
private readonly preloadedUserScripts: Map<string, unknown>;
82+
private buildParams(
83+
app: App,
84+
plugin: QuickAdd,
85+
choiceExecutor: IChoiceExecutor,
86+
sharedVariables: Map<string, unknown>
87+
) {
88+
return {
89+
app,
90+
quickAddApi: QuickAddApi.GetApi(app, plugin, choiceExecutor),
91+
variables: createVariablesProxy(sharedVariables),
92+
obsidian,
93+
abort: (message?: string) => {
94+
throw new MacroAbortError(message);
95+
},
96+
};
97+
}
98+
99+
private initSharedVariables(
100+
choiceExecutor: IChoiceExecutor,
101+
providedVariables?: Map<string, unknown>
102+
): Map<string, unknown> {
103+
const existingVariables = choiceExecutor.variables;
104+
105+
if (providedVariables) {
106+
if (existingVariables && providedVariables !== existingVariables) {
107+
existingVariables.forEach((value, key) => {
108+
if (!providedVariables.has(key)) {
109+
providedVariables.set(key, value);
110+
}
111+
});
112+
}
113+
return providedVariables;
114+
}
115+
116+
return existingVariables ?? new Map<string, unknown>();
117+
}
81118

82119
constructor(
83120
app: App,
@@ -93,19 +130,12 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
93130
this.macro = choice?.macro;
94131
this.choiceExecutor = choiceExecutor;
95132
this.preloadedUserScripts = preloadedUserScripts ?? new Map();
96-
this.params = {
97-
app: this.app,
98-
quickAddApi: QuickAddApi.GetApi(app, plugin, choiceExecutor),
99-
variables: {},
100-
obsidian,
101-
abort: (message?: string) => {
102-
throw new MacroAbortError(message);
103-
},
104-
};
105-
106-
variables?.forEach((value, key) => {
107-
this.params.variables[key] = value;
108-
});
133+
const sharedVariables = this.initSharedVariables(
134+
choiceExecutor,
135+
variables
136+
);
137+
this.choiceExecutor.variables = sharedVariables;
138+
this.params = this.buildParams(app, plugin, choiceExecutor, sharedVariables);
109139
}
110140

111141
async run(): Promise<void> {
@@ -151,14 +181,6 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
151181
if (command?.type === CommandType.Conditional) {
152182
await this.executeConditional(command as IConditionalCommand);
153183
}
154-
155-
this.pullExecutorVariablesIntoParams();
156-
Object.keys(this.params.variables).forEach((key) => {
157-
this.choiceExecutor.variables.set(
158-
key,
159-
this.params.variables[key]
160-
);
161-
});
162184
}
163185
} catch (error) {
164186
if (
@@ -462,7 +484,6 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
462484
}
463485

464486
private async executeConditional(command: IConditionalCommand) {
465-
this.pullExecutorVariablesIntoParams();
466487
const shouldRunThenBranch = await evaluateCondition(command.condition, {
467488
variables: this.params.variables,
468489
evaluateScriptCondition: async (condition: ScriptCondition) =>
@@ -489,12 +510,6 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
489510
this.output = value;
490511
}
491512

492-
private pullExecutorVariablesIntoParams() {
493-
this.choiceExecutor.variables.forEach((value, key) => {
494-
this.params.variables[key] = value;
495-
});
496-
}
497-
498513
private async evaluateScriptCondition(
499514
condition: ScriptCondition
500515
): Promise<boolean> {

src/engine/SingleMacroEngine.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,6 @@ export class SingleMacroEngine {
288288
engine: MacroChoiceEngine,
289289
settings: Record<string, unknown>,
290290
): Promise<unknown> {
291-
// Ensure params reflect latest shared variables before executing
292-
this.choiceExecutor.variables.forEach((value, key) => {
293-
engine.params.variables[key] = value;
294-
});
295-
296291
if (typeof member === "function") {
297292
return await (
298293
member as (

src/quickAddApi.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ export class QuickAddApi {
273273
}
274274

275275
if (settings?.shouldAssignVariables) {
276-
// Copy over `output` and `output-quoted` to the variables (if 'outout' is variable name)
277-
Object.assign(choiceExecutor.variables, assistantRes);
276+
// Copy over `output` and `output-quoted` to the variables (if 'output' is variable name)
277+
Object.entries(assistantRes).forEach(([key, value]) => {
278+
choiceExecutor.variables.set(key, value);
279+
});
278280
}
279281

280282
return assistantRes;
@@ -372,8 +374,10 @@ export class QuickAddApi {
372374
}
373375

374376
if (settings?.shouldAssignVariables) {
375-
// Copy over `output` and `output-quoted` to the variables (if 'outout' is variable name)
376-
Object.assign(choiceExecutor.variables, assistantRes);
377+
// Copy over `output` and `output-quoted` to the variables (if 'output' is variable name)
378+
Object.entries(assistantRes).forEach(([key, value]) => {
379+
choiceExecutor.variables.set(key, value);
380+
});
377381
}
378382

379383
return assistantRes;

0 commit comments

Comments
 (0)