Weekly Recap: August 7-14
Last Monday I published three posts at once — AL Runner v2 , BC on Linux and the AL-Go fast lane , and the bc-code-atlas intro . None of that stopped the week it was published. Here’s what happened across those repos (and a couple of new ones) since then.
The AL Runner: closing correctness gaps and hardening the pipeline
Since the v2 cutover post
, the runner shipped roughly 85 merged PRs in four days, most of them small, targeted fixes to places where the emulator’s behavior diverged from real BC. The pattern was steady: Rename propagating through conditional and filtered TableRelations (#1745
), CalcFormula resolving flow-filter where-conditions and FlowFields named inside a where-clause (#1756
, #1762
), TestPage AutoSplitKey numbering rows from real data instead of a constant and proposing values the way BC’s own AutoKeyGenerator does (#1760
, #1763
), and event dispatch respecting EventSubscriberInstance = Manual for both codeunit and table subscribers (#1750
, #1759
). None of these individually is interesting on its own, but together they’re what “closing the gap between emulation and real BC” actually looks like: run the corpus against real BC, find a case where the runner and BC disagree, fix it. The al-language corpus pin moved four times this week as upstream test suites landed and got wired into CI, each bump pulling in more real-BC-verified assertions.
The other real thread was safety and honesty about what the test suite is actually asserting. #1882
adds a --count-baseline floor, because --strict only ever failed a run on a test that actually ran and failed — it had no way to notice a whole bundle silently vanishing from discovery (an app-id collision, a dropped app group) and still reporting green because every surviving test passed. The floor checks per-suite test counts and app-group counts against a manifest after every bundle finishes, with per-BC-version overrides since runner-extras genuinely has 110 tests on BC 27.x and 116 on 28.x. In the same spirit, #1746
went through the expectations file and reclassified three entries that had been mislabeled as “known gap, intend to fix” when they were actually permanent out-of-scope surfaces or an intended, documented divergence — adding a real expect-divergence mode instead of letting those entries lie about being temporary.
Performance work ran in parallel. #1830
is a good example of measuring before fixing: the unit test step was hitting only 1.83x concurrency against a cap of 4 threads, and the obvious suspects — test distribution, the thread cap itself, a shared lock — all turned out to be innocent. The actual cause was scheduling order: the heaviest test collection, 285 seconds of serial work, was getting dispatched two-thirds of the way through the run, so its tail ran alone with three idle threads. Ordering collections heaviest-first cut the step from 528s to 392s locally. Alongside that, caches got content-addressed instead of keyed on file mtimes or embedded commit SHAs, the compiler’s shared symbol-reference loader got built once per bundle instead of once per dependency, and a new AL_RUNNER_PHASE_LOG instrument gave per-app/bundle/process cost attribution that most of these fixes were measured against. The server protocol also got a cooperative cancel command (#1783
) — a dedicated stdin-reader thread now recognizes cancel and stops a run between tests, which matters for the VS Code extension since the old synchronous dispatch loop couldn’t even read a cancel off stdin until the current run finished.
The reference suite grew by answering AL Runner’s own questions
This week wasn’t about padding coverage in BusinessCentral.AL.Language.Tests
— most of the 26 merged PRs trace back to a specific question AL Runner’s own development raised. When AL Runner’s PR #1770 reported that it silently drops IntegrationEvents raised from inside a table trigger while the same event fired fine from a codeunit, this suite’s #33
didn’t take that report on faith — it built three parallel test cases, one per publisher shape, and ran them against a real BC sandbox to confirm the gap really is an AL Runner bug and not BC behaving in some non-obvious way. Same pattern with Blob storage: #30
measured what happens when you Rename() a record holding an uncommitted Blob write, because an AL Runner issue needed the answer before deciding how to patch its temp-table data provider. The rule behind all of it: if a measured run disagrees with the assumption, the measurement wins.
That discipline shows up again in the TableRelation work. #21
pins how Rename() propagates through conditional and where()-filtered relations, and caught something that isn’t intuitive — renaming a row on the “else” side of a conditional relation updates every matching child row, including ones whose own condition points at the “if” arm. The suite’s first draft of that test assumed the tidier, symmetric behavior, and BC rejected it. Beyond that, this week also nailed down page-level behavior that’s easy to get subtly wrong — TestPage Visible() combining with every enclosing group’s own Visible property, Caption() resolution, DrillDown() dispatch — plus EventSubscriberInstance = Manual binding contracts and new virtual-table coverage for Field and Page Metadata. All of it lands in the same place: a growing, sandbox-verified answer key that AL Runner has to match, not the other way around.
This repo has never had its own dedicated post despite real standing — 355 AL files and 211 test codeunits as of late July, and 26 more PRs this week alone. If the pace holds up over another week or two, it’s a strong candidate for a proper write-up rather than a recap paragraph.
bc-code-atlas: from search to structural graph
Two days after the intro post
, the graph side of bc-code-atlas got a lot more usable. The piece that matters most: global_id is a deterministic key computed the same way on every instance, from the object’s identity, not from anything specific to how that instance was indexed. That means two separately-run bc-code-atlas instances — say, my public one indexing the Microsoft base app, and your own self-hosted instance indexing a customer’s private apps, since the whole project is open source — assign the same global_id to the same base app object independently, with no shared database and no coordination between them. #28
is what makes that useful in practice: it exposes global_id through bcatlas_get_node, adds a bcatlas_find_by_global_id tool, and adds a cross_app edge filter for calls that cross an app boundary. Point a coding agent at both instances at once, and when your private code calls into a Microsoft procedure it can only see as a stub locally, the agent takes that stub’s global_id, looks it up against my public instance, and keeps following the call graph into real base app source — instead of stopping at the boundary or guessing what’s on the other side. #32
fixed a real bug that fell out of that work: the external-stub cache keyed on object name alone, so a table and a codeunit sharing a name (base app has a few, like “No. Series”) collided and the second one got the wrong type. It now keys on type and name together, and calls into out-of-corpus code get per-member stubs instead of collapsing to the object level, so a call stack can be followed across an app boundary without switching to reading source by hand. Three of the underlying fixes landed upstream in Christian Hovenbitzer’s graphify-al itself, not just in my fork — worth mentioning since it’s easy to just patch a fork quietly instead of sending it back.
The other thread was the submodule-bump automation added to keep the indexed AL corpus current, which broke twice in its first day of real use. One bug: git commit with a trailing pathspec re-derives the change from the working tree rather than the index, so on a submodule that’s never checked out, it silently committed a deletion instead of the intended gitlink bump — caught by reproducing the exact sequence in a scratch repo, not by guessing. The other: multiple changed submodules were getting bundled into one PR instead of one each. Both fixes shipped within hours of the bugs surfacing. Separately, the search service got easier to point at something other than the built-in BC corpus — a configurable AL source directory, custom MCP instructions per directory, and an optional watch mode that reindexes on file changes without an explicit refresh.
Fast lane hardening, two On.Linux fixes, and a compile-time benchmark
The Linux fast lane post
went out on August 10, and by the next day I’d already merged six fixes to it. That’s not a bad sign — it’s what happens when a feature that worked on one repo meets the variety of settings other repos actually use. The two that mattered: a project with linuxFastLane: true set as a repo-wide default, but not scoped to the PR workflow, would silently stop building in every other workflow — CI/CD, releases — because nothing consumed its build dimension there. No error, no failed job, just a project that quietly never built. And a compile-only project with real AppSource dependencies it can only compile against, not install, broke with missing-package errors because the fast lane always tries to publish and test. Both are handled now: non-PR workflows fold fast-lane projects back into the normal Windows pipeline, and so does any project that can’t actually be published to a container. I also fixed a PR-build matrix that hard-failed when one side of the Linux/Windows split had zero projects, and added support for external rulesets under workspace compilation. One of the two pin bumps along the way fixes concurrent PR builds on the same self-hosted runner tearing down each other’s containers mid-test — found from a real CI failure, not a hunch.
MsDyn365Bc.On.Linux picked up two small, unrelated additions in the same window, both aimed at running the container behind something other than localhost: the BC login and web client path base are now configurable via environment variables, and a new BC_WEBCLIENT_REQUIRE_SSL flag fixes a redirect-scheme mismatch when a reverse proxy terminates TLS in front of the container. Neither was in last week’s post because neither is fast-lane-specific — they’re proxy plumbing that came up while testing other things.
Separately, I started a new repo, al-go-compile-bench , to measure where AL-Go’s compile step actually spends its time instead of guessing. The fast lane’s speed depends partly on compiling from NuGet-sourced symbols instead of a downloaded artifact, and I wanted to know why that’s faster before leaning on it further. Compiling the same project against the artifact-built compiler folder with its normal assembly probing paths took about 48.8 seconds; dropping the probing paths entirely barely changed that, 48.7 seconds. Compiling against the NuGet-built folder instead took 19.2 seconds — less than half. So the speedup isn’t the probing paths, it’s that the NuGet folder’s package cache only holds the app’s actual dependency closure, where the artifact folder’s cache holds every app bundled in that BC version. That’s a specific, useful answer rather than an assumption, and I’m continuing to bisect other parts of the pipeline the same way.