Skip to content

Commit 5bb9a83

Browse files
fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentRecord, the caller's own read of the row (#23531) (#23667)
Fixes #23531 Clause-②: no This lands the remainder of #23531: the `currentRecord` half that PR #23582 held back. The hold waited on #23586, whose answer landed as PR #23628 (`7dc7d2213`): the protocol now reads that row as the caller. ## What this does A stale `If-Match` (or `expectedVersion`) on `PATCH` or `DELETE /data/:object/:id` is refused by the protocol with `ConcurrentUpdateError` (`409 CONCURRENT_UPDATE`). Both Hono doors render it through `declaredEnvelopeForThrow`. They are `@objectstack/hono`'s `createHonoApp` and a route mounted on `HonoHttpServer`. That function reads the structured fields off `RestServer`'s classification (`structuredCodeAnswer`) in `declaredArmContext`. That helper dropped `currentRecord` by name, because the record used to be the row the existence probe (`probeRecord`) reads with a system context. Since PR #23628, on a version mismatch `metadata-protocol` re-reads the row under the caller's own context. A row the caller's row-level rules hide answers `404 RECORD_NOT_FOUND` with neither field. A readable row's `currentRecord` is the caller's field-masked read. So the hold is lifted: - `declaredArmContext` (`packages/plugins/plugin-hono-server/src/adapter.ts`) now drops only the arm's own `error` and `code`. `currentRecord` rides `error.details` beside `currentVersion`, as `RestServer` forwards it flat. - The docblock lines that said the field is held are rewritten. The field is forwarded because the producer reads it as the caller. An arm that grows a field read with a system context must still be held there by name until its producer reads it as the caller. - FROM `{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…"}}}` - TO `{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…","currentRecord":{…}}}}` No other arm's behaviour changes. `metadata-protocol`, `packages/types` and `packages/adapters/hono/src/index.ts` are not edited. Changeset: `@objectstack/plugin-hono-server` and `@objectstack/hono`, `patch`, as PR #23582 did (one fixed version group, and `createHonoApp`'s answer is the measured door). ## Pins - **`packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts`** (direct-mount route): the `[#23531]` rows flip from absence to presence. `error.details` must equal `{ currentVersion, currentRecord }` exactly, as the producer stated them, so a field the door drops or invents turns them red. The controls are kept: the stored version is accepted, and a conflict stating no current state keeps the two-key envelope. - **`packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts`** (a real `createHonoApp` over the real dispatcher and protocol, on SQLite WASM): - `PATCH` and `DELETE` with a stale `If-Match`: `error.details` equals the `currentVersion` and `currentRecord` the real protocol threw, read off the protocol call, and the record names the stored row. The `currentVersion` echo pins are kept: echoed as the next `If-Match` it is accepted (`PATCH` writes, `DELETE` deletes). - New, `PATCH` and `DELETE`: a row whose `secret` field the caller's read masks. `currentRecord` carries the row's `id` and `name` and no `secret`, and no secret text is on the wire. The record equals both the caller's own read and the record the protocol threw, while the store still holds the field. - The mask is an engine middleware that rewrites a caller-context read after `next()`. That is the seam `@objectstack/plugin-security`'s field-level rules rewrite a read at. The security plugin itself is not booted there, because `http-conformance` does not depend on it, and adding it means `package.json` and the lockfile, outside this claim. The real field- and row-level rules are pinned by `packages/runtime/src/domains/data-conflict-caller-visibility.test.ts` (PR #23628), and are measured on this head below. ## Verification (at `4bc9b0c08b`) The branch merged `origin/main` at `286eb9ef3` (4 commits, none touching these files) before the PR opened. Every reading below is on `4bc9b0c08b` unless it names another head. - **Packages**, after a full `turbo run build` of the workspace (75/75): - `pnpm --filter @objectstack/plugin-hono-server typecheck` (tsc, `tsconfig.typecheck.json`, `check:test-typecheck` OK with 0 debt) and `test`: Test Files 29 passed, Tests 354 passed. - `pnpm --filter @objectstack/http-conformance typecheck` (`check:test-typecheck` OK, ledger unchanged) and `vitest run`: Test Files 13 passed, Tests 144 passed. - Consumer pins in `packages/runtime`, which resolves `@objectstack/plugin-hono-server` through `dist/`: `data-conflict-caller-visibility`, `data-update-expected-version` and `data-delete-expected-version` gave 3 files, 115 passed. - **Ablation** (at `7a8e9bf386`): `node scripts/ablation-replace.mjs` in wrap mode, plus an outer `trap` restore on EXIT INT TERM over the absolute path. The exclusion was put back: anchor x1 to x0, replacement carrying the marker `ABLATION_23531_RESTORE_HOLD` x0 to x1, blob `4b7ca0f62478` to `3e3313e4060c`. `plugin-hono-server` was rebuilt, and `ablation-dist-preflight` found the marker in `dist/index.js` and `dist/index.mjs`. The direction was as predicted: - unit file: 2 failed, 35 passed. PATCH and DELETE gave `expected { Object (currentVersion) } to deeply equal { …(2) }`; - conformance file: 4 failed, 16 passed. These are the two presence rows and both masked-field rows (`expected undefined to match object`). - Restore: the blob equals HEAD and `git diff HEAD` is empty. The rebuild was 6/6 turbo cache hits. Preflight `--absent` found the marker gone from all 6 built files, with a clean tree. Both files were green again (37/37, 20/20). - **Door parity with the real security layer** (at `7a8e9bf386`, a scratch test in `packages/runtime`, deleted after the run and never committed): the same stale `PATCH` and `DELETE` ran on the FLS-masked row and on the plain row. The Hono exit's `error.details` equals `RestServer`'s `{ currentVersion, currentRecord }`, and neither wire carries the masked value: 4/4. The committed `data-conflict-caller-visibility.test.ts` was green beside it (16/16). - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (7 paths vs merge base `286eb9ef3`) derived 95 commands. All 95 ran, each exit captured before any pipe: 95 x exit 0. `--ran` reports 95 derived, 95 run, 0 NOT-MEASURED and 0 UNRUN. - Pass 1 at `7a8e9bf386` caught `check:query-options-erasure`: two `as any` on `findOne` options in the new pin took the test surface from 236 to 238. Fixed in `bc83077a4f`, back to 236. - **Lint**, narrowed: `eslint --no-inline-config` on the 3 changed TS sources exited 0, with 0 errors and 0 warnings each. - Each file was really linted: 6/5/5 active rules by `--print-config`. - `eslint.config.mjs` is unchanged from base to `origin/main`. - The 3 `.mdx` pages fall outside ESLint's config (`File ignored`), as they do under `pnpm lint`. - The full `pnpm lint` is CI's (`Lint · ESLint`). ## Acceptance notes - **Docs corrected, outside the claim's listed file surface.** Three pages said a Hono door never sends `currentRecord`. PR #23616 wrote them, and this change makes them false: `content/docs/api/index.mdx`, `content/docs/api/wire-format.mdx` ("Hono hosts nest the extras", whose example now shows `currentRecord`), and `content/docs/protocol/kernel/error-handling.mdx` (the Hono-door table and the "never sends" line). They are corrected here, with one line each saying the record is the row as the caller reads it. - **Two unreleased changesets still say `currentRecord` stays held:** `.changeset/23531-hono-conflict-body.md` and `.changeset/23587-hono-structured-arm-fields.md`. They are not edited here (outside this claim). This PR's changeset says it supersedes that note. If one CHANGELOG story is preferred, those two sentences can be cut before the next release. Carrier: the seat. - **The runtime pin's dispatcher-door rows** read `currentRecord` off the thrown error, not the wire, because the wire did not carry it before this change. On this head the wire carries it, and it equals `RestServer`'s, measured with a scratch test that was not committed (below). Tightening those rows to the wire is a possible later edit in `packages/runtime`. Noted, not filed. Carrier: none. - **Producer-side ablation not run.** That would mean reverting PR #23628's read and watching the masked-field rows go red. This diff does not change the producer, so it is not owed here. The rows' `not.toHaveProperty('secret')` and equality with the caller's read are what would catch it. --- _Generated by [Claude Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cad343e commit 5bb9a83

7 files changed

Lines changed: 178 additions & 67 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-hono-server': patch
3+
'@objectstack/hono': patch
4+
---
5+
6+
fix(plugin-hono-server): a `409 CONCURRENT_UPDATE` answered by either Hono door carries `currentRecord` as well, the caller's own read of the row, as the standalone REST server's does (#23531)
7+
8+
The two Hono doors are `@objectstack/hono`'s `createHonoApp` and a route mounted on `@objectstack/plugin-hono-server`'s `HonoHttpServer`. Their answer to a stale `If-Match` (or `expectedVersion`) on `PATCH` or `DELETE /data/:object/:id` carried the server's `currentVersion` in `error.details`, and held `currentRecord` back while the protocol read that row with a system context. On a version mismatch the protocol now re-reads the row under the caller's own context (#23586), so the record holds only what the caller may read, and these doors forward it.
9+
10+
- FROM `{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…"}}}`
11+
- TO `{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…","currentRecord":{…}}}}`
12+
13+
`currentRecord` is the row as the caller reads it. A field the caller's field-level rules hide is absent from it, and a row the caller's row-level rules hide answers `404 RECORD_NOT_FOUND` with neither field, as the REST server answers. It is read from the classification the REST server applies (`structuredCodeAnswer` in `@objectstack/types`), so it appears only when the conflict stated it, and `@objectstack/client` reads it as `err.details.currentRecord` on these hosts. Status, `code`, `message` and `currentVersion` are unchanged, and no other answer changes.
14+
15+
This supersedes the earlier note that `currentRecord` stays held on these two doors. Nothing changes in `@objectstack/hono`'s own code: `createHonoApp` renders through `declaredEnvelopeForThrow` from `@objectstack/plugin-hono-server`.
16+
17+
Clause-②: no

‎content/docs/api/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ Validation failures additionally include a `fields` array (one entry per invalid
218218
}
219219
```
220220

221-
On a host built with `@objectstack/hono`'s `createHonoApp`, a declared refusal thrown out of the dispatcher (the `/data` routes' refusals among them, where its catch-all serves `/data`; a host that registers the kernel REST server's routes ahead of the catch-all, the hosted composition, answers those flat, as above) is answered in this envelope **without** `httpStatus`, and so is one escaping a route mounted on `HonoHttpServer`. The fields the kernel REST server's classification adds beside `code` ride in `details` there, for example `DELETE_RESTRICTED`'s `dependentObject`, except `currentRecord` and an `object` taken from the request's route — see [Wire Format](/docs/api/wire-format#hono-hosts-nest-the-extras).
221+
On a host built with `@objectstack/hono`'s `createHonoApp`, a declared refusal thrown out of the dispatcher (the `/data` routes' refusals among them, where its catch-all serves `/data`; a host that registers the kernel REST server's routes ahead of the catch-all, the hosted composition, answers those flat, as above) is answered in this envelope **without** `httpStatus`, and so is one escaping a route mounted on `HonoHttpServer`. The fields the kernel REST server's classification adds beside `code` ride in `details` there, for example `DELETE_RESTRICTED`'s `dependentObject`, except an `object` taken from the request's route — see [Wire Format](/docs/api/wire-format#hono-hosts-nest-the-extras).
222222

223223
Branch on `error.code`, never on `error.httpStatus` — the status answers "what
224224
class of failure" and the code answers "which failure". A branch that has no

‎content/docs/api/wire-format.mdx‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,17 +487,26 @@ release group); any code reading them directly should move to `error.code`.
487487
A host built with `@objectstack/hono`'s `createHonoApp`, and a route mounted on
488488
`@objectstack/plugin-hono-server`'s `HonoHttpServer`, answer a declared refusal that
489489
escapes the handler in the nested envelope above, without `httpStatus`. The extras the
490-
REST door's classification adds (`fields`, `currentVersion`, `dependentObject`, …) ride in
491-
`error.details`; `currentRecord`, and an `object` the flat body takes from the request's
492-
route, are not sent. The concurrent update above, on a `createHonoApp` host whose catch-all serves `/data`:
490+
REST door's classification adds (`fields`, `currentVersion`, `currentRecord`, `dependentObject`, …)
491+
ride in `error.details`; an `object` the flat body takes from the request's route is not
492+
sent. `currentRecord` is the row as the caller reads it, on either door: a field the caller's
493+
field-level rules hide is absent from it. The concurrent update above, on a `createHonoApp`
494+
host whose catch-all serves `/data`:
493495

494496
```json
495497
{
496498
"success": false,
497499
"error": {
498500
"code": "CONCURRENT_UPDATE",
499501
"message": "Record task/tsk_01HQ4A7B9D3F5G8J2K4L was modified by another user (current version 2025-01-20T14:15:00.000Z, expected 2025-01-20T14:10:00.000Z)",
500-
"details": { "currentVersion": "2025-01-20T14:15:00.000Z" }
502+
"details": {
503+
"currentVersion": "2025-01-20T14:15:00.000Z",
504+
"currentRecord": {
505+
"id": "tsk_01HQ4A7B9D3F5G8J2K4L",
506+
"title": "Implement login page",
507+
"updated_at": "2025-01-20T14:15:00.000Z"
508+
}
509+
}
501510
}
502511
}
503512
```

‎content/docs/protocol/kernel/error-handling.mdx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,13 @@ serves `/data`:
247247
| Code | In `error.details` on a Hono door |
248248
|---|---|
249249
| `DELETE_RESTRICTED` | `developerMessage`, `dependentObject`, and `dependentCount` when the flat door would send it |
250-
| `CONCURRENT_UPDATE` | `currentVersion` |
250+
| `CONCURRENT_UPDATE` | `currentVersion`, `currentRecord` (the row as the caller reads it) |
251251
| `INVALID_FIELD` | `field`, `object` |
252252
| `OBJECT_NOT_FOUND` | `object` |
253253
| `VALIDATION_FAILED` | `fields` |
254254
| `FEEDS_DISABLED`, `FILES_DISABLED`, `ATTACHMENT_PARENT_ACCESS`, `ATTACHMENT_DELETE_DENIED`, `RECORD_NOT_ACCESSIBLE` | `object` |
255255
256-
⛔ What a Hono door never sends: `httpStatus`; `CONCURRENT_UPDATE`'s `currentRecord`,
257-
which the flat door sends; and an `object` the flat door takes from the request's route
256+
⛔ What a Hono door never sends: `httpStatus`, and an `object` the flat door takes from the request's route
258257
(on `DELETE_RESTRICTED`, `CONCURRENT_UPDATE` and `VALIDATION_FAILED`). The `message` is the
259258
thrown one and can differ from the flat door's `error`: an unregistered object reads
260259
`Object 'ghost' not found` here and `Object 'ghost' is not registered` there.

‎packages/plugins/plugin-hono-server/src/adapter.ts‎

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,13 @@ function toLoggableError(thrown: unknown): Error {
296296
* ## A structured refusal's context (#23531, #23587)
297297
*
298298
* A refusal `RestServer`'s classification answers with structured fields
299-
* (`409 CONCURRENT_UPDATE`'s `currentVersion`, `409 DELETE_RESTRICTED`'s
300-
* `developerMessage` / `dependentObject` / `dependentCount`, …) also carries
301-
* them here, in `details`, read off that classification by
302-
* {@link declaredArmContext}. Before this, both Hono doors answered those
303-
* refusals with `code` and `message` only. `currentRecord` is held back for
304-
* now (#23586); that helper's docblock says why.
299+
* (`409 CONCURRENT_UPDATE`'s `currentVersion` and `currentRecord`, `409
300+
* DELETE_RESTRICTED`'s `developerMessage` / `dependentObject` /
301+
* `dependentCount`, …) also carries them here, in `details`, read off that
302+
* classification by {@link declaredArmContext}. Before this, both Hono doors
303+
* answered those refusals with `code` and `message` only. `currentRecord` is
304+
* forwarded because the producer reads it under the caller's own context
305+
* (#23586, PR #23628); that helper's docblock says why that matters.
305306
*/
306307
export function declaredEnvelopeForThrow(thrown: unknown): {
307308
status: number;
@@ -378,9 +379,8 @@ export function declaredEnvelopeForThrow(thrown: unknown): {
378379
* under the same rule (each field only when the producer stated it), and an
379380
* arm that grows a field grows it at every door. ⛔ Never list the fields or
380381
* the codes here. The arm's own sentence and code are dropped because this
381-
* envelope already carries both, as `message` and `code`. `currentRecord` is
382-
* the ONE field named here, and it is named only to hold it back (the ⚠️
383-
* paragraph below says why).
382+
* envelope already carries both, as `message` and `code`. No field is held
383+
* back by name (the ⚠️ paragraph below says what that rests on).
384384
*
385385
* ## The arm must agree with the envelope
386386
*
@@ -412,18 +412,19 @@ export function declaredEnvelopeForThrow(thrown: unknown): {
412412
* `object` (`INVALID_FIELD`, `OBJECT_NOT_FOUND`, the 403 access arms) carry it
413413
* here as they do there.
414414
*
415-
* ⚠️ `currentRecord` is HELD here: these doors do not forward it, though the
416-
* REST door does. The protocol reads that row with a system context
417-
* (`probeRecord`), and the OCC check throws it before the caller's write
418-
* policy runs, so it is not narrowed to what the caller may read. It may carry
419-
* a row, or fields, that row- or field-level security hides from the caller.
420-
* Forwarding it would widen that possible disclosure to two more doors. The
421-
* record waits for the producer's answer (#23586), and the exclusion is lifted
422-
* when that lands. Every other field an arm carries today is a version token,
415+
* ⚠️ Row data rides here only because its producer reads it as the caller.
416+
* `CONCURRENT_UPDATE`'s `currentRecord` is a row: on a version mismatch the
417+
* protocol re-reads it under the caller's own `context`, so every row- and
418+
* field-level rule of the read door applies (#23586, PR #23628). A row the
419+
* caller cannot read answers `404 RECORD_NOT_FOUND` with neither field, and a
420+
* readable row's `currentRecord` is that field-masked read, which these doors
421+
* forward as the REST door does. It was held here until that landed, while
422+
* the record was the row the existence probe (`probeRecord`) reads with a
423+
* system context. Every other field an arm carries today is a version token,
423424
* an API name, the caller's own input, or a count the producer has already
424425
* narrowed to what the caller may read (`dependentCountIsDisclosable` in
425-
* `@objectstack/objectql`); none is row data read with a system context. An
426-
* arm that grows such a field must be held here by name, as this one is.
426+
* `@objectstack/objectql`). An arm that grows a field read with a system
427+
* context must be held here by name until its producer reads it as the caller.
427428
*/
428429
function declaredArmContext(
429430
thrown: unknown,
@@ -434,10 +435,7 @@ function declaredArmContext(
434435
return undefined;
435436
}
436437
const context = Object.fromEntries(
437-
Object.entries(arm.body).filter(([key]) => key !== 'error'
438-
&& key !== 'code'
439-
// Held: a system-context row the caller may not be allowed to read (#23586).
440-
&& key !== 'currentRecord'),
438+
Object.entries(arm.body).filter(([key]) => key !== 'error' && key !== 'code'),
441439
);
442440
return Object.keys(context).length > 0 ? context : undefined;
443441
}

‎packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -216,27 +216,28 @@ describe('an escaped throw carrying a declared ADR-0112 envelope is rendered as
216216

217217
/**
218218
* [#23531] A stale `If-Match` on a direct-mount route: the `409
219-
* CONCURRENT_UPDATE` carries the server's `currentVersion`, which `RestServer`'s
220-
* `409` carries and objectui's conflict bar reads.
219+
* CONCURRENT_UPDATE` carries the server's `currentVersion` and `currentRecord`,
220+
* which `RestServer`'s `409` carries and objectui's conflict bar reads.
221221
*
222-
* `currentRecord` is asserted ABSENT. `RestServer` forwards it, but the
223-
* protocol reads that row with a system context before the caller's write
224-
* policy runs, so it is held at these doors until the producer answers #23586.
225-
* The producer below states it, so the absence is the door's doing.
222+
* `currentRecord` is forwarded as the producer threw it, field for field: the
223+
* row below stands for the caller's own read, which is what `metadata-protocol`
224+
* puts there since #23586 (PR #23628). `error.details` is asserted EXACTLY, so
225+
* a field the door drops or invents turns the row red.
226226
*
227227
* The producer below is shaped exactly like `metadata-protocol`'s
228228
* `ConcurrentUpdateError` (`name`, `code`, `status`, and the two own
229229
* properties), because this package does not depend on that one. The routes
230230
* judge the `If-Match` header against a stored version, as `updateData` /
231231
* `deleteData` do. The composed-host twin, over the REAL protocol, is
232-
* `hono-declared-envelope.conformance.test.ts` in `packages/qa/http-conformance`.
232+
* `hono-declared-envelope.conformance.test.ts` in `packages/qa/http-conformance`;
233+
* it also shows a field the caller's read masks staying off `currentRecord`.
233234
*
234-
* On the wrapped envelope the field rides `error.details`: that is the
235+
* On the wrapped envelope the fields ride `error.details`: that is the
235236
* envelope's declared context slot (`ApiErrorSchema.details`), and it is where
236237
* `@objectstack/client` reads a wrapped body's context from
237238
* (`errorBody.error.details`).
238239
*/
239-
describe('[#23531] a stale If-Match on a direct-mount route: the 409 carries currentVersion, and holds currentRecord (#23586)', () => {
240+
describe('[#23531] a stale If-Match on a direct-mount route: the 409 carries currentVersion and currentRecord', () => {
240241
class ConcurrentUpdateError extends Error {
241242
readonly code = 'CONCURRENT_UPDATE';
242243
readonly status = 409;
@@ -274,7 +275,7 @@ describe('[#23531] a stale If-Match on a direct-mount route: the 409 carries cur
274275
return s;
275276
}
276277

277-
it.each(['PATCH', 'DELETE'])('%s: 409 CONCURRENT_UPDATE with currentVersion in `error.details`, and no currentRecord', async (method) => {
278+
it.each(['PATCH', 'DELETE'])('%s: 409 CONCURRENT_UPDATE with currentVersion and currentRecord in `error.details`, as the producer stated them', async (method) => {
278279
const res = await call(occServer(), '/api/v1/data/task/task_1', {
279280
method,
280281
headers: { 'If-Match': `"${STALE_VERSION}"`, ...(method === 'PATCH' ? { 'content-type': 'application/json' } : {}) },
@@ -285,10 +286,8 @@ describe('[#23531] a stale If-Match on a direct-mount route: the 409 carries cur
285286
expect(res.status, JSON.stringify(body)).toBe(409);
286287
expect(body.success).toBe(false);
287288
expect(body.error.code).toBe('CONCURRENT_UPDATE');
288-
expect(body.error.details).toEqual({ currentVersion: STORED_VERSION });
289-
// Held until #23586: the producer stated it, and the door does not forward it.
290-
expect(body.error.details).not.toHaveProperty('currentRecord');
291-
expect(JSON.stringify(body)).not.toContain('original');
289+
// Exactly the two fields the producer stated: nothing dropped, nothing added.
290+
expect(body.error.details).toEqual({ currentVersion: STORED_VERSION, currentRecord: STORED });
292291
});
293292

294293
it('control: the version the route stores is accepted, so the 409 rows are the guard and not the rig', async () => {

0 commit comments

Comments
 (0)