Skip to content

test(harness): shared test harness, coverage baseline, and case-insensitive name uniqueness - #18

Merged
maosuarez merged 6 commits into
mainfrom
batch/tier1-harness-and-naming
Aug 5, 2026
Merged

test(harness): shared test harness, coverage baseline, and case-insensitive name uniqueness#18
maosuarez merged 6 commits into
mainfrom
batch/tier1-harness-and-naming

Conversation

@maosuarez

Copy link
Copy Markdown
Owner

Summary

Lands the two foundational pieces the rest of this batch builds on: a shared HTTP/vault test harness with a coverage baseline, and case-insensitive uniqueness for environment and project names.

Closes #11
Closes #12

Type of Change

  • 🐛 Bug fix
  • 🧪 Tests

What Changed

#11test_support::TestVault builds an unlocked temp vault + bound axum::Router via the same ApiState::new the real server uses. api/mod.rs gains a build_router split so tests and start_server construct state identically. 136 test functions, up from 17. The stale Postman collection is deleted: it asserted a removed GET /health field and none of its 15 requests carried the now-mandatory scope, so all 15 would 422 — nothing in CI ever ran it.

#12idx_environments_name_nocase, plus the same pre-check retrofitted to idx_projects_name_nocase (which could brick VaultDb::open on installs already holding a colliding pair). Colliding rows are deterministically renamed before the index is created, with a report under env_name_dedup_v1. Write path returns 409 CONFLICT with no SQL text in the body; ambiguous name resolution returns 409 AMBIGUOUS_SCOPE instead of guessing.

Dedup name-probing compares via SQLite's own LOWER() on both sides rather than pre-folding in Rust — mixing Rust's full-Unicode fold with SQLite's ASCII-only one made non-ASCII collisions miss the check and then trip the table's unique constraint, turning a routine dedup into a hard init_schema failure. Regression test included.

CIcargo test gains --tests; the four suites under src-tauri/tests/ were not being built or run.

Known gaps (deliberate, tracked)

  • TestVault is reachable only from the lib crate; src/bin/crypt-env and crypt-env-mcp are separate targets and use in-file tests.
  • No timeout anywhere in the harness; a lock-holding deadlock would hang rather than fail.
  • Coverage misses on 5 of the 7 tracked files relative to the plan's targets.
  • environments.name has no case-insensitive uniqueness, unlike the now-fixed projects.name #12 does not close the MCP/CLI resolvers. crypt-env-mcp's pick_environment_id and the CLI's scope.rs/project.rs still take the first name match rather than returning AMBIGUOUS_SCOPE. The DB index prevents ASCII collisions from existing; non-ASCII pairs remain resolvable first-match on those paths. Out of scope here.

Testing

  • cargo check passes
  • cargo test --lib --bins --tests — 221 passed, 0 failed
  • pnpm tauri dev (not run — Linux/WSL, no Windows toolchain this session)
  • Tested on: Linux (WSL2)

Security Implications

No crypto changes. #12 hardens an error path: handle_save_project / handle_save_environment no longer echo the underlying error, which could carry raw sqlx text (table/column/index names), into API responses — the detail is logged server-side instead. Rejections never echo the submitted name.

maosuarez and others added 6 commits August 4, 2026 17:30
…ect names

Issue #12: adds idx_environments_name_nocase (project_id, name COLLATE
NOCASE) and retrofits the same pre-check to idx_projects_name_nocase, so
an install that already holds a case-colliding pair (e.g. MyApp/myapp,
production/Production) is deterministically deduped (lowest id keeps its
name, losers get -2/-3 suffixes, audit persisted under settings key
env_name_dedup_v1) instead of bricking VaultDb::open.

Unique-constraint violations are now detected via sqlx's
is_unique_violation() and mapped to a stable "conflict:" sentinel string
instead of substring-matching sqlx's error text, closing a raw-SQL leak
in the 500 fallback path. project::resolve_environment now rejects
ambiguous case-insensitive project/environment matches instead of
silently taking the first one (SQLite NOCASE folds ASCII only, so
non-ASCII collisions like PRODUCCIÓN/producción survive the index and
need this Unicode-aware check) — the HTTP layer maps that to 409
AMBIGUOUS_SCOPE.

New src-tauri/tests/environment_naming.rs covers T1-T8/T11 from the
plan plus a projects-side dedup case; sqlx added to [dev-dependencies]
to seed pre-collision state that's otherwise unreachable through the
public API once the index exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…overage baseline; drop stale Postman collection

Implements docs/plans/issue-11-test-coverage-and-postman.md: a crate::test_support
harness (TestVault fixture + router()/req() oneshot helpers) so every module's
private/pub(crate) logic is unit-testable without a Tauri runtime or live socket,
plus 119 new test cases across api/, project/, vault/, db/, share/, the CLI's
scope resolver, and the MCP binary's environment-picking logic (17 -> 136 total).

Two pure refactors enable the harness, both behavior-preserving:
- api::build_router / ApiState::new extracted from start_server (pub(crate),
  not pub — zero external API widening).
- vault::set_item_global's fork logic extracted the same way create_project_item
  already was, so the Tauri command becomes a thin delegating wrapper.
- crypt-env-mcp's resolve_environment_id split into network-fetching +
  pure pick_environment_id, mirroring the plan's resolve_environment_id split.

Deletes the stale Postman collection (asserted a removed /health field, every
request 422'd without the mandatory scope params) and replaces it with curl
examples + the executed api::tests::* suite in docs/reference.md. Adds
.github/workflows/test.yml running cargo test on push/PR with an informational
cargo-llvm-cov summary step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lity, sentinel visibility

Addresses adversarial review of the issue-12 case-insensitive-uniqueness
work:

- Critical: next_free_{project,environment}_name folded the rename
  candidate with Rust's to_lowercase() (full Unicode) but compared it
  against SQLite's own LOWER(name) (ASCII-only). For a non-ASCII base
  this made the two folds disagree, so a real collision against a
  pre-existing suffixed sibling (e.g. producciÓn-2) was missed, the
  "free" candidate was accepted, and the following UPDATE then hit the
  table's exact-match UNIQUE constraint -- init_schema returned Err and
  the vault could not be opened at all. Fixed by comparing via
  LOWER(name) = LOWER(?) so both sides are folded by SQLite consistently.
  Added a regression test reproducing the exact crash shape.

- env_name_dedup_v1 (the sole reversal path for an otherwise
  irreversible rename) is now persisted per-rename, inside the dedup
  loops themselves, instead of batched and written only after both
  CREATE UNIQUE INDEX statements succeed -- a renamed row can no longer
  land in the DB without also being recorded, even if a later migration
  step fails or the process is killed mid-run.

- PROJECT_NAME_CONFLICT bumped from private to pub (matching
  ENVIRONMENT_NAME_CONFLICT, also bumped from pub(crate) to pub): each
  src/bin/* target is its own crate separate from crypt_env_lib, so
  pub(crate) was invisible cross-crate. Exported
  project::AMBIGUOUS_MATCH_PREFIX so the ambiguity sentinel is a named
  constant instead of a bare string literal at every match site.

- persist_rename_report no longer silently discards prior rename
  history on a JSON parse failure -- logs to stderr and preserves the
  raw value verbatim under env_name_dedup_v1_corrupt before starting a
  fresh list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The five branches in this batch add four integration targets under
src-tauri/tests/ (environment_naming, path_containment, project_relay,
vault_integration). `cargo test --lib --bins` builds none of them, so
they would have been invisible to CI while appearing to be covered.

Refs #11
@maosuarez
maosuarez merged commit 39a269c into main Aug 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant