On 6 September we wrote about moving a learning core into VARGATES Medical, and on 16 September about the public page its certificates now have. A handful of pages were split off from that move because each needed a package the repository did not yet have, and their acceptance was set as a page rendering in a browser rather than a build passing, because such pages fail in ways a build does not see: a graph library that draws nothing, a component that throws on first paint, an import inside a function body that no static walk follows. The last of those pages is the scenario editor, the screen where an author lays out a branching clinical case as a graph of nodes and decides where each branch goes.
It went through the same checks as everything else. It compiled. The types agreed. Both test suites were green. Then someone opened it in a browser, and three things were wrong, each of them invisible to every check that had passed.
It could not load at all
To open a scenario, the page asked the server for it and then, in the same breath, asked three more addresses for the scenario's nodes, edges and variables. Those three addresses exist, but only for creating a node, an edge or a variable. Asked to list them, each answers method not allowed. So every load produced three refusals, and the page printed “Scenario not found” over a scenario that had loaded perfectly well one request earlier.
The three extra requests were never needed. The server's reply to the first request already carried the nodes, edges and variables of a scenario in the project the core came from. The page did not know, because the frontend's description of that reply omitted the variables, and a type system cannot say “you already have this” about a field the type does not declare.
Nothing between the click and the server could notice. The frontend spells an address in a string, the backend spells it in a route declaration, and no check compared the two. A request naming an address the backend does not serve with that method compiles, type-checks, passes every unit test on both sides, and fails in a browser. The three list functions are deleted rather than left beside a working path.
It hid its own name
The editor's toolbar holds thirteen controls in one row. At the default desktop width, inside the administration shell, their natural widths add up to exactly the space the shell leaves. The other controls kept a minimum width, because unbreakable text keeps a floor. The thirteenth was the scenario's title, styled to truncate with an ellipsis when long. Truncation hides overflow, and an item that hides its overflow has a minimum width of zero. So when the row ran out of room, the one item that could give way gave way completely: the title of the scenario measured 0 pixels wide on its own editor. Unreadable and unclickable.
The fix is two words of styling. The title can no longer be squeezed below its own width (a long one is still capped and truncated, it just cannot reach nothing), and the row scrolls sideways when it is over-full instead of clipping. The second half matters as much as the first: a title that refuses to vanish inside a row that cannot scroll pushes the Publish button off the edge instead.
It drew no branches
The graph library draws a branch from the connector of one node to the connector of the next. None of the three node types rendered a connector. The canvas drew three boxes and nothing between them, and reported no error, on a page whose entire subject is where the branches go. The silence ran the other way too: a new branch is drawn by dragging from a connector, so an author could not create one either.
Each node now has an incoming connector on top and an outgoing one at the bottom. An end state has only the incoming one: nothing continues past an end state, and a connector it does not have is a branch nobody can draw by mistake.
All three were inherited
None of this was introduced by the move. The project this learning core came from has the identical loader, the identical toolbar in the identical shell, and no connector on any node type. Its editor cannot load a scenario and draws no branches for the same reasons, and, having the same markup in the same shell, hides the title the same way. Which is why “the source does it this way” confirmed nothing: two copies of a defect agree with each other.
| Defect | Build and types | Unit tests | A browser |
|---|---|---|---|
| Three requests to addresses that only accept creation | Green: the address exists | Green: the reply shape matched | “Scenario not found” |
| Title squeezed to zero width | Green: valid styling | Green: the element is rendered | 0 px, unreadable |
| No connectors, no branches | Green: valid graph | Green: the nodes are present | Three boxes, nothing between them |
What the check learned
The first defect was the cheapest to prevent and the easiest to miss, so it now has a guard of its own. A test walks every address the ported frontend builds towards the backend and asks the real route table whether that address answers that method. At the time of writing it reads 286 requests across 63 modules of the ported frontend and finds every one served.
Two things about that guard are worth stating, because both were measured rather than assumed.
The method is half the property. The address for a scenario's nodes exists. A check that matched the address alone would have called all three broken requests served and printed a clean run over the exact defect it was written for. With the three requests already deleted, nothing left in the frontend could tell a method-blind check from a correct one, so the difference is pinned by a test that deliberately asks for a method a known address does not answer.
Coverage is asserted, not assumed. The first version of the guard walked only the directory of API client functions while calling itself a check of “the ported frontend”. The review of the change counted what that left out: 73 further requests in pages, hooks and the sign-in module, none of them checked. A guard that makes a false claim is worse than no guard, because the false claim is what stops anyone looking. It now walks the whole ported tree, and a module that issues requests but yields no address to check fails the test, since a rule that has stopped matching is indistinguishable from a tree that is clean.
Two more things the browser did not need to find
Opening the page also settled two disagreements that were visible from the code once someone looked.
The variable picker offered types the server does not accept. The editor lets an author declare a variable as text, number or boolean, and sent exactly those three words. The server's vocabulary is string, integer, boolean and enum. Two of the three choices an author could make were refused on save. Nothing in between failed, because the frontend's list of types was its own definition of the contract rather than a reading of it. The labels stay human; the values sent are now the server's, and a test reads both definitions from disk and fails if they part again.
A printed launch code took its address from whatever the author's browser was on. The editor mints short codes for running a scenario and encodes them as QR images. The address inside the image was built from the current page's origin, so a code minted on a preview deployment would have pointed at that preview forever. A laminated code cannot be reissued. It now resolves through the site's own fixed address, the same correction made for the certificate QR one screen over.
What “not found” now says
The loader had a second mask underneath the first. When the scenario could not be shown, the page printed the same “Scenario not found” whether the cause was a missing identifier, a membership without the right to edit scenarios, a server error or a dropped connection. Fixing only the three refusals would have left that mask in place for the next cause. The not-found screen now shows the diagnostic the loader recorded, so a refusal reads as a refusal and an outage as an outage.
What the browser did not find
It would be dishonest to end the list at three, because the two worst defects on this page were found by neither the build nor the browser. A review of the frozen change, by a fresh process reading the code, found both, and both would have lost an author's work.
Selecting a second node kept the first node's form. The inspector panel initialised its fields once per mounted instance, and without a key React reused the instance across selections. The header changed to node B; the fields still held node A; Save wrote A's content onto B. The panel is now keyed by the node it edits.
Backspace on a node deleted its branches and kept the node. The graph library resolves a selected node to the node plus every branch touching it, and reports the two deletions separately. Only the branch deletion was wired to the server, so the key persisted the loss of every branch while removing the node from the screen only. Next load: the node was back, the branches were gone. Both deletions are now wired.
Neither is the kind of defect the acceptance test catches. It reads the graph and opens panels; it never saves or deletes, by design. A further defect, filed during the same review and still open, is that several of the editor's write paths, moving a node, drawing a branch, adding or removing a variable, can fail without telling the author, and the automatic layout can save half a reordering while showing the old one. That is on production now, known and unfixed.
The proof is a browser, not a build
The acceptance test for the page opens it in a real browser against a real database, signs in as an author account seeded for the purpose, and asserts things the graph library had to actually draw: the three seeded nodes, the two branches between them, the variable panel with its seeded variable, a click on a node opening that node's inspector, and the scenario's title visible in the toolbar. It never drags a node. Dropping one saves its position, and a test that depends on the setup script to undo its own writes is a test whose second run means something different from its first.
The change went through the full gate before merging: the backend suite, the frontend suite, and the end-to-end suite against a production build in a container, 159 of 160 with one declared skip. It has been on production since the early hours of 16 September, Astana time.
If you are evaluating a supplier
Ask what their acceptance test opens. A build proves that the code can be assembled. A type check proves that the pieces agree with each other about their shapes. Neither proves that a page loads, that a title is visible, or that the thing the page exists to draw is drawn. The check that answers those questions is the one that opens the page the way a user will. And ask what their review found that the browser did not, because a supplier who can name it is a supplier who reads their own code.
VARGATES Medical is the platform this learning core is moving into. The scenario editor described here is part of that move.