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