test(mocks): gate the edit-to-approve cycle behind chained-request scenarios #516

Closed
opened 2026-11-07 21:02:03 +00:00 by hermes · 1 comment
Collaborator

The parity harness records one request per scenario and diffs the answer. That covers the read paths, where every response is a function of the estate at rest. It cannot cover the cycle the product exists for, because those answers depend on a request made a moment earlier:

  • editors/stagedata takes two inputs at once - a SASControlTable row carrying ACTION/MESSAGE/LIBDS, and jsdata, the edited grid rows as CSV - and answers sasparams[0].DSID, which the client navigates to. Recording it today stops in record.js with members is not iterable, before any request is posted: the recorder handles a single input table.
  • editors/postdata needs the request stagedata just created. Its answer, the audit rows it writes and the target it changes are all state from the previous step.
  • public/getapprovals and public/gethistory only mean anything off requests those steps produced.
  • The UI issues several requests per user action, so a single action is itself a short chain.

What a chain has to guarantee

A scenario should take an optional steps array in place of input/params:

{ "id": "cycle_class_update",
  "steps": [
    { "service": "editors/getdata",    "params": { "libds": "TESTDATA.CLASS" } },
    { "service": "editors/stagedata",  "input": [ ... ], "params": { ... } },
    { "service": "editors/postdata",   "input": [ ... ] },
    { "service": "public/getapprovals" },
    { "service": "public/gethistory" }
  ] }
  • One estate, steps in order. Each step sees what the previous one wrote; postdata reading a request stagedata created is the behaviour under test.
  • A fresh estate per run. The harness must roll back to the seeded state before the first step. Otherwise a second run stages onto a target the first run already changed and every diff is meaningless.
  • The golden is the sequence, not the final answer. Every step's response is projected and stored in order, so a difference is reported against the step that caused it - an abort at step 2 fails there rather than as a confusing diff at step 4.
  • Generated ids are masked and carried, not compared. DSID, request ids, approval ids and the audit LOAD_REF differ per estate and per run by construction. They need the same masking the staging ids already get, and the step that consumes one takes it from the previous step's response at replay time rather than from the golden.
  • Writes are recorded only on the synthetic estate (makedata plus createdemodata), so a chain that changes data never carries a customer's rows into a golden.

Work

  • record.js: post the steps to one estate in order and collect the responses.
  • check.js: replay the steps through runService against one mock estate.
  • harness.js: a rollback to the seeded state, so a writing chain is repeatable.
  • project.js: mask request/approval ids and LOAD_REF.
  • README: the chain rules above are already written up there; link them once implemented.

Then record stagedata, postdata, getapprovals and gethistory on the happy path. Permission aborts stay out of scope until users and groups are mocked.

The parity harness records one request per scenario and diffs the answer. That covers the read paths, where every response is a function of the estate at rest. It cannot cover the cycle the product exists for, because those answers depend on a request made a moment earlier: - `editors/stagedata` takes two inputs at once - a `SASControlTable` row carrying `ACTION`/`MESSAGE`/`LIBDS`, and `jsdata`, the edited grid rows as CSV - and answers `sasparams[0].DSID`, which the client navigates to. Recording it today stops in `record.js` with `members is not iterable`, before any request is posted: the recorder handles a single input table. - `editors/postdata` needs the request `stagedata` just created. Its answer, the audit rows it writes and the target it changes are all state from the previous step. - `public/getapprovals` and `public/gethistory` only mean anything off requests those steps produced. - The UI issues several requests per user action, so a single action is itself a short chain. ## What a chain has to guarantee A scenario should take an optional `steps` array in place of `input`/`params`: ```json { "id": "cycle_class_update", "steps": [ { "service": "editors/getdata", "params": { "libds": "TESTDATA.CLASS" } }, { "service": "editors/stagedata", "input": [ ... ], "params": { ... } }, { "service": "editors/postdata", "input": [ ... ] }, { "service": "public/getapprovals" }, { "service": "public/gethistory" } ] } ``` - **One estate, steps in order.** Each step sees what the previous one wrote; `postdata` reading a request `stagedata` created is the behaviour under test. - **A fresh estate per run.** The harness must roll back to the seeded state before the first step. Otherwise a second run stages onto a target the first run already changed and every diff is meaningless. - **The golden is the sequence, not the final answer.** Every step's response is projected and stored in order, so a difference is reported against the step that caused it - an abort at step 2 fails there rather than as a confusing diff at step 4. - **Generated ids are masked and carried, not compared.** `DSID`, request ids, approval ids and the audit `LOAD_REF` differ per estate and per run by construction. They need the same masking the staging ids already get, and the step that consumes one takes it from the previous step's response at replay time rather than from the golden. - **Writes are recorded only on the synthetic estate** (`makedata` plus `createdemodata`), so a chain that changes data never carries a customer's rows into a golden. ## Work - `record.js`: post the steps to one estate in order and collect the responses. - `check.js`: replay the steps through `runService` against one mock estate. - `harness.js`: a rollback to the seeded state, so a writing chain is repeatable. - `project.js`: mask request/approval ids and `LOAD_REF`. - README: the chain rules above are already written up there; link them once implemented. Then record `stagedata`, `postdata`, `getapprovals` and `gethistory` on the happy path. Permission aborts stay out of scope until users and groups are mocked.
Author
Collaborator

More detail, from the mock side and the client, so the chain can be built without re-deriving the contract.

The mock already models the stage step

sas/mocks/sasjs/services/editors/stagedata.js (191 lines) is written but has no golden, so nothing checks it. It reads:

  • SASControlTable[0] - ACTION (defaulting to LOAD), MESSAGE, LIBDS;
  • jsdata - the edited grid rows as CSV, parsed into headers and rows.

and it reproduces mpe_loader.sas closely enough that the chain will exercise real behaviour rather than a stub:

  • %mpe_runhook(POST_EDIT_HOOK) runs before %mpe_submit, and a hook may re-point the submission, so the staged CSV is named after the LIBDS as the request found it, not after whatever the hook left behind. A chain scenario that touches a table with a hook must therefore assert the staged name too.
  • MPE_TABLES is read twice: once for the hook lookup, once after, because MPE_SUBMIT records the staged width. A table present in MPE_TABLES with no data set is the first-load state; MPE_TABLES being absent entirely is a different failure.
  • the MPE_DERIVED droplist is dropped from the submitted data before staging.
  • the target's rows are read with libDataDir(libref), so a demo table is read from its own library while the staging directory stays in the control library.

What the client sends

editor.component.ts calls sasStoreService.updateTable(...) with updateParams (LIBDS, MESSAGE, and the approver fields, currently commented out), the edited rows sliced to licenceState.submit_rows_limit, the input table name SASControlTable, the route editors/stagedata, and $dataFormats. On success it navigates to /stage/<sasparams[0].DSID> - so DSID is the id the next step consumes.

licenceState.submit_rows_limit means a chain recorded with more rows than the licence allows stages a truncated submission; keep the staged row count inside the limit so the golden is about the cycle, not the licence.

Service names and hooks

Approvals are fetched from approvers/getapprovals, not public/getapprovals - that is the path the store already calls. sas.service.ts carries a list of services for which the backend runs %mpe_runhook (getdata, stagedata, loadfile, restore, postdata), so those are the steps where a hook can change the payload and where a chain has to be careful about ordering.

Consequences for the implementation

  • The rollback the harness needs is wider than the staging directory: MPE_REQUESTS, MPE_AUDIT and the target table are all changed by a chain. Rolling the whole mock estate back to its seeded state before step 1 is simpler and less error-prone than undoing individual writes.
  • DSID, the request id and the audit LOAD_REF are all generated per run; mask them in the golden and carry the value between steps at replay time.
  • Because the staged CSV is named from LIBDS, and submission can re-point it, the golden should include the staging directory listing rather than only the response bodies - otherwise the stage step passes while writing the wrong file.
More detail, from the mock side and the client, so the chain can be built without re-deriving the contract. ## The mock already models the stage step `sas/mocks/sasjs/services/editors/stagedata.js` (191 lines) is written but has no golden, so nothing checks it. It reads: - `SASControlTable[0]` - `ACTION` (defaulting to `LOAD`), `MESSAGE`, `LIBDS`; - `jsdata` - the edited grid rows as CSV, parsed into headers and rows. and it reproduces `mpe_loader.sas` closely enough that the chain will exercise real behaviour rather than a stub: - `%mpe_runhook(POST_EDIT_HOOK)` runs before `%mpe_submit`, and a hook may re-point the submission, so the staged CSV is named after the `LIBDS` **as the request found it**, not after whatever the hook left behind. A chain scenario that touches a table with a hook must therefore assert the staged name too. - `MPE_TABLES` is read twice: once for the hook lookup, once after, because `MPE_SUBMIT` records the staged width. A table present in `MPE_TABLES` with no data set is the first-load state; `MPE_TABLES` being absent entirely is a different failure. - the `MPE_DERIVED` droplist is dropped from the submitted data before staging. - the target's rows are read with `libDataDir(libref)`, so a demo table is read from its own library while the staging directory stays in the control library. ## What the client sends `editor.component.ts` calls `sasStoreService.updateTable(...)` with `updateParams` (`LIBDS`, `MESSAGE`, and the approver fields, currently commented out), the edited rows sliced to `licenceState.submit_rows_limit`, the input table name `SASControlTable`, the route `editors/stagedata`, and `$dataFormats`. On success it navigates to `/stage/<sasparams[0].DSID>` - so `DSID` is the id the next step consumes. `licenceState.submit_rows_limit` means a chain recorded with more rows than the licence allows stages a truncated submission; keep the staged row count inside the limit so the golden is about the cycle, not the licence. ## Service names and hooks Approvals are fetched from `approvers/getapprovals`, not `public/getapprovals` - that is the path the store already calls. `sas.service.ts` carries a list of services for which the backend runs `%mpe_runhook` (`getdata`, `stagedata`, `loadfile`, `restore`, `postdata`), so those are the steps where a hook can change the payload and where a chain has to be careful about ordering. ## Consequences for the implementation - The rollback the harness needs is wider than the staging directory: `MPE_REQUESTS`, `MPE_AUDIT` and the target table are all changed by a chain. Rolling the whole mock estate back to its seeded state before step 1 is simpler and less error-prone than undoing individual writes. - `DSID`, the request id and the audit `LOAD_REF` are all generated per run; mask them in the golden and carry the value between steps at replay time. - Because the staged CSV is named from `LIBDS`, and submission can re-point it, the golden should include the staging directory listing rather than only the response bodies - otherwise the stage step passes while writing the wrong file.
allan closed this issue 2026-11-08 16:20:49 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dc/dc#516