makepad/libs/flow/tests/host_assets.rs
Arena Agent d8d5ea8b6a Repair integration manifests and port nigig exports onto upstream widget families
Restore missing required asset/flow packages from the pinned nigig revision; retain upstream script isolation, X11 hover, and widget-core fixes. Validated makepad-widgets data feature with Rust 1.97.1.
2026-10-26 11:23:26 +00:00

203 lines
8.7 KiB
Rust

#![cfg(not(target_arch = "wasm32"))]
use makepad_asset_client::{ApiEndpoints, PublishFile, PublishRequest, PublishThumbnail};
use makepad_asset_data::{AssetAlias, AssetKind, FileRole, MediaType, ThumbnailMedia};
use makepad_flow::client::{ClientError, Endpoints, FlowClient};
use makepad_flow::host::{FlowServer, FlowServerConfig};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
struct TempRoot(PathBuf);
impl TempRoot {
fn new(label: &str) -> Self {
let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let path = std::env::temp_dir().join(format!("flow-assets-route-{}-{label}-{nonce}", std::process::id()));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
}
impl Drop for TempRoot { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } }
fn png() -> Vec<u8> {
makepad_ai_hub::testpattern::encode_png_rgba(&vec![180; 256 * 256 * 4], 256, 256).unwrap()
}
#[test]
fn proxy_lists_and_serves_flow_asset_thumbnail() {
let store_root = TempRoot::new("store");
let flow_root = TempRoot::new("flow");
let mut store_config = makepad_asset_store::ServerConfig::new(store_root.0.clone());
store_config.control_addr = "127.0.0.1:0".parse().unwrap();
store_config.data_addr = "127.0.0.1:0".parse().unwrap();
store_config.bootstrap_admin = true;
store_config.discovery = None;
store_config.log = false;
let mut store = makepad_asset_store::AssetServer::start(store_config).unwrap();
let token = std::fs::read_to_string(store_root.0.join("admin-token")).unwrap().trim().to_string();
let endpoints = ApiEndpoints { control: store.control_addr(), data: store.data_addr() };
let mut client_config = makepad_asset_client::ClientConfig::new(store_root.0.join("seed-cache"));
client_config.token = Some(token.clone());
let mut asset_client = makepad_asset_client::AssetClient::connect(client_config, endpoints, Some(store.server_id())).unwrap();
let image = png();
let mut request = PublishRequest::new(
"flows",
AssetKind::Texture,
"Route picture",
PublishFile { bytes: image.clone(), media: MediaType::Png, role: FileRole::Texture, media_millis: 0, dims: Some((256, 256)) },
PublishThumbnail::plain(image.clone(), ThumbnailMedia::Png, 256, 256),
);
request.alias = Some(AssetAlias::new("flows/route-picture").unwrap());
request.tags = vec!["flow".into(), "route".into()];
asset_client.publish_artifact(&request).unwrap();
let large_image = makepad_ai_hub::testpattern::encode_png_rgba(
&vec![180; 1024 * 512 * 4],
1024,
512,
)
.unwrap();
let mut oversized = PublishRequest::new(
"flows",
AssetKind::Texture,
"Oversized preview",
PublishFile {
bytes: large_image.clone(),
media: MediaType::Png,
role: FileRole::Texture,
media_millis: 0,
dims: Some((1024, 512)),
},
PublishThumbnail::plain(large_image, ThumbnailMedia::Png, 1024, 512),
);
oversized.alias = Some(AssetAlias::new("flows/oversized-preview").unwrap());
let oversized_id = asset_client.publish_artifact(&oversized).unwrap().asset_id;
let mut tagged = PublishRequest::new(
"elsewhere",
AssetKind::Texture,
"Tagged picture",
PublishFile {
bytes: image.clone(),
media: MediaType::Png,
role: FileRole::Texture,
media_millis: 0,
dims: Some((256, 256)),
},
PublishThumbnail::plain(image.clone(), ThumbnailMedia::Png, 256, 256),
);
tagged.alias = Some(AssetAlias::new("elsewhere/tagged-picture").unwrap());
tagged.tags = vec!["flow".into()];
asset_client.publish_artifact(&tagged).unwrap();
let mut unaliased = PublishRequest::new(
"flows",
AssetKind::Data,
"Unaliased data",
PublishFile {
bytes: b"original asset bytes".to_vec(),
media: MediaType::Bin,
role: FileRole::Source,
media_millis: 0,
dims: None,
},
PublishThumbnail::plain(image.clone(), ThumbnailMedia::Png, 256, 256),
);
unaliased.alias = None;
let unaliased_id = asset_client.publish_artifact(&unaliased).unwrap().asset_id;
drop(asset_client);
let mut flow_config = FlowServerConfig::new(flow_root.0.clone());
flow_config.log = Box::new(|_| {});
flow_config.asset.endpoints = Some(endpoints);
flow_config.asset.server_id = Some(store.server_id());
flow_config.asset.token = Some(token);
let flow_server = FlowServer::start(flow_config).unwrap();
let served = flow_server.endpoints();
let flow_client = FlowClient::connect(
Endpoints { control: served.control, data: served.data },
served.token.clone(),
Some(served.server_id),
).unwrap();
let rows = flow_client.assets("Route", Some("flows"), 10).unwrap().assets;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].alias.as_deref(), Some("flows/route-picture"));
assert_eq!(rows[0].tags, vec!["flow", "route"]);
let default_rows = flow_client.assets("", Some("flows"), 10).unwrap().assets;
assert_eq!(default_rows.len(), 4);
assert!(default_rows.iter().any(|asset| asset.namespace == "elsewhere"));
let mut page = flow_client.assets_page("", None, 1, None).unwrap();
assert_eq!(page.assets.len(), 1);
let first_cursor = page.cursor.clone().expect("more than one catalog result");
let mut ids = std::collections::HashSet::new();
let mut pages = 0;
loop {
pages += 1;
assert!(pages <= 5, "pagination failed to advance");
for row in &page.assets { assert!(ids.insert(row.id.clone()), "duplicate result on a later page"); }
let Some(cursor) = page.cursor else { break };
page = flow_client.assets_page("", None, 1, Some(&cursor)).unwrap();
}
assert_eq!(ids.len(), 4);
assert!(flow_client.assets_page("different", None, 1, Some(&first_cursor)).is_err());
let thumb = flow_client.asset_thumbnail("flows/route-picture").unwrap();
assert_eq!(thumb.content_type, "image/png");
assert_eq!(thumb.bytes.as_ref(), image.as_slice());
let content = flow_client.asset_content(&unaliased_id.to_string()).unwrap();
assert_eq!(content.content_type, "application/octet-stream");
assert_eq!(content.bytes.as_ref(), b"original asset bytes");
let preview = flow_client.asset_preview(&unaliased_id.to_string()).unwrap();
assert_eq!(preview.content_type, "text/plain; charset=utf-8");
assert_eq!(preview.bytes.as_ref(), "Binary data · 20 bytes".as_bytes());
let oversized_preview = flow_client.asset_preview(&oversized_id.to_string()).unwrap();
assert_eq!(oversized_preview.content_type, "image/png");
assert!(oversized_preview.bytes.starts_with(b"\x89PNG\r\n\x1a\n"));
assert_eq!(
u32::from_be_bytes(oversized_preview.bytes[16..20].try_into().unwrap()),
256
);
assert_eq!(
u32::from_be_bytes(oversized_preview.bytes[20..24].try_into().unwrap()),
128
);
let thumb_by_id = flow_client.asset_thumbnail(&unaliased_id.to_string()).unwrap();
assert_eq!(thumb_by_id.content_type, "image/png");
assert_eq!(thumb_by_id.bytes.as_ref(), image.as_slice());
let mut wrong_token = served.token.clone();
let last = wrong_token.pop().unwrap();
wrong_token.push(if last == '0' { '1' } else { '0' });
let unauthorized = FlowClient::connect(
Endpoints { control: served.control, data: served.data },
wrong_token,
Some(served.server_id),
);
assert!(matches!(unauthorized, Err(ClientError::Unauthorized)));
assert!(matches!(
flow_client.asset_content("not/an-asset-id"),
Err(ClientError::Protocol(_))
));
assert!(matches!(
flow_client.asset_thumbnail("../bad"),
Err(ClientError::Protocol(_))
));
flow_server.shutdown();
store.shutdown();
}
#[test]
fn proxy_reports_no_discovered_store_and_retries_per_request() {
let root = TempRoot::new("missing");
let mut config = FlowServerConfig::new(root.0.clone());
config.log = Box::new(|_| {});
config.asset.discovery_wait_ms = 0;
let server = FlowServer::start(config).unwrap();
let served = server.endpoints();
let client = FlowClient::connect(
Endpoints { control: served.control, data: served.data },
served.token,
Some(served.server_id),
).unwrap();
for _ in 0..2 {
let error = client.assets("", Some("flows"), 10).unwrap_err();
assert!(matches!(error, ClientError::Http { status: 503, .. }));
assert!(error.to_string().contains("no asset server discovered on this LAN"));
}
server.shutdown();
}