@checkstack/automation-backend 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +453 -0
  2. package/drizzle/0000_acoustic_diamondback.sql +80 -0
  3. package/drizzle/0001_mute_vindicator.sql +12 -0
  4. package/drizzle/0002_silky_omega_red.sql +12 -0
  5. package/drizzle/meta/0000_snapshot.json +688 -0
  6. package/drizzle/meta/0001_snapshot.json +785 -0
  7. package/drizzle/meta/0002_snapshot.json +861 -0
  8. package/drizzle/meta/_journal.json +27 -0
  9. package/drizzle.config.ts +12 -0
  10. package/package.json +41 -0
  11. package/src/action-registry.ts +83 -0
  12. package/src/action-types.ts +324 -0
  13. package/src/artifact-store.ts +140 -0
  14. package/src/artifact-type-registry.ts +64 -0
  15. package/src/automation-store.ts +227 -0
  16. package/src/builtin-actions.test.ts +185 -0
  17. package/src/builtin-actions.ts +132 -0
  18. package/src/builtin-triggers.test.ts +264 -0
  19. package/src/builtin-triggers.ts +365 -0
  20. package/src/dispatch/action-kind.ts +44 -0
  21. package/src/dispatch/condition.ts +61 -0
  22. package/src/dispatch/delay-queue.ts +91 -0
  23. package/src/dispatch/engine.test.ts +1198 -0
  24. package/src/dispatch/engine.ts +1672 -0
  25. package/src/dispatch/path-nav.ts +65 -0
  26. package/src/dispatch/render.test.ts +75 -0
  27. package/src/dispatch/render.ts +136 -0
  28. package/src/dispatch/run-state-store.ts +143 -0
  29. package/src/dispatch/run-state.ts +298 -0
  30. package/src/dispatch/scope.test.ts +40 -0
  31. package/src/dispatch/scope.ts +125 -0
  32. package/src/dispatch/stalled-sweeper.ts +164 -0
  33. package/src/dispatch/test-fixtures.ts +558 -0
  34. package/src/dispatch/trigger-subscriber.ts +397 -0
  35. package/src/dispatch/types.ts +259 -0
  36. package/src/extension-points.ts +88 -0
  37. package/src/index.ts +379 -0
  38. package/src/migration/from-webhook-subscriptions.test.ts +237 -0
  39. package/src/migration/from-webhook-subscriptions.ts +398 -0
  40. package/src/registries.test.ts +357 -0
  41. package/src/router.test.ts +724 -0
  42. package/src/router.ts +556 -0
  43. package/src/schema.ts +310 -0
  44. package/src/trigger-registry.ts +99 -0
  45. package/src/validate-definition.test.ts +306 -0
  46. package/src/validate-definition.ts +304 -0
  47. package/tsconfig.json +41 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Tests for the deep automation-definition validator.
3
+ *
4
+ * Builds a tiny trigger + action registry, then asserts that
5
+ * `collectDefinitionIssues` surfaces structural errors, unknown
6
+ * trigger/action ids, and — the gap this module closes — invalid
7
+ * provider-action config values + unknown config keys.
8
+ */
9
+ import { describe, expect, it } from "bun:test";
10
+ import { z } from "zod";
11
+ import { Versioned } from "@checkstack/backend-api";
12
+ import { definePluginMetadata } from "@checkstack/common";
13
+ import type { AutomationDefinition } from "@checkstack/automation-common";
14
+ import { createActionRegistry } from "./action-registry";
15
+ import { createTriggerRegistry } from "./trigger-registry";
16
+ import { collectDefinitionIssues } from "./validate-definition";
17
+
18
+ const meta = definePluginMetadata({ pluginId: "test" });
19
+
20
+ function makeDeps() {
21
+ const triggerRegistry = createTriggerRegistry();
22
+ const actionRegistry = createActionRegistry();
23
+
24
+ triggerRegistry.register(
25
+ {
26
+ id: "fired",
27
+ displayName: "Fired",
28
+ payloadSchema: z.object({ id: z.string() }),
29
+ configSchema: z.object({ intervalSeconds: z.number().int().min(1) }),
30
+ // A trigger must be reachable via a hook or setup; this validator
31
+ // only cares about the config schema, so a no-op setup suffices.
32
+ setup: async () => async () => {},
33
+ },
34
+ meta,
35
+ );
36
+
37
+ actionRegistry.register(
38
+ {
39
+ id: "log",
40
+ displayName: "Log",
41
+ config: new Versioned({
42
+ version: 1,
43
+ schema: z.object({
44
+ message: z.string().min(1),
45
+ level: z.enum(["debug", "info", "warn", "error"]).default("info"),
46
+ }),
47
+ }),
48
+ execute: async () => ({ success: true }),
49
+ },
50
+ meta,
51
+ );
52
+
53
+ return { triggerRegistry, actionRegistry };
54
+ }
55
+
56
+ /**
57
+ * Deps whose action registry includes a producing action (`test.create`,
58
+ * `produces: "thing"`) so the artifact-id invariants can be exercised.
59
+ */
60
+ function makeProducerDeps() {
61
+ const { triggerRegistry, actionRegistry } = makeDeps();
62
+ actionRegistry.register(
63
+ {
64
+ id: "create",
65
+ displayName: "Create Thing",
66
+ config: new Versioned({ version: 1, schema: z.object({}) }),
67
+ produces: "thing",
68
+ execute: async () => ({ success: true, artifact: { ok: true } }),
69
+ },
70
+ meta,
71
+ );
72
+ return { triggerRegistry, actionRegistry };
73
+ }
74
+
75
+ function baseDefinition(
76
+ overrides: Partial<AutomationDefinition> = {},
77
+ ): AutomationDefinition {
78
+ return {
79
+ name: "A",
80
+ triggers: [{ event: "test.fired", config: { intervalSeconds: 5 } }],
81
+ conditions: [],
82
+ actions: [
83
+ {
84
+ action: "test.log",
85
+ config: { message: "hi", level: "info" },
86
+ enabled: true,
87
+ continue_on_error: false,
88
+ },
89
+ ],
90
+ mode: "single",
91
+ max_runs: 1,
92
+ ...overrides,
93
+ };
94
+ }
95
+
96
+ describe("collectDefinitionIssues", () => {
97
+ it("returns no issues for a fully valid definition", () => {
98
+ const issues = collectDefinitionIssues(baseDefinition(), makeDeps());
99
+ expect(issues).toEqual([]);
100
+ });
101
+
102
+ it("flags an invalid enum value in a provider action config", () => {
103
+ const def = baseDefinition({
104
+ actions: [
105
+ {
106
+ action: "test.log",
107
+ config: { message: "hi", level: "debugthisiswrong" },
108
+ enabled: true,
109
+ continue_on_error: false,
110
+ },
111
+ ],
112
+ });
113
+ const issues = collectDefinitionIssues(def, makeDeps());
114
+ expect(issues.length).toBeGreaterThan(0);
115
+ const levelIssue = issues.find((i) => i.path.join(".") === "actions.0.config.level");
116
+ expect(levelIssue).toBeDefined();
117
+ });
118
+
119
+ it("flags a missing required config field", () => {
120
+ const def = baseDefinition({
121
+ actions: [
122
+ {
123
+ action: "test.log",
124
+ config: { level: "info" },
125
+ enabled: true,
126
+ continue_on_error: false,
127
+ },
128
+ ],
129
+ });
130
+ const issues = collectDefinitionIssues(def, makeDeps());
131
+ expect(
132
+ issues.some((i) => i.path.join(".") === "actions.0.config.message"),
133
+ ).toBe(true);
134
+ });
135
+
136
+ it("flags an unknown config key (strict)", () => {
137
+ const def = baseDefinition({
138
+ actions: [
139
+ {
140
+ action: "test.log",
141
+ config: { message: "hi", level: "info", levle: "typo" },
142
+ enabled: true,
143
+ continue_on_error: false,
144
+ },
145
+ ],
146
+ });
147
+ const issues = collectDefinitionIssues(def, makeDeps());
148
+ expect(issues.length).toBeGreaterThan(0);
149
+ // The unknown key is reported under the action's config path.
150
+ expect(issues.some((i) => i.path[0] === "actions" && i.path.includes("config"))).toBe(true);
151
+ });
152
+
153
+ it("flags an unknown action id", () => {
154
+ const def = baseDefinition({
155
+ actions: [
156
+ {
157
+ action: "test.does_not_exist",
158
+ config: {},
159
+ enabled: true,
160
+ continue_on_error: false,
161
+ },
162
+ ],
163
+ });
164
+ const issues = collectDefinitionIssues(def, makeDeps());
165
+ const issue = issues.find((i) => i.path.join(".") === "actions.0.action");
166
+ expect(issue?.message).toMatch(/Unknown action/);
167
+ });
168
+
169
+ it("flags an unknown trigger event", () => {
170
+ const def = baseDefinition({
171
+ triggers: [{ event: "nope.gone" }],
172
+ });
173
+ const issues = collectDefinitionIssues(def, makeDeps());
174
+ const issue = issues.find((i) => i.path.join(".") === "triggers.0.event");
175
+ expect(issue?.message).toMatch(/Unknown trigger/);
176
+ });
177
+
178
+ it("flags an invalid trigger config value", () => {
179
+ const def = baseDefinition({
180
+ triggers: [{ event: "test.fired", config: { intervalSeconds: 0 } }],
181
+ });
182
+ const issues = collectDefinitionIssues(def, makeDeps());
183
+ expect(
184
+ issues.some(
185
+ (i) => i.path.join(".") === "triggers.0.config.intervalSeconds",
186
+ ),
187
+ ).toBe(true);
188
+ });
189
+
190
+ it("validates configs nested inside a choose branch", () => {
191
+ const def = baseDefinition({
192
+ actions: [
193
+ {
194
+ choose: [
195
+ {
196
+ when: "true",
197
+ sequence: [
198
+ {
199
+ action: "test.log",
200
+ config: { message: "hi", level: "bogus" },
201
+ enabled: true,
202
+ continue_on_error: false,
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ enabled: true,
208
+ continue_on_error: false,
209
+ },
210
+ ],
211
+ });
212
+ const issues = collectDefinitionIssues(def, makeDeps());
213
+ expect(
214
+ issues.some(
215
+ (i) =>
216
+ i.path.join(".") === "actions.0.choose.0.sequence.0.config.level",
217
+ ),
218
+ ).toBe(true);
219
+ });
220
+
221
+ it("returns structural issues for a malformed top-level shape", () => {
222
+ const issues = collectDefinitionIssues(
223
+ { name: "", triggers: [] },
224
+ makeDeps(),
225
+ );
226
+ expect(issues.length).toBeGreaterThan(0);
227
+ });
228
+
229
+ it("rejects a duplicate action id", () => {
230
+ const def = baseDefinition({
231
+ actions: [
232
+ {
233
+ id: "dup",
234
+ action: "test.log",
235
+ config: { message: "a", level: "info" },
236
+ enabled: true,
237
+ continue_on_error: false,
238
+ },
239
+ {
240
+ id: "dup",
241
+ action: "test.log",
242
+ config: { message: "b", level: "info" },
243
+ enabled: true,
244
+ continue_on_error: false,
245
+ },
246
+ ],
247
+ });
248
+ const issues = collectDefinitionIssues(def, makeDeps());
249
+ const dupIssue = issues.find(
250
+ (i) => i.path.join(".") === "actions.1.id",
251
+ );
252
+ expect(dupIssue?.message).toMatch(/must be unique/);
253
+ });
254
+
255
+ it("rejects a producing action that has no id", () => {
256
+ const deps = makeProducerDeps();
257
+ const def = baseDefinition({
258
+ actions: [
259
+ {
260
+ action: "test.create",
261
+ config: {},
262
+ enabled: true,
263
+ continue_on_error: false,
264
+ },
265
+ ],
266
+ });
267
+ const issues = collectDefinitionIssues(def, deps);
268
+ const idIssue = issues.find((i) => i.path.join(".") === "actions.0.id");
269
+ expect(idIssue?.message).toMatch(/must have an id/);
270
+ });
271
+
272
+ it("accepts a producing action that has an id", () => {
273
+ const deps = makeProducerDeps();
274
+ const def = baseDefinition({
275
+ actions: [
276
+ {
277
+ id: "make_thing",
278
+ action: "test.create",
279
+ config: {},
280
+ enabled: true,
281
+ continue_on_error: false,
282
+ },
283
+ ],
284
+ });
285
+ const issues = collectDefinitionIssues(def, deps);
286
+ expect(issues).toEqual([]);
287
+ });
288
+
289
+ it("rejects a hyphenated action id via the structural pass", () => {
290
+ const def = baseDefinition({
291
+ actions: [
292
+ {
293
+ id: "bad-id",
294
+ action: "test.log",
295
+ config: { message: "hi", level: "info" },
296
+ enabled: true,
297
+ continue_on_error: false,
298
+ },
299
+ ],
300
+ });
301
+ const issues = collectDefinitionIssues(def, makeDeps());
302
+ expect(
303
+ issues.some((i) => i.path.includes("id")),
304
+ ).toBe(true);
305
+ });
306
+ });
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Deep validation of an automation definition.
3
+ *
4
+ * `AutomationDefinitionSchema` only validates the structural shape —
5
+ * action `config` is typed as `z.record(z.unknown())`, so it never
6
+ * checks a provider action's config against that action's own schema.
7
+ * This walker fills the gap so the editor can surface *any* wrong
8
+ * content, not just structural errors:
9
+ *
10
+ * - unknown trigger `event` / action `action` ids,
11
+ * - per-trigger `config` that violates the trigger's `configSchema`,
12
+ * - per-action `config` that violates the action's config schema
13
+ * (wrong enum value, missing required field, wrong type, AND —
14
+ * because we validate in strict mode — unknown/typo'd keys).
15
+ *
16
+ * Returned issue `path`s are dot-joinable for display, e.g.
17
+ * `actions.0.config.level` or `triggers.1.event`.
18
+ */
19
+ import { z } from "zod";
20
+ import {
21
+ AutomationDefinitionSchema,
22
+ type ActionInput,
23
+ type AutomationDefinition,
24
+ } from "@checkstack/automation-common";
25
+ import type { ActionRegistry } from "./action-registry";
26
+ import type { TriggerRegistry } from "./trigger-registry";
27
+
28
+ export interface DefinitionIssue {
29
+ path: Array<string | number>;
30
+ message: string;
31
+ }
32
+
33
+ export interface ValidateDefinitionDeps {
34
+ triggerRegistry: TriggerRegistry;
35
+ actionRegistry: ActionRegistry;
36
+ }
37
+
38
+ /**
39
+ * Validate a definition both structurally and semantically. Returns an
40
+ * empty array when the definition is fully valid.
41
+ *
42
+ * Structural errors short-circuit the semantic pass: if the top-level
43
+ * shape is wrong we can't reliably walk the action tree, so we return
44
+ * the structural issues alone and let the operator fix those first.
45
+ */
46
+ export function collectDefinitionIssues(
47
+ definition: unknown,
48
+ deps: ValidateDefinitionDeps,
49
+ ): DefinitionIssue[] {
50
+ const parsed = AutomationDefinitionSchema.safeParse(definition);
51
+ if (!parsed.success) {
52
+ return parsed.error.issues.map((issue) => ({
53
+ path: issue.path.map((segment) => toPathSegment(segment)),
54
+ message: issue.message,
55
+ }));
56
+ }
57
+
58
+ const issues: DefinitionIssue[] = [];
59
+ validateTriggers(parsed.data, deps, issues);
60
+ validateActionList(parsed.data.actions, ["actions"], deps, issues);
61
+ validateActionIds(parsed.data.actions, ["actions"], deps, issues);
62
+ return issues;
63
+ }
64
+
65
+ // ─── Semantic action-id validation ──────────────────────────────────────
66
+
67
+ /**
68
+ * Walk the entire action tree and enforce the two artifact-reference
69
+ * invariants the structural zod pass can't:
70
+ *
71
+ * 1. Every action `id` is unique within the automation (so
72
+ * `artifacts.<id>.<name>` is unambiguous).
73
+ * 2. Any provider action whose registered action declares a truthy
74
+ * `produces` MUST carry an `id` (so the produced artifact is
75
+ * referenceable).
76
+ *
77
+ * Identifier-format is already enforced by the zod schema in the
78
+ * structural pass, so we don't re-check it here.
79
+ */
80
+ function validateActionIds(
81
+ actions: ActionInput[],
82
+ basePath: Array<string | number>,
83
+ deps: ValidateDefinitionDeps,
84
+ issues: DefinitionIssue[],
85
+ ): void {
86
+ const seen = new Set<string>();
87
+ walkActionIds(actions, basePath, deps, seen, issues);
88
+ }
89
+
90
+ function walkActionIds(
91
+ actions: ActionInput[],
92
+ basePath: Array<string | number>,
93
+ deps: ValidateDefinitionDeps,
94
+ seen: Set<string>,
95
+ issues: DefinitionIssue[],
96
+ ): void {
97
+ for (const [index, action] of actions.entries()) {
98
+ walkActionId(action, [...basePath, index], deps, seen, issues);
99
+ }
100
+ }
101
+
102
+ function walkActionId(
103
+ action: ActionInput,
104
+ path: Array<string | number>,
105
+ deps: ValidateDefinitionDeps,
106
+ seen: Set<string>,
107
+ issues: DefinitionIssue[],
108
+ ): void {
109
+ if (typeof action.id === "string") {
110
+ if (seen.has(action.id)) {
111
+ issues.push({
112
+ path: [...path, "id"],
113
+ message: `Action id "${action.id}" must be unique within the automation`,
114
+ });
115
+ } else {
116
+ seen.add(action.id);
117
+ }
118
+ }
119
+
120
+ if ("action" in action) {
121
+ const registered = deps.actionRegistry.getAction(action.action);
122
+ if (registered?.produces && !action.id) {
123
+ issues.push({
124
+ path: [...path, "id"],
125
+ message:
126
+ "Actions that produce an artifact must have an id so the artifact can be referenced as artifacts.<id>.<name>",
127
+ });
128
+ }
129
+ return;
130
+ }
131
+
132
+ if ("choose" in action) {
133
+ for (const [branchIndex, branch] of action.choose.entries()) {
134
+ walkActionIds(
135
+ branch.sequence,
136
+ [...path, "choose", branchIndex, "sequence"],
137
+ deps,
138
+ seen,
139
+ issues,
140
+ );
141
+ }
142
+ if (action.else) {
143
+ walkActionIds(action.else, [...path, "else"], deps, seen, issues);
144
+ }
145
+ return;
146
+ }
147
+
148
+ if ("parallel" in action) {
149
+ walkActionIds(action.parallel, [...path, "parallel"], deps, seen, issues);
150
+ return;
151
+ }
152
+
153
+ if ("repeat" in action) {
154
+ walkActionIds(
155
+ action.repeat.sequence,
156
+ [...path, "repeat", "sequence"],
157
+ deps,
158
+ seen,
159
+ issues,
160
+ );
161
+ return;
162
+ }
163
+
164
+ if ("sequence" in action) {
165
+ walkActionIds(action.sequence, [...path, "sequence"], deps, seen, issues);
166
+ return;
167
+ }
168
+
169
+ // delay / variables / condition / stop / wait_for_trigger have no child
170
+ // action lists and don't produce artifacts — nothing more to walk.
171
+ }
172
+
173
+ function validateTriggers(
174
+ definition: AutomationDefinition,
175
+ deps: ValidateDefinitionDeps,
176
+ issues: DefinitionIssue[],
177
+ ): void {
178
+ for (const [index, trigger] of definition.triggers.entries()) {
179
+ const registered = deps.triggerRegistry.getTrigger(trigger.event);
180
+ if (!registered) {
181
+ issues.push({
182
+ path: ["triggers", index, "event"],
183
+ message: `Unknown trigger event "${trigger.event}"`,
184
+ });
185
+ continue;
186
+ }
187
+ if (registered.configSchema) {
188
+ const result = strictParse(registered.configSchema, trigger.config ?? {});
189
+ if (!result.success) {
190
+ pushZodIssues(result.error, ["triggers", index, "config"], issues);
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ function validateActionList(
197
+ actions: ActionInput[],
198
+ basePath: Array<string | number>,
199
+ deps: ValidateDefinitionDeps,
200
+ issues: DefinitionIssue[],
201
+ ): void {
202
+ for (const [index, action] of actions.entries()) {
203
+ validateAction(action, [...basePath, index], deps, issues);
204
+ }
205
+ }
206
+
207
+ function validateAction(
208
+ action: ActionInput,
209
+ path: Array<string | number>,
210
+ deps: ValidateDefinitionDeps,
211
+ issues: DefinitionIssue[],
212
+ ): void {
213
+ if ("action" in action) {
214
+ const registered = deps.actionRegistry.getAction(action.action);
215
+ if (!registered) {
216
+ issues.push({
217
+ path: [...path, "action"],
218
+ message: `Unknown action "${action.action}"`,
219
+ });
220
+ return;
221
+ }
222
+ const result = strictParse(registered.config.schema, action.config);
223
+ if (!result.success) {
224
+ pushZodIssues(result.error, [...path, "config"], issues);
225
+ }
226
+ return;
227
+ }
228
+
229
+ if ("choose" in action) {
230
+ for (const [branchIndex, branch] of action.choose.entries()) {
231
+ validateActionList(
232
+ branch.sequence,
233
+ [...path, "choose", branchIndex, "sequence"],
234
+ deps,
235
+ issues,
236
+ );
237
+ }
238
+ if (action.else) {
239
+ validateActionList(action.else, [...path, "else"], deps, issues);
240
+ }
241
+ return;
242
+ }
243
+
244
+ if ("parallel" in action) {
245
+ validateActionList(action.parallel, [...path, "parallel"], deps, issues);
246
+ return;
247
+ }
248
+
249
+ if ("repeat" in action) {
250
+ validateActionList(
251
+ action.repeat.sequence,
252
+ [...path, "repeat", "sequence"],
253
+ deps,
254
+ issues,
255
+ );
256
+ return;
257
+ }
258
+
259
+ if ("sequence" in action) {
260
+ validateActionList(action.sequence, [...path, "sequence"], deps, issues);
261
+ return;
262
+ }
263
+
264
+ // delay / variables / condition / stop / wait_for_trigger carry no
265
+ // provider config to deep-validate — their structure is already fully
266
+ // covered by AutomationDefinitionSchema.
267
+ }
268
+
269
+ /**
270
+ * Parse against a schema in strict mode when it's a plain object schema,
271
+ * so unknown / typo'd config keys are reported rather than silently
272
+ * stripped. Non-object schemas (unions, records, primitives) fall back
273
+ * to a normal parse.
274
+ */
275
+ function strictParse(schema: z.ZodType<unknown>, value: unknown) {
276
+ if (schema instanceof z.ZodObject) {
277
+ return schema.strict().safeParse(value);
278
+ }
279
+ return schema.safeParse(value);
280
+ }
281
+
282
+ function pushZodIssues(
283
+ error: z.ZodError,
284
+ basePath: Array<string | number>,
285
+ issues: DefinitionIssue[],
286
+ ): void {
287
+ for (const issue of error.issues) {
288
+ issues.push({
289
+ path: [...basePath, ...issue.path.map((segment) => toPathSegment(segment))],
290
+ message: issue.message,
291
+ });
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Zod issue paths are `PropertyKey[]` (string | number | symbol). The
297
+ * automation contract's issue path is `(string | number)[]`, so coerce
298
+ * the rare symbol segment to its string form.
299
+ */
300
+ function toPathSegment(segment: PropertyKey): string | number {
301
+ return typeof segment === "number" || typeof segment === "string"
302
+ ? segment
303
+ : String(segment);
304
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ],
6
+ "references": [
7
+ {
8
+ "path": "../automation-common"
9
+ },
10
+ {
11
+ "path": "../backend-api"
12
+ },
13
+ {
14
+ "path": "../command-backend"
15
+ },
16
+ {
17
+ "path": "../common"
18
+ },
19
+ {
20
+ "path": "../drizzle-helper"
21
+ },
22
+ {
23
+ "path": "../integration-common"
24
+ },
25
+ {
26
+ "path": "../notification-common"
27
+ },
28
+ {
29
+ "path": "../queue-api"
30
+ },
31
+ {
32
+ "path": "../signal-common"
33
+ },
34
+ {
35
+ "path": "../template-engine"
36
+ },
37
+ {
38
+ "path": "../test-utils-backend"
39
+ }
40
+ ]
41
+ }