@sun-asterisk/sungen 3.1.0 → 3.1.2-beta.100

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 (228) hide show
  1. package/README.md +4 -428
  2. package/dist/capabilities/builtins.d.ts +31 -0
  3. package/dist/capabilities/builtins.d.ts.map +1 -0
  4. package/dist/capabilities/builtins.js +84 -0
  5. package/dist/capabilities/builtins.js.map +1 -0
  6. package/dist/capabilities/context-router.d.ts +34 -0
  7. package/dist/capabilities/context-router.d.ts.map +1 -0
  8. package/dist/capabilities/context-router.js +49 -0
  9. package/dist/capabilities/context-router.js.map +1 -0
  10. package/dist/capabilities/context.d.ts +51 -0
  11. package/dist/capabilities/context.d.ts.map +1 -0
  12. package/dist/capabilities/context.js +17 -0
  13. package/dist/capabilities/context.js.map +1 -0
  14. package/dist/capabilities/discover.d.ts +2 -0
  15. package/dist/capabilities/discover.d.ts.map +1 -0
  16. package/dist/capabilities/discover.js +48 -0
  17. package/dist/capabilities/discover.js.map +1 -0
  18. package/dist/capabilities/registry.d.ts +90 -0
  19. package/dist/capabilities/registry.d.ts.map +1 -0
  20. package/dist/capabilities/registry.js +43 -0
  21. package/dist/capabilities/registry.js.map +1 -0
  22. package/dist/capabilities/sensor.d.ts +49 -0
  23. package/dist/capabilities/sensor.d.ts.map +1 -0
  24. package/dist/capabilities/sensor.js +3 -0
  25. package/dist/capabilities/sensor.js.map +1 -0
  26. package/dist/cli/commands/challenge.d.ts.map +1 -1
  27. package/dist/cli/commands/challenge.js +9 -2
  28. package/dist/cli/commands/challenge.js.map +1 -1
  29. package/dist/cli/commands/delivery.d.ts.map +1 -1
  30. package/dist/cli/commands/delivery.js +3 -2
  31. package/dist/cli/commands/delivery.js.map +1 -1
  32. package/dist/cli/commands/generate.d.ts.map +1 -1
  33. package/dist/cli/commands/generate.js +12 -0
  34. package/dist/cli/commands/generate.js.map +1 -1
  35. package/dist/cli/index.js +10 -1
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  38. package/dist/exporters/csv-exporter.js +92 -76
  39. package/dist/exporters/csv-exporter.js.map +1 -1
  40. package/dist/exporters/spec-parser.d.ts.map +1 -1
  41. package/dist/exporters/spec-parser.js +6 -1
  42. package/dist/exporters/spec-parser.js.map +1 -1
  43. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
  44. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  45. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  46. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  47. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  48. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  49. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
  50. package/dist/generators/test-generator/code-generator.d.ts +21 -4
  51. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  52. package/dist/generators/test-generator/code-generator.js +118 -57
  53. package/dist/generators/test-generator/code-generator.js.map +1 -1
  54. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +3 -0
  55. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -0
  56. package/dist/generators/test-generator/patterns/expect-patterns.js +54 -0
  57. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -0
  58. package/dist/generators/test-generator/patterns/index.d.ts +0 -10
  59. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  60. package/dist/generators/test-generator/patterns/index.js +10 -45
  61. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  62. package/dist/generators/test-generator/step-mapper.d.ts +6 -0
  63. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  64. package/dist/generators/test-generator/step-mapper.js +8 -0
  65. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  66. package/dist/generators/test-generator/template-engine.d.ts +4 -0
  67. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  68. package/dist/generators/test-generator/template-engine.js +1 -1
  69. package/dist/generators/test-generator/template-engine.js.map +1 -1
  70. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +1 -1
  71. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  72. package/dist/generators/test-generator/utils/runtime-data-transformer.js +5 -5
  73. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  74. package/dist/harness/annotation-overrides.d.ts +9 -0
  75. package/dist/harness/annotation-overrides.d.ts.map +1 -0
  76. package/dist/harness/annotation-overrides.js +36 -0
  77. package/dist/harness/annotation-overrides.js.map +1 -0
  78. package/dist/harness/audit.d.ts.map +1 -1
  79. package/dist/harness/audit.js +35 -7
  80. package/dist/harness/audit.js.map +1 -1
  81. package/dist/harness/catalog/drivers.yaml +35 -12
  82. package/dist/harness/challenge.d.ts +1 -0
  83. package/dist/harness/challenge.d.ts.map +1 -1
  84. package/dist/harness/challenge.js +49 -2
  85. package/dist/harness/challenge.js.map +1 -1
  86. package/dist/harness/data-driven-lint.d.ts +7 -0
  87. package/dist/harness/data-driven-lint.d.ts.map +1 -0
  88. package/dist/harness/data-driven-lint.js +153 -0
  89. package/dist/harness/data-driven-lint.js.map +1 -0
  90. package/dist/harness/parse.d.ts +3 -0
  91. package/dist/harness/parse.d.ts.map +1 -1
  92. package/dist/harness/parse.js +25 -0
  93. package/dist/harness/parse.js.map +1 -1
  94. package/dist/harness/query-catalog.d.ts +48 -0
  95. package/dist/harness/query-catalog.d.ts.map +1 -0
  96. package/dist/harness/query-catalog.js +0 -0
  97. package/dist/harness/query-catalog.js.map +1 -0
  98. package/dist/harness/script-check.d.ts.map +1 -1
  99. package/dist/harness/script-check.js +7 -4
  100. package/dist/harness/script-check.js.map +1 -1
  101. package/dist/index.d.ts +20 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +32 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
  106. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  107. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +41 -0
  108. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +22 -0
  109. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
  110. package/dist/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
  111. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  112. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +41 -0
  113. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +22 -0
  114. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
  115. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
  116. package/dist/orchestrator/templates/specs-api.d.ts +19 -0
  117. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
  118. package/dist/orchestrator/templates/specs-api.js +128 -0
  119. package/dist/orchestrator/templates/specs-api.js.map +1 -0
  120. package/dist/orchestrator/templates/specs-api.ts +101 -0
  121. package/dist/orchestrator/templates/specs-db.d.ts +8 -0
  122. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  123. package/dist/orchestrator/templates/specs-db.js +22 -0
  124. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  125. package/dist/orchestrator/templates/specs-db.ts +22 -0
  126. package/dist/orchestrator/templates/specs-test-data.ts +76 -15
  127. package/package.json +7 -30
  128. package/src/capabilities/builtins.ts +85 -0
  129. package/src/capabilities/context-router.ts +66 -0
  130. package/src/capabilities/context.ts +46 -0
  131. package/src/capabilities/discover.ts +42 -0
  132. package/src/capabilities/registry.ts +111 -0
  133. package/src/capabilities/sensor.ts +47 -0
  134. package/src/cli/commands/challenge.ts +6 -2
  135. package/src/cli/commands/delivery.ts +3 -2
  136. package/src/cli/commands/generate.ts +12 -0
  137. package/src/cli/index.ts +10 -1
  138. package/src/exporters/csv-exporter.ts +22 -6
  139. package/src/exporters/spec-parser.ts +6 -1
  140. package/src/generators/test-generator/adapters/adapter-interface.ts +2 -1
  141. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  142. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  143. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
  144. package/src/generators/test-generator/code-generator.ts +114 -59
  145. package/src/generators/test-generator/patterns/expect-patterns.ts +49 -0
  146. package/src/generators/test-generator/patterns/index.ts +9 -33
  147. package/src/generators/test-generator/step-mapper.ts +9 -0
  148. package/src/generators/test-generator/template-engine.ts +5 -2
  149. package/src/generators/test-generator/utils/runtime-data-transformer.ts +5 -5
  150. package/src/harness/annotation-overrides.ts +25 -0
  151. package/src/harness/audit.ts +37 -8
  152. package/src/harness/catalog/drivers.yaml +35 -12
  153. package/src/harness/challenge.ts +47 -2
  154. package/src/harness/data-driven-lint.ts +119 -0
  155. package/src/harness/parse.ts +17 -0
  156. package/src/harness/query-catalog.ts +0 -0
  157. package/src/harness/script-check.ts +8 -5
  158. package/src/index.ts +30 -0
  159. package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
  160. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  161. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +41 -0
  162. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +22 -0
  163. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
  164. package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
  165. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  166. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +41 -0
  167. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +22 -0
  168. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
  169. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
  170. package/src/orchestrator/templates/specs-api.ts +101 -0
  171. package/src/orchestrator/templates/specs-db.ts +22 -0
  172. package/src/orchestrator/templates/specs-test-data.ts +76 -15
  173. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
  174. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
  175. package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
  176. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
  177. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
  178. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
  179. package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
  180. package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
  181. package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -5
  182. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
  183. package/dist/generators/test-generator/patterns/database-patterns.js +0 -94
  184. package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
  185. package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
  186. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
  187. package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
  188. package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
  189. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
  190. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
  191. package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
  192. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
  193. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
  194. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
  195. package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
  196. package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
  197. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
  198. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
  199. package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
  200. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
  201. package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
  202. package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
  203. package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
  204. package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
  205. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
  206. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
  207. package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
  208. package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
  209. package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
  210. package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
  211. package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
  212. package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
  213. package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
  214. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
  215. package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
  216. package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
  217. package/docs/orchestration-spec.md +0 -267
  218. package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
  219. package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
  220. package/src/generators/test-generator/patterns/database-patterns.ts +0 -95
  221. package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
  222. package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
  223. package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
  224. package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
  225. package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
  226. package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
  227. package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
  228. package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
package/src/cli/index.ts CHANGED
@@ -26,6 +26,8 @@ import { registerChallengeCommand } from './commands/challenge';
26
26
  import { registerBlindspotCommand } from './commands/blindspot';
27
27
  import { registerCapabilityCommand } from './commands/capability';
28
28
  import { registerFlowCheckCommand } from './commands/flow-check';
29
+ import { capabilityRegistry } from '../capabilities/registry';
30
+ import { discoverAndRegisterCapabilities } from '../capabilities/discover';
29
31
 
30
32
  // Read version from package.json so `--version` never drifts from the released version.
31
33
  const { version } = require('../../package.json') as { version: string };
@@ -42,7 +44,7 @@ async function main() {
42
44
  program
43
45
  .option('-v, --verbose', 'Enable verbose logging');
44
46
 
45
- // Register commands (9)
47
+ // Register commands
46
48
  registerInitCommand(program);
47
49
  registerAddCommand(program);
48
50
  registerGenerateCommand(program);
@@ -65,6 +67,13 @@ async function main() {
65
67
  registerIngestCommand(program);
66
68
  registerEvalCommand(program);
67
69
 
70
+ // Capability-contributed CLI commands (Capability SPI): drivers own their authoring commands
71
+ // (e.g. @sungen/driver-api → `sungen api import`). Discover, then register each.
72
+ discoverAndRegisterCapabilities();
73
+ for (const cap of capabilityRegistry.all()) {
74
+ for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
75
+ }
76
+
68
77
  await program.parseAsync(process.argv);
69
78
  }
70
79
 
@@ -53,7 +53,25 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
53
53
  let fallbackIndex = 1;
54
54
 
55
55
  for (const m of input.merged) {
56
- const { vpId, category1 } = splitVpAndName(m.feature.name);
56
+ // Data-driven (@cases): one scenario ran once per input row — emit one CSV row per
57
+ // executed input. The Playwright results are titled "<scenario> — <label>"; expand
58
+ // by matching that prefix. With no results yet, fall back to a single (Pending) row.
59
+ const isCases = m.feature.tags.some((t) => t.startsWith('@cases:'));
60
+ let variants: Array<{ nameSuffix: string; result?: PlaywrightResult }> = [{ nameSuffix: '' }];
61
+ if (isCases && input.results) {
62
+ // Result titles are "<describe> > <scenario> — <label>"; match the scenario+label marker.
63
+ const marker = `${m.feature.name} — `;
64
+ const rowResults = [...input.results.entries()].filter(([t]) => t.includes(marker));
65
+ if (rowResults.length) {
66
+ variants = rowResults.map(([t, r]) => ({ nameSuffix: ` — ${t.slice(t.indexOf(marker) + marker.length)}`, result: r }));
67
+ }
68
+ } else if (input.results && m.spec) {
69
+ variants = [{ nameSuffix: '', result: input.results.get(m.spec.testTitle) }];
70
+ }
71
+
72
+ for (const variant of variants) {
73
+ const displayName = `${m.feature.name}${variant.nameSuffix}`;
74
+ const { vpId, category1 } = splitVpAndName(displayName);
57
75
  const tcId = generateTcId(input.screen, vpId, fallbackIndex);
58
76
  if (!vpId) fallbackIndex++;
59
77
 
@@ -87,11 +105,8 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
87
105
  // automatically) and render natively as multi-line in XLSX.
88
106
  const testData = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
89
107
 
90
- // Match Playwright result by test title (from .spec.ts) OR by scenarioName
91
- let result: PlaywrightResult | undefined;
92
- if (input.results && m.spec) {
93
- result = input.results.get(m.spec.testTitle);
94
- }
108
+ // Status for this (possibly per-input) row resolved into `variant` above.
109
+ const result: PlaywrightResult | undefined = variant.result;
95
110
 
96
111
  // Determine Test Result
97
112
  let testResult: string;
@@ -141,6 +156,7 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
141
156
  testEnvironment: environment,
142
157
  note,
143
158
  });
159
+ }
144
160
  }
145
161
 
146
162
  return rows;
@@ -20,7 +20,12 @@ 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
- const testRegex = /test\s*\(\s*(['"])((?:(?!\1).)+)\1\s*,\s*(?:\{[^}]*\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
23
+ // Quote char may be ' " or ` — the backtick form is a data-driven (@cases) title
24
+ // like `VP-… — ${__row.__label}`.
25
+ // The options object uses `\{.*?\}` (non-greedy, not `[^}]*`) so a tag value containing a
26
+ // `}` — e.g. `@query:q(p={{var}})` — doesn't truncate the match (issue #271). The header is
27
+ // single-line, so `.*?` anchored on `}, async` is safe.
28
+ const testRegex = /test\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
24
29
  testRegex.lastIndex = startIdx;
25
30
  const match = testRegex.exec(content);
26
31
  if (!match) return null;
@@ -36,6 +36,7 @@ export interface ScenarioData {
36
36
  authRole?: string; // Auth role for storage state
37
37
  isParallel?: boolean; // @parallel: use fresh page from fixture
38
38
  tags?: string; // Pass-through tags for Playwright { tag: [...] }, e.g. "'@smoke', '@high'"
39
+ casesDataset?: string; // @cases:<dataset> — emit one test() per row of the runtime dataset
39
40
  }
40
41
 
41
42
  export interface StepTemplateData {
@@ -63,7 +64,7 @@ export interface TestGeneratorAdapter {
63
64
  // Template rendering methods
64
65
  renderTestFile(data: TestFileData): string;
65
66
  renderScenario(data: ScenarioData): string;
66
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string;
67
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string;
67
68
  renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
68
69
  renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
69
70
  renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
26
26
  return this.templateEngine.renderScenario(data);
27
27
  }
28
28
 
29
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
29
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
30
30
  return this.templateEngine.renderImports(options);
31
31
  }
32
32
 
@@ -6,6 +6,9 @@ import { TestDataLoader } from '{{basePath}}/test-data';
6
6
  {{#if needsDb}}
7
7
  import { db } from '{{basePath}}/db';
8
8
  {{/if}}
9
+ {{#if needsApi}}
10
+ import { api } from '{{basePath}}/api';
11
+ {{/if}}
9
12
 
10
13
  // This file is auto-generated from Gherkin feature files
11
14
  // DO NOT EDIT MANUALLY - changes will be overwritten
@@ -1,3 +1,20 @@
1
+ {{#if casesDataset}}
2
+ for (const __row of testData.cases('{{casesDataset}}')) {
3
+ {{#if tags}}
4
+ test(`{{scenarioName}} — ${__row.__label}`, { tag: [{{{tags}}}] }, async ({{#if isParallel}}{ page }{{/if}}) => {
5
+ {{else}}
6
+ test(`{{scenarioName}} — ${__row.__label}`, async ({{#if isParallel}}{ page }{{/if}}) => {
7
+ {{/if}}
8
+ const rowData = testData.withRow(__row);
9
+ {{#each steps}}
10
+ {{#if comment}}
11
+ // {{comment}}
12
+ {{/if}}
13
+ {{code}}
14
+ {{/each}}
15
+ });
16
+ }
17
+ {{else}}
1
18
  {{#if isParallel}}
2
19
  {{#if tags}}
3
20
  test('{{scenarioName}}', { tag: [{{{tags}}}] }, async ({ page }) => {
@@ -24,4 +41,5 @@
24
41
  {{code}}
25
42
  {{/each}}
26
43
  });
27
- {{/if}}
44
+ {{/if}}
45
+ {{/if}}
@@ -4,7 +4,8 @@ import { ParsedFeature, ParsedScenario, ParsedStep } from '../gherkin-parser';
4
4
  import { StepMapper } from './step-mapper';
5
5
  import { TestGeneratorAdapter, adapterRegistry } from './adapters';
6
6
  import { transformToRuntimeData } from './utils/runtime-data-transformer';
7
- import { isDbStep } from './patterns/database-patterns';
7
+ import { capabilityRegistry } from '../../capabilities/registry';
8
+ import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
8
9
 
9
10
  /**
10
11
  * Filter base scenario steps for @extend: only keep Given→When steps.
@@ -74,7 +75,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
74
75
  const FUNCTIONAL_TAG_PREFIXES = [
75
76
  '@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
76
77
  '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
77
- '@flow',
78
+ '@flow', '@cases:',
78
79
  ];
79
80
 
80
81
  function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
@@ -170,6 +171,8 @@ export class CodeGenerator {
170
171
  private adapter: TestGeneratorAdapter;
171
172
  private screenName?: string;
172
173
  private options: any;
174
+ // Screen/flow name for the CURRENT feature, in catalog-resolution form (`flows/<x>` for flows).
175
+ private queryScreenName: string = '';
173
176
  // Steps registry built per feature during generateTestCode(); used by countSteps()
174
177
  private stepsRegistry = new Map<string, ParsedScenario>();
175
178
 
@@ -237,11 +240,17 @@ export class CodeGenerator {
237
240
  const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
238
241
  const needsCleanupImport = !isParallelFeature && hasCleanupTags;
239
242
 
240
- // Data Driver: if any step verifies DB state, import the `db` helper + emit specs/db.ts
241
- const needsDb = this.featureUsesDb(feature);
242
- if (needsDb) this.ensureDbFile(outputDir);
243
+ // Active capabilities for this feature (registry-driven): the default UI + any whose annotation
244
+ // tags appear (@query) or whose detectsStep matches (declarative DB steps). Each active
245
+ // capability emits its declared runtime helpers (db → specs/db.ts).
246
+ const activeCapabilityIds = this.activeCapabilityIds(feature);
247
+ const needsDb = activeCapabilityIds.includes('db');
248
+ const needsApi = activeCapabilityIds.includes('api');
249
+ for (const id of activeCapabilityIds) {
250
+ for (const h of capabilityRegistry.get(id)?.runtimeHelpers ?? []) this.syncGeneratedHelper(outputDir, h.file, h.template);
251
+ }
243
252
 
244
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb });
253
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi });
245
254
 
246
255
  // Generate test code (async now to support AI mapping)
247
256
  const testCode = await this.generateTestCode(feature);
@@ -297,67 +306,68 @@ export class CodeGenerator {
297
306
  /**
298
307
  * Ensure specs/base.ts exists in the output directory
299
308
  */
300
- /** True when any step (background or scenario) in the feature is a DB-verification step. */
301
- private featureUsesDb(feature: ParsedFeature): boolean {
309
+ /**
310
+ * Capabilities active for a feature (registry-driven): the default (UI) capability, plus any
311
+ * whose annotation tags appear on a scenario (e.g. `@query` → db) or whose `detectsStep`
312
+ * matches a step (db's declarative `User see [table] row where …`). Drives runtime-helper
313
+ * emission + the `db` import — replaces the hardcoded `featureUsesDb` check (R4).
314
+ */
315
+ private activeCapabilityIds(feature: ParsedFeature): string[] {
316
+ discoverAndRegisterCapabilities();
302
317
  const steps: ParsedStep[] = [];
303
318
  if (feature.background?.steps) steps.push(...feature.background.steps);
304
319
  for (const sc of feature.scenarios || []) if (sc.steps) steps.push(...sc.steps);
305
- return steps.some((s) => s && typeof s.text === 'string' && isDbStep(s.text));
320
+ const scenarioTags = (feature.scenarios || []).flatMap((sc) => sc.tags || []);
321
+ const ids = new Set<string>();
322
+ const def = capabilityRegistry.defaultCapabilityId();
323
+ if (def) ids.add(def);
324
+ for (const cap of capabilityRegistry.all()) {
325
+ const annoMatch = (cap.annotations ?? []).some((a) => scenarioTags.some((t) => t === a || t.startsWith(a + ':')));
326
+ const stepMatch = cap.detectsStep ? steps.some((s) => s && typeof s.text === 'string' && cap.detectsStep!(s.text)) : false;
327
+ if (annoMatch || stepMatch) ids.add(cap.id);
328
+ }
329
+ return [...ids];
306
330
  }
307
331
 
308
- /** Copy the Data Driver runtime helper into specs/db.ts (idempotent). */
309
- ensureDbFile(outputDir: string): void {
310
- const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
311
- const dbPath = path.join(outputDir, 'db.ts');
312
- if (!fs.existsSync(dbPath)) {
313
- const templatePath = path.join(templatesRoot, 'specs-db.ts');
314
- if (fs.existsSync(templatePath)) {
315
- if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
316
- fs.copyFileSync(templatePath, dbPath);
317
- console.log('✓ Created: specs/db.ts');
318
- }
319
- }
332
+ /**
333
+ * Precondition steps a scenario's capabilities inject (e.g. db `@query:<name>` → bind {{name}}).
334
+ * Each capability owns its annotation codegen via the SPI (`preconditionCodegen`); the compiler
335
+ * just composes + indents the returned statements (R4).
336
+ */
337
+ private capabilityPreconditions(scenario: ParsedScenario): Array<{ comment?: string; code: string; boundVars?: string[] }> {
338
+ discoverAndRegisterCapabilities();
339
+ const out: Array<{ comment?: string; code: string; boundVars?: string[] }> = [];
340
+ for (const cap of capabilityRegistry.all()) {
341
+ if (!cap.preconditionCodegen) continue;
342
+ out.push(...cap.preconditionCodegen({ tags: scenario.tags || [], screenName: this.queryScreenName, cwd: process.cwd() }));
343
+ }
344
+ return out;
320
345
  }
321
346
 
322
- ensureBaseFile(outputDir: string): void {
323
- const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
324
-
325
- const basePath = path.join(outputDir, 'base.ts');
326
- if (!fs.existsSync(basePath)) {
327
- const templatePath = path.join(templatesRoot, 'specs-base.ts');
328
- if (fs.existsSync(templatePath)) {
329
- const baseDir = path.dirname(basePath);
330
- if (!fs.existsSync(baseDir)) {
331
- fs.mkdirSync(baseDir, { recursive: true });
332
- }
333
- fs.copyFileSync(templatePath, basePath);
334
- console.log('✓ Created: specs/base.ts');
335
- }
336
- }
337
-
338
- // base.ts now depends on locale-fixture.ts — keep them paired.
339
- const localeFixturePath = path.join(outputDir, 'locale-fixture.ts');
340
- if (!fs.existsSync(localeFixturePath)) {
341
- const templatePath = path.join(templatesRoot, 'specs-locale-fixture.ts');
342
- if (fs.existsSync(templatePath)) {
343
- const baseDir = path.dirname(localeFixturePath);
344
- if (!fs.existsSync(baseDir)) {
345
- fs.mkdirSync(baseDir, { recursive: true });
346
- }
347
- fs.copyFileSync(templatePath, localeFixturePath);
348
- console.log('✓ Created: specs/locale-fixture.ts');
349
- }
350
- }
347
+ /**
348
+ * Copy an auto-generated runtime helper into specs/, **refreshing it when the template changed**.
349
+ * These files carry a `DO NOT EDIT — regenerated` header, so a stale copy left by an older sungen
350
+ * (e.g. a `specs/db.ts` from before a feature was added) is replaced instead of kept (issue #270).
351
+ * No-ops when the on-disk content already matches the template, so it stays quiet + idempotent.
352
+ */
353
+ private syncGeneratedHelper(outputDir: string, fileName: string, templateName: string): void {
354
+ const templatePath = path.join(__dirname, '..', '..', 'orchestrator', 'templates', templateName);
355
+ if (!fs.existsSync(templatePath)) return;
356
+ const targetPath = path.join(outputDir, fileName);
357
+ const next = fs.readFileSync(templatePath, 'utf8');
358
+ const exists = fs.existsSync(targetPath);
359
+ if (exists && fs.readFileSync(targetPath, 'utf8') === next) return; // already current
360
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
361
+ fs.writeFileSync(targetPath, next);
362
+ console.log(`✓ ${exists ? 'Updated' : 'Created'}: specs/${fileName}`);
363
+ }
351
364
 
365
+ ensureBaseFile(outputDir: string): void {
366
+ this.syncGeneratedHelper(outputDir, 'base.ts', 'specs-base.ts');
367
+ // base.ts depends on locale-fixture.ts — keep them paired.
368
+ this.syncGeneratedHelper(outputDir, 'locale-fixture.ts', 'specs-locale-fixture.ts');
352
369
  if (this.options.runtimeData) {
353
- const testDataPath = path.join(outputDir, 'test-data.ts');
354
- if (!fs.existsSync(testDataPath)) {
355
- const templatePath = path.join(templatesRoot, 'specs-test-data.ts');
356
- if (fs.existsSync(templatePath)) {
357
- fs.copyFileSync(templatePath, testDataPath);
358
- console.log('✓ Created: specs/test-data.ts');
359
- }
360
- }
370
+ this.syncGeneratedHelper(outputDir, 'test-data.ts', 'specs-test-data.ts');
361
371
  }
362
372
  }
363
373
 
@@ -391,6 +401,8 @@ export class CodeGenerator {
391
401
  }
392
402
  this.stepMapper.setScreenContext(effectiveScreenName);
393
403
  }
404
+ // Catalog-resolution screen name for @query binds (flows are prefixed `flows/`).
405
+ this.queryScreenName = isFlowFeature ? `flows/${effectiveScreenName}` : (effectiveScreenName || '');
394
406
 
395
407
  // Reset flow mode per feature to prevent state leak in --all mode
396
408
  this.stepMapper.setFlowMode(isFlowFeature);
@@ -637,6 +649,33 @@ export class CodeGenerator {
637
649
  // Set scenario context for path variable resolution (full merged list)
638
650
  this.stepMapper.setScenarioContext(stepsToMap);
639
651
 
652
+ // Data-driven (@cases): the scenario's {{col}} refs are dataset *row columns* that
653
+ // exist only at runtime — register them as captured so they resolve to a runtime
654
+ // get() (→ rowData.get) instead of compile-time YAML lookup.
655
+ const casesTag = scenario.tags.find((t) => t.startsWith('@cases:'));
656
+ const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
657
+ if (casesDataset) {
658
+ const refs = new Set<string>();
659
+ for (const st of stepsToMap) {
660
+ for (const m of (st.text || '').matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)) refs.add(m[1]);
661
+ }
662
+ for (const r of refs) this.stepMapper.registerCaptured(r);
663
+ }
664
+
665
+ // Capability preconditions (db `@query:<name>` → bind {{name}}) are owned by the capability via
666
+ // the SPI. Their bound `{{name.*}}` vars exist only at runtime → register them as captured so
667
+ // they resolve to a runtime get() instead of a compile-time YAML lookup that would fail.
668
+ const preconditions = this.capabilityPreconditions(scenario);
669
+ const boundVars = preconditions.flatMap((p) => p.boundVars || []);
670
+ if (boundVars.length) {
671
+ for (const st of stepsToMap) {
672
+ for (const mt of (st.text || '').matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
673
+ const head = mt[1].split(/[.[]/)[0];
674
+ if (boundVars.includes(head)) this.stepMapper.registerCaptured(mt[1]);
675
+ }
676
+ }
677
+ }
678
+
640
679
  const steps: Array<{ comment?: string; code: string }> = [];
641
680
 
642
681
  if (scenario.extendsName && this.stepsRegistry.has(scenario.extendsName)) {
@@ -666,17 +705,33 @@ export class CodeGenerator {
666
705
  }
667
706
  }
668
707
 
708
+ // Capability preconditions (db `@query:<name>` → bind {{name}}; computed above) run BEFORE the
709
+ // scenario's own steps — prepend them, indenting the capability-supplied statements.
710
+ if (preconditions.length) {
711
+ steps.unshift(...preconditions.map((p) => ({ comment: p.comment, code: this.indentCode(p.code, 4) })));
712
+ }
713
+
669
714
  // Extract pass-through tags (feature + scenario, excluding functional tags)
670
715
  const tags = extractPassThroughTags(scenario.tags, featureTags);
671
716
 
672
717
  // Use adapter to render scenario
673
- return this.adapter.renderScenario({
718
+ const rendered = this.adapter.renderScenario({
674
719
  scenarioName: scenario.name,
675
720
  steps,
676
721
  authRole,
677
722
  isParallel,
678
723
  tags,
724
+ casesDataset,
679
725
  });
726
+
727
+ // Data-driven (@cases): the per-row test() binds a row-scoped view (`rowData`).
728
+ // Pre-transform THIS scenario's runtime-data markers to read from `rowData`, so the
729
+ // global `testData` transform that runs next on the rest of the file leaves it alone.
730
+ // The loop header's `testData.cases()/withRow()` are literal code (no markers) → untouched.
731
+ if (casesDataset && this.options.runtimeData) {
732
+ return transformToRuntimeData(rendered, 'rowData');
733
+ }
734
+ return rendered;
680
735
  }
681
736
 
682
737
  /**
@@ -0,0 +1,49 @@
1
+ import { ParsedStep } from '../../gherkin-parser';
2
+ import { StepPattern, PatternContext } from './types';
3
+ import { MappedStep } from '../step-mapper';
4
+
5
+ /**
6
+ * Data-vs-data assertions — compare two runtime values directly (no UI element).
7
+ * The primary use is asserting on an `@query`-bound result via path access, e.g.
8
+ *
9
+ * Then expect {{products.count}} is {{expected}}
10
+ * Then expect {{products.count}} is at least {{one}}
11
+ * Then expect {{active_user.status}} is "active"
12
+ * Then expect {{order.total}} is not {{zero}}
13
+ *
14
+ * Both sides are `{{var|path}}` | "literal" | 'literal' | number. Generic — works for any
15
+ * test-data values, not only DB results.
16
+ */
17
+ const VALUE = String.raw`\{\{[^}]+\}\}|"[^"]*"|'[^']*'|-?\d+(?:\.\d+)?`;
18
+ const reExpect = new RegExp(`^\\s*(?:User\\s+)?expect\\s+(${VALUE})\\s+is\\s+(not\\s+|at\\s+least\\s+|at\\s+most\\s+)?(${VALUE})\\s*$`, 'i');
19
+
20
+ /** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
21
+ function valueExpr(token: string): string {
22
+ const t = token.trim();
23
+ const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
24
+ if (v) return `testData.get(${JSON.stringify(v[1])})`;
25
+ const q = t.match(/^["'](.*)["']$/);
26
+ if (q) return JSON.stringify(q[1]);
27
+ if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
28
+ return JSON.stringify(t);
29
+ }
30
+
31
+ export const expectPatterns: StepPattern[] = [
32
+ {
33
+ name: 'expect-data',
34
+ priority: 62, // above generic see-assertions; sibling of the DB assertions
35
+ matcher: (step: ParsedStep) => reExpect.test(step.text),
36
+ generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
37
+ const m = step.text.match(reExpect)!;
38
+ const a = valueExpr(m[1]);
39
+ const op = (m[2] || '').trim().toLowerCase();
40
+ const b = valueExpr(m[3]);
41
+ let code: string;
42
+ if (op === 'at least') code = `expect(Number(${a})).toBeGreaterThanOrEqual(Number(${b}));`;
43
+ else if (op === 'at most') code = `expect(Number(${a})).toBeLessThanOrEqual(Number(${b}));`;
44
+ else if (op === 'not') code = `expect(String(${a})).not.toBe(String(${b}));`;
45
+ else code = `expect(String(${a})).toBe(String(${b}));`;
46
+ return { code, comment: `Expect ${m[1]} ${op ? op + ' ' : 'is '}${m[3]}` };
47
+ },
48
+ },
49
+ ];
@@ -1,17 +1,8 @@
1
1
  import { ParsedStep } from '../../gherkin-parser';
2
2
  import { MappedStep } from '../step-mapper';
3
3
  import { StepPattern, PatternContext } from './types';
4
- import { navigationPatterns } from './navigation-patterns';
5
- import { formPatterns } from './form-patterns';
6
- import { interactionPatterns } from './interaction-patterns';
7
- import { assertionPatterns } from './assertion-patterns';
8
- import { setupPatterns } from './setup-patterns';
9
- import { keyboardPatterns } from './keyboard-patterns';
10
- import { scrollPatterns } from './scroll-patterns';
11
- import { scopePatterns } from './scope-patterns';
12
- import { tablePatterns } from './table-patterns';
13
- import { capturePatterns } from './capture-patterns';
14
- import { databasePatterns } from './database-patterns';
4
+ import { capabilityRegistry } from '../../../capabilities/registry';
5
+ import { discoverAndRegisterCapabilities } from '../../../capabilities/discover';
15
6
 
16
7
  /**
17
8
  * Pattern Registry - manages all step patterns
@@ -27,17 +18,11 @@ export class PatternRegistry {
27
18
  * Register default patterns from all pattern modules
28
19
  */
29
20
  private registerDefaultPatterns(): void {
30
- this.patterns.push(...setupPatterns);
31
- this.patterns.push(...navigationPatterns);
32
- this.patterns.push(...formPatterns);
33
- this.patterns.push(...interactionPatterns);
34
- this.patterns.push(...assertionPatterns);
35
- this.patterns.push(...keyboardPatterns);
36
- this.patterns.push(...scrollPatterns);
37
- this.patterns.push(...scopePatterns);
38
- this.patterns.push(...tablePatterns);
39
- this.patterns.push(...capturePatterns);
40
- this.patterns.push(...databasePatterns);
21
+ // Patterns are composed from the capability registry (Capability SPI, R1) instead of a
22
+ // hardcoded push list. Built-ins (ui · db · core) register the same set as before, so the
23
+ // composed list + priority sort is behaviour-identical (golden snapshots are the contract).
24
+ discoverAndRegisterCapabilities();
25
+ this.patterns.push(...capabilityRegistry.patterns());
41
26
 
42
27
  // Sort by priority (higher first)
43
28
  this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
@@ -157,15 +142,6 @@ export class PatternRegistry {
157
142
  }
158
143
  }
159
144
 
160
- // Export pattern modules for advanced usage
161
- export { setupPatterns } from './setup-patterns';
162
- export { navigationPatterns } from './navigation-patterns';
163
- export { formPatterns } from './form-patterns';
164
- export { interactionPatterns } from './interaction-patterns';
165
- export { assertionPatterns } from './assertion-patterns';
166
- export { keyboardPatterns } from './keyboard-patterns';
167
- export { scrollPatterns } from './scroll-patterns';
168
- export { scopePatterns } from './scope-patterns';
169
- export { tablePatterns } from './table-patterns';
170
- export { databasePatterns, isDbStep } from './database-patterns';
145
+ // The UI step patterns now live in @sungen/driver-ui (R5.4) and the DB patterns in @sungen/driver-db
146
+ // (R5.5); both are contributed via the capability registry, not re-exported here.
171
147
  export * from './types';
@@ -94,6 +94,15 @@ export class StepMapper {
94
94
  this.templateEngine.resetBaseContext();
95
95
  }
96
96
 
97
+ /**
98
+ * Register a runtime data variable so `{{name}}` resolves to `testData.get('name')`
99
+ * (skipping compile-time YAML validation). Used for @cases row columns, which exist
100
+ * only at runtime in the dataset rows. Scenario-scoped (cleared by setScenarioContext).
101
+ */
102
+ registerCaptured(name: string): void {
103
+ this.dataResolver.registerCaptured(name);
104
+ }
105
+
97
106
  /**
98
107
  * Map a Gherkin step to Playwright code
99
108
  * Uses pattern registry first, falls back to AI if enabled
@@ -229,8 +229,8 @@ export class TemplateEngine {
229
229
  this.baseContext = {};
230
230
  }
231
231
 
232
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
233
- return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb });
232
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
233
+ return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi });
234
234
  }
235
235
 
236
236
  renderTestFile(data: {
@@ -284,6 +284,9 @@ export class TemplateEngine {
284
284
  scenarioName: string;
285
285
  steps: Array<{ comment?: string; code: string }>;
286
286
  authRole?: string;
287
+ isParallel?: boolean;
288
+ tags?: string;
289
+ casesDataset?: string;
287
290
  }): string {
288
291
  return this.render('scenario', data);
289
292
  }
@@ -4,7 +4,7 @@ const MARKER_PATTERN = /__SUNGEN_TD_([A-Za-z0-9_]+)__/;
4
4
  * Replace __SUNGEN_TD_ markers with testData.get() calls in generated code.
5
5
  * Three passes: comments, string literals, then regex literals.
6
6
  */
7
- export function transformToRuntimeData(code: string): string {
7
+ export function transformToRuntimeData(code: string, accessor: string = 'testData'): string {
8
8
  // Pass 0: Comments — replace markers in // comments with decoded key name
9
9
  // Prevents Pass 2 from misinterpreting // comment markers as regex delimiters
10
10
  code = code.replace(
@@ -20,9 +20,9 @@ export function transformToRuntimeData(code: string): string {
20
20
  (_, _quote, prefix, enc, suffix) => {
21
21
  const key = decodeKey(enc);
22
22
  if (!prefix && !suffix) {
23
- return `testData.get('${key}')`;
23
+ return `${accessor}.get('${key}')`;
24
24
  }
25
- return `\`${prefix}\${testData.get('${key}')}${suffix}\``;
25
+ return `\`${prefix}\${${accessor}.get('${key}')}${suffix}\``;
26
26
  }
27
27
  );
28
28
 
@@ -32,7 +32,7 @@ export function transformToRuntimeData(code: string): string {
32
32
  /\/((?:[^/\\\n]|\\.)*?)__SUNGEN_TD_([A-Za-z0-9_]+)__((?:[^/\\\n]|\\.)*?)\/([gimsuy]*)/g,
33
33
  (_, prefix, enc, suffix, flags) => {
34
34
  const key = decodeKey(enc);
35
- const ref = `testData.get('${key}')`;
35
+ const ref = `${accessor}.get('${key}')`;
36
36
  const flagStr = flags ? `, '${flags}'` : '';
37
37
  if (!prefix && !suffix) return `new RegExp(${ref}${flagStr})`;
38
38
  return `new RegExp(\`${prefix}\${${ref}}${suffix}\`${flagStr})`;
@@ -44,7 +44,7 @@ export function transformToRuntimeData(code: string): string {
44
44
  // table/list count templates). testData.get() returns a string, so coerce with Number().
45
45
  code = code.replace(
46
46
  /__SUNGEN_TD_([A-Za-z0-9_]+)__/g,
47
- (_, enc) => `Number(testData.get('${decodeKey(enc)}'))`
47
+ (_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`
48
48
  );
49
49
 
50
50
  return code;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared annotation-override grammar for precondition annotations (`@query`/`@api`).
3
+ *
4
+ * Parses `name(a={{x}},b="lit",c=3)` overrides into a map of JS expressions, e.g.
5
+ * `{ a: "testData.get('x')", b: "\"lit\"", c: "3" }`. Used by the DB and API capability drivers'
6
+ * precondition codegen; lives in core so both drivers (and core's `api` until R5.6) can share it.
7
+ */
8
+ export function parseQueryOverrides(raw?: string): Record<string, string> {
9
+ const out: Record<string, string> = {};
10
+ if (!raw) return out;
11
+ for (const part of raw.split(',')) {
12
+ const eq = part.indexOf('=');
13
+ if (eq < 0) continue;
14
+ const key = part.slice(0, eq).trim();
15
+ const val = part.slice(eq + 1).trim();
16
+ if (!key) continue;
17
+ const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
18
+ const q = val.match(/^["'](.*)["']$/);
19
+ if (v) out[key] = `testData.get(${JSON.stringify(v[1])})`;
20
+ else if (q) out[key] = JSON.stringify(q[1]);
21
+ else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
22
+ else out[key] = JSON.stringify(val);
23
+ }
24
+ return out;
25
+ }