@solidxai/core 0.1.15 → 0.1.16-beta.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 (171) hide show
  1. package/CHANGELOG.md +642 -0
  2. package/dist/commands/run-tests.command.d.ts +3 -1
  3. package/dist/commands/run-tests.command.d.ts.map +1 -1
  4. package/dist/commands/run-tests.command.js +6 -2
  5. package/dist/commands/run-tests.command.js.map +1 -1
  6. package/dist/controllers/test-run.controller.d.ts +12 -0
  7. package/dist/controllers/test-run.controller.d.ts.map +1 -0
  8. package/dist/controllers/test-run.controller.js +54 -0
  9. package/dist/controllers/test-run.controller.js.map +1 -0
  10. package/dist/dtos/test-run-request.dto.d.ts +35 -0
  11. package/dist/dtos/test-run-request.dto.d.ts.map +1 -0
  12. package/dist/dtos/test-run-request.dto.js +120 -0
  13. package/dist/dtos/test-run-request.dto.js.map +1 -0
  14. package/dist/guards/access-token.guard.d.ts +5 -1
  15. package/dist/guards/access-token.guard.d.ts.map +1 -1
  16. package/dist/guards/access-token.guard.js +20 -1
  17. package/dist/guards/access-token.guard.js.map +1 -1
  18. package/dist/index.d.ts +12 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +17 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/interfaces/active-user-data.interface.d.ts +1 -0
  23. package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
  24. package/dist/interfaces/active-user-data.interface.js.map +1 -1
  25. package/dist/jobs/database/test-run-queue-options-database.d.ts +8 -0
  26. package/dist/jobs/database/test-run-queue-options-database.d.ts.map +1 -0
  27. package/dist/jobs/database/test-run-queue-options-database.js +10 -0
  28. package/dist/jobs/database/test-run-queue-options-database.js.map +1 -0
  29. package/dist/jobs/database/test-run-queue-publisher-database.service.d.ts +12 -0
  30. package/dist/jobs/database/test-run-queue-publisher-database.service.d.ts.map +1 -0
  31. package/dist/jobs/database/test-run-queue-publisher-database.service.js +39 -0
  32. package/dist/jobs/database/test-run-queue-publisher-database.service.js.map +1 -0
  33. package/dist/jobs/database/test-run-queue-subscriber-database.service.d.ts +26 -0
  34. package/dist/jobs/database/test-run-queue-subscriber-database.service.d.ts.map +1 -0
  35. package/dist/jobs/database/test-run-queue-subscriber-database.service.js +120 -0
  36. package/dist/jobs/database/test-run-queue-subscriber-database.service.js.map +1 -0
  37. package/dist/seeders/seed-data/solid-core-metadata.json +3 -1
  38. package/dist/services/access-token-denylist.service.d.ts +16 -0
  39. package/dist/services/access-token-denylist.service.d.ts.map +1 -0
  40. package/dist/services/access-token-denylist.service.js +74 -0
  41. package/dist/services/access-token-denylist.service.js.map +1 -0
  42. package/dist/services/authentication.service.d.ts +9 -1
  43. package/dist/services/authentication.service.d.ts.map +1 -1
  44. package/dist/services/authentication.service.js +39 -4
  45. package/dist/services/authentication.service.js.map +1 -1
  46. package/dist/services/import-transaction.service.d.ts +1 -1
  47. package/dist/services/import-transaction.service.d.ts.map +1 -1
  48. package/dist/services/import-transaction.service.js +13 -23
  49. package/dist/services/import-transaction.service.js.map +1 -1
  50. package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
  51. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  52. package/dist/services/settings/default-settings-provider.service.js +11 -0
  53. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  54. package/dist/services/workflow-secret.service.d.ts +3 -0
  55. package/dist/services/workflow-secret.service.d.ts.map +1 -1
  56. package/dist/services/workflow-secret.service.js +25 -0
  57. package/dist/services/workflow-secret.service.js.map +1 -1
  58. package/dist/solid-core.module.d.ts.map +1 -1
  59. package/dist/solid-core.module.js +9 -0
  60. package/dist/solid-core.module.js.map +1 -1
  61. package/dist/testing/adapters/ui/browser-provisioner.d.ts +5 -0
  62. package/dist/testing/adapters/ui/browser-provisioner.d.ts.map +1 -0
  63. package/dist/testing/adapters/ui/browser-provisioner.js +102 -0
  64. package/dist/testing/adapters/ui/browser-provisioner.js.map +1 -0
  65. package/dist/testing/adapters/ui/playwright-adapter.d.ts +40 -1
  66. package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -1
  67. package/dist/testing/adapters/ui/playwright-adapter.js +185 -3
  68. package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -1
  69. package/dist/testing/adapters/ui/ui.types.d.ts +6 -0
  70. package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -1
  71. package/dist/testing/adapters/ui/ui.types.js.map +1 -1
  72. package/dist/testing/contracts/runtime-context.types.d.ts +2 -0
  73. package/dist/testing/contracts/runtime-context.types.d.ts.map +1 -1
  74. package/dist/testing/contracts/runtime-context.types.js.map +1 -1
  75. package/dist/testing/core/interpolation.d.ts +1 -0
  76. package/dist/testing/core/interpolation.d.ts.map +1 -1
  77. package/dist/testing/core/interpolation.js +28 -68
  78. package/dist/testing/core/interpolation.js.map +1 -1
  79. package/dist/testing/core/path-resolution.spec.d.ts +2 -0
  80. package/dist/testing/core/path-resolution.spec.d.ts.map +1 -0
  81. package/dist/testing/core/path-resolution.spec.js +106 -0
  82. package/dist/testing/core/path-resolution.spec.js.map +1 -0
  83. package/dist/testing/core/path-segments.d.ts +2 -0
  84. package/dist/testing/core/path-segments.d.ts.map +1 -0
  85. package/dist/testing/core/path-segments.js +70 -0
  86. package/dist/testing/core/path-segments.js.map +1 -0
  87. package/dist/testing/core/resource-store.d.ts.map +1 -1
  88. package/dist/testing/core/resource-store.js +5 -2
  89. package/dist/testing/core/resource-store.js.map +1 -1
  90. package/dist/testing/core/testing-engine.d.ts.map +1 -1
  91. package/dist/testing/core/testing-engine.js +16 -0
  92. package/dist/testing/core/testing-engine.js.map +1 -1
  93. package/dist/testing/reporter/artifact-sink.d.ts +14 -0
  94. package/dist/testing/reporter/artifact-sink.d.ts.map +1 -0
  95. package/dist/testing/reporter/artifact-sink.js +3 -0
  96. package/dist/testing/reporter/artifact-sink.js.map +1 -0
  97. package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
  98. package/dist/testing/reporter/console-reporter.js +7 -0
  99. package/dist/testing/reporter/console-reporter.js.map +1 -1
  100. package/dist/testing/reporter/file-service-artifact-sink.d.ts +22 -0
  101. package/dist/testing/reporter/file-service-artifact-sink.d.ts.map +1 -0
  102. package/dist/testing/reporter/file-service-artifact-sink.js +31 -0
  103. package/dist/testing/reporter/file-service-artifact-sink.js.map +1 -0
  104. package/dist/testing/reporter/lifecycle-events.types.d.ts +67 -0
  105. package/dist/testing/reporter/lifecycle-events.types.d.ts.map +1 -0
  106. package/dist/testing/reporter/lifecycle-events.types.js +3 -0
  107. package/dist/testing/reporter/lifecycle-events.types.js.map +1 -0
  108. package/dist/testing/reporter/lifecycle-webhook-reporter.d.ts +77 -0
  109. package/dist/testing/reporter/lifecycle-webhook-reporter.d.ts.map +1 -0
  110. package/dist/testing/reporter/lifecycle-webhook-reporter.js +275 -0
  111. package/dist/testing/reporter/lifecycle-webhook-reporter.js.map +1 -0
  112. package/dist/testing/reporter/progress-reporter.d.ts +3 -3
  113. package/dist/testing/reporter/progress-reporter.d.ts.map +1 -1
  114. package/dist/testing/reporter/progress-reporter.js.map +1 -1
  115. package/dist/testing/reporter/reporter.types.d.ts +13 -1
  116. package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
  117. package/dist/testing/reporter/reporter.types.js.map +1 -1
  118. package/dist/testing/runner/read-scenarios-file.d.ts +9 -0
  119. package/dist/testing/runner/read-scenarios-file.d.ts.map +1 -0
  120. package/dist/testing/runner/read-scenarios-file.js +56 -0
  121. package/dist/testing/runner/read-scenarios-file.js.map +1 -0
  122. package/dist/testing/runner/run-from-metadata.d.ts +7 -2
  123. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
  124. package/dist/testing/runner/run-from-metadata.js +57 -6
  125. package/dist/testing/runner/run-from-metadata.js.map +1 -1
  126. package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -1
  127. package/dist/testing/steps/ui/navigation.step.js +12 -12
  128. package/dist/testing/steps/ui/navigation.step.js.map +1 -1
  129. package/package.json +1 -1
  130. package/postman/instant-logout.postman_collection.json +1493 -0
  131. package/src/commands/run-tests.command.ts +3 -0
  132. package/src/controllers/test-run.controller.ts +44 -0
  133. package/src/dtos/test-run-request.dto.ts +130 -0
  134. package/src/guards/access-token.guard.ts +48 -0
  135. package/src/index.ts +21 -1
  136. package/src/interfaces/active-user-data.interface.ts +8 -0
  137. package/src/jobs/database/test-run-queue-options-database.ts +9 -0
  138. package/src/jobs/database/test-run-queue-publisher-database.service.ts +24 -0
  139. package/src/jobs/database/test-run-queue-subscriber-database.service.ts +122 -0
  140. package/src/seeders/seed-data/solid-core-metadata.json +3 -1
  141. package/src/services/access-token-denylist.service.ts +145 -0
  142. package/src/services/authentication.service.ts +118 -2
  143. package/src/services/import-transaction.service.ts +19 -36
  144. package/src/services/settings/default-settings-provider.service.ts +12 -0
  145. package/src/services/workflow-secret.service.ts +61 -2
  146. package/src/solid-core.module.ts +11 -0
  147. package/src/testing/README.md +44 -1
  148. package/src/testing/adapters/ui/browser-provisioner.ts +115 -0
  149. package/src/testing/adapters/ui/playwright-adapter.ts +255 -5
  150. package/src/testing/adapters/ui/ui.types.ts +14 -0
  151. package/src/testing/contracts/runtime-context.types.ts +6 -0
  152. package/src/testing/core/interpolation.ts +39 -69
  153. package/src/testing/core/path-resolution.spec.ts +133 -0
  154. package/src/testing/core/path-segments.ts +81 -0
  155. package/src/testing/core/resource-store.ts +8 -2
  156. package/src/testing/core/testing-engine.ts +22 -0
  157. package/src/testing/reporter/artifact-sink.ts +16 -0
  158. package/src/testing/reporter/console-reporter.ts +9 -0
  159. package/src/testing/reporter/file-service-artifact-sink.ts +46 -0
  160. package/src/testing/reporter/lifecycle-events.types.ts +103 -0
  161. package/src/testing/reporter/lifecycle-webhook-reporter.ts +385 -0
  162. package/src/testing/reporter/progress-reporter.ts +3 -1
  163. package/src/testing/reporter/reporter.types.ts +10 -1
  164. package/src/testing/runner/read-scenarios-file.ts +44 -0
  165. package/src/testing/runner/run-from-metadata.ts +102 -9
  166. package/src/testing/steps/ui/navigation.step.ts +24 -20
  167. package/dist/services/access-token-storage.service.d.ts +0 -1
  168. package/dist/services/access-token-storage.service.d.ts.map +0 -1
  169. package/dist/services/access-token-storage.service.js +0 -1
  170. package/dist/services/access-token-storage.service.js.map +0 -1
  171. package/src/services/access-token-storage.service.ts +0 -0
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectSecretKeys = collectSecretKeys;
3
4
  exports.interpolateString = interpolateString;
4
5
  exports.interpolateDeep = interpolateDeep;
6
+ const path_segments_1 = require("./path-segments");
5
7
  const TOKEN_REGEX = /\$\{([^}]+)\}/g;
6
8
  function getByPath(obj, path) {
7
9
  if (!path)
@@ -15,76 +17,10 @@ function getByPath(obj, path) {
15
17
  }
16
18
  return current;
17
19
  }
18
- function parsePathSegments(path) {
19
- const segments = [];
20
- let buffer = "";
21
- let i = 0;
22
- const pushBuffer = () => {
23
- if (buffer) {
24
- segments.push(buffer);
25
- buffer = "";
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)
39
- break;
40
- let quote = "";
41
- if (path[i] === '"' || path[i] === "'") {
42
- quote = path[i];
43
- i += 1;
44
- }
45
- let value = "";
46
- while (i < path.length) {
47
- const c = path[i];
48
- if (quote) {
49
- if (c === "\\" && i + 1 < path.length) {
50
- value += path[i + 1];
51
- i += 2;
52
- continue;
53
- }
54
- if (c === quote) {
55
- i += 1;
56
- break;
57
- }
58
- value += c;
59
- i += 1;
60
- continue;
61
- }
62
- if (c === "]")
63
- break;
64
- value += c;
65
- i += 1;
66
- }
67
- while (i < path.length && path[i] !== "]") {
68
- i += 1;
69
- }
70
- if (i < path.length && path[i] === "]") {
71
- i += 1;
72
- }
73
- if (value) {
74
- segments.push(value);
75
- }
76
- continue;
77
- }
78
- buffer += ch;
79
- i += 1;
80
- }
81
- pushBuffer();
82
- return segments;
83
- }
84
20
  function getByPathWithBrackets(obj, path) {
85
21
  if (!path)
86
22
  return undefined;
87
- const parts = parsePathSegments(path);
23
+ const parts = (0, path_segments_1.parsePathSegments)(path);
88
24
  let current = obj;
89
25
  for (const part of parts) {
90
26
  if (current == null || typeof current !== "object")
@@ -93,13 +29,37 @@ function getByPathWithBrackets(obj, path) {
93
29
  }
94
30
  return current;
95
31
  }
32
+ const SECRET_PREFIX = "secret:";
33
+ function collectSecretKeys(value) {
34
+ const keys = new Set();
35
+ for (const match of JSON.stringify(value ?? null).matchAll(TOKEN_REGEX)) {
36
+ const token = match[1];
37
+ if (token.startsWith(SECRET_PREFIX)) {
38
+ const name = token.slice(SECRET_PREFIX.length);
39
+ if (name)
40
+ keys.add(name);
41
+ }
42
+ }
43
+ return [...keys];
44
+ }
96
45
  function resolveToken(token, ctx) {
46
+ if (token.startsWith(SECRET_PREFIX)) {
47
+ const name = token.slice(SECRET_PREFIX.length);
48
+ if (!name) {
49
+ throw new Error('Invalid interpolation token: "secret:"');
50
+ }
51
+ const value = ctx.secrets?.[name];
52
+ if (value === undefined) {
53
+ throw new Error(`Missing secret for token: "${token}"`);
54
+ }
55
+ return { value, raw: typeof value !== "string" };
56
+ }
97
57
  if (token.startsWith("env:")) {
98
58
  const name = token.slice("env:".length);
99
59
  if (!name) {
100
60
  throw new Error('Invalid interpolation token: "env:"');
101
61
  }
102
- const value = process.env[name];
62
+ const value = ctx.env?.[name] ?? process.env[name];
103
63
  if (value === undefined) {
104
64
  throw new Error(`Missing env var for token: "${token}"`);
105
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"interpolation.js","sourceRoot":"","sources":["../../../src/testing/core/interpolation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TestContext } from \"../contracts/runtime-context.types\";\n\nconst TOKEN_REGEX = /\\$\\{([^}]+)\\}/g;\n\nfunction getByPath(obj: Record<string, any>, path: string): unknown {\n if (!path) return undefined;\n const parts = path.split(\".\");\n let current: any = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = current[part];\n }\n return current;\n}\n\nfunction parsePathSegments(path: string): string[] {\n const segments: string[] = [];\n let buffer = \"\";\n let i = 0;\n\n const pushBuffer = () => {\n if (buffer) {\n segments.push(buffer);\n buffer = \"\";\n }\n };\n\n while (i < path.length) {\n const ch = path[i];\n if (ch === \".\") {\n pushBuffer();\n i += 1;\n continue;\n }\n if (ch === \"[\") {\n pushBuffer();\n i += 1;\n if (i >= path.length) break;\n let quote = \"\";\n if (path[i] === '\"' || path[i] === \"'\") {\n quote = path[i];\n i += 1;\n }\n let value = \"\";\n while (i < path.length) {\n const c = path[i];\n if (quote) {\n if (c === \"\\\\\" && i + 1 < path.length) {\n value += path[i + 1];\n i += 2;\n continue;\n }\n if (c === quote) {\n i += 1;\n break;\n }\n value += c;\n i += 1;\n continue;\n }\n if (c === \"]\") break;\n value += c;\n i += 1;\n }\n while (i < path.length && path[i] !== \"]\") {\n i += 1;\n }\n if (i < path.length && path[i] === \"]\") {\n i += 1;\n }\n if (value) {\n segments.push(value);\n }\n continue;\n }\n buffer += ch;\n i += 1;\n }\n\n pushBuffer();\n return segments;\n}\n\nfunction getByPathWithBrackets(obj: Record<string, any>, path: string): unknown {\n if (!path) return undefined;\n const parts = parsePathSegments(path);\n let current: any = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = current[part];\n }\n return current;\n}\n\ntype TokenResolution = { value: unknown; raw: boolean };\n\nfunction resolveToken(token: string, ctx: TestContext): TokenResolution {\n if (token.startsWith(\"env:\")) {\n const name = token.slice(\"env:\".length);\n if (!name) {\n throw new Error('Invalid interpolation token: \"env:\"');\n }\n const value = process.env[name];\n if (value === undefined) {\n throw new Error(`Missing env var for token: \"${token}\"`);\n }\n return { value, raw: false };\n }\n\n if (token.startsWith(\"params.\")) {\n const path = token.slice(\"params.\".length);\n if (!path) {\n throw new Error('Invalid interpolation token: \"params.\"');\n }\n const value = getByPath(ctx.params, path);\n if (value === undefined) {\n throw new Error(`Missing param for token: \"${token}\"`);\n }\n return { value, raw: false };\n }\n\n if (token.startsWith(\"data:\") || token.startsWith(\"data.\")) {\n const prefix = token.startsWith(\"data:\") ? \"data:\" : \"data.\";\n let path = token.slice(prefix.length);\n if (!path) {\n throw new Error(`Invalid interpolation token: \"${prefix}\"`);\n }\n let raw = false;\n if (path.endsWith(\"._rec\")) {\n raw = true;\n path = path.slice(0, -(\"._rec\".length));\n }\n const value = getByPathWithBrackets((ctx as any).testData ?? {}, path);\n if (value === undefined) {\n throw new Error(`Missing test data for token: \"${token}\"`);\n }\n return { value, raw };\n }\n\n if (token.startsWith(\"res:\")) {\n const path = token.slice(\"res:\".length);\n if (!path) {\n throw new Error('Invalid interpolation token: \"res:\"');\n }\n const value = ctx.resources.get(path);\n if (value === undefined) {\n throw new Error(`Missing resource for token: \"${token}\"`);\n }\n return { value, raw: false };\n }\n\n throw new Error(`Unknown interpolation token: \"${token}\"`);\n}\n\nexport function interpolateString(input: string, ctx: TestContext): string {\n return input.replace(TOKEN_REGEX, (_match, token: string) => {\n const resolved = resolveToken(token, ctx);\n return typeof resolved.value === \"string\"\n ? resolved.value\n : JSON.stringify(resolved.value);\n });\n}\n\nexport function interpolateDeep<T>(input: T, ctx: TestContext): T {\n if (typeof input === \"string\") {\n const tokenMatch = input.match(/^\\$\\{([^}]+)\\}$/);\n if (tokenMatch) {\n const resolved = resolveToken(tokenMatch[1], ctx);\n if (resolved.raw) {\n return resolved.value as T;\n }\n }\n return interpolateString(input, ctx) as T;\n }\n\n if (Array.isArray(input)) {\n return input.map((item) => interpolateDeep(item, ctx)) as T;\n }\n\n if (input && typeof input === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input as Record<string, any>)) {\n out[key] = interpolateDeep(value, ctx);\n }\n return out as T;\n }\n\n return input;\n}\n"]}
1
+ {"version":3,"file":"interpolation.js","sourceRoot":"","sources":["../../../src/testing/core/interpolation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TestContext } from \"../contracts/runtime-context.types\";\nimport { parsePathSegments } from \"./path-segments\";\n\nconst TOKEN_REGEX = /\\$\\{([^}]+)\\}/g;\n\nfunction getByPath(obj: Record<string, any>, path: string): unknown {\n if (!path) return undefined;\n const parts = path.split(\".\");\n let current: any = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = current[part];\n }\n return current;\n}\n\nfunction getByPathWithBrackets(obj: Record<string, any>, path: string): unknown {\n if (!path) return undefined;\n const parts = parsePathSegments(path);\n let current: any = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = current[part];\n }\n return current;\n}\n\ntype TokenResolution = { value: unknown; raw: boolean };\n\nconst SECRET_PREFIX = \"secret:\";\n\n/**\n * Collects the secret keys referenced anywhere inside `value` — step args, headers,\n * bodies — without depending on its shape. Used by the runner to resolve exactly the\n * secrets a scenario needs, rather than loading the whole store into a run.\n *\n * Shares TOKEN_REGEX with the resolver deliberately: a second hand-written matcher is\n * how the two drift, and drift means a scenario silently losing a secret it referenced.\n */\nexport function collectSecretKeys(value: unknown): string[] {\n const keys = new Set<string>();\n for (const match of JSON.stringify(value ?? null).matchAll(TOKEN_REGEX)) {\n const token = match[1];\n if (token.startsWith(SECRET_PREFIX)) {\n const name = token.slice(SECRET_PREFIX.length);\n if (name) keys.add(name);\n }\n }\n return [...keys];\n}\n\nfunction resolveToken(token: string, ctx: TestContext): TokenResolution {\n if (token.startsWith(SECRET_PREFIX)) {\n const name = token.slice(SECRET_PREFIX.length);\n if (!name) {\n throw new Error('Invalid interpolation token: \"secret:\"');\n }\n // Resolved server-side into ctx.secrets before the run; never caller-supplied.\n const value = ctx.secrets?.[name];\n if (value === undefined) {\n throw new Error(`Missing secret for token: \"${token}\"`);\n }\n // Secrets carry a declared valueType, so a json/number/boolean secret used as an\n // exact token returns typed rather than stringified. Strings behave like env:.\n return { value, raw: typeof value !== \"string\" };\n }\n\n if (token.startsWith(\"env:\")) {\n const name = token.slice(\"env:\".length);\n if (!name) {\n throw new Error('Invalid interpolation token: \"env:\"');\n }\n const value = ctx.env?.[name] ?? process.env[name];\n if (value === undefined) {\n throw new Error(`Missing env var for token: \"${token}\"`);\n }\n return { value, raw: false };\n }\n\n if (token.startsWith(\"params.\")) {\n const path = token.slice(\"params.\".length);\n if (!path) {\n throw new Error('Invalid interpolation token: \"params.\"');\n }\n const value = getByPath(ctx.params, path);\n if (value === undefined) {\n throw new Error(`Missing param for token: \"${token}\"`);\n }\n return { value, raw: false };\n }\n\n if (token.startsWith(\"data:\") || token.startsWith(\"data.\")) {\n const prefix = token.startsWith(\"data:\") ? \"data:\" : \"data.\";\n let path = token.slice(prefix.length);\n if (!path) {\n throw new Error(`Invalid interpolation token: \"${prefix}\"`);\n }\n let raw = false;\n if (path.endsWith(\"._rec\")) {\n raw = true;\n path = path.slice(0, -(\"._rec\".length));\n }\n const value = getByPathWithBrackets((ctx as any).testData ?? {}, path);\n if (value === undefined) {\n throw new Error(`Missing test data for token: \"${token}\"`);\n }\n return { value, raw };\n }\n\n if (token.startsWith(\"res:\")) {\n const path = token.slice(\"res:\".length);\n if (!path) {\n throw new Error('Invalid interpolation token: \"res:\"');\n }\n const value = ctx.resources.get(path);\n if (value === undefined) {\n throw new Error(`Missing resource for token: \"${token}\"`);\n }\n return { value, raw: false };\n }\n\n throw new Error(`Unknown interpolation token: \"${token}\"`);\n}\n\nexport function interpolateString(input: string, ctx: TestContext): string {\n return input.replace(TOKEN_REGEX, (_match, token: string) => {\n const resolved = resolveToken(token, ctx);\n return typeof resolved.value === \"string\"\n ? resolved.value\n : JSON.stringify(resolved.value);\n });\n}\n\nexport function interpolateDeep<T>(input: T, ctx: TestContext): T {\n if (typeof input === \"string\") {\n const tokenMatch = input.match(/^\\$\\{([^}]+)\\}$/);\n if (tokenMatch) {\n const resolved = resolveToken(tokenMatch[1], ctx);\n if (resolved.raw) {\n return resolved.value as T;\n }\n }\n return interpolateString(input, ctx) as T;\n }\n\n if (Array.isArray(input)) {\n return input.map((item) => interpolateDeep(item, ctx)) as T;\n }\n\n if (input && typeof input === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input as Record<string, any>)) {\n out[key] = interpolateDeep(value, ctx);\n }\n return out as T;\n }\n\n return input;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=path-resolution.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-resolution.spec.d.ts","sourceRoot":"","sources":["../../../src/testing/core/path-resolution.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const interpolation_1 = require("./interpolation");
4
+ const path_segments_1 = require("./path-segments");
5
+ const resource_store_1 = require("./resource-store");
6
+ function makeCtx(overrides = {}) {
7
+ return {
8
+ scenarioId: "spec",
9
+ scenarioType: "api",
10
+ params: {},
11
+ resources: new resource_store_1.SimpleResourceStore(),
12
+ reporter: { onScenarioStart: () => { }, onStep: () => { }, onScenarioEnd: () => { } },
13
+ ...overrides,
14
+ };
15
+ }
16
+ describe("parsePathSegments", () => {
17
+ it("treats dot and bracket notation as equivalent", () => {
18
+ expect((0, path_segments_1.parsePathSegments)("a.b.0.c")).toEqual(["a", "b", "0", "c"]);
19
+ expect((0, path_segments_1.parsePathSegments)("a.b[0].c")).toEqual(["a", "b", "0", "c"]);
20
+ expect((0, path_segments_1.parsePathSegments)('a["b"].c')).toEqual(["a", "b", "c"]);
21
+ });
22
+ it("keeps quoted keys containing spaces and punctuation intact", () => {
23
+ expect((0, path_segments_1.parsePathSegments)('book["Clean Code"].title')).toEqual(["book", "Clean Code", "title"]);
24
+ expect((0, path_segments_1.parsePathSegments)("book['a.b'].title")).toEqual(["book", "a.b", "title"]);
25
+ });
26
+ it("returns no segments for an empty path", () => {
27
+ expect((0, path_segments_1.parsePathSegments)("")).toEqual([]);
28
+ });
29
+ });
30
+ describe("SimpleResourceStore", () => {
31
+ function seeded() {
32
+ const store = new resource_store_1.SimpleResourceStore();
33
+ store.set("createBook", {
34
+ status: 201,
35
+ bodyText: '{"data":{"title":"Clean Code"}}',
36
+ bodyJson: { data: { title: "Clean Code" }, result: [{ id: 7 }, { id: 8 }] },
37
+ });
38
+ return store;
39
+ }
40
+ it("resolves dot paths", () => {
41
+ expect(seeded().get("createBook.bodyJson.data.title")).toBe("Clean Code");
42
+ expect(seeded().get("createBook.status")).toBe(201);
43
+ });
44
+ it("resolves array elements by numeric segment", () => {
45
+ expect(seeded().get("createBook.bodyJson.result.0.id")).toBe(7);
46
+ });
47
+ it("resolves array elements by bracket index, equivalently", () => {
48
+ const store = seeded();
49
+ expect(store.get("createBook.bodyJson.result[0].id")).toBe(7);
50
+ expect(store.get("createBook.bodyJson.result[1].id")).toBe(8);
51
+ expect(store.get("createBook.bodyJson.result[0].id")).toBe(store.get("createBook.bodyJson.result.0.id"));
52
+ });
53
+ it("returns undefined for a missing path rather than throwing", () => {
54
+ expect(seeded().get("createBook.bodyJson.nope.deeper")).toBeUndefined();
55
+ expect(seeded().get("neverSaved")).toBeUndefined();
56
+ });
57
+ it("round-trips: anything readable by bracket path is also writable by one", () => {
58
+ const store = new resource_store_1.SimpleResourceStore();
59
+ store.set('a["b c"].d', 42);
60
+ expect(store.get('a["b c"].d')).toBe(42);
61
+ expect(store.get("a.b c.d")).toBe(42);
62
+ });
63
+ it("has() follows get()", () => {
64
+ const store = seeded();
65
+ expect(store.has("createBook.bodyJson.result[0].id")).toBe(true);
66
+ expect(store.has("createBook.bodyJson.result[9].id")).toBe(false);
67
+ });
68
+ });
69
+ describe("interpolation", () => {
70
+ it("stringifies non-string ${res:...} values", () => {
71
+ const ctx = makeCtx();
72
+ ctx.resources.set("r", { status: 200, bodyJson: { id: 1 } });
73
+ expect((0, interpolation_1.interpolateDeep)("${res:r}", ctx)).toBe('{"status":200,"bodyJson":{"id":1}}');
74
+ expect((0, interpolation_1.interpolateDeep)("${res:r.bodyJson.id}", ctx)).toBe("1");
75
+ });
76
+ it("resolves ${res:...} bracket paths", () => {
77
+ const ctx = makeCtx();
78
+ ctx.resources.set("r", { bodyJson: { result: [{ id: 7 }] } });
79
+ expect((0, interpolation_1.interpolateDeep)("${res:r.bodyJson.result[0].id}", ctx)).toBe("7");
80
+ expect((0, interpolation_1.interpolateDeep)("${res:r.bodyJson.result.0.id}", ctx)).toBe("7");
81
+ });
82
+ it("preserves the raw record for ${data:..._rec} only when it is the entire value", () => {
83
+ const ctx = makeCtx({ testData: { book: { CleanCode: { title: "Clean Code", pages: 464 } } } });
84
+ expect((0, interpolation_1.interpolateDeep)('${data:book["CleanCode"]._rec}', ctx)).toEqual({
85
+ title: "Clean Code",
86
+ pages: 464,
87
+ });
88
+ expect((0, interpolation_1.interpolateDeep)('x ${data:book["CleanCode"]._rec}', ctx)).toBe('x {"title":"Clean Code","pages":464}');
89
+ expect((0, interpolation_1.interpolateDeep)('${data:book["CleanCode"].title}', ctx)).toBe("Clean Code");
90
+ });
91
+ it("resolves ${secret:...} and keeps it out of nothing else", () => {
92
+ const ctx = makeCtx({ secrets: { "test-username": "alice" } });
93
+ expect((0, interpolation_1.interpolateDeep)("${secret:test-username}", ctx)).toBe("alice");
94
+ expect(() => (0, interpolation_1.interpolateString)("${secret:absent}", ctx)).toThrow(/Missing secret/);
95
+ });
96
+ it("throws a named error for an unsaved resource rather than yielding undefined", () => {
97
+ expect(() => (0, interpolation_1.interpolateString)("${res:neverSaved.id}", makeCtx())).toThrow(/Missing resource/);
98
+ });
99
+ it("recurses through objects and arrays", () => {
100
+ const ctx = makeCtx({ params: { n: "1" } });
101
+ expect((0, interpolation_1.interpolateDeep)({ a: ["${params.n}", { b: "${params.n}" }] }, ctx)).toEqual({
102
+ a: ["1", { b: "1" }],
103
+ });
104
+ });
105
+ });
106
+ //# sourceMappingURL=path-resolution.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-resolution.spec.js","sourceRoot":"","sources":["../../../src/testing/core/path-resolution.spec.ts"],"names":[],"mappings":"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","sourcesContent":["// Purpose: lock down lookup-path parsing and token interpolation — the behaviours that decide\n// whether a scenario file's `${res:...}` / `${data:...}` references resolve or silently vanish.\n\nimport type { TestContext } from \"../contracts/runtime-context.types\";\nimport { interpolateDeep, interpolateString } from \"./interpolation\";\nimport { parsePathSegments } from \"./path-segments\";\nimport { SimpleResourceStore } from \"./resource-store\";\n\nfunction makeCtx(overrides: Partial<TestContext> = {}): TestContext {\n return {\n scenarioId: \"spec\",\n scenarioType: \"api\",\n params: {},\n resources: new SimpleResourceStore(),\n reporter: { onScenarioStart: () => {}, onStep: () => {}, onScenarioEnd: () => {} } as any,\n ...overrides,\n } as TestContext;\n}\n\ndescribe(\"parsePathSegments\", () => {\n it(\"treats dot and bracket notation as equivalent\", () => {\n expect(parsePathSegments(\"a.b.0.c\")).toEqual([\"a\", \"b\", \"0\", \"c\"]);\n expect(parsePathSegments(\"a.b[0].c\")).toEqual([\"a\", \"b\", \"0\", \"c\"]);\n expect(parsePathSegments('a[\"b\"].c')).toEqual([\"a\", \"b\", \"c\"]);\n });\n\n it(\"keeps quoted keys containing spaces and punctuation intact\", () => {\n expect(parsePathSegments('book[\"Clean Code\"].title')).toEqual([\"book\", \"Clean Code\", \"title\"]);\n expect(parsePathSegments(\"book['a.b'].title\")).toEqual([\"book\", \"a.b\", \"title\"]);\n });\n\n it(\"returns no segments for an empty path\", () => {\n expect(parsePathSegments(\"\")).toEqual([]);\n });\n});\n\ndescribe(\"SimpleResourceStore\", () => {\n function seeded() {\n const store = new SimpleResourceStore();\n store.set(\"createBook\", {\n status: 201,\n bodyText: '{\"data\":{\"title\":\"Clean Code\"}}',\n bodyJson: { data: { title: \"Clean Code\" }, result: [{ id: 7 }, { id: 8 }] },\n });\n return store;\n }\n\n it(\"resolves dot paths\", () => {\n expect(seeded().get(\"createBook.bodyJson.data.title\")).toBe(\"Clean Code\");\n expect(seeded().get(\"createBook.status\")).toBe(201);\n });\n\n it(\"resolves array elements by numeric segment\", () => {\n expect(seeded().get(\"createBook.bodyJson.result.0.id\")).toBe(7);\n });\n\n it(\"resolves array elements by bracket index, equivalently\", () => {\n const store = seeded();\n expect(store.get(\"createBook.bodyJson.result[0].id\")).toBe(7);\n expect(store.get(\"createBook.bodyJson.result[1].id\")).toBe(8);\n // the two spellings must agree — this asymmetry with ${data:...} was the original bug\n expect(store.get(\"createBook.bodyJson.result[0].id\")).toBe(\n store.get(\"createBook.bodyJson.result.0.id\"),\n );\n });\n\n it(\"returns undefined for a missing path rather than throwing\", () => {\n expect(seeded().get(\"createBook.bodyJson.nope.deeper\")).toBeUndefined();\n expect(seeded().get(\"neverSaved\")).toBeUndefined();\n });\n\n it(\"round-trips: anything readable by bracket path is also writable by one\", () => {\n const store = new SimpleResourceStore();\n store.set('a[\"b c\"].d', 42);\n expect(store.get('a[\"b c\"].d')).toBe(42);\n expect(store.get(\"a.b c.d\")).toBe(42);\n });\n\n it(\"has() follows get()\", () => {\n const store = seeded();\n expect(store.has(\"createBook.bodyJson.result[0].id\")).toBe(true);\n expect(store.has(\"createBook.bodyJson.result[9].id\")).toBe(false);\n });\n});\n\ndescribe(\"interpolation\", () => {\n it(\"stringifies non-string ${res:...} values\", () => {\n const ctx = makeCtx();\n ctx.resources.set(\"r\", { status: 200, bodyJson: { id: 1 } });\n // the whole object flattens to text — which is why assert.httpStatus `from` and\n // assert.jsonPath `from` can never receive a live response object\n expect(interpolateDeep(\"${res:r}\", ctx)).toBe('{\"status\":200,\"bodyJson\":{\"id\":1}}');\n // a scalar leaf survives intact, because the store walks the object before flattening\n expect(interpolateDeep(\"${res:r.bodyJson.id}\", ctx)).toBe(\"1\");\n });\n\n it(\"resolves ${res:...} bracket paths\", () => {\n const ctx = makeCtx();\n ctx.resources.set(\"r\", { bodyJson: { result: [{ id: 7 }] } });\n expect(interpolateDeep(\"${res:r.bodyJson.result[0].id}\", ctx)).toBe(\"7\");\n expect(interpolateDeep(\"${res:r.bodyJson.result.0.id}\", ctx)).toBe(\"7\");\n });\n\n it(\"preserves the raw record for ${data:..._rec} only when it is the entire value\", () => {\n const ctx = makeCtx({ testData: { book: { CleanCode: { title: \"Clean Code\", pages: 464 } } } });\n expect(interpolateDeep('${data:book[\"CleanCode\"]._rec}', ctx)).toEqual({\n title: \"Clean Code\",\n pages: 464,\n });\n // embedded in a larger string it has to flatten, since the result must stay a string\n expect(interpolateDeep('x ${data:book[\"CleanCode\"]._rec}', ctx)).toBe(\n 'x {\"title\":\"Clean Code\",\"pages\":464}',\n );\n expect(interpolateDeep('${data:book[\"CleanCode\"].title}', ctx)).toBe(\"Clean Code\");\n });\n\n it(\"resolves ${secret:...} and keeps it out of nothing else\", () => {\n const ctx = makeCtx({ secrets: { \"test-username\": \"alice\" } });\n expect(interpolateDeep(\"${secret:test-username}\", ctx)).toBe(\"alice\");\n expect(() => interpolateString(\"${secret:absent}\", ctx)).toThrow(/Missing secret/);\n });\n\n it(\"throws a named error for an unsaved resource rather than yielding undefined\", () => {\n expect(() => interpolateString(\"${res:neverSaved.id}\", makeCtx())).toThrow(/Missing resource/);\n });\n\n it(\"recurses through objects and arrays\", () => {\n const ctx = makeCtx({ params: { n: \"1\" } });\n expect(interpolateDeep({ a: [\"${params.n}\", { b: \"${params.n}\" }] }, ctx)).toEqual({\n a: [\"1\", { b: \"1\" }],\n });\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function parsePathSegments(path: string): string[];
2
+ //# sourceMappingURL=path-segments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-segments.d.ts","sourceRoot":"","sources":["../../../src/testing/core/path-segments.ts"],"names":[],"mappings":"AAcA,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkExD"}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePathSegments = parsePathSegments;
4
+ function parsePathSegments(path) {
5
+ const segments = [];
6
+ let buffer = "";
7
+ let i = 0;
8
+ const pushBuffer = () => {
9
+ if (buffer) {
10
+ segments.push(buffer);
11
+ buffer = "";
12
+ }
13
+ };
14
+ while (i < path.length) {
15
+ const ch = path[i];
16
+ if (ch === ".") {
17
+ pushBuffer();
18
+ i += 1;
19
+ continue;
20
+ }
21
+ if (ch === "[") {
22
+ pushBuffer();
23
+ i += 1;
24
+ if (i >= path.length)
25
+ break;
26
+ let quote = "";
27
+ if (path[i] === '"' || path[i] === "'") {
28
+ quote = path[i];
29
+ i += 1;
30
+ }
31
+ let value = "";
32
+ while (i < path.length) {
33
+ const c = path[i];
34
+ if (quote) {
35
+ if (c === "\\" && i + 1 < path.length) {
36
+ value += path[i + 1];
37
+ i += 2;
38
+ continue;
39
+ }
40
+ if (c === quote) {
41
+ i += 1;
42
+ break;
43
+ }
44
+ value += c;
45
+ i += 1;
46
+ continue;
47
+ }
48
+ if (c === "]")
49
+ break;
50
+ value += c;
51
+ i += 1;
52
+ }
53
+ while (i < path.length && path[i] !== "]") {
54
+ i += 1;
55
+ }
56
+ if (i < path.length && path[i] === "]") {
57
+ i += 1;
58
+ }
59
+ if (value) {
60
+ segments.push(value);
61
+ }
62
+ continue;
63
+ }
64
+ buffer += ch;
65
+ i += 1;
66
+ }
67
+ pushBuffer();
68
+ return segments;
69
+ }
70
+ //# sourceMappingURL=path-segments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-segments.js","sourceRoot":"","sources":["../../../src/testing/core/path-segments.ts"],"names":[],"mappings":";;AAcA,8CAkEC;AAlED,SAAgB,iBAAiB,CAAC,IAAY;IAC5C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,IAAI,MAAM,EAAE,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACtB,MAAM,GAAG,EAAE,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACnB,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,UAAU,EAAE,CAAC;YACb,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,UAAU,EAAE,CAAC;YACb,CAAC,IAAI,CAAC,CAAC;YACP,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM;gBAAE,MAAM;YAC5B,IAAI,KAAK,GAAG,EAAE,CAAC;YACf,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACvC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBAChB,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YACD,IAAI,KAAK,GAAG,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBACvB,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClB,IAAI,KAAK,EAAE,CAAC;oBACV,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;wBACtC,KAAK,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;wBACrB,CAAC,IAAI,CAAC,CAAC;wBACP,SAAS;oBACX,CAAC;oBACD,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;wBAChB,CAAC,IAAI,CAAC,CAAC;wBACP,MAAM;oBACR,CAAC;oBACD,KAAK,IAAI,CAAC,CAAC;oBACX,CAAC,IAAI,CAAC,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,IAAI,CAAC,KAAK,GAAG;oBAAE,MAAM;gBACrB,KAAK,IAAI,CAAC,CAAC;gBACX,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YACD,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC1C,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YACD,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACvC,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YACD,IAAI,KAAK,EAAE,CAAC;gBACV,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;QACb,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IAED,UAAU,EAAE,CAAC;IACb,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["// Purpose: shared lookup-path parsing for interpolation tokens and the resource store.\n\n/**\n * Split a lookup path into segments, accepting dot and bracket notation interchangeably:\n * `a.b.0.c`, `a.b[0].c` and `a[\"b\"].c` all yield the same segments.\n *\n * Both `${data:...}` and `${res:...}` resolve through this, so the two token families share one\n * path syntax. Previously only `${data:...}` parsed brackets while the resource store split on\n * \".\" alone, which meant `${res:r.bodyJson.result[0].id}` silently resolved to nothing while the\n * `${data:...}` form right next to it worked — the asymmetry was a standing authoring trap.\n *\n * Bracket contents may be quoted (`[\"Clean Code\"]`) for keys containing spaces or punctuation;\n * backslash escapes inside quotes are honoured. Empty brackets contribute no segment.\n */\nexport function parsePathSegments(path: string): string[] {\n const segments: string[] = [];\n let buffer = \"\";\n let i = 0;\n\n const pushBuffer = () => {\n if (buffer) {\n segments.push(buffer);\n buffer = \"\";\n }\n };\n\n while (i < path.length) {\n const ch = path[i];\n if (ch === \".\") {\n pushBuffer();\n i += 1;\n continue;\n }\n if (ch === \"[\") {\n pushBuffer();\n i += 1;\n if (i >= path.length) break;\n let quote = \"\";\n if (path[i] === '\"' || path[i] === \"'\") {\n quote = path[i];\n i += 1;\n }\n let value = \"\";\n while (i < path.length) {\n const c = path[i];\n if (quote) {\n if (c === \"\\\\\" && i + 1 < path.length) {\n value += path[i + 1];\n i += 2;\n continue;\n }\n if (c === quote) {\n i += 1;\n break;\n }\n value += c;\n i += 1;\n continue;\n }\n if (c === \"]\") break;\n value += c;\n i += 1;\n }\n while (i < path.length && path[i] !== \"]\") {\n i += 1;\n }\n if (i < path.length && path[i] === \"]\") {\n i += 1;\n }\n if (value) {\n segments.push(value);\n }\n continue;\n }\n buffer += ch;\n i += 1;\n }\n\n pushBuffer();\n return segments;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"resource-store.d.ts","sourceRoot":"","sources":["../../../src/testing/core/resource-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAEhD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAW1B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAkBvC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAG3B"}
1
+ {"version":3,"file":"resource-store.d.ts","sourceRoot":"","sources":["../../../src/testing/core/resource-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAGxE,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAEhD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAc1B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAoBvC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAG3B"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SimpleResourceStore = void 0;
4
+ const path_segments_1 = require("./path-segments");
4
5
  class SimpleResourceStore {
5
6
  constructor() {
6
7
  this.data = {};
@@ -8,7 +9,7 @@ class SimpleResourceStore {
8
9
  get(path) {
9
10
  if (!path)
10
11
  return undefined;
11
- const parts = path.split(".");
12
+ const parts = (0, path_segments_1.parsePathSegments)(path);
12
13
  let current = this.data;
13
14
  for (const part of parts) {
14
15
  if (current == null || typeof current !== "object")
@@ -20,7 +21,9 @@ class SimpleResourceStore {
20
21
  set(path, value) {
21
22
  if (!path)
22
23
  return;
23
- const parts = path.split(".");
24
+ const parts = (0, path_segments_1.parsePathSegments)(path);
25
+ if (!parts.length)
26
+ return;
24
27
  let current = this.data;
25
28
  for (let i = 0; i < parts.length - 1; i += 1) {
26
29
  const part = parts[i];
@@ -1 +1 @@
1
- {"version":3,"file":"resource-store.js","sourceRoot":"","sources":["../../../src/testing/core/resource-store.ts"],"names":[],"mappings":";;;AAEA,MAAa,mBAAmB;IAAhC;QACmB,SAAI,GAAwB,EAAE,CAAC;IAkClD,CAAC;IAhCC,GAAG,CAAC,IAAY;QACd,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,OAAO,GAAQ,IAAI,CAAC,IAAI,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAC;YACrE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,IAAY,EAAE,KAAc;QAC9B,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,OAAO,GAAQ,IAAI,CAAC,IAAI,CAAC;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,IACE,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI;gBACrB,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,QAAQ;gBACjC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAC5B,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACrB,CAAC;YACD,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;IACtC,CAAC;CACF;AAnCD,kDAmCC","sourcesContent":["import type { ResourceStore } from \"../contracts/runtime-context.types\";\n\nexport class SimpleResourceStore implements ResourceStore {\n private readonly data: Record<string, any> = {};\n\n get(path: string): unknown {\n if (!path) return undefined;\n const parts = path.split(\".\");\n let current: any = this.data;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = current[part];\n }\n return current;\n }\n\n set(path: string, value: unknown): void {\n if (!path) return;\n const parts = path.split(\".\");\n let current: any = this.data;\n for (let i = 0; i < parts.length - 1; i += 1) {\n const part = parts[i];\n if (\n current[part] == null ||\n typeof current[part] !== \"object\" ||\n Array.isArray(current[part])\n ) {\n current[part] = {};\n }\n current = current[part];\n }\n current[parts[parts.length - 1]] = value;\n }\n\n has(path: string): boolean {\n return this.get(path) !== undefined;\n }\n}\n"]}
1
+ {"version":3,"file":"resource-store.js","sourceRoot":"","sources":["../../../src/testing/core/resource-store.ts"],"names":[],"mappings":";;;AACA,mDAAoD;AAEpD,MAAa,mBAAmB;IAAhC;QACmB,SAAI,GAAwB,EAAE,CAAC;IAuClD,CAAC;IArCC,GAAG,CAAC,IAAY;QACd,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAC;QAI5B,MAAM,KAAK,GAAG,IAAA,iCAAiB,EAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,GAAQ,IAAI,CAAC,IAAI,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAC;YACrE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,IAAY,EAAE,KAAc;QAC9B,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,MAAM,KAAK,GAAG,IAAA,iCAAiB,EAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO;QAC1B,IAAI,OAAO,GAAQ,IAAI,CAAC,IAAI,CAAC;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,IACE,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI;gBACrB,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,QAAQ;gBACjC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAC5B,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACrB,CAAC;YACD,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;IACtC,CAAC;CACF;AAxCD,kDAwCC","sourcesContent":["import type { ResourceStore } from \"../contracts/runtime-context.types\";\nimport { parsePathSegments } from \"./path-segments\";\n\nexport class SimpleResourceStore implements ResourceStore {\n private readonly data: Record<string, any> = {};\n\n get(path: string): unknown {\n if (!path) return undefined;\n // Dot and bracket notation both resolve here, so `${res:r.bodyJson.result[0].id}` and\n // `${res:r.bodyJson.result.0.id}` are equivalent — matching `${data:...}`, which has always\n // accepted brackets. `has()` delegates to this, so it follows automatically.\n const parts = parsePathSegments(path);\n let current: any = this.data;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = current[part];\n }\n return current;\n }\n\n set(path: string, value: unknown): void {\n if (!path) return;\n // Parsed the same way as `get`, so any path that can be read back can also be written.\n const parts = parsePathSegments(path);\n if (!parts.length) return;\n let current: any = this.data;\n for (let i = 0; i < parts.length - 1; i += 1) {\n const part = parts[i];\n if (\n current[part] == null ||\n typeof current[part] !== \"object\" ||\n Array.isArray(current[part])\n ) {\n current[part] = {};\n }\n current = current[part];\n }\n current[parts[parts.length - 1]] = value;\n }\n\n has(path: string): boolean {\n return this.get(path) !== undefined;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"testing-engine.d.ts","sourceRoot":"","sources":["../../../src/testing/core/testing-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAGhF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA2C;gBAGnE,QAAQ,EAAE,YAAY,EACtB,QAAQ,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAM/C,WAAW,CACf,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,cAAc,GAAG,QAAQ,CAAC,GACnE,OAAO,CAAC,IAAI,CAAC;YA0DF,QAAQ;CA8CvB"}
1
+ {"version":3,"file":"testing-engine.d.ts","sourceRoot":"","sources":["../../../src/testing/core/testing-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAGhF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA2C;gBAGnE,QAAQ,EAAE,YAAY,EACtB,QAAQ,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAM/C,WAAW,CACf,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,cAAc,GAAG,QAAQ,CAAC,GACnE,OAAO,CAAC,IAAI,CAAC;YAgFF,QAAQ;CA8CvB"}
@@ -24,6 +24,7 @@ class TestingEngine {
24
24
  const scenarioStart = Date.now();
25
25
  let scenarioError;
26
26
  reporter.onScenarioStart(scenario);
27
+ ctx.ui?.resetCapture();
27
28
  try {
28
29
  const execute = async () => {
29
30
  for (const block of scenario.steps) {
@@ -42,6 +43,21 @@ class TestingEngine {
42
43
  scenarioError = err;
43
44
  }
44
45
  finally {
46
+ if (scenarioError && ctx.ui?.page && reporter.attach) {
47
+ try {
48
+ const artifacts = await ctx.ui.collectFailureArtifacts();
49
+ for (const a of artifacts) {
50
+ reporter.attach({
51
+ scenarioId: ctx.scenarioId,
52
+ name: a.name,
53
+ contentType: a.contentType,
54
+ data: a.data,
55
+ });
56
+ }
57
+ }
58
+ catch {
59
+ }
60
+ }
45
61
  const durationMs = Date.now() - scenarioStart;
46
62
  reporter.onScenarioEnd(scenario, {
47
63
  ok: !scenarioError,
@@ -1 +1 @@
1
- {"version":3,"file":"testing-engine.js","sourceRoot":"","sources":["../../../src/testing/core/testing-engine.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TestContext, StepPhase } from \"../contracts/runtime-context.types\";\nimport type { OpStep, ScenarioSpec } from \"../contracts/testing-metadata.types\";\nimport { interpolateDeep } from \"./interpolation\";\nimport { normalizeBlock } from \"./normalize-steps\";\nimport { StepRegistry } from \"./step-registry\";\nimport { withTimeout } from \"./timeout\";\n\nexport class TestingEngine {\n private readonly registry: StepRegistry;\n private readonly defaults?: { timeoutMs?: number; retries?: number };\n\n constructor(\n registry: StepRegistry,\n defaults?: { timeoutMs?: number; retries?: number },\n ) {\n this.registry = registry;\n this.defaults = defaults;\n }\n\n async runScenario(\n scenario: ScenarioSpec,\n ctxBase: Omit<TestContext, \"scenarioId\" | \"scenarioType\" | \"params\">,\n ): Promise<void> {\n const retries = scenario.retries ?? this.defaults?.retries ?? 0;\n const maxAttempts = Math.max(0, retries) + 1;\n const scenarioTimeoutMs =\n scenario.timeoutMs ?? this.defaults?.timeoutMs;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n const ctx: TestContext = {\n ...ctxBase,\n scenarioId: scenario.id,\n scenarioType: scenario.type,\n params: scenario.params ?? {},\n };\n\n const reporter = ctx.reporter;\n const scenarioStart = Date.now();\n let scenarioError: unknown;\n\n reporter.onScenarioStart(scenario);\n\n try {\n const execute = async () => {\n for (const block of scenario.steps) {\n const normalized = normalizeBlock(block);\n await this.runBlock(normalized.phase, normalized.steps, ctx);\n }\n };\n\n if (scenarioTimeoutMs !== undefined) {\n await withTimeout(\n execute(),\n scenarioTimeoutMs,\n `Scenario timeout after ${scenarioTimeoutMs}ms: \"${scenario.id}\"`,\n );\n } else {\n await execute();\n }\n } catch (err: any) {\n scenarioError = err;\n } finally {\n const durationMs = Date.now() - scenarioStart;\n reporter.onScenarioEnd(scenario, {\n ok: !scenarioError,\n error: scenarioError,\n durationMs,\n });\n }\n\n if (!scenarioError) {\n return;\n }\n\n if (attempt >= maxAttempts) {\n throw scenarioError;\n }\n }\n }\n\n private async runBlock(\n phase: StepPhase,\n steps: OpStep[],\n ctx: TestContext,\n ): Promise<void> {\n for (const step of steps) {\n const reporter = ctx.reporter;\n const stepStart = Date.now();\n let stepError: unknown;\n let resolvedStep: OpStep | undefined;\n\n reporter.onStepStart({ scenarioId: ctx.scenarioId, phase, step });\n\n try {\n resolvedStep = interpolateDeep(step, ctx) as OpStep;\n const handler = this.registry.get(resolvedStep.op);\n const run = handler(ctx, resolvedStep);\n const result =\n resolvedStep.timeoutMs !== undefined\n ? await withTimeout(\n run,\n resolvedStep.timeoutMs,\n `Step timeout after ${resolvedStep.timeoutMs}ms: \"${resolvedStep.op}\"`,\n )\n : await run;\n\n if (resolvedStep.saveAs) {\n // console.log(`Step ${resolvedStep.name} attempting to saveAs ${resolvedStep.saveAs}`, JSON.stringify(result));\n ctx.resources.set(resolvedStep.saveAs, result);\n }\n } catch (err: any) {\n stepError = err;\n throw err;\n } finally {\n const durationMs = Date.now() - stepStart;\n reporter.onStepEnd({\n scenarioId: ctx.scenarioId,\n phase,\n step: resolvedStep ?? step,\n ok: !stepError,\n error: stepError,\n durationMs,\n });\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"testing-engine.js","sourceRoot":"","sources":["../../../src/testing/core/testing-engine.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TestContext, StepPhase } from \"../contracts/runtime-context.types\";\nimport type { OpStep, ScenarioSpec } from \"../contracts/testing-metadata.types\";\nimport { interpolateDeep } from \"./interpolation\";\nimport { normalizeBlock } from \"./normalize-steps\";\nimport { StepRegistry } from \"./step-registry\";\nimport { withTimeout } from \"./timeout\";\n\nexport class TestingEngine {\n private readonly registry: StepRegistry;\n private readonly defaults?: { timeoutMs?: number; retries?: number };\n\n constructor(\n registry: StepRegistry,\n defaults?: { timeoutMs?: number; retries?: number },\n ) {\n this.registry = registry;\n this.defaults = defaults;\n }\n\n async runScenario(\n scenario: ScenarioSpec,\n ctxBase: Omit<TestContext, \"scenarioId\" | \"scenarioType\" | \"params\">,\n ): Promise<void> {\n const retries = scenario.retries ?? this.defaults?.retries ?? 0;\n const maxAttempts = Math.max(0, retries) + 1;\n const scenarioTimeoutMs =\n scenario.timeoutMs ?? this.defaults?.timeoutMs;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n const ctx: TestContext = {\n ...ctxBase,\n scenarioId: scenario.id,\n scenarioType: scenario.type,\n params: scenario.params ?? {},\n };\n\n const reporter = ctx.reporter;\n const scenarioStart = Date.now();\n let scenarioError: unknown;\n\n reporter.onScenarioStart(scenario);\n // Start a clean capture window for this scenario (UI adapter buffers console/network\n // events; they're only flushed to the reporter if the scenario fails below).\n ctx.ui?.resetCapture();\n\n try {\n const execute = async () => {\n for (const block of scenario.steps) {\n const normalized = normalizeBlock(block);\n await this.runBlock(normalized.phase, normalized.steps, ctx);\n }\n };\n\n if (scenarioTimeoutMs !== undefined) {\n await withTimeout(\n execute(),\n scenarioTimeoutMs,\n `Scenario timeout after ${scenarioTimeoutMs}ms: \"${scenario.id}\"`,\n );\n } else {\n await execute();\n }\n } catch (err: any) {\n scenarioError = err;\n } finally {\n // On failure, flush the UI adapter's captured artifacts (console + network) BEFORE\n // onScenarioEnd — the lifecycle reporter snapshots the attached artifacts inside\n // onScenarioEnd. No page (e.g. API scenarios) → nothing captured.\n if (scenarioError && ctx.ui?.page && reporter.attach) {\n try {\n const artifacts = await ctx.ui.collectFailureArtifacts();\n for (const a of artifacts) {\n reporter.attach({\n scenarioId: ctx.scenarioId,\n name: a.name,\n contentType: a.contentType,\n data: a.data,\n });\n }\n } catch {\n // Artifact capture is best-effort; never let it mask the scenario error.\n }\n }\n\n const durationMs = Date.now() - scenarioStart;\n reporter.onScenarioEnd(scenario, {\n ok: !scenarioError,\n error: scenarioError,\n durationMs,\n });\n }\n\n if (!scenarioError) {\n return;\n }\n\n if (attempt >= maxAttempts) {\n throw scenarioError;\n }\n }\n }\n\n private async runBlock(\n phase: StepPhase,\n steps: OpStep[],\n ctx: TestContext,\n ): Promise<void> {\n for (const step of steps) {\n const reporter = ctx.reporter;\n const stepStart = Date.now();\n let stepError: unknown;\n let resolvedStep: OpStep | undefined;\n\n reporter.onStepStart({ scenarioId: ctx.scenarioId, phase, step });\n\n try {\n resolvedStep = interpolateDeep(step, ctx) as OpStep;\n const handler = this.registry.get(resolvedStep.op);\n const run = handler(ctx, resolvedStep);\n const result =\n resolvedStep.timeoutMs !== undefined\n ? await withTimeout(\n run,\n resolvedStep.timeoutMs,\n `Step timeout after ${resolvedStep.timeoutMs}ms: \"${resolvedStep.op}\"`,\n )\n : await run;\n\n if (resolvedStep.saveAs) {\n // console.log(`Step ${resolvedStep.name} attempting to saveAs ${resolvedStep.saveAs}`, JSON.stringify(result));\n ctx.resources.set(resolvedStep.saveAs, result);\n }\n } catch (err: any) {\n stepError = err;\n throw err;\n } finally {\n const durationMs = Date.now() - stepStart;\n reporter.onStepEnd({\n scenarioId: ctx.scenarioId,\n phase,\n step: resolvedStep ?? step,\n ok: !stepError,\n error: stepError,\n durationMs,\n });\n }\n }\n }\n}\n"]}
@@ -0,0 +1,14 @@
1
+ export interface IArtifactSink {
2
+ put(args: {
3
+ runId: string;
4
+ scenarioId: string;
5
+ name: string;
6
+ contentType: string;
7
+ data: Buffer | NodeJS.ReadableStream;
8
+ }): Promise<{
9
+ storageKey: string;
10
+ url: string;
11
+ sizeBytes: number;
12
+ }>;
13
+ }
14
+ //# sourceMappingURL=artifact-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-sink.d.ts","sourceRoot":"","sources":["../../../src/testing/reporter/artifact-sink.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,IAAI,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC;KACtC,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrE"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=artifact-sink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-sink.js","sourceRoot":"","sources":["../../../src/testing/reporter/artifact-sink.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Contract for persisting binary test artifacts (screenshots, video, HAR) to shared\n * storage and returning a reference. The concrete {@link FileServiceArtifactSink}\n * (backed by the core FILE_SERVICE) is added in Phase 2 (A6). Kept as a standalone\n * interface so {@link LifecycleWebhookReporter} can depend on it without pulling in\n * Nest DI.\n */\nexport interface IArtifactSink {\n put(args: {\n runId: string;\n scenarioId: string;\n name: string;\n contentType: string;\n data: Buffer | NodeJS.ReadableStream;\n }): Promise<{ storageKey: string; url: string; sizeBytes: number }>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"console-reporter.d.ts","sourceRoot":"","sources":["../../../src/testing/reporter/console-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAgJlE,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,eAAe,CAAK;IAE5B,UAAU,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAI1C,eAAe,CAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAK9D,aAAa,CACX,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EACvC,MAAM,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC3D,IAAI;IAYP,WAAW,CAAC,IAAI,EAAE;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,IAAI;IAMR,SAAS,CAAC,IAAI,EAAE;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI;IAwBR,YAAY,CAAC,IAAI,EAAE;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;SAAE,CAAC;KACxD,GAAG,IAAI;IAQR,MAAM,CAAC,IAAI,EAAE;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;KACvB,GAAG,IAAI;IAQR,QAAQ,CAAC,IAAI,EAAE;QACb,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI;CAUT"}
1
+ {"version":3,"file":"console-reporter.d.ts","sourceRoot":"","sources":["../../../src/testing/reporter/console-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAgJlE,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,eAAe,CAAK;IAE5B,UAAU,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAI1C,eAAe,CAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAK9D,aAAa,CACX,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EACvC,MAAM,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC3D,IAAI;IAYP,WAAW,CAAC,IAAI,EAAE;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,IAAI;IAMR,SAAS,CAAC,IAAI,EAAE;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI;IAwBR,YAAY,CAAC,IAAI,EAAE;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;SAAE,CAAC;KACxD,GAAG,IAAI;IAQR,MAAM,CAAC,IAAI,EAAE;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;KACvB,GAAG,IAAI;IAiBR,QAAQ,CAAC,IAAI,EAAE;QACb,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI;CAUT"}
@@ -193,6 +193,13 @@ class ConsoleReporter {
193
193
  attach(args) {
194
194
  const header = `ATTACH ${args.name} (${args.contentType})`;
195
195
  console.log(indentLines(header, `${STEP_INDENT}${INDENT}`));
196
+ const ct = args.contentType.toLowerCase();
197
+ const isBinary = Buffer.isBuffer(args.data) &&
198
+ (ct.startsWith("image/") || ct.startsWith("video/") || ct === "application/octet-stream");
199
+ if (isBinary) {
200
+ console.log(indentLines(`<${args.data.byteLength} bytes>`, `${STEP_INDENT}${INDENT}${INDENT}`));
201
+ return;
202
+ }
196
203
  const dataText = Buffer.isBuffer(args.data) ? args.data.toString("utf8") : String(args.data ?? "");
197
204
  if (!dataText.length)
198
205
  return;