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:
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.
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 LIBDSas 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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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/stagedatatakes two inputs at once - aSASControlTablerow carryingACTION/MESSAGE/LIBDS, andjsdata, the edited grid rows as CSV - and answerssasparams[0].DSID, which the client navigates to. Recording it today stops inrecord.jswithmembers is not iterable, before any request is posted: the recorder handles a single input table.editors/postdataneeds the requeststagedatajust created. Its answer, the audit rows it writes and the target it changes are all state from the previous step.public/getapprovalsandpublic/gethistoryonly mean anything off requests those steps produced.What a chain has to guarantee
A scenario should take an optional
stepsarray in place ofinput/params:postdatareading a requeststagedatacreated is the behaviour under test.DSID, request ids, approval ids and the auditLOAD_REFdiffer 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.makedatapluscreatedemodata), 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 throughrunServiceagainst one mock estate.harness.js: a rollback to the seeded state, so a writing chain is repeatable.project.js: mask request/approval ids andLOAD_REF.Then record
stagedata,postdata,getapprovalsandgethistoryon the happy path. Permission aborts stay out of scope until users and groups are mocked.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 toLOAD),MESSAGE,LIBDS;jsdata- the edited grid rows as CSV, parsed into headers and rows.and it reproduces
mpe_loader.sasclosely 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 theLIBDSas 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_TABLESis read twice: once for the hook lookup, once after, becauseMPE_SUBMITrecords the staged width. A table present inMPE_TABLESwith no data set is the first-load state;MPE_TABLESbeing absent entirely is a different failure.MPE_DERIVEDdroplist is dropped from the submitted data before staging.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.tscallssasStoreService.updateTable(...)withupdateParams(LIBDS,MESSAGE, and the approver fields, currently commented out), the edited rows sliced tolicenceState.submit_rows_limit, the input table nameSASControlTable, the routeeditors/stagedata, and$dataFormats. On success it navigates to/stage/<sasparams[0].DSID>- soDSIDis the id the next step consumes.licenceState.submit_rows_limitmeans 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, notpublic/getapprovals- that is the path the store already calls.sas.service.tscarries 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
MPE_REQUESTS,MPE_AUDITand 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 auditLOAD_REFare all generated per run; mask them in the golden and carry the value between steps at replay time.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.