donobu 5.23.0 → 5.24.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 (75) hide show
  1. package/dist/apis/SuitesApi.d.ts +36 -0
  2. package/dist/apis/SuitesApi.js +68 -0
  3. package/dist/apis/TestsApi.d.ts +40 -0
  4. package/dist/apis/TestsApi.js +86 -0
  5. package/dist/assets/openapi-schema.yaml +15 -0
  6. package/dist/esm/apis/SuitesApi.d.ts +36 -0
  7. package/dist/esm/apis/SuitesApi.js +68 -0
  8. package/dist/esm/apis/TestsApi.d.ts +40 -0
  9. package/dist/esm/apis/TestsApi.js +86 -0
  10. package/dist/esm/assets/openapi-schema.yaml +15 -0
  11. package/dist/esm/main.d.ts +1 -0
  12. package/dist/esm/managers/AdminApiController.d.ts +8 -0
  13. package/dist/esm/managers/AdminApiController.js +29 -0
  14. package/dist/esm/managers/DonobuFlowsManager.d.ts +29 -1
  15. package/dist/esm/managers/DonobuFlowsManager.js +74 -28
  16. package/dist/esm/managers/DonobuStack.js +1 -1
  17. package/dist/esm/managers/FederatedPagination.js +10 -1
  18. package/dist/esm/managers/FlowCatalog.js +31 -39
  19. package/dist/esm/managers/FlowDependencyAnalyzer.js +12 -0
  20. package/dist/esm/managers/SuitesManager.js +23 -6
  21. package/dist/esm/managers/TestsManager.d.ts +24 -3
  22. package/dist/esm/managers/TestsManager.js +123 -28
  23. package/dist/esm/models/BrowserConfig.d.ts +3 -0
  24. package/dist/esm/models/BrowserStateFlowReference.d.ts +3 -0
  25. package/dist/esm/models/BrowserStateFlowReference.js +13 -2
  26. package/dist/esm/models/CreateDonobuFlow.d.ts +4 -0
  27. package/dist/esm/models/CreateDonobuFlow.js +4 -0
  28. package/dist/esm/models/CreateSuite.d.ts +3 -0
  29. package/dist/esm/models/CreateTest.d.ts +3 -0
  30. package/dist/esm/models/FlowMetadata.d.ts +4 -0
  31. package/dist/esm/models/FlowMetadata.js +8 -0
  32. package/dist/esm/models/RunConfig.d.ts +6 -0
  33. package/dist/esm/models/SuiteMetadata.d.ts +3 -0
  34. package/dist/esm/models/TargetConfig.d.ts +3 -0
  35. package/dist/esm/models/TestMetadata.d.ts +3 -0
  36. package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
  37. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +11 -0
  38. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +7 -1
  39. package/dist/esm/tools/AssertPageTool.d.ts +2 -2
  40. package/dist/esm/tools/TriggerDonobuFlowTool.d.ts +8 -0
  41. package/dist/esm/utils/MiscUtils.d.ts +13 -0
  42. package/dist/esm/utils/MiscUtils.js +21 -0
  43. package/dist/main.d.ts +1 -0
  44. package/dist/managers/AdminApiController.d.ts +8 -0
  45. package/dist/managers/AdminApiController.js +29 -0
  46. package/dist/managers/DonobuFlowsManager.d.ts +29 -1
  47. package/dist/managers/DonobuFlowsManager.js +74 -28
  48. package/dist/managers/DonobuStack.js +1 -1
  49. package/dist/managers/FederatedPagination.js +10 -1
  50. package/dist/managers/FlowCatalog.js +31 -39
  51. package/dist/managers/FlowDependencyAnalyzer.js +12 -0
  52. package/dist/managers/SuitesManager.js +23 -6
  53. package/dist/managers/TestsManager.d.ts +24 -3
  54. package/dist/managers/TestsManager.js +123 -28
  55. package/dist/models/BrowserConfig.d.ts +3 -0
  56. package/dist/models/BrowserStateFlowReference.d.ts +3 -0
  57. package/dist/models/BrowserStateFlowReference.js +13 -2
  58. package/dist/models/CreateDonobuFlow.d.ts +4 -0
  59. package/dist/models/CreateDonobuFlow.js +4 -0
  60. package/dist/models/CreateSuite.d.ts +3 -0
  61. package/dist/models/CreateTest.d.ts +3 -0
  62. package/dist/models/FlowMetadata.d.ts +4 -0
  63. package/dist/models/FlowMetadata.js +8 -0
  64. package/dist/models/RunConfig.d.ts +6 -0
  65. package/dist/models/SuiteMetadata.d.ts +3 -0
  66. package/dist/models/TargetConfig.d.ts +3 -0
  67. package/dist/models/TestMetadata.d.ts +3 -0
  68. package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
  69. package/dist/persistence/flows/FlowsPersistenceSqlite.js +11 -0
  70. package/dist/persistence/flows/FlowsPersistenceVolatile.js +7 -1
  71. package/dist/tools/AssertPageTool.d.ts +2 -2
  72. package/dist/tools/TriggerDonobuFlowTool.d.ts +8 -0
  73. package/dist/utils/MiscUtils.d.ts +13 -0
  74. package/dist/utils/MiscUtils.js +21 -0
  75. package/package.json +1 -1
@@ -159,11 +159,13 @@ class DonobuFlowsManager {
159
159
  const allowedEnvVarsByName = distillAllowedEnvVariableNames(flowParams.overallObjective, flowParams.envVars);
160
160
  const flowMetadata = {
161
161
  id: flowId,
162
+ target: flowParams.target,
162
163
  metadataVersion: 1,
163
164
  createdWithDonobuVersion: MiscUtils_1.MiscUtils.DONOBU_VERSION,
164
165
  name: flowParams.name || null,
165
166
  envVars: allowedEnvVarsByName,
166
167
  runMode: initialRunMode,
168
+ isControlPanelEnabled: flowParams.isControlPanelEnabled ?? true,
167
169
  gptConfigName: gptClientData.gptConfigName,
168
170
  hasGptConfigNameOverride: !gptClientData.agentName,
169
171
  customTools: flowParams.customTools ?? null,
@@ -181,6 +183,7 @@ class DonobuFlowsManager {
181
183
  state: 'UNSTARTED',
182
184
  nextState: null,
183
185
  videoDisabled: flowParams.videoDisabled,
186
+ testId: flowParams.testId ?? null,
184
187
  // Target-specific fields (browser, targetWebsite, isControlPanelEnabled, etc.)
185
188
  ...targetRuntime.getMetadataFields(),
186
189
  };
@@ -277,45 +280,73 @@ class DonobuFlowsManager {
277
280
  throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
278
281
  }
279
282
  /**
280
- * Loads the given flow by ID and returns a `CreateDonobuFlow` object that can be passed to `createFlow`
281
- * as to execute the flow as a rerun (i.e. without agentic decisioning).
283
+ * Converts a prior flow's tool calls into a list of tool calls to invoke when
284
+ * starting a new flow as a rerun (i.e. without agentic decisioning).
282
285
  *
283
- * @param flowId The ID of the flow to prepare as a rerun.
284
- * @returns Parameters that can be passed to createFlow to execute the flow as a rerun.
286
+ * @param priorFlowMetadata The metadata of the flow to prepare as a rerun.
287
+ * @param options The code generation options to use for the rerun.
288
+ *
289
+ * @returns A list of tool calls to invoke when starting the flow.
285
290
  */
286
- async getFlowAsRerun(flowId, options) {
287
- const priorFlowMetadata = await this.getFlowById(flowId);
288
- const originalToolCalls = await this.getToolCalls(flowId);
291
+ async getToolCallsForRerun(priorFlowMetadata, options) {
292
+ const originalToolCalls = await this.getToolCalls(priorFlowMetadata.id);
289
293
  // Delegate tool call preparation to the target runtime plugin so that
290
294
  // rerun logic is fully target-agnostic.
291
295
  const plugin = this.targetRuntimePlugins.get(priorFlowMetadata.target);
292
- const toolCallsOnStart = plugin
296
+ return plugin
293
297
  ? await plugin.prepareToolCallsForRerun(originalToolCalls, options)
294
298
  : await prepareToolCallsForRerun(originalToolCalls, options);
295
- const allowedTools = priorFlowMetadata.allowedTools ?? [];
296
- // Build target-specific config from the prior flow metadata.
299
+ }
300
+ /**
301
+ * Loads the given flow by ID and returns a `CreateDonobuFlow` object that can be passed to `createFlow`
302
+ * to execute the flow as a rerun (i.e. without agentic decisioning).
303
+ *
304
+ * @param flowId The ID of the flow to prepare as a rerun.
305
+ * @returns Parameters that can be passed to createFlow to execute the flow as a rerun.
306
+ */
307
+ async getFlowAsRerun(flowId, options) {
308
+ const priorFlowMetadata = await this.getFlowById(flowId);
309
+ const toolCallsOnStart = await this.getToolCallsForRerun(priorFlowMetadata, options);
310
+ return this.getFlowFromConfigAndToolCalls(MiscUtils_1.MiscUtils.getDisplayName(priorFlowMetadata, 'Untitled Flow'), 'DETERMINISTIC', priorFlowMetadata, toolCallsOnStart);
311
+ }
312
+ /**
313
+ * Takes a RunConfig object, or anything derived from it (FlowMetadata,
314
+ * TestMetadata), plus some additional parameters, and returns a
315
+ * CreateDonobuFlow object that can be passed to `createFlow` to execute the
316
+ * flow.
317
+ *
318
+ * @param name The name for the new flow
319
+ * @param runMode The run mode to be used for the flow
320
+ * @param config The RunConfig object
321
+ * @param toolCallsOnStart An ordered series of tool calls to invoke when
322
+ * starting the flow
323
+ *
324
+ * @returns A CreateDonobuFlow object that can be passed to createFlow to
325
+ * execute the flow.
326
+ */
327
+ getFlowFromConfigAndToolCalls(name, runMode, config, toolCallsOnStart) {
328
+ // Build target-specific config from the config object.
297
329
  const targetConfig = {};
298
- const targetKey = priorFlowMetadata.target;
299
- if (targetKey && targetKey in priorFlowMetadata) {
300
- targetConfig[targetKey] = priorFlowMetadata[targetKey];
330
+ const targetKey = config.target;
331
+ if (targetKey && targetKey in config) {
332
+ targetConfig[targetKey] = config[targetKey];
301
333
  }
302
334
  return {
303
- target: priorFlowMetadata.target,
335
+ target: config.target,
304
336
  ...targetConfig,
305
- overallObjective: priorFlowMetadata.overallObjective ?? undefined,
306
- name: priorFlowMetadata.name ?? undefined,
307
- callbackUrl: priorFlowMetadata.callbackUrl ?? undefined,
308
- customTools: priorFlowMetadata.customTools ?? undefined,
309
- maxToolCalls: priorFlowMetadata.maxToolCalls,
310
- gptConfigNameOverride: priorFlowMetadata.gptConfigName ?? undefined,
311
- defaultMessageDuration: 0,
312
- initialRunMode: 'DETERMINISTIC',
313
- isControlPanelEnabled: false,
314
- allowedTools: allowedTools,
315
- toolCallsOnStart: toolCallsOnStart,
316
- envVars: priorFlowMetadata.envVars ?? undefined,
317
- resultJsonSchema: priorFlowMetadata.resultJsonSchema ?? undefined,
318
- videoDisabled: priorFlowMetadata.videoDisabled,
337
+ overallObjective: config.overallObjective ?? undefined,
338
+ name,
339
+ callbackUrl: config.callbackUrl ?? undefined,
340
+ customTools: config.customTools ?? undefined,
341
+ maxToolCalls: config.maxToolCalls,
342
+ defaultMessageDuration: runMode === 'DETERMINISTIC' ? 1000 : undefined,
343
+ initialRunMode: runMode,
344
+ isControlPanelEnabled: true,
345
+ allowedTools: config.allowedTools ?? [],
346
+ toolCallsOnStart,
347
+ envVars: config.envVars ?? undefined,
348
+ resultJsonSchema: config.resultJsonSchema ?? undefined,
349
+ videoDisabled: config.videoDisabled,
319
350
  };
320
351
  }
321
352
  /** Add a proposed tool call the tool call queue for the given flow by ID. */
@@ -605,6 +636,21 @@ class DonobuFlowsManager {
605
636
  case 'name':
606
637
  flowMetadata = await this.getFlowByName(browserStateRef.value);
607
638
  break;
639
+ case 'testId': {
640
+ // Resolve the test ID to its most recent successful flow.
641
+ const { items } = await this.getFlows({
642
+ testId: browserStateRef.value,
643
+ state: 'SUCCESS',
644
+ sortBy: 'created_at',
645
+ sortOrder: 'desc',
646
+ limit: 1,
647
+ });
648
+ if (items.length === 0) {
649
+ throw new BrowserStateNotFoundException_1.BrowserStateNotFoundException(`test:${browserStateRef.value}`);
650
+ }
651
+ flowMetadata = items[0];
652
+ break;
653
+ }
608
654
  case 'json':
609
655
  return browserStateRef.value;
610
656
  default:
@@ -50,7 +50,7 @@ async function setupDonobuStack(donobuDeploymentEnvironment, controlPanelFactory
50
50
  const envDataManager = new EnvDataManager_1.EnvDataManager(envPersistenceFactory);
51
51
  const flowsManager = new DonobuFlowsManager_1.DonobuFlowsManager(donobuDeploymentEnvironment, gptClientFactory, gptConfigsManager, agentsManager, flowsPersistenceRegistry, envDataManager, controlPanelFactory, environ, resolvedToolRegistry, targetRuntimePlugins);
52
52
  const testsPersistenceRegistry = await TestsPersistenceRegistry_1.TestsPersistenceRegistryImpl.fromEnvironment(environ);
53
- const testsManager = new TestsManager_1.TestsManager(testsPersistenceRegistry, flowsPersistenceRegistry);
53
+ const testsManager = new TestsManager_1.TestsManager(testsPersistenceRegistry, flowsManager);
54
54
  const suitesPersistenceRegistry = await SuitesPersistenceRegistry_1.SuitesPersistenceRegistryImpl.fromEnvironment(environ);
55
55
  const suitesManager = new SuitesManager_1.SuitesManager(suitesPersistenceRegistry, testsPersistenceRegistry);
56
56
  return {
@@ -33,6 +33,7 @@ async function federatedList(layers, query, comparator) {
33
33
  const paginationState = parseCompositePageToken(query.pageToken);
34
34
  const requestedLimit = Math.min(Math.max(1, query.limit ?? 100), 100);
35
35
  const combinedResults = [];
36
+ let layersThatReturnedResults = 0;
36
37
  for (let i = 0; i < layers.length; i++) {
37
38
  if (paginationState.exhaustedSources.includes(i)) {
38
39
  continue;
@@ -44,6 +45,9 @@ async function federatedList(layers, query, comparator) {
44
45
  pageToken: paginationState.sourceTokens[i],
45
46
  };
46
47
  const sourceResult = await layers[i].getItems(sourceQuery);
48
+ if (sourceResult.items.length > 0) {
49
+ layersThatReturnedResults++;
50
+ }
47
51
  combinedResults.push(...sourceResult.items);
48
52
  if (sourceResult.nextPageToken) {
49
53
  paginationState.sourceTokens[i] = sourceResult.nextPageToken;
@@ -52,7 +56,12 @@ async function federatedList(layers, query, comparator) {
52
56
  paginationState.exhaustedSources.push(i);
53
57
  }
54
58
  }
55
- combinedResults.sort(comparator);
59
+ // Only re-sort when results came from multiple layers; a single layer's
60
+ // results are already in the order it returned them, and re-sorting can
61
+ // disturb fields the comparator can't fully account for.
62
+ if (layersThatReturnedResults > 1) {
63
+ combinedResults.sort(comparator);
64
+ }
56
65
  const limitedResults = combinedResults.slice(0, requestedLimit);
57
66
  const hasMore = combinedResults.length > requestedLimit ||
58
67
  paginationState.exhaustedSources.length < layers.length;
@@ -113,47 +113,39 @@ class FlowCatalog {
113
113
  return null;
114
114
  }
115
115
  async getFlows(query) {
116
- const paginationState = (0, FederatedPagination_1.parseCompositePageToken)(query.pageToken);
117
- const requestedLimit = Math.min(Math.max(1, query.limit || 100), 100);
118
- const combinedResults = [];
119
- const persistenceLayers = await this.flowsPersistenceRegistry.getAll();
120
- for (let i = 0; i < persistenceLayers.length; i++) {
121
- if (paginationState.exhaustedSources.includes(i)) {
122
- continue;
123
- }
124
- const sourceLimit = Math.min(requestedLimit * 2, 100);
125
- const sourceQuery = {
126
- ...query,
127
- limit: sourceLimit,
128
- pageToken: paginationState.sourceTokens[i],
129
- };
130
- const sourceResult = await persistenceLayers[i].getFlowsMetadata(sourceQuery);
131
- const itemsWithSource = sourceResult.items.map((flow) => ({
132
- flow,
133
- sourceIndex: i,
134
- }));
135
- combinedResults.push(...itemsWithSource);
136
- if (sourceResult.nextPageToken) {
137
- paginationState.sourceTokens[i] = sourceResult.nextPageToken;
138
- }
139
- else {
140
- paginationState.exhaustedSources.push(i);
116
+ const layers = (await this.flowsPersistenceRegistry.getAll()).map((persistence) => ({
117
+ getItems: (q) => persistence.getFlowsMetadata(q),
118
+ }));
119
+ const sortBy = query.sortBy ?? 'created_at';
120
+ const desc = (query.sortOrder ?? 'desc') === 'desc';
121
+ // For `created_at` we don't have a real field on FlowMetadata; use
122
+ // `startedAt` as a close-enough proxy, falling back to "now" when
123
+ // null so unstarted flows sort to the most-recent end.
124
+ const fieldFor = (flow) => {
125
+ switch (sortBy) {
126
+ case 'created_at':
127
+ return flow.startedAt ?? Date.now();
128
+ case 'name':
129
+ return flow.name ?? '';
130
+ case 'run_mode':
131
+ return flow.runMode;
132
+ case 'state':
133
+ return flow.state;
134
+ default:
135
+ return '';
141
136
  }
142
- }
143
- combinedResults.sort((a, b) => (b.flow.startedAt || 0) - (a.flow.startedAt || 0));
144
- const limitedResults = combinedResults.slice(0, requestedLimit);
145
- const lastTimestamp = limitedResults.length > 0
146
- ? limitedResults[limitedResults.length - 1].flow.startedAt
147
- : null;
148
- paginationState.cursorTimestamp = lastTimestamp;
149
- const hasMore = combinedResults.length > requestedLimit ||
150
- paginationState.exhaustedSources.length < persistenceLayers.length;
151
- return {
152
- items: limitedResults.map((item) => item.flow),
153
- nextPageToken: hasMore
154
- ? (0, FederatedPagination_1.createCompositePageToken)(paginationState)
155
- : undefined,
156
137
  };
138
+ return (0, FederatedPagination_1.federatedList)(layers, query, (a, b) => {
139
+ const aKey = fieldFor(a);
140
+ const bKey = fieldFor(b);
141
+ if (aKey < bKey) {
142
+ return desc ? 1 : -1;
143
+ }
144
+ if (aKey > bKey) {
145
+ return desc ? -1 : 1;
146
+ }
147
+ return 0;
148
+ });
157
149
  }
158
150
  isLocal() {
159
151
  return this.deploymentEnvironment === 'LOCAL';
@@ -83,6 +83,18 @@ class FlowDependencyAnalyzer {
83
83
  return [flowId];
84
84
  }
85
85
  throw new Error(`Flow dependency not found: flow with name "${initialState.value}" does not exist`);
86
+ case 'testId': {
87
+ // Resolve the test ID to its most recent successful flow within the
88
+ // input set. Ordering matches DonobuFlowsManager.getBrowserStorageState,
89
+ // which sorts SUCCESS flows by startedAt desc.
90
+ const candidates = [...flowsMap.values()]
91
+ .filter((f) => f.testId === initialState.value && f.state === 'SUCCESS')
92
+ .sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
93
+ if (candidates.length > 0) {
94
+ return [candidates[0].id];
95
+ }
96
+ throw new Error(`Flow dependency not found: no successful flow exists for test "${initialState.value}"`);
97
+ }
86
98
  case 'json':
87
99
  // Direct JSON state - no dependencies
88
100
  return [];
@@ -53,13 +53,30 @@ class SuitesManager {
53
53
  const layers = (await this.suitesPersistenceRegistry.getAll()).map((persistence) => ({
54
54
  getItems: (q) => persistence.getSuites(q),
55
55
  }));
56
- const sortAsc = query.sortOrder === 'asc';
57
- const sortCol = query.sortBy ?? 'created_at';
56
+ const sortBy = query.sortBy ?? 'created_at';
57
+ const desc = (query.sortOrder ?? 'desc') === 'desc';
58
+ // SuiteMetadata has no `created_at` field — fall back to "now" so newly
59
+ // returned items sort to the most-recent end.
60
+ const fieldFor = (suite) => {
61
+ switch (sortBy) {
62
+ case 'created_at':
63
+ return Date.now();
64
+ case 'name':
65
+ return suite.name ?? '';
66
+ default:
67
+ return '';
68
+ }
69
+ };
58
70
  return (0, FederatedPagination_1.federatedList)(layers, query, (a, b) => {
59
- const aVal = String(a[sortCol] ?? '');
60
- const bVal = String(b[sortCol] ?? '');
61
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
62
- return sortAsc ? cmp : -cmp;
71
+ const aKey = fieldFor(a);
72
+ const bKey = fieldFor(b);
73
+ if (aKey < bKey) {
74
+ return desc ? 1 : -1;
75
+ }
76
+ if (aKey > bKey) {
77
+ return desc ? -1 : 1;
78
+ }
79
+ return 0;
63
80
  });
64
81
  }
65
82
  /**
@@ -1,12 +1,13 @@
1
+ import type { CreateDonobuFlow } from '../models/CreateDonobuFlow';
1
2
  import type { CreateTest } from '../models/CreateTest';
2
3
  import type { PaginatedResult } from '../models/PaginatedResult';
3
4
  import type { TestMetadata, TestsQuery } from '../models/TestMetadata';
4
- import type { FlowsPersistenceRegistry } from '../persistence/flows/FlowsPersistenceRegistry';
5
5
  import type { TestsPersistenceRegistry } from '../persistence/tests/TestsPersistenceRegistry';
6
+ import type { DonobuFlowsManager } from './DonobuFlowsManager';
6
7
  export declare class TestsManager {
7
8
  private readonly testsPersistenceRegistry;
8
- private readonly flowsPersistenceRegistry;
9
- constructor(testsPersistenceRegistry: TestsPersistenceRegistry, flowsPersistenceRegistry: FlowsPersistenceRegistry);
9
+ private readonly flowsManager;
10
+ constructor(testsPersistenceRegistry: TestsPersistenceRegistry, flowsManager: DonobuFlowsManager);
10
11
  createTest(params: CreateTest): Promise<TestMetadata>;
11
12
  getTestById(testId: string): Promise<TestMetadata>;
12
13
  getTests(query: TestsQuery): Promise<PaginatedResult<TestMetadata>>;
@@ -24,5 +25,25 @@ export declare class TestsManager {
24
25
  * layers (Volatile, S3, GCS).
25
26
  */
26
27
  deleteTest(testId: string): Promise<void>;
28
+ /**
29
+ * Gets the list of tool calls to invoke when starting a new flow for the
30
+ * given test, based on the most recent successful flow for the test.
31
+ *
32
+ * @param test - The test to get the tool calls for
33
+ *
34
+ * @returns The tool calls to use when executing the test
35
+ *
36
+ * @throws {Error} if no previous successful flow is found
37
+ */
38
+ private getTestToolCalls;
39
+ /**
40
+ * Creates a new flow (config) for the given test, which should be passed to
41
+ * `flowsManager.createFlow` to execute the test.
42
+ *
43
+ * @param testId - The ID of the test to create a new flow for
44
+ *
45
+ * @returns A new flow configuration
46
+ */
47
+ getNewFlowFromTest(testId: string): Promise<CreateDonobuFlow>;
27
48
  }
28
49
  //# sourceMappingURL=TestsManager.d.ts.map
@@ -2,28 +2,31 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TestsManager = void 0;
4
4
  const crypto_1 = require("crypto");
5
+ const CannotDeleteRunningFlowException_1 = require("../exceptions/CannotDeleteRunningFlowException");
5
6
  const TestNotFoundException_1 = require("../exceptions/TestNotFoundException");
7
+ const MiscUtils_1 = require("../utils/MiscUtils");
6
8
  const FederatedPagination_1 = require("./FederatedPagination");
7
9
  class TestsManager {
8
- constructor(testsPersistenceRegistry, flowsPersistenceRegistry) {
10
+ constructor(testsPersistenceRegistry, flowsManager) {
9
11
  this.testsPersistenceRegistry = testsPersistenceRegistry;
10
- this.flowsPersistenceRegistry = flowsPersistenceRegistry;
12
+ this.flowsManager = flowsManager;
11
13
  }
12
14
  async createTest(params) {
13
15
  const testId = (0, crypto_1.randomUUID)();
16
+ const web = params.web
17
+ ? {
18
+ browser: params.web.browser ?? { using: { type: 'device' } },
19
+ targetWebsite: params.web.targetWebsite,
20
+ }
21
+ : undefined;
14
22
  const testMetadata = {
15
23
  id: testId,
16
24
  metadataVersion: 1,
17
- name: params.name ?? null,
25
+ name: MiscUtils_1.MiscUtils.getDisplayName({ name: params.name ?? null, web }),
18
26
  suiteId: params.suiteId ?? null,
19
27
  nextRunMode: params.nextRunMode ?? 'AUTONOMOUS',
20
28
  target: params.target,
21
- web: params.web
22
- ? {
23
- browser: params.web.browser ?? { using: { type: 'device' } },
24
- targetWebsite: params.web.targetWebsite,
25
- }
26
- : undefined,
29
+ web,
27
30
  envVars: params.envVars ?? null,
28
31
  customTools: params.customTools ?? null,
29
32
  overallObjective: params.overallObjective ?? null,
@@ -55,13 +58,35 @@ class TestsManager {
55
58
  const layers = (await this.testsPersistenceRegistry.getAll()).map((persistence) => ({
56
59
  getItems: (q) => persistence.getTests(q),
57
60
  }));
58
- const sortAsc = query.sortOrder === 'asc';
59
- const sortCol = query.sortBy ?? 'created_at';
61
+ const sortBy = query.sortBy ?? 'created_at';
62
+ const desc = (query.sortOrder ?? 'desc') === 'desc';
63
+ // TestMetadata has no `created_at` field — fall back to "now" so newly
64
+ // returned items sort to the most-recent end. For other sort columns,
65
+ // map the snake_case API name to the camelCase JS field.
66
+ const fieldFor = (test) => {
67
+ switch (sortBy) {
68
+ case 'created_at':
69
+ return Date.now();
70
+ case 'name':
71
+ return test.name ?? '';
72
+ case 'suite_id':
73
+ return test.suiteId ?? '';
74
+ case 'next_run_mode':
75
+ return test.nextRunMode;
76
+ default:
77
+ return '';
78
+ }
79
+ };
60
80
  return (0, FederatedPagination_1.federatedList)(layers, query, (a, b) => {
61
- const aVal = String(a[sortCol] ?? '');
62
- const bVal = String(b[sortCol] ?? '');
63
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
64
- return sortAsc ? cmp : -cmp;
81
+ const aKey = fieldFor(a);
82
+ const bKey = fieldFor(b);
83
+ if (aKey < bKey) {
84
+ return desc ? 1 : -1;
85
+ }
86
+ if (aKey > bKey) {
87
+ return desc ? -1 : 1;
88
+ }
89
+ return 0;
65
90
  });
66
91
  }
67
92
  /**
@@ -91,25 +116,95 @@ class TestsManager {
91
116
  * layers (Volatile, S3, GCS).
92
117
  */
93
118
  async deleteTest(testId) {
94
- const testLayers = await this.testsPersistenceRegistry.getAll();
95
- const flowLayers = await this.flowsPersistenceRegistry.getAll();
96
- for (let i = 0; i < testLayers.length; i++) {
119
+ for (const persistence of await this.testsPersistenceRegistry.getAll()) {
97
120
  try {
98
- await testLayers[i].deleteTest(testId);
99
- // Cascade-delete flows in this layer after successfully deleting
100
- // the test. This mirrors the DB-level ON DELETE CASCADE for
101
- // non-DB layers.
102
- const flowsResult = await flowLayers[i].getFlowsMetadata({
103
- testId,
104
- });
105
- for (const flow of flowsResult.items) {
106
- await flowLayers[i].deleteFlow(flow.id);
107
- }
121
+ await persistence.deleteTest(testId);
108
122
  }
109
123
  catch {
110
124
  // Ignore errors from layers that don't have this test.
111
125
  }
112
126
  }
127
+ // Cascade-delete flows belonging to this test. Paginate through all flows
128
+ // since `getFlows` caps each page at 100.
129
+ const flowsNotDeleted = [];
130
+ let pageToken = undefined;
131
+ do {
132
+ const { items, nextPageToken } = await this.flowsManager.getFlows({
133
+ testId,
134
+ pageToken,
135
+ });
136
+ for (const flow of items) {
137
+ try {
138
+ await this.flowsManager.deleteFlowById(flow.id);
139
+ }
140
+ catch (error) {
141
+ if (!(error instanceof CannotDeleteRunningFlowException_1.CannotDeleteRunningFlowException)) {
142
+ throw error;
143
+ }
144
+ flowsNotDeleted.push(flow);
145
+ }
146
+ }
147
+ pageToken = nextPageToken;
148
+ } while (pageToken);
149
+ if (flowsNotDeleted.length > 0) {
150
+ // TODO: Just warn here? There will be orphaned flows.
151
+ console.warn(`Failed to delete ${flowsNotDeleted.length} flows belonging to test ${testId}: ${flowsNotDeleted.map((f) => f.id).join(', ')}`);
152
+ }
153
+ }
154
+ /**
155
+ * Gets the list of tool calls to invoke when starting a new flow for the
156
+ * given test, based on the most recent successful flow for the test.
157
+ *
158
+ * @param test - The test to get the tool calls for
159
+ *
160
+ * @returns The tool calls to use when executing the test
161
+ *
162
+ * @throws {Error} if no previous successful flow is found
163
+ */
164
+ async getTestToolCalls(test) {
165
+ let previousSuccesfulFlow = undefined;
166
+ let pageToken = undefined;
167
+ while (!previousSuccesfulFlow) {
168
+ const { items: previousFlows, nextPageToken } = await this.flowsManager.getFlows({
169
+ testId: test.id,
170
+ sortBy: 'created_at',
171
+ sortOrder: 'desc',
172
+ pageToken,
173
+ });
174
+ previousSuccesfulFlow = previousFlows.find((f) => f.state === 'SUCCESS');
175
+ if (!previousSuccesfulFlow && !nextPageToken) {
176
+ throw new Error(`No previous successful flow found for test ${test.id}`);
177
+ }
178
+ pageToken = nextPageToken;
179
+ }
180
+ return this.flowsManager.getToolCallsForRerun(previousSuccesfulFlow, {
181
+ areElementIdsVolatile: false,
182
+ disableSelectorFailover: false,
183
+ });
184
+ }
185
+ /**
186
+ * Creates a new flow (config) for the given test, which should be passed to
187
+ * `flowsManager.createFlow` to execute the test.
188
+ *
189
+ * @param testId - The ID of the test to create a new flow for
190
+ *
191
+ * @returns A new flow configuration
192
+ */
193
+ async getNewFlowFromTest(testId) {
194
+ const test = await this.getTestById(testId);
195
+ let toolCallsOnStart = [];
196
+ let runMode = test.nextRunMode;
197
+ if (runMode === 'DETERMINISTIC') {
198
+ try {
199
+ toolCallsOnStart = await this.getTestToolCalls(test);
200
+ }
201
+ catch {
202
+ runMode = 'AUTONOMOUS';
203
+ }
204
+ }
205
+ const newFlowConfig = this.flowsManager.getFlowFromConfigAndToolCalls(test.name ?? `Flow for test ${test.id}`, runMode, test, toolCallsOnStart);
206
+ newFlowConfig.testId = test.id;
207
+ return newFlowConfig;
113
208
  }
114
209
  }
115
210
  exports.TestsManager = TestsManager;
@@ -46,6 +46,9 @@ export declare const BrowserConfigSchema: z.ZodObject<{
46
46
  }, z.core.$strip>, z.ZodObject<{
47
47
  type: z.ZodLiteral<"name">;
48
48
  value: z.ZodString;
49
+ }, z.core.$strip>, z.ZodObject<{
50
+ type: z.ZodLiteral<"testId">;
51
+ value: z.ZodString;
49
52
  }, z.core.$strip>, z.ZodObject<{
50
53
  type: z.ZodLiteral<"json">;
51
54
  value: z.ZodObject<{
@@ -5,6 +5,9 @@ export declare const BrowserStateReferenceSchema: z.ZodDiscriminatedUnion<[z.Zod
5
5
  }, z.core.$strip>, z.ZodObject<{
6
6
  type: z.ZodLiteral<"name">;
7
7
  value: z.ZodString;
8
+ }, z.core.$strip>, z.ZodObject<{
9
+ type: z.ZodLiteral<"testId">;
10
+ value: z.ZodString;
8
11
  }, z.core.$strip>, z.ZodObject<{
9
12
  type: z.ZodLiteral<"json">;
10
13
  value: z.ZodObject<{
@@ -8,11 +8,22 @@ exports.BrowserStateReferenceSchema = v4_1.z
8
8
  .discriminatedUnion('type', [
9
9
  v4_1.z.object({
10
10
  type: v4_1.z.literal('id'),
11
- value: v4_1.z.string(),
11
+ value: v4_1.z
12
+ .string()
13
+ .describe('A specific flow ID to restore browser state from.'),
12
14
  }),
13
15
  v4_1.z.object({
14
16
  type: v4_1.z.literal('name'),
15
- value: v4_1.z.string(),
17
+ value: v4_1.z
18
+ .string()
19
+ .describe('The name of a flow to restore browser state from.'),
20
+ }),
21
+ v4_1.z.object({
22
+ type: v4_1.z.literal('testId'),
23
+ value: v4_1.z
24
+ .string()
25
+ .describe('A test ID. The browser state will be restored from the most ' +
26
+ 'recent successful flow belonging to this test.'),
16
27
  }),
17
28
  v4_1.z.object({
18
29
  type: v4_1.z.literal('json'),
@@ -9,6 +9,9 @@ export declare const CreateDonobuFlowSchema: z.ZodObject<{
9
9
  }, z.core.$strip>, z.ZodObject<{
10
10
  type: z.ZodLiteral<"name">;
11
11
  value: z.ZodString;
12
+ }, z.core.$strip>, z.ZodObject<{
13
+ type: z.ZodLiteral<"testId">;
14
+ value: z.ZodString;
12
15
  }, z.core.$strip>, z.ZodObject<{
13
16
  type: z.ZodLiteral<"json">;
14
17
  value: z.ZodObject<{
@@ -139,6 +142,7 @@ export declare const CreateDonobuFlowSchema: z.ZodObject<{
139
142
  parameters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
140
143
  toolCallId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
141
144
  }, z.core.$strip>>>>;
145
+ testId: z.ZodOptional<z.ZodString>;
142
146
  }, z.core.$loose>;
143
147
  export type CreateDonobuFlow = z.infer<typeof CreateDonobuFlowSchema>;
144
148
  //# sourceMappingURL=CreateDonobuFlow.d.ts.map
@@ -64,6 +64,10 @@ is not defined or unresolvable, the default flow agent configuration will be use
64
64
  .nullable()
65
65
  .optional()
66
66
  .describe('An ordered series of tool calls to invoke when starting the flow'),
67
+ testId: v4_1.z
68
+ .string()
69
+ .optional()
70
+ .describe('The ID of the test that this flow is associated with. Only optional to support legacy flow creation scenarios.'),
67
71
  })
68
72
  .passthrough()
69
73
  .describe('This is the expected payload for a request to create a new Donobu flow');
@@ -28,6 +28,9 @@ export declare const CreateSuiteSchema: z.ZodObject<{
28
28
  }, z.core.$strip>, z.ZodObject<{
29
29
  type: z.ZodLiteral<"name">;
30
30
  value: z.ZodString;
31
+ }, z.core.$strip>, z.ZodObject<{
32
+ type: z.ZodLiteral<"testId">;
33
+ value: z.ZodString;
31
34
  }, z.core.$strip>, z.ZodObject<{
32
35
  type: z.ZodLiteral<"json">;
33
36
  value: z.ZodObject<{