@sun-asterisk/sungen 3.2.9 → 3.2.10-beta.11

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 (162) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +5 -2
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/capabilities/registry.d.ts +9 -0
  5. package/dist/capabilities/registry.d.ts.map +1 -1
  6. package/dist/capabilities/registry.js +4 -0
  7. package/dist/capabilities/registry.js.map +1 -1
  8. package/dist/cli/commands/capability.d.ts.map +1 -1
  9. package/dist/cli/commands/capability.js +44 -2
  10. package/dist/cli/commands/capability.js.map +1 -1
  11. package/dist/cli/commands/delivery.d.ts.map +1 -1
  12. package/dist/cli/commands/delivery.js +15 -5
  13. package/dist/cli/commands/delivery.js.map +1 -1
  14. package/dist/cli/commands/generate.d.ts.map +1 -1
  15. package/dist/cli/commands/generate.js +16 -0
  16. package/dist/cli/commands/generate.js.map +1 -1
  17. package/dist/exporters/spec-parser.d.ts.map +1 -1
  18. package/dist/exporters/spec-parser.js +16 -5
  19. package/dist/exporters/spec-parser.js.map +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-row-column.hbs +2 -0
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
  32. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
  33. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
  34. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
  35. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
  36. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
  37. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
  38. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
  41. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
  42. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
  43. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
  44. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
  45. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
  46. package/dist/generators/test-generator/app-profile.d.ts +89 -0
  47. package/dist/generators/test-generator/app-profile.d.ts.map +1 -0
  48. package/dist/generators/test-generator/app-profile.js +157 -0
  49. package/dist/generators/test-generator/app-profile.js.map +1 -0
  50. package/dist/generators/test-generator/code-generator.d.ts +4 -0
  51. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  52. package/dist/generators/test-generator/code-generator.js +7 -0
  53. package/dist/generators/test-generator/code-generator.js.map +1 -1
  54. package/dist/generators/test-generator/diagnostics.d.ts +36 -0
  55. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -0
  56. package/dist/generators/test-generator/diagnostics.js +44 -0
  57. package/dist/generators/test-generator/diagnostics.js.map +1 -0
  58. package/dist/generators/test-generator/patterns/types.d.ts +12 -0
  59. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  60. package/dist/generators/test-generator/recipe-expander.d.ts +32 -0
  61. package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -0
  62. package/dist/generators/test-generator/recipe-expander.js +118 -0
  63. package/dist/generators/test-generator/recipe-expander.js.map +1 -0
  64. package/dist/generators/test-generator/step-mapper.d.ts +17 -0
  65. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  66. package/dist/generators/test-generator/step-mapper.js +70 -0
  67. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  68. package/dist/generators/test-generator/template-engine.d.ts +5 -1
  69. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  70. package/dist/generators/test-generator/template-engine.js +27 -3
  71. package/dist/generators/test-generator/template-engine.js.map +1 -1
  72. package/dist/generators/test-generator/utils/selector-resolver.d.ts +10 -1
  73. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  74. package/dist/generators/test-generator/utils/selector-resolver.js +32 -0
  75. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  76. package/dist/harness/capability.d.ts +1 -0
  77. package/dist/harness/capability.d.ts.map +1 -1
  78. package/dist/harness/capability.js.map +1 -1
  79. package/dist/harness/catalog/drivers.yaml +2 -1
  80. package/dist/harness/query-catalog.d.ts +32 -2
  81. package/dist/harness/query-catalog.d.ts.map +1 -1
  82. package/dist/harness/query-catalog.js +0 -0
  83. package/dist/harness/query-catalog.js.map +1 -1
  84. package/dist/index.d.ts +5 -2
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +5 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/orchestrator/templates/ai-src/commands/create-data-test.md +99 -0
  89. package/dist/orchestrator/templates/ai-src/commands/create-test.md +3 -0
  90. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -0
  91. package/dist/orchestrator/templates/ai-src/config/claude.md +3 -1
  92. package/dist/orchestrator/templates/ai-src/config/copilot.md +3 -1
  93. package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +116 -0
  94. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
  95. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +43 -8
  96. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
  97. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
  98. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  99. package/dist/orchestrator/templates/specs-db.d.ts +201 -11
  100. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  101. package/dist/orchestrator/templates/specs-db.js +440 -91
  102. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  103. package/dist/orchestrator/templates/specs-db.ts +463 -79
  104. package/dist/orchestrator/templates/specs-test-data.ts +39 -3
  105. package/package.json +8 -4
  106. package/src/capabilities/discover.ts +5 -2
  107. package/src/capabilities/registry.ts +13 -0
  108. package/src/cli/commands/capability.ts +40 -2
  109. package/src/cli/commands/delivery.ts +13 -5
  110. package/src/cli/commands/generate.ts +15 -0
  111. package/src/exporters/spec-parser.ts +17 -5
  112. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  113. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  114. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  115. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-row-column.hbs +2 -0
  116. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  117. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
  118. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
  119. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  120. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
  121. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
  122. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
  123. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
  124. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
  125. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
  126. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
  127. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
  128. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
  129. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
  130. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
  131. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
  132. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
  133. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
  134. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
  135. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
  136. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
  137. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
  138. package/src/generators/test-generator/app-profile.ts +185 -0
  139. package/src/generators/test-generator/code-generator.ts +9 -0
  140. package/src/generators/test-generator/diagnostics.ts +51 -0
  141. package/src/generators/test-generator/patterns/types.ts +12 -0
  142. package/src/generators/test-generator/recipe-expander.ts +129 -0
  143. package/src/generators/test-generator/step-mapper.ts +73 -0
  144. package/src/generators/test-generator/template-engine.ts +29 -3
  145. package/src/generators/test-generator/utils/selector-resolver.ts +49 -2
  146. package/src/harness/capability.ts +1 -0
  147. package/src/harness/catalog/drivers.yaml +2 -1
  148. package/src/harness/query-catalog.ts +0 -0
  149. package/src/index.ts +5 -2
  150. package/src/orchestrator/templates/ai-src/commands/create-data-test.md +99 -0
  151. package/src/orchestrator/templates/ai-src/commands/create-test.md +3 -0
  152. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -0
  153. package/src/orchestrator/templates/ai-src/config/claude.md +3 -1
  154. package/src/orchestrator/templates/ai-src/config/copilot.md +3 -1
  155. package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +116 -0
  156. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
  157. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +43 -8
  158. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
  159. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
  160. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  161. package/src/orchestrator/templates/specs-db.ts +463 -79
  162. package/src/orchestrator/templates/specs-test-data.ts +39 -3
@@ -48,9 +48,27 @@ export class TestDataLoader {
48
48
  if (value === undefined || value === null) {
49
49
  throw new Error(`Test data key not found: ${key}`);
50
50
  }
51
+ // A projected array (e.g. `{{q.rows[*].name}}`) serializes as a JSON list so the full
52
+ // ordered set is preserved for the assert-compare — String([...]) would flatten it to a
53
+ // lossy comma-join that can't be distinguished from a scalar containing commas.
54
+ if (Array.isArray(value)) return this.interpolate(JSON.stringify(value));
51
55
  return this.interpolate(String(value));
52
56
  }
53
57
 
58
+ /**
59
+ * Resolve a key to its RAW, uncoerced value (array/object/number kept as-is) — for binding
60
+ * DB query parameters, where type-strict stores (MySQL numeric columns, MongoDB) require the
61
+ * native type. get() stringifies for Gherkin text; raw() must not. Throws on missing, mirroring
62
+ * get()'s guard.
63
+ */
64
+ raw(key: string): any {
65
+ const value = this.resolve(key);
66
+ if (value === undefined || value === null) {
67
+ throw new Error(`Test data key not found: ${key}`);
68
+ }
69
+ return value;
70
+ }
71
+
54
72
  /**
55
73
  * Substitute embedded `{{ref}}` cross-references inside a resolved string value.
56
74
  * Each ref is looked up via resolve() (flat key, @cases row column, or dotted path),
@@ -78,6 +96,7 @@ export class TestDataLoader {
78
96
  * `q.count` / `q.length` → number of rows
79
97
  * `q.first.col` / `q.last.col` / `q[2].col` → a specific row's column
80
98
  * `q.col` → shorthand for the first row's column
99
+ * `q.rows[*].col` → the `col` of EVERY row, projected to an ordered array
81
100
  */
82
101
  private resolve(key: string): any {
83
102
  // 1. Exact flat key — captured vars (set()) live under a literal, possibly dotted, key.
@@ -88,9 +107,26 @@ export class TestDataLoader {
88
107
  return this.data[key];
89
108
  }
90
109
  // 2. Structured path: head from the row (cases) or shared data, then walk segments.
91
- const tokens = String(key).replace(/\[(\d+)\]/g, '.$1').split('.');
92
- let cur: any = (this.row && tokens[0] in this.row) ? this.row[tokens[0]] : this.data[tokens[0]];
93
- for (let i = 1; i < tokens.length && cur != null; i++) cur = TestDataLoader.step(cur, tokens[i]);
110
+ // `[n]` `.n` (numeric index); `[*]` → `.*` (project the rest of the path over each element).
111
+ const tokens = String(key).replace(/\[(\d+)\]/g, '.$1').replace(/\[\*\]/g, '.*').split('.');
112
+ const cur: any = (this.row && tokens[0] in this.row) ? this.row[tokens[0]] : this.data[tokens[0]];
113
+ return TestDataLoader.walk(cur, tokens, 1);
114
+ }
115
+
116
+ /**
117
+ * Walk the remaining path tokens from `i`. A `*` token on an array projects: the rest of the
118
+ * path is mapped over every element and the collected array returned (recurses for nested `[*]`).
119
+ * Non-`*` tokens use the linear single-step navigation, so numeric/first/last/object access is
120
+ * unchanged.
121
+ */
122
+ private static walk(cur: any, tokens: string[], i: number): any {
123
+ for (; i < tokens.length && cur != null; i++) {
124
+ if (tokens[i] === '*' && Array.isArray(cur)) {
125
+ const rest = tokens.slice(i + 1);
126
+ return cur.map((el) => TestDataLoader.walk(el, rest, 0));
127
+ }
128
+ cur = TestDataLoader.step(cur, tokens[i]);
129
+ }
94
130
  return cur;
95
131
  }
96
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.9",
3
+ "version": "3.2.10-beta.11",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -12,7 +12,7 @@
12
12
  "copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
13
13
  "build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
14
14
  "dev": "tsx src/cli/index.ts",
15
- "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts",
15
+ "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts &&tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
16
16
  "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
17
17
  "prepublishOnly": "npm run build:dashboard && npm run build"
18
18
  },
@@ -39,7 +39,8 @@
39
39
  "@babel/types": "^7.28.5",
40
40
  "@cucumber/gherkin": "^37.0.0",
41
41
  "@cucumber/messages": "^31.0.0",
42
- "@sungen/driver-ui": "3.2.9",
42
+ "@sungen/driver-data-factory": "3.2.10-beta.11",
43
+ "@sungen/driver-ui": "3.2.10-beta.11",
43
44
  "chalk": "^5.6.2",
44
45
  "commander": "^14.0.2",
45
46
  "dotenv": "^17.2.3",
@@ -65,6 +66,9 @@
65
66
  "LICENSE"
66
67
  ],
67
68
  "devDependencies": {
68
- "mysql2": "^3"
69
+ "mysql2": "^3",
70
+ "mongodb": "^7",
71
+ "@aws-sdk/client-dynamodb": "^3",
72
+ "@aws-sdk/lib-dynamodb": "^3"
69
73
  }
70
74
  }
@@ -27,7 +27,7 @@ import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
27
27
  * move to their packages (R5.5/R5.6) they join this list and leave `LOCAL_DRIVERS`; eventually this
28
28
  * becomes a scan of installed `@sungen/driver-*` packages (R5.7).
29
29
  */
30
- const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile'];
30
+ const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile', '@sungen/driver-data-factory'];
31
31
 
32
32
  /**
33
33
  * Bundled (non-opt-in) drivers: shipped as a core dependency (`drivers.yaml` `bundled: true`), so the
@@ -36,7 +36,10 @@ const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/dri
36
36
  * symlink for the package is missing). Surface it LOUDLY: otherwise the entire UI step vocabulary is
37
37
  * absent and every step silently compiles to "Unrecognized step pattern" with no clue why.
38
38
  */
39
- const BUNDLED_DRIVERS = new Set(['@sungen/driver-ui']);
39
+ // `@sungen/driver-data-factory` is bundled too — but it is RUNTIME-FREE (only a knowledge catalog +
40
+ // codegen), so shipping it by default adds no runtime, unlike ui/db/api/mobile. It makes standardized
41
+ // test-data available out of the box (no `capability add` needed); inert until a field-map/recipe exists.
42
+ const BUNDLED_DRIVERS = new Set(['@sungen/driver-ui', '@sungen/driver-data-factory']);
40
43
 
41
44
  function loadExternalDriver(name: string): void {
42
45
  // Resolve from the user's PROJECT first, then from core's own location. Opt-in drivers
@@ -62,6 +62,14 @@ export interface CapabilityDescriptor {
62
62
  */
63
63
  cliCommands?: Array<(program: unknown) => void>;
64
64
 
65
+ /**
66
+ * App-profile presets this capability ships (spec: sungen_app_profile_spec.md §3).
67
+ * Keyed by short name (`radix`, `antd`, `native`); each value is an AppProfile-shaped
68
+ * plain object that `qa/app.yaml` can pull in via `extends:`. UI knowledge stays in
69
+ * the UI driver — core only merges data.
70
+ */
71
+ appProfilePresets?: Record<string, unknown>;
72
+
65
73
  // --- Declared for later R-steps; not consumed yet (kept here so the SPI shape is visible). ---
66
74
  // adapter?: string; // codegen adapter id (existing adapterRegistry)
67
75
  }
@@ -97,6 +105,11 @@ export class CapabilityRegistry {
97
105
  return this.all().flatMap((c) => c.patterns ?? []);
98
106
  }
99
107
 
108
+ /** All app-profile presets contributed by registered capabilities (later registration wins on key collision). */
109
+ appProfilePresets(): Record<string, unknown> {
110
+ return Object.assign({}, ...this.all().map((c) => c.appProfilePresets ?? {}));
111
+ }
112
+
100
113
  /** All registered sensors, optionally filtered by kind ('advisory' | 'gate'). */
101
114
  sensors(kind?: Sensor['kind']): Sensor[] {
102
115
  const all = this.all().flatMap((c) => c.sensors ?? []);
@@ -15,6 +15,36 @@ function coreVersion(): string {
15
15
  try { return require('../../../package.json').version || 'latest'; } catch { return 'latest'; }
16
16
  }
17
17
 
18
+ /**
19
+ * Resolve the driver version to install for the running core `core`.
20
+ *
21
+ * Lockstep wants the EXACT `core` version. But the beta channel publishes `<core>-beta.N` before a
22
+ * stable `<core>` ever exists — so a released core (e.g. 3.2.10) asking for `@3.2.10` hits ETARGET
23
+ * out-of-the-box when npm only has `3.2.10-beta.2` (`sungen capability add` then fails for a fresh
24
+ * user). Fall back on the SAME version line: prefer exact `core`, else the highest `core-beta.N`,
25
+ * else the `beta` dist-tag as a last resort. Keeps the driver's pinned core-dep on the same line.
26
+ */
27
+ function resolveDriverVersion(pkg: string, core: string): string {
28
+ if (core === 'latest') return 'beta';
29
+ try {
30
+ const out = spawnSync('npm', ['view', pkg, 'versions', '--json'], { encoding: 'utf-8', shell: true });
31
+ if (out.status !== 0 || !out.stdout) return core; // can't query → let the exact install try/fail
32
+ let versions: string[] = [];
33
+ const parsed = JSON.parse(out.stdout);
34
+ versions = Array.isArray(parsed) ? parsed : [parsed];
35
+ if (versions.includes(core)) return core; // exact exists → lockstep
36
+ const prefix = `${core}-beta.`;
37
+ const betas = versions
38
+ .filter((v) => typeof v === 'string' && v.startsWith(prefix))
39
+ .map((v) => parseInt(v.slice(prefix.length), 10))
40
+ .filter((n) => Number.isInteger(n));
41
+ if (betas.length) return `${core}-beta.${Math.max(...betas)}`; // highest same-line prerelease
42
+ return 'beta'; // nothing on this line → newest beta dist-tag
43
+ } catch {
44
+ return core;
45
+ }
46
+ }
47
+
18
48
  /**
19
49
  * Add `test:mobile` to the project's package.json scripts (if absent).
20
50
  * Idempotent — skips if the script is already set.
@@ -187,14 +217,22 @@ export function registerCapabilityCommand(program: Command): void {
187
217
  }
188
218
 
189
219
  const adapterName = meta.adapter || meta.id;
190
- const bundled = adapterRegistry.hasAdapter(adapterName); // e.g. `web` is bundled in Phase 2a
220
+ // Bundled = a platform adapter baked into core (e.g. `web`), OR a capability driver shipped
221
+ // as a core dependency (`drivers.yaml` bundled: true, e.g. data-factory) — present already,
222
+ // so no install/verify: `capability add` on it just records it in the profile.
223
+ const bundled = adapterRegistry.hasAdapter(adapterName) || meta.bundled === true;
191
224
 
192
225
  if (!bundled && !o.skipInstall) {
193
226
  // Pin the driver to the RUNNING core's exact version (lockstep). Without this, plain
194
227
  // `npm install @sungen/driver-x` resolves the package's `latest` dist-tag — which on a
195
228
  // beta channel can lag (e.g. a prerelease first-published-as-latest), pulling a driver
196
229
  // whose core dep resolves to the wrong @sun-asterisk/sungen and fails to load.
197
- const spec = `${meta.package}@${coreVersion()}`;
230
+ const core = coreVersion();
231
+ const target = resolveDriverVersion(meta.package, core); // exact core, else same-line beta, else 'beta' tag
232
+ const spec = `${meta.package}@${target}`;
233
+ if (target !== core) {
234
+ console.log(`ℹ ${meta.package}@${core} is not published yet — installing ${spec} (closest on the ${core} line).`);
235
+ }
198
236
  console.log(`📦 Installing ${spec} (dev dependency)...`);
199
237
  const r = spawnSync('npm', ['install', '-D', spec], { stdio: 'inherit', shell: true });
200
238
  if (r.status !== 0) throw new Error(`npm install -D ${spec} failed.`);
@@ -355,11 +355,19 @@ function featureReferencesTestData(featureFile: string): boolean {
355
355
 
356
356
  function checkSelectorsHasEntries(selectorsFile: string, screen: string): boolean {
357
357
  if (!fs.existsSync(selectorsFile)) return false;
358
- const content = fs.readFileSync(selectorsFile, 'utf-8');
359
- // Very small check: YAML has more than just a single `<screen>: page` entry
360
- const topLevelKeys = (content.match(/^[a-zA-Z_][^\s:]*:\s*$/gm) || []).length +
361
- (content.match(/^[a-zA-Z_][^\s:]*:\s/gm) || []).length;
362
- return topLevelKeys > 1;
358
+ // Parse the YAML and count real top-level keys. The old regex heuristic required
359
+ // key names without whitespace, but sungen's own selector-key convention produces
360
+ // multi-word keys ("email address:", "contact points table:") — a file whose keys
361
+ // are ALL multi-word counted as 0 entries and falsely blocked the export.
362
+ try {
363
+ const parsed = parseYaml(fs.readFileSync(selectorsFile, 'utf-8'));
364
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
365
+ // More than just the single `<screen>: page` entry.
366
+ return Object.keys(parsed as Record<string, unknown>).length > 1;
367
+ } catch {
368
+ // Malformed YAML — surface as "missing entries" so preflight points at the file.
369
+ return false;
370
+ }
363
371
  }
364
372
 
365
373
  export function hasBlockingMissing(p: PreflightCheck): boolean {
@@ -99,6 +99,7 @@ export function registerGenerateCommand(program: Command): void {
99
99
  .option('--all', 'Generate tests for all screens, flows, and API areas')
100
100
  .option('--framework <name>', 'Override the platform driver (else read from qa/capabilities.yaml)')
101
101
  .option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
102
+ .option('--strict', 'Exit non-zero when compile diagnostics are found (unmatched steps, dropped {{values}})')
102
103
  .action(async (options) => {
103
104
  try {
104
105
  const screenName = options.screen;
@@ -192,6 +193,20 @@ export function registerGenerateCommand(program: Command): void {
192
193
 
193
194
  console.log(`\n${results.length} test file(s) generated.`);
194
195
 
196
+ // Compile diagnostics (SG-W001 unmatched step, SG-W002 dropped {{value}}, ...).
197
+ // Warning-only by default; --strict turns any diagnostic into exit 1.
198
+ const diagnostics = generator.getDiagnostics();
199
+ if (diagnostics.count > 0) {
200
+ console.log(`\n⚠️ Compile diagnostics (${diagnostics.count}):`);
201
+ for (const line of diagnostics.formatLines().slice(0, 20)) console.log(` ${line}`);
202
+ if (diagnostics.count > 20) console.log(` … and ${diagnostics.count - 20} more`);
203
+ console.log(` Unmatched steps compile to no-op // TODO comments — the test runs weaker than it reads.`);
204
+ if (options.strict) {
205
+ console.error(`\n--strict: failing generation on ${diagnostics.count} diagnostic(s).`);
206
+ process.exit(1);
207
+ }
208
+ }
209
+
195
210
  // Security S0 — warn (never block) if test-data looks to hold a real secret.
196
211
  const scanDirs: string[] = [];
197
212
  if (screenName) scanDirs.push(path.join(process.cwd(), 'qa', 'screens', screenName));
@@ -20,6 +20,10 @@ function extractTestBlock(content: string, startIdx: number): {
20
20
  // test('title', { tag: [...] }, async ({ page }) => {
21
21
  // Backreference \1 lets the inner title contain the opposite quote type
22
22
  // (e.g. test('Footer "X" link', ...) — common when scenarios cite UI labels).
23
+ // `\\.` consumes any backslash-escaped char FIRST, so the wrapping quote may also appear
24
+ // ESCAPED inside the title (codegen emits `row\'s` for an apostrophe in a scenario name).
25
+ // Without it the title capture stopped at `\'` and the whole test() silently vanished from
26
+ // the parse — the scenario was then mislabelled "Not compiled"/N/A although it ran and passed.
23
27
  // Quote char may be ' " or ` — the backtick form is a data-driven (@cases) title
24
28
  // like `VP-… — ${__row.__label}`.
25
29
  // The options object uses `\{.*?\}` (non-greedy, not `[^}]*`) so a tag value containing a
@@ -27,13 +31,14 @@ function extractTestBlock(content: string, startIdx: number): {
27
31
  // single-line, so `.*?` anchored on `}, async` is safe.
28
32
  // Accept both Playwright `test(...)` and mobile/Appium mocha `it(...)` (issue #391). The leading
29
33
  // `\b` prevents `it(` from matching the tail of a larger word like `submit(` / `edit(`.
30
- const testRegex = /\b(?:test|it)\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
34
+ const testRegex = /\b(?:test|it)\s*\(\s*(['"`])((?:\\.|(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
31
35
  testRegex.lastIndex = startIdx;
32
36
  const match = testRegex.exec(content);
33
37
  if (!match) return null;
34
38
 
35
- // [1] = outer quote char, [2] = title
36
- const title = match[2];
39
+ // [1] = outer quote char, [2] = title. Unescape `\'`/`\"`/backtick/backslash: the results.json
40
+ // title is the RUNTIME string (`row's`), so the escaped source form would never match it.
41
+ const title = unescapeQuoted(match[2]);
37
42
  const bodyStart = match.index + match[0].length;
38
43
 
39
44
  // Find matching closing brace (accounting for nested braces)
@@ -54,6 +59,11 @@ function extractTestBlock(content: string, startIdx: number): {
54
59
  return { title, body, endIdx };
55
60
  }
56
61
 
62
+ /** Unescape backslash-escaped quote chars (and backslashes) captured from a quoted JS string. */
63
+ function unescapeQuoted(s: string): string {
64
+ return s.replace(/\\(['"`\\])/g, '$1');
65
+ }
66
+
57
67
  /**
58
68
  * Find describe('name', ...) blocks and return their names + body ranges.
59
69
  */
@@ -61,10 +71,12 @@ function extractDescribeBlocks(content: string): Array<{ name: string; bodyStart
61
71
  const result: Array<{ name: string; bodyStart: number; bodyEnd: number }> = [];
62
72
  // Accept Playwright `test.describe(...)` / `test.describe.serial(...)` AND mobile/Appium mocha
63
73
  // `describe(...)` (issue #391). The optional `test.` prefix keeps web specs matching unchanged.
64
- const regex = /(?:test\.)?describe(?:\.serial)?\s*\(\s*['"]([^'"]+)['"]\s*,\s*\(\s*\)\s*=>\s*\{/g;
74
+ // Same escaped-quote handling as the test-title regex: a feature name with an apostrophe
75
+ // (describe('Manager\'s dashboard')) must not drop the whole describe block from the parse.
76
+ const regex = /(?:test\.)?describe(?:\.serial)?\s*\(\s*(['"])((?:\\.|(?!\1).)+)\1\s*,\s*\(\s*\)\s*=>\s*\{/g;
65
77
  let match: RegExpExecArray | null;
66
78
  while ((match = regex.exec(content)) !== null) {
67
- const name = match[1];
79
+ const name = unescapeQuoted(match[2]);
68
80
  const bodyStart = match.index + match[0].length;
69
81
  let depth = 1;
70
82
  let i = bodyStart;
@@ -1 +1 @@
1
- page.on('dialog', dialog => dialog.accept());
1
+ page.once('dialog', dialog => dialog.accept());
@@ -1 +1 @@
1
- page.on('dialog', dialog => dialog.dismiss());
1
+ page.once('dialog', dialog => dialog.dismiss());
@@ -1 +1 @@
1
- page.on('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
1
+ page.once('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
@@ -0,0 +1,2 @@
1
+ { const tableRow = {{> locator}}.getByRole('row').nth({{{rowIndexExpr}}});
2
+ testData.set('{{varName}}', ((await {{{cellExpr}}}.textContent()) ?? '').trim()); }
@@ -1 +1 @@
1
- await {{> locator-base}}.filter({ hasText: {{#if (and (eq strategy "text") (eq value ""))}}/.*{{escapeRegex dataValue}}$/{{else}}'{{escapeQuotes dataValue}}'{{/if}} }){{> locator-nth}}.click();
1
+ await {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact legacyTail=(and (eq strategy "text") (eq value ""))}} }){{> locator-nth}}.click();
@@ -0,0 +1,2 @@
1
+ page.once('dialog', dialog => dialog.{{dialogAction}}());
2
+ await {{> locator}}.click();
@@ -0,0 +1,3 @@
1
+ {{#each steps}}
2
+ {{> (lookup this 'partial') this}}
3
+ {{/each}}
@@ -0,0 +1,2 @@
1
+ { const tableRow = {{> locator}}.getByRole('row').nth({{{rowIndexExpr}}});
2
+ await {{{actionLocator}}}.{{action}}(); }
@@ -1 +1 @@
1
- await page.getByText('{{escapeQuotes selectorValue}}').filter({ hasText: {{#if (eq selectorValue "")}}/.*{{escapeRegex dataValue}}$/{{else}}'{{escapeQuotes dataValue}}'{{/if}} }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}.{{action}}();
1
+ await page.getByText('{{escapeQuotes selectorValue}}').filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact legacyTail=(eq selectorValue "")}} }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}.{{action}}();
@@ -1 +1,4 @@
1
1
  await expect(page).toHaveURL(/{{escapeRegex path}}/);
2
+ {{#if query}}
3
+ expect(page.url()).toContain('{{escapeQuotes query}}');
4
+ {{/if}}
@@ -1 +1 @@
1
- await {{> locator}}.waitFor({ state: '{{state}}' });
1
+ {{#if (eq state "enabled")}}await expect({{> locator}}).toBeEnabled();{{else if (eq state "disabled")}}await expect({{> locator}}).toBeDisabled();{{else}}await {{> locator}}.waitFor({ state: '{{state}}' });{{/if}}
@@ -0,0 +1,3 @@
1
+ { const refreshIndicator = page.locator('{{escapeQuotes indicator}}');
2
+ await refreshIndicator.first().waitFor({ state: 'visible', timeout: 1500 }).catch(() => {});
3
+ await refreshIndicator.first().waitFor({ state: 'hidden' }); }
@@ -0,0 +1 @@
1
+ {{#if exact}}/^{{escapeRegex dataValue}}$/{{else if legacyTail}}/.*{{escapeRegex dataValue}}$/{{else}}'{{escapeQuotes dataValue}}'{{/if}}
@@ -8,7 +8,7 @@
8
8
  {{~#case 'id'}}locator('{{value}}'){{/case~}}
9
9
  {{~#default}}locator('{{value}}'){{/default~}}
10
10
  {{~/switch}}{{else~}}
11
- {{#if scope}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{#switch strategy~}}
11
+ {{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
12
12
  {{~#case 'testid'}}getByTestId('{{value}}'){{/case~}}
13
13
  {{~#case 'role'}}{{#if name}}getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{else}}getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}){{/if}}{{/case~}}
14
14
  {{~#case 'placeholder'}}getByPlaceholder('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
@@ -8,7 +8,7 @@
8
8
  {{~#case 'id'}}locator('{{value}}'){{/case~}}
9
9
  {{~#default}}locator('{{value}}'){{/default~}}
10
10
  {{~/switch}}{{> locator-nth}}{{else~}}
11
- {{#if scope}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{#switch strategy~}}
11
+ {{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
12
12
  {{~#case 'testid'}}getByTestId('{{value}}'){{/case~}}
13
13
  {{~#case 'role'}}{{#if name}}getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{else}}getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}){{/if}}{{/case~}}
14
14
  {{~#case 'placeholder'}}getByPlaceholder('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
@@ -0,0 +1 @@
1
+ await {{{targetExpr}}}.evaluate((el) => (el instanceof HTMLElement) && el.blur());
@@ -0,0 +1,3 @@
1
+ { const __opt = {{{targetExpr}}};
2
+ await __opt.scrollIntoViewIfNeeded().catch(() => {});
3
+ await __opt.click(); }
@@ -0,0 +1 @@
1
+ await expect({{{targetExpr}}}).toContainText({{{valueExpr}}});
@@ -0,0 +1 @@
1
+ await expect({{{targetExpr}}}).toHaveValue({{{valueExpr}}});
@@ -0,0 +1,5 @@
1
+ for (const item of String({{{sourceExpr}}}).split('{{split}}').map((s) => s.trim())) {
2
+ {{#each body}}
3
+ {{> (lookup this 'partial') this}}
4
+ {{/each}}
5
+ }
@@ -0,0 +1 @@
1
+ await page.keyboard.press('{{key}}');
@@ -0,0 +1 @@
1
+ await {{{targetExpr}}}.scrollIntoViewIfNeeded();
@@ -0,0 +1 @@
1
+ await page.keyboard.type({{{valueExpr}}});
@@ -0,0 +1 @@
1
+ await {{{targetExpr}}}.first().waitFor({ state: '{{state}}' });