@lovrozagar/oat 0.6.1 → 0.6.2
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/README.md +7 -1
- package/dist/cli.js +4 -0
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +23 -1
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/client.d.ts +2 -2
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +28 -9
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/rate-limit.d.ts +40 -2
- package/dist/runtime/rate-limit.d.ts.map +1 -1
- package/dist/runtime/rate-limit.js +115 -4
- package/dist/runtime/rate-limit.js.map +1 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +23 -18
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/world.d.ts +16 -0
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +89 -0
- package/dist/runtime/world.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -817,6 +817,10 @@ Parent path parameters are created first (depth-first through the owning entity'
|
|
|
817
817
|
|
|
818
818
|
A create that returns `>= 300` on the first variant fails the entity (downstream checks `BLOCKED`). Later variants that fail just shorten the cohort — a partial cohort is still used.
|
|
819
819
|
|
|
820
|
+
HTTP 429 is retried first — `Retry-After` or exponential backoff, up to five times — on seed, checks, and teardown, whether or not the operation declared `x-rate-limit`. The first 429 is never a seed failure. A leftover 429 after those retries is a gap, not a backend defect.
|
|
821
|
+
|
|
822
|
+
A 402 / plan-limit (`payment_required`, `*_plan_limit`) on create is not a backend defect when the same-tenant list already has a row — typically an earlier `x-effects` create that filled a free-plan quota. oat reuses that id so children (a `row` after extract created a `table`) can still seed. It does not invent records. Write-path checks on the adopted entity stand down.
|
|
823
|
+
|
|
820
824
|
A documented feature-gate 403 is the exception: if create declares `x-feature-gate` and the body is `vars.type: feature_gate` (and `vars.feature` matches the tag when present), oat records a `COVERAGE_GAP` naming the tag rather than a seed defect. See [`x-feature-gate`](#x-feature-gate).
|
|
821
825
|
|
|
822
826
|
`--seed` / `seed` makes the bodies identical across runs. It does not make server-assigned ids identical.
|
|
@@ -1172,7 +1176,9 @@ x-rate-limit: { category: ai, rps: 3 }
|
|
|
1172
1176
|
|
|
1173
1177
|
Groups operations sharing one throughput budget (`category`) and optionally the rate itself (`rps`). Without this, oat's matrix can hammer a `login` or paid-inference route past its real limit, collect 429s, and report them as backend defects — exactly the false-positive class that erodes trust fastest. With it, requests to that category are paced through a token bucket before they fire.
|
|
1174
1178
|
|
|
1175
|
-
|
|
1179
|
+
Tags are the **proactive** path. HTTP 429 is the **reactive** path and is honoured even when the operation has no `x-rate-limit` at all (untagged JWT writes, teardown DELETEs). oat reads `Retry-After` (delta-seconds or HTTP-date), otherwise backs off 1s, 2s, 4s… capped at 30s, and retries the same request up to five times. The wait is fed into the matching bucket, or into an implicit `untagged-write` bucket so the next call does not immediately re-trip.
|
|
1180
|
+
|
|
1181
|
+
A 429 is only ever reported when the request that drew it was demonstrably under the _declared_ rate — oat's own bucket had a free token, so it did not have to wait for one. A 429 that arrived only after oat's bucket made the request wait means oat's own rate model was too generous, which is paced around, never reported. A 429 against a config-supplied or implicit rate is never a finding.
|
|
1176
1182
|
|
|
1177
1183
|
`config.rateLimits` is checked first and needs no tag at all — it is what keeps oat usable against a backend that has not adopted `x-rate-limit` yet, or against an environment (staging, usually) whose real limit differs from what the document claims for production. A 429 against a config-supplied rate is never a finding: it is the operator's own belief about the environment, not a claim the API made.
|
|
1178
1184
|
|
package/dist/cli.js
CHANGED
|
@@ -236,6 +236,7 @@ export async function main() {
|
|
|
236
236
|
if (command === "conformance") {
|
|
237
237
|
const { postgresAvailable, renderParserSuite, renderSuite, runExampleSpecSuite, runTagUnlockSuite, runParserSuite, runPayloadCatalogSuite, runCoverageReportSuite, runSeedContractSuite, runTenantScopeSuite, runSuite, sqliteAvailable, d1Available, } = await import("./conformance/suite.js");
|
|
238
238
|
const { runFeatureGateSuite } = await import("./conformance/feature-gate.js");
|
|
239
|
+
const { runRateLimitSuite } = await import("./conformance/rate-limit.js");
|
|
239
240
|
const parser = renderParserSuite(runParserSuite());
|
|
240
241
|
process.stdout.write(parser.text);
|
|
241
242
|
/* The documented example is checked in the same breath: it is the only place a reader
|
|
@@ -256,6 +257,9 @@ export async function main() {
|
|
|
256
257
|
const featureGate = renderParserSuite(await runFeatureGateSuite());
|
|
257
258
|
process.stdout.write(featureGate.text);
|
|
258
259
|
parser.failures += featureGate.failures;
|
|
260
|
+
const rateLimit = renderParserSuite(await runRateLimitSuite());
|
|
261
|
+
process.stdout.write(rateLimit.text);
|
|
262
|
+
parser.failures += rateLimit.failures;
|
|
259
263
|
const seedContract = renderParserSuite(await runSeedContractSuite());
|
|
260
264
|
process.stdout.write(seedContract.text);
|
|
261
265
|
parser.failures += seedContract.failures;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/runtime/checks.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/runtime/checks.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/checks.js
CHANGED
|
@@ -33,6 +33,16 @@ function standDownForFeatureGate(ctx, op, exchange, check) {
|
|
|
33
33
|
"down rather than treat the documented 403 as a defect.");
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A 429 that survived Client retries is the bucket, not the backend. The check that needed a
|
|
38
|
+
* 2xx stands down; it is not a defect and it is not a concurrency conclusion.
|
|
39
|
+
*/
|
|
40
|
+
function standDownForRateLimit(ctx, exchange, check) {
|
|
41
|
+
if (exchange.status !== 429)
|
|
42
|
+
return false;
|
|
43
|
+
ctx.findings.unresolved(check, ctx.entityName, "the request was rate-limited (429) after retries, so this check could not complete");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
36
46
|
/** The list endpoint's derived conventions — parameter roles and envelope spellings. */
|
|
37
47
|
function conv(ctx) {
|
|
38
48
|
return ctx.listOp.conventions;
|
|
@@ -3246,6 +3256,10 @@ const noLostUpdate = {
|
|
|
3246
3256
|
return;
|
|
3247
3257
|
if (standDownForFeatureGate(ctx, ctx.updateOp, secondWrite, this.id))
|
|
3248
3258
|
return;
|
|
3259
|
+
if (standDownForRateLimit(ctx, firstWrite, this.id))
|
|
3260
|
+
return;
|
|
3261
|
+
if (standDownForRateLimit(ctx, secondWrite, this.id))
|
|
3262
|
+
return;
|
|
3249
3263
|
if (firstWrite.status >= 300 || secondWrite.status >= 300) {
|
|
3250
3264
|
return ctx.findings.unresolved(this.id, ctx.entityName, `a concurrent PATCH was rejected (${firstWrite.status}, ${secondWrite.status}), so no ` +
|
|
3251
3265
|
"race actually took place");
|
|
@@ -3343,6 +3357,8 @@ const declaredEffectsOccur = {
|
|
|
3343
3357
|
});
|
|
3344
3358
|
if (standDownForFeatureGate(ctx, op, invoked, this.id))
|
|
3345
3359
|
continue;
|
|
3360
|
+
if (standDownForRateLimit(ctx, invoked, this.id))
|
|
3361
|
+
continue;
|
|
3346
3362
|
if (invoked.status >= 400) {
|
|
3347
3363
|
ctx.findings.gap(this.id, ctx.entityName, `${op.operationId} could not be invoked`, `returned ${invoked.status}; its declared effects are unverified`);
|
|
3348
3364
|
continue;
|
|
@@ -3590,6 +3606,8 @@ const stringPayloadSurvives = {
|
|
|
3590
3606
|
const control = await writeField(ASCII_PAYLOAD_PROBE, id);
|
|
3591
3607
|
if (standDownForFeatureGate(ctx, writeOp, control, this.id))
|
|
3592
3608
|
return;
|
|
3609
|
+
if (standDownForRateLimit(ctx, control, this.id))
|
|
3610
|
+
return;
|
|
3593
3611
|
if (control.status === 401 || control.status === 403) {
|
|
3594
3612
|
return ctx.findings.unresolved(this.id, ctx.entityName, `the ASCII control write returned ${control.status}, so later refusals cannot be attributed to the payload`);
|
|
3595
3613
|
}
|
|
@@ -3632,7 +3650,9 @@ const stringPayloadSurvives = {
|
|
|
3632
3650
|
evidence.push(written);
|
|
3633
3651
|
continue;
|
|
3634
3652
|
}
|
|
3635
|
-
if (
|
|
3653
|
+
if (standDownForRateLimit(ctx, written, this.id))
|
|
3654
|
+
return;
|
|
3655
|
+
if (written.status === 404 || written.status === 409 || written.status === 415) {
|
|
3636
3656
|
continue;
|
|
3637
3657
|
}
|
|
3638
3658
|
if (written.status >= 400 && written.status < 500) {
|
|
@@ -3731,6 +3751,8 @@ const documentedStatusHonoured = {
|
|
|
3731
3751
|
continue;
|
|
3732
3752
|
if (isDocumentedFeatureGateDenial(op, exchange.status, exchange.responseBody))
|
|
3733
3753
|
continue;
|
|
3754
|
+
if (exchange.status === 429)
|
|
3755
|
+
continue;
|
|
3734
3756
|
const raw = ctx.model.rawOperations.get(op.operationId);
|
|
3735
3757
|
if (!declaresConcreteStatuses(raw))
|
|
3736
3758
|
continue;
|