返回 CodeWhale
headless_catalog.rs
根目录 / crates / tui / src / runtime_api / tests / headless_catalog.rs
1 use super::*;
2 use codewhale_config::catalog::{
3 CatalogOffering, CatalogSource, CatalogStatus, ProviderCatalogDelta, base_url_fingerprint,
4 now_unix,
5 };
6 use codewhale_config::models_dev::ModelsDevModalities;
7 use codewhale_config::route::CapabilityState;
8
9 const MODEL: &str = "fixture/vision";
10 const FIRST_ENDPOINT: &str = "http://127.0.0.1:9/first/v1";
11 const SECOND_ENDPOINT: &str = "http://127.0.0.1:9/second/v1";
12
13 struct CatalogReset;
14
15 impl Drop for CatalogReset {
16 fn drop(&mut self) {
17 cold_process();
18 crate::tools::large_output_router::WorkshopConfig::install_active(None);
19 }
20 }
21
22 fn cold_process() {
23 crate::provider_catalog_live::reset_cache_for_test();
24 crate::provider_lake::clear_live_snapshot();
25 }
26
27 fn persist_catalog(kind: ApiProvider, identity: &str, endpoint: &str, images: bool) {
28 let fingerprint = base_url_fingerprint(endpoint);
29 let fetched_at = now_unix();
30 let ticket = crate::provider_catalog_live::begin_refresh_for_identity(kind, identity, endpoint);
31 assert_eq!(
32 crate::provider_catalog_live::record_success_if_current(
33 &ticket,
34 ProviderCatalogDelta {
35 provider: identity.into(),
36 base_url_fingerprint: fingerprint.clone(),
37 fetched_at,
38 offerings: vec![CatalogOffering {
39 provider: identity.into(),
40 wire_model_id: MODEL.into(),
41 endpoint_key: "chat".into(),
42 modalities: Some(ModelsDevModalities {
43 input: if images {
44 vec!["text".into(), "image".into()]
45 } else {
46 vec!["text".into()]
47 },
48 output: vec!["text".into()],
49 }),
50 source: CatalogSource::Live {
51 base_url_fingerprint: fingerprint,
52 fetched_at,
53 },
54 ..Default::default()
55 }],
56 },
57 ),
58 Some(CatalogStatus::Fresh)
59 );
60 }
61
62 fn config_for(identity: &str, endpoint: &str) -> Config {
63 let mut config = Config {
64 provider: Some(identity.into()),
65 ..Default::default()
66 };
67 let route = if identity == "openrouter" {
68 &mut config
69 .providers
70 .get_or_insert_with(Default::default)
71 .openrouter
72 } else {
73 let route = config
74 .providers
75 .get_or_insert_with(Default::default)
76 .custom
77 .entry(identity.into())
78 .or_default();
79 route.kind = Some("openai-compatible".into());
80 route
81 };
82 route.base_url = Some(endpoint.into());
83 route.api_key = Some("synthetic-headless-catalog-key".into());
84 route.model = Some(MODEL.into());
85 config
86 }
87
88 fn image_capability(config: &Config) -> CapabilityState {
89 provider_model_image_input_for_api(config, config.api_provider(), MODEL)
90 }
91
92 fn open_server_manager(config: &Config, root: &Path) -> Result<SharedRuntimeThreadManager> {
93 let workspace = root.join("workspace");
94 fs::create_dir_all(&workspace)?;
95 let (manager, _) = open_runtime_threads_for_server(
96 config,
97 workspace.clone(),
98 RuntimeThreadManagerConfig {
99 data_dir: root.join("runtime"),
100 task_data_dir: root.join("tasks"),
101 max_active_threads: 2,
102 },
103 Arc::new(crate::plugins::PluginRegistry::empty(&workspace)),
104 )?;
105 Ok(manager)
106 }
107
108 #[test]
109 fn headless_startup_publishes_cold_endpoint_catalog_capabilities() -> Result<()> {
110 // Lock order: WORKSHOP before ENV (matches workshop-budget tests). ENV then
111 // WORKSHOP ABBA-deadlocks the full libtest suite with the workshop test (#6049).
112 let _workshop = crate::tools::large_output_router::active_workshop_test_guard();
113 let _env = lock_test_env();
114 let _offline = EnvVarGuard::set("CODEWHALE_DISABLE_CLOUD_FACTS", "1");
115 let _live = crate::provider_lake::lock_live_snapshot();
116 let home = tempfile::tempdir()?;
117 let _home = EnvVarGuard::set("CODEWHALE_HOME", home.path());
118 let _reset = CatalogReset;
119 cold_process();
120 persist_catalog(ApiProvider::Openrouter, "openrouter", FIRST_ENDPOINT, true);
121 cold_process();
122
123 let config = config_for("openrouter", FIRST_ENDPOINT);
124 assert!(
125 provider_models_for_api(&config, ApiProvider::Openrouter, ApiProvider::Openrouter)
126 .contains(&MODEL.to_string())
127 );
128 assert_eq!(image_capability(&config), CapabilityState::Unknown);
129 let _manager = open_server_manager(&config, home.path())?;
130 assert_eq!(image_capability(&config), CapabilityState::Supported);
131 assert_eq!(
132 image_capability(&config_for("openrouter", SECOND_ENDPOINT)),
133 CapabilityState::Unknown,
134 "another endpoint must not inherit the saved capabilities"
135 );
136 Ok(())
137 }
138
139 #[tokio::test(flavor = "current_thread")]
140 async fn headless_reload_publishes_only_the_accepted_identity_and_endpoint() -> Result<()> {
141 // Lock order: WORKSHOP before ENV — see headless_startup twin and #6049.
142 let _workshop = crate::tools::large_output_router::active_workshop_test_guard();
143 let _env = lock_test_env();
144 let _offline = EnvVarGuard::set("CODEWHALE_DISABLE_CLOUD_FACTS", "1");
145 let _live = crate::provider_lake::lock_live_snapshot();
146 let home = tempfile::tempdir()?;
147 let _home = EnvVarGuard::set("CODEWHALE_HOME", home.path());
148 let _reset = CatalogReset;
149 cold_process();
150 persist_catalog(ApiProvider::Custom, "vision-one", FIRST_ENDPOINT, true);
151 persist_catalog(ApiProvider::Custom, "vision-two", FIRST_ENDPOINT, false);
152 persist_catalog(ApiProvider::Custom, "vision-one", SECOND_ENDPOINT, false);
153 cold_process();
154
155 let first = config_for("vision-one", FIRST_ENDPOINT);
156 let second = config_for("vision-two", FIRST_ENDPOINT);
157 let manager = open_server_manager(&first, home.path())?;
158 assert_eq!(image_capability(&first), CapabilityState::Supported);
159 assert_eq!(image_capability(&second), CapabilityState::Unknown);
160
161 manager.reload_config(second).await?;
162 assert_eq!(
163 image_capability(&manager.read_config()),
164 CapabilityState::Unsupported
165 );
166 manager
167 .reload_config(config_for("vision-one", "http://127.0.0.1:9/uncached/v1"))
168 .await?;
169 assert_eq!(
170 image_capability(&manager.read_config()),
171 CapabilityState::Unknown
172 );
173 manager
174 .reload_config(config_for("vision-one", SECOND_ENDPOINT))
175 .await?;
176 assert_eq!(
177 image_capability(&manager.read_config()),
178 CapabilityState::Unsupported
179 );
180 manager.reload_config(first).await?;
181 assert_eq!(
182 image_capability(&manager.read_config()),
183 CapabilityState::Supported
184 );
185 Ok(())
186 }
187
187 lines RUST