@solidxai/core 0.1.2 → 0.1.4

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 (281) hide show
  1. package/dist/commands/run-tests.command.d.ts +37 -0
  2. package/dist/commands/run-tests.command.d.ts.map +1 -0
  3. package/dist/commands/run-tests.command.js +345 -0
  4. package/dist/commands/run-tests.command.js.map +1 -0
  5. package/dist/commands/test-data.command.d.ts +6 -6
  6. package/dist/commands/test-data.command.d.ts.map +1 -1
  7. package/dist/commands/test-data.command.js +25 -25
  8. package/dist/commands/test-data.command.js.map +1 -1
  9. package/dist/commands/test.command.d.ts +5 -0
  10. package/dist/commands/test.command.d.ts.map +1 -0
  11. package/dist/commands/test.command.js +26 -0
  12. package/dist/commands/test.command.js.map +1 -0
  13. package/dist/controllers/service.controller.d.ts +0 -9
  14. package/dist/controllers/service.controller.d.ts.map +1 -1
  15. package/dist/controllers/service.controller.js +0 -45
  16. package/dist/controllers/service.controller.js.map +1 -1
  17. package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
  18. package/dist/dtos/basic-filters.dto.js.map +1 -1
  19. package/dist/dtos/create-user.dto.d.ts +1 -0
  20. package/dist/dtos/create-user.dto.d.ts.map +1 -1
  21. package/dist/dtos/create-user.dto.js +2 -1
  22. package/dist/dtos/create-user.dto.js.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  28. package/dist/seeders/module-metadata-seeder.service.js +1 -20
  29. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  30. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  31. package/dist/seeders/module-test-data.service.js +3 -3
  32. package/dist/seeders/module-test-data.service.js.map +1 -1
  33. package/dist/services/chatter-message.service.d.ts +2 -0
  34. package/dist/services/chatter-message.service.d.ts.map +1 -1
  35. package/dist/services/chatter-message.service.js +18 -2
  36. package/dist/services/chatter-message.service.js.map +1 -1
  37. package/dist/services/crud.service.d.ts.map +1 -1
  38. package/dist/services/crud.service.js.map +1 -1
  39. package/dist/services/queues/common.d.ts +3 -0
  40. package/dist/services/queues/common.d.ts.map +1 -0
  41. package/dist/services/queues/common.js +39 -0
  42. package/dist/services/queues/common.js.map +1 -0
  43. package/dist/services/queues/database-publisher.service.d.ts.map +1 -1
  44. package/dist/services/queues/database-publisher.service.js +3 -1
  45. package/dist/services/queues/database-publisher.service.js.map +1 -1
  46. package/dist/services/queues/database-subscriber.service.d.ts.map +1 -1
  47. package/dist/services/queues/database-subscriber.service.js +5 -2
  48. package/dist/services/queues/database-subscriber.service.js.map +1 -1
  49. package/dist/services/queues/rabbitmq-publisher.service.d.ts.map +1 -1
  50. package/dist/services/queues/rabbitmq-publisher.service.js +13 -6
  51. package/dist/services/queues/rabbitmq-publisher.service.js.map +1 -1
  52. package/dist/services/queues/rabbitmq-subscriber.service.d.ts.map +1 -1
  53. package/dist/services/queues/rabbitmq-subscriber.service.js +9 -5
  54. package/dist/services/queues/rabbitmq-subscriber.service.js.map +1 -1
  55. package/dist/solid-core.module.d.ts.map +1 -1
  56. package/dist/solid-core.module.js +4 -0
  57. package/dist/solid-core.module.js.map +1 -1
  58. package/dist/testing/__examples__/register-example-specs.d.ts +3 -0
  59. package/dist/testing/__examples__/register-example-specs.d.ts.map +1 -0
  60. package/dist/testing/__examples__/register-example-specs.js +8 -0
  61. package/dist/testing/__examples__/register-example-specs.js.map +1 -0
  62. package/dist/testing/__examples__/specs/custom-health.spec.d.ts +17 -0
  63. package/dist/testing/__examples__/specs/custom-health.spec.d.ts.map +1 -0
  64. package/dist/testing/__examples__/specs/custom-health.spec.js +30 -0
  65. package/dist/testing/__examples__/specs/custom-health.spec.js.map +1 -0
  66. package/dist/testing/adapters/api/api-adapter.d.ts +9 -0
  67. package/dist/testing/adapters/api/api-adapter.d.ts.map +1 -0
  68. package/dist/testing/adapters/api/api-adapter.js +76 -0
  69. package/dist/testing/adapters/api/api-adapter.js.map +1 -0
  70. package/dist/testing/adapters/api/api.types.d.ts +14 -0
  71. package/dist/testing/adapters/api/api.types.d.ts.map +1 -0
  72. package/dist/testing/adapters/api/api.types.js +3 -0
  73. package/dist/testing/adapters/api/api.types.js.map +1 -0
  74. package/dist/testing/adapters/ui/playwright-adapter.d.ts +14 -0
  75. package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -0
  76. package/dist/testing/adapters/ui/playwright-adapter.js +47 -0
  77. package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -0
  78. package/dist/testing/adapters/ui/ui.types.d.ts +5 -0
  79. package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -0
  80. package/dist/testing/adapters/ui/ui.types.js +3 -0
  81. package/dist/testing/adapters/ui/ui.types.js.map +1 -0
  82. package/dist/testing/contracts/runtime-context.types.d.ts +35 -0
  83. package/dist/testing/contracts/runtime-context.types.d.ts.map +1 -0
  84. package/dist/testing/contracts/runtime-context.types.js +3 -0
  85. package/dist/testing/contracts/runtime-context.types.js.map +1 -0
  86. package/dist/testing/contracts/test-spec.types.d.ts +21 -0
  87. package/dist/testing/contracts/test-spec.types.d.ts.map +1 -0
  88. package/dist/testing/contracts/test-spec.types.js +3 -0
  89. package/dist/testing/contracts/test-spec.types.js.map +1 -0
  90. package/dist/testing/contracts/testing-metadata.types.d.ts +41 -0
  91. package/dist/testing/contracts/testing-metadata.types.d.ts.map +1 -0
  92. package/dist/testing/contracts/testing-metadata.types.js +3 -0
  93. package/dist/testing/contracts/testing-metadata.types.js.map +1 -0
  94. package/dist/testing/core/interpolation.d.ts +4 -0
  95. package/dist/testing/core/interpolation.d.ts.map +1 -0
  96. package/dist/testing/core/interpolation.js +180 -0
  97. package/dist/testing/core/interpolation.js.map +1 -0
  98. package/dist/testing/core/normalize-steps.d.ts +7 -0
  99. package/dist/testing/core/normalize-steps.d.ts.map +1 -0
  100. package/dist/testing/core/normalize-steps.js +20 -0
  101. package/dist/testing/core/normalize-steps.js.map +1 -0
  102. package/dist/testing/core/resource-store.d.ts +8 -0
  103. package/dist/testing/core/resource-store.d.ts.map +1 -0
  104. package/dist/testing/core/resource-store.js +41 -0
  105. package/dist/testing/core/resource-store.js.map +1 -0
  106. package/dist/testing/core/spec-registry.d.ts +10 -0
  107. package/dist/testing/core/spec-registry.d.ts.map +1 -0
  108. package/dist/testing/core/spec-registry.js +32 -0
  109. package/dist/testing/core/spec-registry.js.map +1 -0
  110. package/dist/testing/core/step-registry.d.ts +10 -0
  111. package/dist/testing/core/step-registry.d.ts.map +1 -0
  112. package/dist/testing/core/step-registry.js +26 -0
  113. package/dist/testing/core/step-registry.js.map +1 -0
  114. package/dist/testing/core/testing-engine.d.ts +14 -0
  115. package/dist/testing/core/testing-engine.d.ts.map +1 -0
  116. package/dist/testing/core/testing-engine.js +97 -0
  117. package/dist/testing/core/testing-engine.js.map +1 -0
  118. package/dist/testing/core/timeout.d.ts +2 -0
  119. package/dist/testing/core/timeout.d.ts.map +1 -0
  120. package/dist/testing/core/timeout.js +18 -0
  121. package/dist/testing/core/timeout.js.map +1 -0
  122. package/dist/testing/reporter/attachments.d.ts +4 -0
  123. package/dist/testing/reporter/attachments.d.ts.map +1 -0
  124. package/dist/testing/reporter/attachments.js +25 -0
  125. package/dist/testing/reporter/attachments.js.map +1 -0
  126. package/dist/testing/reporter/console-reporter.d.ts +45 -0
  127. package/dist/testing/reporter/console-reporter.d.ts.map +1 -0
  128. package/dist/testing/reporter/console-reporter.js +189 -0
  129. package/dist/testing/reporter/console-reporter.js.map +1 -0
  130. package/dist/testing/reporter/reporter.types.d.ts +37 -0
  131. package/dist/testing/reporter/reporter.types.d.ts.map +1 -0
  132. package/dist/testing/reporter/reporter.types.js +3 -0
  133. package/dist/testing/reporter/reporter.types.js.map +1 -0
  134. package/dist/testing/runner/lifecycle.d.ts +9 -0
  135. package/dist/testing/runner/lifecycle.d.ts.map +1 -0
  136. package/dist/testing/runner/lifecycle.js +33 -0
  137. package/dist/testing/runner/lifecycle.js.map +1 -0
  138. package/dist/testing/runner/run-from-metadata.d.ts +24 -0
  139. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -0
  140. package/dist/testing/runner/run-from-metadata.js +70 -0
  141. package/dist/testing/runner/run-from-metadata.js.map +1 -0
  142. package/dist/testing/runner/scenario-filter.d.ts +9 -0
  143. package/dist/testing/runner/scenario-filter.d.ts.map +1 -0
  144. package/dist/testing/runner/scenario-filter.js +22 -0
  145. package/dist/testing/runner/scenario-filter.js.map +1 -0
  146. package/dist/testing/steps/api/auth.step.d.ts +3 -0
  147. package/dist/testing/steps/api/auth.step.d.ts.map +1 -0
  148. package/dist/testing/steps/api/auth.step.js +38 -0
  149. package/dist/testing/steps/api/auth.step.js.map +1 -0
  150. package/dist/testing/steps/api/index.d.ts +3 -0
  151. package/dist/testing/steps/api/index.d.ts.map +1 -0
  152. package/dist/testing/steps/api/index.js +10 -0
  153. package/dist/testing/steps/api/index.js.map +1 -0
  154. package/dist/testing/steps/api/request.step.d.ts +3 -0
  155. package/dist/testing/steps/api/request.step.d.ts.map +1 -0
  156. package/dist/testing/steps/api/request.step.js +281 -0
  157. package/dist/testing/steps/api/request.step.js.map +1 -0
  158. package/dist/testing/steps/assert/http.step.d.ts +3 -0
  159. package/dist/testing/steps/assert/http.step.d.ts.map +1 -0
  160. package/dist/testing/steps/assert/http.step.js +27 -0
  161. package/dist/testing/steps/assert/http.step.js.map +1 -0
  162. package/dist/testing/steps/assert/index.d.ts +3 -0
  163. package/dist/testing/steps/assert/index.d.ts.map +1 -0
  164. package/dist/testing/steps/assert/index.js +12 -0
  165. package/dist/testing/steps/assert/index.js.map +1 -0
  166. package/dist/testing/steps/assert/jsonpath.step.d.ts +3 -0
  167. package/dist/testing/steps/assert/jsonpath.step.d.ts.map +1 -0
  168. package/dist/testing/steps/assert/jsonpath.step.js +40 -0
  169. package/dist/testing/steps/assert/jsonpath.step.js.map +1 -0
  170. package/dist/testing/steps/assert/primitives.step.d.ts +3 -0
  171. package/dist/testing/steps/assert/primitives.step.d.ts.map +1 -0
  172. package/dist/testing/steps/assert/primitives.step.js +43 -0
  173. package/dist/testing/steps/assert/primitives.step.js.map +1 -0
  174. package/dist/testing/steps/test/index.d.ts +3 -0
  175. package/dist/testing/steps/test/index.d.ts.map +1 -0
  176. package/dist/testing/steps/test/index.js +8 -0
  177. package/dist/testing/steps/test/index.js.map +1 -0
  178. package/dist/testing/steps/test/test-spec.step.d.ts +3 -0
  179. package/dist/testing/steps/test/test-spec.step.d.ts.map +1 -0
  180. package/dist/testing/steps/test/test-spec.step.js +41 -0
  181. package/dist/testing/steps/test/test-spec.step.js.map +1 -0
  182. package/dist/testing/steps/ui/actions.step.d.ts +3 -0
  183. package/dist/testing/steps/ui/actions.step.d.ts.map +1 -0
  184. package/dist/testing/steps/ui/actions.step.js +31 -0
  185. package/dist/testing/steps/ui/actions.step.js.map +1 -0
  186. package/dist/testing/steps/ui/assertions.step.d.ts +3 -0
  187. package/dist/testing/steps/ui/assertions.step.d.ts.map +1 -0
  188. package/dist/testing/steps/ui/assertions.step.js +41 -0
  189. package/dist/testing/steps/ui/assertions.step.js.map +1 -0
  190. package/dist/testing/steps/ui/form.step.d.ts +3 -0
  191. package/dist/testing/steps/ui/form.step.d.ts.map +1 -0
  192. package/dist/testing/steps/ui/form.step.js +34 -0
  193. package/dist/testing/steps/ui/form.step.js.map +1 -0
  194. package/dist/testing/steps/ui/index.d.ts +3 -0
  195. package/dist/testing/steps/ui/index.d.ts.map +1 -0
  196. package/dist/testing/steps/ui/index.js +14 -0
  197. package/dist/testing/steps/ui/index.js.map +1 -0
  198. package/dist/testing/steps/ui/navigation.step.d.ts +3 -0
  199. package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -0
  200. package/dist/testing/steps/ui/navigation.step.js +39 -0
  201. package/dist/testing/steps/ui/navigation.step.js.map +1 -0
  202. package/dist/testing/steps/util/index.d.ts +3 -0
  203. package/dist/testing/steps/util/index.d.ts.map +1 -0
  204. package/dist/testing/steps/util/index.js +12 -0
  205. package/dist/testing/steps/util/index.js.map +1 -0
  206. package/dist/testing/steps/util/log.step.d.ts +3 -0
  207. package/dist/testing/steps/util/log.step.d.ts.map +1 -0
  208. package/dist/testing/steps/util/log.step.js +18 -0
  209. package/dist/testing/steps/util/log.step.js.map +1 -0
  210. package/dist/testing/steps/util/require.step.d.ts +3 -0
  211. package/dist/testing/steps/util/require.step.d.ts.map +1 -0
  212. package/dist/testing/steps/util/require.step.js +16 -0
  213. package/dist/testing/steps/util/require.step.js.map +1 -0
  214. package/dist/testing/steps/util/sleep.step.d.ts +3 -0
  215. package/dist/testing/steps/util/sleep.step.d.ts.map +1 -0
  216. package/dist/testing/steps/util/sleep.step.js +13 -0
  217. package/dist/testing/steps/util/sleep.step.js.map +1 -0
  218. package/docs/test-data-workflow.md +51 -11
  219. package/package.json +4 -2
  220. package/src/commands/run-tests.command.ts +278 -0
  221. package/src/commands/test-data.command.ts +26 -26
  222. package/src/commands/test.command.ts +14 -0
  223. package/src/controllers/service.controller.ts +58 -59
  224. package/src/dtos/basic-filters.dto.ts +0 -2
  225. package/src/dtos/create-user.dto.ts +1 -0
  226. package/src/index.ts +3 -0
  227. package/src/seeders/module-metadata-seeder.service.ts +3 -24
  228. package/src/seeders/module-test-data.service.ts +5 -3
  229. package/src/services/chatter-message.service.ts +18 -1
  230. package/src/services/crud.service.ts +1 -0
  231. package/src/services/queues/common.ts +75 -0
  232. package/src/services/queues/database-publisher.service.ts +4 -1
  233. package/src/services/queues/database-subscriber.service.ts +5 -3
  234. package/src/services/queues/rabbitmq-publisher.service.ts +17 -7
  235. package/src/services/queues/rabbitmq-subscriber.service.ts +9 -5
  236. package/src/solid-core.module.ts +4 -0
  237. package/src/testing/README.md +364 -0
  238. package/src/testing/__examples__/register-example-specs.ts +6 -0
  239. package/src/testing/__examples__/specs/custom-health.spec.ts +29 -0
  240. package/src/testing/__examples__/testing.sample.json +82 -0
  241. package/src/testing/adapters/api/api-adapter.ts +85 -0
  242. package/src/testing/adapters/api/api.types.ts +15 -0
  243. package/src/testing/adapters/ui/playwright-adapter.ts +54 -0
  244. package/src/testing/adapters/ui/ui.types.ts +4 -0
  245. package/src/testing/contracts/runtime-context.types.ts +36 -0
  246. package/src/testing/contracts/test-spec.types.ts +24 -0
  247. package/src/testing/contracts/testing-metadata.types.ts +46 -0
  248. package/src/testing/core/interpolation.ts +189 -0
  249. package/src/testing/core/normalize-steps.ts +21 -0
  250. package/src/testing/core/resource-store.ts +38 -0
  251. package/src/testing/core/spec-registry.ts +33 -0
  252. package/src/testing/core/step-registry.ts +27 -0
  253. package/src/testing/core/testing-engine.ts +127 -0
  254. package/src/testing/core/timeout.ts +19 -0
  255. package/src/testing/reporter/attachments.ts +25 -0
  256. package/src/testing/reporter/console-reporter.ts +229 -0
  257. package/src/testing/reporter/reporter.types.ts +36 -0
  258. package/src/testing/runner/lifecycle.ts +31 -0
  259. package/src/testing/runner/run-from-metadata.ts +87 -0
  260. package/src/testing/runner/scenario-filter.ts +33 -0
  261. package/src/testing/steps/api/auth.step.ts +66 -0
  262. package/src/testing/steps/api/index.ts +10 -0
  263. package/src/testing/steps/api/request.step.ts +358 -0
  264. package/src/testing/steps/assert/http.step.ts +33 -0
  265. package/src/testing/steps/assert/index.ts +12 -0
  266. package/src/testing/steps/assert/jsonpath.step.ts +50 -0
  267. package/src/testing/steps/assert/primitives.step.ts +69 -0
  268. package/src/testing/steps/test/index.ts +8 -0
  269. package/src/testing/steps/test/test-spec.step.ts +52 -0
  270. package/src/testing/steps/ui/actions.step.ts +36 -0
  271. package/src/testing/steps/ui/assertions.step.ts +54 -0
  272. package/src/testing/steps/ui/form.step.ts +39 -0
  273. package/src/testing/steps/ui/index.ts +12 -0
  274. package/src/testing/steps/ui/navigation.step.ts +53 -0
  275. package/src/testing/steps/util/index.ts +10 -0
  276. package/src/testing/steps/util/log.step.ts +19 -0
  277. package/src/testing/steps/util/require.step.ts +16 -0
  278. package/src/testing/steps/util/sleep.step.ts +15 -0
  279. package/tsconfig.json +35 -25
  280. package/tsconfig.tests.json +14 -0
  281. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,46 @@
1
+ export type ScenarioType = "api" | "ui" | "mixed";
2
+
3
+ export interface TestingDataRecord {
4
+ modelUserKey: string;
5
+ recUserKeyValue: string;
6
+ data: Record<string, any>;
7
+ }
8
+
9
+ export interface TestingMetadata {
10
+ testing: {
11
+ specs?: string[];
12
+ data?: TestingDataRecord[];
13
+ scenarios: ScenarioSpec[];
14
+ };
15
+ }
16
+
17
+ export interface ScenarioSpec {
18
+ id: string;
19
+ name?: string;
20
+ type: ScenarioType;
21
+ params?: Record<string, any>;
22
+ tags?: string[];
23
+ timeoutMs?: number;
24
+ retries?: number;
25
+ steps: StepBlock[];
26
+ }
27
+
28
+ /**
29
+ * A step can be written in a phase block (Given/When/Then/And) or as a flat op step.
30
+ */
31
+ export type StepBlock =
32
+ | { given: OpStep }
33
+ | { when: OpStep }
34
+ | { then: OpStep | OpStep[] }
35
+ | { and: OpStep }
36
+ | OpStep;
37
+
38
+ export interface OpStep {
39
+ op: string;
40
+ with?: Record<string, any>;
41
+ saveAs?: string;
42
+ name?: string;
43
+ // spec is used by op "test.spec" to point to a registered custom spec implementation.
44
+ spec?: string;
45
+ timeoutMs?: number;
46
+ }
@@ -0,0 +1,189 @@
1
+ import type { TestContext } from "../contracts/runtime-context.types";
2
+
3
+ const TOKEN_REGEX = /\$\{([^}]+)\}/g;
4
+
5
+ function getByPath(obj: Record<string, any>, path: string): unknown {
6
+ if (!path) return undefined;
7
+ const parts = path.split(".");
8
+ let current: any = obj;
9
+ for (const part of parts) {
10
+ if (current == null || typeof current !== "object") return undefined;
11
+ current = current[part];
12
+ }
13
+ return current;
14
+ }
15
+
16
+ function parsePathSegments(path: string): string[] {
17
+ const segments: string[] = [];
18
+ let buffer = "";
19
+ let i = 0;
20
+
21
+ const pushBuffer = () => {
22
+ if (buffer) {
23
+ segments.push(buffer);
24
+ buffer = "";
25
+ }
26
+ };
27
+
28
+ while (i < path.length) {
29
+ const ch = path[i];
30
+ if (ch === ".") {
31
+ pushBuffer();
32
+ i += 1;
33
+ continue;
34
+ }
35
+ if (ch === "[") {
36
+ pushBuffer();
37
+ i += 1;
38
+ if (i >= path.length) break;
39
+ let quote = "";
40
+ if (path[i] === '"' || path[i] === "'") {
41
+ quote = path[i];
42
+ i += 1;
43
+ }
44
+ let value = "";
45
+ while (i < path.length) {
46
+ const c = path[i];
47
+ if (quote) {
48
+ if (c === "\\" && i + 1 < path.length) {
49
+ value += path[i + 1];
50
+ i += 2;
51
+ continue;
52
+ }
53
+ if (c === quote) {
54
+ i += 1;
55
+ break;
56
+ }
57
+ value += c;
58
+ i += 1;
59
+ continue;
60
+ }
61
+ if (c === "]") break;
62
+ value += c;
63
+ i += 1;
64
+ }
65
+ while (i < path.length && path[i] !== "]") {
66
+ i += 1;
67
+ }
68
+ if (i < path.length && path[i] === "]") {
69
+ i += 1;
70
+ }
71
+ if (value) {
72
+ segments.push(value);
73
+ }
74
+ continue;
75
+ }
76
+ buffer += ch;
77
+ i += 1;
78
+ }
79
+
80
+ pushBuffer();
81
+ return segments;
82
+ }
83
+
84
+ function getByPathWithBrackets(obj: Record<string, any>, path: string): unknown {
85
+ if (!path) return undefined;
86
+ const parts = parsePathSegments(path);
87
+ let current: any = obj;
88
+ for (const part of parts) {
89
+ if (current == null || typeof current !== "object") return undefined;
90
+ current = current[part];
91
+ }
92
+ return current;
93
+ }
94
+
95
+ type TokenResolution = { value: unknown; raw: boolean };
96
+
97
+ function resolveToken(token: string, ctx: TestContext): TokenResolution {
98
+ if (token.startsWith("env:")) {
99
+ const name = token.slice("env:".length);
100
+ if (!name) {
101
+ throw new Error('Invalid interpolation token: "env:"');
102
+ }
103
+ const value = process.env[name];
104
+ if (value === undefined) {
105
+ throw new Error(`Missing env var for token: "${token}"`);
106
+ }
107
+ return { value, raw: false };
108
+ }
109
+
110
+ if (token.startsWith("params.")) {
111
+ const path = token.slice("params.".length);
112
+ if (!path) {
113
+ throw new Error('Invalid interpolation token: "params."');
114
+ }
115
+ const value = getByPath(ctx.params, path);
116
+ if (value === undefined) {
117
+ throw new Error(`Missing param for token: "${token}"`);
118
+ }
119
+ return { value, raw: false };
120
+ }
121
+
122
+ if (token.startsWith("data:") || token.startsWith("data.")) {
123
+ const prefix = token.startsWith("data:") ? "data:" : "data.";
124
+ let path = token.slice(prefix.length);
125
+ if (!path) {
126
+ throw new Error(`Invalid interpolation token: "${prefix}"`);
127
+ }
128
+ let raw = false;
129
+ if (path.endsWith("._rec")) {
130
+ raw = true;
131
+ path = path.slice(0, -("._rec".length));
132
+ }
133
+ const value = getByPathWithBrackets((ctx as any).testData ?? {}, path);
134
+ if (value === undefined) {
135
+ throw new Error(`Missing test data for token: "${token}"`);
136
+ }
137
+ return { value, raw };
138
+ }
139
+
140
+ if (token.startsWith("res:")) {
141
+ const path = token.slice("res:".length);
142
+ if (!path) {
143
+ throw new Error('Invalid interpolation token: "res:"');
144
+ }
145
+ const value = ctx.resources.get(path);
146
+ if (value === undefined) {
147
+ throw new Error(`Missing resource for token: "${token}"`);
148
+ }
149
+ return { value, raw: false };
150
+ }
151
+
152
+ throw new Error(`Unknown interpolation token: "${token}"`);
153
+ }
154
+
155
+ export function interpolateString(input: string, ctx: TestContext): string {
156
+ return input.replace(TOKEN_REGEX, (_match, token: string) => {
157
+ const resolved = resolveToken(token, ctx);
158
+ return typeof resolved.value === "string"
159
+ ? resolved.value
160
+ : JSON.stringify(resolved.value);
161
+ });
162
+ }
163
+
164
+ export function interpolateDeep<T>(input: T, ctx: TestContext): T {
165
+ if (typeof input === "string") {
166
+ const tokenMatch = input.match(/^\$\{([^}]+)\}$/);
167
+ if (tokenMatch) {
168
+ const resolved = resolveToken(tokenMatch[1], ctx);
169
+ if (resolved.raw) {
170
+ return resolved.value as T;
171
+ }
172
+ }
173
+ return interpolateString(input, ctx) as T;
174
+ }
175
+
176
+ if (Array.isArray(input)) {
177
+ return input.map((item) => interpolateDeep(item, ctx)) as T;
178
+ }
179
+
180
+ if (input && typeof input === "object") {
181
+ const out: Record<string, unknown> = {};
182
+ for (const [key, value] of Object.entries(input as Record<string, any>)) {
183
+ out[key] = interpolateDeep(value, ctx);
184
+ }
185
+ return out as T;
186
+ }
187
+
188
+ return input;
189
+ }
@@ -0,0 +1,21 @@
1
+ import type { StepPhase } from "../contracts/runtime-context.types";
2
+ import type { OpStep, StepBlock } from "../contracts/testing-metadata.types";
3
+
4
+ export function normalizeBlock(
5
+ block: StepBlock,
6
+ ): { phase: StepPhase; steps: OpStep[] } {
7
+ if ("given" in block) {
8
+ return { phase: "given", steps: [block.given] };
9
+ }
10
+ if ("when" in block) {
11
+ return { phase: "when", steps: [block.when] };
12
+ }
13
+ if ("then" in block) {
14
+ const steps = Array.isArray(block.then) ? block.then : [block.then];
15
+ return { phase: "then", steps };
16
+ }
17
+ if ("and" in block) {
18
+ return { phase: "and", steps: [block.and] };
19
+ }
20
+ return { phase: "step", steps: [block] };
21
+ }
@@ -0,0 +1,38 @@
1
+ import type { ResourceStore } from "../contracts/runtime-context.types";
2
+
3
+ export class SimpleResourceStore implements ResourceStore {
4
+ private readonly data: Record<string, any> = {};
5
+
6
+ get(path: string): unknown {
7
+ if (!path) return undefined;
8
+ const parts = path.split(".");
9
+ let current: any = this.data;
10
+ for (const part of parts) {
11
+ if (current == null || typeof current !== "object") return undefined;
12
+ current = current[part];
13
+ }
14
+ return current;
15
+ }
16
+
17
+ set(path: string, value: unknown): void {
18
+ if (!path) return;
19
+ const parts = path.split(".");
20
+ let current: any = this.data;
21
+ for (let i = 0; i < parts.length - 1; i += 1) {
22
+ const part = parts[i];
23
+ if (
24
+ current[part] == null ||
25
+ typeof current[part] !== "object" ||
26
+ Array.isArray(current[part])
27
+ ) {
28
+ current[part] = {};
29
+ }
30
+ current = current[part];
31
+ }
32
+ current[parts[parts.length - 1]] = value;
33
+ }
34
+
35
+ has(path: string): boolean {
36
+ return this.get(path) !== undefined;
37
+ }
38
+ }
@@ -0,0 +1,33 @@
1
+ import type { ISolidTestSpec } from "../contracts/test-spec.types";
2
+
3
+ export type SpecFactory = () => ISolidTestSpec;
4
+
5
+ export class SpecRegistry {
6
+ private readonly factories = new Map<string, SpecFactory>();
7
+
8
+ register(specId: string, factory: SpecFactory): void {
9
+ if (!specId) {
10
+ throw new Error("specId is required");
11
+ }
12
+ if (this.factories.has(specId)) {
13
+ throw new Error(`Spec already registered: "${specId}"`);
14
+ }
15
+ this.factories.set(specId, factory);
16
+ }
17
+
18
+ create(specId: string): ISolidTestSpec {
19
+ const factory = this.factories.get(specId);
20
+ if (!factory) {
21
+ throw new Error(`Spec not registered: "${specId}"`);
22
+ }
23
+ return factory();
24
+ }
25
+
26
+ has(specId: string): boolean {
27
+ return this.factories.has(specId);
28
+ }
29
+
30
+ list(): string[] {
31
+ return Array.from(this.factories.keys()).sort();
32
+ }
33
+ }
@@ -0,0 +1,27 @@
1
+ import type { TestContext } from "../contracts/runtime-context.types";
2
+ import type { OpStep } from "../contracts/testing-metadata.types";
3
+
4
+ export type StepHandler = (ctx: TestContext, step: OpStep) => Promise<any>;
5
+
6
+ export class StepRegistry {
7
+ private readonly handlers = new Map<string, StepHandler>();
8
+
9
+ register(op: string, handler: StepHandler): void {
10
+ if (this.handlers.has(op)) {
11
+ throw new Error(`Step handler already registered for op: "${op}"`);
12
+ }
13
+ this.handlers.set(op, handler);
14
+ }
15
+
16
+ get(op: string): StepHandler {
17
+ const handler = this.handlers.get(op);
18
+ if (!handler) {
19
+ throw new Error(`No step handler registered for op: "${op}"`);
20
+ }
21
+ return handler;
22
+ }
23
+
24
+ has(op: string): boolean {
25
+ return this.handlers.has(op);
26
+ }
27
+ }
@@ -0,0 +1,127 @@
1
+ import type { TestContext, StepPhase } from "../contracts/runtime-context.types";
2
+ import type { OpStep, ScenarioSpec } from "../contracts/testing-metadata.types";
3
+ import { interpolateDeep } from "./interpolation";
4
+ import { normalizeBlock } from "./normalize-steps";
5
+ import { StepRegistry } from "./step-registry";
6
+ import { withTimeout } from "./timeout";
7
+
8
+ export class TestingEngine {
9
+ private readonly registry: StepRegistry;
10
+ private readonly defaults?: { timeoutMs?: number; retries?: number };
11
+
12
+ constructor(
13
+ registry: StepRegistry,
14
+ defaults?: { timeoutMs?: number; retries?: number },
15
+ ) {
16
+ this.registry = registry;
17
+ this.defaults = defaults;
18
+ }
19
+
20
+ async runScenario(
21
+ scenario: ScenarioSpec,
22
+ ctxBase: Omit<TestContext, "scenarioId" | "scenarioType" | "params">,
23
+ ): Promise<void> {
24
+ const retries = scenario.retries ?? this.defaults?.retries ?? 0;
25
+ const maxAttempts = Math.max(0, retries) + 1;
26
+ const scenarioTimeoutMs =
27
+ scenario.timeoutMs ?? this.defaults?.timeoutMs;
28
+
29
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
30
+ const ctx: TestContext = {
31
+ ...ctxBase,
32
+ scenarioId: scenario.id,
33
+ scenarioType: scenario.type,
34
+ params: scenario.params ?? {},
35
+ };
36
+
37
+ const reporter = ctx.reporter;
38
+ const scenarioStart = Date.now();
39
+ let scenarioError: unknown;
40
+
41
+ reporter.onScenarioStart(scenario);
42
+
43
+ try {
44
+ const execute = async () => {
45
+ for (const block of scenario.steps) {
46
+ const normalized = normalizeBlock(block);
47
+ await this.runBlock(normalized.phase, normalized.steps, ctx);
48
+ }
49
+ };
50
+
51
+ if (scenarioTimeoutMs !== undefined) {
52
+ await withTimeout(
53
+ execute(),
54
+ scenarioTimeoutMs,
55
+ `Scenario timeout after ${scenarioTimeoutMs}ms: "${scenario.id}"`,
56
+ );
57
+ } else {
58
+ await execute();
59
+ }
60
+ } catch (err) {
61
+ scenarioError = err;
62
+ } finally {
63
+ const durationMs = Date.now() - scenarioStart;
64
+ reporter.onScenarioEnd(scenario, {
65
+ ok: !scenarioError,
66
+ error: scenarioError,
67
+ durationMs,
68
+ });
69
+ }
70
+
71
+ if (!scenarioError) {
72
+ return;
73
+ }
74
+
75
+ if (attempt >= maxAttempts) {
76
+ throw scenarioError;
77
+ }
78
+ }
79
+ }
80
+
81
+ private async runBlock(
82
+ phase: StepPhase,
83
+ steps: OpStep[],
84
+ ctx: TestContext,
85
+ ): Promise<void> {
86
+ for (const step of steps) {
87
+ const reporter = ctx.reporter;
88
+ const stepStart = Date.now();
89
+ let stepError: unknown;
90
+ let resolvedStep: OpStep | undefined;
91
+
92
+ reporter.onStepStart({ scenarioId: ctx.scenarioId, phase, step });
93
+
94
+ try {
95
+ resolvedStep = interpolateDeep(step, ctx) as OpStep;
96
+ const handler = this.registry.get(resolvedStep.op);
97
+ const run = handler(ctx, resolvedStep);
98
+ const result =
99
+ resolvedStep.timeoutMs !== undefined
100
+ ? await withTimeout(
101
+ run,
102
+ resolvedStep.timeoutMs,
103
+ `Step timeout after ${resolvedStep.timeoutMs}ms: "${resolvedStep.op}"`,
104
+ )
105
+ : await run;
106
+
107
+ if (resolvedStep.saveAs) {
108
+ // console.log(`Step ${resolvedStep.name} attempting to saveAs ${resolvedStep.saveAs}`, JSON.stringify(result));
109
+ ctx.resources.set(resolvedStep.saveAs, result);
110
+ }
111
+ } catch (err) {
112
+ stepError = err;
113
+ throw err;
114
+ } finally {
115
+ const durationMs = Date.now() - stepStart;
116
+ reporter.onStepEnd({
117
+ scenarioId: ctx.scenarioId,
118
+ phase,
119
+ step: resolvedStep ?? step,
120
+ ok: !stepError,
121
+ error: stepError,
122
+ durationMs,
123
+ });
124
+ }
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,19 @@
1
+ export async function withTimeout<T>(
2
+ promise: Promise<T>,
3
+ ms: number,
4
+ message: string,
5
+ ): Promise<T> {
6
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
7
+
8
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
9
+ timeoutId = setTimeout(() => reject(new Error(message)), ms);
10
+ });
11
+
12
+ try {
13
+ return await Promise.race([promise, timeoutPromise]);
14
+ } finally {
15
+ if (timeoutId !== undefined) {
16
+ clearTimeout(timeoutId);
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,25 @@
1
+ import type { TestContext } from "../contracts/runtime-context.types";
2
+
3
+ export function attachJson(
4
+ ctx: TestContext,
5
+ name: string,
6
+ obj: unknown,
7
+ ): void {
8
+ if (!ctx.reporter.attach) return;
9
+ ctx.reporter.attach({
10
+ scenarioId: ctx.scenarioId,
11
+ name,
12
+ contentType: "application/json",
13
+ data: JSON.stringify(obj, null, 2),
14
+ });
15
+ }
16
+
17
+ export function attachText(ctx: TestContext, name: string, text: string): void {
18
+ if (!ctx.reporter.attach) return;
19
+ ctx.reporter.attach({
20
+ scenarioId: ctx.scenarioId,
21
+ name,
22
+ contentType: "text/plain",
23
+ data: text,
24
+ });
25
+ }