Skip to content

fix(tracking): keep the test suite off the user's real database - #3758

Open
KuSh wants to merge 1 commit into
rtk-ai:developfrom
KuSh:fix/test-db-isolation
Open

fix(tracking): keep the test suite off the user's real database#3758
KuSh wants to merge 1 commit into
rtk-ai:developfrom
KuSh:fix/test-db-isolation

Conversation

@KuSh

@KuSh KuSh commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

cargo test writes into ~/.local/share/rtk/history.db — the contributor's own tracking database. Two independent routes.

1. In-process. Several tracking tests call Tracker::new(), which resolves RTK_DB_PATH → config → platform default exactly like a real invocation (src/core/tracking.rs, get_db_path). They insert synthetic rows (rtk cmd1_test_<pid>, rtk git status test_<pid>, …) and never clean up, so rtk gain counts them as real usage.

This has been happening for a long time. The database on my machine held 3069 rows matching %_test_%, the oldest dated 2026-04-04.

2. A spawned rtk child. Eight integration test files run the binary directly via Command::new(env!("CARGO_BIN_EXE_rtk")). The child is built without cfg(test), so it resolves the real path like any other invocation — and nothing in the child can detect that its parent is a test.

Why it matters beyond polluted stats

A test that spawns many rtk children in parallel can destroy the database. I hit exactly this while writing an unrelated test:

$ sqlite3 ~/.local/share/rtk/history.db "PRAGMA integrity_check;"
*** in database main ***
Tree 2 page 8287 cell 449: 2nd reference to page 5617
Tree 2 page 8287 cell 448: Rowid 182442 out of order
Tree 3 page 8216 cell 81: 2nd reference to page 5624
wrong # of entries in index idx_pf_timestamp

After that the failure is self-sustaining: rtk gain stops working, and the tracking tests themselves fail with Error code 11: The database disk image is malformed, so the suite cannot pass again until the user repairs their database by hand. Recovery via sqlite3 old.db ".recover" | sqlite3 new.db worked but lost 57 of 55828 commands rows.

Fix

Each route needs a different mechanism, because cfg(test) reaches only one of them.

In-process — structural. Tracker::new() now goes through resolve_db_path(), which under cfg(test) returns a per-process file in the temp dir unless the test set RTK_DB_PATH itself:

fn resolve_db_path() -> Result<PathBuf> {
    #[cfg(test)]
    {
        if std::env::var_os("RTK_DB_PATH").is_none() {
            return Ok(test_db_path());
        }
    }
    get_db_path()
}

No test can reach the real database by accident, whatever it calls — as opposed to every test remembering to set RTK_DB_PATH, which is what silently failed. The 18 existing Tracker::new() call sites are unchanged.

Tracker::open(path) is added for tests that want an explicit database. get_db_path keeps its own semantics, so test_db_path_env_and_default still exercises the real resolution order.

Spawned children — at the call site. tests/common/rtk_command() builds the Command with RTK_DB_PATH already pointed at a temp file; all eleven spawn sites across the eight files now use it.

Enforcement. core::db_isolation_tests covers both halves:

  • scans src/ and tests/ for code that spawns the binary without RTK_DB_PATH, naming the offending files;
  • asserts a default Tracker in a test build resolves under the temp dir.

The scan is what found the eight integration files — I had identified three by reading.

Verification

Real database is untouched across a full run:

suite exit      : 0
mtime           : 1787999006 -> 1787999006   UNCHANGED
commands rows   : 56062 -> 56062
test-shaped rows: 3069 -> 3069
integrity       : ok

temp DBs created by the run:
/tmp/rtk-itest-history-771556.db
/tmp/rtk-itest-history-771579.db
...

cargo fmt --all --check, cargo clippy --all-targets (zero warnings) and cargo test --all all pass.

Note for maintainers

Existing databases still carry the accumulated rows. They're identifiable by the _test_<pid> shape in rtk_cmd, so a cleanup is:

DELETE FROM commands WHERE rtk_cmd LIKE '%\_test\_%' ESCAPE '\';

Worth deciding whether that belongs in a migration or is left to users — this PR does not touch existing data.

`cargo test` wrote into `~/.local/share/rtk/history.db`, the contributor's own
tracking database. Two independent routes, both now closed.

**In-process.** The tracking tests call `Tracker::new()`, which resolves
`RTK_DB_PATH` → config → platform default like any real invocation, so they
inserted synthetic rows (`rtk cmd1_test_<pid>`, …) into real savings history and
never cleaned up. `rtk gain` counted them. On this machine the database already
held 3069 such rows, the oldest dated 2026-04-04.

`Tracker::new()` now resolves through `resolve_db_path`, which under `cfg(test)`
redirects to a per-process file in the temp dir unless the test set `RTK_DB_PATH`
itself. Structural rather than advisory: no test can reach the real database by
accident, whatever it calls, and the 18 existing `Tracker::new()` call sites did
not have to change. `Tracker::open(path)` is added for tests wanting an explicit
database; `get_db_path` keeps its own semantics so its unit test still exercises
the real resolution order.

**A spawned rtk child.** Eight integration test files ran the binary directly,
and a child is built without `cfg(test)`, so it resolves the real path. Nothing
in the child can tell that its parent is a test, so the fix has to be at the call
site: `tests/common/rtk_command()` builds the `Command` with `RTK_DB_PATH`
already pointed at a temp file, and all eleven spawn sites use it.

Beyond fabricating rows, this could destroy the database outright: a test that
spawns many rtk children in parallel left a 35 MB history.db malformed
(`2nd reference to page 5617`, `wrong # of entries in index idx_pf_timestamp`),
after which `rtk gain` fails and the tracking tests themselves fail with
`database disk image is malformed` — so the suite cannot pass again until the
user repairs it by hand. Recovery via `.recover` cost 57 of 55828 rows.

`core::db_isolation_tests` enforces both halves: it scans `src/` and `tests/` for
code that spawns the binary without `RTK_DB_PATH`, and asserts a default
`Tracker` in a test build resolves under the temp dir. Verified end to end — the
real database's mtime, row count and integrity are unchanged across a full
`cargo test --all`, which now creates temp databases instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant