From 974f49dccbb2a1f46bcaa19aaef3c39c64b360ba Mon Sep 17 00:00:00 2001 From: Adam Clemens Date: Wed, 9 Sep 2026 22:27:08 +0100 Subject: [PATCH] Draw the solved velocity as arrows in pyflow run's declared-field path Closes the gap TASK-051's own commit flagged explicitly rather than fixed: playback.py's combined play() path renders both a solved velocity's arrows and its declared fields' panels together, but bootstrap.py's _add_declared_field_transport -- the identical config shape's live pyflow run path -- colour-mapped the panels and never drew the velocity carrying them at all. Reachable from two shipped demos: pyflow run on smoke_transport.yaml or thermal_buoyancy.yaml showed no indication of flow direction or magnitude despite a real, pressure-corrected velocity driving the transport. Checked docs/implementation/golden-demos.md and Stage 6's Completion Criteria first, per this repo's "deliberately scoped out for a stated reason" convention -- both were silent on live-run arrows either way. tests/unit/test_golden_demo_annotations.py's own _draws_arrows did call the omission "correctly exempt", but that comment predated TASK-051 and was stale rather than a live decision, so this closes it rather than reversing something intentional. _add_declared_field_transport now builds/rebuilds field_visualization.build_vector_field_arrows every frame when config.simulation.velocity_solved is set, the same "remove old, build new" shape _add_solved_velocity_rendering already uses, and returns a third arrows_drawn closure joined into bootstrap()'s show_vector_scale via the existing _either_path_drew_arrows. Written test-first (tests/unit/test_bootstrap.py:: test_bootstrap_with_velocity_solved_and_a_declared_field_also_draws_arrows, confirmed red before the fix landed). Blast radius: test_golden_demo_annotations.py's _draws_arrows no longer exempts velocity_solved-with-declared-fields; smoke_transport.yaml, thermal_buoyancy.yaml and multi_field_plume.yaml all gained a field_display.vector_label, since P-019 requires one on every demo that draws arrows and none needed one while this path drew none. src/pyflow/CLAUDE.md and docs/implementation/golden-demos.md record the fix and its history. roadmap.md/status.md's test count moved 1199 -> 1200 for the one new test. Verified live, not only unit-tested: ran pyflow run --backend offscreen against all three affected demos and confirmed each now holds exactly one arrow gfx.Line object, a correct "Velocity (model units): length = 0.3 x magnitude" HUD line, and 100k+ non-background rendered pixels. Also fixed an unrelated, pre-existing false positive found while running make preflight from inside this session's own git worktree checkout (.claude/worktrees/): test_check_claims.py's test_only_tracked_markdown_files_are_read flagged "worktrees" appearing among repo_root's own ancestor path components as if it were the nested- worktree double-read bug it was written to catch. Narrowed to a path relative to repo_root, which is the only shape the original bug had; the test's actual load-bearing assertion (read == tracked) already passed throughout. Co-Authored-By: Claude Sonnet 5 --- docs/implementation/golden-demos.md | 23 +++++ docs/planning/roadmap.md | 8 +- docs/planning/status.md | 2 +- examples/golden-demos/multi_field_plume.yaml | 7 ++ examples/golden-demos/smoke_transport.yaml | 6 ++ examples/golden-demos/thermal_buoyancy.yaml | 6 ++ src/pyflow/CLAUDE.md | 31 ++++-- src/pyflow/bootstrap.py | 103 ++++++++++++++++--- tests/unit/test_bootstrap.py | 43 ++++++++ tests/unit/test_check_claims.py | 31 +++++- tests/unit/test_golden_demo_annotations.py | 28 ++--- 11 files changed, 249 insertions(+), 39 deletions(-) diff --git a/docs/implementation/golden-demos.md b/docs/implementation/golden-demos.md index 44170e3..fc1360f 100644 --- a/docs/implementation/golden-demos.md +++ b/docs/implementation/golden-demos.md @@ -468,6 +468,16 @@ reproducible, visible run rather than re-validated. implementation; - it runs headlessly via `--backend offscreen`, same as every other demo. +**`pyflow run` also draws the solved velocity as arrows alongside the +temperature panel now (added 2026-09-09)** -- until then +`_add_declared_field_transport` colour-mapped the declared field but +never rendered the velocity carrying it, a gap `playback.py`'s own +combined `play()` path (TASK-051, above) closed for `pyflow play` first +and explicitly flagged as separately open for `pyflow run` +(`src/pyflow/CLAUDE.md`'s `playback.py` entry). Closed the same day; see +`tests/unit/test_bootstrap.py:: +test_bootstrap_with_velocity_solved_and_a_declared_field_also_draws_arrows`. + ## Multi-Field Plume **Stage 6's own claim, made runnable, and the one demo it was missing.** @@ -525,6 +535,11 @@ none of them shows what the stage claims". every other scenario; - it runs headlessly via `--backend offscreen`, same as every other demo. +**`pyflow run` also draws the solved velocity carrying all four fields +as arrows now (added 2026-09-09)**, the same fix applied to Thermal +Buoyancy and Smoke Transport below -- see those sections' own entries +for the full history. + ## Smoke Transport TASK-038's own golden demo -- `docs/planning/implementation-plan.md` @@ -556,6 +571,14 @@ now also carrying a declared `smoke` field. that proof; - it runs headlessly via `--backend offscreen`, same as every other demo. +**`pyflow run` itself also draws the solved velocity as arrows alongside +the smoke panels now (added 2026-09-09)** -- the live counterpart to the +`pyflow play` combined rendering below, closing the gap that fix's own +commit explicitly flagged rather than closed +(`src/pyflow/CLAUDE.md`'s `playback.py` entry); see +`tests/unit/test_bootstrap.py:: +test_bootstrap_with_velocity_solved_and_a_declared_field_also_draws_arrows`. + **Also this stage's own combined-rendering demo for `pyflow play` (TASK-051, Stage 8 reopening, added 2026-09-09).** Solved velocity plus a declared field is exactly the config shape playback used to reject diff --git a/docs/planning/roadmap.md b/docs/planning/roadmap.md index 116b4ab..7ddfcf3 100644 --- a/docs/planning/roadmap.md +++ b/docs/planning/roadmap.md @@ -306,7 +306,13 @@ This paragraph previously said `make install` and `make test` were still expected to fail, pending `uv.lock` and a test suite (B2/C1) -- stale since 2026-08-16 and corrected 2026-08-19. Both now succeed: `uv.lock` is committed (B2) and `make test` runs the suite with coverage -(C1a/C1b): **1199 tests as of 2026-09-09**, up from 1196 the same day +(C1a/C1b): **1200 tests as of 2026-09-09**, up from 1199 the same day +(`_add_declared_field_transport` also drawing the solved velocity as +arrows when `config.simulation.velocity_solved` is set, alongside its +declared fields' own panels -- the live `pyflow run` counterpart to +TASK-051's `pyflow play` fix below, closing the gap that fix's own +commit explicitly flagged rather than closed: 1 in +`tests/unit/test_bootstrap.py`), 1199 itself up from 1196 the same day (TASK-051, Combined Solved-Velocity + Declared-Field Playback: 1 in `tests/unit/test_playback.py` (a materialized frame's own panel colours matching an independently live-stepped run's, `rtol=0, atol=0`), 2 in diff --git a/docs/planning/status.md b/docs/planning/status.md index 70c4028..9e2533e 100644 --- a/docs/planning/status.md +++ b/docs/planning/status.md @@ -46,7 +46,7 @@ pie showData ## Live repository facts - **49** `CLAUDE.md` files -- **1199** tests collected +- **1200** tests collected - **144** Gherkin scenarios (`tests/features/*.feature`) ## Stages diff --git a/examples/golden-demos/multi_field_plume.yaml b/examples/golden-demos/multi_field_plume.yaml index 11b2681..097fd2b 100644 --- a/examples/golden-demos/multi_field_plume.yaml +++ b/examples/golden-demos/multi_field_plume.yaml @@ -124,6 +124,13 @@ field_display: # The other three are transported all the same -- which is why this # demo's own report, not its rendered frame, is what demonstrates it # (`tests/features/multi_field_plume.feature`). + # + # `_add_declared_field_transport` also draws the solved velocity + # carrying all four fields as arrows now (added 2026-09-09, closing the + # gap `playback.py`'s own combined `play()` path, TASK-051, flagged) -- + # P-019 requires the same name-the-quantity treatment + # `lid_driven_cavity.yaml`'s own arrows already get. + vector_label: Velocity (model units) panels: - field: temperature value_range: [0.0, 1.0] diff --git a/examples/golden-demos/smoke_transport.yaml b/examples/golden-demos/smoke_transport.yaml index 49969f8..574fce2 100644 --- a/examples/golden-demos/smoke_transport.yaml +++ b/examples/golden-demos/smoke_transport.yaml @@ -53,6 +53,12 @@ fluid: field_display: low_color: "#0a0a2a" high_color: "#e8e8ff" + # `_add_declared_field_transport` also draws the solved velocity + # carrying `smoke` as arrows now (added 2026-09-09, closing the gap + # `playback.py`'s own combined `play()` path, TASK-051, flagged) -- + # P-019 requires the same name-the-quantity treatment + # `lid_driven_cavity.yaml`'s own arrows already get. + vector_label: Velocity (model units) # Two modular panels (`field_display.panels`), both showing the same # `smoke` field -- added at a user's direct request after watching # this exact demo: smoke decays toward zero over the run, and a fixed diff --git a/examples/golden-demos/thermal_buoyancy.yaml b/examples/golden-demos/thermal_buoyancy.yaml index 6997ce6..5dbb0e8 100644 --- a/examples/golden-demos/thermal_buoyancy.yaml +++ b/examples/golden-demos/thermal_buoyancy.yaml @@ -63,6 +63,12 @@ fluid: field_display: low_color: "#0a0a2a" high_color: "#ff4400" + # `_add_declared_field_transport` also draws the solved velocity + # carrying `temperature` as arrows now (added 2026-09-09, closing the + # gap `playback.py`'s own combined `play()` path, TASK-051, flagged) -- + # P-019 requires the same name-the-quantity treatment + # `lid_driven_cavity.yaml`'s own arrows already get. + vector_label: Velocity (model units) panels: - field: temperature value_range: [0.0, 1.0] diff --git a/src/pyflow/CLAUDE.md b/src/pyflow/CLAUDE.md index 41f99f8..3f3b67e 100644 --- a/src/pyflow/CLAUDE.md +++ b/src/pyflow/CLAUDE.md @@ -379,16 +379,27 @@ build new" way `_rebuild_arrows`/`_rebuild_thumb` already are. -- no solved velocity at all (Heat Diffusion's own shape) -- not "any declared field", which TASK-046/047's own first cut used to reject too. -**This is a real, deliberate widening beyond what `bootstrap.py`'s own -live `_add_declared_field_transport` currently does for the identical -config shape** -- that path draws only the declared fields' panels, -never arrows for a solved velocity alongside them (see this file's own -entry for it, above: "smoke_transport.yaml/thermal_buoyancy.yaml... -don't [set vector_label], because that path has never drawn velocity -as arrows at all"). `playback.py` is not obligated to reproduce a gap -in `pyflow run` just because the two share history; flagged separately -as a possible follow-up rather than fixed here (out of this task's own -scope) or silently matched (worse for a viewer, for no real reason). +**This was a real, deliberate widening beyond what `bootstrap.py`'s own +live `_add_declared_field_transport` did for the identical config shape +at the time this task landed** -- that path drew only the declared +fields' panels, never arrows for a solved velocity alongside them. +`playback.py` was not obligated to reproduce a gap in `pyflow run` just +because the two share history, so this task flagged it separately as a +possible follow-up rather than fixing it as part of this task's own +scope, or silently matching it (worse for a viewer, for no real reason). + +**Closed the same day, in a separate change: `_add_declared_field_transport` +now also draws the solved velocity as arrows** (added 2026-09-09, own +regression test `tests/unit/test_bootstrap.py:: +test_bootstrap_with_velocity_solved_and_a_declared_field_also_draws_arrows`), +the live `pyflow run` counterpart to this task's own `pyflow play` fix, +reusing the same `field_visualization.build_vector_field_arrows` this +module's `_add_solved_velocity_rendering` already calls. `smoke_transport.yaml`, +`thermal_buoyancy.yaml` and `multi_field_plume.yaml` all gained a +`field_display.vector_label` in the same change -- P-019 +(`tests/unit/test_golden_demo_annotations.py`) requires one on every +demo that draws arrows, and none of the three needed one while this +path drew none. **`panel_colors`/`panel_caption`/`build_panel_legend`/`PanelRenderState` were extracted from `bootstrap.py`'s own private `_panel_colors`/ diff --git a/src/pyflow/bootstrap.py b/src/pyflow/bootstrap.py index 993095e..8cc1f8e 100644 --- a/src/pyflow/bootstrap.py +++ b/src/pyflow/bootstrap.py @@ -252,7 +252,7 @@ def _add_legend( def _add_declared_field_transport( window: RenderWindow, mesh: Mesh, config: PyFlowConfig -) -> tuple[Callable[[], None], _Bounds]: +) -> tuple[Callable[[], None], _Bounds, Callable[[], bool]]: """Wires a real `simulation.step()` into a live `pyflow run` (Stage 4 Completion Criterion 1, TASK-030) -- the mechanism the Passive Scalar Transport golden demo needs and no demo before it @@ -313,6 +313,25 @@ def _add_declared_field_transport( happens to it after frame zero, not what it starts as (`src/pyflow/configuration/CLAUDE.md`). + **Also draws the solved velocity as arrows, rebuilt every frame the + same "remove old, build new" way as `_add_solved_velocity_rendering` + (added 2026-09-09).** Until now this function colour-mapped the + declared fields' own panels but never rendered the velocity carrying + them at all -- reachable from two shipped demos, Smoke Transport and + Thermal Buoyancy, whose own `pyflow run` showed no indication of + flow direction or magnitude despite a real, pressure-corrected + velocity driving the transport. `playback.py`'s combined `play()` + path (TASK-051, Stage 8 reopening) built the identical combined + rendering for `pyflow play` first and explicitly flagged this path as + a separate, pre-existing gap rather than fixing it as part of that + task's own scope (`src/pyflow/CLAUDE.md`'s `playback.py` entry) -- + this closes it. The third return value, `arrows_drawn`, is this + path's own per-frame query, mirroring + `_add_solved_velocity_rendering`'s identically-named closure: `False` + always when `config.simulation.velocity_solved` is unset, and a real + per-frame answer otherwise, since a velocity starting from rest draws + nothing on frame zero the same way it does on that other path. + **State construction and per-frame advance moved to `simulation_run. build_simulation_state`/`advance_simulation_state` (TASK-045, Stage 8, 2026-09-07)** -- `recording.py`'s own headless path needs the @@ -381,6 +400,28 @@ def _add_declared_field_transport( state: SimulationState = built_state window.simulation_fields = state.fields + # **Arrows for the solved velocity carrying these fields (added + # 2026-09-09), mirroring `_add_solved_velocity_rendering`'s own + # "remove old, build new" shape.** Until now this function drew only + # the declared fields' own panels, never the velocity itself, even + # when `config.simulation.velocity_solved` is true -- a real, + # previously-flagged gap (this function's own comment used to read + # "a velocity-only live run has nothing this function knows how to + # render yet", and `playback.py`'s combined `play()` path (TASK-051) + # closed the identical gap for `pyflow play` while explicitly leaving + # this one open as "a pre-existing, separately flagged gap"). No + # separate bounds contribution: arrows are drawn over the same mesh + # extent the panels' own `bounds` already covers. + rendered_arrows_object: gfx.Line | None = None + if config.simulation.velocity_solved: + velocity_field = velocity_field_from_state(state) + rendered_arrows_object = build_vector_field_arrows( + velocity_field, config.field_display.arrow_color, config.field_display.arrow_scale + ) + if rendered_arrows_object is not None: + rendered_arrows_object.local.position = (0.0, 0.0, _ARROWS_Z) + window.scene.add(rendered_arrows_object) + mesh_width = bounds[2] - bounds[0] mesh_height = bounds[3] - bounds[1] panel_states = [ @@ -457,9 +498,19 @@ def _add_declared_field_transport( ) def _advance() -> None: - nonlocal state + nonlocal state, rendered_arrows_object state = advance_simulation_state(state, numerics, config.numerics.timestep) window.simulation_fields = state.fields + if config.simulation.velocity_solved: + velocity_field = velocity_field_from_state(state) + if rendered_arrows_object is not None: + window.scene.remove(rendered_arrows_object) + rendered_arrows_object = build_vector_field_arrows( + velocity_field, config.field_display.arrow_color, config.field_display.arrow_scale + ) + if rendered_arrows_object is not None: + rendered_arrows_object.local.position = (0.0, 0.0, _ARROWS_Z) + window.scene.add(rendered_arrows_object) # Note for anyone inspecting `window.scene.children` order (found # while fixing `tests/unit/test_field_declaration_configuration. # py` for Stage 7's own legend addition): after each panel's own @@ -489,7 +540,19 @@ def _advance() -> None: float(rendered_field.values.min()), float(rendered_field.values.max()) ) - return _advance, overall_bounds + def _arrows_drawn() -> bool: + """Queried per frame, not captured once -- the same reason + `_add_solved_velocity_rendering`'s own identically-named closure + is: a solved velocity starting from rest draws no arrows on + frame zero, and this function's own `config.simulation. + velocity_solved` fixtures (Smoke Transport, Thermal Buoyancy) can + start at rest too. Always `False` when `velocity_solved` is + unset -- `rendered_arrows_object` never leaves `None` in that + case. + """ + return rendered_arrows_object is not None + + return _advance, overall_bounds, _arrows_drawn def _add_solved_velocity_rendering( @@ -722,9 +785,10 @@ def _arrows_drawn_constantly(drawn: bool) -> Callable[[], bool]: """`_add_hud`'s `show_vector_scale` for a path whose answer cannot change during the run: a static `vector_pattern` either drew arrows when the scene was built or it never will, and a run drawing no - arrows at all stays that way. Only the live velocity path - (`_add_solved_velocity_rendering`) needs a genuine per-frame query, - and it supplies its own. + arrows at all stays that way. Only the two live paths that render a + solved velocity (`_add_solved_velocity_rendering`, + `_add_declared_field_transport` when `velocity_solved` is set) need a + genuine per-frame query, and each supplies its own. """ return lambda: drawn @@ -984,10 +1048,13 @@ def bootstrap( # unaffected by this addition. run_velocity_only_simulation = config.simulation.velocity_solved and not config.fields run_simulation = run_scalar_simulation or run_velocity_only_simulation - # Vectors are drawn as arrows by two different paths (a static - # `vector_pattern`, or a live, velocity-only solved run) -- neither - # implies the other, so both report separately below, and `False` - # here is the answer for a run that takes neither path. + # Vectors are drawn as arrows by up to three independent paths (a + # static `vector_pattern`, a live velocity-only solved run, or -- + # since 2026-09-09 -- `_add_declared_field_transport`'s own solved + # velocity alongside its declared fields) -- none implies another, so + # each reports separately below and every one that fires joins + # `show_vector_scale` via `_either_path_drew_arrows`. `False` here is + # the answer for a run that takes none of them. # # **Answered by the drawing paths themselves since the Stage 7 exit # audit (2026-09-03), not computed from configuration here.** This @@ -1046,10 +1113,18 @@ def bootstrap( # step()` into this run's own render loop, one timestep per # rendered frame -- every capability before it only ever # rendered one static frame. - # Draws a colour map, never arrows, so it leaves - # `show_vector_scale` alone (`_add_field_display`'s static - # `vector_pattern` above may still have drawn some). - on_frame, declared_field_bounds = _add_declared_field_transport(window, mesh, config) + # Also draws the solved velocity as arrows when + # `config.simulation.velocity_solved` is set (added + # 2026-09-09) -- joined into `show_vector_scale` below the + # same way the velocity-only path already joins its own, + # since `_add_field_display`'s static `vector_pattern` above + # may have drawn some too and neither implies the other. + on_frame, declared_field_bounds, declared_field_arrows_drawn = ( + _add_declared_field_transport(window, mesh, config) + ) + show_vector_scale = _either_path_drew_arrows( + show_vector_scale, declared_field_arrows_drawn + ) # `legend_bounds` is left exactly as `show_fields`'s own # static overlay above set it (or `None`, if it didn't run): # every live panel captions itself directly now diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py index 812d4ae..8bb6edf 100644 --- a/tests/unit/test_bootstrap.py +++ b/tests/unit/test_bootstrap.py @@ -1015,6 +1015,49 @@ def test_bootstrap_with_velocity_solved_and_a_scalar_pressure_corrects_the_veloc ) +def test_bootstrap_with_velocity_solved_and_a_declared_field_also_draws_arrows( + tmp_path: Path, +) -> None: + """Smoke Transport's own shape -- `simulation.velocity_solved: true` + alongside a declared field -- goes through `_add_declared_field_transport`, + which colour-maps the declared field's own panel but, until now, never + drew the solved velocity carrying it as arrows at all: a real, + previously undocumented-as-deliberate gap (`src/pyflow/CLAUDE.md`'s + own `playback.py` entry names it explicitly, "a pre-existing, + separately flagged gap" from TASK-051, which built the identical + combined rendering for `pyflow play`). The moving north wall here is + `lid_driven_cavity.yaml`'s own boundary condition, the same fixture + `test_bootstrap_vector_label_scale_line_returns_once_the_flow_develops` + uses to get real (non-zero) arrows by frame 1. + """ + config_file = tmp_path / "config.yaml" + config_file.write_text( + "rendering:\n backend: offscreen\n" + "mesh:\n extent: [4, 4]\n spacing: [0.25, 0.25]\n" + "numerics:\n timestep: 0.01\n" + " boundary_conditions:\n" + " north:\n type: dirichlet\n field_values:\n" + " velocity.0: 1.0\n velocity.1: 0.0\n" + " south:\n type: dirichlet\n" + " east:\n type: dirichlet\n" + " west:\n type: dirichlet\n" + "fields:\n - name: smoke\n initial_condition: gaussian_blob\n" + "simulation:\n velocity_solved: true\n" + "field_display:\n vector_label: Velocity\n arrow_scale: 0.05\n" + " panels:\n - field: smoke\n" + ) + + window = bootstrap(config_file, max_frames=1) + + assert any(isinstance(child, gfx.Line) for child in window.scene.children), ( + "a solved velocity alongside a declared field must draw arrows too, the same way " + "_add_solved_velocity_rendering already does for a solved velocity with no field" + ) + scale_line = next(t for t in _text_children(window.scene) if "length =" in _text_content(t)) + assert "Velocity" in _text_content(scale_line) + assert "0.05" in _text_content(scale_line) + + def test_bootstrap_backend_override(tmp_path: Path) -> None: config_file = tmp_path / "config.yaml" config_file.write_text( diff --git a/tests/unit/test_check_claims.py b/tests/unit/test_check_claims.py index 4205920..89cd7c3 100644 --- a/tests/unit/test_check_claims.py +++ b/tests/unit/test_check_claims.py @@ -151,6 +151,15 @@ def test_only_tracked_markdown_files_are_read() -> None: absence of one particular directory is the point: it is the property that does not have to be revisited when some future tool writes somewhere new. + + **The belt-and-braces path-component check below false-positived + 2026-09-09, run from inside a real `git worktree` checkout of this + repository** (`.claude/worktrees/`, the same location the + original bug was about) -- `"worktrees"` is a legitimate ancestor of + `repo_root` itself there, not evidence of a nested checkout being + double-read. Narrowed to a path *relative to `repo_root`* so it + fires only for a worktree nested inside the tree being walked, which + is the only shape the original bug had. """ import subprocess @@ -169,4 +178,24 @@ def test_only_tracked_markdown_files_are_read() -> None: read = {path.resolve() for path in iter_markdown_files()} assert read == tracked - assert not any(".venv" in path.parts or "worktrees" in path.parts for path in read) + # Checked as a path *relative to `repo_root`*, not by scanning every + # component of the absolute path -- found 2026-09-09, by this exact + # false positive. This repository's own checkout can itself live at + # `.claude/worktrees/` (a `git worktree` of this same repo, + # which is how this test was running when the bug surfaced), so + # "worktrees" legitimately appears among the *ancestors* of + # `repo_root` without the bug this guards against having occurred. + # What the Stage 6 exit audit actually found was a *nested* worktree + # checkout -- one living inside the walked tree, at + # `.claude/worktrees/` relative to `repo_root` -- being read and + # double-counted; `read == tracked` above already proves that isn't + # happening (a nested worktree's files are untracked from here, so + # `git ls-files` excludes them structurally), which is why this + # assertion is a redundant belt-and-braces check, not the one this + # test depends on. + for path in read: + relative = path.relative_to(repo_root) + assert relative.parts[:2] != (".claude", "worktrees"), ( + f"{relative} was read from a nested worktree checkout" + ) + assert ".venv" not in relative.parts, f"{relative} was read from a virtualenv" diff --git a/tests/unit/test_golden_demo_annotations.py b/tests/unit/test_golden_demo_annotations.py index 06fd2dc..097c114 100644 --- a/tests/unit/test_golden_demo_annotations.py +++ b/tests/unit/test_golden_demo_annotations.py @@ -76,19 +76,23 @@ def _panel_caption(field: str, mode: str, label: str | None) -> str: def _draws_arrows(config: PyFlowConfig) -> bool: """The two ways a demo puts arrows on screen: a static - `vector_pattern`, or a velocity-only solved run - (`_add_solved_velocity_rendering`). - - **`velocity_solved` with declared fields alongside it is not one of - them** -- that configuration takes `_add_declared_field_transport`, - which colour-maps a scalar and draws no arrows at all. Mirrors - `bootstrap.py`'s own `run_velocity_only_simulation`; the two demos - that combine a solved velocity with declared fields - (`smoke_transport`, `thermal_buoyancy`) are correctly exempt. + `vector_pattern`, or any live run with a solved velocity + (`_add_solved_velocity_rendering` for velocity alone, + `_add_declared_field_transport` for velocity alongside declared + fields). + + **`velocity_solved` with declared fields alongside it used to be + exempt here, and that exemption was correct until 2026-09-09** -- + `_add_declared_field_transport` colour-mapped the declared fields' + own panels but never rendered the solved velocity carrying them. + That was a real, separately-flagged gap (`src/pyflow/CLAUDE.md`'s + `playback.py` entry named it explicitly once `playback.py`'s own + combined path, TASK-051, closed the identical gap for `pyflow play` + first), closed the same day for `pyflow run` too -- `smoke_transport` + and `thermal_buoyancy` now draw arrows exactly like every other + solved-velocity config, so they are no longer exempt. """ - return config.field_display.vector_pattern is not None or ( - config.simulation.velocity_solved and not config.fields - ) + return config.field_display.vector_pattern is not None or config.simulation.velocity_solved def _renders_a_mesh_view(config: PyFlowConfig) -> bool: