@sungen/driver-api 3.1.2-beta.111 → 3.1.2-beta.113

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/index.ts +74 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sungen/driver-api",
3
- "version": "3.1.2-beta.111",
3
+ "version": "3.1.2-beta.113",
4
4
  "description": "Sungen API capability — the @api annotation, API catalog, OpenAPI import + `sungen api import`, and the specs/api.ts runtime template. Plugs into @sun-asterisk/sungen via the capability SPI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "author": "eqe team (engineer & quality) — Sun Asterisk",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "@sun-asterisk/sungen": "3.1.2-beta.111",
16
+ "@sun-asterisk/sungen": "3.1.2-beta.113",
17
17
  "commander": "^14.0.2",
18
18
  "yaml": "^2.8.2"
19
19
  },
package/src/index.ts CHANGED
@@ -55,6 +55,18 @@ export { importCsv, parseCsv } from './csv-import';
55
55
  function apiPreconditionCodegen(input: { tags: string[]; screenName: string; cwd: string }): Array<{ comment?: string; code: string; boundVars?: string[] }> {
56
56
  const out: Array<{ comment?: string; code: string; boundVars?: string[] }> = [];
57
57
  const TAG = /^@api:([A-Za-z_][A-Za-z0-9_]*)(?:\((.*)\))?$/;
58
+ // @concurrent:N (AP-5): fire each @api call N times in parallel and bind aggregates
59
+ // ({{name.ok_count}}, {{name.status_counts}}) — the idempotency/race oracle. Applies to the
60
+ // scenario's @api call(s); pair with @query to cross-check the DB ("exactly one charge").
61
+ const concMatch = input.tags.map((t) => t.match(/^@concurrent:(\d+)$/)).find(Boolean);
62
+ const concurrent = concMatch ? Math.max(1, parseInt(concMatch[1], 10)) : 0;
63
+ // @hybrid (AP-6): reuse the UI session — load the scenario's @auth:<role> storageState
64
+ // (specs/.auth/<role>.json, written by the auth global-setup before any test) so the API request
65
+ // carries the same authenticated cookies as the browser. No effect without an @auth role.
66
+ const authMatch = input.tags.map((t) => t.match(/^@auth:([A-Za-z0-9_-]+)$/)).find(Boolean);
67
+ const optsArg = input.tags.includes('@hybrid') && authMatch
68
+ ? `, { storageState: ${JSON.stringify(`specs/.auth/${authMatch[1]}.json`)} }`
69
+ : '';
58
70
  for (const tag of input.tags) {
59
71
  const m = tag.match(TAG);
60
72
  if (!m) continue;
@@ -71,9 +83,16 @@ function apiPreconditionCodegen(input: { tags: string[]; screenName: string; cwd
71
83
  // mechanism (the dynamic value comes via an override like `token={{login.body.token}}`).
72
84
  const hdr = entry.headers && Object.keys(entry.headers).length ? `, headers: ${JSON.stringify(entry.headers)}` : '';
73
85
  const req = `{ method: ${JSON.stringify(entry.method)}, path: ${JSON.stringify(entry.path)}${entry.body !== undefined ? `, body: ${JSON.stringify(entry.body)}` : ''}${hdr}, datasource: ${ds} }`;
86
+ // @concurrent:N → callN (N parallel calls, aggregate result); else a single call. @hybrid adds
87
+ // the storageState opts so the request reuses the UI auth session.
88
+ const callExpr = concurrent
89
+ ? `await api.callN(${label}, ${req}, { ${paramsObj} }, ${concurrent}${optsArg})`
90
+ : `await api.call(${label}, ${req}, { ${paramsObj} }${optsArg})`;
74
91
  out.push({
75
- comment: `@api:${name} → bind {{${name}}} (${entry.method} ${entry.path})`,
76
- code: `testData.bind(${JSON.stringify(name)}, await api.call(${label}, ${req}, { ${paramsObj} }));`,
92
+ comment: concurrent
93
+ ? `@api:${name} ×${concurrent} (@concurrent) → bind {{${name}}} aggregates (${entry.method} ${entry.path})`
94
+ : `@api:${name} → bind {{${name}}} (${entry.method} ${entry.path})`,
95
+ code: `testData.bind(${JSON.stringify(name)}, ${callExpr});`,
77
96
  boundVars: [name],
78
97
  });
79
98
  }
@@ -105,6 +124,58 @@ const apiVerificationSensor: Sensor<GateInput> = {
105
124
  },
106
125
  };
107
126
 
127
+ /**
128
+ * API `viewpoints` gate sensor (AP-6): coverage feedback for the API surface, advisory (warn/info,
129
+ * never fails the gate). Classifies the feature's `@api` scenarios into the three API viewpoints and
130
+ * surfaces gaps — severity-weighted by method (read-only GET/HEAD surfaces don't expect error/idempotency):
131
+ * - `api-contract` — every invoked endpoint must have its response asserted ({{name.status|body}}).
132
+ * - `api-error` — a mutating endpoint without ANY failure scenario (@cases matrix or a 4xx/5xx) — the 70% band.
133
+ * - `api-idempotency` — a mutating endpoint without a @concurrent aggregate check ({{name.ok_count}}) — the 10% band.
134
+ * Reads the already-parsed scenarios (apiRefs + lowercase stepsText + casesDataset) — no re-parse.
135
+ */
136
+ const apiViewpointSensor: Sensor<GateInput> = {
137
+ id: 'api-viewpoints',
138
+ capability: 'api',
139
+ kind: 'gate',
140
+ run: ({ screenName, scenarios, cwd }) => {
141
+ const findings = [] as ReturnType<Sensor['run']>;
142
+ const add = (severity: 'warn' | 'info', message: string) => findings.push({ sensorId: 'api-viewpoints', capability: 'api', message, severity });
143
+ const apiScenarios = scenarios.filter((s) => (s.apiRefs ?? []).length);
144
+ if (!apiScenarios.length) return findings;
145
+
146
+ const refs = new Set<string>();
147
+ for (const s of apiScenarios) for (const r of s.apiRefs ?? []) refs.add(r);
148
+
149
+ // api-contract — each invoked endpoint's response is asserted somewhere.
150
+ for (const name of refs) {
151
+ const re = new RegExp(`\\b${name.toLowerCase()}\\.(status|ok|body|headers|ok_count|status_counts)\\b`);
152
+ if (!apiScenarios.some((s) => re.test(s.stepsText ?? ''))) {
153
+ add('warn', `VIEWPOINT-API-CONTRACT: @api:${name} is invoked but its response is never asserted — add the contract check (expect {{${name}.status}} / {{${name}.body.…}}).`);
154
+ }
155
+ }
156
+
157
+ // Method-aware: error + idempotency only apply when a referenced endpoint MUTATES.
158
+ const mutating: string[] = [];
159
+ for (const name of refs) {
160
+ try { if (/^(POST|PUT|PATCH|DELETE)$/.test(resolveApi(name, screenName, cwd).method)) mutating.push(name); } catch { /* unresolved → the verification sensor reports it */ }
161
+ }
162
+ if (!mutating.length) return findings; // read-only surface — no error/idempotency expectation
163
+
164
+ // api-error — a failure scenario exists: a @cases matrix, or an asserted 4xx/5xx.
165
+ const hasError = apiScenarios.some((s) => s.casesDataset || /\b[45]\d\d\b/.test(s.stepsText ?? ''));
166
+ if (!hasError) {
167
+ add('warn', `VIEWPOINT-API-ERROR: mutating endpoint(s) ${mutating.join(', ')} have no failure scenario (4xx/auth/validation) — add a @cases error matrix or an explicit 4xx assertion (the ~70% success+failure band).`);
168
+ }
169
+
170
+ // api-idempotency — a @concurrent aggregate check exists.
171
+ const hasIdem = apiScenarios.some((s) => /\b\w+\.(ok_count|status_counts)\b/.test(s.stepsText ?? ''));
172
+ if (!hasIdem) {
173
+ add('info', `VIEWPOINT-API-IDEMPOTENCY: no @concurrent idempotency/race check on a mutating endpoint — consider @concurrent:N + a @query DB cross-check (the ~10% band).`);
174
+ }
175
+ return findings;
176
+ },
177
+ };
178
+
108
179
  /** Register the API capability. */
109
180
  export function register(registry: CapabilityRegistry): void {
110
181
  registry.register({
@@ -112,7 +183,7 @@ export function register(registry: CapabilityRegistry): void {
112
183
  annotations: ['@api'],
113
184
  runtimeHelpers: [{ file: 'api.ts', template: 'specs-api.ts' }], // template resolved from core's templates dir
114
185
  preconditionCodegen: apiPreconditionCodegen, // @api:<name> → bind {{name}}
115
- sensors: [apiVerificationSensor],
186
+ sensors: [apiVerificationSensor, apiViewpointSensor],
116
187
  cliCommands: [registerApiCommand as (program: unknown) => void], // `sungen api import <openapi>`
117
188
  });
118
189
  }