donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
140 lines • 6.04 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SuitesManager = void 0;
const crypto_1 = require("crypto");
const SuiteNotFoundException_1 = require("../exceptions/SuiteNotFoundException");
const buildProvenance_1 = require("../utils/buildProvenance");
const FederatedPagination_1 = require("./FederatedPagination");
class SuitesManager {
constructor(suitesPersistenceRegistry, testsPersistenceRegistry) {
this.suitesPersistenceRegistry = suitesPersistenceRegistry;
this.testsPersistenceRegistry = testsPersistenceRegistry;
}
async createSuite(params) {
const suiteId = (0, crypto_1.randomUUID)();
const suiteMetadata = {
id: suiteId,
metadataVersion: 1,
name: params.name,
description: params.description ?? null,
target: params.target,
web: params.web
? {
browser: params.web.browser ?? { using: { type: 'device' } },
targetWebsite: params.web.targetWebsite,
}
: undefined,
envVars: params.envVars ?? null,
customTools: params.customTools ?? null,
callbackUrl: params.callbackUrl ?? null,
allowedTools: params.allowedTools ?? undefined,
resultJsonSchema: params.resultJsonSchema ?? null,
maxToolCalls: params.maxToolCalls ?? null,
videoDisabled: params.videoDisabled,
provenance: (0, buildProvenance_1.buildProvenance)('DONOBU_STUDIO'),
};
// Create in the primary persistence layer only.
const primary = await this.suitesPersistenceRegistry.get();
await primary.createSuite(suiteMetadata);
return suiteMetadata;
}
async getSuiteById(suiteId) {
for (const persistence of await this.suitesPersistenceRegistry.getAll()) {
try {
return await persistence.getSuiteById(suiteId);
}
catch (error) {
if (!(error instanceof SuiteNotFoundException_1.SuiteNotFoundException)) {
throw error;
}
}
}
throw SuiteNotFoundException_1.SuiteNotFoundException.forId(suiteId);
}
async getSuites(query) {
const layers = (await this.suitesPersistenceRegistry.getAll()).map((persistence) => ({
getItems: (q) => persistence.getSuites(q),
}));
const sortBy = query.sortBy ?? 'created_at';
const desc = (query.sortOrder ?? 'desc') === 'desc';
// SuiteMetadata has no `created_at` field — fall back to "now" so newly
// returned items sort to the most-recent end.
const fieldFor = (suite) => {
switch (sortBy) {
case 'created_at':
return Date.now();
case 'name':
return suite.name ?? '';
default:
return '';
}
};
return (0, FederatedPagination_1.federatedList)(layers, query, (a, b) => {
const aKey = fieldFor(a);
const bKey = fieldFor(b);
if (aKey < bKey) {
return desc ? 1 : -1;
}
if (aKey > bKey) {
return desc ? -1 : 1;
}
return 0;
});
}
/**
* Update a suite in the persistence layer where it exists.
* Iterates layers, updates in the first one that has it, ignores
* SuiteNotFoundExceptions from others.
*/
async updateSuite(suiteMetadata) {
for (const persistence of await this.suitesPersistenceRegistry.getAll()) {
try {
await persistence.updateSuite(suiteMetadata);
return;
}
catch (error) {
if (!(error instanceof SuiteNotFoundException_1.SuiteNotFoundException)) {
throw error;
}
}
}
throw SuiteNotFoundException_1.SuiteNotFoundException.forId(suiteMetadata.id);
}
/**
* Delete a suite from all persistence layers.
*
* Orphans tests that belong to the suite first, then deletes the suite
* row. The orphan pass must run before the suite delete: SQLite has an
* ON DELETE SET NULL FK on test_metadata.suite_id, which clears the
* column synchronously inside DELETE FROM suite_metadata. If we deleted
* first, the subsequent getTests({ suiteId }) filter would find zero
* rows in the SQLite layer (column already null) and skip the JSON-blob
* rewrite, leaving each affected test's `metadata.suiteId` pointed at a
* suite that no longer exists. Orphaning first rewrites both the SQL
* column and the JSON blob via updateTest, so the FK cascade is then a
* no-op. Non-DB layers (Volatile, S3, GCS) have no FK and rely on this
* pass for the cascade behavior in either ordering.
*/
async deleteSuite(suiteId) {
for (const { key, persistence, } of await this.suitesPersistenceRegistry.getEntries()) {
try {
// Pair by key — the suites and tests registries can have different
// sets of layers (e.g. plugin-only suites persistence) so positional
// indexing isn't safe.
const testsPersistence = await this.testsPersistenceRegistry.getByKey(key);
if (testsPersistence) {
const testsResult = await testsPersistence.getTests({ suiteId });
for (const test of testsResult.items) {
await testsPersistence.updateTest({ ...test, suiteId: null });
}
}
await persistence.deleteSuite(suiteId);
}
catch {
// Ignore errors from layers that don't have this suite.
}
}
}
}
exports.SuitesManager = SuitesManager;
//# sourceMappingURL=SuitesManager.js.map