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.
- package/dist/apis/SuitesApi.d.ts +36 -0
- package/dist/apis/SuitesApi.js +68 -0
- package/dist/apis/TestsApi.d.ts +40 -0
- package/dist/apis/TestsApi.js +86 -0
- package/dist/assets/openapi-schema.yaml +15 -0
- package/dist/esm/apis/SuitesApi.d.ts +36 -0
- package/dist/esm/apis/SuitesApi.js +68 -0
- package/dist/esm/apis/TestsApi.d.ts +40 -0
- package/dist/esm/apis/TestsApi.js +86 -0
- package/dist/esm/assets/openapi-schema.yaml +15 -0
- package/dist/esm/main.d.ts +1 -0
- package/dist/esm/managers/AdminApiController.d.ts +8 -0
- package/dist/esm/managers/AdminApiController.js +29 -0
- package/dist/esm/managers/DonobuFlowsManager.d.ts +29 -1
- package/dist/esm/managers/DonobuFlowsManager.js +74 -28
- package/dist/esm/managers/DonobuStack.js +1 -1
- package/dist/esm/managers/FederatedPagination.js +10 -1
- package/dist/esm/managers/FlowCatalog.js +31 -39
- package/dist/esm/managers/FlowDependencyAnalyzer.js +12 -0
- package/dist/esm/managers/SuitesManager.js +23 -6
- package/dist/esm/managers/TestsManager.d.ts +24 -3
- package/dist/esm/managers/TestsManager.js +123 -28
- package/dist/esm/models/BrowserConfig.d.ts +3 -0
- package/dist/esm/models/BrowserStateFlowReference.d.ts +3 -0
- package/dist/esm/models/BrowserStateFlowReference.js +13 -2
- package/dist/esm/models/CreateDonobuFlow.d.ts +4 -0
- package/dist/esm/models/CreateDonobuFlow.js +4 -0
- package/dist/esm/models/CreateSuite.d.ts +3 -0
- package/dist/esm/models/CreateTest.d.ts +3 -0
- package/dist/esm/models/FlowMetadata.d.ts +4 -0
- package/dist/esm/models/FlowMetadata.js +8 -0
- package/dist/esm/models/RunConfig.d.ts +6 -0
- package/dist/esm/models/SuiteMetadata.d.ts +3 -0
- package/dist/esm/models/TargetConfig.d.ts +3 -0
- package/dist/esm/models/TestMetadata.d.ts +3 -0
- package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +11 -0
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +7 -1
- package/dist/esm/tools/AssertPageTool.d.ts +2 -2
- package/dist/esm/tools/TriggerDonobuFlowTool.d.ts +8 -0
- package/dist/esm/utils/MiscUtils.d.ts +13 -0
- package/dist/esm/utils/MiscUtils.js +21 -0
- package/dist/main.d.ts +1 -0
- package/dist/managers/AdminApiController.d.ts +8 -0
- package/dist/managers/AdminApiController.js +29 -0
- package/dist/managers/DonobuFlowsManager.d.ts +29 -1
- package/dist/managers/DonobuFlowsManager.js +74 -28
- package/dist/managers/DonobuStack.js +1 -1
- package/dist/managers/FederatedPagination.js +10 -1
- package/dist/managers/FlowCatalog.js +31 -39
- package/dist/managers/FlowDependencyAnalyzer.js +12 -0
- package/dist/managers/SuitesManager.js +23 -6
- package/dist/managers/TestsManager.d.ts +24 -3
- package/dist/managers/TestsManager.js +123 -28
- package/dist/models/BrowserConfig.d.ts +3 -0
- package/dist/models/BrowserStateFlowReference.d.ts +3 -0
- package/dist/models/BrowserStateFlowReference.js +13 -2
- package/dist/models/CreateDonobuFlow.d.ts +4 -0
- package/dist/models/CreateDonobuFlow.js +4 -0
- package/dist/models/CreateSuite.d.ts +3 -0
- package/dist/models/CreateTest.d.ts +3 -0
- package/dist/models/FlowMetadata.d.ts +4 -0
- package/dist/models/FlowMetadata.js +8 -0
- package/dist/models/RunConfig.d.ts +6 -0
- package/dist/models/SuiteMetadata.d.ts +3 -0
- package/dist/models/TargetConfig.d.ts +3 -0
- package/dist/models/TestMetadata.d.ts +3 -0
- package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
- package/dist/persistence/flows/FlowsPersistenceSqlite.js +11 -0
- package/dist/persistence/flows/FlowsPersistenceVolatile.js +7 -1
- package/dist/tools/AssertPageTool.d.ts +2 -2
- package/dist/tools/TriggerDonobuFlowTool.d.ts +8 -0
- package/dist/utils/MiscUtils.d.ts +13 -0
- package/dist/utils/MiscUtils.js +21 -0
- 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
|
-
*
|
|
281
|
-
*
|
|
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
|
|
284
|
-
* @
|
|
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
|
|
287
|
-
const
|
|
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
|
-
|
|
296
|
+
return plugin
|
|
293
297
|
? await plugin.prepareToolCallsForRerun(originalToolCalls, options)
|
|
294
298
|
: await prepareToolCallsForRerun(originalToolCalls, options);
|
|
295
|
-
|
|
296
|
-
|
|
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 =
|
|
299
|
-
if (targetKey && targetKey in
|
|
300
|
-
targetConfig[targetKey] =
|
|
330
|
+
const targetKey = config.target;
|
|
331
|
+
if (targetKey && targetKey in config) {
|
|
332
|
+
targetConfig[targetKey] = config[targetKey];
|
|
301
333
|
}
|
|
302
334
|
return {
|
|
303
|
-
target:
|
|
335
|
+
target: config.target,
|
|
304
336
|
...targetConfig,
|
|
305
|
-
overallObjective:
|
|
306
|
-
name
|
|
307
|
-
callbackUrl:
|
|
308
|
-
customTools:
|
|
309
|
-
maxToolCalls:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
57
|
-
const
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
9
|
-
constructor(testsPersistenceRegistry: TestsPersistenceRegistry,
|
|
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,
|
|
10
|
+
constructor(testsPersistenceRegistry, flowsManager) {
|
|
9
11
|
this.testsPersistenceRegistry = testsPersistenceRegistry;
|
|
10
|
-
this.
|
|
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
|
|
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
|
|
59
|
-
const
|
|
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
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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<{
|