Baniloo Baniloo

March 25, 2026

The test suite that almost wasn't

34 integration tests, a Windows TTY problem, and what a protocol owes its future contributors

This was supposed to be straightforward. Write integration tests for every CLI command. Prove the full lifecycle works end to end. Ship it.

It was not straightforward.

The problem with rpassword on Windows

The test suite was written using assert_cmd — the standard Rust crate for black-box CLI testing. You spawn the binary, feed it stdin, assert on stdout and stderr. Clean, simple, well-understood.

Except rpassword on Windows does not read from stdin. It reads from CONIN$ — the raw console input device. Every test that called loomed commit, loomed log, or loomed verify hung indefinitely, waiting for a human to type a passphrase into a terminal that did not exist.

The first instinct was to run the suite in WSL. Linux rpassword falls back to stdin when there is no TTY — this seemed like it would work. It did not. WSL attaches a pseudo-terminal. rpassword finds /dev/tty and blocks exactly the same way.

The fix

OpenSSL has OPENSSL_PASS. GPG has --passphrase-fd. SSH has SSH_ASKPASS. Every serious cryptographic tool solves this problem the same way: in non-interactive mode, credentials come from the environment.

One function in commands/mod.rs:

pub fn read_passphrase(prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
    if let Ok(p) = std::env::var("LOOMED_PASSPHRASE") {
        return Ok(p);
    }
    Ok(rpassword::prompt_password(prompt)?)
}

Five call sites across the command files — one line each. The production binary is unchanged when LOOMED_PASSPHRASE is not set.

One interesting failure

The first complete run: 31 passing, 1 failing, 3 hanging.

The hangers were obvious — log.rs had been missed, still calling rpassword directly. One-line fix.

The failure was more interesting. The test expected loomed commit to fail when given the wrong passphrase. It does not. It encrypts the commit with whatever AES key it derives, writes the file, and exits cleanly. There is no validation at write time. There cannot be, by design.

The protocol detects the wrong passphrase at read time. loomed verify --chain with the correct passphrase cannot decrypt a commit written with the wrong one. The test was fixed to reflect actual protocol behaviour — commit with wrong passphrase, then verify with correct passphrase, verify fails.

What a protocol owes its future contributors

34 tests across every command — init, status, add, commit, log, show, verify — including error paths, fail-fast precondition checks, tamper-detection, and a full end-to-end lifecycle test.

A protocol is not just a specification. It is everything a future implementer has to understand before they can trust what they are building on. A test suite that only runs if you have a specific terminal configuration is not a test suite — it is a trap for the next person.

The tamper-detection tests write garbage bytes into a committed .lmc file and assert that loomed verify exits with code 1. Every time the suite runs, it proves the fundamental security property of the protocol still holds. That is what a test suite is for.

110 tests total. 0 failures. The chain holds.


Next is typed payload display in loomed show and cloud sync design.