Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Phase log 89: Rust branch coverage push, behaviour tests, ten bugs fixed, PR #46

Date: 2026-10-19 (the work ran 2026-10-18 evening to 2026-10-19). Continues phase log 88 (main at 4e545b1, v0.56.1). Everything here is merged to main (head 685ed5b) by pull request #46, rebased, twelve commits: the ten planned groups plus two CI fixes below. Handoffs are gitignored; never put this file in a commit line.

Goal

The maintainer asked first for "rust nightly test 100%", then "I dont wanna run again, just make it above 80%" (the nightly branch figure was 66.06 percent, 4,956 of 7,502 branches), then "continue / keep adding test", and twice stressed the rule: tests must check that the code behaves as he expects, never fill a quota. 80 percent needs about 1,050 more covered branches. Nothing was measured afterwards (he did not want the workflow run again and local coverage runs are off the table), so the new branch figure is unknown. Do not claim it moved.

Current Progress

Baseline data came from CI, not a local run: gh run download 39160284093 -n rust-branch-coverage (summary.json, per file counts) and gh run download 39163355123 -n rust-coverage (lcov.info, line hits). Uncovered LINE ranges per file were computed from that lcov (scratchpad unc.txt); the old branch artifact has no per-branch records, so I changed coverage-rust.yml to export lcov.info with BRDA lines from the nightly job (cargo +nightly llvm-cov report --branch --lcov). It has not run yet; it needs one dispatch.

Verification at the end (all green): 19 unv-cli contract targets (contract_users_text 8, contract_nodes 11, contract_nodes_text 5, contract_starters 3, contract_export_import 7, contract_authority 5, contract_entry_fields 8, contract_import_bitwarden_types 6, contract_import_onepassword_layout 3, contract_scan_text 7, contract_doctor 6, contract_doctor_checks 6, plus health, scan_enrich, entries, feeds_uid, import_vault, migrate, keepass); unv-app parity 29, parity_edge 1, import_vaults 14, lib 118; cargo test -p vault-core all (467 lib tests plus the integration files, 0 failed); Vitest cli-parity-edge, cli-parity, bundle-import, starters-parity 63 passed; cargo clippy -p unv-app -p unv-cli -p vault-core --all-targets -- -D warnings, cargo fmt --all --check, prettier, eslint, tsc --noEmit clean. Not run: the whole workspace, the full Vitest suite, src-tauri, unv-server tests, the browser suites (CI runs them on the PR).

Files added or changed, and why

unv-cli/tests (all run the real unv, several against an in-process unv-server):

  • contract_users_text.rs: user, class and token commands in human output (empty list message, header and rows, no hash, token refused before minting, --out is 0600, strict-write on and off).
  • contract_nodes_text.rs (+ on_text helper in common/server.rs): node list/show/revoke/policy/approver in text, --yes and --dry-run, token lifetimes 90s 15m 2h 1d, bad lifetime is invalid (10) before anything is minted.
  • contract_authority.rs: delegated administration. A class with manage-users only cannot touch peers, cannot grant manage-classes or delete-projects, cannot assign a class at or above its own; every refusal is exit 5 and changes nothing.
  • contract_export_import.rs: unv export per format with --project, --name, --profile full refusal vault-wide, JSON export then import --json round trip (replaces the vault).
  • contract_starters.rs: project add --type wireguard|docker|nginx equals the golden starters; experimental types need --experimental and start empty here.
  • contract_entry_fields.rs: icon by magic bytes (SVG refused, size cap, nothing created on refusal), labels, generated values, --cert @path (but --key @x is just a key), --expires, --var, --key-stdin.
  • contract_scan_text.rs: scan text report, recovery codes at two or none left, BIP39 checksum (phrase never printed), session never verified, rotate and compromise, status including a missing salt (refused, never regenerated).
  • contract_doctor_checks.rs: pool state checks, loose pool file mode, entry without id, --fix, human report layout.
  • contract_import_bitwarden_types.rs: every Bitwarden item kind, archive of unknown types and id-less items, folders.
  • contract_import_onepassword_layout.rs: real 1Password layout (details.loginFields, typed section values).

vault-core/tests: session_import.rs (curl, PowerShell, HAR, Set-Cookie, Firefox error paths; credentials dropped by name, never value), bundle_scope.rs, bundle_import.rs, totp_import.rs (hand-built protobuf for Google migration payloads), type_emit.rs (npmrc, pypirc, cargo, docker config, netrc, DSN, libpq, JDBC, Wi-Fi QR, recovery codes, BIP39), php_config.rs.

Parity: tests/fixtures/parity/starters.json (generated from the TS factories with esbuild; asserted by tests/starters-parity.test.ts and contract_starters.rs), and the edge pair vault-edge.json (generated by a scratchpad Python script, about 28 KB) + edge-exports.json (written by tests/cli-parity-edge.test.ts, regenerate with PARITY_UPDATE=1 npx vitest run tests/cli-parity-edge.test.ts) + unv-app/tests/parity_edge.rs.

Source: unv-app/src/node_cmd.rs, entries.rs, exporters.rs, doctor.rs, password_sources.rs; vault-core/src/bundle_import.rs, php_config.rs; src/ts/chunk-ops.ts, src/ts/bundle-import.ts; tests/fixtures/parity/traefik.yaml (regenerated); .github/workflows/coverage-rust.yml; CHANGELOG.md (Unreleased).

The bugs that mattered

Ranked by what they did to a user. Each has a regression test that fails with the fix reverted (checked by stashing the source file and running the test).

  1. Compose .env with an unresolvable reference (exporters.rs, chunk-ops.ts): the container started with a blank secret (app) or the escaped text \${Nope/api_key} (CLI), and nothing said why. Now the placeholder is written through raw on both sides, which Compose refuses loudly.
  2. Traefik middleware values unquoted: a resolved secret such as p@ss #1 "q" loaded back as p@ss, so basic-auth users were silently wrong. Now JSON-quoted like routers and services. The old traefik.yaml golden changed by one line.
  3. Kubernetes Deployment env block mis-indented in TS and Rust: any env_var field made YAML no parser accepts. The old golden had no env fields, so nothing caught it. A Vitest test parses the output with the yaml package.
  4. Bundle import marked unparsed source text public (x = 'pw' + 'hunter2' reads as prose): it could be printed. New unread flag in Rust and TS; golden parity files still pass.
  5. 1Password import lost real credentials: only details.fields and sections were read, not details.loginFields (where a real .1pux keeps username and password, marked by designation), and typed values ({"concealed": ...}, {"totp": ...}) read as nothing. Implemented from the published structure, not verified against a real export (rule in .claude/rules/integrations.md: say "implemented from the published format").
  6. entry add --expires soon was accepted, so the scan (which compares the first 10 characters as text) never found the entry expired. Now a real YYYY-MM-DD, optionally followed by T....
  7. unv doctor pool check was dead: it read state["pools"][*]["members"] as arrays, but the file is vaults.<vault key>.<pool>.members.<entry key> (objects). Dangling members were never reported. check_pools now takes the vault key (pool::vault_key); document_findings (the desktop, which has only a document) passes None and says so instead of risking another vault's members as false warnings.
  8. PHP config numbers: 0755 read as decimal 755, 0x1A broke the rest of the array, 2.5E-2 became null plus a stray value (Nextcloud config.php import).
  9. node approver ls cut the fingerprint with an ellipsis that approver rm answered with "No such approver".
  10. --var NOEQUALS exited 1 (generic) instead of 10 (invalid).

Left unfixed (cosmetic): session_import::keep_header reports the same header twice when only its case differs; the PowerShell cookie reader grabs the next cookie's strings when a cookie has fewer than four arguments (DevTools always writes four); write_fields in totp_import indexes a non-object entry (dead guard, entries are always objects).

Decisions made

  • Tests assert documented behaviour or an outside format (PHP manual, npm/PyPI/Cargo/Docker/netrc file formats, RFC 6265, BIP39 vectors), not the implementation read back. Where a pinned value would only record a quirk, I dropped the case (an npmrc for dockerhub produced //index.docker.io/v1//:..., not asserted).
  • The exporter edge fixture uses the TypeScript output as the oracle (the stated parity contract: "two implementations, one golden file"). 1 of about 60 outputs differed, and every real bug was in both implementations, so the oracle method found them by review of the golden, not by the diff. Read the generated edge-exports.json for invalid YAML (I parsed it with Python yaml); that is where bugs 2 and 3 showed.
  • check_pools returns an "ok, checked by unv doctor" finding when there is no vault key rather than guessing.
  • Did not touch AGENTS.md-listed invariants. Did not change the import --json global flag collision (known finding in phase log 83); the tests assert the envelope it produces.
  • Rejected: inventing a way to create an orphan bundle member through the CLI (saving a vault already strips dangling bundle fields, so the --fix branch for them cannot be reached from the CLI).

What Worked / What Didn't Work

Worked: reading uncovered line ranges from the CI lcov and choosing files by behaviour worth pinning; writing a probe shell script against target/debug/unv with the harness environment before writing assertions (unset UNV_SERVER_URL, set HOME and XDG_*); stashing a single source file to prove a regression test; the TS-oracle fixture. Did not work: guessing CLI flags (--projects takes the project id, lowercase slug; --no-password needs --key-file); asserting exact message text I had not seen; cargo fmt rewrote my assertion lines, so a later Python string edit failed until I re-read the file; chaining many commands in one tool call (a long chain was refused twice, the same steps in short calls went through).

How the merge went (PR #46)

Branch behaviour-tests-and-fixes from origin/main, ten commit groups, pushed, PR opened, then two CI failures that no local run showed:

  1. Windows lint and test both failed to compile: contract_doctor_checks.rs had a helper (pools_file) used only by #[cfg(unix)] tests. Under -D warnings an unused function is an error on Windows and stops the whole Windows test build, so every other new test was untested there until it was fixed. Fix: #[cfg(unix)] on the helper (commit 80c51f1). Windows then passed (Test windows 11m20s).
  2. CodeQL: 4 new alerts, 1 critical and 3 high, all in test code (code scanning blocks medium and above on this ruleset): rust/hard-coded-cryptographic-value on const PW: &str = "member-password-12345" in contract_authority.rs, and rust/cleartext-logging on three assertion messages that formatted the secret being checked ("preview leaked {secret}", "the token alone on stdout: {secret}"). Fixed in code, not dismissed: the password is built at run time (format!("member-{}-pw-12345", std::process::id())), messages name an index. Commit 685ed5b. The older contract_users_admin.rs has the same PW literal; it was triaged before, left alone.

Final: 27 of 27 checks passed, CI result green, 0 open alerts on the merge ref, merged with gh pr merge --rebase --delete-branch (no --admin). The k8s, traefik, compose, nginx, haproxy and apache real-validator jobs passed on the new generated output, which is independent confirmation of bugs 2, 3 and 1.

Next Steps

Only the maintainer can: dispatch coverage-rust.yml once after merge to learn the real branch figure and get per-branch records; submit the OpenSSF forms (phase log 88); rotate CODECOV_TOKEN.

Doable next: use the per-branch artifact to pick the next files. Largest gaps at the 66.06 baseline were unv-app/src/ entries.rs (134 branches), unv-server/src/lib.rs (134), exporters.rs (87, partly covered now), password_sources.rs (85, Bitwarden and 1Password partly covered now), unv-cli/src/main.rs (75), enrich.rs (69), recovery_ops.rs (68), node_cmd.rs (61), scan.rs (61), nodes.rs in vault-core and unv-server, users.rs, config_check.rs. SFTP destinations.rs needs the OpenSSH container.

Notes for whoever picks this up

  • Test files: a helper used only by #[cfg(unix)] tests needs #[cfg(unix)] itself, or Windows CI fails to compile every test in the crate. Never format a secret into an assertion message or hard-code a password literal: CodeQL blocks the PR.
  • Everything in .claude/, the session records/ and AGENTS*.md is gitignored; edits there never reach a commit.
  • Commit rules the maintainer confirmed on 2026-10-19: commit and push are allowed when messages carry no co-author or session line, changes are grouped (3 to 10 commits), and each message is one plain sentence of about 10 to 15 words. Work goes on a branch (main is protected), pull request, merge by rebase after CI result is green, never --admin. Commit messages and PR text never name agents or AI tooling.
  • list and scan on an empty vault exit 3 ("Vault is empty"), they do not print an empty list. The scan severity label in JSON is med, not medium.
  • The pool state file is $XDG_STATE_HOME/unv/pools.json; a member only appears once it has been handed out, so a test of a dangling member needs two pool next calls.
  • The harness Vault::run adds --json; import FILE --json therefore answers in an envelope (finding in phase log 83).
  • State after the merge: local main equals origin/main (685ed5b), working tree clean, the branch is deleted locally and on the remote. gh pr merge 46 --rebase --delete-branch changes the SHAs, so the twelve commits on main are not the ones that were pushed (8705a2c is the first, 685ed5b the last).
  • Verification that did not run: the nightly branch coverage after these tests. Dispatch coverage-rust.yml once on main; its branches job now also uploads lcov.info with per-branch records, which is what the next test round should read (gh run download <id> -n rust-branch-coverage).
  • The CI log archive route worked when gh run view --log-failed was too truncated: gh api repos/R/actions/runs/ID/logs > logs.zip, unzip, grep the per-job text file. Code-scanning alerts for a PR: gh api "repos/R/code-scanning/alerts?ref= refs/pull/N/merge&state=open".

Open questions (maintainer decides, nothing is blocked on them)

Left undecided on 2026-10-19 at his request ("I will decide these later"). Each says what is at stake and what I would do.

  1. Global instructions conflict. ~/.claude/CLAUDE.md still says "NEVER run git commit or git push ... not even when I say commit". He gave standing permission on 2026-10-19 when the commit rules hold (branch not main, 3 to 10 groups, one plain sentence of about 10 to 15 words, no co-author or session line, no AI mention, merge by rebase after CI result, never --admin). The memory graph and the local AGENTS.md carry the new rule; the global file does not. At stake: a later session that reads only the global file will refuse to commit, or will hand a one-liner instead. I would rewrite that section to state the permission and its conditions.
  2. Run the nightly branch measurement? gh workflow run coverage-rust.yml on main once gives the real branch figure (last known 66.06 percent, 80 percent is the goal) and, new in this PR, per-branch records. He did not want it run again during the session. At stake: every claim about "above 80 percent" is unverified until it runs. I would run it once now that the tests are merged.
  3. How to count and track the 80 percent goal. The OpenSSF gold criterion test_branch_coverage80 is per language. Options: keep chasing Rust branches file by file with the per-branch artifact, or accept a lower figure for src-tauri and the SFTP code that cannot run on a plain Linux runner. At stake: where the next sessions spend their time.
  4. 1Password import is implemented from the published .1pux structure, not checked against a real export. At stake: a real export may differ (the module itself says so). I would ask him for one sanitised export from a named 1Password version and keep it under tests/fixtures/, as .claude/rules/integrations.md requires before anything is called verified.
  5. import FILE --json answers in an envelope because the local --json flag and the global one share a name (phase log 83 finding). The new tests assert the envelope as it is. At stake: importing a .env in envelope mode still fails. Decide whether to rename the local flag (a visible CLI change).
  6. Cosmetic findings I did not fix: the same header in two cases is listed twice in the session paste report (session_import::keep_header); the PowerShell cookie reader grabs the next cookie's strings if a cookie has fewer than four arguments (DevTools always writes four); a dead guard in totp_import::write_fields. At stake: nothing a user sees today.
  7. Older test password literal. unv-cli/tests/contract_users_admin.rs still has const PW: &str = "member-password-12345", the same pattern CodeQL blocked in the new contract_authority.rs. It was triaged earlier and does not block. At stake: a future CodeQL rule change. I would build it at run time like the new file.
  8. A bundle member whose bundle is gone cannot be produced from the CLI (saving drops the dangling fields), so the --fix branch for it in unv doctor is exercised only by an old-build vault. Decide whether to keep or delete that branch.