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.
- package/dist/esm/lib/ai/PageAi.js +65 -42
- package/dist/esm/lib/page/DonobuExtendedPage.d.ts +33 -0
- package/dist/esm/lib/page/extendPage.js +147 -101
- package/dist/esm/lib/test/testExtension.js +18 -0
- package/dist/esm/reporter/render.js +197 -17
- package/dist/lib/ai/PageAi.js +65 -42
- package/dist/lib/page/DonobuExtendedPage.d.ts +33 -0
- package/dist/lib/page/extendPage.js +147 -101
- package/dist/lib/test/testExtension.js +18 -0
- package/dist/reporter/render.js +197 -17
- package/package.json +1 -1
|
@@ -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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
}
|