Every one of the spec's 52 cy.wait() calls is gone: they ranged from 1s to
20s, 350 seconds of declared sleeping in a spec that runs in 87. Each stood in
for something the app does asynchronously, and each has been replaced by the
state it was waiting for:
- a key change leaves the app re-bootstrapping its licence state, so
waitForAppSettled() waits for .app-loading to clear and for the app to land
on a known screen (nav tree, demo banner, key error or login form);
- the paste form exists only on licensing/update - asking whether the app is
anywhere under licensing/ also matches the error page a bad key produces,
which has no paste button, so gotoLicensingUpdate() navigates unless that is
where it already is;
- a rejected key redirects to the licensing page, so verifyLicensingPage()
waits for that URL instead of sleeping and then checking once, and fails
there rather than hanging when the redirect never comes;
- the log-out, approve and paste-button paths already retry, so the sleeps in
front of them were pure padding.
Measured on the local stack against the same estate: 10 passing in 45s twice,
against 1:27 before (and 1:33 in CI).
The fixture's schema entry lived in makedata's schema object (as a second,
later key in the same literal) and did not survive the move to createdemodata.
saveTable falls back to the file already on the drive, so a reseed looked
fine - but CI deletes the appLoc before seeding, and a fresh estate wrote
mpe_x_search.json with columns: [], losing the column order the
full-table-search spec derives its counts from.
Carry the nine columns (name/type/length/format/label/notnull) in
createdemodata's schema, and add a case that every fixture table is written
with column metadata covering its rows, plus the MPE_X_SEARCH order. It fails
against the previous service.
The panel heading reads 'Configuration (#2 DC_JSLIB.MPE_ALERTS)', so the check
parses lib.dataset out of it and compares it whole. A substring test accepted
another library's table of the same name - this estate seeds MPE_X_TEST under
two libraries - and any name that merely contains the target, which is the
mix-up the check exists to catch.
Verified on the local stack: 12 passing in 2:24 and 2:22.
createdemodata assigned tables.MPE_DERIVED outright, so any row makedata wrote
there would be wiped by a fixture run - and the idempotency test did not count
the table, so nothing would have said so. Filter the TESTDATA rows out and
append, in the same shape as the tables above, and count MPE_DERIVED in the
idempotency test.
New case: a non-TESTDATA derived row survives a createdemodata run exactly
once. It fails against the previous service and passes now.
The table link is looked up inside the matched library node rather than across
the whole picker, so a table of the same name under another expanded library
cannot be clicked instead, and the library fragment is escaped before it
becomes a RegExp - a dot or a bracket in a library name no longer matches more
than the intended node.
The configuration panel is checked after the ~600-800ms window in which
loadData() re-selects the viewbox it just loaded, rather than 400ms into it,
so a moved panel is caught at the click instead of in the column list that
follows.
Verified on the local stack: 12 passing in 2:24 and 2:22.
- createdemodata drops the unused servicesDir binding.
- the DC996664 copy no longer swallows a failure: a broken multi-load fixture
now fails the seed where it happened instead of surfacing in a Cypress spec.
- makedata answers OUTFMT=JSON with a makedata_response member (as the SAS
service does) and the CLI payload asks for it, so the `|| true` comes off
makedata in build.yaml, release.yaml and sites.yaml - a failed seed names the
service that failed instead of reappearing as "createdemodata needs the
control library".
- the makedata comment points at sas/mocks/test/makedata-parity.test.js, not a
macro that does not exist.
makedata seeded two things at once: the control library, and the tables the
Cypress suite needs. The estate it produced was therefore not the estate a
SAS deployment produces - and it had drifted: MPE_MAXKEYVALUES and
MPE_SIGNOFFS were never created, and MPE_CONFIG, MPE_VALIDATIONS, MPE_TABLES,
MPE_SELECTBOX and MPE_XLMAP_RULES were missing seed rows that mpe_makedata.sas
inserts.
makedata now transcribes mpe_makedatamodel.sas and mpe_makedata.sas: every
model table with its columns, and every seed row the macro writes. The
Cypress fixtures move to services/admin/createdemodata.js, which the test
estate runs after makedata; it strips and re-adds its rows so a reseed does
not accumulate.
Checked against real SAS output on the shared estate (fresh deploy of the
server target, makedata, then getrawdata for each control table and a
dictionary.columns probe): all 40 tables and 339 columns match on name, type,
length, format and label, and the seeded rows match row for row apart from
the environment (libref, user id, group names) and the sample rows. That run
found six defects a source-only comparison could not: INT columns are 8 bytes
not 4, one label had lost a double space, the DC_MACROS setting held the SAS
expression instead of a path, the email templates held an escaped newline,
75 selectbox rows held %mf_increment(rk) instead of their key, and one
validation row was truncated at a ';' inside its regex.
makedata-parity.test.js parses both macros and compares them with the seeded
estate, and fails on a value that does not fit its column type, so the next
drift fails a test instead of shipping. The CI workflows, the Sites demo seed
and CONTRIBUTING call createdemodata after makedata.
loadData() re-selects a viewbox about 600ms after its data lands, and that can
land after the row click that opens its configuration - leaving the panel on
the other viewbox and the test looking for a column that table does not have.
The panel's heading names the viewbox it is showing, so the click is repeated
until the heading matches rather than trusting the click once.
The row is only clicked when the panel is not already on that viewbox, since
selectViewbox() toggles and a second click would deselect it.
Verified on the local stack: 12 passing in 2:30 and 2:20, against 5:22 for the
same spec before the change.
openTableFromViewboxTree paused 300ms before looking at each rendered node,
which on a library of this size cost ~13.4s of the ~20s a viewbox test takes.
The helper now expands the library and clicks the table's link, retrying the
expansion when the picker re-renders underneath it.
It also waits for the picker's own state before returning: every requested
table listed, no row spinner, and the ~600ms window in which loadData()
re-selects the box that just loaded. Without those, the next step read the
other viewbox's column list and failed on a column it does not have.
Measured on the local stack: the spec runs 2:18-3:15 against 5:22 before, with
all 12 tests passing in both.
The SAS Logs modal rendered the log, the source code and the generated code as one text block each, so no line could be referred to and the error, warning and note links had to locate a line by arithmetic. Render one row per line instead: every line carries its number, a wrapped line hangs under the text rather than under the number, and the numbers stay in view when a long log line is scrolled sideways.
The links now scroll to the row itself. The old offset was the measured line height multiplied by the line index, and two things were wrong with it: clientHeight rounds the height to a whole pixel, and the wrapper's padding was not counted. In a 168-line log at the stylesheet's own metrics the early links landed within a few pixels of their line while the later ones did not; at an 18.5px line height, which is what 16px monospace at 1.15 produces, the scroll for line 43 was 10.5px out and for line 127 was 52.5px out, so the highlight sat above the line it named and the drift grew with the line number. A row's own position cannot drift: every link lands its line at the top of the log, less the wrapper's 1px border, and flashes the right row.
The flash is a class on the rows rather than an absolutely positioned overlay, so a message SAS wrapped over several lines flashes every line it runs to.
One row per line costs about 0.25ms in Angular - a binding plus a sanitizer pass each - so rendering a whole log would freeze the modal: 20,000 lines took 5.3s to open. Each pane renders a window of 400 lines with a "Show more" line underneath, a jump renders through to the line it was given before scrolling, and opening is now flat at 165-280ms for 1,000, 5,000 and 20,000 lines.
git-describe only sees tags reachable from HEAD. CI checks out a single commit with no tags, so it reports the bare hash and every version in the app reads "v.<hash>" - including the production release builds. The root package.json carries the released version (semantic-release bumps it before the shipped frontend is built), so use it as the tag when git-describe found none, and fill semverString from it for the browser_info diagnostics.
The job installed the same lockfile the build job had just installed and paid
the downloads again (92s), and npx cypress install fetched its binary on every
run. Both now come from the cache the build job already fills, with the
Cypress binary keyed on the lockfile.
sas/mocks/test/ holds the node:test harnesses that read the payload files, and
the job deploys sas/mocks/sasjs/ and nothing else - so a pull request that
changes only those tests cannot change what the browser is served, yet it
started the Cypress suite. The file list is filtered before the match, so any
other file under sas/mocks/ still counts, a change that touches the tests next
to a service included.
The matcher's cases cover both directions.
The capture assigned its body to _webout in one template literal, and that
literal was not JSON: raw line breaks inside the graphviz label and an
unescaped quote in labelloc = "t". fromsas, clickableids, info and flatdata
are members now, written as string literals that keep the label's quotes and
line breaks, with the envelope supplied by the runtime.
sas/mocks/test/sasjs-webout.test.js holds the six services that set their own
content headers, and pins this capture's members. CONTRIBUTING.md lists the
same six.
Each fault shape the tree repairs now has a case that builds it - drive,
OMSOBJ, BUILTIN and DOMAIN - so a heuristic that stops detecting one fails the
suite instead of leaving only the exact-value assertion behind.
A drive letter is path-initial when nothing word-like precedes it, so a path
after <, >, =, [ or a comma is checked too; prose that puts a word before a
colon still is not a drive.
The two cases added for a path after a tag and after a comma were written as
single backslashes in the source, so each one evaluated to a value with no
separator at all and pinned nothing. The value is now assembled from
String.fromCharCode, with an assertion that it starts with the two separators
the case needs.
A service whose payload does not arrive as (kind, name, rows) contributed no
members and passed the tree-wide test with nothing checked; the loop now
records the file with no members captured.
The header describes what the values and the test do. The ticket numbers it
carried are gone: AGENTS.md allows a tracker reference only as a link, and the
behaviour reads without one.
A file that does not emit through webout() and is not a listed helper fails a
scope test, so a renamed emitter shrinks the run set loudly instead of leaving
the replay over an empty set. Every webout kind is captured, a service that
needs a global the sandbox lacks names itself in the error, and the header
states the scope the scan covers.
The header described the captures as live template literals, which this tree
no longer carries; the values now travel through webout() and the same
backslash is one escape away from the same damage.
The replay covers the files that call webout(), so a shared module added to
the tree is skipped instead of failing with a require error, while the
_webout assertion still reads every file - a service that writes its own body
cannot slip past the filter.
A UNC prefix is legal after any character that is not a word character, a
colon or another separator, so a path inside a tag or after a comma keeps its
prefix instead of reading as a doubled separator.
The whole-tree check treated any two consecutive backslashes as a fault, so a
UNC library path (\\server\share\...) failed on a correct value. A run of two
is now legal only where a UNC path puts it - the start of a value or of a quoted
path - and two separators anywhere else, plus any run of three, are still a
fault. faultsIn gains a case for each direction.
The sandbox require delegated relative requests to this test file's own
require, so a service requiring a sibling module resolved it under
sas/mocks/test. A relative request now resolves from the service file; builtins
and absolute paths delegate unchanged.
getdata's ENGINEHOST metadata rows carried a Windows path and an account
name with no separators, which SAS writes and the rest of this tree
already has: the rows read C:DataControllerDC996664mpe_x_test.sas7bdat
and BUILTINAdministrators, against refreshlibinfo's C:\DataController\
DC996664 library and BUILTIN\Administrators owner in the same recording.
Both values now carry single separators.
sas/mocks/test/sas9-separators.test.js replays all 36 files of the tree
under a webout() stub and fails on a doubled separator, or a missing one
in an OMSOBJ URI, a drive path or a BUILTIN/DOMAIN account. It also pins
the two corrected rows and asserts that no service assigns _webout.
The CSV / Excel / PDF export submenu never reached the screen. HOT's
ExportFile plugin contributes a context-menu entry under the reserved key
`export_file`, and ContextMenu's ItemsFactory.setPredefinedItems folds the
plugin's entry over the grid's entry at the same key. That folded entry
carries `hidden() { return this.getSettings().exportFile === undefined }`,
and no grid in this app sets `exportFile`, so the whole item - submenu and
all - was filtered out of the menu. Seen on the 7.22.0 build: the viewer's
context menu offers Copy with header, Copy header only and Show labels, and
nothing else, so the client-side PDF export could not be reached at all.
Give the item a key of the app's own, EXPORT_MENU_KEY, and use it at the
three grids that build the menu (viewer, editor, viewboxes). The submenu
keys move with it.
export-menu.spec.ts pins both halves against a real grid, because the
merge happens inside the plugin at menu-build time and no unit test of the
item object can see it: configured under `export_file` the item is absent,
configured under EXPORT_MENU_KEY it is present.
The 7.22 release's sas-docs job failed with surfer's "Run surfer config first,
or provide --server ... --password" message, and its log shows SURFER_PASSWORD
empty - the CODE_DATACONTROLLER_IO secret resolves to nothing. Authenticate
with WEBDOC_DATACONTROLLER_IO, the password the typedoc job in this same
workflow already uses: a Cloudron app password belongs to the user, and the
same user owns both docs apps, so one credential covers both.
build.yaml, lighthouse.yaml and sites.yaml were moved to Node 24.15.0 on
2026-10-28 (d84a1d50); release.yaml, release-assets.yaml and docs.yaml were left
on 24.5.0. The release workflow builds the same client and runs the same Cypress
suite as build.yaml, so it should not be the one place pinning a different
runtime - and docs.yaml builds the frontend for typedoc with it too.
The release workflow recorded video for every spec and uploaded
cypress-videos.zip on every run, green or red. Recording eleven specs costs
encode time each run and the recording is only ever opened after a failure, so
run with video=false,screenshotOnRunFailure=true - the same pair build.yaml
already passes - and zip screenshots plus videos only under if: failure().
# [7.22.0](https://git.datacontroller.io/dc/dc/compare/v7.21.0...v7.22.0) (2026-11-05)
### Bug Fixes
* **browse:** accept legal folder names, cap the walk, and lock the read ([3572d56](3572d56cfb))
* **build:** build the mock sites with a CLI whose runtime provides webout() ([b1eab98](b1eab98200))
* **build:** publish the changed-path decision and pin the matcher with tests ([ecc8702](ecc87021c3))
* **build:** treat a sas/package.json bump as a Cypress input ([48df795](48df795202))
* **client:** cancel a queued dropdown reposition on destroy ([590b841](590b841f24))
* **client:** drop the synthetic window resize dispatches ([2535baa](2535baa5f7))
* **client:** keep the ARIA pass on the viewbox and multi-dataset grids too ([cb30469](cb30469e79))
* **client:** restrict the table pass to Handsontable's own tables, add the viewer fallback ([46c6aec](46c6aec37a))
* **client:** scope the ARIA pass to each grid and drive it from the render hooks ([bf1e7d6](bf1e7d6b15))
* **client:** stop synthesising aria-labels; name the filter modal's submit in the test ([32d3d49](32d3d49b59))
* **client:** strip only this grid's own wrapper, and pin the re-strip after a data load ([f282d1a](f282d1a44b))
* **deploy:** ACTION cannot seed by default, and the probe stops overwriting ([e84cfaa](e84cfaaabd))
* **deploy:** ask for the admin group before the staging directory on SASjs ([5c64691](5c646913e4))
* **deploy:** await nothing, and encode the values in the makedata URL ([d67a0e5](d67a0e5d5f))
* **deploy:** drop the deprecated service-pack deploy from the setup screen ([59e09e2](59e09e26a5))
* **deploy:** gate the browse actions on the settings file ([5eb8492](5eb8492742))
* **deploy:** make the folder picker work on both estates ([fb38309](fb38309e98))
* **deploy:** match the documented SYSSCPL set and stop guessing at the cap ([5f338ff](5f338ff437))
* **deploy:** read a URL-supplied ACTION before defaulting, and show locked folders ([addb4a5](addb4a56ea))
* **deploy:** stop the makedata poll outliving its screen, and align the release gates ([c808c41](c808c41333)), closes [#407](#407)
* **deploy:** unclamp the folder picker row and probe in the chosen context ([1330925](133092502c))
* **deps:** bump @sasjs/core to 7.0.1 so special missings can be restored ([3d2eefd](3d2eefdd7b)), closes [sasjs/core#499](sasjs/core#499)
* **deps:** bump @sasjs/core to 7.1.3 so a filter abort names the clause ([c7c1815](c7c181518c))
* **editor:** a container resize must not clear an active sort ([5bd7b5d](5bd7b5d724)), closes [#246](#246) [#246](#246)
* **editor:** drop the stale case hint, and narrow the strip to our own form ([bd2739c](bd2739cde4))
* **editor:** one predicate for the CASE marker, and no stacking in the tooltip ([18b0546](18b0546dcb))
* **embed:** a rejected mapping must not block the page ([d845b9e](d845b9e9b7)), closes [dc/dc#404](#404)
* **embed:** read a month-name date from VA as a date, not a character value ([30cefcf](30cefcf2dc))
* **getgroupmembers:** widen membername to $256 for long names ([196d6c9](196d6c931d))
* **lint:** enable noSingleAsteriskComments and noUnusedLibnames ([9071801](9071801ac5)), closes [#376](#376)
* **lint:** make noBannedStrings an error and clear its 13 findings ([0b85f7b](0b85f7bf1e)), closes [#376](#376)
* **lint:** make noUndeclaredMacros an error, and fix what it found ([af28a79](af28a79bd5)), closes [#376](#376)
* **lint:** make noUnusedMacros an error and clear its 54 findings ([c8c0858](c8c0858295)), closes [#376](#376)
* **lint:** rename the short and mixed-case helper macros, and clear the last warnings ([7da67eb](7da67eb17b)), closes [#376](#376)
* **makedata:** let the browse actions live as long as the service does ([912fae4](912fae448b))
* **mocks:** 0 is a closed TX_TO, a libref boundary, and the union of the row keys ([67d9cfd](67d9cfd1bd))
* **mocks:** abort like mp_abort, lock like mp_lockanytable, derive saslibs ([ca60208](ca602082ff))
* **mocks:** answer the deploy screen's DIRLIST and WRITECHECK actions ([e1c3c6e](e1c3c6e872))
* **mocks:** derive startupservice's maps and timezone instead of hardcoding ([87fd46c](87fd46c825))
* **mocks:** fold the audit window, and append MPE_AUDIT under its lock ([5ecc7be](5ecc7bebec))
* **mocks:** keep the OMS and path separators the captures lost ([7d9d466](7d9d466c3e))
* **mocks:** one sasDatetimeNow, a documented TX_TO=0, and a shaped fallback export ([e53c50d](e53c50d4e2))
* **mocks:** read hook columns in either case, and guard duplicates first ([349a2f3](349a2f314c))
* **mocks:** read the metadata member from the response, not the function's arity ([09e9f19](09e9f192c5))
* **mocks:** refuse a runtime whose webout() cannot carry column metadata ([482435f](482435f318))
* **mocks:** resolve the audit table once, record lower-case md5, and report a failed audit write ([1a9f594](1a9f59478b))
* **mocks:** run approval hooks, as mpe_runhook does ([f0208bb](f0208bb0b0))
* **mocks:** separate a body read failure from a missing showmeta floor ([744c89b](744c89b919))
* **mpe_abortmessage:** decide once whether the payload was escaped ([af89126](af89126acf))
* **mpe_abortmessage:** read program and state by name, and trim the prefix ([68b89ca](68b89caf63))
* **mpe_assert_abort:** anchor the cut on the whole escaped separator ([e74a7ba](e74a7ba115))
* **mpe_assert_abort:** compare the hook's message, not the log extract ([8b426c2](8b426c27fd))
* **mpe_assert_abort:** compare the unescaped message, cut at the last marker ([e68d221](e68d221354))
* **mpe_assert_abort:** cut at the separator, not the last marker ([4c7e869](4c7e869369))
* **mpe_assert_abort:** name the separator and tolerate a carriage return ([7b09ade](7b09ade97c))
* **mpe_assert_abort:** size the capture buffer for the whole message ([5d32de9](5d32de9df9))
* **mpe_assert_abort:** strip the separator tail, not every trailing escape ([b29bc1e](b29bc1e7ae))
* **mpe_execute:** assemble the abort text in a macro that can be tested ([e2e9103](e2e91039cb))
* **mpe_execute:** keep a child abort message intact when reporting it ([ebfd395](ebfd39582f))
* **mpe_execute:** keep the child message in the fallback, and test the doubled separator ([11587e2](11587e2f24))
* **mpe_execute:** make the abort-text fallback reachable ([24b8cbe](24b8cbe9dc))
* **mpe_execute:** mask a child abort message before it aborts the caller ([20dbf00](20dbf00fb0))
* **mpe_execute:** one symbol for the abort text, and no values in the call ([700b906](700b906d69))
* **multi-dataset:** a sheet that failed to extract is not a clean finish ([3129648](312964864e))
* **multi-dataset:** show the file size and loading stage while the file loads ([45d6c02](45d6c025c9)), closes [#121](#121)
* **report:** treat a blank PDF footer parameter as its default ([0f729d7](0f729d7101))
* **test:** let the dirlist and writecheck results reach the caller on Viya ([ed678f0](ed678f0b46))
* **test:** run metanav.cy.ts in CI and open a metadata object ([02a8590](02a8590310))
* **va-host:** bound the early replay even when the frame is not postable ([f6d5a04](f6d5a0495e))
* **va-host:** deliver in load mode, honour dataType, and survive odd prompt names ([7d4155d](7d4155dc6c))
* **va-host:** deliver the load-mount message once, and document report.json from the file ([90784b3](90784b342f))
* **va-host:** keep the deploy password off argv, and verify the app is served ([7c63a5c](7c63a5c93f))
* **va-host:** post to the frame's own origin, and key the replay to the document ([9cec292](9cec292681))
* **viewer:** clamp over-tall PDF rows, always emit an otherwise-empty page ([10229d5](10229d5efe))
* **viewer:** defer the blob URL revoke, reject a selection inside skipped columns ([b5bb704](b5bb704a4f))
* **viewer:** keep the PDF export working where hiddenColumns is not enabled ([f84d4ca](f84d4ca7af))
* **viewer:** review findings - CSV skipped column, PDF row cap before reading ([2c4be5e](2c4be5e94b))
### Features
* **admin:** find and test a writeable location during deploy ([442c595](442c595c2b))
* **deploy:** pick and validate the DC location from the deploy screens ([6eeab2e](6eeab2ed16))
* **editor:** convert CASE rule values on entry ([c4eef49](c4eef498b2))
* **logs:** read the logs modal with the shared log parser ([aabc578](aabc578d5e))
* **mocks:** add the control-table services the screens call ([a1615ee](a1615ee8a7))
* **mocks:** build every JS stored program's response with webout() ([ce6bb5d](ce6bb5d4cd))
* **mocks:** emit the lineage, usernav and libinfo responses through webout() ([1f54fad](1f54fad3fe))
* **mocks:** emit the metanav responses through webout() ([e1d877f](e1d877f21d))
* **mocks:** emit the sas9 services through webout() ([7be3f84](7be3f84073)), closes [#461](#461)
* **mocks:** restore a load, and record the diffs it needs ([1a60c68](1a60c689f4))
* **mocks:** use the server's webout() in the JS stored programs ([ccb67ed](ccb67ed5a0))
* **va-host:** emulate a VA report that embeds DC, as a deployable AppStream app ([2185db9](2185db911d))
* **viewer:** download a server-side PDF report ([67ca129](67ca129c4d))
* **viewer:** export the grid as PDF from the context menu ([e58e735](e58e735610))
### Reverts
* keep the helpers nested inside makedata_body ([4424171](4424171b2f))
The SASjs Server / SAS9 screen put the DC Staging Directory first and the admin
group second, the reverse of the Viya screen, so the operator was asked to pick
a location before being told who administers DC. It now asks for the admin group
first and the directory last, with the copy reordered to match.
A rendered-order spec covers it: it reads the labels off the rendered template
and fails if the directory comes first again.
DIRLIST and WRITECHECK were refused once the appLoc had a settings.sas, on the
argument that a failed run could leave that file behind. That is what makes the
gate wrong: a failed run is exactly when the deploy screen still needs to list
and probe directories, and any appLoc configured earlier refuses them for good.
The window needs no separate clock - a successful run deletes this program, so
once DC is configured there is nothing left here to call.
Removed from both variants (server checks the drive over HTTP, Viya checks the
filesystem; neither is needed), and the two tests that asserted the refusal go
with it. The macros themselves are covered by mpe_dirlist.test.sas and
mpe_writecheck.test.sas.
Seen on sas.4gl.io: a deployment to /Public/app/dc477 answered BROWSE with
"DIRLIST and WRITECHECK are only available until DC is configured".
The release pipeline went red at 28157fae: Karma aborted with "Some of your
tests did a full page reload!" after test 753 of 861. The new-window spec calls
runMakedataInNewWindow, which starts the five-second makedata poll; the service
stub answers that makedata is gone, so the first tick assigns window.location.href
and the page reloads under the running spec. A local run finishes in under five
seconds, so the timer never fires there - only CI saw it.
- the poll is cleared in ngOnDestroy, so a timer cannot reload the page after
the user has moved on; a spec covers that
- the spec that exercises the new-window URL no longer starts the real poll
Three open findings on #407, all taken:
- lighthouse.yaml: sas/package.json joins the change-detection pattern, because
the job pins @sasjs/cli from that file
- release-assets.yaml: webpack is installed at the version the client lockfile
resolves rather than whatever is current at release time
- build.yaml: the client audit runs with --audit-level=low, which is what the
comment above it has always claimed
Both test programs pointed _webout at a temporary file so the macros under
test could write their JSON answer, then tried to hand the fileref back with
`filename _webout "&_webout_path" mod;`. On Viya's JES _webout is a filesrvc
fileref, whose pathname() is empty, so that statement fails with "Invalid
physical name" and the temporary file stays in place. The term program's
%webout(OPEN) assigns _webout itself (filesrvc on JES, temp elsewhere), so the
restore is not needed at all; the two programs now leave the fileref to it.
Observed on the nextviya estate: the suite reports 64/64 of the tests that
produced results, with mpe_dirlist and mpe_writecheck absent from the results
JSON and "Invalid physical name" in their logs.
The spec failed on CI: the browse button never appeared. Two causes, both
found by reproducing the CI estate locally (seeded drive):
- The intercept pattern '**/drive/folder**' does not match the request the
app makes ('/SASjsApi/drive/folder?_folderPath=...') in this Cypress
version, so the app saw the real drive, loaded the startup service and
never routed to the deploy screen. Both intercepts are now regexps, which
match deterministically. Verified on the seeded estate: the app routes to
#/deploy and the agreement card renders.
- A secured estate shows a login form first. The boot helper now waits for
one of the states it can act on (a visible login form, the agreement card,
or the deploy screen), signs in when the form is visible, then waits for
the deploy screen. The login form stays in the DOM hidden on other routes,
so visibility - not presence - decides.
Review findings on #476.
- The 'hands the picker the context' test declared only AutomaticComponent
and HostComponent with NO_ERRORS_SCHEMA, so app-dc-location stayed an
unknown element: the [computeContextName] binding was never applied and
the assertion just repeated the fallback case. DcLocationComponent is
now declared (SasService is already stubbed), and the test asserts the
picker's own input - both before a selection (the configured fallback)
and after one is chosen. Verified it bites: removing the binding from
the template fails it.
- The Cypress spec set the path nowhere before clicking Validate, so the
assertion leaned on the field arriving pre-filled; a blank field would
short-circuit with 'Type or pick a folder first' and fail for a reason
unrelated to the layout. It now types the path explicitly.
- The spec slept a fixed 4s after accepting the agreement, which turns a
loaded CI runner into an intermittent failure. Both tests now share one
boot helper whose readiness gate is the Browse button appearing.
Two defects on the deploy screens, found on staging.
1. The folder picker's outer row reused Clarity's 'clr-input-wrapper'
class, whose 'max-height: var(--clr-forms-input-wrapper-height)'
clamps the row to the input's height. The row is flex-wrap: wrap, so
once the Browse/Validate buttons wrapped (or the row was simply
taller than one input) they overflowed the clamp, and the folder
browser panel - positioned after the collapsed box - rendered on
top of the input, the buttons and the surrounding text. Measured on
staging: panel top 256 against input 248-272 and buttons 280-304;
with the class dropped the row contains all its children and the
panel starts at 312.
2. The Viya screen asked for the DC directory before the compute
context, but the folder listing and the write check run inside the
chosen context, as its batch identity - so the probes could inspect
a file system the deployment itself would not use. The fields are
reordered (admin group, then batch id / compute context, then DC
Loc last) and the picker now runs DIRLIST and WRITECHECK with the
selected context in the adapter request config, which every Viya
executor path honours. The SAS 9 / SASjs screens pass no context
and behave exactly as before.
Tests: a Cypress spec drives the real deploy screen estate - the
picker row must contain its own children and the panel must start
below them; both tests fail with the clamp reintroduced. A rendered
TestBed spec pins the Viya field order (admin group < compute context
< DC Loc) and fails with the old order. Unit specs pin the probe
config: no context on SAS 9 / SASjs, the chosen context on Viya.
Three review findings.
- KEY_HASH was upper-cased after the digest. SAS md5() writes lower-case hex,
so the mock's key hash did not match what a real audit table holds. The
digest is stored as it comes out, and the test asserts lower case.
- restore resolved MPE_TABLES.AUDIT_LIBDS per table, while the approval always
appended to the control library's MPE_AUDIT: a table with its own audit table
had its diffs written where restore never looked, and restore then refused
with "No entry for <load>", which no later approval could fix. Resolution now
lives in dcMockUtils.auditLibdsFor, called by both sides, and the approval
reads and saves that table in its own libref.
- An audit failure was only logged to the server console, leaving an approval
with no diffs and no visible reason. It is still not allowed to undo the
approval, but once the control tables are written the failure is raised as an
abort that names the table and says the load can no longer be rewound.
Two tests: one approves a table whose MPE_TABLES row names a different audit
table and asserts the rows land there and not in the control library; one points
AUDIT_LIBDS at a libref with no data folder and asserts the approval still
lands and the response says the diffs could not be recorded.
Two review findings on this branch.
- editors/restore.js spread every audit row for the load into Math.min(...).
MPE_AUDIT is append-only, so a long-lived estate would eventually exceed the
engine's argument limit and the RangeError would surface as a failed restore
of a perfectly restorable load. Folded into a reduce with an Infinity seed.
- auditors/postdata.js appended to MPE_AUDIT as a read-modify-write outside any
lock. postdata.sas locks the table it writes - MPE_REVIEW around its proc
append, MPE_SUBMIT around its update - so the audit append now takes the
MPE_AUDIT lock around a fresh read, the concat and the save, rather than
reading the table up front and saving a copy that may be missing another
approval's rows.
53 mock tests pass (node --test sas/mocks/test/).
`editors/restore` had no mock, and the reason it could not have had one is that
`postdata` wrote no audit rows: an approval applied the load and left nothing
to rewind. Both halves are here.
postdata (final approval): writes MPE_AUDIT the way %mp_storediffs does - one
row per column, keyed by the md5 of the business-key values joined by '|', with
move_type A/D/M, is_pk on the key columns, is_diff on a changed one, and the
old and new value on their own typed columns. An append carries every column
on the new side, a delete every column on the old side (which is what lets a
restore put the row back), and a modification the keys plus only the columns
that actually changed. A failure writing the audit is logged and does not undo
an approval that has already landed.
restore: mpe_checkrestore's two decidable checks (the table has an audit table;
the load has entries in it) with their messages, then mp_stripdiffs' window -
the load's timestamp and every change to that table from then on, because a
restore reverts this load *and all subsequent ones*. Per business key the
pre-window state is the earliest event: an append means the row is staged for
deletion (`_____DELETE__THIS__RECORD_____=YES`), a delete means it is staged
with the values the delete recorded, and a modification means each column takes
its earliest old value while untouched columns keep the current one. The
result goes through the same stageSubmission the editors use, so it arrives as
a new submission with the reason `Restoring <loadref>` and is approved like any
other - restore.sas never writes the base table either.
The access and group/RLS checks are not emulated, consistent with the rest of
the mocks: nothing enforces MPE_SECURITY there.
Tests: 52 pass across the seven mock suites, 7 of them new. They drive a real
approve and then read the staged rows back: append staged for deletion,
modification staged with the pre-load value, delete staged with the row's
values, and restoring the first of two loads reverting the second as well.
With the base postdata and no restore service, all 7 fail.
Four review findings.
- isCurrentOn counted TX_TO 0 as current. 0 is 01JAN1960 in SAS, so a real row
with that window has been superseded; only a missing, null or blank TX_TO is
open besides 31DEC9999. The fixtures are seeded with 31DEC9999 (253717919999)
through an OPEN constant instead, and the 0 clause is gone.
- The MPE_DATADICTIONARY filter compared DD_SOURCE against the libref without a
boundary, so DC_JSLIB2.TABLE was excluded along with DC_JSLIB.TABLE. It now
tests for the libref plus the dot, as the libds rewrite does.
- The schema-less export took its column list from Object.keys(rows[0]), so a
column that first appears in a later row was silently missing from the backup.
The list is the union of the keys across the exported rows, in first-seen
order.
- control-tables.test.js imported readTable and never used it.