5fb77263

By: Michael Lynch <git@mtlynch.io>

Return startup failures through testable command runners

Both binaries previously mixed global flag parsing, dependency construction, serving, and process termination. SQLite migrations and handler construction called log.Fatal below main, which prevented callers from adding context, running deferred cleanup, or directly testing malformed flags and startup failures.

Litestream keeps main as a thin exit-code boundary and exposes command Run methods that accept context and arguments, use private flag.FlagSet values, and return errors. That structure makes command behavior testable in-process while preserving one clear place that decides whether an error terminates the program.

Introduce Main.Run(ctx, args) for the app and Tinybeans importer, migrate both to local flag sets, and return setup and serving failures. Make SQLite, migrations, SMTP setup, and handler construction return errors. Constructors close partially initialized resources, commands own every resource they successfully create, and errors.Join preserves a cleanup failure alongside the operation that triggered it. Direct runner and constructor tests now cover help, invalid arguments, database-open failure, and startup session-cleanup failure.

The most delicate issue was the existing Litestream shutdown contract. Ordinary failures must return errors, but an intentional signal shutdown must still drain HTTP, close SQLite, and return successfully so the Litestream parent performs its final sync. The runner tracks store ownership to avoid a deferred double close while retaining that zero-exit signal path. Migrating handlers.New also required updating route-level test helpers without exporting implementation seams solely for tests.

No external blockers were encountered. Focused tests, the full Go checks, and every flake target passed before the amendment.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 7s 2s 2s 10s 7s
Eval 26s 22s 22s 48s 15s
Build 48s 2m25s - - 3m00s
Suite 7s 2m51s - - 54m52s

Timeline

0s10s30s40s3m50s4m4m10s4m20s4m30s