b301399b5cceef3619aa71b782e8f5db67c0738d
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b301399b5c |
fix(mocks): viewdata sends a blank cell as a space, not the word undefined
String(undefined) put the literal text "undefined" into every cell of a column the row does not carry - the ordinary case in MPE_TABLES, where nine columns are blank on every row - so the viewer rendered the word instead of an empty cell. A value the row does not carry is SAS's missing, so a character column gets a space and a numeric one '.', which is what the empty-table branch beside it already did. sas/mocks/test/viewdata.test.js fails without the change and passes with it. |
||
|
|
3bb9fb52e3 |
fix(mocks): make getdata and viewlibs answer what the estate answers
The two scenarios recorded in the previous commit are now gated, with one declared difference each instead of 388 and 4. The diffs were the mock's, and they are fixed rather than accepted: - cols: dropped the DESC key SAS never emits, and took LABEL/LONGDESC from the MPE_DATADICTIONARY rows for this lib.ds.column, as getdata.sas augments mp_getcols. COLTYPE is blank for DATE/DATETIME/TIME, which is what the estate answers. - sasdata: sorted by the business key; character values trimmed the way the JSON writer writes them, with an all-blank value as a single space. - dqrules: the active MPE_VALIDATIONS rows unioned with the schema's NOTNULL (limited to the columns left in the selection, which is why a NOT NULL on tx_from never reached the grid) and the derived columns' READONLY, then de-duplicated and sorted by column and rule type with X left at the missing value - getdata.sas's union/sort/by-group logic. - sasparams: the current-rows clause mpe_filtermaster appends, and blank character values as the single space SAS sends. - maxvarlengths: the value as stored (trimmed), and the key columns in upper case, which is how the selection spells them. - approvers: computed from MPE_SECURITY's APPROVE rows with the admin group as the fallback, instead of a hardcoded sasdemo. - viewlibs: the shape %mp_getlibs answers - engine in LIBRARYID, blank ENGINE, libref as LIBRARYNAME. The control library and the admin group now come from services/settings.js through dcInit(), which is the mock's %mpe_init(): makedata already writes dc_libref, dc_admin_group and dcpath there, so a deployment with a different DCPATH no longer needs a service change. test/mock-settings.test.js proves it by renaming the library and moving the data. The remaining services still carry const dcLibref = 'DC_JSLIB' and compute paths from it at file scope, so each is a separate change. The sas9 mock keeps its own getdata/viewlibs variant, so it is untouched: the goldens are recorded from the SASjs Server estate. |
||
|
|
49a9bbf022 |
feat(mocks): record getdata and viewlibs, and show what the mock gets wrong
Two more scenarios, and the recorder could not take either of them as it stood. - editors/getdata reads a table input, not URL parameters, so record.js can now post one: it writes the informational header the server expects (the first line is the SAS input statement, not a list of column names), posts it as multipart/form-data, and takes --libref so the estate's control library name stays out of scenarios.json. replay.js hands the mock its own rows through mockInput, since the two sides do not share a libref. - the test harness's webout() stub now honours the showmeta option and writes the body as members are added. Without them the mocks' own requireMemberMetadata() guard throws - it reads the body before CLOSE - and a service that stopped sending formats and labels would have passed here. - --scenario accepts service/id, because two screens legitimately call their first service 'default'. The two goldens are recorded but marked gated: false: the gate prints their difference counts and does not fail, and check.js labels them ADVISORY. They are a work list, not an acceptance. editors/getdata/mpe_tables - 388 differences, all of them the mock's: cols carries a DESC key SAS never emits and misses the LABEL/LONGDESC/COLTYPE SAS reads from MPE_DATADICTIONARY; sasdata leaves blank character values out instead of padding them and is not ordered by the business key; dqrules differ in content and order; approvers is hardcoded to one user where SAS computes it and returns none. public/viewlibs - 4 differences: SAS answers LIBRARYID='V9' with a blank ENGINE and LIBRARYNAME equal to the libref, and lists SASHELP; the mock answers LIBRARYID=<libref>, ENGINE='V9', LIBRARYNAME='Data Controller(<libref>)' and has no SASHELP. dsmeta is excluded on purpose: it is the engine's report on the dataset (owner, host, encoding, paths, created dates), estate state rather than behaviour, and the projection refused the recorded response until it was dropped - the site id and the service account name were in it. |
||
|
|
909580dbda |
docs(mocks): cite the fixture that actually hits the SOME_SHORTNUM rule
Two documentation findings from review:
- The comment in createdemodata.js and the parity README said the excel
fixtures hold "3, 18, 34, 79", which points at values the dropped rule never
saw. The uploads that the rule applies to go to dc_jslib, and
regular_excel.xlsx has five rows where SOME_SHORTNUM is 5 - that is the value
the 1-5 HARDREGEX rejects. The 3s belong to a multi-load fixture whose sheet
targets DC996664.MPE_X_TEST, a libref that carries no seeded validations
because getdata filters MPE_VALIDATIONS on base_lib === requestedLibref and
createdemodata copies only data files into it. Both places now name
regular_excel.xlsx and the value 5.
- README:9 promised the scan refuses an "eight-digit id" while the added class
is \bDC\d{8}\b. It now says "eight-digit DC id", which is what the scan does
and what the date case in parity-golden.test.js relies on.
Checked the fixture rather than the diff: column H (SOME_SHORTNUM) of
regular_excel.xlsx holds 18, 34, 79, 74, 98 ... and exactly five rows of 5.
|
||
|
|
038c40499b |
refactor(mocks): name the synthetic estate by target name, not a sasjsconfig key
The previous commit added "synthetic": true to the server target to stop the recorder claiming an estate it had not checked. That put a harness-only key into the shared build config, where nothing in sasjs reads it and a maintainer could reasonably read it as a sasjs feature. The recorder now carries the list of target names that point at the estate makedata builds (SYNTHETIC_TARGETS = ['server'], which is sas.4gl.io locally) and refuses anything else, so sasjsconfig.json is untouched. The declaration is the same one - it cannot be derived from a response, because makedata is the installer and a customer's control library holds the same rows - it just lives with the tool that acts on it. --target server passes the guard (and needs a token, as before); --target server-mihajlo is refused with a message naming the allowed target. |
||
|
|
7815dcccb8 |
fix(mocks): drop only the MPE_X_TEST rule the excel fixtures break
The previous commit removed every validation rule SAS seeds for MPE_X_TEST, which fixed the excel specs and broke editor 3: the cell dropdown they click for comes from the HARDSELECT_HOOK on SOME_NUM, and that hook had gone with the rest. Only one of SAS's rules actually conflicts with the fixtures - the HARDREGEX on SOME_SHORTNUM, which rejects 1-5 while duplicate_row_excel.xlsx and friends hold 3, 18, 34, 79. Everything else stays, so the mock's rule set for the table is again exactly what it was before the seed work: SOFTSELECT on SOME_BESTNUM, HARDREGEX and SOFTREGEX on SOME_CHAR, SOFTREGEX on PRIMARY_KEY_FIELD and the SOME_NUM hook. The parity README now says this, rather than claiming the table is rule-free, and createdemodata.test.js asserts both halves - the hook is present and the SOME_SHORTNUM rule is not. |
||
|
|
dd2a777438 |
fix(mocks): restore the MPE_X_TEST fixtures and pin the replay's timezone
The parity gate's Cypress job went red after the seed work landed, and the two
causes are the same change seen from the fixture side:
- makedata now carries SAS's demo validation rules for MPE_X_TEST, including the
HARDREGEX on SOME_CHAR that blocks any text but 'the'/'data'. The excel
fixtures upload arbitrary values into that table and expect the submission to
reach the approval screen, so ten excel specs timed out with no Approve
button. createdemodata drops MPE_X_TEST's rules, which is what editor.cy.ts
already documents ("MPE_X_TEST stays clean to match the excel fixtures").
- the LABELs the viewer-labels spec asserts went with the hand-written schema.
createdemodata restores them, because a SAS estate has none and the control
library should not claim otherwise.
Both are declared as fixture divergences in the parity README and asserted in
createdemodata.test.js, so the next change to either side fails a test instead
of a Cypress run.
Review findings:
- the SYSTIMEZONEOFFSET acceptance depended on the host's zone; both entry
points pin TZ=UTC before the harness loads, so the accepted list is the same
everywhere.
- the sas9 mock still published '+0100'; it now answers seconds east of UTC
like the standard estate.
- the README promised an eight-digit id scan that IDENTIFIER_CLASSES no longer
had. A DC-prefixed eight-digit class is back (a plain digit class would fire
on a yyyymmdd date), with a case proving a date is not refused.
|
||
|
|
94cd4912f9 |
fix(mocks): make the golden's claims evidence, not labels
Six findings from review: - estate: 'synthetic' was a constant. The target now declares "synthetic": true in sasjsconfig.json and record.js refuses any other target, so the claim in the file is one the tool had to check before it wrote anything. - project() dropped the key when scrubbing a value, so a top-level member named like a secret was published unredacted. The key is passed through, and checkPublishable now runs secretViolations on the projection, so the recorder refuses before writing rather than leaving it to the next pull request. - denyList restated a subset of ENVELOPE_NEVER. It is built from that list now, so an error text or a hostname in a discarded field is caught too (values shorter than four characters are skipped - they would match half the file). - replay() projected with golden._meta.members while the test validated scenario.members, so editing the scenario had no effect until a re-record. It projects with the scenario's list and reports a member-list mismatch. - replay() deleted SYSCC/SYSENCODING from the expected side when the harness omitted them, which would hide a service that stopped emitting them. The harness now adds the two members the way the server does, and they are compared. - the eight-digit siteId scan class matched any eight-digit payload value (a date, an amount) and refused clean goldens. Removed; the site id is caught by the deny list, which now covers it. |
||
|
|
a57557c60e |
fix(mocks): wire the replay into CI and close the review gaps
Four findings:
- the replay was never run by CI, so the README's "a mock change fails the
pull request" was not true. parity-check.test.js now replays every scenario
through a shared parity/replay.js (which check.js also uses), fails on a
difference that is not declared and on a declared one that no longer differs,
and fails a scenario that has no golden. The differences that remain are
declared per scenario with a reason: the estate's admin group, its
registration state, its timezone and SAS's space padding of blank characters.
- recording stamped tls: verified for a non-https target while sending the
bearer token in the clear. A non-https target is refused outright now, and
--insecure still records tls: unverified.
- the committed-golden gate never applied the secret-key rule. secretViolations
now walks a golden (and a fresh projection) for secret-named fields whose
value is not <REDACTED>, with a case proving a hand-typed licence key fails.
- arg() returned argv[index + 1] unconditionally, so a trailing --ca reached
readFileSync(undefined). Flags without a value are a usage error now.
The replay also found a real mock defect, fixed here: SYSTIMEZONEOFFSET was
emitted as +HHMM while SAS answers seconds east of UTC ('3600'), and the mock's
own test asserted the wrong form - exactly the drift this harness exists for.
The sasdatasets sort used localeCompare, which puts MPE_X_CATALOG-FC before
MPE_XLMAP_RULES; SAS sorts on the byte value, so the mock now does too.
|
||
|
|
6c831bb230 |
fix(mocks): verify TLS when recording, and project staging ids whole
Two findings from review: - record.js turned certificate verification off unconditionally, on the request whose response becomes the golden. Verification is now the default; --ca trusts a private estate's own certificate and --insecure is opt-in, warns on stderr and is recorded as _meta.tls so a golden taken over unverified transport cannot be mistaken for one that was not. The estate's certificate verifies, so the committed golden is tls: verified. - the libref placeholder ran before the staging-id one, so a staging id came out as <LIBREF>-1_2_3. The staging rule now runs first and matches both minted shapes (DC<date>-<time>_<n>_<n>, and the mock's millisecond form); a project() case pins the ordering. |
||
|
|
b98fde1c74 |
docs(mocks): record that the parity refresh is an ad-hoc agent job
The pipeline cannot reach a SAS estate, so the goldens are refreshed where the credentials are - an agent or a maintainer running record.js - and the CI half stays offline: scan the committed goldens, replay them against a mock estate. Say that in the harness README so the next reader does not go looking for a scheduled refresh job that cannot exist. |
||
|
|
6262843f3f |
feat(mocks): check the mock against recorded SAS responses
The mock services are a second implementation of the SAS backend, and the only thing comparing them was a human reading two files. This adds the harness that compares their output instead. A golden is a projection of a SAS response, never a copy: project.js keeps a fixed member list per scenario, drops the whole envelope (hostname, site id, user, home paths, job ids, _PROGRAM), substitutes the identifiers the diff needs (<LIBREF>, <PATH>, <EMAIL>, <STAGING_ID>, <DATETIME>), and redacts secrets by key name. The recorder then refuses to publish if any identifier class survives, or if any literal the envelope just discarded still appears in the projection - which is what catches a hostname echoed inside an error message. record.js records from a live estate into .captures/ (gitignored) and writes only the projection to golden/. check.js replays a golden against a mock estate built by the test harness and prints field-level differences; it needs no server and no credentials. parity-golden.test.js re-runs the identifier scan over every committed golden, so a hand-edited file cannot slip through either. Phase 0 ships one scenario, public/startupservice. The replay is advisory and already reports twelve differences worth a look (admin group casing, the registration counters, timezone representation, the sasdatasets order, blank versus empty Excel map descriptions); the corpus, the merge gate and the nightly drift job follow. Stacked on the mocks context in the parent branch: it seeds the mock estate with seededEstate(), which that branch adds. |
||
|
|
6e0866d876 |
fix(mocks): give MPE_X_SEARCH its column metadata back
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. |
||
|
|
0e0e79ef63 |
test(viewbox): match the configured viewbox by its full lib.dataset
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. |
||
|
|
112e8e4b8b |
fix(mocks): keep the derived rows makedata owns
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. |
||
|
|
b5fe46931f |
test(viewbox): scope the picker lookup, escape the library name, outwait the re-select
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. |
||
|
|
5f89314aaf |
fix(mocks): answer the review on #487
- 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. |
||
|
|
a6cfb12579 |
refactor(mocks): build the control library makedata.sas builds
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. |
||
|
|
acf44dae74 |
test(viewbox): select the config panel by its own heading
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. |
||
|
|
d3f1b2ac0a |
test(viewbox): select a picker table without walking the tree
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. |
||
|
|
7a5286fca8 |
feat(logs): number the lines of the log, the source and the generated code
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. |
||
|
|
da8e7d343c |
fix(version): fall back to package.json when no tag is reachable
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. |
||
|
|
88616c8f62 |
ci(build): reuse the client npm cache and the Cypress binary in the Cypress job
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. |
||
|
|
68dc790698 |
ci(build): keep the mock tree's own tests out of the Cypress gate
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. |
||
|
|
caa58771bf |
fix(mocks): emit the sasjs lineage capture through webout()
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. |
||
|
|
67631133f9 |
test(mocks): pin the missing-separator branches and check a delimited drive
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. |
||
|
|
f1ba6362cc |
test(mocks): build the UNC cases from the code point
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. |
||
|
|
755083bb49 |
test(mocks): fail a service that emits no member, and drop the history from the header
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. |
||
|
|
0e74ab58a1 |
test(mocks): pin the tree's scope and name the service that cannot run
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. |
||
|
|
c9119101a4 |
test(mocks): describe the separator rule as it applies to the webout() tree
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. |
||
|
|
676cdf6fdd |
test(mocks): run only the services that emit, and widen the UNC exception
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. |
||
|
|
88cc68e2f9 |
test(mocks): anchor the doubled-separator rule and resolve sibling requires
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. |
||
|
|
bafbe201ce |
fix(mocks): restore the two separators the sas9 getdata recording lost
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. |
||
|
|
109711b9a0 |
fix(viewer): key the export menu so Handsontable keeps it
Sites / deploy (push) Successful in 3m19s
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.
|
||
|
|
6bf7f9ba0d |
ci(docs): give the SAS docs deploy the Cloudron password that exists
Sites / deploy (push) Successful in 3m19s
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. |
||
|
|
205adf0a2b |
ci: put the remaining workflows on the node version the build uses
build.yaml, lighthouse.yaml and sites.yaml were moved to Node 24.15.0 on
2026-10-28 (
|
||
|
|
f3d1eb11d1 |
ci(release): attach cypress evidence only on failure
Sites / deploy (push) Successful in 3m17s
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(). |
||
|
|
5c646913e4 |
fix(deploy): ask for the admin group before the staging directory on SASjs
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. |
||
|
|
912fae448b |
fix(makedata): let the browse actions live as long as the service does
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". |
||
|
|
c808c41333 |
fix(deploy): stop the makedata poll outliving its screen, and align the release gates
The release pipeline went red at
|
||
|
|
ed678f0b46 |
fix(test): let the dirlist and writecheck results reach the caller on Viya
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. |
||
|
|
e8cdc37a05 |
test(deploy): match the picker's requests with regexps, and sign in on a secured estate
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.
|
||
|
|
677b03eddf |
test(deploy): bind the picker in the Viya order spec, and drop fixed waits
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. |
||
|
|
133092502c |
fix(deploy): unclamp the folder picker row and probe in the chosen context
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. |
||
|
|
1a9f59478b |
fix(mocks): resolve the audit table once, record lower-case md5, and report a failed audit write
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. |
||
|
|
5ecc7bebec |
fix(mocks): fold the audit window, and append MPE_AUDIT under its lock
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/). |
||
|
|
1a60c689f4 |
feat(mocks): restore a load, and record the diffs it needs
`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. |
||
|
|
67d9cfd1bd |
fix(mocks): 0 is a closed TX_TO, a libref boundary, and the union of the row keys
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. |
||
|
|
e53c50d4e2 |
fix(mocks): one sasDatetimeNow, a documented TX_TO=0, and a shaped fallback export
Three review findings on this branch. - dcMockUtils defined sasDatetimeNow twice: once beside isCurrentRow with the epoch written inline and once in the mp_lockanytable section using SAS_EPOCH_MS. The second is the one the file already had; the duplicate is gone, so the epoch has one expression. - isCurrentRow treated TX_TO=0 as current without saying why, and reached that through Number(null) === 0. Both conventions are real: makedata writes 31DEC9999 for a row that was never superseded, and the fixtures in control-tables.test.js use 0 for the same thing. The clause stays and the comment now says which convention is which, plus that a missing, null or blank TX_TO (a table with no version column) is current too. Reading the column moved into isCurrentOn, because Array.filter passes the index as a second argument and would have handed a `column` parameter the row number. - admin/exportconfig used the schema only when it existed: with no columns in the table file it fell back to Object.keys(rows[0]), which exported the TX_/VER_/SELECTBOX_RK columns the spec drops and quoted every number. The fallback now applies the same drop prefixes and infers numeric typing from the rows, so a schema-less estate produces the same shape as a seeded one. A test writes a table file with rows and no columns and asserts both. All 46 mock tests pass (node --test sas/mocks/test/). |
||
|
|
00d37577f7 |
ci(build): restore the Angular cache after postinstall, where it survives
postinstall runs scripts/strip-clr-base64-fonts.mjs, which edits Clarity's CSS
inside node_modules and then deletes client/.angular/cache - because the
builder treats node_modules as immutable and would otherwise serve styles built
from the unstripped file. Measured on the Angular 20 builder: a marker
@font-face injected into clr-ui.min.css was absent from a warm build's CSS
(28s) and present in a cold one (88s).
The restore used to run before the install, so that clear deleted the entry
before anything could use it - run
|
||
|
|
a1615ee8a7 |
feat(mocks): add the control-table services the screens call
Three services the Angular app calls had no mock at all, so on the mock estate the Excel-map screen, the system screen's refresh and the config export either failed or showed nothing: - editors/getxlmaps - the rules and info of one map. Reads MPE_XLMAP_RULES and MPE_XLMAP_INFO for the current rows of that XLMAP_ID, orders by sheet and range id, and coalesces the target to DC_JSLIB.MPE_XLMAP_DATA - the same five columns, order and fallback getxlmaps.sas uses. - admin/refreshcatalog - a libref is optional and must be well formed (Invalid libref otherwise), and the reply is sasparams with the completion message the screen displays. A mock estate has no catalogue to rewrite, which the service says rather than pretending. - admin/exportconfig - a member per non-empty table, named <libref>.<TABLE>.csv, with the per-table treatment exportconfig.sas applies: TX_* dropped, a version filter, the `where also` exclusions, and the control-library name rewritten to the target libref (including the DC_MACROS value and the LIBREF.TABLE of a map target). Zipped as DCBACKUP.zip. The version test and the control-table reader were local to startupservice; they now live in dcMockUtils (isCurrentRow, sasDatetimeNow, and makeControlTableLoader for controlTable), so the rule that decides a row is current exists once. The harness gains the session's stpsrv_header.txt and _SASJS_USERNAME, so a service that streams a file can be exercised: exportconfig is asserted both ways - the zip where the runtime has Buffer, the content listing where it does not. Tests: 45 pass across the six mock suites, 8 of them new. With the three services removed, all 8 new cases fail. |
||
|
|
ce8c41c5db |
ci(build): drop the Angular cache step, which bought nothing across runs
Run
|
||
|
|
c732d7593c |
ci(build): restore the caches after the secret scan
The gitleaks step scans the working tree with --no-git, and the Angular cache
holds webpack/babel JSON full of `{ token: ... }` fragments. Restoring it
before the scan put those files in the tree and the gate failed on run 3174
with a wall of generic-api-key findings in
client/.angular/cache/20.3.41/babel-webpack/*.json.
Both cache steps now run immediately after the scan. The npm download cache
only helps the client's npm ci, which is after that point anyway; the Angular
cache only helps the tests and the build, which are later still.
|
||
|
|
f88e053887 |
ci(build): state the cache invariant in the step comments, not one run's numbers
The comments cited run 3170 and a timing measured on one 4 vCPU box. Both go stale and read as a guarantee about a particular runner. They now state the invariant the workflow relies on: an unreachable cache server degrades restore and save to a warning, so the job stays green. The timing figures stay in the PR description, where they are evidence for this change rather than a promise the workflow makes. |
||
|
|
768755dd37 |
ci(build): namespace the cache keys by base branch
act_runner's cache handler scopes entries to the repository (each entry is stamped with its repo and checked on read), not to a ref, so a pull request into one branch could seed the entry another branch's build restores. The base branch is now part of both keys and both restore-key prefixes. A save against an unreachable cache server is already a warning, not a failure: run 3170 landed on runner4.4gl.io and logged four '::warning::Failed to save: reserveCache failed: Request timeout' lines with the job completing green. |
||
|
|
5f8f668c66 |
ci(build): keep Angular's disk cache in CI so the cache step has something to save
The first run of these cache steps showed the npm entry saving (234 MB) and the Angular one reporting 'Path Validation Error: Path(s) specified in the action for caching do(es) not exist'. Angular keeps its persistent cache in memory when CI is set, so no .angular/cache directory existed to upload; the disk cache is only used outside CI by default. client/angular.json now sets cli.cache.environment to "all", which is the supported switch for that. Verified locally with CI=true: after removing .angular/cache, a build recreates it (396 MB). |
||
|
|
9d30e1855f |
ci(build): restore the npm downloads and the Angular build cache
The runner's cache server is reachable from job containers on two of the three boxes now, so these are the first cache entries in this repo that can actually come back. Probe evidence, four dispatches of a throwaway two-job workflow: - save on runner.4gl.io, restore on runner2.4gl.io: miss (each act_runner runs its own cache server, so entries belong to the box that saved them) - save and restore on runner2.4gl.io: cache-hit=true, the stamp came back - save on runner4.4gl.io: "Failed to save: reserveCache failed: Request timeout" - that box still runs the old runner config Two caches, both keyed on client/package-lock.json so a dependency change invalidates them, with prefix restore-keys so the previous entry still lands: - ~/.npm, for the client's npm ci (81s cold, measured on run 3132) - client/.angular/cache, for the production build (109s cold, 118s -> 33s with a warm cache on a 4 vCPU box) and the two Karma runs, which share it Per run the arithmetic while runner4 is still on the old config is about 1/3 x (+80s: two restores and two saves timing out at 20s each) plus 2/3 x (-100s), so it is a net improvement already and a clear one once the third box is deployed. On an old box both steps only warn and the job stays green. actions/cache does not save a second entry for an existing key, so the Angular cache refreshes when the lockfile changes; the v1- prefix is there to force a reset by hand if the win decays. |
||
|
|
5f338ff437 |
fix(deploy): match the documented SYSSCPL set and stop guessing at the cap
Review findings on #471. The platform rule now checks the documented tokens exactly (['WIN', 'DNTHOST']) instead of their first letters, so an unlisted token beginning with W or D no longer silently gets the drive path back. brDirList raised TRUNCATED whenever it saw rows.length >= maxobs, which included the check made after the last permitted row was added: a listing of exactly maxobs folders with a file sorting after them reported itself truncated although nothing was refused. The flag is now set only where a row is actually refused, and the early exit remains the walk bound. Two new cases pin the shape the picker depends on: a listing of exactly MAXOBS entries is TRUNCATED='N', and a DEPTH=2 browse numbers children at 1 and grandchildren at 2 with no LEVEL 0 row. Both, plus the existing parent-level and write-probe cases, fail against release/7.22's service. Tests: 37 pass across the five mock suites, client spec 9/9. |
||
|
|
ecc87021c3 |
fix(build): publish the changed-path decision and pin the matcher with tests
Two review findings on the first two commits. The two jobs no longer decide separately. Build-and-ng-test now publishes `client` as a job output and Build-and-test-development consumes it instead of re-diffing the base branch: a base commit landing between the two decisions could make the Cypress job expect an artifact the build job had already skipped, and the failure would read as a missing artifact. An empty output (the build job could not decide) still runs everything. The path list is now a PATHS variable the script can be asked about directly (`--match` with a file list on stdin), and tests/cypress-changes.test.sh pins one case per alternative plus the negatives (a sas/ service, docs, the Lighthouse workflow, a "clientx/" directory, the sas lockfile). The build job runs the test, so dropping an alternative is red rather than a suite that silently stops covering a class of change. Checked the test fails when the pattern is mutated. |
||
|
|
48df795202 |
fix(build): treat a sas/package.json bump as a Cypress input
The Cypress job installs the CLI from the version sas/package.json pins (`CLI_VERSION=$(node -p "...dependencies['@sasjs/cli']")` in "Deploy mocked services"), so bumping that pin changes what the suite runs without touching anything under sas/mocks/. A pull request that only bumped it would have been skipped by both jobs. The sas lockfile stays out of the set: the job installs that CLI globally by version string, so the lockfile cannot change what it resolves. |
||
|
|
fb38309e98 |
fix(deploy): make the folder picker work on both estates
Three defects behind staging's "Invalid parent" and the picker that could not list a subfolder. Found by running the real mpe_dirlist on sas.4gl.io, not by reading it. 1. The browser host answers SYSSCPL "browser". The client looked for the first letter of a unix token (L/H/A/S) and fell through to the Windows default for anything else, so the deploy screen pre-filled C:\DataController on a POSIX runtime, which the server then refused as an invalid parent. Windows is now recognised by its own tokens (WIN, DNTHOST); anything else, including a value the list has never seen, gets the POSIX path. 2. mpe_dirlist emitted a non-blank parent's children at LEVEL 0, because mp_dirlist numbers a folder's contents with the folder's own level. The picker keeps LEVEL>0 for a non-root listing, so every row was discarded and a subfolder could never be browsed. The children are shifted to start at 1; a blank parent still lists the platform roots at 0. Verified on the estate: /tmp at DEPTH=1 returns levels [1], a blank parent returns [0, 1] (the root, then its children). 3. The mock's write probe generated one name and wrote it, truncating a file that was already called that; mpe_writecheck tries up to ten names with fileexist()=0 first. The mock now does the same and reports 'No free name for the write probe' when all ten are taken. The mock's DIRLIST now matches the estate's shape too: children at LEVEL 1, roots at 0 followed by their children, and the cap stops the walk at MAXOBS rather than slicing afterwards. Tests: 35 pass across the five mock suites (makedata-actions 15, two of them new: the probe collision and a folder that cannot be written to), sasjs lint clean, client spec 8/8 with the new platform cases and 2 failing without the fix. |
||
|
|
6830d873e5 |
ci(build): skip the cold build when the Cypress job will not run, and stop installing Cypress in the lint job
The build job spent 402s on every pull request (run 3132, release/7.22). Two of its steps were work nobody consumed: - `npx cypress install` (11s, measured 17:07:05-17:07:16) downloaded and unzipped the Cypress binary into a job that only runs Karma, eslint, the licence checker, the mock service tests and `ng build`. Cypress runs in Build-and-test-development, which installs its own binary. - The production build (109s) and the client-dist upload exist only to feed the Cypress job, which already skips pull requests that touch nothing it exercises. On those pull requests the artifact was uploaded and never downloaded. The changed-path decision now lives in .gitea/scripts/cypress-changes.sh and both jobs call it, so the build job's gate cannot drift from the Cypress job's skip. The script keeps the existing fail-open rule: anything it cannot decide (no base ref, failed fetch, failed diff) writes no output, and every caller gates on `!= 'false'`, so the work still runs. |
||
|
|
3572d56cfb |
fix(browse): accept legal folder names, cap the walk, and lock the read
Three findings from the review of the release head. A quote, a comma and both parentheses were refused as parent/path characters, which are legal on both platforms - "C:\Program Files (x86)", "O'Brien", "/data/sas, archive" - so the deploy screen refused to browse or write-check a folder it could have been deployed into. writecheck never turns the value into macro text (it is read back with symget and handed to filename()), and dirlist now passes it to mp_dirlist through %superq, so no character in it is read as macro text; both keep the length, control character and ISPATH checks and drop the character refusals. brCheckPath in the mock agrees, so the two estates answer alike, with a test for a folder named "Program Files (x86), O'Brien". brDirList walked every level and sliced to MAXOBS afterwards, so browsing /usr or /proc at depth 3 still stat'd everything below it - the cost the cap exists to avoid. It stops adding rows once the cap is passed, as mpe_dirlist does. rejection.js and postdata.js loaded MPE_REVIEW before taking the lock and released it straight after the write, so two requests could both read before either locked and the second save dropped the first row. The lock now covers the read-modify-write it is there for, released in a finally. |
||
|
|
5382351c25 |
chore(deps): require @sasjs/core 7.1.8
7.1.7 closes the outstatus step with run, stops truncating the flow table's state, and strips the response envelope before the JSON libname; 7.1.8 lets mp_replace delete, which is how the envelope is stripped. Without them a Viya run cannot read an aborted child's result: ten of Data Controller's SAS suites report no result at all. |
||
|
|
14de2b8a41 |
docs(mocks): say what the sas9 variant does and does not reproduce
The payloads are the SAS 9 recordings, separators included - the front end splits metadata URIs on the backslash, so a value that arrives without one exercises the wrong branch. The envelope is not SAS 9: _PROGRAM, SYSUSERID, SYSVLONG and SYSDATE/SYSTIME come from whatever runtime serves the request, so this variant must not be read for SAS 9 envelope values. |
||
|
|
331460ad25 |
refactor(mocks): load the webout guard as a module, not by eval
The guard is the tree's one shared rule, but each of the three services that needs it repeated the path derivation and then eval'd a file read out of the SASjs Drive at run time - which is what the file's own header said this tree does not need. weboutGuard.js now exports the function and the services require it by absolute path: one copy of the function, no eval, and a load failure is a require error naming the file rather than a silent no-op. Being a module also means the guard has to carry its own dependencies: as an eval'd snippet `fs` came from the program's scope, and the first run as a required module failed with "fs is not defined". It now requires fs itself. dcMockUtils.requireMemberMetadata remains the source of truth for the rule and both files say so, since a site may run both trees and an operator reads whichever one threw. |
||
|
|
349a2f314c |
fix(mocks): read hook columns in either case, and guard duplicates first
Two review findings on the approval hooks. mpeRunhook read only the lower-case column name, while a row written through the editor carries the schema's upper-case names (PRE_APPROVE_HOOK) and the buskey/loadtype reads beside it already try both. A hook registered that way resolved to nothing, so the approval ran without it and said nothing. postdata ran the pre-approve hook before the duplicate-approval guard. postdata.sas checks prev_upload_check first and returns ALREADY_UPDATED without reaching the hook, so a second approval from the same user was aborting with the hook's message instead. The guard is now computed before the hook and the hook is skipped when it fires. The hook still runs for SHOW_DIFFS, which is what the SAS service does too - the macro call sits outside both action branches. Both cases are pinned by a test, and each fails when its fix is removed. |
||
|
|
f0208bb0b0 |
fix(mocks): run approval hooks, as mpe_runhook does
postdata.sas runs PRE_APPROVE_HOOK before the diff and before any write, and POST_APPROVE_HOOK after the load is applied. The mock ran neither, so an approve hook registered on MPE_TABLES did nothing on the browser-hosted estate - while the post-edit hooks (which live in stagedata/loadfile, mirroring mpe_loader) did run. mpe_runhook(tableReg, hookvar) in dcMockUtils picks the hook out of the MPE_TABLES row by column name and returns its source; the service eval()s it in its own scope, which is the JS equivalent of the macro's %include sharing the caller's macro variables. The three call sites that already resolved a hook by hand (getdata pre_edit, stagedata/loadfile post_edit) now go through it, so the next hook column costs one line. postdata runs pre-approve before the staged data is diffed and aborts on failure (nothing is written), and post-approve after the base table and control tables are updated - so its failure is reported but cannot undo the load, which is what postdata.sas does with its SYSCC= response. A hook refuses by throwing or by setting hookErrmsg, the convention the post-edit hooks already use. sas/mocks/test/hooks.test.js covers the ordering: pre-approve refuses with the base table untouched and the submission still SUBMITTED; post-approve aborts with the row already in the base table and MPE_SUBMIT APPROVED; and one hook proves it ran by writing a file, not by the service reporting success. 30 tests pass; 4 of the 5 hook cases fail against the base service. |
||
|
|
ca602082ff |
fix(mocks): abort like mp_abort, lock like mp_lockanytable, derive saslibs
Three more places where the mock answered differently from the SAS service. mp_abort (dcMockUtils): every mock refusal is now a sasjsAbort table (MSG + MAC) written where the macro writes it, so the client shows the abort modal and rejects the promise. A thrown error had become an HTTP 500 "Job execution failed" - the browser host discards the webout when a program throws - which is unreadable to the user and hides the message in a stack. mpAbort also stops the program, so nothing is written after a refusal; each service that can abort catches that stop at its top level. stagedata, loadfile, getauditfile and makedata now go through it, and the hand-rolled sasjsAbort blocks are gone. mp_lockanytable (dcMockUtils): a lock is a row in MPE_LOCKANYTABLE marked LOCKED carrying the holder's job id, user and reference, exactly as the macro records it, and a lock held by another session refuses the write with the macro's message. postdata and rejection now lock MPE_REVIEW and MPE_SUBMIT around each write, which is where postdata.sas and rejection.sas take them. The macro's retry loop is collapsed to a single attempt: the runtime is single threaded, so nothing can release a lock while the request waits. startupservice: saslibs is the distinct LIBREF of the permitted datasets, which is what "create table saslibs as select distinct libref from &syslast" produces - it was a four-column row describing the control library, with fields the client does not read. Tests: abort.test.js and locks.test.js join the harness suites; 25 pass, and 9 of the 13 relevant cases fail against the base services. |
||
|
|
87fd46c825 |
fix(mocks): derive startupservice's maps and timezone instead of hardcoding
Every screen reads public/startupservice, so a divergence there is a
divergence in the whole app. Three of its answers were fixed values:
xlmaps a hardcoded list of three maps, so editing MPE_XLMAP_RULES
or MPE_XLMAP_INFO changed nothing on the site
sasdatasets included the map target dataset, which the SAS service
deletes - the demo offered MPE_XLMAP_DATA as an editable
table while SAS refuses it
timezone TIMEZONE and SYSTIMEZONEIDENT pinned to Europe/London and
the offset to +0100, so a GMT session reported an hour of
DST it did not have
They now come from the same sources the SAS service reads: the current
MPE_XLMAP_RULES rows joined to MPE_XLMAP_INFO (coalescec to
<mpelib>.MPE_XLMAP_DATA), the target datasets removed from sasdatasets,
and the session zone and current offset taken from the runtime, with
TIMEZONE left blank as an unset option is.
The shared harness (sas/mocks/test/harness.js) makes the service tests
one-liners: it builds a temporary estate with every service on its drive
and runs a service file the way the runtime does, webout() included.
sas/mocks/test/startupservice.test.js covers the joins, the target
removal, closed rules and the offset.
|
||
|
|
e1c3c6e872 |
fix(mocks): answer the deploy screen's DIRLIST and WRITECHECK actions
Staging now opens on the deploy screen, and its folder picker asks
makedata for a listing. The mock ignored ACTION entirely: every request
seeded the control library, deleted makedata from the drive and replied
with the HTML success page. So the first BROWSE deployed the site and
closed the wizard the visitor was standing on, behind an "invalid Json
string" error from the adapter, and VALIDATE and SUBMIT then answered
500 "Program not found in drive".
The mock now dispatches on ACTION the way the SAS service does:
DIRLIST the folders below PARENT, one level at a time, with the
dirinfo/dirlist shape mpe_dirlist emits
WRITECHECK the checkwrite row mpe_writecheck emits, probed by creating
and removing a file
MAKEDATA the existing seeding path, unchanged
ACTION is required and refused when absent or unknown, and the two browse
actions are refused once the settings file exists, mirroring browse_gate.
Both input routes are accepted - the screen's fromjs table and a CLI
seed's SASControlTable - because %webout(FETCH) does not care which one
arrived; deploy/makedata.json now carries ACTION=MAKEDATA as the real
service requires.
sas/mocks/test/makedata-actions.test.js runs the service file itself
against a temporary estate and is wired into the build workflow.
|
||
|
|
744c89b919 |
fix(mocks): separate a body read failure from a missing showmeta floor
Three review follow-ups on the guard the sas9 tree loads. - A body that cannot be read is now its own error, naming the path and the filesystem message. Reporting it as missing metadata sent the reader to a @sasjs/utils reinstall that cannot fix a path. - One statement of the dependency fact: showmeta arrived in @sasjs/utils 4.6.0 and @sasjs/server declares that floor from 3.12.3 (3.12.0 and 3.12.2 declare ^4.4.0, checked with npm view), so the message names the server version to install instead of "reinstall the server". - CONTRIBUTING lists _program and weboutPath among the predeclared globals, which the guarded services use to locate the helper. The sasjs tree's copy in dcMockUtils.js had the same two defects, so both now say one thing rather than one tree being clearer than the other. |
||
|
|
7be3f84073 |
feat(mocks): emit the sas9 services through webout()
The sasjs tree has emitted its responses through the runtime's webout() since #461; the SAS 9 tree under sas/mocks/sas9/ still assigned every captured payload to _webout, so its envelope was frozen at the 2022 recording and its values carried the separators a template literal eats. All 35 services now call webout('OPEN') / webout('OBJ'|'ARR', name, rows) / webout('CLOSE'), with the three services that carry column metadata passing { showmeta } and loading the guard from the new weboutGuard.js. The payloads come from the captures read as JS produces them, with the backslashes a template literal silently drops restored: 121 separator values across 14 services - OMS URIs (OMSOBJ:<Class>\<repo>.<id>), BUILTIN\Administrators, SAS\sasdemo and D:\... / C:\... paths. |
||
|
|
9c3d067e0e |
ci(sites): publish staging unconfigured so it opens on the deploy screen
Staging is the preview of the next release, and what it should preview first is the deployment itself. DC routes to /deploy while makedata is still on the drive (at <appLoc>/services/admin) and stops doing so once the wizard has run it and it has removed itself - so a drive that carries the services and the licence but no control library opens on the deploy screen, and the visitor deploys the control library from the wizard into their own IndexedDB drive. The seeding step is now gated on a tag: only the demo (which a visitor should be able to use straight away) runs makedata before the drive is captured. The staging path gets a guard of its own, because a seed that leaked into it would only be visible as a site that opens on the home screen: makedata must be on the drive and no mpe_tables.json may be there. CONTRIBUTING updated: the two sites now ship different drives from the same build. |
||
|
|
7d9d466c3e |
fix(mocks): keep the OMS and path separators the captures lost
The captures wrote values like OMSOBJ:SASLibrary\A59LNVZG.B500000K inside a
template literal, where \A is an escape, so the separator never reached the
client: it served OMSOBJ:SASLibraryA59LNVZG.B500000K. The same happened to
D:\MyDemo\Sankey and BUILTIN\Administrators. Converting the service to webout()
faithfully preserved the corrupted value - and the metadetails ASSOCURI, the one
field fixed by hand at conversion time, was left as the only member that kept its
separator, which is the inconsistency the review named.
Seventeen values corrected across five services:
lineage/fetchtablelineage info[0].LIBURI 1
lineage/getmetacols metacols[].COLURI 11
lineage/getmetatables PATH_SCHEMA, TABLEURI 2
public/refreshlibinfo libinfo[0].OWNERS 1
usernav/usermembersbyrole sasgroups[].URIMEM,
sasmembers[].URIMEM 2
Verified by deploying both trees on one server and comparing the parsed
responses: those seventeen fields are the only differences, and every other
member of all thirteen converted services is unchanged (metadetails' two
ASSOCURI, already carrying its separator, still differ from the capture for the
same reason).
Anything that splits a URI or a Windows path on the separator, or displays
OWNERS or PATH_SCHEMA, now sees the value the SAS metadata server returns.
|
||
|
|
246cc39ca0 |
ci: run the mock estate on 3.14.0
3.14.0 ships @sasjs/lint 5.0.0, so the studio reports OS commands, and it is the release the estate runs. It still declares @sasjs/utils ^4.7.0, so the envelope formats from utils 4.8 (SYSCC as text, DATE7, TIME5, E8601DT26.6) are not yet guaranteed by the floor - a fresh install resolves 4.8.0, a cached tree need not. The comment says so, and the pin moves again when the floor lands. Verified on a local 3.14.0 install (clean, resolving utils 4.8.0): the mock estate's startupservice returns SYSDATE 04NOV26, SYSTIME 12:44, SYSCC "0" (text), END_DTTM 2026-11-04T12:44:14.954000 (no Z). With that estate seeded, and against a fresh app location, licensing.cy.ts passes 10/10 and metanav.cy.ts 1/1 on Chromium. |
||
|
|
1f54fad3fe |
feat(mocks): emit the lineage, usernav and libinfo responses through webout()
The other nine services that answer with a captured response also assigned it to _webout, envelope and all. They now emit their members through the runtime's webout(): lineage/fetchtablelineage info, finalfinal, flatdata, idlookup lineage/getmetacols metacols 11 rows lineage/getmetatables metatables 1 row usernav/usermembers users 2 rows usernav/usermembersbygroup sasmembers 2 rows usernav/usergroupsbymember groups 1 row usernav/usermembersbyrole sasgroups 1 row, sasmembers 1 row usernav/userroles roles 46 rows public/refreshlibinfo libinfo 1 row Each payload was taken from the response the old service serves, then both trees were deployed on one server (/Public/app/dc and /Public/app/dcnew) and compared as parsed JSON with the envelope keys excluded: all nine are equal row for row, including the escaped values (the OMS URIs keep their backslash, and refreshlibinfo keeps the quoted paths). The envelope is the runtime's now, so SYSCC is numeric 0 rather than the string "0", SYSDATE/SYSTIME are today's, SYSSITE is reported (these captures carried a lower-cased syssite, which the client never read), and the SAS-only _METAUSER/_METAPERSON are gone. lineage/fetchlineage is deliberately left alone: its captured body is not valid JSON today (bare quotes in the graphviz labels, raw newlines inside the STRING values, and a lost backslash), so there is no payload to preserve and the intended text needs a decision rather than a rewrite. Tracked separately. |
||
|
|
e1d877f21d |
feat(mocks): emit the metanav responses through webout()
The four metanav services assigned a captured 2022 response to _webout, envelope
and all, instead of using the runtime's webout() like every other service. The
members are unchanged:
metarepos outrepos 2 rows
metaobjects objects 1 row
metatypes types 71 rows
metadetails attributes 12 rows
associations 2 rows
Both trees were deployed on one server, at /Public/app/dc and /Public/app/dcnew,
and the responses compared as parsed JSON with the envelope keys excluded:
outrepos, objects and types are equal row for row.
metadetails changes in one place. The capture wrote the OMS URI as
OMSOBJ:Property\A59..., but \A is an escape in a JavaScript template literal,
so the backslash was consumed and the client received OMSOBJ:PropertyA59... -
a URI that cannot resolve. The member now carries the backslash the SAS
metadata server returns.
The envelope comes from the runtime now, which also drops three artefacts of
the recording: SYSCC "4" on metarepos (an error code from the captured day,
which the client reads as a SAS error), SYSDATE/SYSTIME frozen at 26SEP22, and
the SAS-only _METAUSER/_METAPERSON.
|
||
|
|
b1eab98200 |
fix(build): build the mock sites with a CLI whose runtime provides webout()
The mock services call webout(), as the server's runtime does. The browser mock runtime that @sasjs/cli ships is what provides it there, and the 6.5.0 this repo was pinned to does not: its runtime-core.js has no reference to the webout prelude, and the build writes no webout-prelude.json for it to load. Every service that calls webout() therefore throws "ReferenceError: webout is not defined" in a published mock site while the build itself reports success. 6.9.0 is the first release that ships the prelude; this pins 6.10.0, the current release. The transitive @sasjs/adapter and @sasjs/utils entries move with it. |
||
|
|
09e9f192c5 |
fix(mocks): read the metadata member from the response, not the function's arity
The guard that keeps a runtime without webout()'s options argument from serving a
thinner response tested webout.length >= 4. Function.length stops counting at the
first default or rest parameter, so a runtime that does carry the option but
declares it `options = {}` or gathers the tail with `...args` reports three, and
every service that calls the guard refuses to start on a working runtime.
It now asserts the outcome instead: the member the option produces is looked for
in the response body the program is writing. `$sasdata`, `$viewdata`, `$vals` are
the members the client reads for column formats and labels, and their presence is
the thing that actually has to hold.
Where it runs matters too, and the first attempt got that wrong: called after
webout('CLOSE') the throw leaves a complete body behind, and the server serves it
- the caller sees a normal response with SYSCC 0 and no metadata, which is exactly
the silent failure this exists to prevent. It is called before the envelope is
closed, so an unhonoured option fails the run.
|
||
|
|
aa295b384a | Merge remote-tracking branch 'origin/release/7.22' into fix/449-va-ddc-host-order-free | ||
|
|
ba32d1fa46 |
ci: run the mock estate on the server that reports SYSSITE
@sasjs/utils 4.7 makes webout() emit SYSSITE from a configured site id, defaulting to 123, where %webout takes &syssite. Until 3.13.0 the prelude wrote an empty string, and dc's client routes an expired licence key and a wrong-organisation key only when startup_site_id is truthy - so on the mock estate both pages were unreachable and licensing.cy.ts failed three cases. |
||
|
|
f0e3117540 |
ci(workflows): hand values to the shell as data, not as script text
Three values reached `run:` blocks through `${{ }}`, so the runner pasted them
into the script before the shell ever saw it - which makes the value part of the
code rather than an argument to it.
- `BASE="${{ github.base_ref }}"` (build.yaml, lighthouse.yaml) - a ref name in
the script.
- `echo ${{ secrets.SHEET_PWD }} | gpg --passphrase-fd 0` (all four workflows) -
a passphrase in the script. A quote in it ends the quoting, and a command
substitution in it runs.
- two `replace-in-files --replacement=...${{ secrets.CYPRESS_* }}...` lines
(build.yaml, release.yaml) - the same, inside a replacement string.
Each now arrives as a step `env:` value and is expanded as `"$VAR"`, so the shell
only ever sees data. Checked the replacement lines side by side with the old form:
identical for an ordinary value, and the new form keeps a value containing a space
intact where the old one word-splits it, and survives a value containing a single
quote where the old one is a shell syntax error.
The passphrase pipe also moves from `echo` to `printf '%s\n'`, so the byte the
decryption sees does not depend on how the shell's `echo` treats backslashes.
|
||
|
|
1225eb122a |
test(embed): fail on a missing FILTER_HASH before reading the store back
Review finding: when every body is refused by carriesClause, or the response
carries no FILTER_HASH, capture.hash stays empty - and storedClauseFor('')
then spends its 60 retries before reporting "no clause for <var> under",
which names neither the refused body nor the missing hash.
The hash is now asserted non-empty before the read, with the case's own
message, so a broken delivery fails where it happened.
The call count stays where it was - after the data load - because moving it
ahead of the read would check it before the app has issued the second POST
that the count exists to catch.
|
||
|
|
4a87f8f6ce | chore: merge release/7.22 into fix/449-va-ddc-host-order-free | ||
|
|
4b42d1aaf1 |
test(embed): count deliveries on the app's own signal, not on a clock
The duplicate check waited a fixed 250ms after reading the stored row, which is wrong in both directions: on a slow runner the window can elapse before the first response callback has incremented the counter, and a second POST landing after it passes with two deliveries. The app's sequence for an apply is validatefilter, then the data load for the filtered route, so the load is the completion signal: the count is asserted after it, and nothing depends on how fast the machine is. A delivery issued later than that is a second apply, not a duplicate of this one. Also corrects the ordering paragraph in cypress-specs.sh: it still said specs are appended, in a file that now places va-ddc-host ahead of metanav. va-ddc-host does not depend on the estate being unlicensed either - it passed 3/3 immediately after metanav planted a key in the full CI run - so the bullet says what is true and metanav stays the only spec with a fixed place. |
||
|
|
1c4d2f65cc |
test(embed): match the clause's field wherever the body wraps it
Review finding: the matcher only accepted the variable name at the start of the body or after a comma or newline, so a delivery wrapped any other way was not counted - and the case then failed at the call-count assertion, reading as "the app never called validatefilter". Reproduced against the previous implementation: a urlencoded form value, a form value that starts with the name, a JSON object keyed on the column and a JSON string value holding the row all matched nothing. The body is now expanded into the candidate strings it can carry - the raw text, the percent-decoded text, each form field's value, and every string leaf of a JSON body - and the name has to be a whole CSV field of one of them. Field equality rather than delimiters means a quoted literal, a longer name, an SQL fragment or another variable's name is still refused, and there is no regular expression over user text to get wrong. Also restores the ordering note in cypress-specs.sh: va-ddc-host asserts nothing about licensing, so it runs before metanav, the one spec that plants a licence key. |
||
|
|
75d32d9838 |
ci(build): record why the mock estate's server pin is 3.12.3
The pin moved to 3.12.3 with the webout() change but the comment above it still
named 3.12.2 as the current release - true when it was written, wrong under the
pin it now sits on. The comment also only carried the /ddc-host reason, which
does not explain this bump.
3.12.3 is the first server release declaring @sasjs/utils ^4.6.0, and that is
what gives webout() the options argument getdata, viewdata and getcolvals pass as
{ showmeta }. Say so where the constant is, so the next person to move it knows
the metadata members depend on it.
|
||
|
|
ec92eb4eee |
ci: run the mock estate on the server that carries the utils floor
The services in this branch pass { showmeta } to webout(), which is
@sasjs/utils 4.6 and up. sasjs/server 3.12.3 is the first release whose
package.json asks for ^4.6.0 (a clean install resolves 4.6.1), so the pin
moves from 3.12.2 to 3.12.3 in every workflow that starts the mock estate.
Verified against the published package rather than the release notes:
`npm view @sasjs/server@3.12.3 dependencies.@sasjs/utils` answers ^4.6.0, and
`npm install @sasjs/server@3.12.3` in an empty directory installs
@sasjs/utils 4.6.1, whose webout prelude carries showmeta.
|
||
|
|
36a2bff086 |
ci(workflows): install the tooling at the versions this repo chose
Review of the release diff: `npm install -g @sasjs/cli` and
`replace-in-files-cli` were unpinned, so CI ran whatever release was current
and a bad one would reach the pipeline unreviewed - next to a SASjs server
that is pinned to an exact version.
The CLI now comes from the version sas/package.json pins, which is the shape
docs.yaml and release-assets.yaml already use, so there is one source for it.
replace-in-files-cli, pm2 and wait-on are pinned to the versions in use
(4.0.0, 7.1.2, 9.5.1).
Both node expressions are checked from the directories they run in:
`require('./sas/package.json')` at the root and `require('../../package.json')`
in sas/mocks/sasjs both answer 6.5.0.
|
||
|
|
482435f318 |
fix(mocks): refuse a runtime whose webout() cannot carry column metadata
The three services that pass { showmeta } - getdata, viewdata, getcolvals - rely
on webout()'s fourth argument, which arrived in @sasjs/utils 4.6.0. @sasjs/server
3.12 declares ^4.4.0, so an install can resolve 4.5.x, and there the argument is
dropped silently: the response still looks complete but $sasdata, $viewdata and
$vals are absent, so the client's format and label paths stop being exercised
while the mock estate reports success.
requireWeboutOptions() checks that the generated webout() takes an options
argument (arity, because the option names live outside the callable function) and
throws naming the member, the cause and both fixes. A 4.6 runtime is unchanged.
|
||
|
|
9c53e5b90d |
test(embed): match the clause's field, and wait for the response as long as the page takes
The matcher tested the variable as a substring of the whole request body, so a
RAW_VALUE, an SQL fragment or another variable whose name contains it was counted
as another delivery of this clause and failed the count. It now requires the name
between its CSV delimiters, which is the filterquery VARIABLE_NM field; a quoted
literal cannot match, because its delimiters are quotes.
cy.wait('@validatefilter') used the 5s default, where the polling loop it replaced
tolerated 15s, and the message only arrives once the framed page has rendered.
It now waits for longerCommandTimeout, as the rest of this spec does.
The capture assertion also carries the tail of the host page's log in its message:
the failure reads "expected -1 to be above -1" otherwise, and the log is what says
whether the app never exposed its buffer or the frame never loaded.
|
||
|
|
90784b342f |
fix(va-host): deliver the load-mount message once, and document report.json from the file
Two review findings: - The load handler re-posted pendingPayload on every load event of the frame, so an in-app navigation or a self-reload posted a filter that belonged to the mount which had already delivered it, and logged it as that mount's. The handler now takes the payload (clearing pendingPayload and pendingTiming) before posting, so a later load has nothing to send. - The README illustrated report.json with one parameter while the committed file carries three, and the prose below reads as though one parameter were the shape. The example block is gone: the file is the example, and the keys are documented as a table with the parameter rules kept underneath. |
||
|
|
ce6bb5d4cd |
feat(mocks): build every JS stored program's response with webout()
The last eleven services still built the response envelope through dcMockUtils' webOutOpen/webOutObj/webOutClose. They now use the webout() the SASjs Server emits into every JS program, so dcMockUtils no longer emulates %webout at all - the helper existed only for these callers and is deleted with them. That needed the options object added to webout() in @sasjs/utils 4.6 (showmeta, maxobs), which is what a clean install of @sasjs/server@3.12.2 resolves to (^4.4.0 -> 4.6.1, checked). A server whose install still holds 4.4/4.5 refuses the option by name rather than ignoring it, so the failure is loud rather than a response that is quietly missing its column metadata. getdata, viewdata and getcolvals send their metadata as `webout('OBJ', name, rows, { showmeta })`, and every other call keeps its member name and rows unchanged. One call was positional all along: startupservice's xlmaps is an array of arrays, which the old helper serialised as-is and webout() refuses for OBJ. It is now webout('ARR', 'xlmaps', xlmaps) - caught by the comparison run below, not by the rewrite. |
||
|
|
6508ac7f5c | chore: merge release/7.22 into fix/449-va-ddc-host-order-free | ||
|
|
93af2dabe6 |
test(embed): match a parsed request body, and count duplicates after the read
Two gaps in the guard that decides which validatefilter response belongs to the case. A body Cypress parses into an object stringified to "[object Object]", so no call matched and the case failed on the count with nothing naming the body shape. The body is now serialised the way the response already was - a string is used as it is, anything else is JSON.stringify'd. The count was asserted immediately after the first response, which is before a second POST from the same apply could reach its own callback, so a duplicate read as one call. The count is now asserted after the stored row has been read back, with a settle that covers a second POST still in flight when the read returned. |
||
|
|
9cec292681 |
fix(va-host): post to the frame's own origin, and key the replay to the document
Two review findings on host.js: - postToFrame() used '*' as targetOrigin, so the filter clause went to whatever document occupied the frame. The frame is the app on report.server, so that origin is the only one that should receive it; the origin is now resolved once from report.server (new URL(...).origin) and a report.server that is not a URL logs a visible error instead of widening delivery. The page also says so when it is not on the frame's origin, because the capture probe cannot read across origins and that would otherwise look like a harness failure. - startEarlyReplay() accepted any __vaLastMessage once it had posted, and on a remount to the same URL the old document is still on screen carrying the buffer the previous mount captured. A capture now only counts once the document has been replaced (mountDocument), both in the interval and in the load-event check. Keying to the document rather than waiting for a load event keeps the first post where the early window is - the interval starts exactly as before. |
||
|
|
cde90830fb | chore(ci): record why the client-dist artefact stays on upload-artifact v3 | ||
|
|
05bd1e2e7f |
ci(lighthouse): cancel the check for a superseded pull request head
Same reasoning as build.yaml: the older head cannot be merged, and a ten-minute check queued ahead of the current one only delays it. |
||
|
|
7a2b01db84 |
ci(build): cancel the build for a superseded pull request head
Every push to a pull request queued another full run - lint, licence check, the mocked estate and the twenty-six minute suite - and nothing cancelled the run for the head it replaced. With two runners shared by every pull request in the repo, those stale runs are picked oldest-first and delay the head that can actually be merged: runs 3002, 3003 and 3005 were queued for heads that had already been superseded while the current one waited behind them. The group is the pull request, so a newer push cancels the run for the older head. Pushes that are not pull requests are not affected - the release workflow answers to its own group. |