Raise the quality bar to felis's #13

Merged
natsukium merged 8 commits from chore/quality into main 2026-10-14 19:29:06 +09:00
Owner

Adopts felis's verification stack (just, nextest, proptest, cargo-fuzz, cargo-mutants, machete, lychee, MSRV) and fixes what it found.

Fixes:

  • a grid dimension past 16 bits was silently truncated (as u16)
  • serve --speed with a tiny positive value panicked on Duration/Instant overflow
  • unknown flags and unreadable values were silently ignored; now usage errors (exit 2)

Tests: property tests for the parser, unit grammar, recorder, session ids and theme hint; serve pacing/lifecycle/preface tests; CLI end-to-end (record captures what serve replays). Mutation sweep went from 51 missed of 156 to only equivalent mutants (excluded with reasons). Two fuzz targets ran 60s each without a crash.

See docs/reference/testing.md.

Adopts felis's verification stack (just, nextest, proptest, cargo-fuzz, cargo-mutants, machete, lychee, MSRV) and fixes what it found. Fixes: - a `grid` dimension past 16 bits was silently truncated (`as u16`) - `serve --speed` with a tiny positive value panicked on Duration/Instant overflow - unknown flags and unreadable values were silently ignored; now usage errors (exit 2) Tests: property tests for the parser, unit grammar, recorder, session ids and theme hint; serve pacing/lifecycle/preface tests; CLI end-to-end (record captures what serve replays). Mutation sweep went from 51 missed of 156 to only equivalent mutants (excluded with reasons). Two fuzz targets ran 60s each without a crash. See docs/reference/testing.md.
The parser read `grid.rows` and `grid.cols` as u64 and narrowed them
with `as u16`, so a header claiming 65536 rows parsed as a 0-row grid
and 70000 as 4464: a replay seeded with a size the file never stated.
felis's grid dimensions are u16, so a larger value is not a size any
felis session had; the header is now refused with the field named, and
the reference states the range.
`serve` computed each unit's deadline with Duration::from_secs_f64 and
added it to an Instant, both of which panic past their range. --speed
accepts any positive float, so `--speed 1e-320` (or a modest slowdown
of a long recording) overflowed and took the connection's task down.
A deadline beyond what an Instant holds now means the unit never comes
due, and the replay holds the screen it reached, as an idle session
would.
The flag parsers read std::env::args in place and forgave everything:
an unknown flag (`--sokcet`) was skipped, a value that did not parse
(`--rows many`, `--rows 70000`) silently kept the default, and a flag
at the end of the line took an empty value. Each recorded or served
something other than what was typed, with no hint why. Every one of
those is now a usage error that names the problem, exits 2 and prints
the banner, as the reference already promised for a missing flag.

Parsing takes its argument list instead of reading the process's, so
each flag and each refusal is pinned by a unit test; none were before.
A .fcast is untrusted input to every player, and serve decodes each
frame through felis-grid's felis-json and the unit grammar before it
binds. Proptest covers the shapes its generators know; libFuzzer
explores the bytes they do not. Two targets: `parse`, which also
asserts the monotonic timestamps of what it accepts, and `replay`,
which drives the decode and grammar serve runs.

That check is now `serve::validate`, public so the target calls what
serve calls rather than a copy. The felis-json version check moved into
it, so a recording at a foreign version is refused by the same path.
The fuzz crate is its own workspace so the sanitizer flags never reach
the main build; the seeds are the gen_* sample recordings.
The unit tests pinned one hand-picked case per rule, so a rule held
only where someone had thought to look. felis core states its parser
and grid invariants as proptest properties; this does the same for the
envelope and the unit grammar:

- the parser never panics, round-trips any valid recording whatever
  its blank lines, line endings, unknown header fields and extension
  frames, and refuses a decreasing timestamp, a bare unrecorded kind or
  an oversized grid dimension for the stated reason;
- the unit tracker agrees with a model written from the spec's wording,
  and serve accepts exactly the recordings that grammar does;
- the recorder writes closed units whole, stamped with their last
  frame's arrival, and pulls exactly when a burst or cycle closes;
- session ids round-trip through hex, and the theme hint keeps exactly
  the well-formed colors.
A cargo-mutants sweep left 51 of 156 mutants alive. The CLI bodies had
no test at all, and serve's pacing, pull gate and connection lifecycle
were reachable only through the happy path. The survivors left after
these tests are equivalent mutants, excluded with their reason in
.cargo/mutants.toml.

- `tests/cli.rs` drives the binary: the usage contract, serve refusing
  a non-recording, record waiting for a late daemon, the theme hint
  from a named, discovered and broken config, and record capturing
  frame for frame what serve replays, with config discovery isolated
  from the user's own felis config;
- serve: units due at t/speed, the non-positive speed fallback, a pull
  releasing only due cycles, the held final screen still answering, a
  detach before and after attach, the preface refusal of a foreign
  major, image replay, and a recording cut off inside a cycle;
- the Display of parse and unit errors, rehydrate_end inside a cycle,
  and the open unit the tracker reports.

Frame reads in the serve tests time out after three seconds, so a
regression fails instead of hanging the run.
chore: adopt felis's verification stack
Some checks failed
ci / cargo fmt / clippy / test / deny (pull_request) Failing after 42s
ci / cargo fuzz smoke (pull_request) Successful in 1m59s
ab0aaad875
felis-cast's gate was fmt, clippy, cargo test and deny, while the
engine it speaks to also runs nextest, property tests, fuzzing,
mutation testing and an MSRV build. A tool that replays untrusted
recordings into felis clients should be held to the same bar, and the
property, fuzz and mutation layers just added need a way to run.

- justfile: `just check` is the CI gate (fmt, clippy, nextest, deny,
  machete, offline doc links); `msrv`, `fuzz`, `mutants` and
  `mutants-pr` run the rest. felis drives its stack the same way.
- The dev shell gains just, cargo-machete, cargo-mutants, cargo-fuzz
  and lychee, and an `msrv` shell builds at the `rust-version` floor,
  which nothing checked while the default toolchain is nightly.
- cargo-mutants runs through nextest under a debuginfo-free profile,
  skips the sample-authoring examples, and excludes the two retry-count
  mutants no test should pin.
- CI runs `just check`, the MSRV build and the package build, plus a
  30-second smoke per fuzz target.
- docs/reference/testing.md records the layers, what each pins, and
  the commands; the replay-path coverage item leaves the backlog, since
  its gaps are now tested.
docs: link felis-web-component by its forge URL
All checks were successful
ci / cargo fuzz smoke (pull_request) Successful in 1m59s
ci / cargo fmt / clippy / test / deny (push) Successful in 37s
ci / cargo fuzz smoke (push) Successful in 1m58s
ci / cargo fmt / clippy / test / deny (pull_request) Successful in 1m28s
ad2864ecf6
The README linked the sibling repository as ../felis-web-component,
which resolves only in a checkout laid out next to it; on the forge, in
CI and for anyone else it is a dead link, and the offline link check
now in the gate refuses it.
natsukium deleted branch chore/quality 2026-10-14 19:29:06 +09:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
natsukium/felis-fcast!13
No description provided.