@lovrozagar/oat 0.5.0 → 0.6.1

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 (49) hide show
  1. package/README.md +65 -37
  2. package/bin/oat.js +3 -1
  3. package/dist/cli.js +50 -11
  4. package/dist/config/define-config.d.ts +17 -3
  5. package/dist/config/define-config.d.ts.map +1 -1
  6. package/dist/config/define-config.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +1 -0
  9. package/dist/runtime/async.d.ts +20 -5
  10. package/dist/runtime/async.d.ts.map +1 -1
  11. package/dist/runtime/async.js +65 -9
  12. package/dist/runtime/async.js.map +1 -1
  13. package/dist/runtime/auth.d.ts +23 -3
  14. package/dist/runtime/auth.d.ts.map +1 -1
  15. package/dist/runtime/auth.js +111 -27
  16. package/dist/runtime/auth.js.map +1 -1
  17. package/dist/runtime/checks.d.ts +2 -0
  18. package/dist/runtime/checks.d.ts.map +1 -1
  19. package/dist/runtime/checks.js +67 -20
  20. package/dist/runtime/checks.js.map +1 -1
  21. package/dist/runtime/client.d.ts +17 -1
  22. package/dist/runtime/client.d.ts.map +1 -1
  23. package/dist/runtime/client.js +86 -58
  24. package/dist/runtime/client.js.map +1 -1
  25. package/dist/runtime/fixture.d.ts.map +1 -1
  26. package/dist/runtime/fixture.js +86 -21
  27. package/dist/runtime/fixture.js.map +1 -1
  28. package/dist/runtime/run.d.ts +0 -6
  29. package/dist/runtime/run.d.ts.map +1 -1
  30. package/dist/runtime/run.js +17 -19
  31. package/dist/runtime/run.js.map +1 -1
  32. package/dist/runtime/sse.d.ts +17 -0
  33. package/dist/runtime/sse.d.ts.map +1 -0
  34. package/dist/runtime/sse.js +62 -0
  35. package/dist/runtime/sse.js.map +1 -0
  36. package/dist/runtime/teardown.js +1 -1
  37. package/dist/runtime/teardown.js.map +1 -1
  38. package/dist/runtime/world.js +2 -2
  39. package/dist/runtime/world.js.map +1 -1
  40. package/dist/spec/collection.d.ts +2 -0
  41. package/dist/spec/collection.d.ts.map +1 -1
  42. package/dist/spec/collection.js +16 -0
  43. package/dist/spec/collection.js.map +1 -1
  44. package/dist/spec/graph.d.ts +2 -0
  45. package/dist/spec/graph.d.ts.map +1 -1
  46. package/dist/spec/graph.js +2 -1
  47. package/dist/spec/graph.js.map +1 -1
  48. package/labs/oob-auth.config.ts +11 -1
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -86,15 +86,15 @@ oat help # same as oat --help
86
86
  oat --help
87
87
  ```
88
88
 
89
- Unknown commands and missing required flags exit `2`.
89
+ Unknown commands, unknown flags, and missing required flags exit `2`.
90
90
 
91
91
  ## How a run works
92
92
 
93
93
  1. **Load** the OpenAPI document (URL or path). Internal `$ref`s are inlined. External `$ref`s are reported, never fetched.
94
94
  2. **Model** entities by inverting `x-invalidate` (or path heuristics) into a read surface per entity.
95
- 3. **Authenticate** every configured principal (static headers and/or an auth flow). Credentials refresh themselves before they expire.
95
+ 3. **Authenticate** every configured principal (static headers and/or an auth flow). Credentials refresh on a countdown from `exp` (default 30s buffer) before every dispatch and each async poll.
96
96
  4. **Seed** a cohort of records per entity, in parent-before-child order, using each entity's create operation.
97
- 5. **Test** the matrix, one entity at a time: foundations first, then composition, writes, isolation, declared effects. Checks inside an entity stay ordered. Entities may run in parallel (`concurrency`).
97
+ 5. **Test** the matrix, one entity at a time: foundations first, then composition, writes, isolation, declared effects. Entities run in series. Checks inside an entity stay ordered.
98
98
  6. **Teardown** everything the run created, unless `--keep-fixtures` / `keepFixtures: true`.
99
99
 
100
100
  The first principal is the writer. Isolation needs a second principal with different `roots`. A rank lattice needs two or more principals that share `roots` and differ in `rank`. Invite checks need `x-invite` plus a peer with `inviteAs`.
@@ -188,7 +188,6 @@ Same object. `${NAME}` is interpolated after load. There is no `defineConfig` wr
188
188
  ],
189
189
  "seed": 42,
190
190
  "cohortSize": 7,
191
- "concurrency": 1,
192
191
  "outDir": "./oat-out"
193
192
  }
194
193
  ```
@@ -254,24 +253,23 @@ oat help
254
253
 
255
254
  Requires `--config`. CLI flags override the same field in the config when both are set.
256
255
 
257
- | flag | default | meaning |
258
- | ----------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
259
- | `--config` | required | module or JSON file, default export |
260
- | `--base-url` | `config.baseUrl` | backend origin |
261
- | `--only` | `config.only` or all entities | comma-separated entity names as `oat plan` prints them (singularised) |
262
- | `--seed` | `config.seed` or `1` | fixture generation seed (reproducible) |
263
- | `--out` | `config.outDir` or `./oat-out` | report directory |
264
- | `--concurrency` | `config.concurrency` or `1` | entities in parallel. Use `1` on nested graphs; higher values insert children while a parent page-walk is running and invent pagination bugs |
265
- | `--max-in-flight` | `config.maxInFlight` or `4` | HTTP requests allowed at once |
266
- | `--keep-fixtures` | `config.keepFixtures` or false | do not DELETE what the run created |
267
- | `--quiet` | false | no stderr progress; files under `--out` still update |
256
+ | flag | default | meaning |
257
+ | ----------------- | ------------------------------ | --------------------------------------------------------------------- |
258
+ | `--config` | required | module or JSON file, default export |
259
+ | `--base-url` | `config.baseUrl` | backend origin |
260
+ | `--only` | `config.only` or all entities | comma-separated entity names as `oat plan` prints them (singularised) |
261
+ | `--seed` | `config.seed` or `1` | fixture generation seed (reproducible) |
262
+ | `--out` | `config.outDir` or `./oat-out` | report directory |
263
+ | `--max-in-flight` | `config.maxInFlight` or `4` | HTTP requests allowed at once |
264
+ | `--keep-fixtures` | `config.keepFixtures` or false | do not DELETE what the run created |
265
+ | `--quiet` | false | no stderr progress; files under `--out` still update |
268
266
 
269
- **Exit codes:** `0` no defects, `1` at least one root-cause finding (`BACKEND_BUG`, `SPEC_BUG`, `SECURITY`, `AMBIGUITY`), `2` usage error (missing `--config`, no principals). `COVERAGE_GAP` and `BLOCKED` do not fail the process.
267
+ **Exit codes:** `0` no defects, `1` at least one root-cause finding (`BACKEND_BUG`, `SPEC_BUG`, `SECURITY`, `AMBIGUITY`), `2` usage error (missing `--config`, no principals, unknown flag). `COVERAGE_GAP` and `BLOCKED` do not fail the process.
270
268
 
271
269
  Example:
272
270
 
273
271
  ```bash
274
- oat run --config oat.config.ts --only store,product --concurrency 1 --out oat-out/prod
272
+ oat run --config oat.config.ts --only store,product --out oat-out/prod
275
273
  ```
276
274
 
277
275
  `--only store,product` matches the **entity names** from `oat plan`, not path segments. `/v1/stores` is usually the entity `store`. If a name is unknown, that entity is simply not tested (the others still run).
@@ -431,7 +429,6 @@ export default defineConfig({
431
429
  roots: { org_id: "org_shared" }, // path params oat cannot create; also declarable via x-root
432
430
  seed: 42, // fixture generation; a failing run with the same seed is identical
433
431
  cohortSize: 12, // records created per entity (default 7)
434
- concurrency: 1, // entities in parallel
435
432
  maxInFlight: 4, // HTTP in flight
436
433
  only: ["store", "product"], // restrict entities
437
434
  keepFixtures: false,
@@ -450,7 +447,6 @@ export default defineConfig({
450
447
  | `roots` | no | `{}` | Shared path params (merged with each principal's `roots`) |
451
448
  | `seed` | no | `1` | Integer. Same seed → same fixture bodies |
452
449
  | `cohortSize` | no | `7` | Sliced from the 7 built-in variants. Larger repeats the pattern |
453
- | `concurrency` | no | `1` | Entity-level only. Checks inside one entity stay ordered |
454
450
  | `maxInFlight` | no | `4` | Across the whole run |
455
451
  | `only` | no | all | Entity names from `oat plan` |
456
452
  | `keepFixtures` | no | `false` | Skip DELETE at the end |
@@ -458,7 +454,7 @@ export default defineConfig({
458
454
 
459
455
  `spec` may be a path relative to `baseUrl` (`/v1/openapi/spec`) or an absolute URL or a file.
460
456
 
461
- CLI `--base-url`, `--only`, `--seed`, `--out`, `--concurrency`, `--max-in-flight`, `--keep-fixtures` override these when passed.
457
+ CLI `--base-url`, `--only`, `--seed`, `--out`, `--max-in-flight`, `--keep-fixtures` override these when passed.
462
458
 
463
459
  ### Spec loading
464
460
 
@@ -540,16 +536,33 @@ principals: [
540
536
 
541
537
  ```ts
542
538
  auth: {
543
- steps: [ /* at least one */ ],
539
+ steps: [ /* register / verify — first acquire only */ ],
544
540
  credentialFrom: "$.access_token", // JSON path in the last (or saved) response
545
541
  expiresInFrom: "$.expires_in", // lifetime in seconds
546
542
  header: "authorization", // default
547
543
  template: "Bearer {credential}", // default
548
544
  assumeTtlMs: 3600000, // used only if neither expiresInFrom nor JWT exp is present
545
+ refreshBufferMs: 30_000, // optional; default 30s. Proactive when expiresAt - now <= this
546
+ refresh: { // signup flows must set this — re-running steps is not a refresh
547
+ steps: [
548
+ {
549
+ operationId: "auth.refreshToken",
550
+ body: { refresh_token: "{refreshToken}" },
551
+ saveAs: {
552
+ credential: "$.access_token",
553
+ refreshToken: "$.refresh_token",
554
+ },
555
+ },
556
+ ],
557
+ },
549
558
  }
550
559
  ```
551
560
 
552
- Expiry is `expiresInFrom` (seconds) → JWT `exp` claim → `assumeTtlMs`. Refresh happens before dispatch, at roughly three quarters of the assumed window (`assumeTtlMs` default 300000 ms when used as the window), never by retrying a 401.
561
+ Expiry is `expiresInFrom` (seconds) → JWT `exp` claim → `assumeTtlMs`. `assumeTtlMs` is only the fallback lifetime when nothing else revealed expiry — it is not used for the refresh threshold when `expiresAt` is known.
562
+
563
+ Refresh is countdown-based (`expiresAt - refreshBufferMs`, default 30s) before every dispatch and before each async poll. Signup flows declare `auth.refresh` (refresh-token operation). Re-running `steps` is only the fallback when `refresh` is omitted (API-key / token-exchange principals). A register-like first hop without `refresh` fails closed (`AUTH_REFRESH_REQUIRED`) rather than signing up again.
564
+
565
+ One 401 → force refresh + single retry with live headers. A second 401 is evidence. 5xx / 429 are never a refresh trigger. `expiresAt === null` (static-header principal) never proactive-refreshes.
553
566
 
554
567
  Each step is one of:
555
568
 
@@ -561,7 +574,7 @@ Each step is one of:
561
574
  body: { key: "${API_KEY}" },
562
575
  headers: { "x-extra": "1" },
563
576
  query: { realm: "test" },
564
- saveAs: { token: "$.access_token" },
577
+ saveAs: { credential: "$.access_token", refreshToken: "$.refresh_token" },
565
578
  saveClaimsFrom: { token: "$.access_token", bind: { orgId: "orgs.0.oid" } },
566
579
  bind: { address: "user@example.test" }, // literals, with {name} interpolation
567
580
  expect: [200], // default: any 2xx
@@ -585,7 +598,7 @@ Each step is one of:
585
598
  { outOfBand: { address: "{address}", kind: "email-verify", as: "verifyToken" } }
586
599
  ```
587
600
 
588
- Later steps interpolate `{name}` from the flow scope. `saveAs` paths are `$.foo.bar` / `$.orgs.0.id` (dot + numeric index only; no JSON Pointer, no filters). `saveClaimsFrom` reads a JWT's _claims_ (signature is not verified — oat is reading its own credential). `rootsFromFlow` maps path parameter names to those bound keys.
601
+ Later steps interpolate `{name}` from the flow scope. `saveAs` paths are `$.foo.bar` / `$.orgs.0.id` (dot + numeric index only; no JSON Pointer, no filters). Bind `saveAs.refreshToken` from `$.refresh_token` so `{refreshToken}` interpolates in `auth.refresh`. `saveClaimsFrom` reads a JWT's _claims_ (signature is not verified — oat is reading its own credential). `rootsFromFlow` maps path parameter names to those bound keys.
589
602
 
590
603
  `bind` on a step runs **before** the request. `saveAs` / `saveClaimsFrom` run **after**. `credentialFrom` is read from the last HTTP response unless a step already saved `credential`.
591
604
 
@@ -598,6 +611,16 @@ function signUp(email: string): AuthFlow {
598
611
  return {
599
612
  credentialFrom: "$.access_token",
600
613
  expiresInFrom: "$.access_token_expires_in",
614
+ refresh: {
615
+ steps: [
616
+ {
617
+ body: { refresh_token: "{refreshToken}" },
618
+ method: "POST",
619
+ path: "/v1/auth/refresh",
620
+ saveAs: { credential: "$.access_token", refreshToken: "$.refresh_token" },
621
+ },
622
+ ],
623
+ },
601
624
  steps: [
602
625
  {
603
626
  bind: { address: email },
@@ -610,6 +633,7 @@ function signUp(email: string): AuthFlow {
610
633
  body: { token: "{verifyToken}" },
611
634
  method: "POST",
612
635
  path: "/v1/auth/email/verify",
636
+ saveAs: { credential: "$.access_token", refreshToken: "$.refresh_token" },
613
637
  saveClaimsFrom: {
614
638
  token: "$.access_token",
615
639
  bind: { orgId: "orgs.0.oid", projectId: "orgs.0.pids.0" },
@@ -783,7 +807,7 @@ Numbers use the ladder `1, 2, 5, 10, 20, 50, 100` so **lexical order ≠ numeric
783
807
 
784
808
  Empty schemas (`{}`, `true`, `additionalProperties: {}`) and cyclic `$ref`s after inlining stop the walk — they become a scalar or `{}`, never another object descent. A `RangeError` during generation is a `COVERAGE_GAP` on that entity naming the operationId and JSON pointer (`fixture generation overflow on table.create (/)`), not `blocked by unknown`.
785
809
 
786
- String fields honour `format` and `pattern`: `email` → `oat-{variant}-{index}@example.test`, `uri` / `url` → `https://example.test/...`, `uuid` → a fixed-shape UUID, `pattern` → a string that matches (or the field is omitted / the entity is a gap). `"Quarterly Report N"` is only used when the document does not constrain the string.
810
+ String fields honour `format`, `pattern`, and `minLength` together: `email` → `oat-{variant}-{index}@example.test`, `uri` / `url` → `https://example.test/...`, `uuid` → a fixed-shape UUID, `pattern` → a string that matches (or the field is omitted / the entity is a gap). A generated string is padded to `minLength` (repeat the last character) without breaking `pattern`. `"Quarterly Report N"` is only used when the document does not constrain the string. `minLength` greater than `maxLength` omits an optional field and records `missingRequired` on a required one.
787
811
 
788
812
  An operation with `x-invite` is not `entity.create`. oat does not POST a generated invitee. The invite check sends `granteeField` = the peer's `inviteAs`. Missing `inviteAs` is a coverage gap naming the tag.
789
813
 
@@ -870,7 +894,7 @@ Collection shape is derived from the success JSON schema, not from hardcoded wra
870
894
  | page | `page`, `pageNumber`, `page_number`, `offset` |
871
895
  | limit | `limit`, `perPage`, `per_page`, `pageSize`, `page_size` |
872
896
 
873
- Success schema is the first JSON media type on responses `200`, `201`, `202`, `2XX`, or `default`. Request schema is taken from `requestBody` preferring `multipart/form-data`, then `application/x-www-form-urlencoded`, then JSON. A raw `text/event-stream` body is not treated as a JSON schema defect.
897
+ Success schema is the first JSON media type on responses `200`, `201`, `202`, `2XX`, or `default`. Request schema is taken from `requestBody` preferring `multipart/form-data`, then `application/x-www-form-urlencoded`, then JSON. A success response that lists `text/event-stream` is a stream: oat consumes it to the end and does not treat the raw body as a JSON schema defect. Media type is the stream tag — there is no `x-stream`.
874
898
 
875
899
  ## OpenAPI meta tags
876
900
 
@@ -949,6 +973,8 @@ An invite operation is **not** the entity's fixture create, even when it is `POS
949
973
 
950
974
  Timeline asserted: cannot read → invite → still cannot → accept → can → revoke → cannot.
951
975
 
976
+ Accept and revoke send the documented JSON request body when the operation declares one, filled from the invite token / grant id (and other known scope values). A path-only accept (`POST /invites/{token}` with no body) stays path-only — oat does not invent a body.
977
+
952
978
  **Fallback:** the check does not run.
953
979
 
954
980
  ### `x-query`
@@ -983,11 +1009,17 @@ x-async:
983
1009
  pollIntervalMs: 2000
984
1010
  ```
985
1011
 
986
- The HTTP response is a receipt. oat polls until `until` matches, then treats that payload as the result. `poll` may be an operationId or `"GET /path/{id}"`. Defaults: `timeoutMs: 120000`, `pollIntervalMs: 2000`.
1012
+ `x-async` means **POST is a receipt, then poll**. It is not how you mark a stream. A stream is a success response that lists `text/event-stream`.
1013
+
1014
+ When the start response is JSON, oat polls `poll` until `until` matches, then treats that payload as the result. `poll` may be an operationId or `"GET /path/{id}"`. Defaults: `timeoutMs: 120000`, `pollIntervalMs: 2000`.
1015
+
1016
+ When the start response is `text/event-stream`, oat parses `event:` / `data:` frames. `data` is JSON when it starts with `{` / `[`. `idFrom` is resolved against **each event's JSON `data`**, first hit wins — `$.batch_id` on `event: batch` data `{ "batch_id": "…" }` works. A frame named `complete` / `error`, or one whose `data` matches `until`, is the terminal record; `successWhen` is applied to it and oat does **not** GET the poll route. Poll only if the stream ended without a terminal frame **and** `idFrom` resolved.
1017
+
1018
+ A spec may leave both `x-async` and `text/event-stream` on the same operation. The stream is still the result; `x-async` only supplies `idFrom` / `until` / the poll fallback.
987
1019
 
988
1020
  `until` / `successWhen` use the same `field.op.value` predicates as filters (`eq`, `in`, …).
989
1021
 
990
- **Fallback:** treated as synchronous; async checks are `COVERAGE_GAP`.
1022
+ **Fallback:** treated as synchronous; async checks are `COVERAGE_GAP`. A 2xx stream without `x-async` is consumed and recorded; async checks do not run.
991
1023
 
992
1024
  ### `x-effects`
993
1025
 
@@ -1211,8 +1243,8 @@ Order is fixed (foundations first) so cascade suppression has a cause to point a
1211
1243
  | `softdelete.absent-from-default-list` | a soft-deleted row is gone from the default list | `x-soft-delete` | `list.read-after-write` |
1212
1244
  | `invalidation.declared-route-changes` | after a write, the other entity's listed route actually changes | `x-invalidate` naming another entity | list, persist |
1213
1245
  | `effects.declared-effect-occurs` | `x-effects` cardinality delta is observed on the named list | `x-effects` | `list.read-after-write` |
1214
- | `async.reaches-terminal-state` | polling `x-async` reaches `until` before `timeoutMs` | `x-async` | — |
1215
- | `async.receipt-identifies-the-job` | `idFrom` on the receipt resolves to a pollable job | `x-async` + `idFrom` | — |
1246
+ | `async.reaches-terminal-state` | `x-async` reaches `until` (poll, or a terminal SSE frame) before `timeoutMs` | `x-async` | — |
1247
+ | `async.receipt-identifies-the-job` | `idFrom` on the receipt (JSON object or SSE event JSON) resolves to a pollable job | `x-async` + `idFrom` | — |
1216
1248
  | `patch.minimality` | PATCH `{ name }` does not clear other writable fields | update + item route | — |
1217
1249
  | `idempotency.replay-does-not-duplicate` | same Idempotency-Key + same body does not create a second row | create + documented Idempotency-Key header | list, persist |
1218
1250
  | `delete.absent-record-returns-404` | DELETE of a missing id is 404, not 200 | delete | — |
@@ -1471,7 +1503,6 @@ const result = await run({
1471
1503
  baseUrl: config.baseUrl,
1472
1504
  principals: config.principals,
1473
1505
  seed: 1,
1474
- concurrency: 1,
1475
1506
  maxInFlight: 4,
1476
1507
  onProgress: (snap) => {
1477
1508
  // snap.phase, snap.entity, snap.check, snap.message, …
@@ -1494,7 +1525,7 @@ const { doc: resolved, externalRefs } = dereference(doc)
1494
1525
  const model = buildModel(resolved)
1495
1526
  ```
1496
1527
 
1497
- Types exported: `OatConfig`, `Principal`, `AuthFlow`, `AuthStep`, `Hooks`, `Uploads`, `UploadRequest`, `UploadFile`, `RunOptions`, `RunResult`, `Finding`, `Verdict`, `Actor`, `SpecModel`, `EntityModel`, `OperationModel`, `OpenApiDocument`, matrix types.
1528
+ Types exported: `OatConfig`, `Principal`, `AuthFlow`, `AuthRefresh`, `AuthStep`, `Hooks`, `Uploads`, `UploadRequest`, `UploadFile`, `RunOptions`, `RunResult`, `Finding`, `Verdict`, `Actor`, `SpecModel`, `EntityModel`, `OperationModel`, `OpenApiDocument`, `AuthRefreshRequiredError`, matrix types.
1498
1529
 
1499
1530
  ## CI
1500
1531
 
@@ -1508,7 +1539,7 @@ Against **your** API:
1508
1539
  # GitHub Actions sketch
1509
1540
  - run: npm i -D @lovrozagar/oat
1510
1541
  - run: oat doctor --spec "$SPEC_URL"
1511
- - run: oat run --config oat.config.ts --concurrency 1
1542
+ - run: oat run --config oat.config.ts
1512
1543
  env:
1513
1544
  API_TOKEN: ${{ secrets.API_TOKEN }}
1514
1545
  API_TOKEN_B: ${{ secrets.API_TOKEN_B }}
@@ -1516,8 +1547,6 @@ Against **your** API:
1516
1547
 
1517
1548
  Gate on exit code 1 (defects). Read `oat-out/oat-report.json` if you need to classify verdicts.
1518
1549
 
1519
- Nested APIs: keep `--concurrency 1`. Higher values create children while a parent page-walk is in flight and produce false `pagination.page-walk-covers-set` findings.
1520
-
1521
1550
  Wipe leftover rows on a shared database between runs if a previous `--keep-fixtures` or a crashed teardown left data. Leftover rows make numeric/filter checks look like type bugs (`1, 10, 2` from old TEXT-sorted leftovers mixed with a fresh numeric cohort).
1522
1551
 
1523
1552
  ## Reference defects (`oat serve --defects`)
@@ -1599,7 +1628,7 @@ oat run --config labs/local.config.ts --base-url <url>
1599
1628
  These are deliberate. An agent should not invent a flag for them.
1600
1629
 
1601
1630
  - **No request timeout.** `fetch` waits until the server answers. Watch `idle_ms`.
1602
- - **No retry on 5xx / 429 / 401.** A 401 mid-run is evidence, not a refresh trigger. Refresh is expiry-based only.
1631
+ - **No retry on 5xx / 429.** One 401 → force refresh + single retry. A second 401 is evidence.
1603
1632
  - **No OpenAPI `security`.** Put credentials in `principals`. Cookie auth is a `headers: { cookie: "…" }` (or a flow that sets that header).
1604
1633
  - **No `servers[]`.** Always set `baseUrl`.
1605
1634
  - **No OCR.** Multipart and file parts are sent as dummy / pool / `resolveUpload` bytes. oat checks HTTP status and JSON responses, not whether a PDF is a real invoice.
@@ -1609,7 +1638,6 @@ These are deliberate. An agent should not invent a flag for them.
1609
1638
  - **Rate-limit pacing only covers requests oat itself sends.** It cannot see traffic from anything else hitting the backend at the same time, so a shared budget can still trip even when oat's own share was within the declared rate.
1610
1639
  - **Equality filter grammar** cannot express `neq` / `gt` / `like` / `and` / `or`. Those checks did-not-apply, they do not fail.
1611
1640
  - **`or()` is postgrest-only.**
1612
- - **Concurrency > 1 on nested graphs invents pagination bugs.** Use `1`.
1613
1641
  - **Leftover rows on a shared DB** poison numeric and filter checks. Wipe between runs.
1614
1642
  - **`--only` uses plan names** (`store`, not `stores` or `/v1/stores`).
1615
1643
  - **Default cohort is 7.** `pagination.limit-respects-documented-max` needs `cohortSize > maxLimit`.
package/bin/oat.js CHANGED
@@ -1,2 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "../dist/cli.js"
2
+ import { start } from "../dist/cli.js"
3
+
4
+ start()
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { createWriteStream, writeFileSync } from "node:fs";
3
3
  import { mkdir, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
5
6
  import { interpolate, loadConfig } from "./config/load.js";
6
7
  import { report } from "./report/console.js";
7
8
  import { renderMatrixGraph, renderMatrixHtml } from "./report/matrix.js";
@@ -10,7 +11,28 @@ import { createStderrProgress, formatProgressJsonl, formatProgressLine, formatPr
10
11
  import { renderTeardown } from "./runtime/teardown.js";
11
12
  import { buildModel } from "./spec/graph.js";
12
13
  import { dereference, loadSpec } from "./spec/load.js";
13
- function parseArgs(argv) {
14
+ export const KNOWN_FLAGS = new Set([
15
+ "help",
16
+ "config",
17
+ "spec",
18
+ "base-url",
19
+ "only",
20
+ "profile",
21
+ "seed",
22
+ "out",
23
+ "keep-fixtures",
24
+ "max-in-flight",
25
+ "quiet",
26
+ "untagged",
27
+ "backend",
28
+ "dialect",
29
+ "fuzz",
30
+ "precision",
31
+ "max-defects",
32
+ "json",
33
+ "parser",
34
+ ]);
35
+ export function parseArgs(argv) {
14
36
  const [command = "help", ...rest] = argv;
15
37
  const flags = {};
16
38
  for (let i = 0; i < rest.length; i++) {
@@ -29,7 +51,14 @@ function parseArgs(argv) {
29
51
  }
30
52
  return { command, flags };
31
53
  }
32
- const USAGE = `oat — OpenAPI Tester
54
+ export function unknownFlag(flags) {
55
+ for (const key of Object.keys(flags)) {
56
+ if (!KNOWN_FLAGS.has(key))
57
+ return key;
58
+ }
59
+ return undefined;
60
+ }
61
+ export const USAGE = `oat — OpenAPI Tester
33
62
 
34
63
  oat run --config <file> test a live backend and write a report
35
64
  oat plan --spec <url|file> derive and print the test model (offline)
@@ -46,7 +75,6 @@ Flags
46
75
  --seed integer seed for fixture generation (default 1)
47
76
  --out output directory for the report (default ./oat-out)
48
77
  --keep-fixtures leave created records in place instead of tearing them down
49
- --concurrency entities tested in parallel (default 1)
50
78
  --max-in-flight requests allowed in flight at once (default 4)
51
79
  --quiet no live progress on stderr (progress.log still written)
52
80
  --untagged serve (or test) a document with every x-* tag stripped
@@ -139,7 +167,6 @@ async function commandRun(flags) {
139
167
  ...(config.profiles === undefined ? {} : { profiles: config.profiles }),
140
168
  ...(config.rateLimits === undefined ? {} : { rateLimits: config.rateLimits }),
141
169
  keepFixtures: flags["keep-fixtures"] === true || config.keepFixtures === true,
142
- concurrency: Number.parseInt(str(flags, "concurrency") ?? "", 10) || config.concurrency || 1,
143
170
  maxInFlight: Number.parseInt(str(flags, "max-in-flight") ?? "", 10) || config.maxInFlight || 4,
144
171
  seed: seedFlag === undefined ? (config.seed ?? 1) : Number.parseInt(seedFlag, 10),
145
172
  onProgress,
@@ -195,8 +222,13 @@ async function commandRun(flags) {
195
222
  * not failures, and a CI gate should react to defects only. */
196
223
  return result.findings.filter((f) => f.verdict !== "COVERAGE_GAP" && f.verdict !== "BLOCKED").length > 0 ? 1 : 0;
197
224
  }
198
- async function main() {
225
+ export async function main() {
199
226
  const { command, flags } = parseArgs(process.argv.slice(2));
227
+ const unknown = unknownFlag(flags);
228
+ if (unknown !== undefined) {
229
+ process.stderr.write(`oat: unknown flag "--${unknown}"\n\n${USAGE}`);
230
+ return 2;
231
+ }
200
232
  if (command === "help" || flags.help === true) {
201
233
  process.stdout.write(USAGE);
202
234
  return 0;
@@ -403,10 +435,17 @@ async function main() {
403
435
  return 2;
404
436
  }
405
437
  }
406
- main().then((code) => {
407
- process.exitCode = code;
408
- }, (error) => {
409
- process.stderr.write(`oat: ${error instanceof Error ? error.message : String(error)}\n`);
410
- process.exitCode = 1;
411
- });
438
+ export function start() {
439
+ main().then((code) => {
440
+ process.exitCode = code;
441
+ }, (error) => {
442
+ process.stderr.write(`oat: ${error instanceof Error ? error.message : String(error)}\n`);
443
+ process.exitCode = 1;
444
+ });
445
+ }
446
+ /* `node dist/cli.js …` is the test/CI entry. `bin/oat.js` calls start() itself because
447
+ * argv[1] is the bin wrapper, not this module. */
448
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
449
+ start();
450
+ }
412
451
  //# sourceMappingURL=cli.js.map
@@ -56,8 +56,13 @@ export interface OutOfBandStep {
56
56
  };
57
57
  }
58
58
  export type AuthStep = OperationStep | RequestStep | OutOfBandStep;
59
+ /** How to renew a credential without re-running the acquire chain. */
60
+ export interface AuthRefresh {
61
+ /** Run against the existing scope so `{refreshToken}` interpolates. */
62
+ steps: [AuthStep, ...AuthStep[]];
63
+ }
59
64
  export interface AuthFlow {
60
- /** Run in order; values flow forward through the scope. */
65
+ /** Run in order; values flow forward through the scope. First acquire only. */
61
66
  steps: [AuthStep, ...AuthStep[]];
62
67
  /** Where the credential is in the final response, e.g. `$.access_token`. */
63
68
  credentialFrom: string;
@@ -69,6 +74,17 @@ export interface AuthFlow {
69
74
  template?: string;
70
75
  /** Used only when neither the response nor the credential reveals an expiry. */
71
76
  assumeTtlMs?: number;
77
+ /**
78
+ * Proactive refresh fires when `expiresAt - now <= refreshBufferMs`.
79
+ * Defaults to 30_000. Ignored when `expiresAt` is null (static-header principal).
80
+ */
81
+ refreshBufferMs?: number;
82
+ /**
83
+ * Renew via these steps (typically `auth.refreshToken` + `{refreshToken}`).
84
+ * Signup flows must set this — re-running `steps` would register again.
85
+ * Omitted: re-run `steps` (API-key / token-exchange). Register-like first hops fail closed.
86
+ */
87
+ refresh?: AuthRefresh;
72
88
  }
73
89
  export interface Principal {
74
90
  id: string;
@@ -213,8 +229,6 @@ export interface OatConfig {
213
229
  seed?: number;
214
230
  /** Instances seeded per entity. Larger cohorts discriminate more, and cost more. */
215
231
  cohortSize?: number;
216
- /** Entities tested in parallel. Checks within one entity always stay ordered. */
217
- concurrency?: number;
218
232
  /** Requests allowed in flight at once. Past a server's comfort this makes runs slower. */
219
233
  maxInFlight?: number;
220
234
  /** Restrict the run to these entity names. */
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.js","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA6PH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC7C,OAAO,MAAM,CAAA;AACd,CAAC"}
1
+ {"version":3,"file":"define-config.js","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA4QH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC7C,OAAO,MAAM,CAAA;AACd,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { defineConfig } from "./config/define-config.ts";
2
- export type { AuthFlow, AuthStep, Hooks, OatConfig, OperationStep, OutOfBandRequest, OutOfBandStep, Principal, ProfileSpec, RateLimitSpec, RequestStep, UploadFile, UploadRequest, UploadResolution, Uploads, } from "./config/define-config.ts";
2
+ export type { AuthFlow, AuthRefresh, AuthStep, Hooks, OatConfig, OperationStep, OutOfBandRequest, OutOfBandStep, Principal, ProfileSpec, RateLimitSpec, RequestStep, UploadFile, UploadRequest, UploadResolution, Uploads, } from "./config/define-config.ts";
3
3
  export { loadConfig } from "./config/load.ts";
4
4
  export { deriveCollectionShape, deriveIdentity } from "./spec/collection.ts";
5
5
  export type { CollectionShape } from "./spec/collection.ts";
@@ -7,6 +7,7 @@ export { buildModel } from "./spec/graph.ts";
7
7
  export type { EntityModel, OperationModel, SpecModel } from "./spec/graph.ts";
8
8
  export { dereference, loadSpec, normalisePath, parseRouteRef } from "./spec/load.ts";
9
9
  export type { OpenApiDocument } from "./spec/types.ts";
10
+ export { AuthRefreshRequiredError } from "./runtime/auth.ts";
10
11
  export { run } from "./runtime/run.ts";
11
12
  export type { RunOptions, RunResult } from "./runtime/run.ts";
12
13
  export type { Actor } from "./runtime/checks.ts";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { loadConfig } from "./config/load.js";
3
3
  export { deriveCollectionShape, deriveIdentity } from "./spec/collection.js";
4
4
  export { buildModel } from "./spec/graph.js";
5
5
  export { dereference, loadSpec, normalisePath, parseRouteRef } from "./spec/load.js";
6
+ export { AuthRefreshRequiredError } from "./runtime/auth.js";
6
7
  export { run } from "./runtime/run.js";
7
8
  export { buildMatrixGraph, matrixViewFromReport, renderMatrixGraph, renderMatrixHtml } from "./report/matrix.js";
8
9
  export { coverageByCheck, renderConsole, renderJson, renderMarkdown, renderRepros } from "./report/render.js";
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Async operation support.
3
3
  *
4
- * For an operation declared with `x-async`, the HTTP response is a receipt, not an outcome. oat
5
- * drives the poll route to a terminal state and treats that payload as the real result — without
6
- * this, an extraction or batch API is testable only as "returned 202", which for such an API is
7
- * most of the surface.
4
+ * `x-async` means the HTTP response is a receipt, then oat polls. A `text/event-stream` body is
5
+ * the result itself — parse events, do not `JSON.parse` the raw string. Poll only when the
6
+ * stream ends without a terminal frame and `idFrom` resolved.
8
7
  */
9
8
  import type { AsyncSpec } from "../spec/extensions.ts";
10
9
  import type { Client, Exchange } from "./client.ts";
10
+ import { type SseEvent } from "./sse.ts";
11
11
  export interface AsyncOutcome {
12
12
  /** Terminal state reached, or null on timeout. */
13
13
  terminal: Record<string, unknown> | null;
@@ -23,6 +23,21 @@ export interface AsyncOutcome {
23
23
  * language as the filter grammar rather than inventing a second one.
24
24
  */
25
25
  export declare function matchesPredicate(record: Record<string, unknown>, expression: string): boolean;
26
+ export declare function readPath(body: unknown, path: string): unknown;
27
+ /**
28
+ * Resolve `idFrom` against a JSON receipt, or against each SSE event's JSON `data` (first hit).
29
+ */
30
+ export declare function resolveAsyncId(receipt: unknown, idFrom: string): unknown;
31
+ export interface StreamAsyncView {
32
+ id: unknown;
33
+ /** Event data that carried `idFrom`, for binding the poll path. */
34
+ idRecord: Record<string, unknown> | null;
35
+ /** `complete` / `error` frame, or a frame whose `data` matches `until`. */
36
+ terminal: Record<string, unknown> | null;
37
+ events: SseEvent[];
38
+ }
39
+ /** Read job id and terminal state from SSE frames. `null` when the body is not a stream. */
40
+ export declare function inspectStreamAsync(receipt: unknown, spec: AsyncSpec): StreamAsyncView | null;
26
41
  /**
27
42
  * Polls until `until` matches, the timeout expires, or the poll route stops responding.
28
43
  *
@@ -30,5 +45,5 @@ export declare function matchesPredicate(record: Record<string, unknown>, expres
30
45
  * behave the same way, and the interval is fixed rather than exponential — the point is to
31
46
  * observe when the state changes, not to be gentle with an API oat is already hammering.
32
47
  */
33
- export declare function driveAsync(client: Client, spec: AsyncSpec, receipt: unknown, scope: Record<string, string>, headers: Record<string, string>): Promise<AsyncOutcome>;
48
+ export declare function driveAsync(client: Client, spec: AsyncSpec, receipt: unknown, scope: Record<string, string>, headers: () => Record<string, string>, refreshIfStale?: (force?: boolean) => Promise<void>): Promise<AsyncOutcome>;
34
49
  //# sourceMappingURL=async.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../../src/runtime/async.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAGnD,MAAM,WAAW,YAAY;IAC5B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,2FAA2F;IAC3F,SAAS,EAAE,OAAO,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CA0B7F;AAcD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,OAAO,CAAC,YAAY,CAAC,CAsEvB"}
1
+ {"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../../src/runtime/async.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,KAAK,QAAQ,EAAa,MAAM,UAAU,CAAA;AAGnD,MAAM,WAAW,YAAY;IAC5B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,2FAA2F;IAC3F,SAAS,EAAE,OAAO,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CA0B7F;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAU7D;AAOD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAYxE;AAED,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,OAAO,CAAA;IACX,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,MAAM,EAAE,QAAQ,EAAE,CAAA;CAClB;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,eAAe,GAAG,IAAI,CAsB5F;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,cAAc,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GACjD,OAAO,CAAC,YAAY,CAAC,CA4EvB"}