Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .mise/config.macos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@ lld_flag = "-C link-arg=-fuse-ld={{ vars.conda_lld }}/bin/ld64.lld"
# Xcode's libclang (the lib dir beside `xcrun`'s clang).
# clang-sys doesn't always auto-find it on CI. The system libclang knows the SDK, so no bindgen args.
mac_libclang = "{{ exec(command='dirname $(dirname $(xcrun --find clang))') }}/lib"
# Matches rustc's own per-arch default deployment target, so cc-rs compiles native C/asm to the same minimum.
#
# `rustc -Z unstable-options --print target-spec-json` describes aarch64-apple-darwin as "11.0+, Big Sur+" and
# x86_64-apple-darwin as "10.12+, Sierra+". cc-rs (the `cc` crate) reads this env var when compiling aws-lc-
# sys/tree-sitter's C and assembly sources, but without it clang stamps those .o files with the full installed
# SDK version instead -- e.g. `ld64.lld: .../libaws_lc_sys-*.rlib(*-jitterentropy-base.o) has version 26.5.0,
# which is newer than target minimum of 11.0.0`, one line per object file, surfaced as a build warning by
# rustc's `linker_messages` lint. Setting it here makes cc-rs target the same minimum rustc's own codegen
# already uses, so the .o files' stamped version and the linked binary's requested minimum agree and ld64.lld
# has nothing to warn about -- fixing the mismatch instead of just silencing the lint that reports it.
macosx_deployment_target = "{% if arch() == 'arm64' %}11.0{% else %}10.12{% endif %}"

[env]
# lld_flag points Apple's clang at ld64.lld (see [vars]).
# The rpath/pylib flags (from config.toml) record conda's OpenSSL soname + the CPython lib dir so the runtime
# loader finds them. Applies to every rustc call, including the `cargo:` source-build subprocess.
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.lld_flag }} {{ vars.rpath_flag }} {{ vars.pylib_flag }}"
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.lld_flag }} {{ vars.rpath_flag }} {{ vars.pylib_flag }}"
# See the macosx_deployment_target comment in [vars] above.
LIBCLANG_PATH = "{{ vars.mac_libclang }}"
MACOSX_DEPLOYMENT_TARGET = "{{ vars.macosx_deployment_target }}"
# Pin SDKROOT so cc-rs and other build scripts find the macOS SDK.
# This covers headers like stdlib.h regardless of which `cc` resolves first. With the `_.path` prepend below
# putting Xcode CLT's /usr/bin/cc ahead of any brew gcc wrapper, the cc-rs invocation no longer inherits SDK
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/ast-grep/rules/doc-summary-ends-with-period.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ files:
- libs/test-helpers/src/lib.rs
- libs/ws-runner-common/tests/config.rs
- services/modules/tests/api_modules.rs
- services/modules/tests/configure_missing_root.rs
- services/modules/tests/get_module_file.rs
- services/storage/src/lib.rs
- services/storage/src/routes.rs
- services/storage/src/tty_image.rs
- services/storage/tests/image_logging.rs
- services/storage/tests/put.rs
- services/websockify/src/lib.rs
- services/websockify/tests/relay.rs
- services/ws-modules/except1/src/lib.rs
Expand All @@ -37,13 +44,17 @@ files:
- services/ws-server/src/lib.rs
- services/ws-server/src/main.rs
- services/ws-server/src/net.rs
- services/ws-server/tests/configure_app.rs
- services/ws-server/tests/health.rs
- services/ws-server/tests/net.rs
- services/ws-server/tests/tls.rs
- services/ws-wasi-runner/tests/modules.rs
- services/ws-wasi-runner/tests/otel_propagation.rs
- services/ws-wasi-runner/tests/vector_otlp_relay.rs
- services/ws-web-runner/build.rs
- services/ws-web-runner/tests/modules.rs
- services/ws/tests/config.rs
- services/ws/tests/load_registry.rs
- utilities/int-gen/src/error.rs
- utilities/int-gen/src/lib.rs
- utilities/int-gen/src/zig.rs
Expand Down
15 changes: 15 additions & 0 deletions services/modules/tests/configure_missing_root.rs
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.
Comment on lines +1 to +2

Copy link
Copy Markdown

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-L4
  • services/ws-server/tests/configure_app.rs#L1-L2
  • services/ws/tests/load_registry.rs#L1-L3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/modules/tests/configure_missing_root.rs` around lines 1 - 2, The
Rust doc summaries wrap before their terminating punctuation. Rewrap the
first-line summaries in services/modules/tests/configure_missing_root.rs (lines
1-2), services/modules/tests/get_module_file.rs (lines 1-4),
services/ws-server/tests/configure_app.rs (lines 1-2), and
services/ws/tests/load_registry.rs (lines 1-3) so each summary’s first line ends
with punctuation.

Source: Coding guidelines

#![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;
}
15 changes: 15 additions & 0 deletions services/modules/tests/get_module_file.rs
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

#![cfg(feature = "openapi-spec")] removes the entire test target when the feature is absent, allowing normal test runs to pass without exercising this regression. Move feature selection to explicit test-target/CI configuration and run the target with openapi-spec enabled.

As per coding guidelines, tests under tests/ must never be conditionally disabled without explicit approval.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/modules/tests/get_module_file.rs` around lines 5 - 6, Remove the
crate-level cfg(feature = "openapi-spec") gate from the integration test target
in get_module_file.rs so it is never silently disabled. Configure the test
target or CI invocation explicitly to enable the openapi-spec feature and ensure
this regression test runs during normal validation.

Source: 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);
}
6 changes: 3 additions & 3 deletions services/storage/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ where
Ok(HttpResponse::Ok().finish())
}

/// Render a thumbnail of the image at `path` directly to stdout, so the operator actually *sees* what was
/// stored rather than just its filename and byte count.
/// Render a thumbnail of the image at `path` directly to stdout.
///
/// This bypasses `tracing` entirely and writes straight to the terminal: the escape sequences (ANSI
/// This lets the operator actually *see* what was stored, rather than just its filename and byte count. It
/// bypasses `tracing` entirely and writes straight to the terminal: the escape sequences (ANSI
/// truecolor half-block art -- see the `tty_image` module) are tty-only presentation, not structured log
/// data, and would otherwise get shipped to the OTLP log exporter as log-record noise. Decode/render
/// failures (a corrupt upload, a non-terminal stdout) only cost tty visibility, not the request, so they're
Expand Down
22 changes: 12 additions & 10 deletions services/storage/tests/put.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ async fn rejects_unknown_agent_with_404() {
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

/// `actix-router` doesn't decode `%2F` in path captures (it would collide
/// with segment matching), so a regular `PUT /storage/a/b%2Fc` request
/// can't deliver a multi-component filename into the handler. Bypass the
/// router and invoke the handler directly with the parameters that the
/// filter expects to reject (`nested/path.txt` -> 2 components). This still
/// exercises the same `StorageError::InvalidFilename` -> 400 path that the
/// `ResponseError` impl wires up.
/// Invokes the handler directly, since a multi-component filename can't otherwise reach it.
///
/// `actix-router` doesn't decode `%2F` in path captures (it would collide with segment matching), so a
/// regular `PUT /storage/a/b%2Fc` request can't deliver a multi-component filename into the handler. Bypass
/// the router and invoke the handler directly with the parameters that the filter expects to reject
/// (`nested/path.txt` -> 2 components). This still exercises the same `StorageError::InvalidFilename` -> 400
/// path that the `ResponseError` impl wires up.
#[actix_rt::test]
async fn rejects_multi_component_filename_with_400() {
let tmp = tempfile::tempdir().unwrap();
Expand Down Expand Up @@ -107,9 +107,11 @@ async fn writes_file_for_registered_agent() {
assert_eq!(written, body);
}

/// An image PUT under `cargo test` has no real terminal on stdout, so `viuer`'s decode-and-render step
/// (triggered by the `.png` extension) is expected to fail internally. The route must still store the file
/// and return 200 regardless -- tty display is a best-effort side effect, never a reason to fail the upload.
/// Storing an image must succeed even though tty rendering cannot, since it is a best-effort side effect.
///
/// An image PUT under `cargo test` has no real terminal on stdout, so `tty_image::render`'s decode-and-render
/// step (triggered by the `.png` extension) is expected to fail internally. The route must still store the
/// file and return 200 regardless -- tty display is a best-effort side effect, never a reason to fail the upload.
#[actix_rt::test]
async fn stores_an_image_and_returns_200_even_though_tty_rendering_cannot_succeed_in_tests() {
let tmp = tempfile::tempdir().unwrap();
Expand Down
41 changes: 41 additions & 0 deletions services/ws-server/tests/configure_app.rs
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);
}
22 changes: 22 additions & 0 deletions services/ws-server/tests/health.rs
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

  • services/ws-server/tests/health.rs#L16-L16: assert StatusCode::OK instead of is_success().
  • services/ws-server/tests/configure_app.rs#L31-L33: assert StatusCode::OK instead of is_success().
📍 Affects 2 files
  • services/ws-server/tests/health.rs#L16-L16 (this comment)
  • services/ws-server/tests/configure_app.rs#L31-L33
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/ws-server/tests/health.rs` at line 16, Update the health response
assertions in services/ws-server/tests/health.rs lines 16-16 and
services/ws-server/tests/configure_app.rs lines 31-33 to compare the response
status with StatusCode::OK instead of using is_success(), ensuring both tests
require exactly HTTP 200.

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");
}
43 changes: 41 additions & 2 deletions services/ws-server/tests/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate the missing-certificate case.

Both cert and key are absent, so this test only proves that some missing file causes failure. Generate a valid pair, delete only cert, and then call load_tls_certs so the certificate-read path is specifically covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/ws-server/tests/tls.rs` around lines 45 - 48, Update the test around
load_tls_certs to create a valid certificate/key pair in the temporary
directory, remove only the certificate file, and then invoke load_tls_certs with
both paths. Keep the key present so the assertion specifically exercises the
missing-certificate read path.

}

#[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));
}
1 change: 1 addition & 0 deletions services/ws/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ uuid.workspace = true
[dev-dependencies]
et-test-helpers.workspace = true
serde-env.workspace = true
tempfile.workspace = true

[lints]
workspace = true
90 changes: 90 additions & 0 deletions services/ws/tests/load_registry.rs
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() {

Check warning on line 22 in services/ws/tests/load_registry.rs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

services/ws/tests/load_registry.rs#L22

Method round_trips_state_ip_and_pending_messages_but_not_sessions has 52 lines of code (limit is 50)
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"),
}
}
Loading