donobu 5.33.0 → 5.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,13 @@ export declare const PlaywrightAssertionStepSchema: z.ZodObject<{
33
33
  export type PlaywrightAssertionStep = z.infer<typeof PlaywrightAssertionStepSchema>;
34
34
  export type AssertCacheExecutor = (context: {
35
35
  page: Page;
36
+ /**
37
+ * Optional env mapping used to interpolate `{{$.env.X}}` placeholders that
38
+ * the AI may have embedded into step `value`/`attributeValue` fields. When
39
+ * absent, steps run unchanged (backwards compatible with cache entries
40
+ * recorded before env-aware caching).
41
+ */
42
+ envData?: Record<string, string>;
36
43
  }) => Promise<void>;
37
44
  /**
38
45
  * Builds an executor function from structured assertion steps.
@@ -5,6 +5,7 @@ exports.buildAssertExecutor = buildAssertExecutor;
5
5
  exports.buildLocateExecutor = buildLocateExecutor;
6
6
  const test_1 = require("@playwright/test");
7
7
  const v4_1 = require("zod/v4");
8
+ const TemplateInterpolator_1 = require("../../../utils/TemplateInterpolator");
8
9
  const buildLocator_1 = require("../locate/buildLocator");
9
10
  // ---------------------------------------------------------------------------
10
11
  // Structured assertion step schema
@@ -84,17 +85,33 @@ Common roles: 'heading', 'button', 'link', 'tab', 'tabpanel', 'dialog', 'navigat
84
85
  - toContainText: set to the text substring to match within the element
85
86
  - All other assertions: set to null`),
86
87
  });
88
+ /**
89
+ * Resolves any `{{$.env.X}}` placeholders in a step field against the
90
+ * supplied env data. Returns the input verbatim when no env data is given,
91
+ * preserving backwards compatibility with cached entries that contain
92
+ * literal values only.
93
+ */
94
+ function resolveStepField(value, envData) {
95
+ if (!envData || !value.includes('{{')) {
96
+ return value;
97
+ }
98
+ return (0, TemplateInterpolator_1.interpolateString)(value, { env: envData, calls: [] });
99
+ }
87
100
  /**
88
101
  * Builds an executor function from structured assertion steps.
89
102
  * Each step maps to exactly one Playwright `expect` call — no string
90
103
  * evaluation, no VM contexts.
91
104
  */
92
105
  function buildAssertExecutor(steps) {
93
- return async ({ page }) => {
106
+ return async ({ page, envData }) => {
94
107
  for (const step of steps) {
108
+ const resolvedValue = resolveStepField(step.value, envData);
109
+ const resolvedAttrValue = step.attributeValue === null
110
+ ? null
111
+ : resolveStepField(step.attributeValue, envData);
95
112
  const matcher = step.valueIsRegex
96
- ? new RegExp(step.value)
97
- : step.value;
113
+ ? new RegExp(resolvedValue)
114
+ : resolvedValue;
98
115
  // Page-level assertions (no element locator needed)
99
116
  if (step.assertion === 'toHaveTitle') {
100
117
  await (0, test_1.expect)(page).toHaveTitle(matcher);
@@ -138,13 +155,13 @@ function buildAssertExecutor(steps) {
138
155
  await (0, test_1.expect)(locator).toBeChecked();
139
156
  break;
140
157
  case 'toHaveValue':
141
- await (0, test_1.expect)(locator).toHaveValue(step.attributeValue ?? '');
158
+ await (0, test_1.expect)(locator).toHaveValue(resolvedAttrValue ?? '');
142
159
  break;
143
160
  case 'toContainText':
144
- await (0, test_1.expect)(locator).toContainText(step.attributeValue ?? '');
161
+ await (0, test_1.expect)(locator).toContainText(resolvedAttrValue ?? '');
145
162
  break;
146
163
  case 'toHaveAttribute':
147
- await (0, test_1.expect)(locator).toHaveAttribute(step.value, step.attributeValue ?? '');
164
+ await (0, test_1.expect)(locator).toHaveAttribute(resolvedValue, resolvedAttrValue ?? '');
148
165
  break;
149
166
  }
150
167
  }
@@ -40,8 +40,32 @@ export type AssertOptions = {
40
40
  * and generates equivalent Playwright code which is cached. Subsequent
41
41
  * runs execute the cached code directly, skipping the AI call entirely.
42
42
  * Defaults to `true`.
43
+ *
44
+ * Cached steps preserve `{{$.env.*}}` placeholders for any value that came
45
+ * from an env var, so changing an env value between runs replays the same
46
+ * cached steps with the new value rather than re-invoking the AI.
43
47
  */
44
48
  cache?: boolean;
49
+ /**
50
+ * Explicit environment variable names (in addition to the heuristically
51
+ * derived ones) that the assertion may read via `{{$.env.*}}` interpolations.
52
+ */
53
+ envVars?: string[];
54
+ /**
55
+ * Explicitly supply environment variable values that amend (or override)
56
+ * the environment observed by this `page.ai.assert` call. Keys are merged
57
+ * with any names derived from {@link AssertOptions.envVars} and from
58
+ * `{{$.env.*}}` interpolations in the assertion text.
59
+ *
60
+ * - A `string` value sets or overrides the variable for this invocation.
61
+ * - An `undefined` value *removes* the variable, even if it would
62
+ * otherwise be resolved from persistence.
63
+ *
64
+ * Only the **names** (keys) influence cache lookup; changing a value
65
+ * replays the cached steps with the new value via `{{$.env.*}}` placeholder
66
+ * substitution rather than busting the cache.
67
+ */
68
+ envVals?: Record<string, string | undefined>;
45
69
  };
46
70
  type PageAiAct = {
47
71
  <Schema extends z.ZodObject>(instruction: string, options?: PageAiActWithSchemaOptions<Schema>): Promise<z.infer<Schema>>;
@@ -10,6 +10,7 @@ const GptApiKeysNotSetupException_1 = require("../../exceptions/GptApiKeysNotSet
10
10
  const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
11
11
  const ToolCallFailedException_1 = require("../../exceptions/ToolCallFailedException");
12
12
  const ToolRequiresGptException_1 = require("../../exceptions/ToolRequiresGptException");
13
+ const DonobuFlowsManager_1 = require("../../managers/DonobuFlowsManager");
13
14
  const InteractionVisualizer_1 = require("../../managers/InteractionVisualizer");
14
15
  const ToolManager_1 = require("../../managers/ToolManager");
15
16
  const WebTargetInspector_1 = require("../../managers/WebTargetInspector");
@@ -220,6 +221,33 @@ Valid options:
220
221
  const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
221
222
  const retries = options?.retries ?? 0;
222
223
  const retryDelaySeconds = options?.retryDelaySeconds ?? 3;
224
+ // Distill env var names from `{{$.env.*}}` interpolations in the
225
+ // assertion plus any explicitly provided names/overrides. Cached
226
+ // Playwright steps may carry the same `{{$.env.X}}` placeholders in
227
+ // their `value`/`attributeValue` fields, so we resolve env data at
228
+ // replay time and let the executor interpolate before applying.
229
+ const envVarNames = (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(assertion, [
230
+ ...(options?.envVars ?? []),
231
+ ...Object.keys(options?.envVals ?? {}),
232
+ ]);
233
+ const hasEnvRefs = envVarNames.length > 0;
234
+ const resolveEnvData = async () => {
235
+ if (!hasEnvRefs) {
236
+ return undefined;
237
+ }
238
+ const envData = await sharedState.donobuStack.envDataManager.getByNames(envVarNames);
239
+ if (options?.envVals) {
240
+ for (const [k, v] of Object.entries(options.envVals)) {
241
+ if (v === undefined) {
242
+ delete envData[k];
243
+ }
244
+ else {
245
+ envData[k] = v;
246
+ }
247
+ }
248
+ }
249
+ return envData;
250
+ };
223
251
  // --- Cache lookup (when enabled and not clearing) ---
224
252
  if (useCache && !clearCache) {
225
253
  const cache = getOrInitPageAiCache();
@@ -227,6 +255,7 @@ Valid options:
227
255
  const cached = await cache.getAssert({ pageUrl, assertion });
228
256
  if (cached) {
229
257
  Logger_1.appLogger.debug(`Assert cache HIT for: "${assertion}" - running cached Playwright assertion`);
258
+ const envData = await resolveEnvData();
230
259
  let lastError = null;
231
260
  for (let attempt = 0; attempt <= retries; attempt++) {
232
261
  if (attempt > 0) {
@@ -234,7 +263,7 @@ Valid options:
234
263
  await page.waitForTimeout(retryDelaySeconds * 1000);
235
264
  }
236
265
  try {
237
- await cached.run({ page });
266
+ await cached.run({ page, envData });
238
267
  return; // Assertion passed
239
268
  }
240
269
  catch (error) {
@@ -263,12 +292,28 @@ Valid options:
263
292
  await cache.deleteAssert({ pageUrl, assertion });
264
293
  Logger_1.appLogger.debug(`Assert cache invalidated for: "${assertion}"`);
265
294
  }
266
- // --- Cache miss or cache disabled: run AI assertion ---
267
- const result = await runTool(page, AssertTool_1.AssertTool.NAME, {
268
- assertionToTestFor: assertion,
269
- retries: options?.retries,
270
- retryWaitSeconds: options?.retryDelaySeconds,
271
- }, options?.gptClient);
295
+ // Make env vars available to runTool's envData for `{{$.env.*}}`
296
+ // interpolation inside `assertionToTestFor` and so AssertTool can
297
+ // instruct the AI to emit placeholders in cached step values. Mirrors
298
+ // PageAi.ai for `act`: metadata.envVars is set (overwriting), envVals
299
+ // is restored.
300
+ if (hasEnvRefs) {
301
+ sharedState.donobuFlowMetadata.envVars = envVarNames;
302
+ }
303
+ const previousEnvVals = sharedState.envVals;
304
+ sharedState.envVals = options?.envVals;
305
+ let result;
306
+ try {
307
+ // --- Cache miss or cache disabled: run AI assertion ---
308
+ result = await runTool(page, AssertTool_1.AssertTool.NAME, {
309
+ assertionToTestFor: assertion,
310
+ retries: options?.retries,
311
+ retryWaitSeconds: options?.retryDelaySeconds,
312
+ }, options?.gptClient);
313
+ }
314
+ finally {
315
+ sharedState.envVals = previousEnvVals;
316
+ }
272
317
  if (!result.outcome.isSuccessful) {
273
318
  throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, result.outcome);
274
319
  }
@@ -87,6 +87,9 @@ class ToolManager {
87
87
  startedAt: startedAt,
88
88
  completedAt: null,
89
89
  });
90
+ // Expose the un-interpolated parameters so tools that need to
91
+ // preserve `{{...}}` references (e.g. AssertTool) can read them.
92
+ context.rawParameters = toolParameters;
90
93
  // Use the interpolated parameters when calling the tool.
91
94
  toolCallResult = isFromGpt
92
95
  ? await tool.callFromGpt(context, tool.inputSchemaForGpt.parse(interpolatedParameters))
@@ -22,5 +22,16 @@ export type ToolCallContext = {
22
22
  readonly invokedToolCalls: ToolCall[];
23
23
  readonly metadata: FlowMetadata;
24
24
  readonly toolCallId: string;
25
+ /**
26
+ * Original (un-interpolated) parameters supplied to the current tool call.
27
+ *
28
+ * `ToolManager.invokeTool` interpolates `{{...}}` expressions in the tool's
29
+ * parameters before invoking the tool, but a tool may need the raw text to
30
+ * preserve env var references in its output (e.g. `AssertTool` emits
31
+ * Playwright steps that retain `{{$.env.X}}` so cached replays stay correct
32
+ * across env value changes). Set by `ToolManager` immediately before the
33
+ * tool runs; absent for direct/legacy invocation paths.
34
+ */
35
+ rawParameters?: Record<string, any>;
25
36
  };
26
37
  //# sourceMappingURL=ToolCallContext.d.ts.map
@@ -124,6 +124,36 @@ It will use a screenshot of the current viewport of the webpage, the webpage's t
124
124
  Logger_1.appLogger.warn(msg);
125
125
  return msg;
126
126
  });
127
+ const rawAssertion = typeof context.rawParameters?.assertionToTestFor === 'string'
128
+ ? context.rawParameters.assertionToTestFor
129
+ : parameters.assertionToTestFor;
130
+ const envEntries = Object.entries(context.envData ?? {});
131
+ // Only treat env vars as "in play" when the raw assertion actually
132
+ // references one — keeps the prompt small for the common case.
133
+ const referencedEnvEntries = envEntries.filter(([name]) => rawAssertion.includes(`{{$.env.${name}}}`));
134
+ const hasEnvRefs = referencedEnvEntries.length > 0;
135
+ const envBlock = hasEnvRefs
136
+ ? `
137
+
138
+ The user's original assertion contains environment variable references using the
139
+ syntax \`{{$.env.NAME}}\`. To keep cached Playwright steps valid across runs with
140
+ different env values, you MUST emit those same placeholders in any
141
+ playwrightAssertionStep \`value\`/\`attributeValue\` field whose contents come from
142
+ an env var. Do NOT bake the literal current value into the step.
143
+
144
+ Original (uninterpolated) assertion: ${rawAssertion}
145
+
146
+ Current env mapping (use these to identify which substrings on the page came
147
+ from which env var, then emit the placeholder rather than the literal):
148
+ ${referencedEnvEntries.map(([name, value]) => ` - {{$.env.${name}}} = ${JSON.stringify(value)}`).join('\n')}
149
+
150
+ Examples:
151
+ - Raw assertion "Welcome banner says hello {{$.env.USERNAME}}", USERNAME="alice", page shows "Welcome alice" →
152
+ [{ locator: "text", role: null, value: "{{$.env.USERNAME}}", valueIsRegex: false, assertion: "toBeVisible", attributeValue: null }]
153
+ - Raw assertion "The username field shows {{$.env.USERNAME}}", USERNAME="alice", page input value is "alice" →
154
+ [{ locator: "label", role: null, value: "Username", valueIsRegex: false, assertion: "toHaveValue", attributeValue: "{{$.env.USERNAME}}" }]
155
+ - For literal page text unrelated to env vars, keep the literal value as usual.`
156
+ : '';
127
157
  const promptMessages = [
128
158
  {
129
159
  type: 'system',
@@ -142,7 +172,7 @@ CRITICAL RULES for generating structured steps — follow these precisely:
142
172
  - Text input / textarea content: use 'toHaveValue' with locator='label' and set attributeValue to the expected text. Do NOT use 'toBeVisible' on the textbox.
143
173
  - Selected tabs, pills, or items with aria-selected: use 'toHaveAttribute' with value='aria-selected' and attributeValue='true', NOT 'toBeVisible' on the text.
144
174
  - Text content within an element: use 'toContainText' with attributeValue set to the substring, NOT 'toBeVisible'.
145
- - Only use 'toBeVisible' when the assertion is genuinely about whether something is visible — not as a fallback for state or value checks.`,
175
+ - Only use 'toBeVisible' when the assertion is genuinely about whether something is visible — not as a fallback for state or value checks.${envBlock}`,
146
176
  },
147
177
  {
148
178
  type: 'user',
@@ -184,7 +214,7 @@ careful positioning lost, etc. A screenshot of the webpage has also been provide
184
214
  verifiedSteps.length > 0) {
185
215
  try {
186
216
  const executor = (0, assertCache_1.buildAssertExecutor)(verifiedSteps);
187
- await executor({ page: page });
217
+ await executor({ page: page, envData: context.envData });
188
218
  }
189
219
  catch (error) {
190
220
  Logger_1.appLogger.debug(`Structured assertion steps failed verification for: "${parameters.assertionToTestFor}" — discarding steps. Error: ${error.message}`);
@@ -33,6 +33,13 @@ export declare const PlaywrightAssertionStepSchema: z.ZodObject<{
33
33
  export type PlaywrightAssertionStep = z.infer<typeof PlaywrightAssertionStepSchema>;
34
34
  export type AssertCacheExecutor = (context: {
35
35
  page: Page;
36
+ /**
37
+ * Optional env mapping used to interpolate `{{$.env.X}}` placeholders that
38
+ * the AI may have embedded into step `value`/`attributeValue` fields. When
39
+ * absent, steps run unchanged (backwards compatible with cache entries
40
+ * recorded before env-aware caching).
41
+ */
42
+ envData?: Record<string, string>;
36
43
  }) => Promise<void>;
37
44
  /**
38
45
  * Builds an executor function from structured assertion steps.
@@ -5,6 +5,7 @@ exports.buildAssertExecutor = buildAssertExecutor;
5
5
  exports.buildLocateExecutor = buildLocateExecutor;
6
6
  const test_1 = require("@playwright/test");
7
7
  const v4_1 = require("zod/v4");
8
+ const TemplateInterpolator_1 = require("../../../utils/TemplateInterpolator");
8
9
  const buildLocator_1 = require("../locate/buildLocator");
9
10
  // ---------------------------------------------------------------------------
10
11
  // Structured assertion step schema
@@ -84,17 +85,33 @@ Common roles: 'heading', 'button', 'link', 'tab', 'tabpanel', 'dialog', 'navigat
84
85
  - toContainText: set to the text substring to match within the element
85
86
  - All other assertions: set to null`),
86
87
  });
88
+ /**
89
+ * Resolves any `{{$.env.X}}` placeholders in a step field against the
90
+ * supplied env data. Returns the input verbatim when no env data is given,
91
+ * preserving backwards compatibility with cached entries that contain
92
+ * literal values only.
93
+ */
94
+ function resolveStepField(value, envData) {
95
+ if (!envData || !value.includes('{{')) {
96
+ return value;
97
+ }
98
+ return (0, TemplateInterpolator_1.interpolateString)(value, { env: envData, calls: [] });
99
+ }
87
100
  /**
88
101
  * Builds an executor function from structured assertion steps.
89
102
  * Each step maps to exactly one Playwright `expect` call — no string
90
103
  * evaluation, no VM contexts.
91
104
  */
92
105
  function buildAssertExecutor(steps) {
93
- return async ({ page }) => {
106
+ return async ({ page, envData }) => {
94
107
  for (const step of steps) {
108
+ const resolvedValue = resolveStepField(step.value, envData);
109
+ const resolvedAttrValue = step.attributeValue === null
110
+ ? null
111
+ : resolveStepField(step.attributeValue, envData);
95
112
  const matcher = step.valueIsRegex
96
- ? new RegExp(step.value)
97
- : step.value;
113
+ ? new RegExp(resolvedValue)
114
+ : resolvedValue;
98
115
  // Page-level assertions (no element locator needed)
99
116
  if (step.assertion === 'toHaveTitle') {
100
117
  await (0, test_1.expect)(page).toHaveTitle(matcher);
@@ -138,13 +155,13 @@ function buildAssertExecutor(steps) {
138
155
  await (0, test_1.expect)(locator).toBeChecked();
139
156
  break;
140
157
  case 'toHaveValue':
141
- await (0, test_1.expect)(locator).toHaveValue(step.attributeValue ?? '');
158
+ await (0, test_1.expect)(locator).toHaveValue(resolvedAttrValue ?? '');
142
159
  break;
143
160
  case 'toContainText':
144
- await (0, test_1.expect)(locator).toContainText(step.attributeValue ?? '');
161
+ await (0, test_1.expect)(locator).toContainText(resolvedAttrValue ?? '');
145
162
  break;
146
163
  case 'toHaveAttribute':
147
- await (0, test_1.expect)(locator).toHaveAttribute(step.value, step.attributeValue ?? '');
164
+ await (0, test_1.expect)(locator).toHaveAttribute(resolvedValue, resolvedAttrValue ?? '');
148
165
  break;
149
166
  }
150
167
  }
@@ -40,8 +40,32 @@ export type AssertOptions = {
40
40
  * and generates equivalent Playwright code which is cached. Subsequent
41
41
  * runs execute the cached code directly, skipping the AI call entirely.
42
42
  * Defaults to `true`.
43
+ *
44
+ * Cached steps preserve `{{$.env.*}}` placeholders for any value that came
45
+ * from an env var, so changing an env value between runs replays the same
46
+ * cached steps with the new value rather than re-invoking the AI.
43
47
  */
44
48
  cache?: boolean;
49
+ /**
50
+ * Explicit environment variable names (in addition to the heuristically
51
+ * derived ones) that the assertion may read via `{{$.env.*}}` interpolations.
52
+ */
53
+ envVars?: string[];
54
+ /**
55
+ * Explicitly supply environment variable values that amend (or override)
56
+ * the environment observed by this `page.ai.assert` call. Keys are merged
57
+ * with any names derived from {@link AssertOptions.envVars} and from
58
+ * `{{$.env.*}}` interpolations in the assertion text.
59
+ *
60
+ * - A `string` value sets or overrides the variable for this invocation.
61
+ * - An `undefined` value *removes* the variable, even if it would
62
+ * otherwise be resolved from persistence.
63
+ *
64
+ * Only the **names** (keys) influence cache lookup; changing a value
65
+ * replays the cached steps with the new value via `{{$.env.*}}` placeholder
66
+ * substitution rather than busting the cache.
67
+ */
68
+ envVals?: Record<string, string | undefined>;
45
69
  };
46
70
  type PageAiAct = {
47
71
  <Schema extends z.ZodObject>(instruction: string, options?: PageAiActWithSchemaOptions<Schema>): Promise<z.infer<Schema>>;
@@ -10,6 +10,7 @@ const GptApiKeysNotSetupException_1 = require("../../exceptions/GptApiKeysNotSet
10
10
  const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
11
11
  const ToolCallFailedException_1 = require("../../exceptions/ToolCallFailedException");
12
12
  const ToolRequiresGptException_1 = require("../../exceptions/ToolRequiresGptException");
13
+ const DonobuFlowsManager_1 = require("../../managers/DonobuFlowsManager");
13
14
  const InteractionVisualizer_1 = require("../../managers/InteractionVisualizer");
14
15
  const ToolManager_1 = require("../../managers/ToolManager");
15
16
  const WebTargetInspector_1 = require("../../managers/WebTargetInspector");
@@ -220,6 +221,33 @@ Valid options:
220
221
  const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
221
222
  const retries = options?.retries ?? 0;
222
223
  const retryDelaySeconds = options?.retryDelaySeconds ?? 3;
224
+ // Distill env var names from `{{$.env.*}}` interpolations in the
225
+ // assertion plus any explicitly provided names/overrides. Cached
226
+ // Playwright steps may carry the same `{{$.env.X}}` placeholders in
227
+ // their `value`/`attributeValue` fields, so we resolve env data at
228
+ // replay time and let the executor interpolate before applying.
229
+ const envVarNames = (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(assertion, [
230
+ ...(options?.envVars ?? []),
231
+ ...Object.keys(options?.envVals ?? {}),
232
+ ]);
233
+ const hasEnvRefs = envVarNames.length > 0;
234
+ const resolveEnvData = async () => {
235
+ if (!hasEnvRefs) {
236
+ return undefined;
237
+ }
238
+ const envData = await sharedState.donobuStack.envDataManager.getByNames(envVarNames);
239
+ if (options?.envVals) {
240
+ for (const [k, v] of Object.entries(options.envVals)) {
241
+ if (v === undefined) {
242
+ delete envData[k];
243
+ }
244
+ else {
245
+ envData[k] = v;
246
+ }
247
+ }
248
+ }
249
+ return envData;
250
+ };
223
251
  // --- Cache lookup (when enabled and not clearing) ---
224
252
  if (useCache && !clearCache) {
225
253
  const cache = getOrInitPageAiCache();
@@ -227,6 +255,7 @@ Valid options:
227
255
  const cached = await cache.getAssert({ pageUrl, assertion });
228
256
  if (cached) {
229
257
  Logger_1.appLogger.debug(`Assert cache HIT for: "${assertion}" - running cached Playwright assertion`);
258
+ const envData = await resolveEnvData();
230
259
  let lastError = null;
231
260
  for (let attempt = 0; attempt <= retries; attempt++) {
232
261
  if (attempt > 0) {
@@ -234,7 +263,7 @@ Valid options:
234
263
  await page.waitForTimeout(retryDelaySeconds * 1000);
235
264
  }
236
265
  try {
237
- await cached.run({ page });
266
+ await cached.run({ page, envData });
238
267
  return; // Assertion passed
239
268
  }
240
269
  catch (error) {
@@ -263,12 +292,28 @@ Valid options:
263
292
  await cache.deleteAssert({ pageUrl, assertion });
264
293
  Logger_1.appLogger.debug(`Assert cache invalidated for: "${assertion}"`);
265
294
  }
266
- // --- Cache miss or cache disabled: run AI assertion ---
267
- const result = await runTool(page, AssertTool_1.AssertTool.NAME, {
268
- assertionToTestFor: assertion,
269
- retries: options?.retries,
270
- retryWaitSeconds: options?.retryDelaySeconds,
271
- }, options?.gptClient);
295
+ // Make env vars available to runTool's envData for `{{$.env.*}}`
296
+ // interpolation inside `assertionToTestFor` and so AssertTool can
297
+ // instruct the AI to emit placeholders in cached step values. Mirrors
298
+ // PageAi.ai for `act`: metadata.envVars is set (overwriting), envVals
299
+ // is restored.
300
+ if (hasEnvRefs) {
301
+ sharedState.donobuFlowMetadata.envVars = envVarNames;
302
+ }
303
+ const previousEnvVals = sharedState.envVals;
304
+ sharedState.envVals = options?.envVals;
305
+ let result;
306
+ try {
307
+ // --- Cache miss or cache disabled: run AI assertion ---
308
+ result = await runTool(page, AssertTool_1.AssertTool.NAME, {
309
+ assertionToTestFor: assertion,
310
+ retries: options?.retries,
311
+ retryWaitSeconds: options?.retryDelaySeconds,
312
+ }, options?.gptClient);
313
+ }
314
+ finally {
315
+ sharedState.envVals = previousEnvVals;
316
+ }
272
317
  if (!result.outcome.isSuccessful) {
273
318
  throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, result.outcome);
274
319
  }
@@ -87,6 +87,9 @@ class ToolManager {
87
87
  startedAt: startedAt,
88
88
  completedAt: null,
89
89
  });
90
+ // Expose the un-interpolated parameters so tools that need to
91
+ // preserve `{{...}}` references (e.g. AssertTool) can read them.
92
+ context.rawParameters = toolParameters;
90
93
  // Use the interpolated parameters when calling the tool.
91
94
  toolCallResult = isFromGpt
92
95
  ? await tool.callFromGpt(context, tool.inputSchemaForGpt.parse(interpolatedParameters))
@@ -22,5 +22,16 @@ export type ToolCallContext = {
22
22
  readonly invokedToolCalls: ToolCall[];
23
23
  readonly metadata: FlowMetadata;
24
24
  readonly toolCallId: string;
25
+ /**
26
+ * Original (un-interpolated) parameters supplied to the current tool call.
27
+ *
28
+ * `ToolManager.invokeTool` interpolates `{{...}}` expressions in the tool's
29
+ * parameters before invoking the tool, but a tool may need the raw text to
30
+ * preserve env var references in its output (e.g. `AssertTool` emits
31
+ * Playwright steps that retain `{{$.env.X}}` so cached replays stay correct
32
+ * across env value changes). Set by `ToolManager` immediately before the
33
+ * tool runs; absent for direct/legacy invocation paths.
34
+ */
35
+ rawParameters?: Record<string, any>;
25
36
  };
26
37
  //# sourceMappingURL=ToolCallContext.d.ts.map
@@ -124,6 +124,36 @@ It will use a screenshot of the current viewport of the webpage, the webpage's t
124
124
  Logger_1.appLogger.warn(msg);
125
125
  return msg;
126
126
  });
127
+ const rawAssertion = typeof context.rawParameters?.assertionToTestFor === 'string'
128
+ ? context.rawParameters.assertionToTestFor
129
+ : parameters.assertionToTestFor;
130
+ const envEntries = Object.entries(context.envData ?? {});
131
+ // Only treat env vars as "in play" when the raw assertion actually
132
+ // references one — keeps the prompt small for the common case.
133
+ const referencedEnvEntries = envEntries.filter(([name]) => rawAssertion.includes(`{{$.env.${name}}}`));
134
+ const hasEnvRefs = referencedEnvEntries.length > 0;
135
+ const envBlock = hasEnvRefs
136
+ ? `
137
+
138
+ The user's original assertion contains environment variable references using the
139
+ syntax \`{{$.env.NAME}}\`. To keep cached Playwright steps valid across runs with
140
+ different env values, you MUST emit those same placeholders in any
141
+ playwrightAssertionStep \`value\`/\`attributeValue\` field whose contents come from
142
+ an env var. Do NOT bake the literal current value into the step.
143
+
144
+ Original (uninterpolated) assertion: ${rawAssertion}
145
+
146
+ Current env mapping (use these to identify which substrings on the page came
147
+ from which env var, then emit the placeholder rather than the literal):
148
+ ${referencedEnvEntries.map(([name, value]) => ` - {{$.env.${name}}} = ${JSON.stringify(value)}`).join('\n')}
149
+
150
+ Examples:
151
+ - Raw assertion "Welcome banner says hello {{$.env.USERNAME}}", USERNAME="alice", page shows "Welcome alice" →
152
+ [{ locator: "text", role: null, value: "{{$.env.USERNAME}}", valueIsRegex: false, assertion: "toBeVisible", attributeValue: null }]
153
+ - Raw assertion "The username field shows {{$.env.USERNAME}}", USERNAME="alice", page input value is "alice" →
154
+ [{ locator: "label", role: null, value: "Username", valueIsRegex: false, assertion: "toHaveValue", attributeValue: "{{$.env.USERNAME}}" }]
155
+ - For literal page text unrelated to env vars, keep the literal value as usual.`
156
+ : '';
127
157
  const promptMessages = [
128
158
  {
129
159
  type: 'system',
@@ -142,7 +172,7 @@ CRITICAL RULES for generating structured steps — follow these precisely:
142
172
  - Text input / textarea content: use 'toHaveValue' with locator='label' and set attributeValue to the expected text. Do NOT use 'toBeVisible' on the textbox.
143
173
  - Selected tabs, pills, or items with aria-selected: use 'toHaveAttribute' with value='aria-selected' and attributeValue='true', NOT 'toBeVisible' on the text.
144
174
  - Text content within an element: use 'toContainText' with attributeValue set to the substring, NOT 'toBeVisible'.
145
- - Only use 'toBeVisible' when the assertion is genuinely about whether something is visible — not as a fallback for state or value checks.`,
175
+ - Only use 'toBeVisible' when the assertion is genuinely about whether something is visible — not as a fallback for state or value checks.${envBlock}`,
146
176
  },
147
177
  {
148
178
  type: 'user',
@@ -184,7 +214,7 @@ careful positioning lost, etc. A screenshot of the webpage has also been provide
184
214
  verifiedSteps.length > 0) {
185
215
  try {
186
216
  const executor = (0, assertCache_1.buildAssertExecutor)(verifiedSteps);
187
- await executor({ page: page });
217
+ await executor({ page: page, envData: context.envData });
188
218
  }
189
219
  catch (error) {
190
220
  Logger_1.appLogger.debug(`Structured assertion steps failed verification for: "${parameters.assertionToTestFor}" — discarding steps. Error: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donobu",
3
- "version": "5.33.0",
3
+ "version": "5.34.0",
4
4
  "description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/esm/main.js",