diff --git a/Cargo.lock b/Cargo.lock index e2c8d16..eeb325d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4605,6 +4605,7 @@ dependencies = [ "serde_default", "serde_json", "serde_yaml", + "tempfile", "tokio", "tracing", "tracing-actix-web", @@ -9992,9 +9993,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" [[package]] name = "spirv" diff --git a/libs/edge-toolkit/tests/registry.rs b/libs/edge-toolkit/tests/registry.rs new file mode 100644 index 0000000..69fbd2b --- /dev/null +++ b/libs/edge-toolkit/tests/registry.rs @@ -0,0 +1,50 @@ +//! Covers the `AgentRegistry` persistence + session-lookup paths the ws-server integration tests skip: +//! save/load round-trip (including the missing-file and populated branches), reconnect, `agent_session`, +//! and the `with_pending_direct_messages` builder. `S = String` stands in for the runtime session handle +//! (which is `#[serde(skip)]`, so it is never persisted). +#![cfg(test)] + +use std::collections::BTreeMap; + +use edge_toolkit::ws::AgentConnectionState; +use edge_toolkit::ws_server::{AgentRecord, AgentRegistry}; +use tempfile::tempdir; + +#[test] +fn save_load_roundtrip_and_session_lookup() { + let registry = AgentRegistry::::default(); + + // Fresh connection (Assigned), then a reconnect for the same id which swaps in a new session. + let (agent_id, _assigned) = registry.connect_agent(None, "agent-1".to_string(), "127.0.0.1", "sess-a".to_string()); + let (_same_id, _reconnected) = registry.connect_agent( + Some(agent_id.clone()), + "agent-x".to_string(), + "127.0.0.2", + "sess-b".to_string(), + ); + + assert_eq!( + registry.agent_session(&agent_id).as_deref(), + Some("sess-b"), + "reconnect keeps the newest session" + ); + assert_eq!(registry.agent_session("nobody"), None, "unknown agent has no session"); + + // Persist and reload. Sessions are #[serde(skip)], so they return as None, but the agent survives. + let dir = tempdir().unwrap(); + let path = dir.path().join("registry.yaml"); + registry.save(&path).unwrap(); + let reloaded = AgentRegistry::::load(&path).unwrap(); + assert_eq!(reloaded.agent_session(&agent_id), None, "sessions are not persisted"); + + // load() on a missing file yields an empty registry rather than erroring. + let empty = AgentRegistry::::load(&dir.path().join("absent.yaml")).unwrap(); + assert_eq!(empty.agent_session(&agent_id), None); +} + +#[test] +fn agent_record_with_pending_builder_replaces_the_map() { + let record = AgentRecord::::new(AgentConnectionState::Disconnected, None, None) + .with_pending_direct_messages(BTreeMap::new()); + assert!(record.pending_direct_messages.is_empty()); +} diff --git a/services/ws-server/Cargo.toml b/services/ws-server/Cargo.toml index 8fb7e70..98a29a4 100644 --- a/services/ws-server/Cargo.toml +++ b/services/ws-server/Cargo.toml @@ -50,5 +50,8 @@ tracing-subscriber.workspace = true utoipa = { workspace = true, optional = true } uuid.workspace = true +[dev-dependencies] +tempfile.workspace = true + [lints] workspace = true diff --git a/services/ws-server/src/lib.rs b/services/ws-server/src/lib.rs index 0b1de91..43d42be 100644 --- a/services/ws-server/src/lib.rs +++ b/services/ws-server/src/lib.rs @@ -3,6 +3,7 @@ pub use et_ws_service::{AgentSession, WsAgentRegistry}; pub mod config; pub mod routes; +pub mod tls; pub use self::routes::health; use crate::config::Config; diff --git a/services/ws-server/src/main.rs b/services/ws-server/src/main.rs index b30b2d9..8c8c5dc 100644 --- a/services/ws-server/src/main.rs +++ b/services/ws-server/src/main.rs @@ -13,13 +13,12 @@ use clap::Parser; use et_modules_service::list_modules; use et_ws_server::config::Config; use et_ws_server::configure_app; +use et_ws_server::tls; use et_ws_service::load_registry; use tracing::{error, info}; use tracing_actix_web::TracingLogger; use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; -mod tls; - #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { diff --git a/services/ws-server/src/tls.rs b/services/ws-server/src/tls.rs index 36790ca..ead614f 100644 --- a/services/ws-server/src/tls.rs +++ b/services/ws-server/src/tls.rs @@ -1,7 +1,6 @@ #![expect( - clippy::single_call_fn, clippy::unwrap_used, - reason = "TLS bootstrap helpers are single-use by main() and panic on invalid PEM / cert-gen failure intentionally" + reason = "TLS bootstrap helpers panic on invalid PEM / cert-gen failure intentionally; callers are main + tests" )] use std::path::Path; @@ -13,12 +12,14 @@ type CertKeyPair = ( rustls::pki_types::PrivateKeyDer<'static>, ); +#[must_use] pub fn load_tls_certs(cert_filename: &Path, key_filename: &Path) -> CertKeyPair { let cert_der = rustls::pki_types::CertificateDer::from_pem_file(cert_filename).unwrap(); let key_der = rustls::pki_types::PrivateKeyDer::from_pem_file(key_filename).unwrap(); (cert_der, key_der) } +#[must_use] pub fn generate_tls_certs(cert_filename: &Path, key_filename: &Path) -> CertKeyPair { let certified = rcgen::generate_simple_self_signed(vec![ "localhost".to_string(), @@ -35,6 +36,7 @@ pub fn generate_tls_certs(cert_filename: &Path, key_filename: &Path) -> CertKeyP (cert_der, key_der) } +#[must_use] pub fn build_tls_server_config( cert_der: rustls::pki_types::CertificateDer<'static>, key_der: rustls::pki_types::PrivateKeyDer<'static>, diff --git a/services/ws-server/tests/tls.rs b/services/ws-server/tests/tls.rs new file mode 100644 index 0000000..3be61ea --- /dev/null +++ b/services/ws-server/tests/tls.rs @@ -0,0 +1,32 @@ +//! Covers the TLS bootstrap helpers: self-signed generation, PEM round-trip load, and rustls config build. +#![cfg(test)] + +use et_ws_server::tls::{build_tls_server_config, generate_tls_certs, load_tls_certs}; +use tempfile::tempdir; + +#[test] +fn generate_write_load_and_build_server_config() { + let dir = tempdir().unwrap(); + let cert = dir.path().join("cert.pem"); + let key = dir.path().join("key.pem"); + + // Generate a self-signed pair, which also writes both PEM files to disk. + let (gen_cert, gen_key) = generate_tls_certs(&cert, &key); + assert!( + cert.exists() && key.exists(), + "generate_tls_certs must write both PEM files" + ); + let from_generated = build_tls_server_config(gen_cert, gen_key); + assert!( + from_generated.alpn_protocols.is_empty(), + "no ALPN protocols are configured by default" + ); + + // The freshly-written PEMs must load back into a der pair that also builds a valid config. + let (loaded_cert, loaded_key) = load_tls_certs(&cert, &key); + let from_loaded = build_tls_server_config(loaded_cert, loaded_key); + assert!( + from_loaded.alpn_protocols.is_empty(), + "reloaded config also has no ALPN protocols" + ); +} diff --git a/utilities/int-gen/src/wit/upstream.rs b/utilities/int-gen/src/wit/upstream.rs index a823de6..b290b04 100644 --- a/utilities/int-gen/src/wit/upstream.rs +++ b/utilities/int-gen/src/wit/upstream.rs @@ -272,13 +272,11 @@ fn fetch_and_trim_webgpu(deps_root: &Path) -> Result<(), Error> { /// Parse the upstream `webgpu.wit` via `wit-parser`, filter the parsed AST /// down to our compute-only subset, and re-emit using `wit-encoder`. #[expect( - clippy::unnecessary_wraps, - clippy::single_call_fn, clippy::unwrap_used, clippy::unwrap_in_result, - reason = "Result lets caller use ? like fetch_* helpers; called once; wit-parser anyhow::Error, inputs literals" + reason = "inputs are trusted (upstream WIT or the committed fixture); a parse failure is a bug, so unwrap" )] -fn strip_webgpu(raw: &str) -> Result { +pub fn strip_webgpu(raw: &str) -> Result { let mut resolve = wit_parser::Resolve::default(); let _stub: wit_parser::PackageId = resolve.push_str("wasi-io-stub.wit", WASI_IO_STUB).unwrap(); let _stub: wit_parser::PackageId = resolve diff --git a/utilities/int-gen/tests/webgpu_trim.rs b/utilities/int-gen/tests/webgpu_trim.rs new file mode 100644 index 0000000..11c8e95 --- /dev/null +++ b/utilities/int-gen/tests/webgpu_trim.rs @@ -0,0 +1,27 @@ +//! Exercises the `strip_webgpu` WIT-trimming pipeline on the committed compute-subset fixture. +//! +//! The upstream fetch (`run` / `fetch_*`) is network-bound, but the parse-filter-reemit core is pure, so we +//! drive it directly against the trimmed `webgpu.wit` under `generated/` -- a self-contained `wasi:webgpu` +//! package with records, variants, resources, enums and flags, which walks every arm of `mutate_interface` +//! and `collect_type_refs`. +#![cfg(test)] + +use et_int_gen::wit::upstream::strip_webgpu; + +#[test] +fn strips_webgpu_wit_and_reemits_the_package() { + // Anchor to the repo root (no relative-path literal) and read the committed fixture at runtime. + let wit_path = edge_toolkit::config::get_project_root().join("generated/specs/wit/deps/wasi-webgpu/webgpu.wit"); + let raw = fs_err::read_to_string(&wit_path).unwrap(); + + let out = strip_webgpu(&raw).unwrap(); + assert!( + out.contains("package wasi:webgpu"), + "re-emitted WIT should still declare the wasi:webgpu package, got:\n{out}" + ); + // The trimmer keeps the compute resources (e.g. gpu-device) while dropping cross-package glue. + assert!( + out.contains("resource gpu-device"), + "expected the gpu-device resource to survive, got:\n{out}" + ); +}