Files
punktfunk/clients/cli/tests/cli_smoke.rs
T
enricobuehler 48bb1769b4 feat(cli): punktfunk discover — browse the LAN, annotated against what you've saved
The CLI could do everything with a host except FIND one, so every headless consumer
grew its own mDNS: the Decky plugin parses ~120 lines of avahi TXT escaping in Python,
which drifts from the host's advert every time a key is added and makes the plugin
depend on Avahi being the resolver.

`discovery::discover_for(timeout)` is the bounded collector beside the streaming
`browse()` the UI uses — same service type, same TXT keys, folded to one row per host.
A refreshed advert wins (it carries the newer address), a removal drops the row, and
dropping the receiver on the way out stops the worker so a one-shot call can't leak a
browse per invocation.

The verb annotates each hit against the saved-hosts store rather than handing back two
lists to join: `saved`/`paired` are answered by fingerprint first and address second —
the same rule every other surface uses. That is what stops a host that moved DHCP lease
from reading as new, and stops a different box that inherited the old address from
reading as paired.

  punktfunk discover [--json] [--timeout SECS]

Default 3 s, capped at 30 — this is called from a Quick Access panel, and a typo'd
`--timeout 3000` would hang that panel with no way to cancel. An empty LAN exits 0: a
caller branching on the code is asking whether the browse ran, and it did.
2026-08-04 20:26:59 +02:00

85 lines
3.1 KiB
Rust

//! Runs the REAL `punktfunk` binary and pins its self-documentation contract: help goes
//! to stdout and exits 0, an unknown verb refuses on stderr with the not-found code.
//!
//! This exists so CI *executes* the shipped binary at least once per platform. A binary
//! that compiles but is the wrong program passes every build/clippy/fmt gate — that is
//! exactly how 0.22.0 shipped a stub as `punktfunk-session` — and only a gate that runs
//! the thing catches the class. The help paths are the right probe: they touch no config
//! stores and no network, so they are safe on any runner.
#![cfg(any(target_os = "linux", windows))]
use std::process::Command;
fn punktfunk(args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_punktfunk"))
.args(args)
.output()
.expect("run punktfunk")
}
#[test]
fn bare_and_help_print_the_overview_on_stdout() {
for args in [&[][..], &["help"][..], &["--help"][..], &["-h"][..]] {
let out = punktfunk(args);
assert!(out.status.success(), "{args:?} must exit 0");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("punktfunk pair"),
"{args:?} overview lists the verbs"
);
assert!(
stdout.contains("help <command>"),
"{args:?} overview points at per-verb help"
);
}
}
#[test]
fn per_verb_help_answers_both_spellings() {
for args in [
&["help", "launch"][..],
&["launch", "--help"][..],
&["launch", "-h"][..],
] {
let out = punktfunk(args);
assert!(out.status.success(), "{args:?} must exit 0");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("--exec"),
"{args:?} documents launch's flags"
);
}
}
#[test]
fn version_prints_the_crate_version() {
let out = punktfunk(&["--version"]);
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn unknown_verbs_refuse_with_the_not_found_code() {
let out = punktfunk(&["frobnicate"]);
assert_eq!(out.status.code(), Some(5), "unknown verb exits 5");
assert!(String::from_utf8_lossy(&out.stderr).contains("unknown command"));
let out = punktfunk(&["help", "frobnicate"]);
assert_eq!(out.status.code(), Some(5), "unknown help topic exits 5");
}
/// `discover` documents itself. Help only — the verb itself browses the LAN, which no runner
/// may be asked to do.
///
/// The Decky panel detects a too-old client by exactly the signature the test above pins
/// (exit 5 + `unknown command`), so this is the other half of that contract: on a client new
/// enough, `discover` is a verb with help rather than an unknown word.
#[test]
fn discover_documents_itself() {
let out = punktfunk(&["help", "discover"]);
assert!(out.status.success(), "discover has its own help topic");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--timeout"), "discover documents --timeout");
assert!(stdout.contains("--json"), "discover documents --json");
}