@sun-asterisk/sungen 3.2.4 → 3.2.6

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 (102) hide show
  1. package/dist/cli/commands/dashboard.js +2 -2
  2. package/dist/cli/commands/dashboard.js.map +1 -1
  3. package/dist/cli/commands/delivery.d.ts.map +1 -1
  4. package/dist/cli/commands/delivery.js +22 -5
  5. package/dist/cli/commands/delivery.js.map +1 -1
  6. package/dist/dashboard/snapshot-builder.d.ts +3 -0
  7. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  8. package/dist/dashboard/snapshot-builder.js +47 -19
  9. package/dist/dashboard/snapshot-builder.js.map +1 -1
  10. package/dist/dashboard/templates/index.html +4 -4
  11. package/dist/dashboard/types.d.ts +4 -0
  12. package/dist/dashboard/types.d.ts.map +1 -1
  13. package/dist/exporters/api-testcase-formatter.d.ts +55 -0
  14. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -0
  15. package/dist/exporters/api-testcase-formatter.js +304 -0
  16. package/dist/exporters/api-testcase-formatter.js.map +1 -0
  17. package/dist/exporters/csv-exporter.d.ts +13 -1
  18. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  19. package/dist/exporters/csv-exporter.js +44 -28
  20. package/dist/exporters/csv-exporter.js.map +1 -1
  21. package/dist/exporters/feature-parser.d.ts +6 -0
  22. package/dist/exporters/feature-parser.d.ts.map +1 -1
  23. package/dist/exporters/feature-parser.js +9 -3
  24. package/dist/exporters/feature-parser.js.map +1 -1
  25. package/dist/exporters/json-exporter.d.ts.map +1 -1
  26. package/dist/exporters/json-exporter.js +58 -48
  27. package/dist/exporters/json-exporter.js.map +1 -1
  28. package/dist/exporters/result-variants.d.ts +29 -0
  29. package/dist/exporters/result-variants.d.ts.map +1 -0
  30. package/dist/exporters/result-variants.js +33 -0
  31. package/dist/exporters/result-variants.js.map +1 -0
  32. package/dist/exporters/types.d.ts +7 -0
  33. package/dist/exporters/types.d.ts.map +1 -1
  34. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  35. package/dist/exporters/xlsx-exporter.js +45 -13
  36. package/dist/exporters/xlsx-exporter.js.map +1 -1
  37. package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +1 -1
  38. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +6 -6
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +30 -8
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +16 -1
  41. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
  42. package/dist/generators/test-generator/patterns/expect-patterns.js +23 -0
  43. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
  44. package/dist/generators/test-generator/patterns/index.d.ts +12 -0
  45. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  46. package/dist/generators/test-generator/patterns/index.js +21 -1
  47. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  48. package/dist/generators/test-generator/utils/selector-resolver.d.ts +7 -0
  49. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  50. package/dist/generators/test-generator/utils/selector-resolver.js +13 -2
  51. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  52. package/dist/orchestrator/project-initializer.d.ts +23 -0
  53. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  54. package/dist/orchestrator/project-initializer.js +118 -3
  55. package/dist/orchestrator/project-initializer.js.map +1 -1
  56. package/dist/orchestrator/templates/ai-src/commands/add-api.md +76 -0
  57. package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  58. package/dist/orchestrator/templates/ai-src/config/codex.md +3 -2
  59. package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +73 -0
  60. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +42 -8
  61. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +21 -0
  62. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +2 -0
  63. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +33 -0
  64. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +6 -0
  65. package/dist/orchestrator/templates/specs-api.d.ts +26 -0
  66. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  67. package/dist/orchestrator/templates/specs-api.js +131 -3
  68. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  69. package/dist/orchestrator/templates/specs-api.ts +118 -5
  70. package/dist/orchestrator/templates/specs-test-data.ts +20 -1
  71. package/package.json +4 -3
  72. package/src/cli/commands/dashboard.ts +2 -2
  73. package/src/cli/commands/delivery.ts +24 -6
  74. package/src/dashboard/snapshot-builder.ts +46 -19
  75. package/src/dashboard/templates/index.html +4 -4
  76. package/src/dashboard/types.ts +5 -1
  77. package/src/exporters/api-testcase-formatter.ts +325 -0
  78. package/src/exporters/csv-exporter.ts +55 -27
  79. package/src/exporters/feature-parser.ts +8 -2
  80. package/src/exporters/json-exporter.ts +56 -47
  81. package/src/exporters/result-variants.ts +47 -0
  82. package/src/exporters/types.ts +7 -0
  83. package/src/exporters/xlsx-exporter.ts +48 -13
  84. package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +1 -1
  85. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +6 -6
  86. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +30 -8
  87. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +16 -1
  88. package/src/generators/test-generator/patterns/expect-patterns.ts +23 -0
  89. package/src/generators/test-generator/patterns/index.ts +21 -1
  90. package/src/generators/test-generator/utils/selector-resolver.ts +30 -2
  91. package/src/orchestrator/project-initializer.ts +120 -3
  92. package/src/orchestrator/templates/ai-src/commands/add-api.md +76 -0
  93. package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  94. package/src/orchestrator/templates/ai-src/config/codex.md +3 -2
  95. package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +73 -0
  96. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +42 -8
  97. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +21 -0
  98. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +2 -0
  99. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +33 -0
  100. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +6 -0
  101. package/src/orchestrator/templates/specs-api.ts +118 -5
  102. package/src/orchestrator/templates/specs-test-data.ts +20 -1
@@ -159,6 +159,12 @@ Build a mapping table: for each applicable group, does the feature have a matchi
159
159
  | **Syntax compliance** — steps match `sungen-gherkin-syntax` patterns, correct `[Ref] type with {{v}}`, no bare `is visible` | 4 |
160
160
  | Grouping — each case has 2–7 related assertions for the same action | 4 |
161
161
 
162
+ **API scenarios (`@api`) — depth rubric:** a success case scores on Assertion Quality only if it proves the effect, not just the status. Apply per scenario:
163
+ - **status-only mutating success = fail the "Specific" check** — `expect {{x.status}} is 201` alone proves nothing happened. Require a server-derived body field **and** a proof of effect (`@query` read-back / follow-up GET / `@concurrent` `ok_count`).
164
+ - **read contract** — status + ≥1 body field (medium is acceptable for reads).
165
+ - **error (4xx)** — the status IS the assertion; also check the error-message field when the contract defines one.
166
+ - **anti-pattern (counts against "Avoids anti-patterns")** — re-asserting the input just sent instead of a server-derived value.
167
+
162
168
  ### Test Data (10)
163
169
 
164
170
  | Check | Pts |
@@ -1,3 +1,13 @@
1
+ /** A multipart file field embedded by the compiler (string = shorthand for { path }). */
2
+ type FileSpec = string | {
3
+ path: string;
4
+ mimeType?: string;
5
+ filename?: string;
6
+ name?: string;
7
+ };
8
+ export declare function inferMime(name: string): string;
9
+ /** Resolve a fixture path: cwd-relative/absolute first, else qa/fixtures/<path>. */
10
+ export declare function resolveFixture(p: string, label: string): string;
1
11
  /**
2
12
  * Join a datasource base URL with a catalog path. Concatenate rather than rely on Playwright's
3
13
  * baseURL resolution: an absolute path (`/user/1`) resolves against the base ORIGIN and would drop
@@ -19,7 +29,10 @@ declare class ApiClient {
19
29
  path: string;
20
30
  body?: unknown;
21
31
  encoding?: 'json' | 'form' | 'multipart';
32
+ files?: Record<string, FileSpec>;
33
+ bodyFile?: FileSpec;
22
34
  headers?: Record<string, string>;
35
+ timeout?: number;
23
36
  datasource?: string;
24
37
  }, params?: Record<string, any>, opts?: {
25
38
  storageState?: string;
@@ -41,7 +54,10 @@ declare class ApiClient {
41
54
  path: string;
42
55
  body?: unknown;
43
56
  encoding?: 'json' | 'form' | 'multipart';
57
+ files?: Record<string, FileSpec>;
58
+ bodyFile?: FileSpec;
44
59
  headers?: Record<string, string>;
60
+ timeout?: number;
45
61
  datasource?: string;
46
62
  }, params?: Record<string, any>, n?: number, opts?: {
47
63
  storageState?: string;
@@ -56,6 +72,16 @@ declare class ApiClient {
56
72
  status_counts: Record<string, number>;
57
73
  statuses: number[];
58
74
  }>;
75
+ /**
76
+ * Validate a response body against a named JSON Schema from `qa/api/schemas.yaml` (populated by
77
+ * `sungen api import` from the OpenAPI `components.schemas`). Backs `expect {{name.body}} matches
78
+ * schema [Ref]` — a full-contract check beyond individual field asserts. Returns `{ ok, errors }`;
79
+ * the generated assertion feeds `errors` into the failure message. Uses Ajv (a QA-project dep).
80
+ */
81
+ assertSchema(value: unknown, schemaName: string): {
82
+ ok: boolean;
83
+ errors: string;
84
+ };
59
85
  }
60
86
  export declare const api: ApiClient;
61
87
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"specs-api.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"AAmDA;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,cAAM,SAAS;IACb,OAAO,CAAC,OAAO,CAA8C;IAE7D,OAAO,CAAC,GAAG;IAWX;;;;;OAKG;IACG,IAAI,CACR,KAAK,EAAE,MAAM,EACb,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,EACtJ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAChC,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAO,GACnC,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IA6CvF;;;;;;OAMG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,EACtJ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAChC,CAAC,SAAI,EACL,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAO,GACnC,OAAO,CAAC;QACT,SAAS,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,OAAO,CAAC;YAAC,IAAI,EAAE,GAAG,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;SAAE,CAAC,CAAC;QAC9F,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CAYH;AAED,eAAO,MAAM,GAAG,WAAkB,CAAC"}
1
+ {"version":3,"file":"specs-api.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.api = void 0;
37
+ exports.inferMime = inferMime;
38
+ exports.resolveFixture = resolveFixture;
37
39
  exports.joinApiUrl = joinApiUrl;
38
40
  /* eslint-disable */
39
41
  /**
@@ -75,6 +77,37 @@ function loadConfig() {
75
77
  function substitute(text, params) {
76
78
  return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
77
79
  }
80
+ /** Bind `:param` tokens WITHOUT URL-encoding — for headers, body-file paths, and multipart file paths. */
81
+ function substituteRaw(text, params) {
82
+ return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
83
+ }
84
+ /** Delete any case-insensitive variant of a header (HTTP header names are case-insensitive, but a
85
+ * plain object keys on exact case — so a datasource `Content-Type` and our `content-type` would
86
+ * both be sent). Call before setting, or to let Playwright own the header (multipart boundary). */
87
+ function deleteHeader(headers, name) {
88
+ const lower = name.toLowerCase();
89
+ for (const k of Object.keys(headers))
90
+ if (k.toLowerCase() === lower)
91
+ delete headers[k];
92
+ }
93
+ const MIME = {
94
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
95
+ svg: 'image/svg+xml', pdf: 'application/pdf', csv: 'text/csv', txt: 'text/plain',
96
+ json: 'application/json', xml: 'application/xml', zip: 'application/zip', mp4: 'video/mp4',
97
+ };
98
+ function inferMime(name) {
99
+ return MIME[(name.split('.').pop() || '').toLowerCase()] || 'application/octet-stream';
100
+ }
101
+ /** Resolve a fixture path: cwd-relative/absolute first, else qa/fixtures/<path>. */
102
+ function resolveFixture(p, label) {
103
+ const direct = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
104
+ if (fs.existsSync(direct))
105
+ return direct;
106
+ const inFixtures = path.join(process.cwd(), 'qa', 'fixtures', p);
107
+ if (fs.existsSync(inFixtures))
108
+ return inFixtures;
109
+ throw new Error(`API Driver: ${label} — fixture "${p}" not found (looked in ${direct} and qa/fixtures/${p}).`);
110
+ }
78
111
  /**
79
112
  * Join a datasource base URL with a catalog path. Concatenate rather than rely on Playwright's
80
113
  * baseURL resolution: an absolute path (`/user/1`) resolves against the base ORIGIN and would drop
@@ -116,7 +149,7 @@ class ApiClient {
116
149
  const headers = { ...(conf.headers || {}) };
117
150
  // catalog headers; :param tokens bind at runtime — raw (no URL-encoding, unlike the path)
118
151
  for (const [k, v] of Object.entries(req.headers || {}))
119
- headers[k] = String(v).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
152
+ headers[k] = substituteRaw(String(v), params);
120
153
  // Body: substitute `:param` into the body template (object values), then encode per `encoding`.
121
154
  let body;
122
155
  if (req.body !== undefined && req.body !== null) {
@@ -124,8 +157,43 @@ class ApiClient {
124
157
  }
125
158
  // Map the wire format to the right Playwright option (#345): json → data (application/json,
126
159
  // default), form → form (application/x-www-form-urlencoded), multipart → multipart (form-data).
160
+ // A `files` block is a real upload: read each fixture into a { name, mimeType, buffer } part and
161
+ // send multipart, with any `body` values riding along as text fields.
127
162
  const bodyOpt = {};
128
- if (body !== undefined) {
163
+ if (req.bodyFile) {
164
+ // Raw-binary body: the whole request body is the fixture's bytes. Content-Type comes from the
165
+ // spec's mimeType (default application/octet-stream) — this is what octet-stream upload
166
+ // endpoints expect (a multipart body would be rejected 415).
167
+ const spec = typeof req.bodyFile === 'string' ? { path: req.bodyFile } : req.bodyFile;
168
+ const fpath = substituteRaw(String(spec.path), params);
169
+ if (fpath) {
170
+ const resolved = resolveFixture(fpath, label);
171
+ bodyOpt.data = fs.readFileSync(resolved);
172
+ // Own the Content-Type: drop any datasource default (case-insensitive) so we don't send both.
173
+ deleteHeader(headers, 'content-type');
174
+ headers['content-type'] = spec.mimeType ?? 'application/octet-stream';
175
+ }
176
+ }
177
+ else if (req.files && Object.keys(req.files).length) {
178
+ const mp = {};
179
+ if (body && typeof body === 'object')
180
+ for (const [k, v] of Object.entries(body))
181
+ mp[k] = typeof v === 'string' ? v : JSON.stringify(v);
182
+ for (const [field, raw] of Object.entries(req.files)) {
183
+ const spec = typeof raw === 'string' ? { path: raw } : raw;
184
+ const fpath = substituteRaw(String(spec.path), params);
185
+ if (!fpath)
186
+ continue; // empty param → omit the part (a missing-file / wrong-type error row)
187
+ const resolved = resolveFixture(fpath, label);
188
+ const filename = spec.filename ?? path.basename(resolved);
189
+ mp[spec.name ?? field] = { name: filename, mimeType: spec.mimeType ?? inferMime(filename), buffer: fs.readFileSync(resolved) };
190
+ }
191
+ // Playwright generates the multipart/form-data Content-Type (with boundary); drop any datasource
192
+ // default Content-Type so it doesn't conflict with / duplicate the generated one.
193
+ deleteHeader(headers, 'content-type');
194
+ bodyOpt.multipart = mp;
195
+ }
196
+ else if (body !== undefined) {
129
197
  const enc = req.encoding ?? 'json';
130
198
  if (enc === 'form')
131
199
  bodyOpt.form = body;
@@ -139,7 +207,7 @@ class ApiClient {
139
207
  // authenticated cookies. Disposed per call so no request context lingers and hangs the process.
140
208
  const ctx = await test_1.request.newContext({
141
209
  extraHTTPHeaders: headers,
142
- timeout: conf.timeout_ms ?? 15000,
210
+ timeout: req.timeout ?? conf.timeout_ms ?? 15000, // per-endpoint override → datasource → 15s
143
211
  ...(opts.storageState ? { storageState: opts.storageState } : {}),
144
212
  });
145
213
  try {
@@ -177,6 +245,66 @@ class ApiClient {
177
245
  statuses: responses.map((r) => r.status),
178
246
  };
179
247
  }
248
+ /**
249
+ * Validate a response body against a named JSON Schema from `qa/api/schemas.yaml` (populated by
250
+ * `sungen api import` from the OpenAPI `components.schemas`). Backs `expect {{name.body}} matches
251
+ * schema [Ref]` — a full-contract check beyond individual field asserts. Returns `{ ok, errors }`;
252
+ * the generated assertion feeds `errors` into the failure message. Uses Ajv (a QA-project dep).
253
+ */
254
+ assertSchema(value, schemaName) {
255
+ return assertSchemaImpl(value, schemaName);
256
+ }
257
+ }
258
+ // ── Response-schema validation (Ajv) ─────────────────────────────────────────────────────────────
259
+ // Schemas live in qa/api/schemas.yaml (name → JSON Schema, incl. #/components/schemas/* $refs).
260
+ // Loaded + compiled lazily so a project that never asserts a schema needs no Ajv resolution.
261
+ let _apiSchemas;
262
+ function loadApiSchemas() {
263
+ if (_apiSchemas !== undefined)
264
+ return _apiSchemas || {};
265
+ const file = [path.join(process.cwd(), 'qa', 'api', 'schemas.yaml'), path.join(process.cwd(), 'schemas.yaml')].find((f) => fs.existsSync(f));
266
+ if (!file) {
267
+ _apiSchemas = null;
268
+ return {};
269
+ }
270
+ const { parse } = require('yaml');
271
+ _apiSchemas = parse(fs.readFileSync(file, 'utf8')) || {};
272
+ return _apiSchemas || {};
273
+ }
274
+ let _ajv;
275
+ function getAjv() {
276
+ if (_ajv)
277
+ return _ajv;
278
+ const Ajv = require('ajv');
279
+ _ajv = new Ajv({ allErrors: true, strict: false });
280
+ try {
281
+ require('ajv-formats')(_ajv);
282
+ }
283
+ catch { /* formats optional */ }
284
+ // Register every named schema under its component ref so inter-schema $refs resolve.
285
+ for (const [name, schema] of Object.entries(loadApiSchemas())) {
286
+ try {
287
+ _ajv.addSchema(schema, `#/components/schemas/${name}`);
288
+ }
289
+ catch { /* dup/invalid → skip */ }
290
+ }
291
+ return _ajv;
292
+ }
293
+ function assertSchemaImpl(value, schemaName) {
294
+ const schema = loadApiSchemas()[schemaName];
295
+ if (!schema)
296
+ return { ok: false, errors: `schema "${schemaName}" not found in qa/api/schemas.yaml` };
297
+ let validate;
298
+ try {
299
+ const ajv = getAjv();
300
+ validate = ajv.getSchema(`#/components/schemas/${schemaName}`) || ajv.compile(schema);
301
+ }
302
+ catch (e) {
303
+ return { ok: false, errors: `schema "${schemaName}" failed to compile: ${e?.message || e}` };
304
+ }
305
+ const ok = validate(value) === true;
306
+ const errors = ok ? '' : (validate.errors || []).map((e) => `${e.instancePath || '(root)'} ${e.message}`).join('; ') || 'did not match schema';
307
+ return { ok, errors };
180
308
  }
181
309
  exports.api = new ApiClient();
182
310
  //# sourceMappingURL=specs-api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"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"}
@@ -48,6 +48,37 @@ function loadConfig(): Record<string, ApiDataSource> {
48
48
  function substitute(text: string, params: Record<string, any>): string {
49
49
  return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
50
50
  }
51
+ /** Bind `:param` tokens WITHOUT URL-encoding — for headers, body-file paths, and multipart file paths. */
52
+ function substituteRaw(text: string, params: Record<string, any>): string {
53
+ return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
54
+ }
55
+ /** Delete any case-insensitive variant of a header (HTTP header names are case-insensitive, but a
56
+ * plain object keys on exact case — so a datasource `Content-Type` and our `content-type` would
57
+ * both be sent). Call before setting, or to let Playwright own the header (multipart boundary). */
58
+ function deleteHeader(headers: Record<string, string>, name: string): void {
59
+ const lower = name.toLowerCase();
60
+ for (const k of Object.keys(headers)) if (k.toLowerCase() === lower) delete headers[k];
61
+ }
62
+
63
+ /** A multipart file field embedded by the compiler (string = shorthand for { path }). */
64
+ type FileSpec = string | { path: string; mimeType?: string; filename?: string; name?: string };
65
+
66
+ const MIME: Record<string, string> = {
67
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
68
+ svg: 'image/svg+xml', pdf: 'application/pdf', csv: 'text/csv', txt: 'text/plain',
69
+ json: 'application/json', xml: 'application/xml', zip: 'application/zip', mp4: 'video/mp4',
70
+ };
71
+ export function inferMime(name: string): string {
72
+ return MIME[(name.split('.').pop() || '').toLowerCase()] || 'application/octet-stream';
73
+ }
74
+ /** Resolve a fixture path: cwd-relative/absolute first, else qa/fixtures/<path>. */
75
+ export function resolveFixture(p: string, label: string): string {
76
+ const direct = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
77
+ if (fs.existsSync(direct)) return direct;
78
+ const inFixtures = path.join(process.cwd(), 'qa', 'fixtures', p);
79
+ if (fs.existsSync(inFixtures)) return inFixtures;
80
+ throw new Error(`API Driver: ${label} — fixture "${p}" not found (looked in ${direct} and qa/fixtures/${p}).`);
81
+ }
51
82
 
52
83
  /**
53
84
  * Join a datasource base URL with a catalog path. Concatenate rather than rely on Playwright's
@@ -82,7 +113,7 @@ class ApiClient {
82
113
  */
83
114
  async call(
84
115
  label: string,
85
- req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; headers?: Record<string, string>; datasource?: string },
116
+ req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; files?: Record<string, FileSpec>; bodyFile?: FileSpec; headers?: Record<string, string>; timeout?: number; datasource?: string },
86
117
  params: Record<string, any> = {},
87
118
  opts: { storageState?: string } = {},
88
119
  ): Promise<{ status: number; ok: boolean; body: any; headers: Record<string, string> }> {
@@ -94,7 +125,7 @@ class ApiClient {
94
125
  const headers: Record<string, string> = { ...(conf.headers || {}) };
95
126
  // catalog headers; :param tokens bind at runtime — raw (no URL-encoding, unlike the path)
96
127
  for (const [k, v] of Object.entries(req.headers || {}))
97
- headers[k] = String(v).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
128
+ headers[k] = substituteRaw(String(v), params);
98
129
  // Body: substitute `:param` into the body template (object values), then encode per `encoding`.
99
130
  let body: any;
100
131
  if (req.body !== undefined && req.body !== null) {
@@ -102,8 +133,39 @@ class ApiClient {
102
133
  }
103
134
  // Map the wire format to the right Playwright option (#345): json → data (application/json,
104
135
  // default), form → form (application/x-www-form-urlencoded), multipart → multipart (form-data).
136
+ // A `files` block is a real upload: read each fixture into a { name, mimeType, buffer } part and
137
+ // send multipart, with any `body` values riding along as text fields.
105
138
  const bodyOpt: Record<string, unknown> = {};
106
- if (body !== undefined) {
139
+ if (req.bodyFile) {
140
+ // Raw-binary body: the whole request body is the fixture's bytes. Content-Type comes from the
141
+ // spec's mimeType (default application/octet-stream) — this is what octet-stream upload
142
+ // endpoints expect (a multipart body would be rejected 415).
143
+ const spec = typeof req.bodyFile === 'string' ? { path: req.bodyFile } : req.bodyFile;
144
+ const fpath = substituteRaw(String(spec.path), params);
145
+ if (fpath) {
146
+ const resolved = resolveFixture(fpath, label);
147
+ bodyOpt.data = fs.readFileSync(resolved);
148
+ // Own the Content-Type: drop any datasource default (case-insensitive) so we don't send both.
149
+ deleteHeader(headers, 'content-type');
150
+ headers['content-type'] = spec.mimeType ?? 'application/octet-stream';
151
+ }
152
+ } else if (req.files && Object.keys(req.files).length) {
153
+ const mp: Record<string, any> = {};
154
+ if (body && typeof body === 'object')
155
+ for (const [k, v] of Object.entries(body as Record<string, unknown>)) mp[k] = typeof v === 'string' ? v : JSON.stringify(v);
156
+ for (const [field, raw] of Object.entries(req.files)) {
157
+ const spec = typeof raw === 'string' ? { path: raw } : raw;
158
+ const fpath = substituteRaw(String(spec.path), params);
159
+ if (!fpath) continue; // empty param → omit the part (a missing-file / wrong-type error row)
160
+ const resolved = resolveFixture(fpath, label);
161
+ const filename = spec.filename ?? path.basename(resolved);
162
+ mp[spec.name ?? field] = { name: filename, mimeType: spec.mimeType ?? inferMime(filename), buffer: fs.readFileSync(resolved) };
163
+ }
164
+ // Playwright generates the multipart/form-data Content-Type (with boundary); drop any datasource
165
+ // default Content-Type so it doesn't conflict with / duplicate the generated one.
166
+ deleteHeader(headers, 'content-type');
167
+ bodyOpt.multipart = mp;
168
+ } else if (body !== undefined) {
107
169
  const enc = req.encoding ?? 'json';
108
170
  if (enc === 'form') bodyOpt.form = body;
109
171
  else if (enc === 'multipart') bodyOpt.multipart = body;
@@ -115,7 +177,7 @@ class ApiClient {
115
177
  // authenticated cookies. Disposed per call so no request context lingers and hangs the process.
116
178
  const ctx: APIRequestContext = await request.newContext({
117
179
  extraHTTPHeaders: headers,
118
- timeout: conf.timeout_ms ?? 15000,
180
+ timeout: req.timeout ?? conf.timeout_ms ?? 15000, // per-endpoint override → datasource → 15s
119
181
  ...(opts.storageState ? { storageState: opts.storageState } : {}),
120
182
  });
121
183
  try {
@@ -139,7 +201,7 @@ class ApiClient {
139
201
  */
140
202
  async callN(
141
203
  label: string,
142
- req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; headers?: Record<string, string>; datasource?: string },
204
+ req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; files?: Record<string, FileSpec>; bodyFile?: FileSpec; headers?: Record<string, string>; timeout?: number; datasource?: string },
143
205
  params: Record<string, any> = {},
144
206
  n = 1,
145
207
  opts: { storageState?: string } = {},
@@ -160,6 +222,57 @@ class ApiClient {
160
222
  statuses: responses.map((r) => r.status),
161
223
  };
162
224
  }
225
+
226
+ /**
227
+ * Validate a response body against a named JSON Schema from `qa/api/schemas.yaml` (populated by
228
+ * `sungen api import` from the OpenAPI `components.schemas`). Backs `expect {{name.body}} matches
229
+ * schema [Ref]` — a full-contract check beyond individual field asserts. Returns `{ ok, errors }`;
230
+ * the generated assertion feeds `errors` into the failure message. Uses Ajv (a QA-project dep).
231
+ */
232
+ assertSchema(value: unknown, schemaName: string): { ok: boolean; errors: string } {
233
+ return assertSchemaImpl(value, schemaName);
234
+ }
235
+ }
236
+
237
+ // ── Response-schema validation (Ajv) ─────────────────────────────────────────────────────────────
238
+ // Schemas live in qa/api/schemas.yaml (name → JSON Schema, incl. #/components/schemas/* $refs).
239
+ // Loaded + compiled lazily so a project that never asserts a schema needs no Ajv resolution.
240
+ let _apiSchemas: Record<string, any> | null | undefined;
241
+ function loadApiSchemas(): Record<string, any> {
242
+ if (_apiSchemas !== undefined) return _apiSchemas || {};
243
+ const file = [path.join(process.cwd(), 'qa', 'api', 'schemas.yaml'), path.join(process.cwd(), 'schemas.yaml')].find((f) => fs.existsSync(f));
244
+ if (!file) { _apiSchemas = null; return {}; }
245
+ const { parse } = require('yaml');
246
+ _apiSchemas = (parse(fs.readFileSync(file, 'utf8')) as Record<string, any>) || {};
247
+ return _apiSchemas || {};
248
+ }
249
+
250
+ let _ajv: any;
251
+ function getAjv(): any {
252
+ if (_ajv) return _ajv;
253
+ const Ajv = require('ajv');
254
+ _ajv = new Ajv({ allErrors: true, strict: false });
255
+ try { require('ajv-formats')(_ajv); } catch { /* formats optional */ }
256
+ // Register every named schema under its component ref so inter-schema $refs resolve.
257
+ for (const [name, schema] of Object.entries(loadApiSchemas())) {
258
+ try { _ajv.addSchema(schema, `#/components/schemas/${name}`); } catch { /* dup/invalid → skip */ }
259
+ }
260
+ return _ajv;
261
+ }
262
+
263
+ function assertSchemaImpl(value: unknown, schemaName: string): { ok: boolean; errors: string } {
264
+ const schema = loadApiSchemas()[schemaName];
265
+ if (!schema) return { ok: false, errors: `schema "${schemaName}" not found in qa/api/schemas.yaml` };
266
+ let validate: any;
267
+ try {
268
+ const ajv = getAjv();
269
+ validate = ajv.getSchema(`#/components/schemas/${schemaName}`) || ajv.compile(schema);
270
+ } catch (e: any) {
271
+ return { ok: false, errors: `schema "${schemaName}" failed to compile: ${e?.message || e}` };
272
+ }
273
+ const ok = validate(value) === true;
274
+ const errors = ok ? '' : (validate.errors || []).map((e: any) => `${e.instancePath || '(root)'} ${e.message}`).join('; ') || 'did not match schema';
275
+ return { ok, errors };
163
276
  }
164
277
 
165
278
  export const api = new ApiClient();
@@ -48,7 +48,26 @@ export class TestDataLoader {
48
48
  if (value === undefined || value === null) {
49
49
  throw new Error(`Test data key not found: ${key}`);
50
50
  }
51
- return String(value);
51
+ return this.interpolate(String(value));
52
+ }
53
+
54
+ /**
55
+ * Substitute embedded `{{ref}}` cross-references inside a resolved string value.
56
+ * Each ref is looked up via resolve() (flat key, @cases row column, or dotted path),
57
+ * so `expected_operator_id_label: 'ID : {{operator.id}}'` yields the real value at
58
+ * runtime — matching the delivery report's substituteTestDataVars. `$`-prefixed
59
+ * dynamic vars ({{$timestamp}} …) are already substituted at load time and never
60
+ * match `[\w.]+`, so they are left untouched. Resolution recurses (a referenced value
61
+ * may itself hold refs) up to a depth cap that guards against reference cycles; an
62
+ * unknown or empty ref is left as the literal `{{ref}}` — never blanked.
63
+ */
64
+ private interpolate(value: string, depth: number = 0): string {
65
+ if (depth > 10 || value.indexOf('{{') === -1) return value;
66
+ return value.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, ref: string) => {
67
+ const resolved = this.resolve(ref);
68
+ if (resolved === undefined || resolved === null || resolved === '') return match;
69
+ return this.interpolate(String(resolved), depth + 1);
70
+ });
52
71
  }
53
72
 
54
73
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.4",
3
+ "version": "3.2.6",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -12,7 +12,7 @@
12
12
  "copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
13
13
  "build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
14
14
  "dev": "tsx src/cli/index.ts",
15
- "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts",
15
+ "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts",
16
16
  "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
17
17
  "prepublishOnly": "npm run build:dashboard && npm run build"
18
18
  },
@@ -33,7 +33,7 @@
33
33
  "node": ">=18.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@sungen/driver-ui": "3.2.4",
36
+ "@sungen/driver-ui": "3.2.6",
37
37
  "@anthropic-ai/sdk": "^0.71.0",
38
38
  "@babel/parser": "^7.28.5",
39
39
  "@babel/traverse": "^7.28.5",
@@ -47,6 +47,7 @@
47
47
  "fast-glob": "^3.3.3",
48
48
  "glob": "^13.0.0",
49
49
  "handlebars": "^4.7.8",
50
+ "jszip": "^3.10.1",
50
51
  "ora": "^9.0.0",
51
52
  "plop": "^4.0.4",
52
53
  "tsx": "^4.22.4",
@@ -82,12 +82,12 @@ export function registerDashboardCommand(program: Command): void {
82
82
  } else {
83
83
  targets = listDashboardTargets(cwd);
84
84
  if (targets.length === 0) {
85
- console.error(`${COLOR.red}No screens or flows found in qa/screens/ or qa/flows/${COLOR.reset}`);
85
+ console.error(`${COLOR.red}No units found in qa/screens/, qa/flows/, or qa/api/${COLOR.reset}`);
86
86
  process.exit(1);
87
87
  }
88
88
  }
89
89
 
90
- const labels = targets.map((t) => t.isFlow ? `flow/${t.name}` : t.name);
90
+ const labels = targets.map((t) => t.kind === 'flow' ? `flow/${t.name}` : t.kind === 'api' ? `api/${t.name}` : t.name);
91
91
  log(`${COLOR.bold}sungen dashboard${COLOR.reset} — building report for ${targets.length} target(s): ${labels.join(', ')}`);
92
92
 
93
93
  // 2. Build snapshot
@@ -137,18 +137,30 @@ function qaDir(cwd: string, target: DeliveryTarget): string {
137
137
  }
138
138
 
139
139
  /**
140
- * Load the apis.yaml catalog for an api-kind unit.
141
- * The catalog lives at qa/api/<screen>/api/apis.yaml.
142
- * Returns an empty object when the file is absent (allows graceful degradation).
140
+ * Load the apis.yaml catalog for an api-kind unit, merging the shared catalog under
141
+ * the unit catalog. The runtime resolves an endpoint unit-scope-first then falls back
142
+ * to the shared catalog (driver-api api-catalog.ts), so an endpoint defined only in
143
+ * `qa/api/apis.yaml` still runs — we must load it too, or its request block would come
144
+ * back empty (the very bug this feature fixes). Unit wins on a name collision.
145
+ * Returns an empty object when neither file is present (graceful degradation).
143
146
  */
144
147
  function loadApiCatalog(cwd: string, target: DeliveryTarget): Record<string, ApiCatalogEntry> {
145
148
  if (target.kind !== 'api') return {};
146
- const catalogPath = path.join(cwd, 'qa', 'api', target.screen, 'api', 'apis.yaml');
149
+ const shared = readApiCatalogFile(path.join(cwd, 'qa', 'api', 'apis.yaml'));
150
+ const unit = readApiCatalogFile(path.join(cwd, 'qa', 'api', target.screen, 'api', 'apis.yaml'));
151
+ return { ...shared, ...unit };
152
+ }
153
+
154
+ /** Parse one apis.yaml file into an endpoint map, unwrapping the optional `apis:` wrapper. */
155
+ function readApiCatalogFile(catalogPath: string): Record<string, ApiCatalogEntry> {
147
156
  if (!fs.existsSync(catalogPath)) return {};
148
157
  try {
149
- const parsed = parseYaml(fs.readFileSync(catalogPath, 'utf-8'));
158
+ const parsed = parseYaml(fs.readFileSync(catalogPath, 'utf-8')) as Record<string, unknown> | null;
150
159
  if (parsed && typeof parsed === 'object') {
151
- return parsed as Record<string, ApiCatalogEntry>;
160
+ // The runtime loader accepts both a flat map and an `apis:` wrapper — unwrap it
161
+ // here too so both loaders read the same file identically.
162
+ const root = parsed.apis && typeof parsed.apis === 'object' ? parsed.apis : parsed;
163
+ return root as Record<string, ApiCatalogEntry>;
152
164
  }
153
165
  } catch { /* malformed yaml — skip */ }
154
166
  return {};
@@ -489,6 +501,8 @@ async function exportTarget(
489
501
  results: resolveResultsPath(cwd, target) ? loadPlaywrightReport(resolveResultsPath(cwd, target)!) : null,
490
502
  env,
491
503
  selectorKeyMap,
504
+ kind: target.kind,
505
+ apiCatalog,
492
506
  });
493
507
  const tempSummary = buildSummary(label, rows, '');
494
508
  const csv = renderCsv(tempSummary, rows, specLink);
@@ -533,6 +547,8 @@ async function exportTarget(
533
547
  results: variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null,
534
548
  env,
535
549
  selectorKeyMap,
550
+ kind: target.kind,
551
+ apiCatalog,
536
552
  });
537
553
  const csv = renderCsv(buildSummary(label, variantRows, ''), variantRows, specLink);
538
554
  const csvPath = writeCsv(cwd, target.featureBaseName, csv, variant.locale);
@@ -580,6 +596,8 @@ async function exportTarget(
580
596
  results: variantResults,
581
597
  env,
582
598
  selectorKeyMap,
599
+ kind: target.kind,
600
+ apiCatalog,
583
601
  });
584
602
 
585
603
  // CSV: always one file per locale, every row (CSV has no sheet concept).