You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
[0.24.4] - 2026-09-25
Added
Test Flow runner panel — View > Test Flow opens an app-owned panel that runs the JSON test flows of the headless harness (test_flow/) against the live circuit: pick a flow file, see its conditions and measurements resolved against the loaded components, run it, read the per-row results, and export them as JSON. The panel never forms its own opinion of a flow — its pre-flight is the harness's own ValidateFlow() pass, exhaustive over every condition value and reporting every issue in RunFlow()'s check order and wording, with Run disabled until that list is empty — so a flow the panel offers is a flow the harness accepts, and a mistyped path (a serialize() key such as atten_dB, not the inspector's attenuation_dB label) or a value the resolved slot cannot take is named before the click instead of failing the run. A run is net-neutral: every engine is serialized before execution and restored afterwards, each independently so one failure cannot strand the rest, the graph's input pointers are rewired, and component state, links and the project's dirty flag are exactly what they were. A restoration failure is the exception — it is latched and reported, the run's result is suppressed, and further runs are refused until the circuit is reloaded (newProject(), a successful load, or a failed load that reset the circuit, all clear it). An unmeasurable sample renders as N/A and exports as JSON null rather than failing the run, and the panel's selection, load state and last result are session-only while the panel toggle persists with the other window states.
Node hover SNR — the node-body hover tooltip now carries the strongest-tone SNR of the hovered component's first output, measured through the new SpectrumAnalyzerEngine::computeStrongestToneSNRdB() with the analyzer's current RBW, so changing the spectrum analyzer's resolution bandwidth changes the hover row. The tooltip prints SNR: <value> dB, or SNR: -- when the measurement is unavailable (no finite tone, no strictly ascending two-bin grid, a tone outside the swept span, a non-positive RBW); the call is read-only and never touches the analyzer's render caches, VBW or trace history, and a multi-output part still exposes its other outputs through the existing Ctrl+click probe hint. Pin, link and collapsed-subcircuit tooltips are unchanged and carry no SNR row.
Changed
The flow pre-flight is shared, not reimplemented — ValidateFlow(spec, components) is the harness's condition and measurement resolution pass exposed as a reusable call, and RunFlow() is now a caller of it that reports the first issue verbatim; an attached UI therefore cannot drift from the harness's verdicts in either order or wording.
Value checking is split from writing — resolveConditionSlot() resolves a condition's path once per condition and reports the slot kind it found, and conditionSlotAccepts() applies the value rules to that kind with no JSON involved, which are exactly the rules applyConditionValue() enforces. Checking a whole sweep therefore costs one path parse plus arithmetic instead of a serialize, clone and patch per value (a 10000-value sweep measures ~0.1 ms, affordable on every frame), and RunFlow()'s own pre-flight drops from ~14.7 µs per value to that same check.
One click cannot hang the window — the panel refuses a sweep above kMaxRunRows (10000) before it snapshots anything, because the cartesian product executes synchronously on the UI thread (10000 rows measured ~4 s on a two-node circuit in an unoptimized build), and the results table is height-bounded and row-clipped (34 ms per frame at 10000 rows against 0.27 ms bounded). The row ceiling and the current issue are shown in the panel, so a disabled Run always has a visible reason.
Flow addressing is documented, not implicit — flow conditions[].component is an IComponentEngine::id(), assigned from a base of 100 in saved order, so a clean save/load reproduces the ids while any deletion or re-order shifts every later one; because ids stay dense, a stale reference can silently bind to a different component of the same type. Both test_flow/AGENTS.md and app/AGENTS.md now state this, along with the pre-flight, row-ceiling and table contracts.
Testing
Add three standalone test targets (the established pattern for keeping coverage out of the MinGW TEST_CASE registration ceiling): tests/test_spectrum_analyzer_snr.cpp (RBW sensitivity, full stored tone power on real-domain and complex-baseband spectra, extreme finite tone levels whose watt conversion underflows or overflows, unavailable-input guards, nonuniform-grid and tie-bin lookup, and the read-only contract against render caches and trace history), tests/test_node_hover_snr.cpp (the app's hover adapter: registry summary passthrough, the analyzer SNR of the node's first output only — an output-1 read would be ~20 dB off and is asserted against — and the empty-optional fallback), and tests/test_test_flow_widget.cpp (the panel contract in full, below).
Add tests/test_test_flow_widget.cpp coverage for the snapshot/restore boundary around RunFlow() (a clean sweep and an ordinary failure both leave every component's serialize() state, every graph link and the dirty flag untouched; a restoration failure is a distinct latched failure with no result that may leave the throwing component at its swept state, cleared only by resetAfterCircuitReload()), the app's failed-load switch, preview id/port resolution, the saturating expected row count, N/A rendering, export success/failure, reload revalidation of the retained selection, and draw callback dispatch. The pre-flight is pinned against a mistyped or type-inapplicable path — including a fractional value sitting between two acceptable ones, the case a sampled pre-flight would miss — the kMaxRunRows refusal is asserted through an engine that counts deserialize() calls (nothing may be executed), and frame-cost guards cover both the exhaustive pre-flight over 10000 values and the bounded table.
Extend tests/test_issue87_flow.cpp with the ValidateFlow() contract (no issues for a runnable flow, one FlowError per failing reference in RunFlow()'s order and wording with RunFlow() reporting the first verbatim, and a value rejected mid-list rejected the same way) and the resolveConditionSlot()/conditionSlotAccepts() split it rests on, including a value-by-value equivalence loop asserting the check agrees with the write on outcome, message and the exact JSON type written, and that resolving touches nothing.
Extend test_engine/ui_tests.cpp with the tooltip-window visibility case's node-body data assertions (non-empty summary plus a populated analyzer SNR from the app callback the widget renders) and the View > Test Flow menu/window integration; the panel's own model is covered by the headless standalone target.