@sun-asterisk/sungen 3.0.1 → 3.1.1
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.
- package/dist/cli/commands/challenge.d.ts.map +1 -1
- package/dist/cli/commands/challenge.js +9 -2
- package/dist/cli/commands/challenge.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +3 -2
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +8 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +92 -76
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +3 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
- package/dist/generators/test-generator/code-generator.d.ts +12 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +137 -4
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +6 -0
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/database-patterns.js +95 -0
- package/dist/generators/test-generator/patterns/database-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +3 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js +54 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +8 -1
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +8 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +4 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +5 -5
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/harness/audit.js +1 -1
- package/dist/harness/capability-plan.js +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +1 -1
- package/dist/harness/challenge.d.ts +1 -0
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +49 -2
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts +7 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -0
- package/dist/harness/data-driven-lint.js +153 -0
- package/dist/harness/data-driven-lint.js.map +1 -0
- package/dist/harness/flow-plan.js +1 -1
- package/dist/harness/parse.d.ts +2 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/query-catalog.d.ts +48 -0
- package/dist/harness/query-catalog.d.ts.map +1 -0
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +11 -5
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +40 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +40 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/specs-db.d.ts +26 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-db.js +193 -0
- package/dist/orchestrator/templates/specs-db.js.map +1 -0
- package/dist/orchestrator/templates/specs-db.ts +169 -0
- package/dist/orchestrator/templates/specs-test-data.ts +76 -15
- package/docs/orchestration-spec.md +3 -3
- package/package.json +2 -2
- package/src/cli/commands/challenge.ts +6 -2
- package/src/cli/commands/delivery.ts +3 -2
- package/src/cli/commands/generate.ts +8 -0
- package/src/exporters/csv-exporter.ts +22 -6
- package/src/exporters/spec-parser.ts +3 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
- package/src/generators/test-generator/code-generator.ts +133 -4
- package/src/generators/test-generator/patterns/database-patterns.ts +96 -0
- package/src/generators/test-generator/patterns/expect-patterns.ts +49 -0
- package/src/generators/test-generator/patterns/index.ts +5 -0
- package/src/generators/test-generator/step-mapper.ts +9 -0
- package/src/generators/test-generator/template-engine.ts +5 -2
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +5 -5
- package/src/harness/audit.ts +1 -1
- package/src/harness/capability-plan.ts +1 -1
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/catalog/universal-viewpoints.yaml +1 -1
- package/src/harness/challenge.ts +47 -2
- package/src/harness/data-driven-lint.ts +119 -0
- package/src/harness/flow-plan.ts +1 -1
- package/src/harness/parse.ts +12 -0
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/script-check.ts +12 -6
- package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +40 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +40 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/specs-db.ts +169 -0
- package/src/orchestrator/templates/specs-test-data.ts +76 -15
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import { CodeGenerator } from '../../generators/test-generator/code-generator';
|
|
5
5
|
import { adapterRegistry } from '../../generators/test-generator/adapters';
|
|
6
6
|
import { scanTestDataSecrets } from '../../harness/secret-scan';
|
|
7
|
+
import { lintDataDriven } from '../../harness/data-driven-lint';
|
|
7
8
|
import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -182,6 +183,13 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
182
183
|
console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
// Data-driven lint (@cases / @query) — advisory, never blocks generation.
|
|
187
|
+
const ddWarnings = scanDirs.flatMap((d) => lintDataDriven(d, cwd));
|
|
188
|
+
if (ddWarnings.length) {
|
|
189
|
+
console.log(`\n⚠️ Data-driven lint (@cases / @query) — review:`);
|
|
190
|
+
for (const w of ddWarnings.slice(0, 20)) console.log(` ${w.scenario ? w.scenario + ': ' : ''}${w.message}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
185
193
|
console.log(`Next step: npx playwright test --ui\n`);
|
|
186
194
|
} catch (error) {
|
|
187
195
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
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,9 @@ 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
|
-
|
|
23
|
+
// Quote char may be ' " or ` — the backtick form is a data-driven (@cases) title
|
|
24
|
+
// like `VP-… — ${__row.__label}`.
|
|
25
|
+
const testRegex = /test\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{[^}]*\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
|
|
24
26
|
testRegex.lastIndex = startIdx;
|
|
25
27
|
const match = testRegex.exec(content);
|
|
26
28
|
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 }): string;
|
|
67
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: 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 }): string {
|
|
29
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -3,6 +3,9 @@ import { type Page, type BrowserContext } from '@playwright/test';
|
|
|
3
3
|
{{#if runtimeData}}
|
|
4
4
|
import { TestDataLoader } from '{{basePath}}/test-data';
|
|
5
5
|
{{/if}}
|
|
6
|
+
{{#if needsDb}}
|
|
7
|
+
import { db } from '{{basePath}}/db';
|
|
8
|
+
{{/if}}
|
|
6
9
|
|
|
7
10
|
// This file is auto-generated from Gherkin feature files
|
|
8
11
|
// 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,6 +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';
|
|
8
|
+
import { resolveQuery, compileQuery } from '../../harness/query-catalog';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Filter base scenario steps for @extend: only keep Given→When steps.
|
|
@@ -73,7 +75,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
|
|
|
73
75
|
const FUNCTIONAL_TAG_PREFIXES = [
|
|
74
76
|
'@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
75
77
|
'@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
|
|
76
|
-
'@flow',
|
|
78
|
+
'@flow', '@cases:',
|
|
77
79
|
];
|
|
78
80
|
|
|
79
81
|
function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
|
|
@@ -169,6 +171,8 @@ export class CodeGenerator {
|
|
|
169
171
|
private adapter: TestGeneratorAdapter;
|
|
170
172
|
private screenName?: string;
|
|
171
173
|
private options: any;
|
|
174
|
+
// Screen/flow name for the CURRENT feature, in catalog-resolution form (`flows/<x>` for flows).
|
|
175
|
+
private queryScreenName: string = '';
|
|
172
176
|
// Steps registry built per feature during generateTestCode(); used by countSteps()
|
|
173
177
|
private stepsRegistry = new Map<string, ParsedScenario>();
|
|
174
178
|
|
|
@@ -236,7 +240,11 @@ export class CodeGenerator {
|
|
|
236
240
|
const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
|
|
237
241
|
const needsCleanupImport = !isParallelFeature && hasCleanupTags;
|
|
238
242
|
|
|
239
|
-
|
|
243
|
+
// Data Driver: if any step verifies DB state, import the `db` helper + emit specs/db.ts
|
|
244
|
+
const needsDb = this.featureUsesDb(feature);
|
|
245
|
+
if (needsDb) this.ensureDbFile(outputDir);
|
|
246
|
+
|
|
247
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb });
|
|
240
248
|
|
|
241
249
|
// Generate test code (async now to support AI mapping)
|
|
242
250
|
const testCode = await this.generateTestCode(feature);
|
|
@@ -292,8 +300,82 @@ export class CodeGenerator {
|
|
|
292
300
|
/**
|
|
293
301
|
* Ensure specs/base.ts exists in the output directory
|
|
294
302
|
*/
|
|
303
|
+
/** True when any step (background or scenario) in the feature is a DB-verification step. */
|
|
304
|
+
private featureUsesDb(feature: ParsedFeature): boolean {
|
|
305
|
+
const steps: ParsedStep[] = [];
|
|
306
|
+
if (feature.background?.steps) steps.push(...feature.background.steps);
|
|
307
|
+
for (const sc of feature.scenarios || []) if (sc.steps) steps.push(...sc.steps);
|
|
308
|
+
if (steps.some((s) => s && typeof s.text === 'string' && isDbStep(s.text))) return true;
|
|
309
|
+
// A scenario may carry only a @query:<name> post-condition tag (no DB step in its body).
|
|
310
|
+
return (feature.scenarios || []).some((sc) => (sc.tags || []).some((t) => t.startsWith('@query:')));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Build the precondition bind steps for a scenario's `@query:<name>[(p={{v}},…)]` tags.
|
|
315
|
+
* Each runs the catalog query and binds the result array to a `{{name}}` variable.
|
|
316
|
+
*/
|
|
317
|
+
private buildQueryBinds(scenario: ParsedScenario): Array<{ comment?: string; code: string }> {
|
|
318
|
+
const out: Array<{ comment?: string; code: string }> = [];
|
|
319
|
+
const TAG = /^@query:([A-Za-z_][A-Za-z0-9_]*)(?:\((.*)\))?$/;
|
|
320
|
+
for (const tag of scenario.tags || []) {
|
|
321
|
+
const m = tag.match(TAG);
|
|
322
|
+
if (!m) continue;
|
|
323
|
+
const name = m[1];
|
|
324
|
+
const overrides = this.parseQueryOverrides(m[2]);
|
|
325
|
+
const entry = resolveQuery(name, this.queryScreenName); // throws (fail-fast) if missing/ambiguous
|
|
326
|
+
const { sql, paramNames } = compileQuery(entry);
|
|
327
|
+
const paramExprs = paramNames.map((p) =>
|
|
328
|
+
p in overrides ? overrides[p] : `testData.get(${JSON.stringify(p)})`,
|
|
329
|
+
);
|
|
330
|
+
const label = JSON.stringify(entry.description ? `query "${name}" — ${entry.description}` : `query "${name}"`);
|
|
331
|
+
const ds = entry.datasource ? JSON.stringify(entry.datasource) : 'undefined';
|
|
332
|
+
out.push({
|
|
333
|
+
comment: `@query:${name} → bind {{${name}}} from ${entry.datasource || 'default datasource'}`,
|
|
334
|
+
code: this.indentCode(
|
|
335
|
+
`testData.bind(${JSON.stringify(name)}, await db.fetchQuery(${label}, ${JSON.stringify(sql)}, [${paramExprs.join(', ')}], ${ds}));`,
|
|
336
|
+
4,
|
|
337
|
+
),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Parse `@query:name(a={{x}},b="lit",c=3)` overrides → { a: "testData.get('x')", … } JS exprs. */
|
|
344
|
+
private parseQueryOverrides(raw?: string): Record<string, string> {
|
|
345
|
+
const out: Record<string, string> = {};
|
|
346
|
+
if (!raw) return out;
|
|
347
|
+
for (const part of raw.split(',')) {
|
|
348
|
+
const eq = part.indexOf('=');
|
|
349
|
+
if (eq < 0) continue;
|
|
350
|
+
const key = part.slice(0, eq).trim();
|
|
351
|
+
const val = part.slice(eq + 1).trim();
|
|
352
|
+
if (!key) continue;
|
|
353
|
+
const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
354
|
+
const q = val.match(/^["'](.*)["']$/);
|
|
355
|
+
if (v) out[key] = `testData.get(${JSON.stringify(v[1])})`;
|
|
356
|
+
else if (q) out[key] = JSON.stringify(q[1]);
|
|
357
|
+
else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
|
|
358
|
+
else out[key] = JSON.stringify(val);
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Copy the Data Driver runtime helper into specs/db.ts (idempotent). */
|
|
364
|
+
ensureDbFile(outputDir: string): void {
|
|
365
|
+
const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
|
|
366
|
+
const dbPath = path.join(outputDir, 'db.ts');
|
|
367
|
+
if (!fs.existsSync(dbPath)) {
|
|
368
|
+
const templatePath = path.join(templatesRoot, 'specs-db.ts');
|
|
369
|
+
if (fs.existsSync(templatePath)) {
|
|
370
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
371
|
+
fs.copyFileSync(templatePath, dbPath);
|
|
372
|
+
console.log('✓ Created: specs/db.ts');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
295
377
|
ensureBaseFile(outputDir: string): void {
|
|
296
|
-
const templatesRoot = path.join(__dirname, '..', '..', '
|
|
378
|
+
const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
|
|
297
379
|
|
|
298
380
|
const basePath = path.join(outputDir, 'base.ts');
|
|
299
381
|
if (!fs.existsSync(basePath)) {
|
|
@@ -364,6 +446,8 @@ export class CodeGenerator {
|
|
|
364
446
|
}
|
|
365
447
|
this.stepMapper.setScreenContext(effectiveScreenName);
|
|
366
448
|
}
|
|
449
|
+
// Catalog-resolution screen name for @query binds (flows are prefixed `flows/`).
|
|
450
|
+
this.queryScreenName = isFlowFeature ? `flows/${effectiveScreenName}` : (effectiveScreenName || '');
|
|
367
451
|
|
|
368
452
|
// Reset flow mode per feature to prevent state leak in --all mode
|
|
369
453
|
this.stepMapper.setFlowMode(isFlowFeature);
|
|
@@ -610,6 +694,35 @@ export class CodeGenerator {
|
|
|
610
694
|
// Set scenario context for path variable resolution (full merged list)
|
|
611
695
|
this.stepMapper.setScenarioContext(stepsToMap);
|
|
612
696
|
|
|
697
|
+
// Data-driven (@cases): the scenario's {{col}} refs are dataset *row columns* that
|
|
698
|
+
// exist only at runtime — register them as captured so they resolve to a runtime
|
|
699
|
+
// get() (→ rowData.get) instead of compile-time YAML lookup.
|
|
700
|
+
const casesTag = scenario.tags.find((t) => t.startsWith('@cases:'));
|
|
701
|
+
const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
|
|
702
|
+
if (casesDataset) {
|
|
703
|
+
const refs = new Set<string>();
|
|
704
|
+
for (const st of stepsToMap) {
|
|
705
|
+
for (const m of (st.text || '').matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)) refs.add(m[1]);
|
|
706
|
+
}
|
|
707
|
+
for (const r of refs) this.stepMapper.registerCaptured(r);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// @query-bound vars: `{{name.col}}` / `{{name[2].col}}` / `{{name.count}}` exist only at
|
|
711
|
+
// runtime (the bind fetches them) — register as captured so they resolve to a runtime get()
|
|
712
|
+
// instead of a compile-time YAML lookup that would fail.
|
|
713
|
+
const queryNames = (scenario.tags || [])
|
|
714
|
+
.map((t) => t.match(/^@query:([A-Za-z_][A-Za-z0-9_]*)/))
|
|
715
|
+
.filter((m): m is RegExpMatchArray => !!m)
|
|
716
|
+
.map((m) => m[1]);
|
|
717
|
+
if (queryNames.length) {
|
|
718
|
+
for (const st of stepsToMap) {
|
|
719
|
+
for (const mt of (st.text || '').matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
|
|
720
|
+
const head = mt[1].split(/[.[]/)[0];
|
|
721
|
+
if (queryNames.includes(head)) this.stepMapper.registerCaptured(mt[1]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
613
726
|
const steps: Array<{ comment?: string; code: string }> = [];
|
|
614
727
|
|
|
615
728
|
if (scenario.extendsName && this.stepsRegistry.has(scenario.extendsName)) {
|
|
@@ -639,17 +752,33 @@ export class CodeGenerator {
|
|
|
639
752
|
}
|
|
640
753
|
}
|
|
641
754
|
|
|
755
|
+
// @query:<name>[(p={{v}},…)] tags → run the catalog query as a PRECONDITION and bind its
|
|
756
|
+
// result to a `{{name}}` variable (scenario asserts on it with `expect …` + path access).
|
|
757
|
+
// Prepended so the binds run before the scenario's own steps.
|
|
758
|
+
const queryBinds = this.buildQueryBinds(scenario);
|
|
759
|
+
if (queryBinds.length) steps.unshift(...queryBinds);
|
|
760
|
+
|
|
642
761
|
// Extract pass-through tags (feature + scenario, excluding functional tags)
|
|
643
762
|
const tags = extractPassThroughTags(scenario.tags, featureTags);
|
|
644
763
|
|
|
645
764
|
// Use adapter to render scenario
|
|
646
|
-
|
|
765
|
+
const rendered = this.adapter.renderScenario({
|
|
647
766
|
scenarioName: scenario.name,
|
|
648
767
|
steps,
|
|
649
768
|
authRole,
|
|
650
769
|
isParallel,
|
|
651
770
|
tags,
|
|
771
|
+
casesDataset,
|
|
652
772
|
});
|
|
773
|
+
|
|
774
|
+
// Data-driven (@cases): the per-row test() binds a row-scoped view (`rowData`).
|
|
775
|
+
// Pre-transform THIS scenario's runtime-data markers to read from `rowData`, so the
|
|
776
|
+
// global `testData` transform that runs next on the rest of the file leaves it alone.
|
|
777
|
+
// The loop header's `testData.cases()/withRow()` are literal code (no markers) → untouched.
|
|
778
|
+
if (casesDataset && this.options.runtimeData) {
|
|
779
|
+
return transformToRuntimeData(rendered, 'rowData');
|
|
780
|
+
}
|
|
781
|
+
return rendered;
|
|
653
782
|
}
|
|
654
783
|
|
|
655
784
|
/**
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ParsedStep } from '../../gherkin-parser';
|
|
2
|
+
import { StepPattern, PatternContext } from './types';
|
|
3
|
+
import { MappedStep } from '../step-mapper';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database verification patterns (Data Driver v1) — declarative, no-SQL DB assertions
|
|
7
|
+
* that compile to calls on the runtime `db` helper (specs/db.ts). Read-only.
|
|
8
|
+
*
|
|
9
|
+
* User see [users] row where [email] is {{reg_email}}
|
|
10
|
+
* User see [users] row where [email] is {{reg_email}} has [status] = "active"
|
|
11
|
+
* User see [users] no row where [email] is {{dup_email}}
|
|
12
|
+
* User see [orders] where [buyer] is {{buyer}} count is {{expected_count}}
|
|
13
|
+
*
|
|
14
|
+
* Identifiers ([table]/[column]) are validated by the helper; values bind as parameters.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TABLE = String.raw`\[([A-Za-z_][A-Za-z0-9_]*)\]`;
|
|
18
|
+
const VALUE = String.raw`\{\{[^}]+\}\}|"[^"]*"|'[^']*'|-?\d+(?:\.\d+)?`;
|
|
19
|
+
|
|
20
|
+
const reRow = new RegExp(`see\\s+${TABLE}\\s+row\\s+where\\b`, 'i');
|
|
21
|
+
const reNoRow = new RegExp(`see\\s+${TABLE}\\s+no\\s+row\\s+where\\b`, 'i');
|
|
22
|
+
const reCount = new RegExp(`see\\s+${TABLE}.*\\bcount\\s+is\\b`, 'i');
|
|
23
|
+
|
|
24
|
+
/** True when a step is a declarative DB-verification step (used to wire the `db` import).
|
|
25
|
+
* Named queries are invoked via the `@query:` annotation, not a step — see code-generator. */
|
|
26
|
+
export function isDbStep(text: string): boolean {
|
|
27
|
+
return reNoRow.test(text) || reRow.test(text) || reCount.test(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
|
|
31
|
+
function valueExpr(token: string): string {
|
|
32
|
+
const t = token.trim();
|
|
33
|
+
const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
34
|
+
if (v) return `testData.get(${JSON.stringify(v[1])})`;
|
|
35
|
+
const q = t.match(/^["'](.*)["']$/);
|
|
36
|
+
if (q) return JSON.stringify(q[1]);
|
|
37
|
+
if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
|
|
38
|
+
return JSON.stringify(t);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parse a `[col] is VALUE [and [col2] is VALUE2]` segment into a JS object literal. */
|
|
42
|
+
function parseFilter(segment: string): string {
|
|
43
|
+
const re = new RegExp(`\\[([A-Za-z_][A-Za-z0-9_]*)\\]\\s+is\\s+(${VALUE})`, 'gi');
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
let m: RegExpExecArray | null;
|
|
46
|
+
while ((m = re.exec(segment))) parts.push(`${JSON.stringify(m[1])}: ${valueExpr(m[2])}`);
|
|
47
|
+
return `{ ${parts.join(', ')} }`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse a `has [col] = VALUE [and [col2] = VALUE2]` segment into a JS object literal. */
|
|
51
|
+
function parseExpected(segment: string): string {
|
|
52
|
+
const re = new RegExp(`\\[([A-Za-z_][A-Za-z0-9_]*)\\]\\s*=\\s*(${VALUE})`, 'gi');
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
let m: RegExpExecArray | null;
|
|
55
|
+
while ((m = re.exec(segment))) parts.push(`${JSON.stringify(m[1])}: ${valueExpr(m[2])}`);
|
|
56
|
+
return parts.length ? `{ ${parts.join(', ')} }` : '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const databasePatterns: StepPattern[] = [
|
|
60
|
+
{
|
|
61
|
+
name: 'db-no-row',
|
|
62
|
+
priority: 60, // above generic see-assertions
|
|
63
|
+
matcher: (step: ParsedStep) => reNoRow.test(step.text),
|
|
64
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
65
|
+
const m = step.text.match(new RegExp(`${TABLE}\\s+no\\s+row\\s+where\\s+(.+)$`, 'i'))!;
|
|
66
|
+
const table = m[1];
|
|
67
|
+
const filter = parseFilter(m[2]);
|
|
68
|
+
return { code: `await db.assertNoRow(${JSON.stringify(table)}, ${filter});`, comment: `DB: no row in ${table}` };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'db-count',
|
|
73
|
+
priority: 60,
|
|
74
|
+
matcher: (step: ParsedStep) => reCount.test(step.text) && !reRow.test(step.text),
|
|
75
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
76
|
+
const m = step.text.match(new RegExp(`${TABLE}(?:\\s+where\\s+(.+?))?\\s+count\\s+is\\s+(${VALUE})`, 'i'))!;
|
|
77
|
+
const table = m[1];
|
|
78
|
+
const filter = m[2] ? parseFilter(m[2]) : '{}';
|
|
79
|
+
return { code: `await db.assertCount(${JSON.stringify(table)}, ${filter}, Number(${valueExpr(m[3])}));`, comment: `DB: count rows in ${table}` };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'db-row',
|
|
84
|
+
priority: 60,
|
|
85
|
+
matcher: (step: ParsedStep) => reRow.test(step.text),
|
|
86
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
87
|
+
// [table] row where <filter> [has <expected>]
|
|
88
|
+
const m = step.text.match(new RegExp(`${TABLE}\\s+row\\s+where\\s+(.+?)(?:\\s+has\\s+(.+))?$`, 'i'))!;
|
|
89
|
+
const table = m[1];
|
|
90
|
+
const filter = parseFilter(m[2]);
|
|
91
|
+
const expected = m[3] ? parseExpected(m[3]) : '';
|
|
92
|
+
const args = expected ? `${JSON.stringify(table)}, ${filter}, ${expected}` : `${JSON.stringify(table)}, ${filter}`;
|
|
93
|
+
return { code: `await db.assertRow(${args});`, comment: `DB: row in ${table}` };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
@@ -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
|
+
];
|
|
@@ -11,6 +11,8 @@ import { scrollPatterns } from './scroll-patterns';
|
|
|
11
11
|
import { scopePatterns } from './scope-patterns';
|
|
12
12
|
import { tablePatterns } from './table-patterns';
|
|
13
13
|
import { capturePatterns } from './capture-patterns';
|
|
14
|
+
import { databasePatterns } from './database-patterns';
|
|
15
|
+
import { expectPatterns } from './expect-patterns';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Pattern Registry - manages all step patterns
|
|
@@ -36,6 +38,8 @@ export class PatternRegistry {
|
|
|
36
38
|
this.patterns.push(...scopePatterns);
|
|
37
39
|
this.patterns.push(...tablePatterns);
|
|
38
40
|
this.patterns.push(...capturePatterns);
|
|
41
|
+
this.patterns.push(...databasePatterns);
|
|
42
|
+
this.patterns.push(...expectPatterns);
|
|
39
43
|
|
|
40
44
|
// Sort by priority (higher first)
|
|
41
45
|
this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
@@ -165,4 +169,5 @@ export { keyboardPatterns } from './keyboard-patterns';
|
|
|
165
169
|
export { scrollPatterns } from './scroll-patterns';
|
|
166
170
|
export { scopePatterns } from './scope-patterns';
|
|
167
171
|
export { tablePatterns } from './table-patterns';
|
|
172
|
+
export { databasePatterns, isDbStep } from './database-patterns';
|
|
168
173
|
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 }): string {
|
|
233
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport });
|
|
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 });
|
|
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
|
|
23
|
+
return `${accessor}.get('${key}')`;
|
|
24
24
|
}
|
|
25
|
-
return `\`${prefix}\${
|
|
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 =
|
|
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(
|
|
47
|
+
(_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
return code;
|
package/src/harness/audit.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The score is INTENTIONALLY weighted toward business-critical coverage/depth
|
|
6
6
|
* (not breadth), so it surfaces the gaps a count-based view hides. See
|
|
7
|
-
* docs/orchestration-spec.md §5 and
|
|
7
|
+
* docs/orchestration-spec.md §5 and docs/spec/sungen_refactor_spec.md.
|
|
8
8
|
*/
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Classifies each scenario's execution mode + each @manual case by reason code
|
|
5
5
|
* (M1–M9), maps capability-reasons to drivers, and emits the manual-reason KPI.
|
|
6
6
|
* Never installs anything (that's `sungen capability add`). See
|
|
7
|
-
*
|
|
7
|
+
* docs/spec/sungen_phase2b_spec.md.
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Driver Catalog (metadata only — NO driver code is bundled here).
|
|
2
2
|
# Lets Sungen RECOMMEND/RESOLVE a driver that may not be installed yet, and tells
|
|
3
|
-
# `sungen capability add` which package to install. See
|
|
3
|
+
# `sungen capability add` which package to install. See docs/spec/sungen_phase2a_spec.md.
|
|
4
4
|
#
|
|
5
5
|
# kind: platform → the runtime/codegen adapter for a target (pick ONE per project)
|
|
6
6
|
# kind: capability → an extra ability added on top of a platform (Phase 3)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Each page-type lists must-cover themes. A theme is "covered" when the project's
|
|
7
7
|
# viewpoint-overview (or generated scenarios) contains one of its keywords.
|
|
8
|
-
# See docs/orchestration-spec.md §5.2 and
|
|
8
|
+
# See docs/orchestration-spec.md §5.2 and docs/spec/sungen_refactor_spec.md §9.
|
|
9
9
|
#
|
|
10
10
|
# `depth:` (optional, harness-roadmap P1) marks a theme as DATA-correctness:
|
|
11
11
|
# requires: data-assertion → scenarios on this theme must assert DATA (not just
|