donobu 5.36.0 → 5.36.2

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 (35) hide show
  1. package/dist/esm/managers/DonobuFlowsManager.d.ts +21 -1
  2. package/dist/esm/managers/DonobuFlowsManager.js +51 -2
  3. package/dist/esm/managers/DonobuStack.js +4 -4
  4. package/dist/esm/managers/FederatedPagination.d.ts +43 -13
  5. package/dist/esm/managers/FederatedPagination.js +122 -39
  6. package/dist/esm/managers/SuitesManager.js +11 -6
  7. package/dist/esm/managers/TestsManager.d.ts +20 -2
  8. package/dist/esm/managers/TestsManager.js +67 -14
  9. package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +10 -1
  10. package/dist/esm/persistence/flows/FlowsPersistenceRegistry.d.ts +25 -1
  11. package/dist/esm/persistence/flows/FlowsPersistenceRegistry.js +17 -5
  12. package/dist/esm/persistence/suites/SuitesPersistenceRegistry.d.ts +18 -1
  13. package/dist/esm/persistence/suites/SuitesPersistenceRegistry.js +17 -5
  14. package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +18 -1
  15. package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +20 -5
  16. package/dist/esm/reporter/render.js +36 -10
  17. package/dist/esm/tools/ScrollPageTool.js +1 -1
  18. package/dist/managers/DonobuFlowsManager.d.ts +21 -1
  19. package/dist/managers/DonobuFlowsManager.js +51 -2
  20. package/dist/managers/DonobuStack.js +4 -4
  21. package/dist/managers/FederatedPagination.d.ts +43 -13
  22. package/dist/managers/FederatedPagination.js +122 -39
  23. package/dist/managers/SuitesManager.js +11 -6
  24. package/dist/managers/TestsManager.d.ts +20 -2
  25. package/dist/managers/TestsManager.js +67 -14
  26. package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +10 -1
  27. package/dist/persistence/flows/FlowsPersistenceRegistry.d.ts +25 -1
  28. package/dist/persistence/flows/FlowsPersistenceRegistry.js +17 -5
  29. package/dist/persistence/suites/SuitesPersistenceRegistry.d.ts +18 -1
  30. package/dist/persistence/suites/SuitesPersistenceRegistry.js +17 -5
  31. package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +18 -1
  32. package/dist/persistence/tests/TestsPersistenceRegistry.js +20 -5
  33. package/dist/reporter/render.js +36 -10
  34. package/dist/tools/ScrollPageTool.js +1 -1
  35. package/package.json +1 -1
@@ -8,67 +8,150 @@ function createCompositePageToken(state) {
8
8
  }
9
9
  function parseCompositePageToken(token) {
10
10
  if (!token) {
11
- return { sourceTokens: {}, exhaustedSources: [], cursorTimestamp: null };
11
+ return { sources: [] };
12
12
  }
13
13
  try {
14
- return JSON.parse(Buffer.from(token, 'base64').toString());
14
+ const decoded = JSON.parse(Buffer.from(token, 'base64').toString());
15
+ if (decoded && Array.isArray(decoded.sources)) {
16
+ return decoded;
17
+ }
15
18
  }
16
19
  catch {
17
- return { sourceTokens: {}, exhaustedSources: [], cursorTimestamp: null };
20
+ // fall through
18
21
  }
22
+ return { sources: [] };
19
23
  }
20
24
  /**
21
- * Federate a paginated listing across multiple persistence layers.
25
+ * Federate a paginated listing across multiple persistence layers
26
+ * (resume-by-id).
27
+ *
28
+ * Each layer holds an opaque cursor and an optional `resumeAfterId`. On
29
+ * each page request, federation fetches from each non-exhausted layer
30
+ * (advancing through batches as needed until it has at least `limit`
31
+ * candidates per layer), stable-sorts the merged contributions with the
32
+ * caller's `comparator`, and trims to `limit`. For each layer, the
33
+ * **last kept item in that layer's order** becomes the new
34
+ * `resumeAfterId`, with `cursor` set to the layer-token used to fetch
35
+ * that item's batch. Layers whose contribution is wholly dropped by the
36
+ * trim retain their pre-call state so their items remain reachable on
37
+ * later pages.
22
38
  *
23
- * Each layer is queried with up to the requested limit, results are merged,
24
- * sorted by the provided comparator, and trimmed to the requested limit.
25
- * A composite page token tracks per-source pagination state.
39
+ * **Critical contract: the comparator must NOT tiebreak by id.** Ties
40
+ * on the sort key must return 0 from `comparator`. `Array.sort` is
41
+ * stable since ES2019, and federation feeds the merge in
42
+ * `[layer0..., layer1..., ...]` insertion order, so ties resolve by
43
+ * `(layerIndex, layer-internal position)` — exactly the property that
44
+ * keeps each layer's kept items as a *prefix* of its own order, which
45
+ * is what makes a single `resumeAfterId` representable. An id
46
+ * tiebreaker would silently break this and lose items.
26
47
  *
27
- * @param layers The persistence layers to query.
28
- * @param query The query object (must have `limit`, `pageToken`).
29
- * @param fetcher Function that queries a single layer.
30
- * @param comparator Sort comparator for merging results across layers.
48
+ * **Layer contract.** Layers must return the same items in the same
49
+ * order for the same `(query, pageToken)` pair across calls. Their
50
+ * internal ordering does not need to match federation's tiebreaker —
51
+ * just be deterministic.
31
52
  */
32
53
  async function federatedList(layers, query, comparator) {
33
- const paginationState = parseCompositePageToken(query.pageToken);
34
54
  const requestedLimit = Math.min(Math.max(1, query.limit ?? 100), 100);
35
- const combinedResults = [];
36
- let layersThatReturnedResults = 0;
55
+ const incomingState = parseCompositePageToken(query.pageToken);
56
+ const initialFor = (i) => {
57
+ const s = incomingState.sources[i];
58
+ return s ? { ...s } : { exhausted: false };
59
+ };
60
+ const work = [];
37
61
  for (let i = 0; i < layers.length; i++) {
38
- if (paginationState.exhaustedSources.includes(i)) {
62
+ const init = initialFor(i);
63
+ const w = { contribution: [], layerExhausted: false };
64
+ work.push(w);
65
+ if (init.exhausted) {
66
+ w.layerExhausted = true;
39
67
  continue;
40
68
  }
41
- const sourceLimit = Math.min(requestedLimit * 2, 100);
42
- const sourceQuery = {
43
- ...query,
44
- limit: sourceLimit,
45
- pageToken: paginationState.sourceTokens[i],
46
- };
47
- const sourceResult = await layers[i].getItems(sourceQuery);
48
- if (sourceResult.items.length > 0) {
49
- layersThatReturnedResults++;
69
+ let cursor = init.cursor;
70
+ let resume = init.resumeAfterId;
71
+ while (w.contribution.length < requestedLimit) {
72
+ const layerQuery = {
73
+ ...query,
74
+ limit: requestedLimit,
75
+ pageToken: cursor,
76
+ };
77
+ const batch = await layers[i].getItems(layerQuery);
78
+ let items = batch.items;
79
+ if (resume !== undefined) {
80
+ const idx = items.findIndex((it) => it.id === resume);
81
+ if (idx >= 0) {
82
+ items = items.slice(idx + 1);
83
+ }
84
+ // The skip is one-shot: clear `resume` regardless of whether we
85
+ // found it. If found, we've passed it; if not, it was in a
86
+ // previously-fetched batch and won't reappear in subsequent
87
+ // batches we fetch in this loop.
88
+ resume = undefined;
89
+ }
90
+ for (const it of items) {
91
+ w.contribution.push({
92
+ item: it,
93
+ layerIndex: i,
94
+ fetchCursor: cursor,
95
+ });
96
+ }
97
+ if (!batch.nextPageToken) {
98
+ w.layerExhausted = true;
99
+ break;
100
+ }
101
+ cursor = batch.nextPageToken;
50
102
  }
51
- combinedResults.push(...sourceResult.items);
52
- if (sourceResult.nextPageToken) {
53
- paginationState.sourceTokens[i] = sourceResult.nextPageToken;
103
+ }
104
+ // Stable sort: ties resolve by insertion order = (layerIndex, layer
105
+ // position). Do NOT add an id tiebreaker (see header comment).
106
+ const merged = [];
107
+ for (const w of work) {
108
+ merged.push(...w.contribution);
109
+ }
110
+ merged.sort((a, b) => comparator(a.item, b.item));
111
+ const page = merged.slice(0, requestedLimit);
112
+ const newSources = [];
113
+ for (let i = 0; i < layers.length; i++) {
114
+ const init = initialFor(i);
115
+ if (init.exhausted) {
116
+ newSources.push({ exhausted: true });
117
+ continue;
118
+ }
119
+ const keptFromLayer = page.filter((w) => w.layerIndex === i);
120
+ if (keptFromLayer.length === 0) {
121
+ if (work[i].contribution.length === 0 && work[i].layerExhausted) {
122
+ // Layer truly has nothing — empty stream. Mark exhausted so
123
+ // we don't re-query it forever.
124
+ newSources.push({ exhausted: true });
125
+ }
126
+ else {
127
+ // Layer's items were all outranked by other layers. Roll back
128
+ // to the incoming state so they remain reachable on later
129
+ // pages, when the outranking layers have moved on.
130
+ newSources.push(init);
131
+ }
132
+ continue;
133
+ }
134
+ const lastKept = keptFromLayer[keptFromLayer.length - 1];
135
+ const allFromLayer = work[i].contribution;
136
+ const allKept = allFromLayer.length === keptFromLayer.length;
137
+ if (work[i].layerExhausted && allKept) {
138
+ newSources.push({ exhausted: true });
54
139
  }
55
140
  else {
56
- paginationState.exhaustedSources.push(i);
141
+ newSources.push({
142
+ cursor: lastKept.fetchCursor,
143
+ resumeAfterId: lastKept.item.id,
144
+ exhausted: false,
145
+ });
57
146
  }
58
147
  }
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
- }
65
- const limitedResults = combinedResults.slice(0, requestedLimit);
66
- const hasMore = combinedResults.length > requestedLimit ||
67
- paginationState.exhaustedSources.length < layers.length;
148
+ const someUnconsumed = merged.length > requestedLimit;
149
+ const someNotExhausted = newSources.some((s) => !s.exhausted);
150
+ const hasMore = someUnconsumed || someNotExhausted;
68
151
  return {
69
- items: limitedResults,
152
+ items: page.map((w) => w.item),
70
153
  nextPageToken: hasMore
71
- ? createCompositePageToken(paginationState)
154
+ ? createCompositePageToken({ sources: newSources })
72
155
  : undefined,
73
156
  };
74
157
  }
@@ -108,16 +108,21 @@ class SuitesManager {
108
108
  * behavior for non-DB persistence layers (Volatile, S3, GCS).
109
109
  */
110
110
  async deleteSuite(suiteId) {
111
- const suiteLayers = await this.suitesPersistenceRegistry.getAll();
112
- const testLayers = await this.testsPersistenceRegistry.getAll();
113
- for (let i = 0; i < suiteLayers.length; i++) {
111
+ for (const { key, persistence, } of await this.suitesPersistenceRegistry.getEntries()) {
114
112
  try {
115
- await suiteLayers[i].deleteSuite(suiteId);
113
+ await persistence.deleteSuite(suiteId);
116
114
  // Orphan tests in this layer after successfully deleting the suite.
117
115
  // This mirrors the DB-level ON DELETE SET NULL for non-DB layers.
118
- const testsResult = await testLayers[i].getTests({ suiteId });
116
+ // Pair by key the suites and tests registries can have different
117
+ // sets of layers (e.g. plugin-only suites persistence) so positional
118
+ // indexing isn't safe.
119
+ const testsPersistence = await this.testsPersistenceRegistry.getByKey(key);
120
+ if (!testsPersistence) {
121
+ continue;
122
+ }
123
+ const testsResult = await testsPersistence.getTests({ suiteId });
119
124
  for (const test of testsResult.items) {
120
- await testLayers[i].updateTest({ ...test, suiteId: null });
125
+ await testsPersistence.updateTest({ ...test, suiteId: null });
121
126
  }
122
127
  }
123
128
  catch {
@@ -2,14 +2,30 @@ import type { CreateDonobuFlow } from '../models/CreateDonobuFlow';
2
2
  import type { CreateTest } from '../models/CreateTest';
3
3
  import type { PaginatedResult } from '../models/PaginatedResult';
4
4
  import type { TestListItem, TestMetadata, TestsQuery } from '../models/TestMetadata';
5
+ import type { SuitesPersistenceRegistry } from '../persistence/suites/SuitesPersistenceRegistry';
5
6
  import type { TestsPersistenceRegistry } from '../persistence/tests/TestsPersistenceRegistry';
6
7
  import type { DonobuFlowsManager } from './DonobuFlowsManager';
7
8
  export declare class TestsManager {
8
9
  private readonly testsPersistenceRegistry;
10
+ private readonly suitesPersistenceRegistry;
9
11
  private readonly flowsManager;
10
- constructor(testsPersistenceRegistry: TestsPersistenceRegistry, flowsManager: DonobuFlowsManager);
12
+ constructor(testsPersistenceRegistry: TestsPersistenceRegistry, suitesPersistenceRegistry: SuitesPersistenceRegistry, flowsManager: DonobuFlowsManager);
11
13
  createTest(params: CreateTest): Promise<TestMetadata>;
12
14
  getTestById(testId: string): Promise<TestMetadata>;
15
+ /**
16
+ * Picks the tests persistence layer to use when creating a new test.
17
+ *
18
+ * - If `suiteId` is null/undefined: use the primary tests layer.
19
+ * - If `suiteId` is set: look up the suite's layer key and use the matching
20
+ * tests layer. If no tests layer matches the suite's key (rare — would
21
+ * require asymmetric registry config), fall back to the primary tests
22
+ * layer; the FK won't hold but at least the test is persisted somewhere.
23
+ * - If `suiteId` is set but the suite doesn't exist anywhere: fall back
24
+ * to the primary tests layer (the SQLite FK will reject if applicable;
25
+ * non-DB layers will accept the dangling reference).
26
+ */
27
+ private resolveLayerForTestCreate;
28
+ private findSuiteLayerKey;
13
29
  getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
14
30
  /**
15
31
  * Update a test in the persistence layer where it exists.
@@ -38,7 +54,9 @@ export declare class TestsManager {
38
54
  private getTestToolCalls;
39
55
  /**
40
56
  * Creates a new flow (config) for the given test, which should be passed to
41
- * `flowsManager.createFlow` to execute the test.
57
+ * `flowsManager.createFlow` to execute the test. The returned config's
58
+ * `testId` is set, which `createFlow` uses to route the flow to the same
59
+ * persistence layer as the test.
42
60
  *
43
61
  * @param testId - The ID of the test to create a new flow for
44
62
  *
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TestsManager = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const CannotDeleteRunningFlowException_1 = require("../exceptions/CannotDeleteRunningFlowException");
6
+ const SuiteNotFoundException_1 = require("../exceptions/SuiteNotFoundException");
6
7
  const TestNotFoundException_1 = require("../exceptions/TestNotFoundException");
7
8
  const buildProvenance_1 = require("../utils/buildProvenance");
8
9
  const displayName_1 = require("../utils/displayName");
9
10
  const FederatedPagination_1 = require("./FederatedPagination");
10
11
  class TestsManager {
11
- constructor(testsPersistenceRegistry, flowsManager) {
12
+ constructor(testsPersistenceRegistry, suitesPersistenceRegistry, flowsManager) {
12
13
  this.testsPersistenceRegistry = testsPersistenceRegistry;
14
+ this.suitesPersistenceRegistry = suitesPersistenceRegistry;
13
15
  this.flowsManager = flowsManager;
14
16
  }
15
17
  async createTest(params) {
@@ -38,9 +40,12 @@ class TestsManager {
38
40
  videoDisabled: params.videoDisabled,
39
41
  provenance: (0, buildProvenance_1.buildProvenance)('DONOBU_STUDIO'),
40
42
  };
41
- // Create in the primary persistence layer only.
42
- const primary = await this.testsPersistenceRegistry.get();
43
- await primary.createTest(testMetadata);
43
+ // If the test is part of a suite, write it to the same persistence layer
44
+ // as the suite — otherwise the suite_id foreign key fails (in SQLite)
45
+ // or leaves a dangling reference (in non-DB layers). When standalone,
46
+ // fall back to the primary layer.
47
+ const persistence = await this.resolveLayerForTestCreate(testMetadata.suiteId);
48
+ await persistence.createTest(testMetadata);
44
49
  return testMetadata;
45
50
  }
46
51
  async getTestById(testId) {
@@ -56,6 +61,49 @@ class TestsManager {
56
61
  }
57
62
  throw TestNotFoundException_1.TestNotFoundException.forId(testId);
58
63
  }
64
+ /**
65
+ * Picks the tests persistence layer to use when creating a new test.
66
+ *
67
+ * - If `suiteId` is null/undefined: use the primary tests layer.
68
+ * - If `suiteId` is set: look up the suite's layer key and use the matching
69
+ * tests layer. If no tests layer matches the suite's key (rare — would
70
+ * require asymmetric registry config), fall back to the primary tests
71
+ * layer; the FK won't hold but at least the test is persisted somewhere.
72
+ * - If `suiteId` is set but the suite doesn't exist anywhere: fall back
73
+ * to the primary tests layer (the SQLite FK will reject if applicable;
74
+ * non-DB layers will accept the dangling reference).
75
+ */
76
+ async resolveLayerForTestCreate(suiteId) {
77
+ if (!suiteId) {
78
+ return this.testsPersistenceRegistry.get();
79
+ }
80
+ let suiteLayerKey;
81
+ try {
82
+ suiteLayerKey = await this.findSuiteLayerKey(suiteId);
83
+ }
84
+ catch (error) {
85
+ if (!(error instanceof SuiteNotFoundException_1.SuiteNotFoundException)) {
86
+ throw error;
87
+ }
88
+ return this.testsPersistenceRegistry.get();
89
+ }
90
+ const matched = await this.testsPersistenceRegistry.getByKey(suiteLayerKey);
91
+ return matched ?? (await this.testsPersistenceRegistry.get());
92
+ }
93
+ async findSuiteLayerKey(suiteId) {
94
+ for (const { key, persistence, } of await this.suitesPersistenceRegistry.getEntries()) {
95
+ try {
96
+ await persistence.getSuiteById(suiteId);
97
+ return key;
98
+ }
99
+ catch (error) {
100
+ if (!(error instanceof SuiteNotFoundException_1.SuiteNotFoundException)) {
101
+ throw error;
102
+ }
103
+ }
104
+ }
105
+ throw SuiteNotFoundException_1.SuiteNotFoundException.forId(suiteId);
106
+ }
59
107
  async getTests(query) {
60
108
  const layers = (await this.testsPersistenceRegistry.getAll()).map((persistence) => ({
61
109
  getItems: (q) => persistence.getTests(q),
@@ -65,13 +113,6 @@ class TestsManager {
65
113
  // TestMetadata has no `created_at` field — fall back to "now" so newly
66
114
  // returned items sort to the most-recent end. For other sort columns,
67
115
  // map the snake_case API name to the camelCase JS field.
68
- //
69
- // `flow_count` and `latest_flow_created_at` are only known at the
70
- // persistence layer (SQLite computes them via JOIN; the cloud handles
71
- // them server-side). Returning 0 here means each layer's pre-sorted
72
- // results stay in their own order during the federated merge, but
73
- // ordering across layers degrades to "first seen wins" — fine for the
74
- // common single-primary-layer case.
75
116
  const fieldFor = (test) => {
76
117
  switch (sortBy) {
77
118
  case 'created_at':
@@ -83,8 +124,9 @@ class TestsManager {
83
124
  case 'next_run_mode':
84
125
  return test.nextRunMode;
85
126
  case 'flow_count':
127
+ return test.flowCount ?? 0;
86
128
  case 'latest_flow_created_at':
87
- return 0;
129
+ return test.latestFlow?.startedAt ?? Date.now();
88
130
  default:
89
131
  return '';
90
132
  }
@@ -196,7 +238,9 @@ class TestsManager {
196
238
  }
197
239
  /**
198
240
  * Creates a new flow (config) for the given test, which should be passed to
199
- * `flowsManager.createFlow` to execute the test.
241
+ * `flowsManager.createFlow` to execute the test. The returned config's
242
+ * `testId` is set, which `createFlow` uses to route the flow to the same
243
+ * persistence layer as the test.
200
244
  *
201
245
  * @param testId - The ID of the test to create a new flow for
202
246
  *
@@ -210,7 +254,16 @@ class TestsManager {
210
254
  try {
211
255
  toolCallsOnStart = await this.getTestToolCalls(test);
212
256
  }
213
- catch {
257
+ catch (error) {
258
+ // Fallback to AUTONOMOUS is only viable when the test has an
259
+ // overallObjective for the agent to work toward. For tests without
260
+ // one (e.g. Playwright-imported tests, or AI tests where the user
261
+ // cleared the objective), AUTONOMOUS would just fail downstream
262
+ // with a misleading "overallObjective is required" — propagate the
263
+ // original error instead so the user sees the real cause.
264
+ if ((test.overallObjective?.trim().length ?? 0) === 0) {
265
+ throw error;
266
+ }
214
267
  runMode = 'AUTONOMOUS';
215
268
  }
216
269
  }
@@ -125,7 +125,16 @@ class FlowsPersistenceDonobuApi {
125
125
  params.set('orphaned', query.orphaned ? 'true' : 'false');
126
126
  }
127
127
  if (query.sortBy) {
128
- params.set('sort_by', query.sortBy);
128
+ switch (query.sortBy) {
129
+ case 'created_at':
130
+ // `donobu-api` only supports `started_at`, but that's essentially
131
+ // equivalent to `created_at`
132
+ params.set('sort_by', 'started_at');
133
+ break;
134
+ default:
135
+ params.set('sort_by', query.sortBy);
136
+ break;
137
+ }
129
138
  }
130
139
  if (query.sortOrder) {
131
140
  params.set('sort_order', query.sortOrder);
@@ -2,6 +2,17 @@ import type { EnvPick } from 'env-struct';
2
2
  import type { env } from '../../envVars';
3
3
  import { PersistencePluginRegistry } from '../PersistencePlugin';
4
4
  import type { FlowsPersistence } from './FlowsPersistence';
5
+ /**
6
+ * A persistence layer paired with the `PERSISTENCE_PRIORITY` key it was
7
+ * created from. The key is shared across the flows/tests/suites registries:
8
+ * the same key in each registry refers to the same logical store, which is
9
+ * how cross-entity foreign keys are kept aligned (e.g. a flow's `testId`
10
+ * resolves to a test that lives in the same layer).
11
+ */
12
+ export interface FlowsPersistenceLayer {
13
+ key: string;
14
+ persistence: FlowsPersistence;
15
+ }
5
16
  export interface FlowsPersistenceRegistry {
6
17
  /**
7
18
  * Returns the primary persistence layer.
@@ -9,6 +20,17 @@ export interface FlowsPersistenceRegistry {
9
20
  get(): Promise<FlowsPersistence>;
10
21
  /** Returns a list of all valid persistence layers. Guaranteed to be non-empty. */
11
22
  getAll(): Promise<FlowsPersistence[]>;
23
+ /**
24
+ * Returns all valid layers paired with their `PERSISTENCE_PRIORITY` key.
25
+ * Used by callers that need to align writes across registries (e.g. write
26
+ * a flow into the same layer as the test it references).
27
+ */
28
+ getEntries(): Promise<FlowsPersistenceLayer[]>;
29
+ /**
30
+ * Returns the persistence layer registered under the given
31
+ * `PERSISTENCE_PRIORITY` key, or undefined if no such layer exists.
32
+ */
33
+ getByKey(key: string): Promise<FlowsPersistence | undefined>;
12
34
  }
13
35
  /**
14
36
  * A factory class for creating FlowsPersistence instances. Persistence layers are constructed
@@ -19,7 +41,7 @@ export declare class FlowsPersistenceRegistryImpl implements FlowsPersistenceReg
19
41
  /**
20
42
  * Creates an instance with pre-built persistence layers.
21
43
  */
22
- constructor(layers: FlowsPersistence[]);
44
+ constructor(layers: FlowsPersistenceLayer[]);
23
45
  /**
24
46
  * Creates an instance by reading environment variables and eagerly constructing
25
47
  * all applicable persistence layers.
@@ -31,5 +53,7 @@ export declare class FlowsPersistenceRegistryImpl implements FlowsPersistenceReg
31
53
  get(): Promise<FlowsPersistence>;
32
54
  /** Returns all persistence layers. Guaranteed to be non-empty. */
33
55
  getAll(): Promise<FlowsPersistence[]>;
56
+ getEntries(): Promise<FlowsPersistenceLayer[]>;
57
+ getByKey(key: string): Promise<FlowsPersistence | undefined>;
34
58
  }
35
59
  //# sourceMappingURL=FlowsPersistenceRegistry.d.ts.map
@@ -32,21 +32,27 @@ class FlowsPersistenceRegistryImpl {
32
32
  switch (key) {
33
33
  case 'DONOBU':
34
34
  if (donobuApiKey && donobuApiBaseUrl) {
35
- layers.push(new FlowsPersistenceDonobuApi_1.FlowsPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey));
35
+ layers.push({
36
+ key,
37
+ persistence: new FlowsPersistenceDonobuApi_1.FlowsPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey),
38
+ });
36
39
  }
37
40
  break;
38
41
  case 'LOCAL':
39
- layers.push(await FlowsPersistenceSqlite_1.FlowsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
42
+ layers.push({
43
+ key,
44
+ persistence: await FlowsPersistenceSqlite_1.FlowsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()),
45
+ });
40
46
  break;
41
47
  case 'RAM':
42
- layers.push(new FlowsPersistenceVolatile_1.FlowsPersistenceVolatile());
48
+ layers.push({ key, persistence: new FlowsPersistenceVolatile_1.FlowsPersistenceVolatile() });
43
49
  break;
44
50
  default: {
45
51
  const plugin = persistencePlugins.get(key);
46
52
  if (plugin) {
47
53
  const impl = await plugin.createFlowsPersistence();
48
54
  if (impl) {
49
- layers.push(impl);
55
+ layers.push({ key, persistence: impl });
50
56
  }
51
57
  }
52
58
  break;
@@ -59,12 +65,18 @@ class FlowsPersistenceRegistryImpl {
59
65
  * Returns the primary persistence layer.
60
66
  */
61
67
  async get() {
62
- return this.layers[0];
68
+ return this.layers[0].persistence;
63
69
  }
64
70
  /** Returns all persistence layers. Guaranteed to be non-empty. */
65
71
  async getAll() {
72
+ return this.layers.map((layer) => layer.persistence);
73
+ }
74
+ async getEntries() {
66
75
  return this.layers;
67
76
  }
77
+ async getByKey(key) {
78
+ return this.layers.find((layer) => layer.key === key)?.persistence;
79
+ }
68
80
  }
69
81
  exports.FlowsPersistenceRegistryImpl = FlowsPersistenceRegistryImpl;
70
82
  //# sourceMappingURL=FlowsPersistenceRegistry.js.map
@@ -2,17 +2,34 @@ import type { EnvPick } from 'env-struct';
2
2
  import type { env } from '../../envVars';
3
3
  import { PersistencePluginRegistry } from '../PersistencePlugin';
4
4
  import type { SuitesPersistence } from './SuitesPersistence';
5
+ /**
6
+ * A persistence layer paired with its `PERSISTENCE_PRIORITY` key. See
7
+ * {@link FlowsPersistenceLayer} for why keys must align across registries.
8
+ */
9
+ export interface SuitesPersistenceLayer {
10
+ key: string;
11
+ persistence: SuitesPersistence;
12
+ }
5
13
  export interface SuitesPersistenceRegistry {
6
14
  /** Returns the primary persistence layer. */
7
15
  get(): Promise<SuitesPersistence>;
8
16
  /** Returns all valid persistence layers. Guaranteed to be non-empty. */
9
17
  getAll(): Promise<SuitesPersistence[]>;
18
+ /** Returns all valid layers paired with their `PERSISTENCE_PRIORITY` key. */
19
+ getEntries(): Promise<SuitesPersistenceLayer[]>;
20
+ /**
21
+ * Returns the persistence layer registered under the given
22
+ * `PERSISTENCE_PRIORITY` key, or undefined if no such layer exists.
23
+ */
24
+ getByKey(key: string): Promise<SuitesPersistence | undefined>;
10
25
  }
11
26
  export declare class SuitesPersistenceRegistryImpl implements SuitesPersistenceRegistry {
12
27
  private readonly layers;
13
- constructor(layers: SuitesPersistence[]);
28
+ constructor(layers: SuitesPersistenceLayer[]);
14
29
  static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>, persistencePlugins?: PersistencePluginRegistry): Promise<SuitesPersistenceRegistryImpl>;
15
30
  get(): Promise<SuitesPersistence>;
16
31
  getAll(): Promise<SuitesPersistence[]>;
32
+ getEntries(): Promise<SuitesPersistenceLayer[]>;
33
+ getByKey(key: string): Promise<SuitesPersistence | undefined>;
17
34
  }
18
35
  //# sourceMappingURL=SuitesPersistenceRegistry.d.ts.map
@@ -21,21 +21,27 @@ class SuitesPersistenceRegistryImpl {
21
21
  switch (key) {
22
22
  case 'DONOBU':
23
23
  if (donobuApiKey && donobuApiBaseUrl) {
24
- layers.push(new SuitesPersistenceDonobuApi_1.SuitesPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey));
24
+ layers.push({
25
+ key,
26
+ persistence: new SuitesPersistenceDonobuApi_1.SuitesPersistenceDonobuApi(donobuApiBaseUrl, donobuApiKey),
27
+ });
25
28
  }
26
29
  break;
27
30
  case 'LOCAL':
28
- layers.push(await SuitesPersistenceSqlite_1.SuitesPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
31
+ layers.push({
32
+ key,
33
+ persistence: await SuitesPersistenceSqlite_1.SuitesPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()),
34
+ });
29
35
  break;
30
36
  case 'RAM':
31
- layers.push(new SuitesPersistenceVolatile_1.SuitesPersistenceVolatile());
37
+ layers.push({ key, persistence: new SuitesPersistenceVolatile_1.SuitesPersistenceVolatile() });
32
38
  break;
33
39
  default: {
34
40
  const plugin = persistencePlugins.get(key);
35
41
  if (plugin?.createSuitesPersistence) {
36
42
  const impl = await plugin.createSuitesPersistence();
37
43
  if (impl) {
38
- layers.push(impl);
44
+ layers.push({ key, persistence: impl });
39
45
  }
40
46
  }
41
47
  break;
@@ -45,11 +51,17 @@ class SuitesPersistenceRegistryImpl {
45
51
  return new SuitesPersistenceRegistryImpl(layers);
46
52
  }
47
53
  async get() {
48
- return this.layers[0];
54
+ return this.layers[0].persistence;
49
55
  }
50
56
  async getAll() {
57
+ return this.layers.map((layer) => layer.persistence);
58
+ }
59
+ async getEntries() {
51
60
  return this.layers;
52
61
  }
62
+ async getByKey(key) {
63
+ return this.layers.find((layer) => layer.key === key)?.persistence;
64
+ }
53
65
  }
54
66
  exports.SuitesPersistenceRegistryImpl = SuitesPersistenceRegistryImpl;
55
67
  //# sourceMappingURL=SuitesPersistenceRegistry.js.map
@@ -3,15 +3,30 @@ import type { env } from '../../envVars';
3
3
  import type { FlowsPersistenceRegistry } from '../flows/FlowsPersistenceRegistry';
4
4
  import { PersistencePluginRegistry } from '../PersistencePlugin';
5
5
  import type { TestsPersistence } from './TestsPersistence';
6
+ /**
7
+ * A persistence layer paired with its `PERSISTENCE_PRIORITY` key. See
8
+ * {@link FlowsPersistenceLayer} for why keys must align across registries.
9
+ */
10
+ export interface TestsPersistenceLayer {
11
+ key: string;
12
+ persistence: TestsPersistence;
13
+ }
6
14
  export interface TestsPersistenceRegistry {
7
15
  /** Returns the primary persistence layer. */
8
16
  get(): Promise<TestsPersistence>;
9
17
  /** Returns all valid persistence layers. Guaranteed to be non-empty. */
10
18
  getAll(): Promise<TestsPersistence[]>;
19
+ /** Returns all valid layers paired with their `PERSISTENCE_PRIORITY` key. */
20
+ getEntries(): Promise<TestsPersistenceLayer[]>;
21
+ /**
22
+ * Returns the persistence layer registered under the given
23
+ * `PERSISTENCE_PRIORITY` key, or undefined if no such layer exists.
24
+ */
25
+ getByKey(key: string): Promise<TestsPersistence | undefined>;
11
26
  }
12
27
  export declare class TestsPersistenceRegistryImpl implements TestsPersistenceRegistry {
13
28
  private readonly layers;
14
- constructor(layers: TestsPersistence[]);
29
+ constructor(layers: TestsPersistenceLayer[]);
15
30
  static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>,
16
31
  /**
17
32
  * Flows registry — used by the volatile tests layer to compute
@@ -22,5 +37,7 @@ export declare class TestsPersistenceRegistryImpl implements TestsPersistenceReg
22
37
  flowsRegistry?: FlowsPersistenceRegistry, persistencePlugins?: PersistencePluginRegistry): Promise<TestsPersistenceRegistryImpl>;
23
38
  get(): Promise<TestsPersistence>;
24
39
  getAll(): Promise<TestsPersistence[]>;
40
+ getEntries(): Promise<TestsPersistenceLayer[]>;
41
+ getByKey(key: string): Promise<TestsPersistence | undefined>;
25
42
  }
26
43
  //# sourceMappingURL=TestsPersistenceRegistry.d.ts.map