donobu 5.35.1 → 5.36.1

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.
@@ -148,52 +148,75 @@ class PageAi {
148
148
  return new PageAi(donobu, gptClient, new cache_1.InMemoryPageAiCache());
149
149
  }
150
150
  async ai(page, instruction, options) {
151
- const descriptor = this.buildDescriptor(page, instruction, options);
152
- // Keep the per-page metadata in sync with the env vars needed for this invocation so cached
153
- // replays can resolve interpolations via runTool.
154
- page._dnb.donobuFlowMetadata.envVars = descriptor.envVarNames;
155
- const cachedEntry = descriptor.useCache
156
- ? await this.cache.get(descriptor.key)
157
- : null;
158
- if (cachedEntry) {
159
- page._dnb.donobuFlowMetadata.runMode = 'DETERMINISTIC';
160
- page._dnb.envVals = descriptor.envVals;
161
- try {
162
- await cachedEntry.run({ page });
151
+ const startedAt = Date.now();
152
+ let cacheHit = false;
153
+ let thrownError = undefined;
154
+ try {
155
+ const descriptor = this.buildDescriptor(page, instruction, options);
156
+ // Keep the per-page metadata in sync with the env vars needed for this invocation so cached
157
+ // replays can resolve interpolations via runTool.
158
+ page._dnb.donobuFlowMetadata.envVars = descriptor.envVarNames;
159
+ const cachedEntry = descriptor.useCache
160
+ ? await this.cache.get(descriptor.key)
161
+ : null;
162
+ cacheHit = !!cachedEntry;
163
+ if (cachedEntry) {
164
+ page._dnb.donobuFlowMetadata.runMode = 'DETERMINISTIC';
165
+ page._dnb.envVals = descriptor.envVals;
166
+ try {
167
+ await cachedEntry.run({ page });
168
+ }
169
+ finally {
170
+ page._dnb.envVals = undefined;
171
+ }
172
+ return this.synthesizeResultFromMetadata(page, instruction, descriptor, options);
163
173
  }
164
- finally {
165
- page._dnb.envVals = undefined;
174
+ else {
175
+ const runResult = await this.runner.run({
176
+ page,
177
+ instruction,
178
+ schema: descriptor.schema,
179
+ jsonSchema: descriptor.jsonSchema,
180
+ allowedTools: descriptor.allowedTools,
181
+ maxToolCalls: descriptor.maxToolCalls,
182
+ envVarNames: descriptor.envVarNames,
183
+ envVals: descriptor.envVals,
184
+ runMode: 'AUTONOMOUS',
185
+ gptClient: options?.gptClient,
186
+ });
187
+ if (descriptor.useCache) {
188
+ const preparedToolCalls = await (0, DonobuFlowsManager_1.prepareToolCallsForRerun)(
189
+ // Only retain successfully run tool calls, otherwise when a cache file
190
+ // with some bad calls in it runs in the future, the test will blow up
191
+ // when the first bad tool call is read.
192
+ runResult.donobuFlow.invokedToolCalls.filter((tc) => {
193
+ return tc.outcome.isSuccessful;
194
+ }), {
195
+ areElementIdsVolatile: options?.volatileElementIds,
196
+ disableSelectorFailover: options?.noSelectorFailover,
197
+ }, this.donobu.toolRegistry);
198
+ const cacheEntry = cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(descriptor.key.pageUrl, runResult.donobuFlow.metadata, preparedToolCalls);
199
+ await this.cache.put(cacheEntry);
200
+ }
201
+ return runResult.parsedResult;
166
202
  }
167
- return this.synthesizeResultFromMetadata(page, instruction, descriptor, options);
168
203
  }
169
- else {
170
- const runResult = await this.runner.run({
171
- page,
172
- instruction,
173
- schema: descriptor.schema,
174
- jsonSchema: descriptor.jsonSchema,
175
- allowedTools: descriptor.allowedTools,
176
- maxToolCalls: descriptor.maxToolCalls,
177
- envVarNames: descriptor.envVarNames,
178
- envVals: descriptor.envVals,
179
- runMode: 'AUTONOMOUS',
180
- gptClient: options?.gptClient,
204
+ catch (e) {
205
+ thrownError = e;
206
+ throw e;
207
+ }
208
+ finally {
209
+ page._dnb.aiInvocations.push({
210
+ kind: 'act',
211
+ description: instruction,
212
+ startedAt,
213
+ endedAt: Date.now(),
214
+ cacheHit,
215
+ passed: thrownError === undefined,
216
+ error: thrownError !== undefined
217
+ ? { message: thrownError?.message }
218
+ : undefined,
181
219
  });
182
- if (descriptor.useCache) {
183
- const preparedToolCalls = await (0, DonobuFlowsManager_1.prepareToolCallsForRerun)(
184
- // Only retain successfully run tool calls, otherwise when a cache file
185
- // with some bad calls in it runs in the future, the test will blow up
186
- // when the first bad tool call is read.
187
- runResult.donobuFlow.invokedToolCalls.filter((tc) => {
188
- return tc.outcome.isSuccessful;
189
- }), {
190
- areElementIdsVolatile: options?.volatileElementIds,
191
- disableSelectorFailover: options?.noSelectorFailover,
192
- }, this.donobu.toolRegistry);
193
- const cacheEntry = cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(descriptor.key.pageUrl, runResult.donobuFlow.metadata, preparedToolCalls);
194
- await this.cache.put(cacheEntry);
195
- }
196
- return runResult.parsedResult;
197
220
  }
198
221
  }
199
222
  /**
@@ -13,6 +13,7 @@ import type { FlowsPersistence } from '../../persistence/flows/FlowsPersistence'
13
13
  import type { TestsPersistence } from '../../persistence/tests/TestsPersistence';
14
14
  import type { CookieAnalyses } from '../../tools/CreateBrowserCookieReportTool';
15
15
  import type { AccessibilityResults } from '../../tools/RunAccessibilityTestTool';
16
+ import type { PlaywrightAssertionStep } from '../ai/cache/assertCache';
16
17
  import type { PageAiCache } from '../ai/cache/cache';
17
18
  import type { LocateOptions } from '../ai/locate/locateTypes';
18
19
  import type { PageAi, PageAiNoSchemaOptions, PageAiOptions, PageAiSchemaOptions } from '../ai/PageAi';
@@ -466,6 +467,38 @@ export interface DonobuExtendedPage extends Page {
466
467
  envVals?: Record<string, string | undefined>;
467
468
  /** Sessions recorded by {@link tbd} for post-test code generation. */
468
469
  tbdSessions: TbdSession[];
470
+ /**
471
+ * Wrapping records for every `page.ai`, `page.ai.assert`, and
472
+ * `page.ai.locate` invocation in this test. The HTML reporter renders
473
+ * each as a parent node containing whichever Donobu tool calls and
474
+ * native Playwright steps fell inside its time window, with a
475
+ * `[cached]` badge driven by the per-record `cacheHit` flag.
476
+ *
477
+ * Recording happens for ALL calls (cache hit or miss) so the wrapper
478
+ * is visible regardless. Nested AI calls (e.g. a cached `page.ai`
479
+ * whose runSource calls `page.ai.assert(...)`) become nested wrappers
480
+ * — each carries its own cache state.
481
+ */
482
+ aiInvocations: AiInvocationRecord[];
469
483
  };
470
484
  }
485
+ export interface AiInvocationRecord {
486
+ kind: 'act' | 'assert' | 'locate';
487
+ description: string;
488
+ startedAt: number;
489
+ endedAt: number;
490
+ cacheHit: boolean;
491
+ passed: boolean;
492
+ error?: {
493
+ message?: string;
494
+ };
495
+ /**
496
+ * For cached `page.ai.assert` invocations: the structured Playwright
497
+ * assertion steps that were replayed. The reporter formats these back
498
+ * into source-code lines so the report shows exactly what was checked
499
+ * (e.g. `expect(page.getByRole('heading', { name: '…' })).toBeVisible()`).
500
+ * Undefined for live assert runs, `act`, and `locate` records.
501
+ */
502
+ assertSteps?: PlaywrightAssertionStep[];
503
+ }
471
504
  //# sourceMappingURL=DonobuExtendedPage.d.ts.map
@@ -137,6 +137,7 @@ async function extendPage(page, options) {
137
137
  clearPageAiCache: MiscUtils_1.MiscUtils.yn(envVars_1.env.data.DONOBU_PAGE_AI_CLEAR_CACHE),
138
138
  },
139
139
  tbdSessions: [],
140
+ aiInvocations: [],
140
141
  };
141
142
  const showMouse = async (p) => {
142
143
  if (interactionVisualizer.defaultMessageDurationMillis > 0) {
@@ -217,120 +218,146 @@ Valid options:
217
218
  const pageAi = Object.assign(act, {
218
219
  act,
219
220
  assert: async (assertion, options) => {
220
- const useCache = options?.cache !== false;
221
- const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
222
- const retries = options?.retries ?? 0;
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];
221
+ const aiInvocationStartedAt = Date.now();
222
+ let aiInvocationCacheHit = false;
223
+ let aiInvocationError = undefined;
224
+ let aiInvocationAssertSteps;
225
+ try {
226
+ const useCache = options?.cache !== false;
227
+ const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
228
+ const retries = options?.retries ?? 0;
229
+ const retryDelaySeconds = options?.retryDelaySeconds ?? 3;
230
+ // Distill env var names from `{{$.env.*}}` interpolations in the
231
+ // assertion plus any explicitly provided names/overrides. Cached
232
+ // Playwright steps may carry the same `{{$.env.X}}` placeholders in
233
+ // their `value`/`attributeValue` fields, so we resolve env data at
234
+ // replay time and let the executor interpolate before applying.
235
+ const envVarNames = (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(assertion, [
236
+ ...(options?.envVars ?? []),
237
+ ...Object.keys(options?.envVals ?? {}),
238
+ ]);
239
+ const hasEnvRefs = envVarNames.length > 0;
240
+ const resolveEnvData = async () => {
241
+ if (!hasEnvRefs) {
242
+ return undefined;
243
+ }
244
+ const envData = await sharedState.donobuStack.envDataManager.getByNames(envVarNames);
245
+ if (options?.envVals) {
246
+ for (const [k, v] of Object.entries(options.envVals)) {
247
+ if (v === undefined) {
248
+ delete envData[k];
249
+ }
250
+ else {
251
+ envData[k] = v;
252
+ }
243
253
  }
244
- else {
245
- envData[k] = v;
254
+ }
255
+ return envData;
256
+ };
257
+ // --- Cache lookup (when enabled and not clearing) ---
258
+ if (useCache && !clearCache) {
259
+ const cache = getOrInitPageAiCache();
260
+ const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
261
+ const cached = await cache.getAssert({ pageUrl, assertion });
262
+ if (cached) {
263
+ aiInvocationCacheHit = true;
264
+ aiInvocationAssertSteps = cached.steps;
265
+ Logger_1.appLogger.debug(`Assert cache HIT for: "${assertion}" - running cached Playwright assertion`);
266
+ const envData = await resolveEnvData();
267
+ let lastError = null;
268
+ for (let attempt = 0; attempt <= retries; attempt++) {
269
+ if (attempt > 0) {
270
+ Logger_1.appLogger.info(`Retry ${attempt} of ${retries} for cached assert`);
271
+ await page.waitForTimeout(retryDelaySeconds * 1000);
272
+ }
273
+ try {
274
+ await cached.run({ page, envData });
275
+ return; // Assertion passed
276
+ }
277
+ catch (error) {
278
+ lastError = error;
279
+ }
246
280
  }
281
+ // All retry attempts exhausted
282
+ throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, {
283
+ isSuccessful: false,
284
+ // Strip ANSI: Playwright matchers style their messages for the
285
+ // terminal, but this string flows into JSON-stringified exception
286
+ // messages, the LLM, and HTML/markdown reports — places where the
287
+ // codes never render and just become visible junk.
288
+ forLlm: `Assertion FAILED (cached) for: ${assertion}\nPlaywright Error: ${(0, ansi_1.stripAnsi)(lastError?.message ?? '')}`,
289
+ metadata: {
290
+ cached: true,
291
+ steps: cached.steps,
292
+ },
293
+ });
247
294
  }
248
295
  }
249
- return envData;
250
- };
251
- // --- Cache lookup (when enabled and not clearing) ---
252
- if (useCache && !clearCache) {
253
- const cache = getOrInitPageAiCache();
254
- const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
255
- const cached = await cache.getAssert({ pageUrl, assertion });
256
- if (cached) {
257
- Logger_1.appLogger.debug(`Assert cache HIT for: "${assertion}" - running cached Playwright assertion`);
258
- const envData = await resolveEnvData();
259
- let lastError = null;
260
- for (let attempt = 0; attempt <= retries; attempt++) {
261
- if (attempt > 0) {
262
- Logger_1.appLogger.info(`Retry ${attempt} of ${retries} for cached assert`);
263
- await page.waitForTimeout(retryDelaySeconds * 1000);
264
- }
296
+ // --- Cache invalidation (when clearing) ---
297
+ if (useCache && clearCache) {
298
+ const cache = getOrInitPageAiCache();
299
+ const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
300
+ await cache.deleteAssert({ pageUrl, assertion });
301
+ Logger_1.appLogger.debug(`Assert cache invalidated for: "${assertion}"`);
302
+ }
303
+ // Make env vars available to runTool's envData for `{{$.env.*}}`
304
+ // interpolation inside `assertionToTestFor` and so AssertTool can
305
+ // instruct the AI to emit placeholders in cached step values. Mirrors
306
+ // PageAi.ai for `act`: metadata.envVars is set (overwriting), envVals
307
+ // is restored.
308
+ if (hasEnvRefs) {
309
+ sharedState.donobuFlowMetadata.envVars = envVarNames;
310
+ }
311
+ const previousEnvVals = sharedState.envVals;
312
+ sharedState.envVals = options?.envVals;
313
+ let result;
314
+ try {
315
+ // --- Cache miss or cache disabled: run AI assertion ---
316
+ result = await runTool(page, AssertTool_1.AssertTool.NAME, {
317
+ assertionToTestFor: assertion,
318
+ retries: options?.retries,
319
+ retryWaitSeconds: options?.retryDelaySeconds,
320
+ }, options?.gptClient);
321
+ }
322
+ finally {
323
+ sharedState.envVals = previousEnvVals;
324
+ }
325
+ if (!result.outcome.isSuccessful) {
326
+ throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, result.outcome);
327
+ }
328
+ // --- Cache the Playwright assertion for future runs ---
329
+ if (useCache) {
330
+ const steps = result.outcome.metadata?.playwrightAssertionSteps;
331
+ if (Array.isArray(steps) && steps.length > 0) {
265
332
  try {
266
- await cached.run({ page, envData });
267
- return; // Assertion passed
333
+ const cache = getOrInitPageAiCache();
334
+ const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
335
+ await cache.putAssert({ pageUrl, assertion, steps });
336
+ Logger_1.appLogger.debug(`Assert cache STORED for: "${assertion}"`);
268
337
  }
269
338
  catch (error) {
270
- lastError = error;
339
+ Logger_1.appLogger.debug(`Skipping assert cache for: "${assertion}" - failed to persist: ${error.message}`);
271
340
  }
272
341
  }
273
- // All retry attempts exhausted
274
- throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, {
275
- isSuccessful: false,
276
- // Strip ANSI: Playwright matchers style their messages for the
277
- // terminal, but this string flows into JSON-stringified exception
278
- // messages, the LLM, and HTML/markdown reports — places where the
279
- // codes never render and just become visible junk.
280
- forLlm: `Assertion FAILED (cached) for: ${assertion}\nPlaywright Error: ${(0, ansi_1.stripAnsi)(lastError?.message ?? '')}`,
281
- metadata: {
282
- cached: true,
283
- steps: cached.steps,
284
- },
285
- });
286
342
  }
287
343
  }
288
- // --- Cache invalidation (when clearing) ---
289
- if (useCache && clearCache) {
290
- const cache = getOrInitPageAiCache();
291
- const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
292
- await cache.deleteAssert({ pageUrl, assertion });
293
- Logger_1.appLogger.debug(`Assert cache invalidated for: "${assertion}"`);
294
- }
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);
344
+ catch (e) {
345
+ aiInvocationError = e;
346
+ throw e;
313
347
  }
314
348
  finally {
315
- sharedState.envVals = previousEnvVals;
316
- }
317
- if (!result.outcome.isSuccessful) {
318
- throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, result.outcome);
319
- }
320
- // --- Cache the Playwright assertion for future runs ---
321
- if (useCache) {
322
- const steps = result.outcome.metadata?.playwrightAssertionSteps;
323
- if (Array.isArray(steps) && steps.length > 0) {
324
- try {
325
- const cache = getOrInitPageAiCache();
326
- const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
327
- await cache.putAssert({ pageUrl, assertion, steps });
328
- Logger_1.appLogger.debug(`Assert cache STORED for: "${assertion}"`);
329
- }
330
- catch (error) {
331
- Logger_1.appLogger.debug(`Skipping assert cache for: "${assertion}" - failed to persist: ${error.message}`);
332
- }
333
- }
349
+ sharedState.aiInvocations.push({
350
+ kind: 'assert',
351
+ description: assertion,
352
+ startedAt: aiInvocationStartedAt,
353
+ endedAt: Date.now(),
354
+ cacheHit: aiInvocationCacheHit,
355
+ passed: aiInvocationError === undefined,
356
+ error: aiInvocationError !== undefined
357
+ ? { message: aiInvocationError?.message }
358
+ : undefined,
359
+ assertSteps: aiInvocationAssertSteps,
360
+ });
334
361
  }
335
362
  },
336
363
  extract: async (schema, options) => {
@@ -405,6 +432,9 @@ Use this information to return an appropriate JSON object.`,
405
432
  return result.metadata;
406
433
  },
407
434
  locate: async (description, options) => {
435
+ const aiInvocationStartedAt = Date.now();
436
+ let aiInvocationCacheHit = false;
437
+ let aiInvocationError = undefined;
408
438
  const useCache = options?.cache !== false;
409
439
  const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
410
440
  const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
@@ -464,6 +494,7 @@ Use this information to return an appropriate JSON object.`,
464
494
  });
465
495
  Logger_1.appLogger.debug(`Locate cache HIT for: "${description}" — rebuilt locator from cache`);
466
496
  Logger_1.appLogger.info(`Located: ${candidate}`);
497
+ aiInvocationCacheHit = true;
467
498
  return candidate;
468
499
  }
469
500
  catch {
@@ -503,7 +534,22 @@ Use this information to return an appropriate JSON object.`,
503
534
  Logger_1.appLogger.info(`Located: ${locator}`);
504
535
  return locator;
505
536
  }
537
+ catch (e) {
538
+ aiInvocationError = e;
539
+ throw e;
540
+ }
506
541
  finally {
542
+ sharedState.aiInvocations.push({
543
+ kind: 'locate',
544
+ description,
545
+ startedAt: aiInvocationStartedAt,
546
+ endedAt: Date.now(),
547
+ cacheHit: aiInvocationCacheHit,
548
+ passed: aiInvocationError === undefined,
549
+ error: aiInvocationError !== undefined
550
+ ? { message: aiInvocationError?.message }
551
+ : undefined,
552
+ });
507
553
  clearTimeout(timeoutId);
508
554
  }
509
555
  },
@@ -877,6 +877,24 @@ async function finalizeTest(page, testInfo, logBuffer, videoOption) {
877
877
  catch {
878
878
  // Non-fatal: native step collection failing must not affect the test result.
879
879
  }
880
+ // Attach AI invocation wrappers (page.ai / page.ai.assert / page.ai.locate)
881
+ // so the HTML reporter can render each as a parent node containing the
882
+ // tool calls and native steps that fell inside its time window. The
883
+ // `cacheHit` flag drives the `[cached]` badge on the wrapper itself —
884
+ // not on inner actions, since a cached `page.ai` may legitimately invoke
885
+ // a live `page.ai.assert` and vice versa.
886
+ try {
887
+ const aiInvocations = sharedState.aiInvocations;
888
+ if (aiInvocations.length > 0) {
889
+ await testInfo.attach('donobu-ai-invocations', {
890
+ body: JSON.stringify(aiInvocations),
891
+ contentType: 'application/json',
892
+ });
893
+ }
894
+ }
895
+ catch {
896
+ // Non-fatal.
897
+ }
880
898
  const browserState = await BrowserUtils_1.BrowserUtils.getBrowserStorageState(page.context());
881
899
  await sharedState.persistence.setBrowserState(sharedState.donobuFlowMetadata.id, browserState);
882
900
  }