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.
Files changed (47) hide show
  1. package/dist/cli/donobu-cli.d.ts +8 -0
  2. package/dist/cli/donobu-cli.js +27 -0
  3. package/dist/codegen/CodeGenerator.js +83 -38
  4. package/dist/codegen/TargetCodeGenerator.d.ts +54 -0
  5. package/dist/codegen/TargetCodeGenerator.js +21 -0
  6. package/dist/esm/cli/donobu-cli.d.ts +8 -0
  7. package/dist/esm/cli/donobu-cli.js +27 -0
  8. package/dist/esm/codegen/CodeGenerator.js +83 -38
  9. package/dist/esm/codegen/TargetCodeGenerator.d.ts +54 -0
  10. package/dist/esm/codegen/TargetCodeGenerator.js +21 -0
  11. package/dist/esm/lib/ai/cache/assertCache.d.ts +14 -1
  12. package/dist/esm/lib/ai/cache/cache.d.ts +12 -4
  13. package/dist/esm/lib/ai/cache/cache.js +40 -24
  14. package/dist/esm/lib/ai/cache/cacheEntryBuilder.d.ts +3 -3
  15. package/dist/esm/lib/ai/cache/cacheEntryBuilder.js +4 -6
  16. package/dist/esm/lib/page/extendPage.js +2 -0
  17. package/dist/esm/lib/test/testExtension.js +7 -0
  18. package/dist/esm/lib/test/utils/selfHealing.js +14 -0
  19. package/dist/esm/main.d.ts +7 -1
  20. package/dist/esm/main.js +7 -1
  21. package/dist/esm/managers/DonobuFlowsManager.js +17 -4
  22. package/dist/esm/managers/DonobuStack.js +9 -0
  23. package/dist/esm/managers/TestsManager.js +5 -0
  24. package/dist/esm/managers/ToolManager.js +11 -0
  25. package/dist/esm/models/CreateDonobuFlow.js +1 -1
  26. package/dist/esm/models/RunConfig.js +1 -1
  27. package/dist/esm/models/ToolTemplateDataSource.d.ts +5 -0
  28. package/dist/esm/targets/TargetRuntimePlugin.d.ts +13 -0
  29. package/dist/lib/ai/cache/assertCache.d.ts +14 -1
  30. package/dist/lib/ai/cache/cache.d.ts +12 -4
  31. package/dist/lib/ai/cache/cache.js +40 -24
  32. package/dist/lib/ai/cache/cacheEntryBuilder.d.ts +3 -3
  33. package/dist/lib/ai/cache/cacheEntryBuilder.js +4 -6
  34. package/dist/lib/page/extendPage.js +2 -0
  35. package/dist/lib/test/testExtension.js +7 -0
  36. package/dist/lib/test/utils/selfHealing.js +14 -0
  37. package/dist/main.d.ts +7 -1
  38. package/dist/main.js +7 -1
  39. package/dist/managers/DonobuFlowsManager.js +17 -4
  40. package/dist/managers/DonobuStack.js +9 -0
  41. package/dist/managers/TestsManager.js +5 -0
  42. package/dist/managers/ToolManager.js +11 -0
  43. package/dist/models/CreateDonobuFlow.js +1 -1
  44. package/dist/models/RunConfig.js +1 -1
  45. package/dist/models/ToolTemplateDataSource.d.ts +5 -0
  46. package/dist/targets/TargetRuntimePlugin.d.ts +13 -0
  47. 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.web?.targetWebsite ?? 'flow'}`;
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.name === GoToWebpageTool_1.GoToWebpageTool.NAME && remaingToolCalls.length > 0;
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 gptSetupNote = ` * This test replays a recorded Donobu flow via \`page.ai(...)\` using the cached
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.web?.targetWebsite ?? 'flow'}`;
250
+ : `Test for ${getFlowTargetLabel(flowMetadata)}`;
229
251
  const instructionSource = flowMetadata.overallObjective?.trim()
230
252
  ? flowMetadata.overallObjective
231
- : `Replay the recorded flow for ${flowMetadata.web?.targetWebsite ?? 'flow'}`;
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
- ? `page.ai(\`${sanitizedInstruction}\`, ${aiOptionsLiteral})`
269
- : `page.ai(\`${sanitizedInstruction}\`)`;
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 page.ai caches for flows that have an objective.
483
- .filter(({ metadata }) => metadata.overallObjective?.trim() &&
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
- // If the first tool call is "GoToWebpage", then we peel it off and treat it
489
- // specially (i.e. it will be an explicit tool call in the generated test file).
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
- // Extract hostname from URL for cache key to allow caching across different
494
- // paths and query parameters on the same domain
495
- let pageUrlForCache;
496
- try {
497
- const url = new URL(metadata.web?.targetWebsite ?? '');
498
- pageUrlForCache = url.hostname;
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 useConfig = {
742
- screenshot: 'on',
743
- video: 'on',
744
- ...(runInHeadedMode && { headless: !runInHeadedMode }),
745
- ...(slowMotionDelay &&
746
- slowMotionDelay > 0 && { launchOptions: { slowMo: slowMotionDelay } }),
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 getFlowAsPlaywrightScript(metadata, toolCalls, options, toolRegistry)
812
- : await getFlowAsAiPlaywrightScript(metadata, toolCalls, options, toolRegistry);
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 website tests made with Donobu',
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: PlaywrightAssertionStep[];
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
- /** Discriminator for web vs mobile cache entries. Defaults to `'web'` for backwards compat. */
13
- deviceType?: 'web' | 'android' | 'ios';
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
- /** Plugin-provided device (e.g. mobile). Typed as `unknown` — callers cast as needed. */
44
- device?: unknown;
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.pageUrl === right.pageUrl && left.assertion === right.assertion;
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
- run: (0, assertCache_1.buildAssertExecutor)(entry.steps),
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
- const cloneAssertEntry = (entry) => ({
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.map((a) => JSON.stringify({ pageUrl: a.pageUrl, assertion: a.assertion, steps: a.steps })).join(', ')}]`
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
- ...(entry.deviceType === 'android'
374
- ? { deviceType: 'android' }
375
- : entry.deviceType === 'ios'
376
- ? { deviceType: 'ios' }
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
- * Utility for translating flow metadata into cache entries. By centralising the
6
- * logic here, both the runtime (`PageAi`) and the code generator can build
7
- * matching cache files without duplicating knowledge about the schema.
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
- * Utility for translating flow metadata into cache entries. By centralising the
7
- * logic here, both the runtime (`PageAi`) and the code generator can build
8
- * matching cache files without duplicating knowledge about the schema.
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,
@@ -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.