@sun-asterisk/sungen 3.2.20-beta.5 → 3.2.21-beta.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.
Files changed (35) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +2 -0
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/exporters/api-testcase-formatter.d.ts +8 -0
  5. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  6. package/dist/exporters/api-testcase-formatter.js +44 -22
  7. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  8. package/dist/exporters/matrix/build.d.ts +4 -2
  9. package/dist/exporters/matrix/build.d.ts.map +1 -1
  10. package/dist/exporters/matrix/build.js +25 -2
  11. package/dist/exporters/matrix/build.js.map +1 -1
  12. package/dist/exporters/matrix/export.d.ts +6 -0
  13. package/dist/exporters/matrix/export.d.ts.map +1 -1
  14. package/dist/exporters/matrix/export.js +9 -1
  15. package/dist/exporters/matrix/export.js.map +1 -1
  16. package/dist/exporters/matrix/gates.js +7 -1
  17. package/dist/exporters/matrix/gates.js.map +1 -1
  18. package/dist/exporters/matrix/types.d.ts +5 -0
  19. package/dist/exporters/matrix/types.d.ts.map +1 -1
  20. package/dist/exporters/matrix/types.js.map +1 -1
  21. package/dist/harness/script-check.d.ts.map +1 -1
  22. package/dist/harness/script-check.js +17 -4
  23. package/dist/harness/script-check.js.map +1 -1
  24. package/dist/harness/spec-coverage.d.ts.map +1 -1
  25. package/dist/harness/spec-coverage.js +7 -2
  26. package/dist/harness/spec-coverage.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/cli/commands/delivery.ts +2 -0
  29. package/src/exporters/api-testcase-formatter.ts +46 -20
  30. package/src/exporters/matrix/build.ts +30 -4
  31. package/src/exporters/matrix/export.ts +17 -1
  32. package/src/exporters/matrix/gates.ts +7 -1
  33. package/src/exporters/matrix/types.ts +5 -0
  34. package/src/harness/script-check.ts +18 -4
  35. package/src/harness/spec-coverage.ts +7 -2
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import * as fs from 'fs';
11
+ import { loadApiCatalog } from '../api-catalog-loader';
11
12
  import { parseFeatureMetadata } from '../feature-parser';
12
13
  import { parseSpecFile } from '../spec-parser';
13
14
  import { loadTestData } from '../test-data-resolver';
@@ -23,11 +24,18 @@ import { buildMatrix, deriveVariants } from './build';
23
24
  import { renderMatrixXlsx } from './render-xlsx';
24
25
  import { renderMatrixCsv } from './render-csv';
25
26
  import { DeliveryMap, MatrixFinding, MatrixModel } from './types';
27
+ import { ApiCatalogEntry } from '../types';
26
28
 
27
29
  export interface MatrixTargetPaths {
28
30
  cwd: string;
29
31
  /** Feature basename — names the deliverable files. */
30
32
  unit: string;
33
+ /** Unit kind — 'api' loads the endpoint catalog so cells render request blocks.
34
+ * Optional for back-compat: absent means non-api (no catalog). */
35
+ kind?: 'screen' | 'flow' | 'api';
36
+ /** Area/screen directory name (differs from `unit` for sub-features) — keys the
37
+ * unit-level apis.yaml path. Defaults to `unit`. */
38
+ area?: string;
31
39
  /** Display label (flow/… prefix for flows). */
32
40
  label: string;
33
41
  featureFile: string;
@@ -45,6 +53,11 @@ function readSpecText(paths: MatrixTargetPaths): string {
45
53
  : '';
46
54
  }
47
55
 
56
+ /** apis.yaml endpoint catalog for an api unit ({} for screen/flow — loader no-ops). */
57
+ function catalogFor(paths: MatrixTargetPaths): Record<string, ApiCatalogEntry> {
58
+ return loadApiCatalog(paths.cwd, paths.kind ?? 'screen', paths.area ?? paths.unit);
59
+ }
60
+
48
61
  export interface MatrixLoadResult {
49
62
  model?: MatrixModel;
50
63
  map?: DeliveryMap;
@@ -75,6 +88,7 @@ export function loadMatrixModel(paths: MatrixTargetPaths): MatrixLoadResult {
75
88
  map,
76
89
  transformerVersion: getPackageVersion(),
77
90
  specText: readSpecText(paths),
91
+ apiCatalog: catalogFor(paths),
78
92
  });
79
93
  // Perf verdict (#569): budgets are config (qa/perf.yaml), samples are the run's
80
94
  // per-test durations. Advisory — reported here because delivery is where runs
@@ -106,15 +120,17 @@ export function approveMatrix(paths: MatrixTargetPaths, groupIds?: string[]): {
106
120
  const merged = mergeFeatureAndSpec(feature, spec);
107
121
  const testData = fs.existsSync(paths.testDataFile) ? loadTestData(paths.testDataFile, process.env.SUNGEN_ENV ?? null) : null;
108
122
 
123
+ const apiCatalog = catalogFor(paths);
109
124
  const model = buildMatrix({
110
125
  unit: paths.unit, feature, merged, testData, results: null, map,
111
126
  transformerVersion: getPackageVersion(),
112
127
  specText: readSpecText(paths),
128
+ apiCatalog,
113
129
  });
114
130
  const blocking = model.findings.filter((f) => f.severity === 'error');
115
131
  if (blocking.length > 0) return { findings: blocking, approved: [] };
116
132
 
117
- const variants = deriveVariants({ feature, merged, testData, results: null });
133
+ const variants = deriveVariants({ feature, merged, testData, results: null, apiCatalog });
118
134
  const byRef = new Map(variants.map((v) => [v.ref, v] as const));
119
135
  const byVp = new Map<string, typeof variants>();
120
136
  for (const v of variants) {
@@ -248,7 +248,13 @@ function gateDExecutability(ctx: GateContext, findings: MatrixFinding[]): void {
248
248
  for (const g of map.groups) {
249
249
  const vs = g.variants.flatMap((ref) => expandMapRef(ref, variantsByVp));
250
250
  for (const v of vs) {
251
- const hasPrecondition = v.precondition.length > 0 || bucketTexts(feature.backgroundOrderedSteps, 'given').length > 0;
251
+ // An api-first variant (`@api:` calls, no UI steps) legitimately has no UI start
252
+ // state — the request blocks in its trigger (Method/API/Header/Body) carry
253
+ // everything a tester needs. Demanding a Background/Given here blocked 100% of
254
+ // api-first units (field report 3.2.19).
255
+ const hasPrecondition = v.precondition.length > 0
256
+ || bucketTexts(feature.backgroundOrderedSteps, 'given').length > 0
257
+ || v.apiCalls > 0;
252
258
  if (!hasPrecondition) {
253
259
  findings.push({
254
260
  gate: 'D', severity: 'error', ref: v.ref,
@@ -98,6 +98,11 @@ export interface CoverageVariant {
98
98
  /** Variables this scenario produces at RUN TIME (`remember … as {{v}}`, capability
99
99
  * bindings) — they never exist in test-data, so Gate D must not demand them. */
100
100
  runtimeVars: string[];
101
+ /** Count of `@api:<name>` invocations. An api-first variant's start state lives in
102
+ * its request blocks (Method/API/Header/Body), so Gate D's precondition demand is
103
+ * waived when this is > 0 — requiring a Background/Given blocked EVERY api-first
104
+ * unit (field report 3.2.19). */
105
+ apiCalls: number;
101
106
  /** Condition summary: differentiating dimension shown on the sub-row. */
102
107
  condition: string;
103
108
  /** Resolved data pairs (test-data values / dataset-row columns), `key: value` lines. */
@@ -43,7 +43,10 @@ export function extractTestBlocks(specSrc: string): { title: string; body: strin
43
43
  const lines = specSrc.split('\n');
44
44
  const blocks: { title: string; body: string[] }[] = [];
45
45
  for (let i = 0; i < lines.length; i++) {
46
- const m = lines[i].match(/\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/);
46
+ // Same escape-aware title match as extractTestTitles — with `[^'"`]+` a
47
+ // quote-bearing title extracted truncated (or not at all), missed the
48
+ // automatedTitles lookup, and silently dropped out of the BYPASS scan.
49
+ const m = lines[i].match(/\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])((?:\\.|(?!\1).)+)\1/);
47
50
  if (!m) continue;
48
51
  let depth = 0, started = false;
49
52
  const body: string[] = [];
@@ -52,7 +55,7 @@ export function extractTestBlocks(specSrc: string): { title: string; body: strin
52
55
  body.push(lines[j]);
53
56
  if (started && depth <= 0) break;
54
57
  }
55
- blocks.push({ title: m[2].trim(), body });
58
+ blocks.push({ title: unescapeJsLiteral(m[2]).trim(), body });
56
59
  }
57
60
  return blocks;
58
61
  }
@@ -104,13 +107,24 @@ function extractTestTitles(specSrc: string): string[] {
104
107
  // Count real test cases only. Web/Playwright specs use test(...) (+ .only/.skip/.fixme);
105
108
  // mobile/Appium specs use mocha it(...) (#429 — matching only test() reported EVERY mobile
106
109
  // scenario as MISSING). Exclude describe / hooks (not test cases).
110
+ // The content match is escape-aware and excludes ONLY the actual delimiter (same
111
+ // pattern as spec-parser.ts): the old `[^'"`]+` class truncated a title at its
112
+ // escaped same-quote (`lottery\'s` → EXTRA `lottery\` + the real title MISSING)
113
+ // and dropped a title containing an other-quote (`the "x" case`) entirely — so
114
+ // in-sync specs of quote-bearing scenarios failed the CI gate (field report 3.2.19).
107
115
  const titles: string[] = [];
108
- const re = /\b(?:test|it)(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/g;
116
+ const re = /\b(?:test|it)(?:\.(?:only|skip|fixme))?\(\s*(['"`])((?:\\.|(?!\1).)+)\1/g;
109
117
  let m: RegExpExecArray | null;
110
- while ((m = re.exec(specSrc))) titles.push(m[2].trim());
118
+ while ((m = re.exec(specSrc))) titles.push(unescapeJsLiteral(m[2]).trim());
111
119
  return titles;
112
120
  }
113
121
 
122
+ /** Undo the compiler's escapeQuotes escaping (\' \\ \n \r \t) so extracted titles
123
+ * compare against the raw scenario names from the .feature. */
124
+ function unescapeJsLiteral(s: string): string {
125
+ return s.replace(/\\(.)/g, (_m, c: string) => (c === 'n' ? '\n' : c === 'r' ? '\r' : c === 't' ? '\t' : c));
126
+ }
127
+
114
128
  function normalize(src: string): string {
115
129
  return src
116
130
  .split('\n')
@@ -90,14 +90,19 @@ export function parseSpecClauses(specPath: string): { frs: FrClause[]; valRows:
90
90
  }
91
91
 
92
92
  // Validation Rules table: a row carries a Constraint, a Trigger cell, and (often) a code.
93
+ // A "Trigger" column alone is NOT enough to claim the table (#578): a screen-STATES table
94
+ // ("State ID | Trigger | URL/heading oracle | …") uses Trigger for the user action that
95
+ // enters the state, and reading it as validation rows invented three gate-relevant
96
+ // TRIGGER-UNCOVERED gaps on a real spec. The table must also name a Constraint/Rule/
97
+ // Validation column — the thing a validation row is ABOUT.
93
98
  const valRows: ValRow[] = [];
94
99
  let cTrigger = -1, cConstraint = -1, cCode = -1, inTable = false;
95
100
  for (const raw of lines) {
96
101
  const line = raw.trim();
97
- if (line.startsWith('|') && /\btrigger\b/i.test(line) && cTrigger < 0) {
102
+ if (line.startsWith('|') && /\btrigger\b/i.test(line) && /\b(constraint|rule|validation)\b/i.test(line) && cTrigger < 0) {
98
103
  const cells = line.split('|').map((c) => c.trim());
99
104
  cTrigger = cells.findIndex((c) => /^trigger$/i.test(c));
100
- cConstraint = cells.findIndex((c) => /constraint/i.test(c));
105
+ cConstraint = cells.findIndex((c) => /constraint|rule|validation/i.test(c));
101
106
  cCode = cells.findIndex((c) => /code/i.test(c));
102
107
  inTable = cTrigger >= 0;
103
108
  continue;