Skip to content

fix: give fdp chat/query the FDP environment again - #19

Merged
sammuli merged 5 commits into
mainfrom
fix/chat-query-environment
Sep 7, 2026
Merged

fix: give fdp chat/query the FDP environment again#19
sammuli merged 5 commits into
mainfrom
fix/chat-query-environment

Conversation

@sammuli

@sammuli sammuli commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Why

fdp chat and fdp query execvpe into python -m toksearch.llm.cli, and the LLM agent in that session fetches shot data — so it needs the FDP environment.

It hasn't had one. fe72baa marked both subcommands needs_env=False because setup_environment() raises when no device contributor is installed, which broke even fdp chat --help inside fdp's own dev env. That fixed the crash, but it also meant the preferred conversational entry point ran with no PTDATA_LOC, no default_tree_path and no BEARER_TOKEN — the agent could not reach any shot data at all.

This surfaced while correcting the TokSearch docs, which asserted the environment was configured. Rather than document the broken behaviour, fix it.

What

A third needs_env state, BEST_EFFORT. chat/query attempt setup and warn-and-continue only when no device contributor is installed at all — an LLM session that never fetches shot data is still useful. Everything else stays hard.

Setting the environment up here is safe precisely because these subcommands execvpe into a fresh process, so libfdpio and XRootD read the variables at load time — the same shape as fdp run, the known-good path. The hazard to avoid is setup_environment() followed by MDSplus use in the same process; that is not what happens here.

Degradation is gated on type, not on the except clause. A first cut caught (ValueError, KeyError), which also swallowed a mistyped --device and a genuine DeviceEnvConflict — a session would warn and then silently fail to fetch anything. NoDevicesError(ValueError) is now raised at the single _registered_names site that means "nothing installed", and only that degrades:

$ python -m fdp --device nosuch query "hi"
Error: Unknown device 'nosuch'. Registered devices: d3d, mast.   # exit 1
$ python -m fdp chat --help                                      # exit 0

--device works after the subcommand. _add_device_arg was never called for p_chat/p_query, so fdp chat --device d3d failed with "unrecognized arguments" — harmless when those subcommands ignored devices, a papercut now that the flag is load-bearing for them.

Cleanup in code being touched: dropped the vestigial handle parameter that _build_llm_cmd documented as unused, and the resolver that existed only to produce it. Rewrote __main__.py's docstring, which still described the graphviz bin/fdp collision as unfixed and pointed at a design doc that does not exist in this repo — recipe/recipe.yaml has declared graphviz a run dependency since 0.6.0, with a guard test.

Verification

  • 257 passed (was 246).
  • Mutation-verified. Reverting NoDevicesError to a plain ValueError — reintroducing exactly the bug this PR fixes — must fail the suite. It does: 4 failed, 253 passed. An earlier revision of these tests could not tell the fixed code from the broken code, because the degradation test injected the exception into a mocked setup_environment; it now runs the real path against an empty catalog.
  • Both --device spellings and the unknown-device exit code exercised against the real CLI.

Coordination

Lands with GA-FDP/toksearch#docs/refresh-2026-09 and GA-FDP/toksearch_cmf#docs/readme-refresh, which document this behaviour. The docs describe the fixed behaviour, so this needs to ship — merge, tag, and bless through fdp-core — for those docs to be true.

Known, not addressed

  • tests/test_cli.py doesn't neutralize $FDP_DEFAULT_DEVICE or Path.home the way test_composition.py's CatalogFixture does; 7 of its existing tests fail if a developer exports that variable. Pre-existing.
  • NoDevicesError/DeviceEnvConflict aren't re-exported from fdp/__init__.py, so downstream callers wanting the same distinction must reach into fdp.devices. This PR is the first to make the distinction load-bearing.

🤖 Generated with Claude Code

https://claude.ai/code/session_014c9v9tjK6XWLKxNrvXuzx4

sammuli and others added 5 commits September 4, 2026 13:07
fe72baa marked chat/query needs_env=False so they would run inside fdp's
own dev env, where no device contributor is installed and
setup_environment raises. That fixed the crash but also meant the
preferred conversational entry point ran with no PTDATA_LOC, no
default_tree_path and no BEARER_TOKEN -- the agent could not reach any
shot data.

Add a third needs_env state, "best-effort": attempt setup, and warn and
continue on the failures that mean no device is available. Strict
subcommands are untouched and still exit 1.

Setting the environment up here is safe because chat/query execvpe into
a fresh python -m toksearch.llm.cli, so libfdpio and XRootD read the
variables at load time in the new process -- the same shape as fdp run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c9v9tjK6XWLKxNrvXuzx4
The best-effort branch caught (ValueError, KeyError) wholesale, so every
ValueError from setup_environment turned into a warning for chat/query.
Two of those should stay hard:

  * `fdp --device nosuch chat` -- devices._handle raises "Unknown device
    'nosuch'". A typo in --device is exactly where failing loudly beats
    continuing: warn-and-continue handed the user an interactive chat
    whose agent silently could not fetch any data.
  * DeviceEnvConflict, which subclasses ValueError so cli.main renders it
    as a message rather than a traceback -- and thereby also inherited
    the degradation.

Narrow the condition rather than the handler: devices.NoDevicesError (a
ValueError subclass, so `except ValueError` callers are unaffected) now
marks the one situation a chat can reasonably continue past, and the
best-effort branch tests for that type. auth.AuthError is untouched.

Also, from the same review:

  * Drop `from . import llm_shims`. Nothing in cli.py used it; it existed
    so tests could write `mock.patch.object(cli.llm_shims.os, ...)`,
    which reads as if scoped to the shim module but patches the one `os`
    module either way. Those five tests now patch `cli.os`, like their
    neighbours already did.
  * Name the magic string as BEST_EFFORT. `if needs_env:` treats any
    truthy value as strict, so a typo'd "best_effort" would have been
    silently strict; a name makes it a NameError.
  * Say the exclusivity in the code (`else:`) instead of leaning on
    sys.exit to make the following print unreachable.
  * Reword both warnings: no nested parenthetical, and the AuthError one
    now names the remedy (`fdp login`) its sibling already had.
  * Assert what the strict-path guard is actually for (an Error, and no
    Warning), and assert auto_login on the query test as its chat twin
    does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c9v9tjK6XWLKxNrvXuzx4
Mutation says the previous commit's central behaviour was uncovered.
Changing devices.py back to `raise ValueError(_NO_DEVICES)` -- exactly
the bug that commit exists to fix, `fdp chat` exiting 1 in a bare dev env
instead of warning -- left the suite reporting 253 passed.

Two blind spots, both from the same cause: NoDevicesError's entire
purpose is a *type* distinction, and nothing asserted the type.

  * test_chat_survives_missing_device injected NoDevicesError as a
    side_effect on a mocked setup_environment, so it proved the CLI
    handles the type it was handed and nothing about what devices.py
    actually raises. It now patches an empty catalog and runs the real
    setup_environment, so the raise site is what is under test. This
    stays hermetic because active_handles() raises out of
    _registered_names() before explicit_device_name() consults
    $FDP_DEFAULT_DEVICE or ~/.fdp/config.toml, and before
    apply_environment() writes to os.environ -- verified by a tripwire on
    read_default_device (never called), and the second half is now
    asserted in the test itself.
  * TestNoDevicesInstalled asserted ValueError plus a message substring,
    which a plain ValueError satisfies. Now NoDevicesError, which is
    strictly stronger since it subclasses ValueError.

With the mutant in place the suite goes 4 failed, 250 passed; restored,
254 passed.

Also make the conflict half real. test_chat_exits_on_device_env_conflict
injects DeviceEnvConflict, which pins the CLI's dispatch but not the path
that produces one, so the end-to-end version joins the two conflicting
ptdata catalogs it needs, in test_composition.py: `fdp chat` over a
genuine PTDATA_JSON_INDEX_DIR collision must exit 1 rather than warn. It
lives there rather than in test_cli.py because two registered devices do
reach ~/.fdp/config.toml, and CatalogFixture already supplies the temp
$HOME that makes that safe. A second mutation (dropping the
isinstance check from the best-effort condition) fails it along with both
test_cli.py guards, so all three directions of the dispatch now bite.

_patch_catalog's entry-point/cache dance moves to _patch_entry_points so
the empty-catalog helper shares it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c9v9tjK6XWLKxNrvXuzx4
_add_device_arg is called for run, env, login, logout, ls and the
top-level parser, but was never called for chat or query. That was
harmless while those two ignored devices entirely; the previous three
commits made them compose the device environment and made an unknown
--device a hard exit, so the flag is now load-bearing for them -- yet
`fdp chat --device d3d` died with "unrecognized arguments" and only
`fdp --device d3d chat` worked. README teaches the post-subcommand
spelling (`fdp ls -D d3d`) for every other subcommand.

Declare it on both. _add_device_arg's SUPPRESS default already exists so
the flag works on either side, so nothing else changes. Ordering: `run`
has to declare it before its REMAINDER positional, but `query`'s
positional is a plain one, which cannot swallow a following optional --
verified in both orders -- so the placement there is cosmetic, matching
`run` for consistency.

Three tests, all in the four spellings a user can type: the flag after
`chat`, after `query` (also asserting the query string still reaches the
exec'd argv, and that --device does not leak into it), and the top-level
form that worked before, as a regression guard. Dropping either
_add_device_arg call fails the first two. test_chat_exits_on_unknown_device
loses its now-false parenthetical about chat not declaring the flag.

Also correct fdp/__main__.py's docstring, which still described the
graphviz `bin/fdp` collision as unfixed ("graphviz wins", "fdp run
silently invokes a graph layout tool") and pointed at
docs/2026-09-02-fdp-cli-rename.md for a planned rename. That file does
not exist in this repository, and recipe/recipe.yaml has declared
graphviz a run dependency since 0.6.0 precisely so link order gives us
the file back, with a packaged test guarding it and a rename framed as
the fallback "if it ever fails". Docstring only; no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c9v9tjK6XWLKxNrvXuzx4
The device section listed `fdp env` / `fdp run` as the commands that
compose all installed devices. Since this branch, `fdp chat` and
`fdp query` do the same -- and now accept `--device` on either side of
the subcommand -- so they belong in that bullet. Their one difference,
warning rather than exiting when nothing is installed, is worth naming
there because it is the whole reason the best-effort state exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c9v9tjK6XWLKxNrvXuzx4
@sammuli
sammuli merged commit 055c264 into main Sep 7, 2026
1 check passed
@sammuli
sammuli deleted the fix/chat-query-environment branch September 7, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant