Skip to content

feat(cli): add --client/-c filter flag, deprecate per-client booleans#464

Merged
junhoyeo merged 8 commits intomainfrom
feat/cli-client-filter-flag
Apr 26, 2026
Merged

feat(cli): add --client/-c filter flag, deprecate per-client booleans#464
junhoyeo merged 8 commits intomainfrom
feat/cli-client-filter-flag

Conversation

@junhoyeo
Copy link
Copy Markdown
Owner

@junhoyeo junhoyeo commented Apr 25, 2026

Summary

  • Replaces 19 individual per-client boolean flags (--opencode, --claude, --codex, ...) with a single repeatable, comma-separated --client/-c flag backed by a clap ValueEnum.
  • Adds a defaultClients setting in ~/.config/tokscale/settings.json for personalized filter defaults.
  • Unifies TUI internal state around a single HashSet<ClientFilter> (Synthetic is now a regular set member instead of a parallel bool include_synthetic).
  • Cache schema unchanged on disk — existing user caches keep loading without migration.
  • Legacy boolean flags remain functional but hidden from --help and slated for removal in the next major (see feat(cli)!: remove deprecated per-client boolean flags #465). Using one prints a TTY-gated stderr deprecation warning.

Why

Adding a new agent integration meant editing four places (struct field, build_client_filter array, help string, tests) and added one more line to a --help block that was already 19 lines long. Discoverability was suffering — users couldn't easily scan all supported clients, and the help output kept growing with every new integration.

Before / After

Beforetokscale --help:

--opencode             Show only OpenCode usage
--claude               Show only Claude Code usage
--codex                Show only Codex CLI usage
--copilot              Show only Copilot CLI usage
--gemini               Show only Gemini CLI usage
--cursor               Show only Cursor IDE usage
--amp                  Show only Amp usage
--droid                Show only Droid usage
--openclaw             Show only OpenClaw usage
--hermes               Show only Hermes Agent usage
--pi                   Show only Pi usage
--kimi                 Show only Kimi CLI usage
--qwen                 Show only Qwen CLI usage
--roocode              Show only Roo Code usage
--kilocode             Show only KiloCode usage
--kilo                 Show only Kilo CLI usage
--mux                  Show only Mux usage
--crush                Show only Crush usage
--synthetic            Show only Synthetic usage

After:

-c, --client <CLIENTS>     Filter by client(s). Repeatable or comma-separated (e.g. -c opencode,claude).
                           [possible values: opencode, claude, codex, cursor, gemini, amp, droid,
                            openclaw, pi, kimi, qwen, roocode, kilocode, mux, kilo, crush, hermes,
                            copilot, synthetic]

Usage

# Comma-separated
tokscale --client opencode,claude

# Repeatable (useful with shell aliases)
tokscale -c opencode -c claude

# Combine with other filters
tokscale --client opencode,claude --week --json

# Synthetic (synthetic.new) is detected from other agent sessions
tokscale --client synthetic

# Legacy still works (hidden from --help, prints TTY-gated deprecation warning)
tokscale --opencode --claude

Configuration

New top-level defaultClients field in ~/.config/tokscale/settings.json:

{
  "colorPalette": "blue",
  "defaultClients": ["opencode", "claude"]
}
  • Applies only when the user passes no --client/-c and no legacy flag.
  • CLI flags override completely — no merging.
  • Unknown ids are silently dropped (stale config never breaks tokscale).

Design Notes

ClientFilter lives in tokscale-cli, not tokscale-core

This keeps the core crate free of CLI-parsing dependencies. The Synthetic meta-client (which has no scan path of its own and is detected by post-processing other agents' sessions) is a first-class --client value without changing core invariants. ClientFilter::as_filter_str mirrors ClientId::as_str exactly, with a unit test enforcing round-trip parity.

Resolution order in build_client_filter

  1. Canonical --client/-c values (preserves user-typed order)
  2. Legacy boolean flags (in struct declaration order)
  3. defaultClients from settings.json — only when steps 1 and 2 produced nothing

CLI flags always win. Mixing canonical and legacy forms doesn't double-list.

Cache schema preserved

load_cache and save_cached_data still serialize (enabled_clients: Vec<String>, include_synthetic: bool) on disk. Projection between the unified HashSet<ClientFilter> and this legacy shape happens at the read/write boundary. Caches written by older releases keep loading.

TTY-gated deprecation warning

The legacy-flag warning fires only when stderr is a TTY, so tokscale --json | jq and assert_cmd-driven tests don't pick up unexpected stderr noise.

Migration

Old New
tokscale --opencode tokscale --client opencode
tokscale --opencode --claude tokscale --client opencode,claude
tokscale --synthetic tokscale --client synthetic

Test Plan

  • cargo build -p tokscale-cli — clean
  • cargo test -p tokscale-cli --bin tokscale374 pass, 0 fail (includes new tests for ClientFilter round-trip, declaration-order invariant, default_set semantics, defaultClients resolution, hermetic build_client_filter tests, and TUI default-state regression)
  • cargo test -p tokscale-cli --test cli_tests83 pass, 0 fail (existing integration tests using legacy flags continue to work)
  • Manual smoke tests:
    • tokscale --client opencode,claude --json
    • tokscale -c opencode -c claude --json
    • tokscale --client invalidclient → clap error with full possible-values list ✓
    • tokscale --opencode --json → still works ✓
    • defaultClients in settings.json honored when no flag passed ✓
    • Warm cache after tokscale submit produces fresh cache hit on next launch ✓

Review history

All review-bot threads addressed and resolved:

  • Synthetic-by-default behavior change (Devin BUG_0001-0003) — fixed with ClientFilter::default_set() excluding Synthetic, plus a regression test asserting the no-filter App default stays in lockstep.
  • run_warm_tui_cache ignored defaultClients (Devin BUG follow-up) — fixed by extracting resolve_default_tui_filter_set() mirroring the no-flag-launch resolution chain. Warm cache now matches what the next no-flag TUI launch wants.
  • Non-hermetic build_client_filter tests (cubic-dev-ai P2) — fixed by switching all 5 unit tests to build_client_filter_with_defaults(flags, &[]).
  • Stale legacy-flag examples in 4 READMEs (Oracle MEDIUM) — fixed in all 12 sites across en/ko/ja/zh-cn.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
tokscale Ignored Ignored Preview Apr 26, 2026 3:52am

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 6 files

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@junhoyeo

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@junhoyeo

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

junhoyeo and others added 7 commits April 26, 2026 12:45
Replace 19 individual boolean flags (--opencode, --claude, --codex, ...)
with a single repeatable, comma-separated --client/-c flag backed by a
clap ValueEnum. Cleans up --help (19 lines collapse to 1) and stops
flag-list growth as new clients are added.

Legacy boolean flags remain functional but are hidden from --help and
will be removed in the next major release; using them prints a one-line
deprecation warning to stderr only when stderr is a TTY (so JSON
pipelines and tests stay clean).

The Synthetic meta-client is exposed as a first-class --client value
without changing tokscale-core: ClientFilter mirrors ClientId on the CLI
side, leaving the core crate's clap-free layering and synthetic
post-processing semantics intact.

Updates filtering docs in README.md, README.ko.md, README.ja.md, and
README.zh-cn.md.
…ients setting

Replace the split TUI state (HashSet<ClientId> + bool include_synthetic) with
a single HashSet<ClientFilter>, so the source picker, in-memory app state,
disk cache key, and CLI flag parser all share one representation. The
Synthetic meta-client now lives as a regular set member instead of a
parallel boolean.

Cache schema unchanged on disk: load_cache and save_cached_data still
serialize (enabled_clients: Vec<String>, include_synthetic: bool) so caches
written by older releases keep loading without a migration step. The
projection happens at the read/write boundary.

Add ClientFilter::to_client_id / from_client_id / from_filter_str helpers
for boundary code that still needs Vec<ClientId> (the loader API) or
parses canonical id strings (settings.json). Reorder ClientFilter
variants to mirror ClientId::ALL declaration order so --help possible
values, the TUI picker rows, and value_variants() iteration all agree on
a single chronological ordering, with Synthetic appended at the end.

Add a defaultClients setting (top-level in settings.json) that applies
when the user passes neither --client/-c nor a legacy boolean flag. CLI
flags always override defaults completely — no merging — so 'tokscale
--client codex' is not surprised by extra entries from settings. Unknown
ids in defaultClients are silently dropped to keep stale config from
breaking tokscale.

Refactor source_picker.rs to drop its local SourceOption enum and walk
ClientFilter::value_variants() directly. Hotkey for Synthetic stays 'x'
to preserve current display; it collides with Mux which also binds 'x'
in client_ui — pre-existing bug noted with a TODO, scoped out of this
change.

Update README.md / README.ko.md / README.ja.md / README.zh-cn.md
Configuration sections to document defaultClients.
…ADME examples

Two related issues caught in code review:

1. The no-filter TUI default and the `submit` warm-cache filter set
   drifted apart. TUI launch with no `--client` flag enabled every
   ClientFilter including Synthetic; `run_warm_tui_cache()` warmed the
   cache with Synthetic excluded. Result: every TUI launch after
   `submit` was a stale-cache hit instead of fresh, defeating the
   warming. Pre-refactor behavior was 'every ClientId, include_synthetic
   = false'; this restores it.

   Fix: introduce `ClientFilter::default_set()` as the single source of
   truth for 'no filter' semantics. Both code paths (App constructor in
   tui/app.rs, the cache lookup in tui/mod.rs, and run_warm_tui_cache in
   main.rs) now consult it. Drop the now-unused `clap::ValueEnum`
   imports in tui/app.rs and tui/mod.rs.

   Synthetic detection has always been opt-in because it post-processes
   other clients' sessions to re-attribute messages — flipping it on
   silently for everyone is a real behavior change.

   Add unit tests:
   - `test_client_filter_default_set_excludes_synthetic` — guards the
     contract directly.
   - `test_app_no_filter_default_matches_default_set` — guards that
     App's constructor stays in lockstep.

2. Replace stale legacy-flag examples that still appeared in README
   sections unrelated to the 'Filtering by Platform' deprecation block:
   `tokscale models --week --claude`, `tokscale submit --opencode
   --claude ...`, `tokscale graph --opencode --claude`. After the major
   bump that removes the deprecated flags these examples become outright
   wrong; even before that they contradict the canonical interface.
   Updated in all four READMEs (en/ko/ja/zh-cn).

Test plan: 369 unit + 83 integration = 452 pass, 0 fail.
… tests hermetic

Two issues caught in PR code review:

1. **`run_warm_tui_cache` ignored `defaultClients`** (devin-ai-integration P2)

   `run_warm_tui_cache` always used `ClientFilter::default_set()` (every
   real client, Synthetic excluded), regardless of the user's
   `defaultClients` setting. `tui::run` on a no-flag launch DOES honor
   `defaultClients` via `build_client_filter` upstream. Result: a user
   with `defaultClients = ["opencode", "claude"]` got a warm cache
   storing all 18 clients while the next `tokscale` launch wanted only
   the 2 configured ones — guaranteed cache miss, defeating the warming.

   Fix: extract `resolve_default_tui_filter_set` mirroring the
   no-flag-launch resolution chain (defaultClients → fall back to
   default_set). Both `run_warm_tui_cache` and the documentation comment
   now consult it.

2. **`build_client_filter` tests were non-hermetic** (cubic-dev-ai P2)

   The 5 unit tests for `build_client_filter` were calling the wrapper
   that loads `~/.config/tokscale/settings.json`. They passed for me
   (no `defaultClients` set) but a developer with their own
   `defaultClients` would get unrelated filter ids appended to every
   assertion. Pure variant `build_client_filter_with_defaults` already
   existed; these tests now use it with `&[]` for hermetic input.

Adds:
- `resolve_default_tui_filter_set_with(&[String])` — pure variant of
  the resolver, unit-testable.
- 4 new tests: configured defaults override, empty fallback, unknown id
  drop, all-unknown fallback to `default_set`, synthetic supported via
  `defaultClients` for opt-in power users.

Test plan: 374 unit + 83 integration = 457 pass, 0 fail.
…system

- pass an empty defaults slice when resolving the submit subcommand's client
  filter so 'tokscale submit' falls back to default_submit_clients() instead
  of silently uploading the user's defaultClients subset
- add Goose to ClientFilter, hidden legacy flag, default_set membership, and
  filter round-trip helpers so the new --client/-c flag covers every
  supported client
- accept malformed elements in defaultClients without invalidating the rest
  of settings.json; non-string entries are dropped silently

Synthetic hotkey collision with Mux ('x') is left as-is (pre-existing behavior
documented in source_picker.rs); fixing it requires a wider audit of TUI
hotkey display and is scoped out of this commit.
@junhoyeo junhoyeo force-pushed the feat/cli-client-filter-flag branch from 1ccb7dc to 757f2a3 Compare April 26, 2026 03:51
@junhoyeo junhoyeo merged commit 01dcfa7 into main Apr 26, 2026
6 checks passed
@junhoyeo junhoyeo deleted the feat/cli-client-filter-flag branch April 26, 2026 06:39
junhoyeo added a commit that referenced this pull request Apr 26, 2026
…nd client filter UX (#467)

Pre-release correctness and security fixes across the four PRs landed this cycle (#464, #454, #359, #355).

## Antigravity

Trust boundary around the local language-server RPC was too loose:

- Bound RPC body sizes at 16 MiB (Content-Length, chunked, and read-to-end paths)
- Verify process identity by checking the executable path (`lsof` on macOS, `/proc/<pid>/exe` on Linux); accept paths containing either `antigravity` or `language_server` since some Antigravity-flavored servers launch from generic `language_server` binaries with `--app_data_dir antigravity`
- Probe candidate endpoints with a real RPC call and JSON-shape check (probe body capped at 4 KiB) instead of trusting any 200 response; consume HTTP headers before reading body so the cap applies to the JSON body alone
- Lock concurrent syncs on a per-cache PID lock file with bounded retry (3 attempts) instead of unbounded recursion. Eviction is gated on PID liveness only — long-running syncs no longer get stomped on by age-based timeouts.
- Enforce manifest version on load: future versions abort, older versions start fresh
- Recover corrupted manifests by moving them aside as `manifest.json.corrupt-<ts>` instead of failing every subsequent sync

## Codebuff

Parser correctness around silent data loss:

- Accumulate run-state usage across the full reverse `messageHistory` walk instead of returning on the first signal-bearing entry. Previously a newest assistant entry carrying only a model id would short-circuit the walk and silently drop real token counts on earlier entries.
- Include the source-array ordinal in the fallback dedup key so two id-less assistant messages with identical session/timestamp/model/tokens no longer collapse into a single record
- Reject non-positive numeric timestamps in the shared `parse_timestamp_value`/`parse_timestamp_str` helpers so messages with `timestamp: 0` or negative epochs fall through to chat-id / file-mtime fallback chain

## TUI + parsing

User-visible client-filter UX:

- Move `SYNTHETIC_HOTKEY` from `'x'` to `'n'`. `'x'` collided with Mux's hotkey, and the dispatch order made the displayed `[x]` for Synthetic purely cosmetic
- Enable `ignore_case` on `--client/-c` so `OPENCODE`, `Codebuff`, and `antigravity` all parse as the same canonical filter

## Test results

- `cargo test -p tokscale-cli` — 415 unit + 83 integration, all pass
- `cargo test -p tokscale-core` — 566 unit + 10 codebuff + 3 hermes, all pass
- `cargo check --workspace` — clean

Each fix has a regression test where applicable.
junhoyeo pushed a commit that referenced this pull request Apr 26, 2026
…ale on macOS (#468)

Resolves a documented-vs-actual path mismatch on macOS. `Settings::config_path()` and `star_cache_path()` were calling `dirs::config_dir()`, which returns `~/Library/Application Support/` on macOS, while `auth.rs`, `cursor.rs`, `antigravity.rs`, and all four READMEs document `~/.config/tokscale/`. macOS users editing the documented `settings.json` were touching a file tokscale never read — and the new `defaultClients` setting from #464 was silently no-op for them.

- New `crates/tokscale-cli/src/paths.rs::get_config_dir()` is now the single source of truth for the config dir. Resolution: `TOKSCALE_CONFIG_DIR` env override → macOS/Linux `$HOME/.config/tokscale` → Windows `dirs::config_dir().join("tokscale")` → `./.tokscale` last-ditch fallback.
- `Settings::load()` falls back to the legacy `~/Library/Application Support/tokscale/settings.json` on macOS only, so existing users keep their theme, scanner, and defaultClients across the upgrade. `save()` always writes the new canonical path.
- `TOKSCALE_CONFIG_DIR` env var added (mirrors `TOKSCALE_HEADLESS_DIR` semantics), documented in all four READMEs.
- Corrects the Windows-Specific Configuration sections: settings.json lives at `%APPDATA%\tokscale\` (the actual `dirs::config_dir()` default on Windows), not `%USERPROFILE%\.config\tokscale\`. Adds the missing `credentials.json` row.
- Out of scope: `auth.rs`, `cursor.rs`, `antigravity.rs` already hardcode `~/.config/tokscale/` correctly. Cache and headless dirs were left alone.
junhoyeo added a commit that referenced this pull request Apr 27, 2026
… cache eagerly, add `--write-cache` opt-in for light mode (#473)

* refactor(paths): hoist path helpers to tokscale-core and add get_cache_dir

Tokscale-core's caches (source-message bincode, pricing JSON, the
OpenCode migration record) all need the same config dir resolution that
`Settings::load()` and `load_star_cache()` use, but the helpers lived
only in tokscale-cli. Move them to a new `tokscale_core::paths` module
and re-export them from the CLI side so existing call sites stay
untouched.

Adds two new helpers:

- `get_cache_dir()` returns `<config_dir>/cache`. The next commit
  consolidates every cache (TUI display data, source-message bincode,
  pricing JSON, opencode-migration.json, Wrapped fonts/images) under
  this single subdirectory so an isolated profile
  (`TOKSCALE_CONFIG_DIR=...`) covers everything in one shot.
- `legacy_dirs_cache_dir()` and `legacy_dot_cache_tokscale_dir()` resolve
  the historic `dirs::cache_dir()/tokscale` and
  `~/.cache/tokscale` locations respectively. Both probes return None
  when `TOKSCALE_CONFIG_DIR` is set so legacy data does not leak into
  isolated profiles. Both are needed on macOS because the historic split
  put the TUI display cache under `~/.cache/tokscale` and the source
  message / pricing caches under `~/Library/Caches/tokscale`.

No behavior change yet: all current callers still resolve the same
paths they did before. The next commit switches the cache modules over
and adds the legacy fallback chain.

* feat(cache): consolidate all caches under ~/.config/tokscale/cache with legacy fallback

Tokscale state was scattered across up to four directories per platform.
On macOS specifically the source-message bincode and pricing JSON lived
under ~/Library/Caches/tokscale while the TUI display cache lived under
~/.cache/tokscale, and Wrapped image/font caches were in a third place.
The new defaultClients setting from #464 made this split visible: users
who set TOKSCALE_CONFIG_DIR for an isolated profile got a hermetic
config root but the caches still leaked from the host paths.

Consolidates every cache under <config_dir>/cache so:
- TOKSCALE_CONFIG_DIR controls everything in one shot (hermeticity)
- rm -rf ~/.config/tokscale/cache wipes only regenerable state
- The macOS-vs-Linux directory split is gone

Per-file moves:
- tui-data-cache.json: ~/.cache/tokscale/ -> <config_dir>/cache/
- source-message-cache.bin + .lock: dirs::cache_dir()/tokscale/ -> <config_dir>/cache/
- pricing-litellm.json + pricing-openrouter.json: same as above
- opencode-migration.json: same as above
- fonts/, images/ (Wrapped): ~/.cache/tokscale/<dir>/ -> <config_dir>/cache/<dir>/

Read-side migration is a one-time fallback per file: try the canonical
path first, then probe the legacy locations (gated off when
TOKSCALE_CONFIG_DIR is set). Writes always land at the new path. Legacy
files are left in place so a downgrade keeps working — no copy, no
delete.

Schemas stay at TUI=6, core=7. Path moves alone don't warrant a bump.

Closes #470

* feat(tui): render disk cache regardless of age and always background-refresh

The 5-minute cache staleness threshold previously gated the render decision on TUI launch: a Stale cache rendered with a background refresh, but a hard Miss (cache older than its hard-expiry window, schema drift, or filter-set mismatch) blocked the user behind a full re-aggregation pass. Schema drift and filter mismatch still produce a Miss because rendering wrong-shape or wrong-filter data is worse than showing nothing — but the time-based staleness check no longer affects the render decision.

The Fresh-vs-Stale distinction stays in CacheResult for future status-bar use (data age metadata), but the call site treats both as renderable. needs_background_load becomes unconditionally true on TUI launch so cached data always gets refreshed.

Closes #471

* feat(cli,settings): add --write-cache flag and light.writeCache setting

Adds an opt-in mechanism to warm the TUI cache from a --light run. Previously tokscale --light was strictly read-only — no way to opportunistically refresh the TUI cache from a CLI report even though the underlying UsageData was already computed. Daily-cron users would see a cold TUI on the next interactive launch.

CLI flags:
- --write-cache (requires --light): overwrite the TUI cache atomically after the report renders
- --no-write-cache (requires --light, conflicts with --write-cache): skip the cache write even if the setting opts in

Settings file:
- settings.json grows a light section with writeCache: false default
- #[serde(default)] keeps existing settings.json files loading unchanged

Resolution: --no-write-cache > --write-cache > settings.light.writeCache > false. Cache write uses the existing atomic temp-file rename, so a process crash mid-write never loses the cache.

Closes #472

* docs: document unified cache layout, --write-cache flag, and light.writeCache setting

- Cache layout: all regenerable caches now live under <config_dir>/cache. Documented for both posix and Windows in all four language READMEs.
- TOKSCALE_CONFIG_DIR row: clarifies the override now covers caches too.
- light.writeCache row: new opt-in setting documented in the configuration table.
- Cache directory layout subsection: explains what lives where and notes the directory is safe to delete (caches regenerate).

* style: auto-fix lint issues [skip ci]

* fix(cli,paths): refuse light cache write when filters scope it and reject empty TOKSCALE_CONFIG_DIR

The TUI cache key is `(enabled_clients, group_by)` only — it does NOT
include `--since`, `--until`, `--year`, or `--home`. `write_light_cache`
previously forwarded all of those into the DataLoader, so a
`tokscale --since 2025-01-01 --light --write-cache` invocation built a
date-filtered or home-scoped slice and saved it under the unfiltered
key. Subsequent `tokscale tui` launches would hit that cache and render
the filtered slice as if it were the default report — silent data
correctness loss flagged by both Devin and Codex review.

`write_light_cache` now refuses the write when any of those filters is
present and prints an eprintln explaining why. The CLI report still
prints normally; only the cache-warm side-effect is skipped.

Separately, `get_config_dir` previously treated an empty
`TOKSCALE_CONFIG_DIR=""` as a valid override and returned
`PathBuf::from("")`, while `is_config_dir_overridden` treated empty as
unset. After cache consolidation that mismatch could send writes to
relative paths like `./cache/...` even though the legacy fallback logic
still behaved as if no override was active. Resolver now agrees with
the predicate: empty string falls through to the platform default.

* fix(antigravity): route cache discovery through config dir

* fix(cli): forward light write-cache flags from models

* fix(cache): create message-cache lock dir with ensure_cache_dir

* test(cli): isolate wrapped and override-cache path fixtures

* test(core): make env-mutating cache path tests panic-safe

* fix(cli): treat write_light_cache as best-effort so scan failures don't tank exit codes

The cache-warm step ran AFTER the report had already been flushed to
stdout, but propagated DataLoader errors via `?`. A scan failure
(e.g. a corrupt session file in one client's directory) would surface
as a non-zero exit code on a CLI invocation the user already saw print
correctly, breaking scripts and CI pipelines that key off exit status.

Changes write_light_cache return type from `Result<()>` to `()` and
swallows loader errors via `if let Ok(data) = loader.load(...)`,
matching the pattern in run_warm_tui_cache. The call site no longer
needs `?`. The eprintln-on-skip behavior (when --since/--until/--year/
--home are set) is unchanged.

Also fixes a stray clippy carryover in paths::tests where an empty
TOKSCALE_CONFIG_DIR fallback assertion compared a PathBuf against a
\&str literal.

* fix(clients): make PathRoot::Config match get_config_dir on Windows

Antigravity's scanner uses PathRoot::Config to resolve where to look
for synced sessions. Without this fix the resolver hardcoded
`{home_dir}/.config/tokscale` as the non-Linux fallback, while
get_antigravity_cache_dir() (the writer side) routes through
paths::get_config_dir() which calls dirs::config_dir() on Windows. The
two diverged on Windows: tokscale antigravity sync wrote to
%APPDATA%\\tokscale\\antigravity-cache\\ while the scanner read from
%USERPROFILE%\\.config\\tokscale\\antigravity-cache\\sessions\\, so
synced data silently never appeared in reports.

PathRoot::Config now mirrors get_config_dir's platform branches:
TOKSCALE_CONFIG_DIR override > Linux XDG_CONFIG_HOME > Windows
dirs::config_dir() > generic `{home}/.config/tokscale`. macOS still
falls through to `{home}/.config/tokscale` because get_config_dir()
deliberately overrides dirs::config_dir() there (which would otherwise
return ~/Library/Application Support/).

New regression test test_path_root_config_uses_dirs_config_dir_on_windows
locks the writer/scanner agreement on Windows.

* test(cli): pin XDG_CONFIG_HOME in offline test command and seed pricing cache at canonical path

CI runners can have XDG_CONFIG_HOME set globally outside the sandboxed
HOME, so the post-#470 cache resolver leaks the binary's read+write
to the host's config dir. Three integration tests (test_monthly_json_offline_*,
test_submit_offline_*) flaked on Linux runners because the cost
expectations assumed the sandboxed pricing cache (or absence thereof),
but the binary was finding pricing data via the host's
$XDG_CONFIG_HOME/tokscale/cache/ — or worse, would write to it.

offline_cmd_with_home now sets XDG_CONFIG_HOME alongside the existing
XDG_CACHE_HOME and XDG_DATA_HOME pins, mirroring cmd_with_home and
keeping the binary's cache resolution fully inside the temp dir.

write_pricing_cache also seeds the canonical
.config/tokscale/cache/ directory so tests that exercise the new path
work even when legacy fallback is suppressed (e.g. via
TOKSCALE_CONFIG_DIR override).

* test(core): pin XDG_CONFIG_HOME in cache fallback tests so CI stays hermetic

CI runners that set XDG_CONFIG_HOME globally were leaking the binary's
canonical cache root outside the test's sandboxed HOME, so
SourceMessageCache::load read from the host's
$XDG_CONFIG_HOME/tokscale/cache/source-message-cache.bin (or wrote
there) instead of the temp dir. Three message_cache tests
(test_source_message_cache_round_trip,
load_falls_back_to_legacy_dirs_cache_path,
load_falls_back_to_legacy_dot_cache_path) and one pricing/cache test
(load_falls_back_to_legacy_dirs_cache_path) flaked on Linux runners as
a result.

Adds sandbox_cache_env / restore_cache_env helpers to message_cache so
the round-trip test pins HOME + XDG_CONFIG_HOME + XDG_CACHE_HOME
together. The two legacy-fallback tests gain explicit
XDG_CONFIG_HOME pinning to match. The pricing test does the same.
All 4 now pass on Linux CI.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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