donobu 5.57.0 → 5.60.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.
- package/dist/cli/donobu-cli.d.ts +8 -0
- package/dist/cli/donobu-cli.js +27 -0
- package/dist/codegen/CodeGenerator.js +83 -38
- package/dist/codegen/TargetCodeGenerator.d.ts +54 -0
- package/dist/codegen/TargetCodeGenerator.js +21 -0
- package/dist/esm/cli/donobu-cli.d.ts +8 -0
- package/dist/esm/cli/donobu-cli.js +27 -0
- package/dist/esm/codegen/CodeGenerator.js +83 -38
- package/dist/esm/codegen/TargetCodeGenerator.d.ts +54 -0
- package/dist/esm/codegen/TargetCodeGenerator.js +21 -0
- package/dist/esm/lib/ai/cache/assertCache.d.ts +14 -1
- package/dist/esm/lib/ai/cache/cache.d.ts +12 -4
- package/dist/esm/lib/ai/cache/cache.js +40 -24
- package/dist/esm/lib/ai/cache/cacheEntryBuilder.d.ts +3 -3
- package/dist/esm/lib/ai/cache/cacheEntryBuilder.js +4 -6
- package/dist/esm/lib/page/extendPage.js +2 -0
- package/dist/esm/lib/test/testExtension.js +7 -0
- package/dist/esm/lib/test/utils/selfHealing.js +14 -0
- package/dist/esm/main.d.ts +7 -1
- package/dist/esm/main.js +7 -1
- package/dist/esm/managers/DonobuFlowsManager.js +17 -4
- package/dist/esm/managers/DonobuStack.js +9 -0
- package/dist/esm/managers/TestsManager.js +5 -0
- package/dist/esm/managers/ToolManager.js +11 -0
- package/dist/esm/models/CreateDonobuFlow.js +1 -1
- package/dist/esm/models/RunConfig.js +1 -1
- package/dist/esm/models/ToolTemplateDataSource.d.ts +5 -0
- package/dist/esm/targets/TargetRuntimePlugin.d.ts +13 -0
- package/dist/lib/ai/cache/assertCache.d.ts +14 -1
- package/dist/lib/ai/cache/cache.d.ts +12 -4
- package/dist/lib/ai/cache/cache.js +40 -24
- package/dist/lib/ai/cache/cacheEntryBuilder.d.ts +3 -3
- package/dist/lib/ai/cache/cacheEntryBuilder.js +4 -6
- package/dist/lib/page/extendPage.js +2 -0
- package/dist/lib/test/testExtension.js +7 -0
- package/dist/lib/test/utils/selfHealing.js +14 -0
- package/dist/main.d.ts +7 -1
- package/dist/main.js +7 -1
- package/dist/managers/DonobuFlowsManager.js +17 -4
- package/dist/managers/DonobuStack.js +9 -0
- package/dist/managers/TestsManager.js +5 -0
- package/dist/managers/ToolManager.js +11 -0
- package/dist/models/CreateDonobuFlow.js +1 -1
- package/dist/models/RunConfig.js +1 -1
- package/dist/models/ToolTemplateDataSource.d.ts +5 -0
- package/dist/targets/TargetRuntimePlugin.d.ts +13 -0
- package/package.json +1 -1
|
@@ -76,6 +76,7 @@ const SummarizeLearningsTool_1 = require("../tools/SummarizeLearningsTool");
|
|
|
76
76
|
const WaitTool_1 = require("../tools/WaitTool");
|
|
77
77
|
const JsonUtils_1 = require("../utils/JsonUtils");
|
|
78
78
|
const MiscUtils_1 = require("../utils/MiscUtils");
|
|
79
|
+
const TargetCodeGenerator_1 = require("./TargetCodeGenerator");
|
|
79
80
|
function getLocalPlaywrightVersion() {
|
|
80
81
|
const pkgPath = require.resolve('playwright/package.json');
|
|
81
82
|
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -112,6 +113,28 @@ function computeRuntimeCacheKeyFields(toolCalls, defaultToolNames, minimalToolNa
|
|
|
112
113
|
maxToolCalls: resolvedMaxToolCalls,
|
|
113
114
|
};
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Built-in code generator for the web (Playwright) target. Owns the full web
|
|
118
|
+
* spec/cache output; non-web targets (API, mobile, …) register their own
|
|
119
|
+
* generators, which may emit an entirely different shape.
|
|
120
|
+
*/
|
|
121
|
+
const webCodeGenerator = {
|
|
122
|
+
targetType: 'web',
|
|
123
|
+
generateClassicScript: (ctx) => getFlowAsPlaywrightScript(ctx.flowMetadata, ctx.toolCalls, ctx.options, ctx.toolRegistry),
|
|
124
|
+
generateAiScript: (ctx) => getFlowAsAiPlaywrightScript(ctx.flowMetadata, ctx.toolCalls, ctx.options, ctx.toolRegistry),
|
|
125
|
+
buildCacheEntry: (ctx) => cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(ctx.pageUrl, ctx.metadata, ctx.toolCalls),
|
|
126
|
+
cachePageUrl: (metadata) => {
|
|
127
|
+
const website = metadata.web?.targetWebsite ?? '';
|
|
128
|
+
try {
|
|
129
|
+
return new URL(website).hostname;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return website;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
requiresBrowserInstall: () => true,
|
|
136
|
+
};
|
|
137
|
+
(0, TargetCodeGenerator_1.registerCodeGenerator)(webCodeGenerator);
|
|
115
138
|
/** Creates a Node.js Microsoft Playwright script to replay the given flow. */
|
|
116
139
|
async function getFlowAsPlaywrightScript(flowMetadata, toolCalls, options = {}, toolRegistry) {
|
|
117
140
|
// These tools are not supported in the generated script because they have
|
|
@@ -166,12 +189,10 @@ async function getFlowAsPlaywrightScript(flowMetadata, toolCalls, options = {},
|
|
|
166
189
|
.replace(/\n/g, '\\n')
|
|
167
190
|
// Escape carriage returns.
|
|
168
191
|
.replace(/\r/g, '\\r')
|
|
169
|
-
: `Test for ${flowMetadata
|
|
192
|
+
: `Test for ${getFlowTargetLabel(flowMetadata)}`;
|
|
170
193
|
const scriptedToolCalls = toolCalls
|
|
171
194
|
.filter((toolCall) => !unsupportedToolsByName.has(toolCall.name))
|
|
172
|
-
.map((toolCall) =>
|
|
173
|
-
return convertProposedToolCallToPlaywrightCode(toolCall);
|
|
174
|
-
})
|
|
195
|
+
.map((toolCall) => convertProposedToolCallToPlaywrightCode(toolCall))
|
|
175
196
|
.join('\n\n');
|
|
176
197
|
const annotationsLiteral = combinedAnnotations.length > 0
|
|
177
198
|
? JSON.stringify({ annotation: combinedAnnotations }, null, 2)
|
|
@@ -185,7 +206,7 @@ const extractedObject = await page.ai.extract(
|
|
|
185
206
|
testInfo.attach('extracted-object', { body: JSON.stringify(extractedObject), contentType: 'application/json' });`
|
|
186
207
|
: '';
|
|
187
208
|
const needsExpectImport = toolCalls.some((toolCall) => toolCall.name === AssertPageTool_1.AssertPageTool.NAME);
|
|
188
|
-
const needsJsonSchemaToZodImport = flowMetadata.resultJsonSchema;
|
|
209
|
+
const needsJsonSchemaToZodImport = !!flowMetadata.resultJsonSchema;
|
|
189
210
|
const preamble = gptSetupNote.trim().length > 0
|
|
190
211
|
? `/**
|
|
191
212
|
${gptSetupNote}*/`
|
|
@@ -207,9 +228,10 @@ async function getFlowAsAiPlaywrightScript(flowMetadata, toolCalls, options, too
|
|
|
207
228
|
const [firstToolCall, ...remaingToolCalls] = toolCalls;
|
|
208
229
|
// If the first tool call is "GoToWebpage", then we peel it off and treat it
|
|
209
230
|
// specially.
|
|
210
|
-
const specialCaseGoto = firstToolCall
|
|
231
|
+
const specialCaseGoto = firstToolCall?.name === GoToWebpageTool_1.GoToWebpageTool.NAME && remaingToolCalls.length > 0;
|
|
211
232
|
const cachePath = (0, cacheLocator_1.relativePageAiCachePathForSource)(node_path_1.default.join('tests', getTestFileName(flowMetadata)));
|
|
212
|
-
const
|
|
233
|
+
const aiHelper = 'page.ai';
|
|
234
|
+
const gptSetupNote = ` * This test replays a recorded Donobu flow via \`${aiHelper}(...)\` using the cached
|
|
213
235
|
* tool calls stored for this spec in \`${cachePath}\`.
|
|
214
236
|
* If the cache entry is missing or the parameters change, the run falls back
|
|
215
237
|
* to autonomous mode and will require a GPT API key (e.g. DONOBU_API_KEY,
|
|
@@ -225,10 +247,10 @@ async function getFlowAsAiPlaywrightScript(flowMetadata, toolCalls, options, too
|
|
|
225
247
|
.replace(/\n/g, '\\n')
|
|
226
248
|
// Escape carriage returns.
|
|
227
249
|
.replace(/\r/g, '\\r')
|
|
228
|
-
: `Test for ${flowMetadata
|
|
250
|
+
: `Test for ${getFlowTargetLabel(flowMetadata)}`;
|
|
229
251
|
const instructionSource = flowMetadata.overallObjective?.trim()
|
|
230
252
|
? flowMetadata.overallObjective
|
|
231
|
-
: `Replay the recorded flow for ${flowMetadata
|
|
253
|
+
: `Replay the recorded flow for ${getFlowTargetLabel(flowMetadata)}`;
|
|
232
254
|
const sanitizedInstruction = sanitizeForTemplateLiteral(instructionSource);
|
|
233
255
|
const annotations = [
|
|
234
256
|
...(options?.flowAnnotations?.[flowMetadata.id] ?? []),
|
|
@@ -265,8 +287,8 @@ async function getFlowAsAiPlaywrightScript(flowMetadata, toolCalls, options, too
|
|
|
265
287
|
}
|
|
266
288
|
const aiOptionsLiteral = optionsLines.length > 0 ? `{${optionsLines.join(',')}}` : '';
|
|
267
289
|
const aiCallExpression = optionsLines.length > 0
|
|
268
|
-
?
|
|
269
|
-
:
|
|
290
|
+
? `${aiHelper}(\`${sanitizedInstruction}\`, ${aiOptionsLiteral})`
|
|
291
|
+
: `${aiHelper}(\`${sanitizedInstruction}\`)`;
|
|
270
292
|
const needsTestInfo = flowMetadata.resultJsonSchema !== null;
|
|
271
293
|
const aiInvocation = needsTestInfo
|
|
272
294
|
? `const extractedObj = await ${aiCallExpression};
|
|
@@ -479,29 +501,29 @@ async function buildCacheContents(flowsWithToolCalls, toolRegistry) {
|
|
|
479
501
|
const defaultToolNames = new Set(toolRegistry.defaultTools().map((tool) => tool.name));
|
|
480
502
|
const minimalToolNames = new Set(toolRegistry.minimalTools().map((t) => t.name));
|
|
481
503
|
const entries = flowsWithToolCalls
|
|
482
|
-
// We can only create
|
|
483
|
-
|
|
504
|
+
// We can only create ai caches for targets that codegen knows how to
|
|
505
|
+
// replay (web + any plugin with a registered generator), with an objective.
|
|
506
|
+
.filter(({ metadata }) => (0, TargetCodeGenerator_1.hasCodeGenerator)(metadata.target) &&
|
|
507
|
+
metadata.overallObjective?.trim() &&
|
|
484
508
|
(metadata.runMode === 'AUTONOMOUS' ||
|
|
485
509
|
metadata.runMode === 'SUPERVISED'))
|
|
486
510
|
.map(({ metadata, toolCalls }) => {
|
|
511
|
+
// Filtered above on hasCodeGenerator, so this is defined.
|
|
512
|
+
const generator = (0, TargetCodeGenerator_1.getCodeGenerator)(metadata.target);
|
|
487
513
|
const [firstToolCall, ...remaingToolCalls] = toolCalls;
|
|
488
|
-
//
|
|
489
|
-
//
|
|
514
|
+
// A leading GoToWebpage is peeled off (it becomes an explicit nav in the
|
|
515
|
+
// generated spec). Naturally a no-op for non-web targets, which never
|
|
516
|
+
// emit GoToWebpage.
|
|
490
517
|
const specialCaseGoto = firstToolCall?.name === GoToWebpageTool_1.GoToWebpageTool.NAME &&
|
|
491
518
|
remaingToolCalls.length > 0;
|
|
492
519
|
const toolCallsForCache = specialCaseGoto ? remaingToolCalls : toolCalls;
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
// Fallback to full URL if parsing fails
|
|
502
|
-
pageUrlForCache = metadata.web?.targetWebsite ?? '';
|
|
503
|
-
}
|
|
504
|
-
const cacheEntry = cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(pageUrlForCache, metadata, toolCallsForCache);
|
|
520
|
+
// Cache key pageUrl (e.g. hostname) is target-specific.
|
|
521
|
+
const pageUrlForCache = generator.cachePageUrl(metadata);
|
|
522
|
+
const cacheEntry = generator.buildCacheEntry({
|
|
523
|
+
pageUrl: pageUrlForCache,
|
|
524
|
+
metadata,
|
|
525
|
+
toolCalls: toolCallsForCache,
|
|
526
|
+
});
|
|
505
527
|
// Compute allowedTools and maxToolCalls as the runtime will see them,
|
|
506
528
|
// so the cache lock file keys match the keys built by PageAi.buildDescriptor
|
|
507
529
|
// when the generated test code is executed.
|
|
@@ -738,13 +760,18 @@ async function generatePlaywrightConfig(flows, storageStatePaths, options) {
|
|
|
738
760
|
projects.push(generateProjectConfig(projectName, flow, storageStatePath));
|
|
739
761
|
}
|
|
740
762
|
const { areElementIdsVolatile, disableSelectorFailover, runInHeadedMode, slowMotionDelay, } = options;
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
763
|
+
const hasBrowserFlows = flows.some((flow) => (0, TargetCodeGenerator_1.getCodeGenerator)(flow.target)?.requiresBrowserInstall(flow) ?? false);
|
|
764
|
+
const useConfig = hasBrowserFlows
|
|
765
|
+
? {
|
|
766
|
+
screenshot: 'on',
|
|
767
|
+
video: 'on',
|
|
768
|
+
...(runInHeadedMode && { headless: !runInHeadedMode }),
|
|
769
|
+
...(slowMotionDelay &&
|
|
770
|
+
slowMotionDelay > 0 && {
|
|
771
|
+
launchOptions: { slowMo: slowMotionDelay },
|
|
772
|
+
}),
|
|
773
|
+
}
|
|
774
|
+
: {};
|
|
748
775
|
const selfHealingOptions = {
|
|
749
776
|
areElementIdsVolatile,
|
|
750
777
|
disableSelectorFailover,
|
|
@@ -783,6 +810,14 @@ function generateProjectConfig(projectName, flow, storageStatePath) {
|
|
|
783
810
|
// Round up to the nearest 10000ms
|
|
784
811
|
const timeoutMilliseconds = Math.max(minimumTimeoutMilliseconds, Math.ceil(calculatedTimeout / 10000) * 10000);
|
|
785
812
|
const testMatch = `tests/${getTestFileName(flow)}`;
|
|
813
|
+
if (!((0, TargetCodeGenerator_1.getCodeGenerator)(flow.target)?.requiresBrowserInstall(flow) ?? true)) {
|
|
814
|
+
return `{
|
|
815
|
+
name: '${projectName}',
|
|
816
|
+
testMatch: '${testMatch}',
|
|
817
|
+
use: {},
|
|
818
|
+
timeout: ${timeoutMilliseconds}
|
|
819
|
+
}`;
|
|
820
|
+
}
|
|
786
821
|
// Get device name from flow config, default to 'Desktop Chromium'
|
|
787
822
|
const deviceName = flow.web?.browser?.using?.type === 'device'
|
|
788
823
|
? flow.web.browser.using.deviceName || 'Desktop Chromium'
|
|
@@ -804,12 +839,19 @@ async function generateTestFiles(flowsWithToolCalls, options, toolRegistry) {
|
|
|
804
839
|
const files = [];
|
|
805
840
|
const scriptVariant = options.playwrightScriptVariant === 'classic' ? 'classic' : 'ai';
|
|
806
841
|
for (const { metadata, toolCalls } of flowsWithToolCalls) {
|
|
842
|
+
const generator = (0, TargetCodeGenerator_1.getCodeGenerator)(metadata.target) ?? webCodeGenerator;
|
|
807
843
|
const fileName = getTestFileName(metadata);
|
|
844
|
+
const ctx = {
|
|
845
|
+
flowMetadata: metadata,
|
|
846
|
+
toolCalls,
|
|
847
|
+
options,
|
|
848
|
+
toolRegistry,
|
|
849
|
+
};
|
|
808
850
|
const content = scriptVariant === 'classic' ||
|
|
809
851
|
!metadata.overallObjective?.trim() ||
|
|
810
852
|
(metadata.runMode !== 'AUTONOMOUS' && metadata.runMode !== 'SUPERVISED')
|
|
811
|
-
? await
|
|
812
|
-
: await
|
|
853
|
+
? await generator.generateClassicScript(ctx)
|
|
854
|
+
: await generator.generateAiScript(ctx);
|
|
813
855
|
files.push({
|
|
814
856
|
path: `tests/${fileName}`,
|
|
815
857
|
content,
|
|
@@ -827,7 +869,7 @@ function generatePackageJson(options) {
|
|
|
827
869
|
return JSON.stringify({
|
|
828
870
|
name: 'playwright-tests',
|
|
829
871
|
version: '1.0.0',
|
|
830
|
-
description: 'Playwright-based
|
|
872
|
+
description: 'Playwright-based tests made with Donobu',
|
|
831
873
|
scripts: {
|
|
832
874
|
test: `donobu test${selfHealingArg}`,
|
|
833
875
|
},
|
|
@@ -856,7 +898,7 @@ Some tests depend on pre-existing browser state (cookies, localStorage, etc.) fr
|
|
|
856
898
|
: '';
|
|
857
899
|
return `# Playwright Tests
|
|
858
900
|
|
|
859
|
-
This project contains [Playwright](https://playwright.dev/)-based tests made with [Donobu](https://www.donobu.com/).
|
|
901
|
+
This project contains [Playwright](https://playwright.dev/)-based web and API tests made with [Donobu](https://www.donobu.com/).
|
|
860
902
|
|
|
861
903
|
## Installation
|
|
862
904
|
|
|
@@ -879,6 +921,9 @@ npm test
|
|
|
879
921
|
\`\`\`
|
|
880
922
|
${browserStatesSection}`;
|
|
881
923
|
}
|
|
924
|
+
function getFlowTargetLabel(flow) {
|
|
925
|
+
return flow.web?.targetWebsite ?? 'flow';
|
|
926
|
+
}
|
|
882
927
|
/**
|
|
883
928
|
* Gets a project name for a flow
|
|
884
929
|
*/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PageAiCacheEntry } from '../lib/ai/cache/cache';
|
|
2
|
+
import type { ToolRegistry } from '../managers/ToolRegistry';
|
|
3
|
+
import type { CodeGenerationOptions } from '../models/CodeGenerationOptions';
|
|
4
|
+
import type { FlowMetadata } from '../models/FlowMetadata';
|
|
5
|
+
import type { ProposedToolCall } from '../models/ProposedToolCall';
|
|
6
|
+
/**
|
|
7
|
+
* Inputs for generating one flow's replay spec.
|
|
8
|
+
*/
|
|
9
|
+
export interface CodeGenContext {
|
|
10
|
+
flowMetadata: FlowMetadata;
|
|
11
|
+
toolCalls: ProposedToolCall[];
|
|
12
|
+
options: CodeGenerationOptions;
|
|
13
|
+
toolRegistry: ToolRegistry;
|
|
14
|
+
}
|
|
15
|
+
export interface CacheEntryContext {
|
|
16
|
+
pageUrl: string;
|
|
17
|
+
metadata: FlowMetadata;
|
|
18
|
+
toolCalls: ProposedToolCall[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A target's code generator. This is an opaque, whole-artifact contract: core
|
|
22
|
+
* asks a generator to produce a flow's replay spec / cache entry and makes NO
|
|
23
|
+
* assumptions about the artifact's internals. Web (Playwright specs) is the
|
|
24
|
+
* built-in; other targets (API, mobile, …) register their own and are free to
|
|
25
|
+
* emit a completely different shape — or none. Deliberately NOT parameterized
|
|
26
|
+
* with web-spec knobs (fixtures, AI helpers, etc.): those would presume every
|
|
27
|
+
* target produces a Playwright test, which they do not.
|
|
28
|
+
*/
|
|
29
|
+
export interface TargetCodeGenerator {
|
|
30
|
+
readonly targetType: string;
|
|
31
|
+
/** Produce the deterministic ("classic") replay spec for one flow. */
|
|
32
|
+
generateClassicScript(ctx: CodeGenContext): Promise<string>;
|
|
33
|
+
/** Produce the AI/cache-backed replay spec for one flow. */
|
|
34
|
+
generateAiScript(ctx: CodeGenContext): Promise<string>;
|
|
35
|
+
/** Build the AI-cache entry (runSource) for one flow. */
|
|
36
|
+
buildCacheEntry(ctx: CacheEntryContext): PageAiCacheEntry;
|
|
37
|
+
/** Cache-key pageUrl for a flow of this target. */
|
|
38
|
+
cachePageUrl(metadata: FlowMetadata): string;
|
|
39
|
+
/**
|
|
40
|
+
* Whether flows of this target require a browser install in the generated
|
|
41
|
+
* project (drives CI setup and the Playwright project `use` block). A coarse
|
|
42
|
+
* capability question, not a directive about spec contents.
|
|
43
|
+
*/
|
|
44
|
+
requiresBrowserInstall(metadata: FlowMetadata): boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Register a target's code generator. Core registers `web`; plugins register
|
|
47
|
+
* their own at startup. */
|
|
48
|
+
export declare function registerCodeGenerator(generator: TargetCodeGenerator): void;
|
|
49
|
+
/** Resolve the generator for a target type, or `undefined` if none is
|
|
50
|
+
* registered (the caller decides how to handle unsupported targets). */
|
|
51
|
+
export declare function getCodeGenerator(targetType: string): TargetCodeGenerator | undefined;
|
|
52
|
+
/** Whether a code generator is registered for the given target type. */
|
|
53
|
+
export declare function hasCodeGenerator(targetType: string): boolean;
|
|
54
|
+
//# sourceMappingURL=TargetCodeGenerator.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCodeGenerator = registerCodeGenerator;
|
|
4
|
+
exports.getCodeGenerator = getCodeGenerator;
|
|
5
|
+
exports.hasCodeGenerator = hasCodeGenerator;
|
|
6
|
+
const registry = new Map();
|
|
7
|
+
/** Register a target's code generator. Core registers `web`; plugins register
|
|
8
|
+
* their own at startup. */
|
|
9
|
+
function registerCodeGenerator(generator) {
|
|
10
|
+
registry.set(generator.targetType, generator);
|
|
11
|
+
}
|
|
12
|
+
/** Resolve the generator for a target type, or `undefined` if none is
|
|
13
|
+
* registered (the caller decides how to handle unsupported targets). */
|
|
14
|
+
function getCodeGenerator(targetType) {
|
|
15
|
+
return registry.get(targetType);
|
|
16
|
+
}
|
|
17
|
+
/** Whether a code generator is registered for the given target type. */
|
|
18
|
+
function hasCodeGenerator(targetType) {
|
|
19
|
+
return registry.has(targetType);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=TargetCodeGenerator.js.map
|
|
@@ -53,15 +53,28 @@ export declare function buildAssertExecutor(steps: PlaywrightAssertionStep[]): A
|
|
|
53
53
|
* on the same domain always resolves to the same cached entry.
|
|
54
54
|
*/
|
|
55
55
|
export type AssertCacheKey = {
|
|
56
|
+
/**
|
|
57
|
+
* Target device discriminator; defaults to `'web'`. Plugin targets supply
|
|
58
|
+
* their own opaque value — core treats any non-`'web'` value as an opaque
|
|
59
|
+
* string.
|
|
60
|
+
*/
|
|
61
|
+
deviceType?: string;
|
|
56
62
|
pageUrl: string;
|
|
57
63
|
assertion: string;
|
|
58
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* A structured assertion step. Web entries store
|
|
67
|
+
* {@link PlaywrightAssertionStep}s, which core compiles into Playwright
|
|
68
|
+
* `expect` calls. Non-web (plugin-target) entries store their own step
|
|
69
|
+
* shapes — opaque to core; the owning plugin interprets them on replay.
|
|
70
|
+
*/
|
|
71
|
+
export type AssertStep = PlaywrightAssertionStep | Record<string, unknown>;
|
|
59
72
|
/**
|
|
60
73
|
* Serialised cache entry stored on disk. Contains structured assertion
|
|
61
74
|
* steps rather than raw code strings.
|
|
62
75
|
*/
|
|
63
76
|
export type AssertCacheEntry = AssertCacheKey & {
|
|
64
|
-
steps:
|
|
77
|
+
steps: AssertStep[];
|
|
65
78
|
};
|
|
66
79
|
/**
|
|
67
80
|
* Entry hydrated with an executable runner.
|
|
@@ -9,8 +9,12 @@ import type { AssertCacheEntry, AssertCacheEntryWithRunner, AssertCacheKey, Loca
|
|
|
9
9
|
* For mobile entries, `pageUrl` is the sentinel `"mobile://app"`.
|
|
10
10
|
*/
|
|
11
11
|
export type PageAiCacheKey = {
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Target device discriminator; defaults to `'web'`. Plugin targets supply
|
|
14
|
+
* their own opaque value (e.g. mobile `'android'`/`'ios'`, API `'api'`) — core
|
|
15
|
+
* treats any non-`'web'` value as an opaque string.
|
|
16
|
+
*/
|
|
17
|
+
deviceType?: string;
|
|
14
18
|
pageUrl: string;
|
|
15
19
|
instruction: string | null;
|
|
16
20
|
schema: Record<string, unknown> | null;
|
|
@@ -39,9 +43,13 @@ export type PageAiCacheContents = {
|
|
|
39
43
|
locators?: LocateCacheEntry[];
|
|
40
44
|
};
|
|
41
45
|
export type PageAiCacheExecutionContext = {
|
|
46
|
+
/** Legacy cutout for the built-in web target. */
|
|
42
47
|
page?: DonobuExtendedPage;
|
|
43
|
-
/**
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Plugin targets supply their own handle under a target-specific key,
|
|
50
|
+
* opaque to core; the plugin's cache replay casts to its concrete shape.
|
|
51
|
+
*/
|
|
52
|
+
[targetHandle: string]: unknown;
|
|
45
53
|
};
|
|
46
54
|
export type PageAiCacheExecutor = (context: PageAiCacheExecutionContext) => Promise<unknown>;
|
|
47
55
|
/**
|
|
@@ -108,15 +108,30 @@ const cloneEntryWithRunner = (entry) => ({
|
|
|
108
108
|
// ---------------------------------------------------------------------------
|
|
109
109
|
// Assert cache helpers
|
|
110
110
|
// ---------------------------------------------------------------------------
|
|
111
|
-
const assertCacheKeysMatch = (left, right) => left.
|
|
111
|
+
const assertCacheKeysMatch = (left, right) => (left.deviceType ?? 'web') === (right.deviceType ?? 'web') &&
|
|
112
|
+
left.pageUrl === right.pageUrl &&
|
|
113
|
+
left.assertion === right.assertion;
|
|
112
114
|
const materializeAssertEntry = (entry) => ({
|
|
113
115
|
...entry,
|
|
114
|
-
|
|
116
|
+
// Only web steps compile into Playwright `expect` calls. Non-web entries
|
|
117
|
+
// are interpreted by their owning plugin, which reads `steps` directly.
|
|
118
|
+
run: (entry.deviceType ?? 'web') === 'web'
|
|
119
|
+
? (0, assertCache_1.buildAssertExecutor)(entry.steps)
|
|
120
|
+
: async () => {
|
|
121
|
+
throw new Error(`No assert executor for deviceType '${entry.deviceType}'; the owning plugin interprets cached steps directly.`);
|
|
122
|
+
},
|
|
115
123
|
});
|
|
116
|
-
|
|
124
|
+
// Drops the live runner to produce a JSON-safe shape for storage/serialization.
|
|
125
|
+
const stripAssertRunner = (entry) => ({
|
|
126
|
+
...(entry.deviceType && entry.deviceType !== 'web'
|
|
127
|
+
? { deviceType: entry.deviceType }
|
|
128
|
+
: {}),
|
|
117
129
|
pageUrl: entry.pageUrl,
|
|
118
130
|
assertion: entry.assertion,
|
|
119
131
|
steps: entry.steps,
|
|
132
|
+
});
|
|
133
|
+
const cloneAssertEntry = (entry) => ({
|
|
134
|
+
...stripAssertRunner(entry),
|
|
120
135
|
run: entry.run,
|
|
121
136
|
});
|
|
122
137
|
// ---------------------------------------------------------------------------
|
|
@@ -152,7 +167,17 @@ const serializeEntry = (entry) => {
|
|
|
152
167
|
const renderCacheModule = (entries, assertions, locators) => {
|
|
153
168
|
const serializedEntries = entries.map(serializeEntry).join(', ');
|
|
154
169
|
const assertionsPart = assertions && assertions.length > 0
|
|
155
|
-
? `, assertions: [${assertions
|
|
170
|
+
? `, assertions: [${assertions
|
|
171
|
+
.map((a) => JSON.stringify({
|
|
172
|
+
// Only emit deviceType for non-web entries (backwards compat).
|
|
173
|
+
...(a.deviceType && a.deviceType !== 'web'
|
|
174
|
+
? { deviceType: a.deviceType }
|
|
175
|
+
: {}),
|
|
176
|
+
pageUrl: a.pageUrl,
|
|
177
|
+
assertion: a.assertion,
|
|
178
|
+
steps: a.steps,
|
|
179
|
+
}))
|
|
180
|
+
.join(', ')}]`
|
|
156
181
|
: '';
|
|
157
182
|
const locatorsPart = locators && locators.length > 0
|
|
158
183
|
? `, locators: [${locators.map((l) => JSON.stringify({ pageUrl: l.pageUrl, description: l.description, result: l.result })).join(', ')}]`
|
|
@@ -227,11 +252,7 @@ class FilePageAiCache {
|
|
|
227
252
|
state,
|
|
228
253
|
result: {
|
|
229
254
|
caches: state.caches.map((entry) => stripRunner(entry)),
|
|
230
|
-
assertions: state.assertions.map((a) => (
|
|
231
|
-
pageUrl: a.pageUrl,
|
|
232
|
-
assertion: a.assertion,
|
|
233
|
-
steps: a.steps,
|
|
234
|
-
})),
|
|
255
|
+
assertions: state.assertions.map((a) => stripAssertRunner(a)),
|
|
235
256
|
locators: state.locators.map((l) => ({
|
|
236
257
|
pageUrl: l.pageUrl,
|
|
237
258
|
description: l.description,
|
|
@@ -370,11 +391,11 @@ class FilePageAiCache {
|
|
|
370
391
|
throw new Error(`Invalid cache entry: run must be a function for ${entry.pageUrl ?? 'unknown page'}.`);
|
|
371
392
|
}
|
|
372
393
|
const normalized = {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
394
|
+
// Preserve any non-web device type verbatim — values are opaque to
|
|
395
|
+
// core (web is the built-in; plugin targets supply their own).
|
|
396
|
+
...(entry.deviceType && entry.deviceType !== 'web'
|
|
397
|
+
? { deviceType: String(entry.deviceType) }
|
|
398
|
+
: {}),
|
|
378
399
|
pageUrl: String(entry.pageUrl ?? ''),
|
|
379
400
|
instruction: entry.instruction === null || entry.instruction === undefined
|
|
380
401
|
? null
|
|
@@ -406,6 +427,9 @@ class FilePageAiCache {
|
|
|
406
427
|
? entry.steps
|
|
407
428
|
: [];
|
|
408
429
|
return materializeAssertEntry({
|
|
430
|
+
...(entry.deviceType && entry.deviceType !== 'web'
|
|
431
|
+
? { deviceType: String(entry.deviceType) }
|
|
432
|
+
: {}),
|
|
409
433
|
pageUrl: String(entry.pageUrl ?? ''),
|
|
410
434
|
assertion: String(entry.assertion ?? ''),
|
|
411
435
|
steps,
|
|
@@ -431,11 +455,7 @@ class FilePageAiCache {
|
|
|
431
455
|
}
|
|
432
456
|
async writeCacheFile(state) {
|
|
433
457
|
const serializedCaches = state.caches.map((entry) => stripRunner(entry));
|
|
434
|
-
const serializedAssertions = state.assertions.map((e) => (
|
|
435
|
-
pageUrl: e.pageUrl,
|
|
436
|
-
assertion: e.assertion,
|
|
437
|
-
steps: e.steps,
|
|
438
|
-
}));
|
|
458
|
+
const serializedAssertions = state.assertions.map((e) => stripAssertRunner(e));
|
|
439
459
|
const serializedLocators = state.locators.map((e) => ({
|
|
440
460
|
pageUrl: e.pageUrl,
|
|
441
461
|
description: e.description,
|
|
@@ -517,11 +537,7 @@ class InMemoryPageAiCache {
|
|
|
517
537
|
async snapshot() {
|
|
518
538
|
return {
|
|
519
539
|
caches: this.cache.map((entry) => stripRunner(entry)),
|
|
520
|
-
assertions: this.assertions.map((a) => (
|
|
521
|
-
pageUrl: a.pageUrl,
|
|
522
|
-
assertion: a.assertion,
|
|
523
|
-
steps: a.steps,
|
|
524
|
-
})),
|
|
540
|
+
assertions: this.assertions.map((a) => stripAssertRunner(a)),
|
|
525
541
|
locators: this.locators.map((l) => ({
|
|
526
542
|
pageUrl: l.pageUrl,
|
|
527
543
|
description: l.description,
|
|
@@ -2,9 +2,9 @@ import type { FlowMetadata } from '../../../models/FlowMetadata';
|
|
|
2
2
|
import type { ProposedToolCall } from '../../../models/ProposedToolCall';
|
|
3
3
|
import type { PageAiCacheEntry } from './cache';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Builds web (Playwright) cache entries. Shared by the web runtime (`PageAi`)
|
|
6
|
+
* and the web code generator so both emit matching `page.run(...)` replay
|
|
7
|
+
* source. Non-web targets build their own cache entries in their own plugins.
|
|
8
8
|
*/
|
|
9
9
|
export declare class PageAiCacheEntryBuilder {
|
|
10
10
|
/**
|
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PageAiCacheEntryBuilder = void 0;
|
|
4
4
|
const CodeGenerator_1 = require("../../../codegen/CodeGenerator");
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Builds web (Playwright) cache entries. Shared by the web runtime (`PageAi`)
|
|
7
|
+
* and the web code generator so both emit matching `page.run(...)` replay
|
|
8
|
+
* source. Non-web targets build their own cache entries in their own plugins.
|
|
9
9
|
*/
|
|
10
10
|
class PageAiCacheEntryBuilder {
|
|
11
11
|
/**
|
|
@@ -20,9 +20,7 @@ class PageAiCacheEntryBuilder {
|
|
|
20
20
|
maxToolCalls: metadata.maxToolCalls,
|
|
21
21
|
envVars: metadata.envVars,
|
|
22
22
|
runSource: `async ({ page }) => {${toolCallCache
|
|
23
|
-
.map((toolCall) =>
|
|
24
|
-
return (0, CodeGenerator_1.convertProposedToolCallToPlaywrightCode)(toolCall);
|
|
25
|
-
})
|
|
23
|
+
.map((toolCall) => (0, CodeGenerator_1.convertProposedToolCallToPlaywrightCode)(toolCall))
|
|
26
24
|
.join('\n\n')}}`,
|
|
27
25
|
};
|
|
28
26
|
}
|
|
@@ -266,6 +266,8 @@ Valid options:
|
|
|
266
266
|
const cached = await cache.getAssert({ pageUrl, assertion });
|
|
267
267
|
if (cached) {
|
|
268
268
|
aiInvocationCacheHit = true;
|
|
269
|
+
// The key above carries no deviceType, so it only matches web
|
|
270
|
+
// entries — whose steps are Playwright-shaped by construction.
|
|
269
271
|
aiInvocationAssertSteps = cached.steps;
|
|
270
272
|
Logger_1.appLogger.debug(`Assert cache HIT for: "${assertion}" - running cached Playwright assertion`);
|
|
271
273
|
const envData = await resolveEnvData();
|
|
@@ -1109,6 +1109,12 @@ function getSanitizedTestName(testInfo) {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
/** Builds a TestMetadata from the fields available on a FlowMetadata. */
|
|
1111
1111
|
function flowMetadataToTestMetadata(testId, flowMeta) {
|
|
1112
|
+
const targetConfig = {};
|
|
1113
|
+
if (flowMeta.target &&
|
|
1114
|
+
flowMeta.target !== 'web' &&
|
|
1115
|
+
flowMeta.target in flowMeta) {
|
|
1116
|
+
targetConfig[flowMeta.target] = flowMeta[flowMeta.target];
|
|
1117
|
+
}
|
|
1112
1118
|
return {
|
|
1113
1119
|
id: testId,
|
|
1114
1120
|
name: flowMeta.name,
|
|
@@ -1125,6 +1131,7 @@ function flowMetadataToTestMetadata(testId, flowMeta) {
|
|
|
1125
1131
|
suiteId: null,
|
|
1126
1132
|
nextRunMode: 'DETERMINISTIC',
|
|
1127
1133
|
provenance: (0, buildProvenance_1.buildProvenance)('CODE'),
|
|
1134
|
+
...targetConfig,
|
|
1128
1135
|
};
|
|
1129
1136
|
}
|
|
1130
1137
|
//# sourceMappingURL=testExtension.js.map
|
|
@@ -267,6 +267,20 @@ to someone who is tasked with completing it as a goal.
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
async function buildToolList(donobuFlowMetadata, toolRegistry) {
|
|
270
|
+
if (donobuFlowMetadata.target !== 'web') {
|
|
271
|
+
const targetDefaultTools = toolRegistry
|
|
272
|
+
.defaultTools()
|
|
273
|
+
.filter((tool) => tool.supportedTargets.length === 0 ||
|
|
274
|
+
tool.supportedTargets.includes(donobuFlowMetadata.target))
|
|
275
|
+
.map((tool) => tool.name);
|
|
276
|
+
const defaultTools = new Set([
|
|
277
|
+
...(donobuFlowMetadata.allowedTools
|
|
278
|
+
? donobuFlowMetadata.allowedTools
|
|
279
|
+
: []),
|
|
280
|
+
...targetDefaultTools,
|
|
281
|
+
]);
|
|
282
|
+
return [...defaultTools];
|
|
283
|
+
}
|
|
270
284
|
const baseTools = [
|
|
271
285
|
AnalyzePageTextTool_1.AnalyzePageTextTool.NAME,
|
|
272
286
|
AssertTool_1.AssertTool.NAME,
|
package/dist/esm/main.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { GptClient } from './clients/GptClient';
|
|
|
6
6
|
export { type GptClientPlugin, GptClientPluginRegistry, } from './clients/GptClientPlugin';
|
|
7
7
|
export { OpenAiGptClient } from './clients/OpenAiGptClient';
|
|
8
8
|
export { VercelAiGptClient } from './clients/VercelAiGptClient';
|
|
9
|
+
export * from './codegen/TargetCodeGenerator';
|
|
9
10
|
export { env } from './envVars';
|
|
10
11
|
export * from './exceptions/DonobuException';
|
|
11
12
|
export * from './exceptions/FlowIdCollisionException';
|
|
@@ -14,6 +15,7 @@ export * from './exceptions/GptApiKeysNotSetupException';
|
|
|
14
15
|
export * from './exceptions/GptPlatformAuthenticationFailedException';
|
|
15
16
|
export * from './exceptions/InvalidParamValueException';
|
|
16
17
|
export * from './exceptions/ToolCallFailedException';
|
|
18
|
+
export * from './lib/ai/cache/assertCache';
|
|
17
19
|
export * from './lib/ai/cache/cache';
|
|
18
20
|
export * from './lib/ai/cache/cacheLocator';
|
|
19
21
|
export type * from './lib/page/DonobuExtendedPage';
|
|
@@ -29,7 +31,7 @@ export { type DonobuStack, setupDonobuStack } from './managers/DonobuStack';
|
|
|
29
31
|
export { InteractionVisualizer } from './managers/InteractionVisualizer';
|
|
30
32
|
export { type LoadedPlugins, type PluginDependencies, PluginLoader, } from './managers/PluginLoader';
|
|
31
33
|
export type * from './managers/TargetInspector';
|
|
32
|
-
export { ToolManager } from './managers/ToolManager';
|
|
34
|
+
export { buildToolInterpolationDataSource, ToolManager, } from './managers/ToolManager';
|
|
33
35
|
export { createDefaultToolRegistry, type ToolRegistry, } from './managers/ToolRegistry';
|
|
34
36
|
export * from './managers/WebTargetInspector';
|
|
35
37
|
export type * from './models/AiQuery';
|
|
@@ -55,6 +57,7 @@ export type * from './models/ToolCall';
|
|
|
55
57
|
export type * from './models/ToolCallContext';
|
|
56
58
|
export * from './models/ToolCallResult';
|
|
57
59
|
export * from './models/ToolSchema';
|
|
60
|
+
export type * from './models/ToolTemplateDataSource';
|
|
58
61
|
export type { VideoSegment } from './models/VideoSegment';
|
|
59
62
|
export type { EnvPersistence } from './persistence/env/EnvPersistence';
|
|
60
63
|
export { type FileUploadAggregateStatus, type FileUploadPlatformStatus, getFileUploadAggregateStatus, shutdownFileUploadWorkers, } from './persistence/files/fileUploadWorkerRegistry';
|
|
@@ -65,17 +68,20 @@ export type { TestsPersistence } from './persistence/tests/TestsPersistence';
|
|
|
65
68
|
export type * from './targets/TargetProvider';
|
|
66
69
|
export type { TargetRuntime } from './targets/TargetRuntime';
|
|
67
70
|
export { type TargetRuntimeParams, type TargetRuntimePlugin, TargetRuntimePluginRegistry, } from './targets/TargetRuntimePlugin';
|
|
71
|
+
export * from './tools/InputFakerTool';
|
|
68
72
|
export * from './tools/ReplayableInteraction';
|
|
69
73
|
export * from './tools/Tool';
|
|
70
74
|
export * from './utils/buildProvenance';
|
|
71
75
|
export * from './utils/createTool';
|
|
72
76
|
export * from './utils/FlowLogBuffer';
|
|
77
|
+
export * from './utils/JsonPath';
|
|
73
78
|
export * from './utils/JsonSchemaUtils';
|
|
74
79
|
export * from './utils/JsonUtils';
|
|
75
80
|
export * from './utils/Logger';
|
|
76
81
|
export * from './utils/MiscUtils';
|
|
77
82
|
export * from './utils/PlaywrightUtils';
|
|
78
83
|
export * from './utils/TargetUtils';
|
|
84
|
+
export * from './utils/TemplateInterpolator';
|
|
79
85
|
/**
|
|
80
86
|
* Starts a Donobu API server at the given port. The server assumes that the
|
|
81
87
|
* Playwright browsers have been installed.
|