docs: link each server finding to its filed qqq issue
James approved filing the 22 findings; one issue per finding except the archive-table one (already open as #784), the two record-search ones (one root cause, #1343) and the presence one (qbit-collaboration#52). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf859a5bb8
commit
6c56b8955a
1 file changed
+25
-25
+25
-25
@@ -1,29 +1,29 @@
|
||||
# UI API server findings from the terminal clients
|
||||
|
||||
Observed while building the CLI and TUI against QQQ 4.1.0-SNAPSHOT (`develop` 6ac64fc66, UI API 1.8.0) with
|
||||
the sample app and MOCK authentication. Each is a candidate issue for `QRun-IO/qqq`; none is filed yet.
|
||||
the sample app and MOCK authentication. Filed on 2026-10-31 (James's OK); the `Issue` column links each one. Finding 10 was already open as #784, 9 and 21 share #1343, 18 is in qbit-collaboration.
|
||||
|
||||
| # | Finding | How to reproduce | Client workaround |
|
||||
|---|---|---|---|
|
||||
| 1 | A bearer session UUID cannot continue an asynchronous process step: the job fails with `Permission denied for process state`. The same UUID as the `sessionUUID` cookie works. | `POST /manageSession` (MOCK), then `init` and `step` `sleepInteractive` with `stepTimeoutMillis=0` using `Authorization: Bearer {uuid}`; poll status. | Session sign-ins are sent as a cookie (`sessionCookie` option of the client). |
|
||||
| 2 | Process `init` and `step` silently ignore a url-encoded `values` JSON field; only `multipart/form-data` is read. No 400. | `curl -d 'values={"sleepMillis":4000}'` runs with the default 1000 ms. | Clients always send multipart. A 415 or 400 would help. |
|
||||
| 3 | `GET /jobs` answers 404 although `/version` lists `durableJobs` in `features`. | Sample server, `qqq job list`. | The CLI explains; the TUI footer shows `jobs n/a`. Either drop the feature flag when no job store exists, or serve an empty list. |
|
||||
| 4 | `searchTablePossibleValues` with `ids: ["Narnia"]` on an INTEGER possible-value field returns 500 `QException (Error executing query)`. | `qqq api searchTablePossibleValues --path tableName=placeAddress --path fieldName=countryId --body-json '{"ids":["Narnia"]}'` | Clients only send ids that can parse for the field type. A 400 with `fieldErrors` would be right. |
|
||||
| 5 | `searchTablePossibleValues` with `searchTerm` matches case-sensitively (or by prefix) on this source: `can` does not find `Canada`. | Same table, `{"searchTerm":"can"}` on `countryId`. | Clients fetch the first page and match client-side when the search returns nothing. Document the matching rule in the spec. |
|
||||
| 6 | `GET /table/{t}/{pk}` returns `{ record: {...} }` while `query` returns `{ records: [...] }` with bare records; the design doc and the 0.1 CLI expected a bare record. | Any record. | Clients unwrap both. Worth a note in `UI-API-CLIENTS.md`. |
|
||||
| 7 | Process `init` on `clonePeople` without records answers `ERROR` with `No records were selected…` only after `init`; `getProcessMetaData` says `minInputRecords` 0. | `qqq process run clonePeople`. | Clients show the message. The metadata could carry the real minimum. |
|
||||
| 8 | `manageSession` has no documented username and password fields for `TABLE_BASED`, so a native client cannot sign in to such an app without the browser. | Spec `manageSession` request body. | `qqq login` refuses TABLE_BASED with a message. The design doc's nice-to-have "documented TABLE_BASED bearer recipe". |
|
||||
| 9 | `searchRecords` returned no results for `Avery` although `person` has search fields; unclear whether the sample app configures a search provider. | `qqq search Avery`. | None; needs a look at the sample's search setup. |
|
||||
| 10 | The sample app configures no uploaded-file archive table (`QJavalinMetaData.uploadedFileArchiveTableName`), so every bulk-insert `upload` step and `POST /uploads` fail with `UploadFileArchiveTableName was not specified in javalinMetaData`. Bulk load cannot be exercised against the sample. | `qqq api initProcess --path processName=person.bulkInsert --form 'values={"tableName":"person"}'` then step `upload` with a file. | None in the client. The sample needs an archive table (memory or filesystem backend) and the javalin setting; the Next UI acceptance fixtures may already have one. |
|
||||
| 11 | `CONTAINS` on the sample's memory backend matches case-sensitively: `lastName CONTAINS sample` finds nothing while `Sample` finds five people. The quick search (web UI and CLI) inherits it. | `qqq table query person -w "lastName ~ sample"` | None; the RDBMS backends are case-insensitive by collation. The memory backend could lower-case both sides, or the spec could say matching is backend-defined. |
|
||||
| 12 | `POST /table/{t}/export` answers 500 `INTERNAL_ERROR` for an unsupported `format` (`Unsupported report format: pdf.`) and for an unknown name in `fieldNames` (`Field name bogus was not found on the Person table.`). Both are request errors. | `qqq api exportTable --path tableName=person --body-json '{"format":"pdf"}'`; same with `"fieldNames":["bogus"]`. | `qqq table export` checks the format and the field names before sending (exit 2). A 400 with `fieldErrors` would be right. |
|
||||
| 13 | `POST /processes/{p}/{uuid}/cancel` answers `200 {}` for a run that declares no cancel step, and the run can still be stepped afterwards (its state is kept). A malformed UUID answers 500 `INTERNAL_ERROR` (`IllegalArgumentException`) instead of 400. | init `sleepInteractive`, cancel, then step `screen0`: it runs. `POST …/nonexistent-uuid/cancel`. | `qqq process run` treats 200 as cancelled and stops; nothing to do for the 500. A cancelled run should refuse further steps (409 or 404). |
|
||||
| 14 | The `isStepBack=true` query parameter of `stepProcess` (going back to `backStep`) is described in the `ProcessStepComplete.backStep` schema text but is not a declared parameter of the operation, so a spec-driven client cannot know it exists. | Spec: `stepProcess` lists only `tableVariant`. | The kernel sends `isStepBack=true` as the Next UI does. Declare the parameter. |
|
||||
| 15 | A step that fails on missing input answers `ERROR` with only `error` (`Error message: Unable to get Cities by state. Missing required input.`), no `userFacingError`, and the field is not marked required in the step metadata, so a client cannot tell an input problem from a crash or ask for the field first. | `qqq process run placeAddressPick --no-prompt`. | The CLI exits 6 (validation) when the message says required input is missing or the error is user-facing, 1 otherwise. Mark the fields required, or return `userFacingError` (or a 400 with `fieldErrors`). |
|
||||
| 16 | `downloadRecordFieldFile` on a BLOB field without adornments answers 500 `NullPointerException (Cannot invoke "java.util.List.stream()" because the return value of "QFieldMetaData.getAdornments()" is null)`. | `qqq table create fieldLab --set name=x --upload blobValue=./a.txt`, then `qqq api downloadRecordFieldFile --path tableName=fieldLab --path primaryKey=<id> --path fieldName=blobValue --path filename=a.txt -o out`. | `qqq table download` uses the route only for FILE_DOWNLOAD fields and decodes the record's base64 BLOB value otherwise. The route should treat no adornments as none (or answer 404/400). |
|
||||
| 17 | The sample app declares no field adornments other than LINK, no hidden fields or sections, no help content, no exposed joins, no composite or aliased associations and no table the mock user may not read, so CHIP, REVEAL, RENDER_HTML, CODE_EDITOR, ERROR, FILE_DOWNLOAD, WIDGET, TOOLTIP, help display, denied child reads and composite associations can only be checked with unit tests against the client. | `GET /metaData/table/{t}` for every sample table. | None needed in the client; a `fieldLab`-style adornment table in the sample (one field per adornment, a hidden section, help on a field, section and table, a composite association) would make these rows checkable live. |
|
||||
| 18 | qbit-collaboration 1.1.0: when the last viewer leaves, `presence.changed` (and the table snapshot) is `{"table":"person"}` with no `viewers`, although `CollaborationPresenceSnapshotV1.viewers` is a non-nullable array. A client that waits for `viewers` keeps showing the last person. Seen on the qbit's `CollaborationDevServer` (QQQ 4.1.0-RC.15). | Two sessions (`?user=bob` and alice); bob POSTs then DELETEs `/qbit/collaboration/presence` while alice is subscribed to `table:person`. | The kernel treats a payload with `table` as the snapshot and missing `viewers` as none. The qbit should send `"viewers": []`. |
|
||||
| 19 | The v1 insert, update and delete routes do not read a table variant: `TableInsertExecutor`, `TableUpdateExecutor` and `TableDeleteExecutor` never call `ExecutorSessionUtils.setTableVariantInSession`, and the spec gives them no `tableVariant` (query, count, get, export and the process routes have it). A client cannot write to a variant-backed table over v1. | Read `executors/Table{Insert,Update,Delete}Executor.java`; the legacy `QJavalinImplementation.setupSession` read a `tableVariant` form or query parameter for every route. | `qqq table create/update/delete/copy --variant` send `tableVariant` as a query parameter (as the get route takes it) and say the server may ignore it. Add the parameter to the three routes. |
|
||||
| 20 | Joins do not reach the export route: `exportTable` has no `joins` member and a joined name in `fieldNames` (`pet.name` on `person`, a join the query route accepts) answers 500 `Field name pet.name was not found on the Person table.` The web UI exports joined columns by name; unclear whether exposed joins work there. | `qqq api exportTable --path tableName=person --body-json '{"format":"csv","fieldNames":["id","pet.name"]}'` | `qqq table export` accepts joined columns of exposed joins only and checks them before sending. |
|
||||
| 21 | `GET /version` lists the `recordSearch` feature, but `POST /search` on the sample answers `{"results":[],"tables":[],"provider":{"status":"NONE"}}` for any text: the feature is advertised with no provider behind it, so a client cannot tell "nothing matched" from "this server cannot search" without reading `provider.status`. | `qqq api searchRecords --body-json '{"query":"avery"}'` | The TUI palette reads `provider.status` and says the server has no record search. Either list `recordSearch` only when a provider is configured, or document `provider.status` as the signal. |
|
||||
| 22 | The `multiStatistics` widget payload names its list `statisticList` (singular) inside `statisticsGroupData`, while other statistics payloads and the kernel's first reading used `statisticsList`; the OpenAPI document does not type the group, so a client can only guess. | `qqq widget get SampleMultiStatisticsWidget --output json` | The kernel render model accepts both names. Type `MultiStatisticsData` group items in the spec. |
|
||||
| # | Finding | How to reproduce | Client workaround | Issue |
|
||||
|---|---|---|---|---|
|
||||
| 1 | A bearer session UUID cannot continue an asynchronous process step: the job fails with `Permission denied for process state`. The same UUID as the `sessionUUID` cookie works. | `POST /manageSession` (MOCK), then `init` and `step` `sleepInteractive` with `stepTimeoutMillis=0` using `Authorization: Bearer {uuid}`; poll status. | Session sign-ins are sent as a cookie (`sessionCookie` option of the client). | [qqq#1335](https://github.com/QRun-IO/qqq/issues/1335) |
|
||||
| 2 | Process `init` and `step` silently ignore a url-encoded `values` JSON field; only `multipart/form-data` is read. No 400. | `curl -d 'values={"sleepMillis":4000}'` runs with the default 1000 ms. | Clients always send multipart. A 415 or 400 would help. | [qqq#1336](https://github.com/QRun-IO/qqq/issues/1336) |
|
||||
| 3 | `GET /jobs` answers 404 although `/version` lists `durableJobs` in `features`. | Sample server, `qqq job list`. | The CLI explains; the TUI footer shows `jobs n/a`. Either drop the feature flag when no job store exists, or serve an empty list. | [qqq#1337](https://github.com/QRun-IO/qqq/issues/1337) |
|
||||
| 4 | `searchTablePossibleValues` with `ids: ["Narnia"]` on an INTEGER possible-value field returns 500 `QException (Error executing query)`. | `qqq api searchTablePossibleValues --path tableName=placeAddress --path fieldName=countryId --body-json '{"ids":["Narnia"]}'` | Clients only send ids that can parse for the field type. A 400 with `fieldErrors` would be right. | [qqq#1338](https://github.com/QRun-IO/qqq/issues/1338) |
|
||||
| 5 | `searchTablePossibleValues` with `searchTerm` matches case-sensitively (or by prefix) on this source: `can` does not find `Canada`. | Same table, `{"searchTerm":"can"}` on `countryId`. | Clients fetch the first page and match client-side when the search returns nothing. Document the matching rule in the spec. | [qqq#1339](https://github.com/QRun-IO/qqq/issues/1339) |
|
||||
| 6 | `GET /table/{t}/{pk}` returns `{ record: {...} }` while `query` returns `{ records: [...] }` with bare records; the design doc and the 0.1 CLI expected a bare record. | Any record. | Clients unwrap both. Worth a note in `UI-API-CLIENTS.md`. | [qqq#1340](https://github.com/QRun-IO/qqq/issues/1340) |
|
||||
| 7 | Process `init` on `clonePeople` without records answers `ERROR` with `No records were selected…` only after `init`; `getProcessMetaData` says `minInputRecords` 0. | `qqq process run clonePeople`. | Clients show the message. The metadata could carry the real minimum. | [qqq#1341](https://github.com/QRun-IO/qqq/issues/1341) |
|
||||
| 8 | `manageSession` has no documented username and password fields for `TABLE_BASED`, so a native client cannot sign in to such an app without the browser. | Spec `manageSession` request body. | `qqq login` refuses TABLE_BASED with a message. The design doc's nice-to-have "documented TABLE_BASED bearer recipe". | [qqq#1342](https://github.com/QRun-IO/qqq/issues/1342) |
|
||||
| 9 | `searchRecords` returned no results for `Avery` although `person` has search fields; unclear whether the sample app configures a search provider. | `qqq search Avery`. | None; needs a look at the sample's search setup. | [qqq#1343](https://github.com/QRun-IO/qqq/issues/1343) |
|
||||
| 10 | The sample app configures no uploaded-file archive table (`QJavalinMetaData.uploadedFileArchiveTableName`), so every bulk-insert `upload` step and `POST /uploads` fail with `UploadFileArchiveTableName was not specified in javalinMetaData`. Bulk load cannot be exercised against the sample. | `qqq api initProcess --path processName=person.bulkInsert --form 'values={"tableName":"person"}'` then step `upload` with a file. | None in the client. The sample needs an archive table (memory or filesystem backend) and the javalin setting; the Next UI acceptance fixtures may already have one. | [qqq#784](https://github.com/QRun-IO/qqq/issues/784) |
|
||||
| 11 | `CONTAINS` on the sample's memory backend matches case-sensitively: `lastName CONTAINS sample` finds nothing while `Sample` finds five people. The quick search (web UI and CLI) inherits it. | `qqq table query person -w "lastName ~ sample"` | None; the RDBMS backends are case-insensitive by collation. The memory backend could lower-case both sides, or the spec could say matching is backend-defined. | [qqq#1344](https://github.com/QRun-IO/qqq/issues/1344) |
|
||||
| 12 | `POST /table/{t}/export` answers 500 `INTERNAL_ERROR` for an unsupported `format` (`Unsupported report format: pdf.`) and for an unknown name in `fieldNames` (`Field name bogus was not found on the Person table.`). Both are request errors. | `qqq api exportTable --path tableName=person --body-json '{"format":"pdf"}'`; same with `"fieldNames":["bogus"]`. | `qqq table export` checks the format and the field names before sending (exit 2). A 400 with `fieldErrors` would be right. | [qqq#1345](https://github.com/QRun-IO/qqq/issues/1345) |
|
||||
| 13 | `POST /processes/{p}/{uuid}/cancel` answers `200 {}` for a run that declares no cancel step, and the run can still be stepped afterwards (its state is kept). A malformed UUID answers 500 `INTERNAL_ERROR` (`IllegalArgumentException`) instead of 400. | init `sleepInteractive`, cancel, then step `screen0`: it runs. `POST …/nonexistent-uuid/cancel`. | `qqq process run` treats 200 as cancelled and stops; nothing to do for the 500. A cancelled run should refuse further steps (409 or 404). | [qqq#1346](https://github.com/QRun-IO/qqq/issues/1346) |
|
||||
| 14 | The `isStepBack=true` query parameter of `stepProcess` (going back to `backStep`) is described in the `ProcessStepComplete.backStep` schema text but is not a declared parameter of the operation, so a spec-driven client cannot know it exists. | Spec: `stepProcess` lists only `tableVariant`. | The kernel sends `isStepBack=true` as the Next UI does. Declare the parameter. | [qqq#1347](https://github.com/QRun-IO/qqq/issues/1347) |
|
||||
| 15 | A step that fails on missing input answers `ERROR` with only `error` (`Error message: Unable to get Cities by state. Missing required input.`), no `userFacingError`, and the field is not marked required in the step metadata, so a client cannot tell an input problem from a crash or ask for the field first. | `qqq process run placeAddressPick --no-prompt`. | The CLI exits 6 (validation) when the message says required input is missing or the error is user-facing, 1 otherwise. Mark the fields required, or return `userFacingError` (or a 400 with `fieldErrors`). | [qqq#1348](https://github.com/QRun-IO/qqq/issues/1348) |
|
||||
| 16 | `downloadRecordFieldFile` on a BLOB field without adornments answers 500 `NullPointerException (Cannot invoke "java.util.List.stream()" because the return value of "QFieldMetaData.getAdornments()" is null)`. | `qqq table create fieldLab --set name=x --upload blobValue=./a.txt`, then `qqq api downloadRecordFieldFile --path tableName=fieldLab --path primaryKey=<id> --path fieldName=blobValue --path filename=a.txt -o out`. | `qqq table download` uses the route only for FILE_DOWNLOAD fields and decodes the record's base64 BLOB value otherwise. The route should treat no adornments as none (or answer 404/400). | [qqq#1349](https://github.com/QRun-IO/qqq/issues/1349) |
|
||||
| 17 | The sample app declares no field adornments other than LINK, no hidden fields or sections, no help content, no exposed joins, no composite or aliased associations and no table the mock user may not read, so CHIP, REVEAL, RENDER_HTML, CODE_EDITOR, ERROR, FILE_DOWNLOAD, WIDGET, TOOLTIP, help display, denied child reads and composite associations can only be checked with unit tests against the client. | `GET /metaData/table/{t}` for every sample table. | None needed in the client; a `fieldLab`-style adornment table in the sample (one field per adornment, a hidden section, help on a field, section and table, a composite association) would make these rows checkable live. | [qqq#1350](https://github.com/QRun-IO/qqq/issues/1350) |
|
||||
| 18 | qbit-collaboration 1.1.0: when the last viewer leaves, `presence.changed` (and the table snapshot) is `{"table":"person"}` with no `viewers`, although `CollaborationPresenceSnapshotV1.viewers` is a non-nullable array. A client that waits for `viewers` keeps showing the last person. Seen on the qbit's `CollaborationDevServer` (QQQ 4.1.0-RC.15). | Two sessions (`?user=bob` and alice); bob POSTs then DELETEs `/qbit/collaboration/presence` while alice is subscribed to `table:person`. | The kernel treats a payload with `table` as the snapshot and missing `viewers` as none. The qbit should send `"viewers": []`. | [qbit-collaboration#52](https://github.com/QRun-IO/qbit-collaboration/issues/52) |
|
||||
| 19 | The v1 insert, update and delete routes do not read a table variant: `TableInsertExecutor`, `TableUpdateExecutor` and `TableDeleteExecutor` never call `ExecutorSessionUtils.setTableVariantInSession`, and the spec gives them no `tableVariant` (query, count, get, export and the process routes have it). A client cannot write to a variant-backed table over v1. | Read `executors/Table{Insert,Update,Delete}Executor.java`; the legacy `QJavalinImplementation.setupSession` read a `tableVariant` form or query parameter for every route. | `qqq table create/update/delete/copy --variant` send `tableVariant` as a query parameter (as the get route takes it) and say the server may ignore it. Add the parameter to the three routes. | [qqq#1351](https://github.com/QRun-IO/qqq/issues/1351) |
|
||||
| 20 | Joins do not reach the export route: `exportTable` has no `joins` member and a joined name in `fieldNames` (`pet.name` on `person`, a join the query route accepts) answers 500 `Field name pet.name was not found on the Person table.` The web UI exports joined columns by name; unclear whether exposed joins work there. | `qqq api exportTable --path tableName=person --body-json '{"format":"csv","fieldNames":["id","pet.name"]}'` | `qqq table export` accepts joined columns of exposed joins only and checks them before sending. | [qqq#1352](https://github.com/QRun-IO/qqq/issues/1352) |
|
||||
| 21 | `GET /version` lists the `recordSearch` feature, but `POST /search` on the sample answers `{"results":[],"tables":[],"provider":{"status":"NONE"}}` for any text: the feature is advertised with no provider behind it, so a client cannot tell "nothing matched" from "this server cannot search" without reading `provider.status`. | `qqq api searchRecords --body-json '{"query":"avery"}'` | The TUI palette reads `provider.status` and says the server has no record search. Either list `recordSearch` only when a provider is configured, or document `provider.status` as the signal. | [qqq#1343](https://github.com/QRun-IO/qqq/issues/1343) |
|
||||
| 22 | The `multiStatistics` widget payload names its list `statisticList` (singular) inside `statisticsGroupData`, while other statistics payloads and the kernel's first reading used `statisticsList`; the OpenAPI document does not type the group, so a client can only guess. | `qqq widget get SampleMultiStatisticsWidget --output json` | The kernel render model accepts both names. Type `MultiStatisticsData` group items in the spec. | [qqq#1353](https://github.com/QRun-IO/qqq/issues/1353) |
|
||||
Reference in new issue
Block a user