@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.
- package/package.json +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.
|
|
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.
|
|
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
|
}
|