Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,202 @@
use nomifun_common::*;
// --- ID generation ---
#[test]
fn test_generate_id_returns_uuid() {
let id = generate_id();
assert_eq!(id.len(), 36); // UUID string length
assert!(id.contains('-'));
}
#[test]
fn test_generate_prefixed_id_has_prefix() {
let id = generate_prefixed_id("cron");
assert!(id.starts_with("cron_"));
}
#[test]
fn test_generate_prefixed_id_is_prefix_plus_short_id() {
// Entity ID convention: `{prefix}_{shortId}` — a 16-char base32 body
// (9-char millisecond timestamp + 7-char random), time-ordered and far
// shorter than the former UUIDv7 tail.
const SHORT_ID_ALPHABET: &str = "0123456789abcdefghjkmnpqrstvwxyz";
let id = generate_prefixed_id("conv");
let body = id.strip_prefix("conv_").expect("prefix with underscore separator");
assert_eq!(body.len(), 16);
assert!(
body.chars().all(|c| SHORT_ID_ALPHABET.contains(c)),
"short-id body {body} must use only the base32 alphabet"
);
}
#[test]
fn test_prefixed_id_is_time_ordered() {
// The timestamp is the high-order prefix of the body, so a later mint
// sorts lexicographically after an earlier one.
let earlier = generate_prefixed_id("conv");
std::thread::sleep(std::time::Duration::from_millis(2));
let later = generate_prefixed_id("conv");
assert!(later > earlier, "{later} should sort after {earlier}");
}
#[test]
fn test_id_uniqueness_across_calls() {
let ids: std::collections::HashSet<String> = (0..1000).map(|_| generate_id()).collect();
assert_eq!(ids.len(), 1000);
}
#[test]
fn test_id_time_ordering() {
let id1 = generate_id();
let id2 = generate_id();
assert!(id2 >= id1, "UUID v7 should be time-ordered");
}
// --- Timestamp ---
#[test]
fn test_now_ms_returns_positive() {
assert!(now_ms() > 0);
}
#[test]
fn test_now_ms_monotonic() {
let t1 = now_ms();
let t2 = now_ms();
assert!(t2 >= t1);
}
// --- Crypto ---
#[test]
fn test_encrypt_decrypt_roundtrip() {
let key = [0xAB_u8; 32];
let encrypted = encrypt_string("hello world", &key).unwrap();
let decrypted = decrypt_string(&encrypted, &key).unwrap();
assert_eq!(decrypted, "hello world");
}
#[test]
fn test_encrypt_decrypt_empty_string() {
let key = [0xCD_u8; 32];
let encrypted = encrypt_string("", &key).unwrap();
let decrypted = decrypt_string(&encrypted, &key).unwrap();
assert_eq!(decrypted, "");
}
#[test]
fn test_encrypt_decrypt_unicode() {
let key = [0xEF_u8; 32];
let encrypted = encrypt_string("你好世界🌍", &key).unwrap();
let decrypted = decrypt_string(&encrypted, &key).unwrap();
assert_eq!(decrypted, "你好世界🌍");
}
#[test]
fn test_decrypt_wrong_key_fails() {
let key = [0x11_u8; 32];
let encrypted = encrypt_string("secret", &key).unwrap();
let wrong_key = [0x22_u8; 32];
assert!(decrypt_string(&encrypted, &wrong_key).is_err());
}
#[test]
fn test_encrypt_same_plaintext_different_ciphertext() {
let key = [0x33_u8; 32];
let e1 = encrypt_string("test", &key).unwrap();
let e2 = encrypt_string("test", &key).unwrap();
assert_ne!(e1, e2, "random nonce should produce different ciphertexts");
}
#[test]
fn test_encrypt_large_text() {
let key = [0x44_u8; 32];
let large = "x".repeat(1_000_000);
let encrypted = encrypt_string(&large, &key).unwrap();
let decrypted = decrypt_string(&encrypted, &key).unwrap();
assert_eq!(decrypted, large);
}
// --- AppError ---
#[test]
fn test_app_error_status_codes() {
use axum::http::StatusCode;
assert_eq!(AppError::NotFound("x".into()).status_code(), StatusCode::NOT_FOUND);
assert_eq!(AppError::BadRequest("x".into()).status_code(), StatusCode::BAD_REQUEST);
assert_eq!(
AppError::Unauthorized("x".into()).status_code(),
StatusCode::UNAUTHORIZED
);
assert_eq!(AppError::Forbidden("x".into()).status_code(), StatusCode::FORBIDDEN);
assert_eq!(AppError::Conflict("x".into()).status_code(), StatusCode::CONFLICT);
assert_eq!(AppError::RateLimited.status_code(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(
AppError::Internal("x".into()).status_code(),
StatusCode::INTERNAL_SERVER_ERROR
);
assert_eq!(AppError::BadGateway("x".into()).status_code(), StatusCode::BAD_GATEWAY);
assert_eq!(AppError::Timeout("x".into()).status_code(), StatusCode::BAD_GATEWAY);
}
#[test]
fn test_app_error_json_format() {
use axum::response::IntoResponse;
let resp = AppError::NotFound("user 123".into()).into_response();
assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);
}
// --- PaginatedResult ---
#[test]
fn test_paginated_result_serialize() {
let result = PaginatedResult {
items: vec!["a", "b"],
total: 100,
has_more: true,
};
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["has_more"], true);
assert_eq!(json["total"], 100);
}
#[test]
fn test_paginated_result_empty() {
let result: PaginatedResult<i32> = PaginatedResult {
items: vec![],
total: 0,
has_more: false,
};
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["items"], serde_json::json!([]));
}
// --- Enums ---
#[test]
fn test_enum_serde_roundtrip() {
let roundtrip_cases: Vec<(&str, AgentType)> = vec![
(r#""acp""#, AgentType::Acp),
(r#""nanobot""#, AgentType::Nanobot),
(r#""openclaw-gateway""#, AgentType::OpenclawGateway),
];
for (json_str, expected) in roundtrip_cases {
let parsed: AgentType = serde_json::from_str(json_str).unwrap();
assert_eq!(parsed, expected);
let serialized = serde_json::to_string(&expected).unwrap();
assert_eq!(serialized, json_str);
}
}
// --- Constants ---
#[test]
fn test_constants_values() {
assert_eq!(constants::DEFAULT_PORT, 25808);
assert_eq!(constants::HEARTBEAT_INTERVAL_MS, 30_000);
assert_eq!(constants::BODY_LIMIT, 10 * 1024 * 1024);
assert_eq!(constants::COOKIE_NAME, "nomifun-session");
assert!(constants::SUPPORTED_IMAGE_EXTENSIONS.contains(&".png"));
}