Ambient system companions over one privacy-preserving signal daemon (aggregate-only, no keystroke content): a git-driven terminal garden and IOKit hardware collectors. ambient daemon macos privacy terminal

crates/signald/tests/streaming.rs

75 lines · 2431 bytes

 1//! Live-streaming acceptance test (spec §1.3): a subscriber gets the current
 2//! value of each metric immediately on connect (last-value cache), then receives
 3//! updates as values change — over the real Unix socket, not just the hub.
 4
 5use std::io::BufReader;
 6use std::os::unix::net::UnixStream;
 7use std::path::{Path, PathBuf};
 8use std::thread;
 9use std::time::{Duration, Instant};
10
11use signal_schema::{wire, Signal, SignalName, Source, Value, SCHEMA_VERSION};
12use signald::hub::Hub;
13use signald::publish;
14
15fn sig(name: SignalName, value: f64) -> Signal {
16    Signal {
17        schema_version: SCHEMA_VERSION,
18        ts: 1,
19        source: Source::Terminal,
20        name,
21        value: Value(value),
22        tag: None,
23    }
24}
25
26#[test]
27fn subscriber_sees_cached_value_then_a_live_update() {
28    let socket = unique_socket();
29    let hub = Hub::new();
30
31    // A value published before anyone connects must still reach a subscriber
32    // (the last-value cache).
33    hub.publish(sig(SignalName::KeysPerMin, 12.0));
34
35    let serve_hub = hub.clone();
36    let serve_socket = socket.clone();
37    thread::spawn(move || {
38        let _ = publish::serve(&serve_socket, serve_hub);
39    });
40
41    let stream = connect(&socket);
42    let mut reader = BufReader::new(stream);
43
44    // 1. On connect: the cached snapshot arrives immediately.
45    let first = wire::read_frame(&mut reader).unwrap().expect("snapshot frame");
46    assert_eq!(first.name, SignalName::KeysPerMin);
47    assert_eq!(first.value, Value(12.0));
48
49    // 2. A change after subscribing is streamed live.
50    hub.publish(sig(SignalName::KeysPerMin, 99.0));
51    let update = wire::read_frame(&mut reader).unwrap().expect("live update frame");
52    assert_eq!(update.name, SignalName::KeysPerMin);
53    assert_eq!(update.value, Value(99.0));
54
55    let _ = std::fs::remove_file(&socket);
56}
57
58fn connect(socket: &Path) -> UnixStream {
59    let deadline = Instant::now() + Duration::from_secs(5);
60    loop {
61        if let Ok(s) = UnixStream::connect(socket) {
62            return s;
63        }
64        assert!(Instant::now() < deadline, "signald socket never came up");
65        thread::sleep(Duration::from_millis(20));
66    }
67}
68
69fn unique_socket() -> PathBuf {
70    let nanos = std::time::SystemTime::now()
71        .duration_since(std::time::UNIX_EPOCH)
72        .unwrap()
73        .as_nanos();
74    std::env::temp_dir().join(format!("signald-stream-{}-{nanos}.sock", std::process::id()))
75}