@sungen/driver-api 3.1.2-beta.112 → 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 +64 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sungen/driver-api",
3
- "version": "3.1.2-beta.112",
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.112",
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
@@ -60,6 +60,13 @@ function apiPreconditionCodegen(input: { tags: string[]; screenName: string; cwd
60
60
  // scenario's @api call(s); pair with @query to cross-check the DB ("exactly one charge").
61
61
  const concMatch = input.tags.map((t) => t.match(/^@concurrent:(\d+)$/)).find(Boolean);
62
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
+ : '';
63
70
  for (const tag of input.tags) {
64
71
  const m = tag.match(TAG);
65
72
  if (!m) continue;
@@ -76,10 +83,11 @@ function apiPreconditionCodegen(input: { tags: string[]; screenName: string; cwd
76
83
  // mechanism (the dynamic value comes via an override like `token={{login.body.token}}`).
77
84
  const hdr = entry.headers && Object.keys(entry.headers).length ? `, headers: ${JSON.stringify(entry.headers)}` : '';
78
85
  const req = `{ method: ${JSON.stringify(entry.method)}, path: ${JSON.stringify(entry.path)}${entry.body !== undefined ? `, body: ${JSON.stringify(entry.body)}` : ''}${hdr}, datasource: ${ds} }`;
79
- // @concurrent:N → callN (N parallel calls, aggregate result); else a single call.
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.
80
88
  const callExpr = concurrent
81
- ? `await api.callN(${label}, ${req}, { ${paramsObj} }, ${concurrent})`
82
- : `await api.call(${label}, ${req}, { ${paramsObj} })`;
89
+ ? `await api.callN(${label}, ${req}, { ${paramsObj} }, ${concurrent}${optsArg})`
90
+ : `await api.call(${label}, ${req}, { ${paramsObj} }${optsArg})`;
83
91
  out.push({
84
92
  comment: concurrent
85
93
  ? `@api:${name} ×${concurrent} (@concurrent) → bind {{${name}}} aggregates (${entry.method} ${entry.path})`
@@ -116,6 +124,58 @@ const apiVerificationSensor: Sensor<GateInput> = {
116
124
  },
117
125
  };
118
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
+
119
179
  /** Register the API capability. */
120
180
  export function register(registry: CapabilityRegistry): void {
121
181
  registry.register({
@@ -123,7 +183,7 @@ export function register(registry: CapabilityRegistry): void {
123
183
  annotations: ['@api'],
124
184
  runtimeHelpers: [{ file: 'api.ts', template: 'specs-api.ts' }], // template resolved from core's templates dir
125
185
  preconditionCodegen: apiPreconditionCodegen, // @api:<name> → bind {{name}}
126
- sensors: [apiVerificationSensor],
186
+ sensors: [apiVerificationSensor, apiViewpointSensor],
127
187
  cliCommands: [registerApiCommand as (program: unknown) => void], // `sungen api import <openapi>`
128
188
  });
129
189
  }