-
Notifications
You must be signed in to change notification settings - Fork 0
General tidy up #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
General tidy up #101
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| //! Covers `configure`'s fail-fast panic when `config.root` names a module that isn't among the | ||
| //! scanned `config.paths` -- a misconfiguration that must surface at startup, not as a silent 404. | ||
| #![cfg(test)] | ||
|
|
||
| use actix_web::{App, test}; | ||
| use et_modules_service::{ModulesConfig, configure}; | ||
|
|
||
| #[actix_rt::test] | ||
| #[should_panic(expected = "Root module 'nonexistent-root' not found")] | ||
| async fn configure_panics_when_root_module_is_missing() { | ||
| let tmp = tempfile::tempdir().unwrap(); | ||
| let config = ModulesConfig::new(vec![tmp.path().to_path_buf()], "nonexistent-root".to_string()); | ||
|
|
||
| let _app = test::init_service(App::new().configure(|cfg| configure(cfg, &config))).await; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| //! Covers `get_module_file`: a fake stub that exists only to host the utoipa route annotation for | ||
| //! `et-int-gen`, since the real `GET /modules/{name}/{path}` route is served by the `actix_files::Files` | ||
| //! mounts `configure` registers, not by Rust code. Pins its stub response so a future change to it is | ||
| //! deliberate, not accidental. | ||
| #![cfg(test)] | ||
| #![cfg(feature = "openapi-spec")] | ||
|
Comment on lines
+5
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Do not silently disable this integration test with a feature gate.
As per coding guidelines, tests under 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| use actix_web::http::StatusCode; | ||
| use et_modules_service::routes::get_module_file; | ||
|
|
||
| #[test] | ||
| fn returns_not_implemented() { | ||
| let resp = get_module_file(); | ||
| assert_eq!(resp.status(), StatusCode::NOT_IMPLEMENTED); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| //! Covers `no_content` and `configure_app`'s route wiring: favicon, health, and delegation to the | ||
| //! sub-services `configure_app` chains together. | ||
| #![cfg(test)] | ||
|
|
||
| use actix_web::http::StatusCode; | ||
| use actix_web::{App, test, web}; | ||
| use et_storage_service::StorageConfig; | ||
| use et_ws_server::config::Config; | ||
| use et_ws_server::{WsAgentRegistry, configure_app, no_content}; | ||
| use tempfile::tempdir; | ||
|
|
||
| #[actix_rt::test] | ||
| async fn no_content_returns_204() { | ||
| let resp = no_content().await; | ||
| assert_eq!(resp.status(), StatusCode::NO_CONTENT); | ||
| } | ||
|
|
||
| #[actix_rt::test] | ||
| async fn configure_app_wires_favicon_health_and_modules() { | ||
| let storage_dir = tempdir().unwrap(); | ||
| let mut config = Config::default(); | ||
| config.storage = StorageConfig::new(storage_dir.path().to_path_buf()); | ||
|
|
||
| let registry = web::Data::new(WsAgentRegistry::default()); | ||
| let app = test::init_service(App::new().configure(|cfg| configure_app(cfg, registry, &config))).await; | ||
|
|
||
| let favicon_req = test::TestRequest::get().uri("/favicon.ico").to_request(); | ||
| let favicon_resp = test::call_service(&app, favicon_req).await; | ||
| assert_eq!(favicon_resp.status(), StatusCode::NO_CONTENT); | ||
|
|
||
| let health_req = test::TestRequest::get().uri("/health").to_request(); | ||
| let health_resp = test::call_service(&app, health_req).await; | ||
| assert!(health_resp.status().is_success()); | ||
|
|
||
| // `/modules/` only resolves if `et_modules_service::configure` found and served the root module, | ||
| // which only runs if every configure call before it in `configure_app` (ws, storage, websockify) | ||
| // succeeded -- so a 200 here is proof the whole wiring chain ran. | ||
| let modules_req = test::TestRequest::get().uri("/modules/").to_request(); | ||
| let modules_resp = test::call_service(&app, modules_req).await; | ||
| assert_eq!(modules_resp.status(), StatusCode::OK); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| //! Covers the `/health` liveness-probe route: status code and response body shape. | ||
| #![cfg(test)] | ||
|
|
||
| use actix_web::http::header; | ||
| use actix_web::{App, test, web}; | ||
| use et_ws_server::health; | ||
| use et_ws_server::routes::HealthResponse; | ||
|
|
||
| #[actix_rt::test] | ||
| async fn health_returns_200_with_healthy_status() { | ||
| let app = test::init_service(App::new().route("/health", web::get().to(health))).await; | ||
|
|
||
| let req = test::TestRequest::get().uri("/health").to_request(); | ||
| let resp = test::call_service(&app, req).await; | ||
|
|
||
| assert!(resp.status().is_success()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Pin both health tests to HTTP 200. Both assertions accept any 2xx response, so they would miss a regression from 200 to another successful status.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "application/json"); | ||
|
|
||
| let body: HealthResponse = test::read_body_json(resp).await; | ||
| assert_eq!(body.status, "healthy"); | ||
| assert_eq!(body.service, "ws-server"); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,17 +16,56 @@ fn generate_write_load_and_build_server_config() { | |
| cert.exists() && key.exists(), | ||
| "generate_tls_certs must write both PEM files" | ||
| ); | ||
|
|
||
| // The freshly-written PEMs must load back into the exact same der bytes that were generated. | ||
| let (loaded_cert, loaded_key) = load_tls_certs(&cert, &key); | ||
| assert_eq!(gen_cert, loaded_cert, "reloaded cert der must match the generated der"); | ||
| assert_eq!( | ||
| gen_key.secret_der(), | ||
| loaded_key.secret_der(), | ||
| "reloaded key der must match the generated der" | ||
| ); | ||
|
|
||
| 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" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "NotFound")] | ||
| fn load_tls_certs_missing_cert_file_panics() { | ||
| let dir = tempdir().unwrap(); | ||
| let cert = dir.path().join("missing-cert.pem"); | ||
| let key = dir.path().join("missing-key.pem"); | ||
| drop(load_tls_certs(&cert, &key)); | ||
|
Comment on lines
+45
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Isolate the missing-certificate case. Both 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "NotFound")] | ||
| fn load_tls_certs_missing_key_file_panics() { | ||
| let dir = tempdir().unwrap(); | ||
| let cert = dir.path().join("cert.pem"); | ||
| let key = dir.path().join("key.pem"); | ||
| drop(generate_tls_certs(&cert, &key)); | ||
| fs_err::remove_file(&key).unwrap(); | ||
|
|
||
| drop(load_tls_certs(&cert, &key)); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "InconsistentKeys")] | ||
| fn build_tls_server_config_with_mismatched_key_panics() { | ||
| let dir = tempdir().unwrap(); | ||
| let (cert_a, _key_a) = generate_tls_certs(&dir.path().join("a-cert.pem"), &dir.path().join("a-key.pem")); | ||
| let (_cert_b, key_b) = generate_tls_certs(&dir.path().join("b-cert.pem"), &dir.path().join("b-key.pem")); | ||
|
|
||
| drop(build_tls_server_config(cert_a, key_b)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| //! Covers `load_registry`: the missing-file empty-registry fallback, a save/load round trip through the | ||
| //! session-less `BareRecord` shape (state, last-known IP, pending direct messages survive; sessions don't), | ||
| //! and the malformed-YAML error path. | ||
| #![cfg(test)] | ||
|
|
||
| use edge_toolkit::ws::{AgentConnectionState, AgentSummary, ConnectStatus}; | ||
| use edge_toolkit::ws_server::RegistryError; | ||
| use et_ws_service::{WsAgentRegistry, load_registry}; | ||
| use tempfile::tempdir; | ||
|
|
||
| #[test] | ||
| fn missing_file_yields_an_empty_registry() { | ||
| let dir = tempdir().unwrap(); | ||
| let path = dir.path().join("no-such-registry.yaml"); | ||
|
|
||
| let registry = load_registry(&path).unwrap(); | ||
|
|
||
| assert!(registry.list_agents().is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn round_trips_state_ip_and_pending_messages_but_not_sessions() { | ||
| let dir = tempdir().unwrap(); | ||
| let path = dir.path().join("registry.yaml"); | ||
|
|
||
| let registry = WsAgentRegistry::default(); | ||
| let (tx1, _rx1) = tokio::sync::mpsc::unbounded_channel(); | ||
| let (tx2, _rx2) = tokio::sync::mpsc::unbounded_channel(); | ||
| let (agent_1_id, agent_1_status) = registry.connect_agent(None, "agent-1".to_string(), "10.0.0.5", tx1); | ||
| let (agent_2_id, agent_2_status) = registry.connect_agent(None, "agent-2".to_string(), "10.0.0.6", tx2); | ||
| assert_eq!( | ||
| (agent_1_id.as_str(), agent_1_status), | ||
| ("agent-1", ConnectStatus::Assigned) | ||
| ); | ||
| assert_eq!( | ||
| (agent_2_id.as_str(), agent_2_status), | ||
| ("agent-2", ConnectStatus::Assigned) | ||
| ); | ||
| let queued = registry.queue_direct( | ||
| "msg-1".to_string(), | ||
| "agent-1", | ||
| "agent-2", | ||
| "2026-07-22T00:00:00Z".to_string(), | ||
| serde_json::json!({"hello": "world"}), | ||
| ); | ||
| assert!(queued.is_some(), "agent-2 must exist to receive the queued message"); | ||
| registry.mark_disconnected("agent-2"); | ||
| registry.save(&path).unwrap(); | ||
|
|
||
| let loaded = load_registry(&path).unwrap(); | ||
|
|
||
| assert_eq!( | ||
| loaded.list_agents(), | ||
| vec![ | ||
| AgentSummary::new( | ||
| "agent-1".to_string(), | ||
| AgentConnectionState::Connected, | ||
| Some("10.0.0.5".to_string()) | ||
| ), | ||
| AgentSummary::new( | ||
| "agent-2".to_string(), | ||
| AgentConnectionState::Disconnected, | ||
| Some("10.0.0.6".to_string()) | ||
| ), | ||
| ] | ||
| ); | ||
| assert!( | ||
| loaded.agent_session("agent-1").is_none(), | ||
| "sessions are never persisted, so a reloaded agent must have none even though it was Connected" | ||
| ); | ||
|
|
||
| let pending = loaded.pending_messages_for("agent-2"); | ||
| assert_eq!(pending.len(), 1); | ||
| assert_eq!(pending[0].message_id, "msg-1"); | ||
| assert_eq!(pending[0].from_agent_id, "agent-1"); | ||
| assert_eq!(pending[0].message, serde_json::json!({"hello": "world"})); | ||
| } | ||
|
|
||
| #[test] | ||
| fn malformed_yaml_is_a_registry_error() { | ||
| let dir = tempdir().unwrap(); | ||
| let path = dir.path().join("registry.yaml"); | ||
| fs_err::write(&path, "- this\n- is\n- a list, not a map of agents\n").unwrap(); | ||
|
|
||
| match load_registry(&path) { | ||
| Err(RegistryError::Yaml(_)) => {} | ||
| Err(other) => panic!("expected RegistryError::Yaml, got {other:?}"), | ||
| Ok(_) => panic!("expected an error for malformed YAML"), | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the first-line Rust doc summaries across the new test files.
Each file wraps its summary before punctuation, violating the same documentation rule.
services/modules/tests/configure_missing_root.rs#L1-L2: rewrap the summary so Line 1 ends with punctuation.services/modules/tests/get_module_file.rs#L1-L4: rewrap the summary so Line 1 ends with punctuation.services/ws-server/tests/configure_app.rs#L1-L2: rewrap the summary so Line 1 ends with punctuation.services/ws/tests/load_registry.rs#L1-L3: rewrap the summary so Line 1 ends with punctuation.📍 Affects 4 files
services/modules/tests/configure_missing_root.rs#L1-L2(this comment)services/modules/tests/get_module_file.rs#L1-L4services/ws-server/tests/configure_app.rs#L1-L2services/ws/tests/load_registry.rs#L1-L3🤖 Prompt for AI Agents
Source: Coding guidelines