Commit Graph
100 Commits
Author SHA1 Message Date
dc b301399b5c fix(mocks): viewdata sends a blank cell as a space, not the word undefined
Build / Build-and-ng-test (pull_request) Successful in 3m54s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m58s
Build / Build-and-test-development (pull_request) Canceled after 7m13s
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.
2026-11-07 00:45:54 +00:00
dc 3bb9fb52e3 fix(mocks): make getdata and viewlibs answer what the estate answers
Build / Build-and-ng-test (pull_request) Successful in 4m28s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m24s
Build / Build-and-test-development (pull_request) Canceled after 10m4s
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.
2026-11-07 00:31:16 +00:00
dc 49a9bbf022 feat(mocks): record getdata and viewlibs, and show what the mock gets wrong
Build / Build-and-ng-test (pull_request) Successful in 2m49s
Build / Build-and-test-development (pull_request) Successful in 12s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m15s
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.
2026-11-06 21:28:51 +00:00
dc 909580dbda docs(mocks): cite the fixture that actually hits the SOME_SHORTNUM rule
Build / Build-and-ng-test (pull_request) Successful in 3m56s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m57s
Build / Build-and-test-development (pull_request) Successful in 27m3s
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.
2026-11-06 20:41:40 +00:00
dc 038c40499b refactor(mocks): name the synthetic estate by target name, not a sasjsconfig key
Build / Build-and-ng-test (pull_request) Canceled after 2m53s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 2m50s
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.
2026-11-06 20:38:46 +00:00
dc 7815dcccb8 fix(mocks): drop only the MPE_X_TEST rule the excel fixtures break
Build / Build-and-ng-test (pull_request) Successful in 3m50s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m18s
Build / Build-and-test-development (pull_request) Successful in 27m4s
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.
2026-11-06 19:59:06 +00:00
dc dd2a777438 fix(mocks): restore the MPE_X_TEST fixtures and pin the replay's timezone
Build / Build-and-ng-test (pull_request) Successful in 3m54s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m1s
Build / Build-and-test-development (pull_request) Failing after 34m59s
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.
2026-11-06 18:41:13 +00:00
dc 94cd4912f9 fix(mocks): make the golden's claims evidence, not labels
Build / Build-and-ng-test (pull_request) Successful in 4m25s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m26s
Build / Build-and-test-development (pull_request) Failing after 49m36s
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.
2026-11-06 17:03:45 +00:00
dc a57557c60e fix(mocks): wire the replay into CI and close the review gaps
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m2s
Build / Build-and-test-development (pull_request) Canceled after 19m3s
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.
2026-11-06 16:40:43 +00:00
dc 6c831bb230 fix(mocks): verify TLS when recording, and project staging ids whole
Build / Build-and-ng-test (pull_request) Successful in 4m23s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m20s
Build / Build-and-test-development (pull_request) Canceled after 48m35s
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.
2026-11-06 15:47:38 +00:00
dc b98fde1c74 docs(mocks): record that the parity refresh is an ad-hoc agent job
Build / Build-and-ng-test (pull_request) Successful in 3m54s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m12s
Build / Build-and-test-development (pull_request) Canceled after 16m5s
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.
2026-11-06 15:18:13 +00:00
dc 6262843f3f feat(mocks): check the mock against recorded SAS responses
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 5m36s
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.
2026-11-06 15:08:26 +00:00
dc 6e0866d876 fix(mocks): give MPE_X_SEARCH its column metadata back
Build / Build-and-ng-test (pull_request) Successful in 4m22s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m13s
Build / Build-and-test-development (pull_request) Failing after 49m40s
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.
2026-11-06 14:42:56 +00:00
dc 0e0e79ef63 test(viewbox): match the configured viewbox by its full lib.dataset
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m1s
Build / Build-and-test-development (pull_request) Successful in 24m20s
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.
2026-11-06 14:33:28 +00:00
dc 112e8e4b8b fix(mocks): keep the derived rows makedata owns
Build / Build-and-ng-test (pull_request) Successful in 4m0s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m43s
Build / Build-and-test-development (pull_request) Canceled after 31m43s
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.
2026-11-06 14:02:33 +00:00
dc b5fe46931f test(viewbox): scope the picker lookup, escape the library name, outwait the re-select
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m7s
Build / Build-and-test-development (pull_request) Successful in 24m26s
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.
2026-11-06 14:00:48 +00:00
dc 5f89314aaf fix(mocks): answer the review on #487
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m13s
Build / Build-and-test-development (pull_request) Canceled after 22m0s
- 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.
2026-11-06 13:36:26 +00:00
dc a6cfb12579 refactor(mocks): build the control library makedata.sas builds
Build / Build-and-ng-test (pull_request) Successful in 4m26s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 4m56s
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.
2026-11-06 13:29:14 +00:00
dc acf44dae74 test(viewbox): select the config panel by its own heading
Build / Build-and-ng-test (pull_request) Successful in 3m56s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m2s
Build / Build-and-test-development (pull_request) Canceled after 26m52s
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.
2026-11-06 13:27:25 +00:00
dc d3f1b2ac0a test(viewbox): select a picker table without walking the tree
Build / Build-and-ng-test (pull_request) Successful in 3m52s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m19s
Build / Build-and-test-development (pull_request) Successful in 26m30s
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.
2026-11-06 12:10:28 +00:00
dc 7a5286fca8 feat(logs): number the lines of the log, the source and the generated code
Build / Build-and-ng-test (pull_request) Successful in 4m22s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m17s
Build / Build-and-test-development (pull_request) Successful in 33m3s
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.
2026-11-06 11:25:50 +00:00
dc da8e7d343c fix(version): fall back to package.json when no tag is reachable
Build / Build-and-ng-test (pull_request) Successful in 4m21s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m28s
Build / Build-and-test-development (pull_request) Successful in 29m8s
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.
2026-11-06 09:46:31 +00:00
dc 88616c8f62 ci(build): reuse the client npm cache and the Cypress binary in the Cypress job
Build / Build-and-ng-test (pull_request) Successful in 3m47s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m37s
Build / Build-and-test-development (pull_request) Successful in 32m31s
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.
2026-11-06 09:44:42 +00:00
dc 68dc790698 ci(build): keep the mock tree's own tests out of the Cypress gate
Build / Build-and-ng-test (pull_request) Successful in 4m21s
Build / Build-and-test-development (pull_request) Canceled after 8m50s
Lighthouse Checks / lighthouse (pull_request) Canceled after 9m22s
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.
2026-11-06 09:30:50 +00:00
dc caa58771bf fix(mocks): emit the sasjs lineage capture through webout()
Build / Build-and-ng-test (pull_request) Successful in 3m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m16s
Build / Build-and-test-development (pull_request) Successful in 29m9s
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.
2026-11-06 09:03:01 +00:00
dc 67631133f9 test(mocks): pin the missing-separator branches and check a delimited drive
Build / Build-and-ng-test (pull_request) Successful in 4m57s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m56s
Build / Build-and-test-development (pull_request) Successful in 32m56s
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.
2026-11-06 02:53:11 +00:00
dc f1ba6362cc test(mocks): build the UNC cases from the code point
Build / Build-and-ng-test (pull_request) Successful in 4m17s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m8s
Build / Build-and-test-development (pull_request) Canceled after 15m30s
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.
2026-11-06 02:33:19 +00:00
dc 755083bb49 test(mocks): fail a service that emits no member, and drop the history from the header
Build / Build-and-ng-test (pull_request) Successful in 6m17s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m30s
Build / Build-and-test-development (pull_request) Canceled after 13m34s
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.
2026-11-06 02:13:24 +00:00
dc 0e74ab58a1 test(mocks): pin the tree's scope and name the service that cannot run
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m1s
Build / Build-and-test-development (pull_request) Successful in 29m25s
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.
2026-11-06 01:34:26 +00:00
dc c9119101a4 test(mocks): describe the separator rule as it applies to the webout() tree
Build / Build-and-ng-test (pull_request) Successful in 3m44s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m5s
Build / Build-and-test-development (pull_request) Successful in 29m15s
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.
2026-11-06 00:55:17 +00:00
dc 676cdf6fdd test(mocks): run only the services that emit, and widen the UNC exception
Build / Build-and-ng-test (pull_request) Successful in 5m23s
Build / Build-and-test-development (pull_request) Canceled after 2m39s
Lighthouse Checks / lighthouse (pull_request) Canceled after 8m1s
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.
2026-11-06 00:47:13 +00:00
dc 88cc68e2f9 test(mocks): anchor the doubled-separator rule and resolve sibling requires
Build / Build-and-ng-test (pull_request) Successful in 3m47s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m2s
Build / Build-and-test-development (pull_request) Successful in 26m56s
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.
2026-11-06 00:15:03 +00:00
dc bafbe201ce fix(mocks): restore the two separators the sas9 getdata recording lost
Build / Build-and-ng-test (pull_request) Successful in 4m47s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m51s
Build / Build-and-test-development (pull_request) Successful in 29m4s
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.
2026-11-05 23:18:52 +00:00
dc 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.
2026-11-05 22:24:45 +00:00
dc 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.
2026-11-05 21:29:35 +00:00
dc 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 (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.
2026-11-05 21:29:23 +00:00
dc 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().
2026-11-05 21:17:10 +00:00
dc 5c646913e4 fix(deploy): ask for the admin group before the staging directory on SASjs
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m31s
Build / Build-and-test-development (pull_request) Successful in 33m3s
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.
2026-11-05 18:21:54 +00:00
dc 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".
2026-11-05 18:21:54 +00:00
dc c808c41333 fix(deploy): stop the makedata poll outliving its screen, and align the release gates
Build / Build-and-ng-test (pull_request) Successful in 4m20s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m36s
Build / Build-and-test-development (pull_request) Successful in 33m14s
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
2026-11-05 17:09:52 +00:00
dc 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.
2026-11-05 17:09:49 +00:00
dc e8cdc37a05 test(deploy): match the picker's requests with regexps, and sign in on a secured estate
Build / Build-and-ng-test (pull_request) Successful in 5m10s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m34s
Build / Build-and-test-development (pull_request) Successful in 32m58s
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.
2026-11-05 15:10:51 +00:00
dc 677b03eddf test(deploy): bind the picker in the Viya order spec, and drop fixed waits
Build / Build-and-ng-test (pull_request) Successful in 4m13s
Lighthouse Checks / lighthouse (pull_request) Successful in 7m56s
Build / Build-and-test-development (pull_request) Failing after 37m51s
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.
2026-11-05 14:13:16 +00:00
dc 133092502c fix(deploy): unclamp the folder picker row and probe in the chosen context
Build / Build-and-ng-test (pull_request) Successful in 3m43s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m35s
Build / Build-and-test-development (pull_request) Canceled after 24m24s
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.
2026-11-05 13:44:31 +00:00
dc 1a9f59478b fix(mocks): resolve the audit table once, record lower-case md5, and report a failed audit write
Build / Build-and-ng-test (pull_request) Successful in 6m28s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m32s
Build / Build-and-test-development (pull_request) Successful in 27m3s
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.
2026-11-05 10:44:40 +00:00
dc 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/).
2026-11-05 10:44:40 +00:00
dc 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.
2026-11-05 10:44:40 +00:00
dc 67d9cfd1bd fix(mocks): 0 is a closed TX_TO, a libref boundary, and the union of the row keys
Build / Build-and-ng-test (pull_request) Successful in 5m40s
Lighthouse Checks / lighthouse (pull_request) Successful in 7m54s
Build / Build-and-test-development (pull_request) Successful in 32m44s
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.
2026-11-05 10:44:28 +00:00
dc e53c50d4e2 fix(mocks): one sasDatetimeNow, a documented TX_TO=0, and a shaped fallback export
Build / Build-and-ng-test (pull_request) Successful in 5m56s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m3s
Build / Build-and-test-development (pull_request) Successful in 29m43s
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/).
2026-11-05 09:39:25 +00:00
dc 00d37577f7 ci(build): restore the Angular cache after postinstall, where it survives
Build / Build-and-ng-test (pull_request) Successful in 5m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 7m56s
Build / Build-and-test-development (pull_request) Successful in 32m57s
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 c732d75 restored 396MB and logged "cleared
/workspace/.../client/.angular/cache" minutes later. It now runs immediately
after "Install dependencies": the clear has already happened, and every entry
in the cache was produced from a stripped stylesheet, because every run strips
before it builds. The invariant the clear exists to enforce is preserved.

No restore-keys on this cache, deliberately: because the builder cannot see a
change inside node_modules, a prefix match could restore entries compiled
against a different Clarity after a lockfile bump. Exact key only, so a
dependency change misses and rebuilds.

Measured locally with the tree matching the lockfile (CI=true): cold with the
cache cleared 90s, warm with no source change 27s, warm with version.ts
regenerated as a new commit does 58s. So a build that is 99-109s in CI should
land near 58s; the rest needs the version out of the compiled graph, which is
an application change.

The strip script's comment described webpack 5; it now describes what was
measured, and notes that CI restores after it runs.
2026-11-05 08:21:21 +00:00
dc a1615ee8a7 feat(mocks): add the control-table services the screens call
Build / Build-and-ng-test (pull_request) Successful in 5m35s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m36s
Build / Build-and-test-development (pull_request) Successful in 28m53s
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.
2026-11-04 22:15:54 +00:00
dc ce8c41c5db ci(build): drop the Angular cache step, which bought nothing across runs
Build / Build-and-ng-test (pull_request) Successful in 6m10s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m27s
Build / Build-and-test-development (pull_request) Successful in 26m56s
Run c732d75 restored both entries on runner2 and the build stayed at 99s (it
was 99-109s cold); the tz suite's 62s -> 25s came from the disk cache inside
the job, not from the restore. postinstall rewrites
src/environments/version.ts with the commit hash and a timestamp on every run,
and the app shell imports it, so the application and test graphs are
invalidated regardless of what was restored.

The npm cache stays, and it is the one that pays: the client install fell from
80s to 25s in that run. angular.json keeps cli.cache.environment=all, which is
what makes the within-job reuse possible.
2026-11-04 22:11:15 +00:00
dc c732d7593c ci(build): restore the caches after the secret scan
Build / Build-and-ng-test (pull_request) Successful in 4m38s
Build / Build-and-test-development (pull_request) Canceled after 2m5s
Lighthouse Checks / lighthouse (pull_request) Canceled after 6m42s
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.
2026-11-04 22:04:31 +00:00
dc f88e053887 ci(build): state the cache invariant in the step comments, not one run's numbers
Build / Build-and-ng-test (pull_request) Failing after 1m23s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Canceled after 2m36s
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.
2026-11-04 21:59:45 +00:00
dc 768755dd37 ci(build): namespace the cache keys by base branch
Build / Build-and-ng-test (pull_request) Successful in 5m16s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m34s
Build / Build-and-test-development (pull_request) Successful in 29m6s
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.
2026-11-04 21:23:33 +00:00
dc 5f8f668c66 ci(build): keep Angular's disk cache in CI so the cache step has something to save
Build / Build-and-ng-test (pull_request) Successful in 6m6s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m20s
Build / Build-and-test-development (pull_request) Canceled after 8m43s
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).
2026-11-04 21:08:40 +00:00
dc 9d30e1855f ci(build): restore the npm downloads and the Angular build cache
Lighthouse Checks / lighthouse (pull_request) Successful in 10s
Build / Build-and-ng-test (pull_request) Successful in 5m47s
Build / Build-and-test-development (pull_request) Canceled after 11m42s
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.
2026-11-04 20:51:03 +00:00
dc 5f338ff437 fix(deploy): match the documented SYSSCPL set and stop guessing at the cap
Build / Build-and-ng-test (pull_request) Failing after 16m15s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 20m24s
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.
2026-11-04 20:32:32 +00:00
dc ecc87021c3 fix(build): publish the changed-path decision and pin the matcher with tests
Build / Build-and-ng-test (pull_request) Successful in 6m32s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m59s
Build / Build-and-test-development (pull_request) Successful in 27m9s
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.
2026-11-04 19:17:35 +00:00
dc 48df795202 fix(build): treat a sas/package.json bump as a Cypress input
Build / Build-and-ng-test (pull_request) Successful in 4m56s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m12s
Build / Build-and-test-development (pull_request) Successful in 33m35s
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.
2026-11-04 18:36:02 +00:00
dc fb38309e98 fix(deploy): make the folder picker work on both estates
Build / Build-and-ng-test (pull_request) Successful in 5m45s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m11s
Build / Build-and-test-development (pull_request) Successful in 27m14s
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.
2026-11-04 18:31:12 +00:00
dc 6830d873e5 ci(build): skip the cold build when the Cypress job will not run, and stop installing Cypress in the lint job
Build / Build-and-ng-test (pull_request) Successful in 5m29s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m58s
Build / Build-and-test-development (pull_request) Canceled after 16m5s
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.
2026-11-04 18:13:23 +00:00
dc 3572d56cfb fix(browse): accept legal folder names, cap the walk, and lock the read
Build / Build-and-ng-test (pull_request) Successful in 5m48s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m2s
Build / Build-and-test-development (pull_request) Successful in 27m29s
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.
2026-11-04 17:41:57 +00:00
dc 5382351c25 chore(deps): require @sasjs/core 7.1.8
Lighthouse Checks / lighthouse (pull_request) Successful in 7s
Build / Build-and-ng-test (pull_request) Successful in 6m42s
Build / Build-and-test-development (pull_request) Successful in 11s
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.
2026-11-04 16:53:59 +00:00
dc 14de2b8a41 docs(mocks): say what the sas9 variant does and does not reproduce
Build / Build-and-ng-test (pull_request) Successful in 6m43s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m1s
Build / Build-and-test-development (pull_request) Successful in 27m17s
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.
2026-11-04 16:51:08 +00:00
dc 331460ad25 refactor(mocks): load the webout guard as a module, not by eval
Build / Build-and-ng-test (pull_request) Canceled after 3m3s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
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.
2026-11-04 16:48:07 +00:00
dc 349a2f314c fix(mocks): read hook columns in either case, and guard duplicates first
Build / Build-and-ng-test (pull_request) Successful in 5m12s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m16s
Build / Build-and-test-development (pull_request) Successful in 29m43s
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.
2026-11-04 16:47:21 +00:00
dc f0208bb0b0 fix(mocks): run approval hooks, as mpe_runhook does
Build / Build-and-ng-test (pull_request) Successful in 6m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m11s
Build / Build-and-test-development (pull_request) Successful in 29m49s
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.
2026-11-04 15:56:50 +00:00
dc ca602082ff fix(mocks): abort like mp_abort, lock like mp_lockanytable, derive saslibs
Build / Build-and-ng-test (pull_request) Successful in 5m43s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m58s
Build / Build-and-test-development (pull_request) Successful in 29m42s
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.
2026-11-04 15:37:31 +00:00
dc 87fd46c825 fix(mocks): derive startupservice's maps and timezone instead of hardcoding
Build / Build-and-ng-test (pull_request) Successful in 6m46s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m8s
Build / Build-and-test-development (pull_request) Canceled after 7m30s
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.
2026-11-04 15:23:14 +00:00
dc e1c3c6e872 fix(mocks): answer the deploy screen's DIRLIST and WRITECHECK actions
Lighthouse Checks / lighthouse (pull_request) Successful in 9m45s
Build / Build-and-ng-test (pull_request) Failing after 15m6s
Build / Build-and-test-development (pull_request) Skipped
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.
2026-11-04 14:59:19 +00:00
dc 744c89b919 fix(mocks): separate a body read failure from a missing showmeta floor
Build / Build-and-ng-test (pull_request) Successful in 5m9s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m46s
Build / Build-and-test-development (pull_request) Successful in 33m40s
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.
2026-11-04 14:43:26 +00:00
dc 7be3f84073 feat(mocks): emit the sas9 services through webout()
Build / Build-and-ng-test (pull_request) Successful in 6m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m57s
Build / Build-and-test-development (pull_request) Canceled after 14m28s
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.
2026-11-04 14:21:44 +00:00
dc 9c3d067e0e ci(sites): publish staging unconfigured so it opens on the deploy screen
Build / Build-and-ng-test (pull_request) Successful in 5m10s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m6s
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.
2026-11-04 13:41:50 +00:00
dc 7d9d466c3e fix(mocks): keep the OMS and path separators the captures lost
Build / Build-and-ng-test (pull_request) Successful in 6m45s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m1s
Build / Build-and-test-development (pull_request) Canceled after 7m2s
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.
2026-11-04 13:23:13 +00:00
dc 246cc39ca0 ci: run the mock estate on 3.14.0
Build / Build-and-ng-test (pull_request) Successful in 5m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m8s
Build / Build-and-test-development (pull_request) Successful in 29m36s
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.
2026-11-04 13:06:55 +00:00
dc 1f54fad3fe feat(mocks): emit the lineage, usernav and libinfo responses through webout()
Build / Build-and-ng-test (pull_request) Successful in 6m40s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m2s
Build / Build-and-test-development (pull_request) Successful in 33m38s
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.
2026-11-04 11:46:19 +00:00
dc e1d877f21d feat(mocks): emit the metanav responses through webout()
Build / Build-and-ng-test (pull_request) Canceled after 56s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 55s
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.
2026-11-04 11:45:17 +00:00
dc b1eab98200 fix(build): build the mock sites with a CLI whose runtime provides webout()
Build / Build-and-ng-test (pull_request) Canceled after 3m16s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m9s
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.
2026-11-04 11:03:47 +00:00
dc 09e9f192c5 fix(mocks): read the metadata member from the response, not the function's arity
Lighthouse Checks / lighthouse (pull_request) Successful in 8s
Build / Build-and-ng-test (pull_request) Successful in 6m37s
Build / Build-and-test-development (pull_request) Successful in 27m32s
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.
2026-11-04 10:34:15 +00:00
dc aa295b384a Merge remote-tracking branch 'origin/release/7.22' into fix/449-va-ddc-host-order-free
Build / Build-and-ng-test (pull_request) Successful in 5m41s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 4m49s
2026-11-04 10:20:17 +00:00
dc ba32d1fa46 ci: run the mock estate on the server that reports SYSSITE
Sites / deploy (push) Successful in 4m8s
Build / Build-and-ng-test (pull_request) Successful in 5m7s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m8s
Build / Build-and-test-development (pull_request) Canceled after 19m25s
@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.
2026-11-04 10:15:04 +00:00
dc f0e3117540 ci(workflows): hand values to the shell as data, not as script text
Build / Build-and-ng-test (pull_request) Successful in 6m40s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m8s
Build / Build-and-test-development (pull_request) Successful in 33m40s
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.
2026-11-04 09:55:22 +00:00
dc 1225eb122a test(embed): fail on a missing FILTER_HASH before reading the store back
Build / Build-and-ng-test (pull_request) Successful in 6m45s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m14s
Build / Build-and-test-development (pull_request) Failing after 37m14s
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.
2026-11-04 09:08:55 +00:00
dc 4a87f8f6ce chore: merge release/7.22 into fix/449-va-ddc-host-order-free
Build / Build-and-ng-test (pull_request) Successful in 6m37s
Lighthouse Checks / lighthouse (pull_request) Successful in 9m12s
Build / Build-and-test-development (pull_request) Failing after 37m22s
2026-11-04 01:20:13 +00:00
dc 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.
2026-11-04 01:20:05 +00:00
dc 1c4d2f65cc test(embed): match the clause's field wherever the body wraps it
Build / Build-and-ng-test (pull_request) Successful in 9m10s
Build / Build-and-test-development (pull_request) Canceled after 4m21s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
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.
2026-11-04 00:40:47 +00:00
dc 75d32d9838 ci(build): record why the mock estate's server pin is 3.12.3
Build / Build-and-ng-test (pull_request) Successful in 5m10s
Lighthouse Checks / lighthouse (pull_request) Successful in 7s
Build / Build-and-test-development (pull_request) Successful in 10s
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.
2026-11-04 00:38:40 +00:00
dc ec92eb4eee ci: run the mock estate on the server that carries the utils floor
Sites / deploy (push) Successful in 3m46s
Build / Build-and-ng-test (pull_request) Canceled after 1m4s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
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.
2026-11-04 00:27:34 +00:00
dc 36a2bff086 ci(workflows): install the tooling at the versions this repo chose
Sites / deploy (push) Canceled after 0s
Build / Build-and-ng-test (pull_request) Canceled after 0s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
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.
2026-11-04 00:12:46 +00:00
dc 482435f318 fix(mocks): refuse a runtime whose webout() cannot carry column metadata
Build / Build-and-ng-test (pull_request) Successful in 6m51s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m57s
Build / Build-and-test-development (pull_request) Failing after 30m50s
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.
2026-11-04 00:10:53 +00:00
dc 9c53e5b90d test(embed): match the clause's field, and wait for the response as long as the page takes
Build / Build-and-ng-test (pull_request) Successful in 5m49s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m58s
Build / Build-and-test-development (pull_request) Canceled after 0s
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.
2026-11-03 23:54:42 +00:00
dc 90784b342f fix(va-host): deliver the load-mount message once, and document report.json from the file
Build / Build-and-ng-test (pull_request) Successful in 5m9s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m59s
Build / Build-and-test-development (pull_request) Successful in 33m54s
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.
2026-11-03 23:45:40 +00:00
dc ce6bb5d4cd feat(mocks): build every JS stored program's response with webout()
Build / Build-and-ng-test (pull_request) Successful in 5m44s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m9s
Build / Build-and-test-development (pull_request) Canceled after 0s
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.
2026-11-03 23:16:48 +00:00
dc 6508ac7f5c chore: merge release/7.22 into fix/449-va-ddc-host-order-free
Build / Build-and-ng-test (pull_request) Successful in 5m47s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m23s
Build / Build-and-test-development (pull_request) Canceled after 0s
2026-11-03 23:16:37 +00:00
dc 93af2dabe6 test(embed): match a parsed request body, and count duplicates after the read
Build / Build-and-ng-test (pull_request) Canceled after 0s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
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.
2026-11-03 23:15:50 +00:00
dc 9cec292681 fix(va-host): post to the frame's own origin, and key the replay to the document
Lighthouse Checks / lighthouse (pull_request) Successful in 14s
Build / Build-and-ng-test (pull_request) Successful in 6m35s
Build / Build-and-test-development (pull_request) Canceled after 0s
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.
2026-11-03 23:11:20 +00:00
dc cde90830fb chore(ci): record why the client-dist artefact stays on upload-artifact v3
Sites / deploy (push) Successful in 3m50s
Build / Build-and-ng-test (pull_request) Successful in 5m11s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m18s
Build / Build-and-test-development (pull_request) Canceled after 12m58s
2026-11-03 23:07:17 +00:00
dc 05bd1e2e7f ci(lighthouse): cancel the check for a superseded pull request head
Lighthouse Checks / lighthouse (pull_request) Successful in 9s
Build / Build-and-ng-test (pull_request) Successful in 5m6s
Build / Build-and-test-development (pull_request) Successful in 29m36s
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.
2026-11-03 22:49:30 +00:00
dc 7a2b01db84 ci(build): cancel the build for a superseded pull request head
Build / Build-and-ng-test (pull_request) Canceled after 0s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Successful in 11m14s
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.
2026-11-03 22:48:50 +00:00