Commit abad7515 by PLN (Algolia)

fix(gig-log): resolve the Ardour session without argv — a 3-rung evidence ladder

A GUI-launched Ardour has a BARE argv (only gig-up passes the session as an
argument), so argv-matching alone left the files lens blind on the real rig.
Resolution now climbs: argv → /proc/<pid>/cwd holding a *.ardour → open-fd
symlink targets (most-fds wins on ambiguity, logged once). Injectable proc_root
makes each rung testable against a fake /proc; 9 new tests reproduce the exact
live gap (163 green). check-mix.py shares the latent argv-only bug — flagged in
the sync comment, not fixed this pass.
parent 33897540
......@@ -1204,18 +1204,22 @@ def test_osc_lens_sample_reports_inactive_before_anything_arrives():
# ---- FilesLens: session resolution must never crash off-stage ------------ #
def test_resolve_audiofiles_dir_never_raises_when_no_ardour_is_running():
def test_resolve_audiofiles_dir_never_raises_against_the_real_machine():
"""Same contract as check-mix.py's running_session(): absence of Ardour
degrades to None, never an exception — this runs in CI with no /proc
entry for Ardour at all."""
(or of evidence for it) degrades to None, never an exception. This runs
against the REAL /proc, so on a machine where Ardour genuinely is
running this may legitimately return a Path — only the type is pinned."""
got = gl.FilesLens.resolve_audiofiles_dir()
assert got is None or isinstance(got, Path)
def test_files_lens_sample_is_inactive_and_silent_by_default_in_tests(capsys):
"""No Ardour running in the test sandbox: the lens must report inactive
(not raise, not claim a false 'recording'), logging the reason ONCE."""
lens = gl.FilesLens()
def test_files_lens_sample_is_inactive_and_silent_with_no_evidence(tmp_path, capsys):
"""An EMPTY fake /proc (no processes at all): the lens must report
inactive (not raise, not claim a false 'recording'), logging the reason
ONCE. Uses the injectable proc_root — real /proc is not touched, so this
is deterministic regardless of whether Ardour happens to be running on
the machine this test executes on."""
lens = gl.FilesLens(proc_root=tmp_path)
first = lens.sample()
assert first == {"active": False, "rec": False, "el": 0.0, "by": 0}
second = lens.sample()
......@@ -1225,6 +1229,139 @@ def test_files_lens_sample_is_inactive_and_silent_by_default_in_tests(capsys):
assert len(err_lines) <= 1
# ---- the argv > cwd > open-fds evidence ladder (2026-09-05 live-rig gap) - #
#
# The bug that prompted the ladder: a real, running Ardour was missed
# because it was opened from the GUI's recent-sessions list, so its argv
# was bare (just the binary, no session path) — argv alone (check-mix.py's
# whole check) is provably insufficient. These build a fake /proc tree —
# real directories and real symlinks, no mocking — shaped exactly like the
# genuine one (numeric pid dir, `cmdline`, a `cwd` symlink, an `fd/` dir of
# symlinks) so the pid-scan and symlink-following code runs unmodified.
def _fake_proc(root: Path, pid: str = "123", argv: tuple[bytes, ...] = (b"ardour-9.7.0",),
cwd: Path | None = None, fds: tuple[Path, ...] = ()) -> Path:
proc = root / "proc"
proc.mkdir(exist_ok=True)
pdir = proc / pid
pdir.mkdir()
(pdir / "cmdline").write_bytes(b"\0".join(argv) + b"\0")
if cwd is not None:
os.symlink(cwd, pdir / "cwd")
fdd = pdir / "fd"
fdd.mkdir()
for i, target in enumerate(fds):
os.symlink(target, fdd / str(i))
return proc
def _session(root: Path, name: str = "Foo") -> Path:
"""A minimal real session dir: <root>/<name>/<name>.ardour."""
d = root / name
d.mkdir()
(d / f"{name}.ardour").write_text("<Session/>")
return d
def test_ladder_prefers_argv_when_it_is_present(tmp_path):
session = _session(tmp_path, "Argv")
other = _session(tmp_path, "Decoy") # cwd points elsewhere
proc = _fake_proc(tmp_path,
argv=(b"ardour-9.7.0", str(session / "Argv.ardour").encode()),
cwd=other)
got = gl.FilesLens.resolve_session_file(proc)
assert got == session / "Argv.ardour"
def test_ladder_falls_back_to_cwd_when_argv_is_bare(tmp_path):
"""THE live-rig bug, reproduced: argv carries only the binary name."""
session = _session(tmp_path, "GuiLaunch")
proc = _fake_proc(tmp_path, argv=(b"ardour-9.7.0",), cwd=session)
got = gl.FilesLens.resolve_session_file(proc)
assert got == session / "GuiLaunch.ardour"
def test_ladder_falls_back_to_open_fds_when_argv_and_cwd_both_fail(tmp_path):
session = _session(tmp_path, "ViaFd")
audiofiles = session / "interchange" / "ViaFd" / "audiofiles"
audiofiles.mkdir(parents=True)
wav = audiofiles / "Tidal01.wav"
wav.write_bytes(b"\0" * 64)
unrelated_cwd = tmp_path / "home"
unrelated_cwd.mkdir()
proc = _fake_proc(tmp_path, argv=(b"ardour-9.7.0",), cwd=unrelated_cwd, fds=(wav,))
got = gl.FilesLens.resolve_session_file(proc)
assert got == session / "ViaFd.ardour"
def test_ladder_returns_none_with_no_evidence_anywhere(tmp_path):
unrelated = tmp_path / "somewhere"
unrelated.mkdir()
proc = _fake_proc(tmp_path, argv=(b"ardour-9.7.0",), cwd=unrelated,
fds=(unrelated / "not-a-real-fd-target",))
assert gl.FilesLens.resolve_session_file(proc) is None
def test_ladder_ignores_processes_that_are_not_ardour(tmp_path):
session = _session(tmp_path, "NotArdour")
proc = _fake_proc(tmp_path, pid="456",
argv=(b"firefox", str(session / "NotArdour.ardour").encode()))
assert gl.FilesLens.resolve_session_file(proc) is None
def test_files_lens_log_ambiguity_dedups_the_same_counts_but_not_a_new_one(tmp_path, capsys):
"""The instance-level 'log the reason ONCE' contract, same as `_log` —
exercised directly since sample()'s FILES_RESCAN_S guard means a second
`sample()` call would not even re-run resolution to hit this path."""
lens = gl.FilesLens()
a, b = tmp_path / "a", tmp_path / "b"
lens._log_ambiguity({a: 1, b: 3})
lens._log_ambiguity({a: 1, b: 3}) # same counts: no second line
lines = [ln for ln in capsys.readouterr().err.splitlines() if "ambiguous" in ln]
assert len(lines) == 1
lens._log_ambiguity({a: 1, b: 4}) # counts CHANGED: logs again
lines = [ln for ln in capsys.readouterr().err.splitlines() if "ambiguous" in ln]
assert len(lines) == 1
def test_ladder_logs_ambiguity_once_and_picks_the_dir_with_most_open_fds(tmp_path):
"""Two distinct session dirs show up via open fds (e.g. a leftover fd
into an old session): must not pick silently — the caller is told."""
minority = _session(tmp_path, "Stale")
majority = _session(tmp_path, "Live")
proc = _fake_proc(tmp_path, argv=(b"ardour-9.7.0",),
fds=(minority / "one.wav", majority / "a.wav",
majority / "b.wav", majority / "c.wav"))
for f in (minority / "one.wav", majority / "a.wav", majority / "b.wav",
majority / "c.wav"):
f.write_bytes(b"x")
calls = []
got = gl.FilesLens.resolve_session_file(proc, log_ambiguity=calls.append)
assert got == majority / "Live.ardour"
assert len(calls) == 1
assert set(calls[0]) == {minority, majority}
def test_files_lens_end_to_end_through_the_fake_proc_root(tmp_path):
"""The full production path (not just the pure ladder helpers): a
FilesLens pointed at a fake proc_root, resolving via cwd (the live-rig
case), then actually detecting growth in the resolved audiofiles dir."""
session = _session(tmp_path, "E2E")
audiofiles = session / "interchange" / "E2E" / "audiofiles"
audiofiles.mkdir(parents=True)
wav = audiofiles / "Tidal01.wav"
wav.write_bytes(b"\0" * 100)
proc = _fake_proc(tmp_path, argv=(b"ardour-9.7.0",), cwd=session)
lens = gl.FilesLens(proc_root=proc)
first = lens.sample()
assert first["active"] is True and first["rec"] is False # no prior size yet
wav.write_bytes(b"\0" * 500) # grew
second = lens.sample()
assert second["active"] is True and second["rec"] is True
assert second["by"] == 400
# ---- Recorder.poll_rec(): the emit-on-transition-or-while-recording rule - #
class _FixedLens:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment