From dd35593f6fd5799edb02c875a85e9f0a461740ae Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 1 Oct 2026 14:16:18 +0300 Subject: [PATCH 1/3] docs: require unit tests in *_tests.rs files No inline `#[cfg(test)] mod` blocks; tests go in a sibling `_tests.rs`. Co-authored-by: Medulla --- AGENTS.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 5cab329..2efcd73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ Do not log provider queries, credentials, or result content. Provider credential arrive through sensitive module initialization and reinitialization config. Every public item needs rustdoc. Keep types and behavior in focused modules, -with tests in neighboring `test.rs` files. Cover wire representations and real +with tests in neighboring `*_tests.rs` files. Cover wire representations and real in-memory TinyBus calls. No placeholders, ignored tests, or lint exemptions. Run from repository root: @@ -24,3 +24,28 @@ cargo test --all-features ``` PRs go to the canonical upstream repository and should be ready for review. + +## Tests live in `*_tests.rs` files + +- Unit tests are never inline. Do not write a `#[cfg(test)] mod tests { ... }` + block in a source file. Put the tests in a sibling `_tests.rs` + (`mod_tests.rs` beside a `mod.rs`, `lib_tests.rs` beside `lib.rs`) and declare + it at the bottom of the module: + + ```rust + #[cfg(test)] + #[path = "foo_tests.rs"] + mod tests; + ``` + +- The test file starts with `use super::*;` and carries no `#[cfg(test)]` of its + own. It is still a child module, so it reaches private items exactly as an + inline module did. +- Name test files `_tests.rs`; a second group for the same module is + `__tests.rs`. Never `test.rs`, `tests.rs` or `_test.rs`. +- Integration tests stay in the crate's `tests/` directory. +- OpenHuman's `scripts/externalize-inline-tests.mjs --write` moves + inline test modules out mechanically; without `--write` it only reports. +- Existing `test.rs` and `_test.rs` files predate this rule. Rename each + to `_tests.rs` (keep its `mod` name, add the `#[path]` attribute) the + next time you touch it. From c426f9a492643676988659f8f9238f5b4571ef80 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 1 Oct 2026 19:27:23 +0300 Subject: [PATCH 2/3] chore(tests): replace `is_empty` assertions with explicit length checks Replace all `assert!(x.is_empty())` calls with `assert_eq!(x.len(), 0)` across test files to improve diagnostic output on failure, as the length-based assertion shows the actual value when the assertion fails. Auto-committed-on: dragonfly --- .../tinysearch-bus/src/search/catalog/test.rs | 8 ++---- crates/tinysearch-bus/src/search/test.rs | 4 +-- .../src/provider/direct/parallel/test.rs | 2 +- .../src/provider/direct/tinyfish/test.rs | 6 ++--- crates/tinysearch/src/search/roles/test.rs | 26 +++++++++---------- crates/tinysearch/src/search/test.rs | 18 +++---------- crates/tinysearch/src/tinybus_module/test.rs | 6 ++--- crates/tinysearch/tests/public_api.rs | 7 +---- vendor/tinybus | 2 +- 9 files changed, 30 insertions(+), 49 deletions(-) diff --git a/crates/tinysearch-bus/src/search/catalog/test.rs b/crates/tinysearch-bus/src/search/catalog/test.rs index 67fb78d..2d83b4a 100644 --- a/crates/tinysearch-bus/src/search/catalog/test.rs +++ b/crates/tinysearch-bus/src/search/catalog/test.rs @@ -139,7 +139,7 @@ fn provider_roles_and_defaults_match_the_contract() { role_provider_tool(Role::Contents, "parallel"), Some("parallel_extract") ); - assert!(provider_roles("unknown").is_empty()); + assert_eq!(provider_roles("unknown").len(), 0); assert_eq!(provider_roles("gemini"), [Role::Answer]); assert_eq!(provider_roles("gemini_deep_research"), [Role::Answer]); assert_eq!(provider_roles("tinyfish"), [Role::Search, Role::Contents]); @@ -307,11 +307,7 @@ fn roles_mode_without_backend_credential_drops_backend_providers() { listed.tools[1].parameters["properties"]["provider"]["enum"], json!(["tinyfish"]) ); - assert!( - select_tools(&BTreeMap::new(), &config.presentation) - .tools - .is_empty() - ); + assert_eq!( select_tools(&BTreeMap::new(), &config.presentation) .tools .len(), 0); } #[test] diff --git a/crates/tinysearch-bus/src/search/test.rs b/crates/tinysearch-bus/src/search/test.rs index 93f19e2..6db844f 100644 --- a/crates/tinysearch-bus/src/search/test.rs +++ b/crates/tinysearch-bus/src/search/test.rs @@ -83,7 +83,7 @@ fn roles_and_role_lists_round_trip_as_snake_case() -> serde_json::Result<()> { assert_eq!(presentation.mode, PresentationMode::Roles); assert_eq!(presentation.roles[&Role::Search], ["brave", "exa"]); assert_eq!(presentation.roles[&Role::Answer], ["gemini"]); - assert!(presentation.roles[&Role::Contents].is_empty()); + assert_eq!(presentation.roles[&Role::Contents].len(), 0); assert_eq!( serde_json::to_value(&presentation)?["roles"]["search"], serde_json::json!(["brave", "exa"]) @@ -103,7 +103,7 @@ fn role_response_fields_are_optional_on_the_wire() -> serde_json::Result<()> { "provider":"exa","results":[],"citations":[],"answer":null,"status":"empty" }))?; assert_eq!(legacy.role, None); - assert!(legacy.fallback_from.is_empty()); + assert_eq!(legacy.fallback_from.len(), 0); let encoded = serde_json::to_value(&legacy)?; assert!(encoded.get("role").is_none()); assert!(encoded.get("fallback_from").is_none()); diff --git a/crates/tinysearch/src/provider/direct/parallel/test.rs b/crates/tinysearch/src/provider/direct/parallel/test.rs index 8c9e6c3..91a3602 100644 --- a/crates/tinysearch/src/provider/direct/parallel/test.rs +++ b/crates/tinysearch/src/provider/direct/parallel/test.rs @@ -391,7 +391,7 @@ fn all_tools_catalog_is_direct_only() -> std::result::Result<(), Box TestResult<()> { )?; assert_eq!(method, Method::GET); assert_eq!(base, SEARCH_BASE); - assert!(path.is_empty()); + assert_eq!(path.len(), 0); assert!(body.is_none()); assert_eq!( params, @@ -48,8 +48,8 @@ fn fetch_posts_urls_and_format_only() -> TestResult<()> { )?; assert_eq!(method, Method::POST); assert_eq!(base, FETCH_BASE); - assert!(path.is_empty()); - assert!(params.is_empty()); + assert_eq!(path.len(), 0); + assert_eq!(params.len(), 0); assert_eq!( body, Some(json!({"urls":["https://a.example","https://b.example"],"format":"html"})) diff --git a/crates/tinysearch/src/search/roles/test.rs b/crates/tinysearch/src/search/roles/test.rs index 0100e90..0eebe2c 100644 --- a/crates/tinysearch/src/search/roles/test.rs +++ b/crates/tinysearch/src/search/roles/test.rs @@ -164,7 +164,7 @@ async fn search_uses_the_first_provider_and_translates_arguments() -> Result<()> .await?; assert_eq!(response.provider, "exa"); assert_eq!(response.role, Some(Role::Search)); - assert!(response.fallback_from.is_empty()); + assert_eq!(response.fallback_from.len(), 0); let sent = fixture.calls("exa"); assert_eq!(sent.len(), 1); assert_eq!(sent[0].name, "exa_search"); @@ -216,7 +216,7 @@ async fn invalid_arguments_do_not_fall_back() { "provider returned HTTP 400".into() )) ); - assert!(fixture.calls("brave").is_empty()); + assert_eq!(fixture.calls("brave").len(), 0); } #[tokio::test] @@ -248,8 +248,8 @@ async fn explicit_provider_is_used_alone_without_fallback() -> Result<()> { )) .await; assert_eq!(result.err(), Some(Error::InsufficientBalance)); - assert!(fixture.calls("exa").is_empty()); - assert!(fixture.calls("tavily").is_empty()); + assert_eq!(fixture.calls("exa").len(), 0); + assert_eq!(fixture.calls("tavily").len(), 0); let response = fixture .service @@ -259,7 +259,7 @@ async fn explicit_provider_is_used_alone_without_fallback() -> Result<()> { )) .await?; assert_eq!(response.provider, "tavily"); - assert!(fixture.calls("exa").is_empty()); + assert_eq!(fixture.calls("exa").len(), 0); Ok(()) } @@ -296,7 +296,7 @@ async fn role_arguments_are_validated_before_dispatch() { .err(), Some(Error::UnavailableTool("exa_search".into())) ); - assert!(fixture.calls("exa").is_empty()); + assert_eq!(fixture.calls("exa").len(), 0); } #[tokio::test] @@ -312,7 +312,7 @@ async fn quick_answers_skip_deep_research() -> Result<()> { assert_eq!(fixture.calls("gemini")[0].name, "gemini_agentic_search"); assert_eq!(fixture.calls("exa")[0].name, "exa_answer"); assert_eq!(fixture.calls("exa")[0].arguments, json!({"query":"why"})); - assert!(fixture.calls("gemini_deep_research").is_empty()); + assert_eq!(fixture.calls("gemini_deep_research").len(), 0); Ok(()) } @@ -368,7 +368,7 @@ async fn explicit_quick_depth_is_rejected_when_only_deep_research_is_usable() { )) .await; assert_eq!(result.err(), Some(Error::InvalidArguments)); - assert!(fixture.calls("gemini_deep_research").is_empty()); + assert_eq!(fixture.calls("gemini_deep_research").len(), 0); } #[tokio::test] @@ -564,7 +564,7 @@ async fn parallel_is_never_usable_on_the_backend_route() { parallel.route = ProviderRoute::Backend; } }); - assert!(fixture.service.list_tools().tools.is_empty()); + assert_eq!(fixture.service.list_tools().tools.len(), 0); for (tool, arguments) in [ (tools::WEB_SEARCH, json!({"query":"rust"})), (tools::WEB_ANSWER, json!({"query":"why"})), @@ -582,7 +582,7 @@ async fn parallel_is_never_usable_on_the_backend_route() { Some(Error::UnavailableTool(tool.into())) ); } - assert!(fixture.calls("parallel").is_empty()); + assert_eq!(fixture.calls("parallel").len(), 0); } #[tokio::test] @@ -602,7 +602,7 @@ async fn role_calls_fall_back_into_parallel() -> Result<()> { .await?; assert_eq!(response.provider, "parallel"); assert_eq!(response.fallback_from, ["exa", "brave", "tavily"]); - assert!(fixture.calls("tinyfish").is_empty()); + assert_eq!(fixture.calls("tinyfish").len(), 0); let response = fixture .service @@ -650,7 +650,7 @@ async fn deep_answers_skip_parallel() -> Result<()> { .await; assert!(matches!(result, Err(Error::ProviderUnavailable(_)))); assert_eq!(fixture.calls("gemini_deep_research").len(), 1); - assert!(fixture.calls("parallel").is_empty()); + assert_eq!(fixture.calls("parallel").len(), 0); assert_eq!( fixture @@ -663,7 +663,7 @@ async fn deep_answers_skip_parallel() -> Result<()> { .err(), Some(Error::InvalidArguments) ); - assert!(fixture.calls("parallel").is_empty()); + assert_eq!(fixture.calls("parallel").len(), 0); let response = fixture .service diff --git a/crates/tinysearch/src/search/test.rs b/crates/tinysearch/src/search/test.rs index cb90ff8..e6fc609 100644 --- a/crates/tinysearch/src/search/test.rs +++ b/crates/tinysearch/src/search/test.rs @@ -186,7 +186,7 @@ async fn router_never_dispatches_search_to_a_non_search_provider() { )] .into(); let service = SearchService::with_providers(config, providers); - assert!(service.list_tools().tools.is_empty()); + assert_eq!(service.list_tools().tools.len(), 0); let result = service .execute_tool(ExecuteToolRequest { name: "search".into(), @@ -223,7 +223,7 @@ async fn rejects_unadvertised_tool_and_invalid_arguments() { fn disabling_search_suppresses_tools() { let mut service = service(PresentationMode::AllTools); service.config.enabled = false; - assert!(service.list_tools().tools.is_empty()); + assert_eq!(service.list_tools().tools.len(), 0); } #[tokio::test] @@ -303,12 +303,7 @@ fn router_hides_unknown_providers() { Arc::new(MockProvider) as Arc, )] .into(); - assert!( - SearchService::with_providers(config, providers) - .list_tools() - .tools - .is_empty() - ); + assert_eq!( SearchService::with_providers(config, providers) .list_tools() .tools .len(), 0); } #[test] @@ -368,12 +363,7 @@ fn direct_provider_without_credential_is_hidden() { .insert("tinyfish".into(), ProviderConfig::default()); let mut providers: BTreeMap> = BTreeMap::new(); providers.insert("tinyfish".into(), Arc::new(Keyed)); - assert!( - SearchService::with_providers(config.clone(), providers.clone()) - .list_tools() - .tools - .is_empty() - ); + assert_eq!( SearchService::with_providers(config.clone(), providers.clone()) .list_tools() .tools .len(), 0); config.providers.insert( "tinyfish".into(), ProviderConfig { diff --git a/crates/tinysearch/src/tinybus_module/test.rs b/crates/tinysearch/src/tinybus_module/test.rs index 8ca373c..03d1c0a 100644 --- a/crates/tinysearch/src/tinybus_module/test.rs +++ b/crates/tinysearch/src/tinybus_module/test.rs @@ -36,7 +36,7 @@ async fn empty_configuration_serves_no_tools_and_rejects_execution() -> tinybus: let client = Connection::connect(bus.connect().await?).await?; let proxy = client.proxy(names::INTERFACE, names::OBJECT_PATH, names::INTERFACE)?; let listed: ListToolsResponse = proxy.call(names::methods::LIST_TOOLS, ()).await?; - assert!(listed.tools.is_empty()); + assert_eq!(listed.tools.len(), 0); let result = proxy .call::( names::methods::EXECUTE_TOOL, @@ -84,10 +84,10 @@ fn builtins_discover_and_reinitialize_from_private_configuration() { provider.enabled = false; } let refreshed = SearchService::with_providers(config.clone(), crate::provider::builtins()); - assert!(refreshed.list_tools().tools.is_empty()); + assert_eq!(refreshed.list_tools().tools.len(), 0); config.enabled = false; let disabled = SearchService::with_providers(config, crate::provider::builtins()); - assert!(disabled.list_tools().tools.is_empty()); + assert_eq!(disabled.list_tools().tools.len(), 0); } #[tokio::test] diff --git a/crates/tinysearch/tests/public_api.rs b/crates/tinysearch/tests/public_api.rs index c7af60f..8921769 100644 --- a/crates/tinysearch/tests/public_api.rs +++ b/crates/tinysearch/tests/public_api.rs @@ -4,10 +4,5 @@ use tinysearch::{SearchConfig, SearchService, names}; #[test] fn host_can_discover_search_interface() { assert_eq!(names::methods::LIST_TOOLS, "ListTools"); - assert!( - SearchService::with_providers(SearchConfig::default(), BTreeMap::new()) - .list_tools() - .tools - .is_empty() - ); + assert_eq!( SearchService::with_providers(SearchConfig::default(), BTreeMap::new()) .list_tools() .tools .len(), 0); } diff --git a/vendor/tinybus b/vendor/tinybus index df6f990..dc8c92f 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit df6f990cec3b130db12d6cd0c8e24f11c30b28e6 +Subproject commit dc8c92f58b5dcb8b8af4a5b3a81aafe3333fa1e4 From c301593bd371b0ca4e00dd11448de43fae169214 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 1 Oct 2026 19:28:02 +0300 Subject: [PATCH 3/3] chore: reformat long assertion chains in tests Reformatted several test assertions that were written as single long lines into multi-line expressions for improved readability. The changes are purely cosmetic and do not alter any test logic or behaviour. Auto-committed-on: dragonfly --- crates/tinysearch-bus/src/search/catalog/test.rs | 7 ++++++- crates/tinysearch/src/search/test.rs | 16 ++++++++++++++-- crates/tinysearch/tests/public_api.rs | 8 +++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/tinysearch-bus/src/search/catalog/test.rs b/crates/tinysearch-bus/src/search/catalog/test.rs index 2d83b4a..4cee742 100644 --- a/crates/tinysearch-bus/src/search/catalog/test.rs +++ b/crates/tinysearch-bus/src/search/catalog/test.rs @@ -307,7 +307,12 @@ fn roles_mode_without_backend_credential_drops_backend_providers() { listed.tools[1].parameters["properties"]["provider"]["enum"], json!(["tinyfish"]) ); - assert_eq!( select_tools(&BTreeMap::new(), &config.presentation) .tools .len(), 0); + assert_eq!( + select_tools(&BTreeMap::new(), &config.presentation) + .tools + .len(), + 0 + ); } #[test] diff --git a/crates/tinysearch/src/search/test.rs b/crates/tinysearch/src/search/test.rs index e6fc609..727c227 100644 --- a/crates/tinysearch/src/search/test.rs +++ b/crates/tinysearch/src/search/test.rs @@ -303,7 +303,13 @@ fn router_hides_unknown_providers() { Arc::new(MockProvider) as Arc, )] .into(); - assert_eq!( SearchService::with_providers(config, providers) .list_tools() .tools .len(), 0); + assert_eq!( + SearchService::with_providers(config, providers) + .list_tools() + .tools + .len(), + 0 + ); } #[test] @@ -363,7 +369,13 @@ fn direct_provider_without_credential_is_hidden() { .insert("tinyfish".into(), ProviderConfig::default()); let mut providers: BTreeMap> = BTreeMap::new(); providers.insert("tinyfish".into(), Arc::new(Keyed)); - assert_eq!( SearchService::with_providers(config.clone(), providers.clone()) .list_tools() .tools .len(), 0); + assert_eq!( + SearchService::with_providers(config.clone(), providers.clone()) + .list_tools() + .tools + .len(), + 0 + ); config.providers.insert( "tinyfish".into(), ProviderConfig { diff --git a/crates/tinysearch/tests/public_api.rs b/crates/tinysearch/tests/public_api.rs index 8921769..a2fe42b 100644 --- a/crates/tinysearch/tests/public_api.rs +++ b/crates/tinysearch/tests/public_api.rs @@ -4,5 +4,11 @@ use tinysearch::{SearchConfig, SearchService, names}; #[test] fn host_can_discover_search_interface() { assert_eq!(names::methods::LIST_TOOLS, "ListTools"); - assert_eq!( SearchService::with_providers(SearchConfig::default(), BTreeMap::new()) .list_tools() .tools .len(), 0); + assert_eq!( + SearchService::with_providers(SearchConfig::default(), BTreeMap::new()) + .list_tools() + .tools + .len(), + 0 + ); }