338 lines
17 KiB
Rust
338 lines
17 KiB
Rust
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
|
|
// version: 11
|
|
|
|
//! Desktop template and frontend contract tests for the Store Desk scaffold.
|
|
|
|
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
|
|
return serde_json::from_str::<serde_json::Value>(source);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_002_tauri_package_and_ports_match_store_desk_identity() {
|
|
let parsed = parse_json(include_str!("../tauri.conf.json"));
|
|
assert!(parsed.is_ok(), "committed Tauri JSON must be valid");
|
|
let config = match parsed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(config["productName"], "KSP Store Desk");
|
|
assert_eq!(config["identifier"], "com.sasedev.ksp-app-store-desk");
|
|
assert_eq!(config["build"]["devUrl"], "http://localhost:1438");
|
|
assert!(config.get("version").is_none());
|
|
let vite = include_str!("../vite.config.ts");
|
|
assert!(vite.contains("port: 1438"));
|
|
assert!(vite.contains("strictPort: true"));
|
|
assert!(vite.contains("port: 1439"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_002_fix_001_vite_config_matches_ksp_desk_template_except_identity_ports_and_header() {
|
|
let store = include_str!("../vite.config.ts")
|
|
.replace("crates/ksp-app-store-desk/vite.config.ts", "crates/ksp-app-backfill-desk/vite.config.ts")
|
|
.replace("// version: 2", "// version: 1")
|
|
.replace("ksp-app-store-desk", "ksp-app-backfill-desk")
|
|
.replace("1438", "1436")
|
|
.replace("1439", "1437");
|
|
let reference = include_str!("../../ksp-app-backfill-desk/vite.config.ts");
|
|
assert_eq!(store, reference);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_012_fix_001_tsconfig_matches_common_ksp_desk_template_byte_exact() {
|
|
let store = include_str!("../tsconfig.json");
|
|
let reference = include_str!("../../ksp-app-backfill-desk/tsconfig.json");
|
|
assert_eq!(store, reference);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_002_frontend_dependency_baseline_is_ksp_owned_and_select_free() {
|
|
let parsed = parse_json(include_str!("../package.json"));
|
|
assert!(parsed.is_ok(), "committed package JSON must be valid");
|
|
let package = match parsed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let dependencies = &package["dependencies"];
|
|
for required in [
|
|
"@fltsci/tauri-plugin-tracing",
|
|
"@fortawesome/fontawesome-free",
|
|
"@tauri-apps/api",
|
|
"bootstrap",
|
|
"datatables.net-bs5",
|
|
"resize-observer-polyfill",
|
|
"simplebar",
|
|
] {
|
|
assert!(dependencies.get(required).is_some(), "required frontend dependency missing: {required}");
|
|
}
|
|
assert!(dependencies.get("datatables.net-select-bs5").is_none());
|
|
let dev_dependencies = &package["devDependencies"];
|
|
let expected = ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"];
|
|
let count = match dev_dependencies.as_object() {
|
|
std::option::Option::Some(value) => value.len(),
|
|
std::option::Option::None => 0,
|
|
};
|
|
assert_eq!(count, expected.len());
|
|
for dependency in expected {
|
|
assert!(dev_dependencies.get(dependency).is_some(), "required dev dependency missing: {dependency}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_002_binary_assets_reuse_the_ksp_desktop_template_byte_exact() {
|
|
assert_eq!(include_bytes!("../frontend/imgs/logo.png"), include_bytes!("../../ksp-app-backfill-desk/frontend/imgs/logo.png"));
|
|
assert_eq!(include_bytes!("../frontend/imgs/splash.png"), include_bytes!("../../ksp-app-backfill-desk/frontend/imgs/splash.png"));
|
|
assert_eq!(include_bytes!("../frontend/fonts/DOS_Amazigh.ttf"), include_bytes!("../../ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf"));
|
|
assert_eq!(include_bytes!("../icons/favicon.png"), include_bytes!("../../ksp-app-backfill-desk/icons/favicon.png"));
|
|
assert_eq!(include_bytes!("../icons/favicon.ico"), include_bytes!("../../ksp-app-backfill-desk/icons/favicon.ico"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_shell_exposes_transaction_filters_detail_and_planned_account_view() {
|
|
let html = include_str!("../frontend/main.html");
|
|
for view in ["overview", "transactions", "accounts", "diagnostics"] {
|
|
assert!(html.contains(&format!("data-view=\"{view}\"")));
|
|
assert!(html.contains(&format!("data-view-panel=\"{view}\"")));
|
|
}
|
|
assert!(html.contains("id=\"rawTransactionsTable\""));
|
|
assert!(html.contains("id=\"rawAccountsTable\""));
|
|
for overview_id in ["refreshOverview", "overviewStoreProfile", "overviewStoreTarget", "overviewStoreHealth", "overviewStorePool", "overviewStoreMigration"]
|
|
{
|
|
assert!(html.contains(&format!("id=\"{overview_id}\"")), "missing Store Overview field: {overview_id}");
|
|
}
|
|
for transaction_id in ["refreshTransactions", "transactionSlotMin", "transactionSlotMax", "transactionDirection", "transactionDetailModal"] {
|
|
assert!(html.contains(&format!("id=\"{transaction_id}\"")), "missing pre.007 transaction control: {transaction_id}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_transaction_datatable_is_server_side_and_no_second_pager_exists() {
|
|
let source = include_str!("../frontend/ts/main.ts");
|
|
assert!(source.contains("datatables.net-bs5"));
|
|
assert!(source.contains("paging: true"));
|
|
assert!(source.contains("pageLength: 25"));
|
|
assert!(source.contains("lengthMenu: [25, 50, 100]"));
|
|
assert!(source.contains("store_query_transactions"));
|
|
assert!(source.contains("serverSide: true"));
|
|
assert!(source.contains("Store Desk transaction DataTable initialized"));
|
|
assert!(source.contains("searching: false"));
|
|
assert!(source.contains("ordering: false"));
|
|
let html = include_str!("../frontend/main.html");
|
|
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
|
|
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_composite_and_packaged_resources_cover_store_desk_and_the_complete_config_registry() {
|
|
let composite = parse_json(include_str!("../../../config/composite.ksp-app-store-desk.json"));
|
|
assert!(composite.is_ok(), "Store Desk composite JSON must be valid");
|
|
if let std::result::Result::Ok(composite) = composite {
|
|
assert_eq!(composite["default_profile"], "mainnet");
|
|
let profiles = composite["profiles"].as_array();
|
|
assert!(profiles.is_some());
|
|
if let std::option::Option::Some(profiles) = profiles {
|
|
assert_eq!(profiles.len(), 3);
|
|
for profile in profiles {
|
|
let documents = profile["documents"].as_array();
|
|
assert!(documents.is_some());
|
|
if let std::option::Option::Some(documents) = documents {
|
|
assert_eq!(documents.len(), 2);
|
|
assert_eq!(documents[0]["component_id"], "logging");
|
|
assert_eq!(documents[0]["file_id"], "cfg.std.logging");
|
|
assert_eq!(documents[0]["profile_id"], "supertrace");
|
|
assert_eq!(documents[1]["component_id"], "store");
|
|
assert_eq!(documents[1]["file_id"], "cfg.std.store");
|
|
assert_eq!(documents[1]["profile_id"], profile["profile_id"]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let tauri = parse_json(include_str!("../tauri.conf.json"));
|
|
assert!(tauri.is_ok(), "Store Desk Tauri JSON must be valid");
|
|
if let std::result::Result::Ok(tauri) = tauri {
|
|
let resources = tauri["bundle"]["resources"].as_object();
|
|
assert!(resources.is_some());
|
|
if let std::option::Option::Some(resources) = resources {
|
|
assert_eq!(resources.len(), 16);
|
|
assert!(resources.contains_key("../../config/composite.ksp-app-raw-transaction-ingest-desk.json"));
|
|
assert!(resources.contains_key("../../config/composite.ksp-app-store-desk.json"));
|
|
assert!(resources.contains_key("../../config/std.store.json"));
|
|
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_store_lifecycle_is_composite_managed_health_visible_and_bounded_on_window_close() {
|
|
let bootstrap = include_str!("../src/bootstrap.rs");
|
|
let store = include_str!("../src/store_runtime.rs");
|
|
let tauri = include_str!("../src/tauri.rs");
|
|
assert!(bootstrap.contains("FILE_ID_COMPOSITE_KSP_APP_STORE_DESK"));
|
|
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_STORE"));
|
|
assert!(store.contains("ksp_store_lib::Store::open"));
|
|
assert!(store.contains("store.runtime_snapshot()"));
|
|
assert!(store.contains("store.health().await"));
|
|
assert!(store.contains("store.close().await"));
|
|
assert!(tauri.contains("CloseRequested"));
|
|
assert!(tauri.contains("api.prevent_close()"));
|
|
assert!(tauri.contains("begin_shutdown"));
|
|
assert!(tauri.contains("close_store().await"));
|
|
assert!(tauri.contains("app_handle.exit(0)"));
|
|
assert!(tauri.contains("store_runtime_status"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_transaction_ipc_maps_datatables_offset_limit_counts_and_detail_without_cursor_contract() {
|
|
let dto = include_str!("../src/dto_transaction.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
let tauri = include_str!("../src/tauri.rs");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
assert!(dto.contains("records_total_decimal"));
|
|
assert!(dto.contains("records_filtered_decimal"));
|
|
assert!(runtime.contains("RawInspectionPageRequest::new(offset, limit)"));
|
|
assert!(runtime.contains("RawTransactionInspectionQuery::new"));
|
|
assert!(tauri.contains("store_query_transactions"));
|
|
assert!(tauri.contains("store_get_transaction_detail"));
|
|
assert!(frontend.contains("offset: page.start.toString()"));
|
|
assert!(frontend.contains("limit: page.length"));
|
|
assert!(frontend.contains("decimalCountToSafeNumber"));
|
|
assert!(frontend.contains("recordsTotalDecimal"));
|
|
assert!(frontend.contains("recordsFilteredDecimal"));
|
|
assert!(frontend.contains("data-transaction-detail"));
|
|
assert!(!dto.contains("RawPageCursor"));
|
|
assert!(!frontend.contains("storePager"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_fix_001_long_transaction_identifiers_are_truncated_tooltipped_and_copyable() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let sass = include_str!("../frontend/sass/_app.scss");
|
|
assert!(frontend.contains("truncateLongText"));
|
|
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "transaction-signature")"#));
|
|
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "transaction-content-hash")"#));
|
|
assert!(frontend.contains(r#"setCopyableLongText("transactionDetailSignature""#));
|
|
assert!(frontend.contains(r#"setCopyableLongText("transactionDetailContentHash""#));
|
|
assert!(frontend.contains(r#"title="${escapedValue}""#));
|
|
assert!(frontend.contains("data-copy-long-text"));
|
|
assert!(frontend.contains("navigator.clipboard.writeText"));
|
|
assert!(frontend.contains(r#"document.execCommand("copy")"#));
|
|
assert!(sass.contains(".app-copyable-long-text__value"));
|
|
assert!(sass.contains("text-overflow: ellipsis"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_fix_002_payload_preview_remains_bounded_and_is_explicitly_copyable() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let html = include_str!("../frontend/main.html");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
assert!(runtime.contains("const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;"));
|
|
assert!(html.contains("transactionDetailPayloadPreviewCopy"));
|
|
assert!(html.contains("Copier le preview hex affiché"));
|
|
assert!(html.contains("Preview tronquée à 512 bytes"));
|
|
assert!(frontend.contains(r#"payloadPreviewCopy.dataset.copyField = "transaction-payload-preview""#));
|
|
assert!(frontend.contains("payloadPreviewCopy.dataset.copyLongText = payloadPreview"));
|
|
assert!(frontend.contains("payloadPreviewCopy.hidden = false"));
|
|
assert!(frontend.contains("payloadPreviewCopy.hidden = true"));
|
|
assert!(frontend.contains("const previousTitle = button.title"));
|
|
assert!(frontend.contains("button.title = previousTitle"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_008_account_ipc_maps_datatables_filters_counts_and_bounded_detail_without_cursor_contract() {
|
|
let dto = include_str!("../src/dto_account.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
let tauri = include_str!("../src/tauri.rs");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
assert!(dto.contains("records_total_decimal"));
|
|
assert!(dto.contains("records_filtered_decimal"));
|
|
assert!(runtime.contains("RawAccountStateInspectionQuery::new"));
|
|
assert!(runtime.contains("RawInspectionPageRequest::new(offset, limit)"));
|
|
assert!(runtime.contains("RawAccountStateRead::get_raw_account_state"));
|
|
assert!(runtime.contains("const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;"));
|
|
assert!(tauri.contains("store_query_accounts"));
|
|
assert!(tauri.contains("store_get_account_detail"));
|
|
assert!(frontend.contains("store_query_accounts"));
|
|
assert!(frontend.contains("store_get_account_detail"));
|
|
assert!(frontend.contains(r#"pubkey: pubkey === "" ? null : pubkey"#));
|
|
assert!(frontend.contains("recordsTotalDecimal"));
|
|
assert!(frontend.contains("recordsFilteredDecimal"));
|
|
assert!(frontend.contains("data-account-detail"));
|
|
assert!(!dto.contains("RawPageCursor"));
|
|
assert!(!frontend.contains("storePager"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_008_account_datatable_is_server_side_copyable_and_uses_datatables_as_only_pager() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let html = include_str!("../frontend/main.html");
|
|
assert!(frontend.contains("Store Desk account DataTable initialized"));
|
|
assert!(frontend.contains("Store Desk account DataTables server-side query started"));
|
|
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-pubkey")"#));
|
|
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-state-hash")"#));
|
|
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-owner")"#));
|
|
assert!(frontend.contains(r#"dataPreviewCopy.dataset.copyField = "account-data-preview""#));
|
|
for control in ["refreshAccounts", "accountPubkey", "accountSlotMin", "accountSlotMax", "accountDirection", "accountDetailModal"] {
|
|
assert!(html.contains(&format!("id=\"{control}\"")), "missing pre.008 account control: {control}");
|
|
}
|
|
assert!(html.contains("Account data preview hex"));
|
|
assert!(html.contains("Preview tronquée à 512 bytes"));
|
|
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
|
|
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_010_observations_are_server_side_inside_entity_details_with_datatables_as_only_pager() {
|
|
let html = include_str!("../frontend/main.html");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let tauri = include_str!("../src/tauri.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
for expected in [
|
|
"id=\"transactionObservationsTable\"",
|
|
"id=\"accountObservationsTable\"",
|
|
"id=\"transactionObservationsError\"",
|
|
"id=\"accountObservationsError\"",
|
|
] {
|
|
assert!(html.contains(expected), "missing pre.010 observation UI contract: {expected}");
|
|
}
|
|
assert!(frontend.contains("store_query_transaction_observations"));
|
|
assert!(frontend.contains("store_query_account_observations"));
|
|
assert!(frontend.contains("initializeTransactionObservationTable"));
|
|
assert!(frontend.contains("initializeAccountObservationTable"));
|
|
assert!(frontend.contains("currentTransactionObservationSignature"));
|
|
assert!(frontend.contains("currentAccountObservationIdentity"));
|
|
assert!(frontend.contains("stale transaction observation response ignored"));
|
|
assert!(frontend.contains("stale account observation response ignored"));
|
|
assert!(tauri.contains("store_query_transaction_observations"));
|
|
assert!(tauri.contains("store_query_account_observations"));
|
|
assert!(runtime.contains("RawSortDirection::Descending"));
|
|
assert!(!html.contains("store-observation-pager"));
|
|
assert!(!frontend.contains("cursor:"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_010_observation_rows_project_safe_provenance_and_reuse_long_text_copy_without_source_bytes() {
|
|
let dto = include_str!("../src/dto_observation.rs");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
for expected in [
|
|
"source_payload_hash",
|
|
"source_payload_size_decimal",
|
|
"received_at_unix_millis_decimal",
|
|
"observed_at_unix_millis_decimal",
|
|
"capture_session_id",
|
|
"endpoint_id",
|
|
"filter_id",
|
|
"transaction_signature",
|
|
"write_version_decimal",
|
|
] {
|
|
assert!(dto.contains(expected), "missing safe observation metadata field: {expected}");
|
|
}
|
|
for forbidden in ["source_payload_bytes", "payload_preview", "data_preview", "raw_bytes"] {
|
|
assert!(!dto.contains(forbidden), "observation DTO leaked forbidden bytes field: {forbidden}");
|
|
}
|
|
assert!(frontend.contains("transaction-observation-key"));
|
|
assert!(frontend.contains("account-observation-key"));
|
|
assert!(frontend.contains("account-observation-transaction-signature"));
|
|
assert!(frontend.contains("renderCopyableLongText"));
|
|
}
|